-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_clean.py
More file actions
365 lines (317 loc) · 23.6 KB
/
Copy pathtest_clean.py
File metadata and controls
365 lines (317 loc) · 23.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
"""Тест чистки текста: паразиты уходят, слова остаются.
python test_clean.py
Микрофон и модель не нужны - это чистые функции над строками.
Половина случаев здесь - ЛОВУШКИ, и они важнее остальных. «Вот дом», «короче
путь», «в общем виде» обязаны пройти нетронутыми: цена ошибки несимметрична -
оставить паразита полбеды, съесть слово значит потерять текст, а этого мы не
делаем никогда (README, «Что нельзя ломать»).
Материал не выдуман: строки из «речи владельца» - настоящие диктовки, на
которых фильтр и попался.
"""
import sys
import time
import punct_rules
from cleaner import (_apply_dictionary, _apply_snippets, _strip_fillers, _strip_md,
extract_commands)
# (что сказано, что должно получиться, зачем этот случай)
FILLERS = [
# --- настоящая речь, на которой старый фильтр не делал ничего ---
("И вот, короче, я, угу. Ну вот, типа, убирайте вот эту штуку.",
"И я. Убирайте вот эту штуку.",
"речь владельца: паразиты вокруг запятых"),
("Ну вот, короче, я думаю, типа, надо сделать.",
"Я думаю, надо сделать.",
"четыре паразита подряд - нужна сходимость"),
("Да, сделай четырнадцать, наверное, и сделай всё. В общем, сделай всё. "
"Вот так они должны думать, понимаешь?",
"Да, сделай четырнадцать, наверное, и сделай всё. Сделай всё. "
"Вот так они должны думать?",
"заглавная после вырезанного начала предложения"),
("Эм, я хотел сказать, ну, что отчёт готов.",
"Я хотел сказать, что отчёт готов.",
"междометие плюс паразит"),
("Эм, сессия начинается в пятницу в 10:00 утра. ммм, вот.",
"Сессия начинается в пятницу в 10:00 утра.",
"паразит в конце: осиротевшая точка не должна удвоить настоящую"),
# --- ЛОВУШКИ: тронуть = потерять текст ---
("Вот дом, в котором я живу.", "Вот дом, в котором я живу.",
"ЛОВУШКА: «вот» без запятой - обычное слово"),
("Короче путь через двор.", "Короче путь через двор.",
"ЛОВУШКА: «короче» - сравнительная степень"),
("В общем виде это работает.", "В общем виде это работает.",
"ЛОВУШКА: «в общем» - часть оборота"),
# Слабая ловушка, и это выяснила мутационная проверка: она проходит не
# потому, что «просто» исключено из _PARASITES, а потому что запятой нет.
# Внеси «просто» в список - тест останется зелёным. Решение не проверено
# тестом, и подделывать проверку не буду: «просто» между запятыми
# двусмысленно по-настоящему («сделай, просто, красиво»), и рассудить это
# может только замер на живой речи, а не пример, выдуманный мной.
("Просто нажми кнопку.", "Просто нажми кнопку.",
"«просто» без запятой - обычное слово"),
("Ты меня понимаешь?", "Ты меня понимаешь?",
"ЛОВУШКА: «понимаешь» - сказуемое, а не паразит"),
("Это самое дорогое, что у меня есть.", "Это самое дорогое, что у меня есть.",
"ЛОВУШКА: «это самое» - подлежащее"),
("Значит так: работаем.", "Значит так: работаем.",
"ЛОВУШКА: «значит» без запятой"),
("Позвони, пожалуйста, Ивану.", "Позвони, пожалуйста, Ивану.",
"ЛОВУШКА: «пожалуйста» - вежливость, не сор"),
("Выручка выросла на 15% за квартал.", "Выручка выросла на 15% за квартал.",
"ЛОВУШКА: чистый текст не трогаем вовсе"),
("Встреча в пятницу, наверное, в десять.", "Встреча в пятницу, наверное, в десять.",
"ЛОВУШКА: «наверное» - неуверенность говорящего, это смысл"),
# --- предохранитель ---
("Э-э, ммм, вот.", "Э-э, ммм, вот.",
"ПРЕДОХРАНИТЕЛЬ: фраза из одних паразитов - отдаём как есть, не пустоту"),
]
DICT_WORDS = ["пулреквест", "Соколов", "стратсессия"]
DICTIONARY = [
("пришли полреквест до обеда", "пришли пулреквест до обеда",
"осечка на длинном слове"),
("сокалов звонил", "Соколов звонил", "фамилия с осечкой"),
("Соколовым подписано", "Соколовым подписано",
"ЛОВУШКА: словоформа - именительный сломал бы фразу"),
("кости лежат", "кости лежат",
"ЛОВУШКА: короткое слово - гадать не имеем права"),
]
# --------------------------------------------------------------------------
# Запятые по правилам (punct_rules).
#
# Половина случаев - ловушки, где запятая НЕ нужна, и это не для симметрии.
# Лишняя запятая здесь не косметическая беда: фильтр бубнежа опознаёт паразита
# ПО ЗАПЯТЫМ вокруг него, и наша лишняя запятая заставляет его удалить обычное
# слово. Один такой случай («короче путь через двор» -> «Путь через двор»)
# поймался уже на живом конвейере.
PUNCT = [
# --- запятая нужна ---
("я думаю что надо сделать", "Я думаю, что надо сделать.",
"перед «что» - подчинительный союз"),
("позвони если будешь рядом", "Позвони, если будешь рядом.",
"перед «если»"),
("я хочу чтобы всё получилось", "Я хочу, чтобы всё получилось.",
"перед «чтобы»"),
("вот дом в котором я живу", "Вот дом, в котором я живу.",
"перед предлогом с «который», а не перед самим «котором»"),
("книга которую я читал", "Книга, которую я читал.",
"«который» в любом падеже"),
("не пришёл потому что заболел", "Не пришёл, потому что заболел.",
"«потому что» - один союз, запятая перед ним целиком"),
("я пошёл а он остался", "Я пошёл, а он остался.",
"перед противительным «а» в середине"),
("хотел позвонить но забыл", "Хотел позвонить, но забыл.",
"перед «но»"),
("кстати я забыл сказать", "Кстати, я забыл сказать.",
"после вводного в начале"),
("сделаю когда вернусь", "Сделаю, когда вернусь.",
"перед «когда»"),
# --- ЛОВУШКИ: запятая НЕ нужна ---
("короче путь через двор", "Короче путь через двор.",
"ЛОВУШКА: «короче» тут прилагательное, а не паразит"),
("в общем виде это работает", "В общем виде это работает.",
"ЛОВУШКА: «в общем» тут не вводное"),
("правда жизни такова", "Правда жизни такова.",
"ЛОВУШКА: «правда» тут существительное"),
("а вот дом", "А вот дом.",
"ЛОВУШКА: «а» в начале фразы запятой не требует"),
("вот так и сделаем", "Вот так и сделаем.",
"ЛОВУШКА: одно «вот» в начале - обычная фраза, не трогаем"),
("дай мне что-нибудь почитать", "Дай мне что-нибудь почитать.",
"ЛОВУШКА: «что-нибудь» не союз"),
("хлеб и молоко и сыр", "Хлеб и молоко и сыр.",
"ЛОВУШКА: перед «и» не ставим никогда - слишком много ложных"),
("это или то", "Это или то.",
"ЛОВУШКА: перед «или» тоже не ставим"),
("встреча в пятницу в 10:00 утра", "Встреча в пятницу в 10:00 утра.",
"ЛОВУШКА: двоеточие во времени - не признак расставленной пунктуации"),
("Уже с большой буквы, и с запятой.", "Уже с большой буквы, и с запятой.",
"ЛОВУШКА: знаки уже стоят - не трогаем ничего"),
]
# --------------------------------------------------------------------------
# Переменные в подстановках и дата-время голосом (PLAN, задача 13).
#
# Время фиксированное: 19.07.2026, воскресенье, 21:40. Иначе тест пришлось бы
# сверять с той же функцией, которую он проверяет, - то есть не проверять
# ничего. Здесь ожидаемое написано буквами, как его увидит человек.
NOW = time.struct_time((2026, 7, 19, 21, 40, 0, 6, 200, 0))
SNIPPET_RULES = {
"моя подпись": "С уважением, Роман, {дата}",
"шапка": "{дата-словами}, {время}, {день-недели}",
"кривая": "смета на {чушь}",
"адрес": "Тверская, 1",
}
SNIPPETS = [
("моя подпись", "С уважением, Роман, 19.07.2026",
"переменная в подстановке становится живой датой"),
("шапка", "19 июля, 21:40, воскресенье",
"три переменные в одной подстановке - и все от одного момента времени"),
("Отправь на адрес сегодня.", "Отправь на Тверская, 1 сегодня.",
"подстановка без переменных работает как работала"),
# --- ЛОВУШКИ ---
("Ставим {дата} в шаблон письма.", "Ставим {дата} в шаблон письма.",
"ЛОВУШКА: {дата} в обычной диктовке - чужой текст, не наша переменная"),
("кривая", "смета на {чушь}",
"ЛОВУШКА: неизвестное имя остаётся как есть, а не пропадает"),
("Подпись поставлю сам.", "Подпись поставлю сам.",
"ЛОВУШКА: ключ «моя подпись» не срабатывает от одного слова"),
]
VOICE_VARS = [
("Встреча сегодняшняя дата в десять.", "Встреча 19.07.2026 в десять.",
"«сегодняшняя дата» голосом - без всякой настройки"),
("Записал в текущее время.", "Записал в 21:40.",
"«текущее время» голосом"),
# --- ЛОВУШКИ ---
("Займёмся сегодняшней датой позже.", "Займёмся сегодняшней датой позже.",
"ЛОВУШКА: другой падеж - это речь о дате, а не просьба её вставить"),
("Дата сегодня хорошая.", "Дата сегодня хорошая.",
"ЛОВУШКА: слова врозь - не команда"),
("Текущее состояние дел такое.", "Текущее состояние дел такое.",
"ЛОВУШКА: «текущее» без «времени» не трогаем"),
]
# Перенос строки и абзац голосом. Ключевое и неочевидное: команду ищем уже
# ПОСЛЕ расстановки знаков, а пунктуатор отделяет одиночный предлог «с»
# запятой - «с, новой строки». Без терпимости к этой запятой команда писалась
# словами в текст (владелец надиктовал перенос, получил «с, новой строки»).
NEWLINE_CMD = [
("Тест функции с новой строки привет", "Тест функции\nПривет",
"перенос строки - базовый случай, без знаков"),
("Тест функции с, новой строки привет", "Тест функции\nПривет",
"перенос строки с запятой пунктуатора после предлога «с»"),
("Привет, с, новой строки как дела", "Привет\nКак дела",
"запятая и перед предлогом, и внутри - обе съедаются"),
("Абзац с нового абзаца дальше", "Абзац\n\nДальше",
"новый абзац - базовый случай"),
("Абзац с, нового абзаца дальше", "Абзац\n\nДальше",
"новый абзац с запятой пунктуатора после «с»"),
# --- ЛОВУШКИ ---
("Новая строка кода идёт с отступа", "Новая строка кода идёт с отступа",
"ЛОВУШКА: «новая строка» без предлога - обычная речь, не команда"),
("Перешёл на новую строку сам", "Перешёл на новую строку сам",
"ЛОВУШКА: «на новую строку» - речь о действии, не команда"),
]
# Разметка от LLM. Владелец увидел в надиктованном тексте пары звёздочек
# «рандомно, без закономерности» - это Ollama выделяла жирным то, что сочла
# важным. Первый случай ниже - дословно из его истории.
#
# Первое значение пары - исходник ДО модели, второе - её ответ. Снимаем только
# то, чего в исходнике не было: знак, который человек продиктовал сам, наш.
MD = [
("Править выделенное текстом это непонятно, что это как используется",
"Править выделенное текстом **это** непонятно, что это **как** используется",
"Править выделенное текстом это непонятно, что это как используется",
"жирное вокруг слов снимается - случай владельца"),
("важно", "***важно***", "важно",
"тройные снимаются за несколько проходов, а не оставляют по звёздочке"),
("это важно", "это __важно__", "это важно", "двойное подчёркивание - тоже разметка"),
("слово", "*слово*", "слово", "одиночная звёздочка - курсив, снимаем"),
# --- ЛОВУШКИ ---
("посчитай 5 * 3 = 15", "посчитай 5 * 3 = 15", "посчитай 5 * 3 = 15",
"ЛОВУШКА: умножение не разметка - вокруг звёздочки пробелы"),
("пути foo_bar и send_mail", "пути foo_bar и send_mail", "пути foo_bar и send_mail",
"ЛОВУШКА: одиночные подчёркивания в именах не трогаем"),
("формула **a** тут", "формула **a** и **b**", "формула **a** и **b**",
"ЛОВУШКА: звёздочки были в исходнике - значит они человека, не лезем"),
("обычный текст", "обычный текст", "обычный текст",
"ЛОВУШКА: нет разметки - нет и правки"),
]
def run_md() -> int:
"""Снятие разметки: сверяем на паре (исходник, ответ модели)."""
bad = 0
for before, after, want, why in MD:
got = _strip_md(before, after)
if got != want:
bad += 1
print(f" ПРОВАЛ [{why}]")
print(f" было: {after!r}")
print(f" ждали: {want!r}")
print(f" вышло: {got!r}")
print(f"разметка от LLM: {len(MD) - bad}/{len(MD)}")
return bad
def run_commands_off() -> int:
"""Выключенные голосовые команды выключают и дату со временем.
Отдельной проверкой, потому что здесь проверяется решение по конфигу, а не
текст. Человек, снявший галочку, просил не трогать его слова - значит и
«сегодняшняя дата» обязана остаться словами.
"""
text = "Встреча сегодняшняя дата в десять."
got, _ = extract_commands(text, {"voice_commands": False}, now=NOW)
if got != text:
print(" ПРОВАЛ [выключенные команды не должны подставлять дату]")
print(f" вышло: {got!r}")
return 1
print("голосовые команды выключены: 1/1")
return 0
def run_gate() -> int:
"""Гейт по модели: правила работают только там, где модель знаков не ставит.
Отдельной функцией, а не строкой в таблице: проверяем не текст, а решение
конвейера, и для этого нужен конфиг.
Зачем это вообще. У basic_punctuation гейт был по заглавным буквам, и при
замене на punct_rules он едва не потерялся. Без него правила лезли бы
поверх e2e-модели: она расставляет знаки сама и местами осознанно НЕ ставит
запятую там, где правило её поставит. Вышли бы дубли и спор с моделью.
Случай не выдуманный: у всех, кто обновился со старой версии, в конфиге
лежит именно gigaam-v3-e2e-rnnt.
"""
import cleaner
import config as C
сырое = "ну вот короче я думаю что надо сделать"
bad = 0
def проверь(model, ждём_знаки, why):
nonlocal bad
cfg = dict(C.DEFAULTS)
cfg["llm"] = {"enabled": False}
cfg["model"] = model
got = cleaner.clean(сырое, cfg, lambda _m: None)
тронули = got != сырое
if тронули != ждём_знаки:
bad += 1
print(f" ПРОВАЛ [{why}]")
print(f" модель: {model}")
print(f" вышло: {got!r}")
проверь("gigaam-v2-ctc", True, "модель без знаков - правила работают")
проверь("gigaam-v2-rnnt", True, "RNN-T тоже без знаков")
проверь("gigaam-v3-e2e-rnnt", False,
"e2e ставит знаки сама - правила молчат")
проверь("gigaam-v3-e2e-ctc", False, "второй e2e-вариант тоже")
проверь("alphacep/vosk-model-ru", True, "модель не из каталога, без e2e")
# Приёмочный критерий из задания, дословно.
cfg = dict(C.DEFAULTS)
cfg["llm"] = {"enabled": False}
cfg["model"] = "gigaam-v2-ctc"
got = cleaner.clean(сырое, cfg, lambda _m: None)
if got != "Я думаю, что надо сделать.":
bad += 1
print(" ПРОВАЛ [приёмочный критерий]")
print(f" вышло: {got!r}")
print(f"гейт по модели: {6 - bad}/6")
return bad
def run(name, cases, fn) -> int:
bad = 0
for text, want, why in cases:
got = fn(text)
if got != want:
bad += 1
print(f" ПРОВАЛ [{why}]")
print(f" было: {text!r}")
print(f" ждали: {want!r}")
print(f" вышло: {got!r}")
print(f"{name}: {len(cases) - bad}/{len(cases)}")
return bad
def main() -> None:
bad = run("паразиты", FILLERS, _strip_fillers)
bad += run("запятые по правилам", PUNCT, punct_rules.apply)
bad += run_gate()
bad += run("словарь", DICTIONARY, lambda t: _apply_dictionary(t, DICT_WORDS))
bad += run("переменные в подстановках", SNIPPETS,
lambda t: _apply_snippets(t, SNIPPET_RULES, now=NOW))
bad += run("дата и время голосом", VOICE_VARS,
lambda t: extract_commands(t, {"voice_commands": True},
now=NOW)[0])
bad += run("перенос строки голосом", NEWLINE_CMD,
lambda t: extract_commands(t, {"voice_commands": True},
now=NOW)[0])
bad += run_md()
bad += run_commands_off()
print("\nОК" if not bad else f"\nПРОВАЛЕНО: {bad}")
sys.exit(1 if bad else 0)
if __name__ == "__main__":
main()