VladGeekPro commited on
Commit
3d44974
·
1 Parent(s): e446b1b

ChangedWhisperToLargeV3TurboAndAddedTestEndPoint

Browse files
Files changed (3) hide show
  1. Dockerfile +1 -1
  2. app.py +267 -16
  3. requirements.txt +2 -1
Dockerfile CHANGED
@@ -2,7 +2,7 @@ FROM python:3.11-slim
2
 
3
  ENV PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 HOME=/home/user \
4
  PATH=/home/user/.local/bin:$PATH PORT=7860 \
5
- WHISPER_MODEL=large-v3 WHISPER_COMPUTE_TYPE=int8
6
 
7
  RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
8
  && rm -rf /var/lib/apt/lists/* \
 
2
 
3
  ENV PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1 HOME=/home/user \
4
  PATH=/home/user/.local/bin:$PATH PORT=7860 \
5
+ WHISPER_MODEL=openai/whisper-large-v3-turbo
6
 
7
  RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
8
  && rm -rf /var/lib/apt/lists/* \
app.py CHANGED
@@ -33,11 +33,173 @@ HF_TOKEN = os.getenv("HF_TOKEN")
33
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
34
  _MODEL: Optional[SentenceTransformer] = None
35
  _WHISPER_MODEL: Optional[Any] = None
 
36
 
37
 
38
  app = Flask(__name__)
39
  app.config["MAX_CONTENT_LENGTH"] = 20 * 1024 * 1024
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  def get_embedding_model() -> SentenceTransformer:
43
  """Возвращает модель эмбеддингов (ленивая загрузка)."""
@@ -49,19 +211,50 @@ def get_embedding_model() -> SentenceTransformer:
49
  return _MODEL
50
 
51
 
52
- def get_whisper_model() -> Any:
53
- """Возвращает модель Whisper (ленивая загрузка)."""
54
- global _WHISPER_MODEL
55
 
56
  if _WHISPER_MODEL is None:
57
- from faster_whisper import WhisperModel
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- model_name = os.getenv("WHISPER_MODEL", "large-v3")
60
- compute_type = os.getenv("WHISPER_COMPUTE_TYPE", "float16" if torch.cuda.is_available() else "int8")
61
- _WHISPER_MODEL = WhisperModel(model_name, device=DEVICE, compute_type=compute_type)
 
 
 
 
 
62
 
63
  return _WHISPER_MODEL
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  class ExpenseTextExtractor:
66
  """
67
  Главный экстрактор данных о расходах.
@@ -179,7 +372,7 @@ def polish_notes_text(text: str) -> str:
179
  return normalized
180
 
181
 
182
- def transcribe_audio_text(audio_path: str) -> tuple[str, float]:
183
  """Транскрибирует аудио в текст. Возвращает (текст, время в секундах)."""
184
  mock_text = os.getenv("EXPENSE_VOICE_MOCK_TEXT")
185
  if mock_text:
@@ -187,17 +380,31 @@ def transcribe_audio_text(audio_path: str) -> tuple[str, float]:
187
 
188
  try:
189
  t0 = time.time()
190
- whisper_model = get_whisper_model()
191
- segments, _ = whisper_model.transcribe(audio_path, language="ru", vad_filter=True)
192
- text = " ".join(segment.text.strip() for segment in segments if segment.text and segment.text.strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  elapsed = round(time.time() - t0, 3)
194
  print(f"[TIMINGS] whisper_transcribe: {elapsed}s")
195
  if text:
196
  return text, elapsed
197
- except Exception:
198
- pass
199
 
200
- raise RuntimeError("Speech-to-text backend is unavailable. Install faster-whisper or set EXPENSE_VOICE_MOCK_TEXT.")
201
 
202
 
203
  def process_voice_request(audio_path: str, mode: str, payload: dict[str, Any]) -> dict[str, Any]:
@@ -208,7 +415,11 @@ def process_voice_request(audio_path: str, mode: str, payload: dict[str, Any]) -
208
  supplier_names = extract_names(context.get("suppliers"))
209
  user_names = extract_names(context.get("users"))
210
 
211
- transcript, whisper_time = transcribe_audio_text(audio_path)
 
 
 
 
212
 
213
  if mode == "notes":
214
  notes = polish_notes_text(transcript)
@@ -291,7 +502,8 @@ def index():
291
  "message": "Voice processing API is running",
292
  "endpoints": {
293
  "POST /process-audio": "Process audio file",
294
- "GET /health": "Health check"
 
295
  }
296
  })
297
 
@@ -302,6 +514,45 @@ def health():
302
  return jsonify({"status": "ok"})
303
 
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  @app.post("/process-audio")
306
  def process_audio():
307
  """Обработка аудио файла."""
 
33
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
34
  _MODEL: Optional[SentenceTransformer] = None
35
  _WHISPER_MODEL: Optional[Any] = None
36
+ _WHISPER_PROCESSOR: Optional[Any] = None
37
 
38
 
39
  app = Flask(__name__)
40
  app.config["MAX_CONTENT_LENGTH"] = 20 * 1024 * 1024
41
 
42
+ TEST_USERS = [
43
+ "Я",
44
+ "Оля",
45
+ "Олечка",
46
+ "Влад",
47
+ "Владислав",
48
+ "Wallach",
49
+ ]
50
+
51
+ TEST_SUPPLIERS = [
52
+ "Яндекс подписка",
53
+ "Wellness & Spa Thermal",
54
+ "Volta",
55
+ "Velmart",
56
+ "Vatsak",
57
+ "Vasconi",
58
+ "Valconi",
59
+ "Тема",
60
+ "Temix",
61
+ "Телемаркет Крикова",
62
+ "Takume",
63
+ "Tagaer",
64
+ "Supraten",
65
+ "Startur",
66
+ "Sankos",
67
+ "Samurai",
68
+ "Salomer",
69
+ "Vragob",
70
+ "Primul Discounter",
71
+ "Premier Energy",
72
+ "Пицца 9",
73
+ "Piața Centrală",
74
+ "Peon Farm",
75
+ "Peach Girl",
76
+ "Pandashop",
77
+ "Vivația",
78
+ "Oldcom",
79
+ "Ocean Fish",
80
+ "Номер 1",
81
+ "Nanu Market",
82
+ "Mozza",
83
+ "Moldpressa",
84
+ "Moldova-Presa",
85
+ "MoldovaGaz",
86
+ "Modus Vivendi",
87
+ "Micov",
88
+ "Metro",
89
+ "Mersi",
90
+ "Megapolis",
91
+ "Medical Market",
92
+ "Максимум",
93
+ "Maestro Delice",
94
+ "Maestro",
95
+ "Local",
96
+ "Linella 115",
97
+ "Linel",
98
+ "Vats",
99
+ "Kiss Beauty Salon",
100
+ "Кебаб",
101
+ "Кауфленд",
102
+ "Cat Shop",
103
+ "Joom",
104
+ "Ярди Маркет",
105
+ "Uiti Credit",
106
+ "U.T. Credit",
107
+ "iHerb",
108
+ "Ярмареку",
109
+ "Хипократис",
110
+ "Herb",
111
+ "Граньер",
112
+ "Global Store",
113
+ "Giganet",
114
+ "Franzeluța",
115
+ "Эфилете",
116
+ "Fidesco",
117
+ "Феличи",
118
+ "Fast Food",
119
+ "Farmacia Familia",
120
+ "Eurotelicom",
121
+ "Энергоком",
122
+ "Елика",
123
+ "Дёйнер-Кебаб",
124
+ ]
125
+
126
+ TEST_PHRASES = [
127
+ "Вчера оплатил Яндекс подписку 455,90 лей",
128
+ "Через 3 дня был в Wellness & spa Thermal на 1200,70 лей",
129
+ "вчера заплатил в вольта 425,40 лей",
130
+ "Сегодня купил в velmart на 755,50 лей",
131
+ "Вчера платил в vatsak 185,80 лей",
132
+ "Сегодня был в Vasconi на 455,30 lei",
133
+ "Вчера платил в Valconi 325,90 lei",
134
+ "Сегодня заказал в тему на 895,60 лей",
135
+ "Вчера купил в temix на 185,50 лей",
136
+ "Сегодня оплатил в телемаркет Крикова 655,80 лей",
137
+ "Вчера был в такуме на 425,7 лей",
138
+ "Сегодня купил в tagaer на 285,40 лей",
139
+ "Вчера оплатил в Supraten 1200,50 лень",
140
+ "Сегодня был в стоматологии о фамилии на 455,90 лень",
141
+ "Я на следующей неделе заказал в стартур билеты на 855,60 лень",
142
+ "Сегодня оплатил в Sankos 245,70 лей",
143
+ "Вчера купил в Samurai на 325,40 лей",
144
+ "Сегодня был в Salomer на 185,50 lei",
145
+ "Вчера купил vragob na 655,80 lei",
146
+ "Сегодня купил в primul discounter на 425,03 лей",
147
+ "Вчера оплатил Premier Energy 985,90 lei",
148
+ "Сегодня заказали в пицце 9 на 285,60 лей",
149
+ "На прошлой неделе ходили в piața centrală, купили на 455,7 lei",
150
+ "Сегодня купил в peon farm на 325,40 лей",
151
+ "Вчера Wallach купила в Peach Girl на 755,50 лей",
152
+ "Через 2 дня купил в Pandashop на 895,80 лей",
153
+ "Pazavchora był vivația i kupil na 185,30 lei",
154
+ "Сегодня оплатил в oldcom 655,90 лей",
155
+ "Вчера купил рыбу в Ocean Fish на 280 lei",
156
+ "Сегодня купил в номер 1 на 420 лей",
157
+ "вчера воля купила в nanu market на 250 lei",
158
+ "Сегодня купил в Mozza на 380 lei",
159
+ "Вчера оплатил moldpressa 90 lei",
160
+ "Сегодня заплатил в Moldova-Presa 180 lei",
161
+ "Вчера платил MoldovaGaz 1250 lei",
162
+ "Сегодня был в modus vivendi, я ставил 420 lei",
163
+ "Вчера купил в Micov na 150 lei",
164
+ "Сегодня оплатил в метрах 890,13 лей",
165
+ "Вчера купил в Мерси на 210 lei",
166
+ "Сегодня заплатил в Megapolis 680 lei",
167
+ "Вчера Оля купила лекарство в Medical Market на 340 лей",
168
+ "Сегодня оплатил в максимум 450 lei",
169
+ "Вчера купил десерт в maestro delice на 120 lei",
170
+ "Сегодня оплатил в maestro 750 lei",
171
+ "вчера оля купила в local на 190 лей",
172
+ "Сегодня был в Linelo 115 и купил на 280 лей",
173
+ "Вчера купил продукты в Linel на 420,55 лей",
174
+ "Сегодня оплатил vats 320 lei",
175
+ "Вчера Олечка была в Kiss Beauty Salon на 450 lei",
176
+ "Сегодня купил кебаб в кебаб на 150 лей",
177
+ "Вчера Оля была в Кауфленд и потратила 890,15 лей",
178
+ "Сегодня купил в cat shop на 650 lei",
179
+ "Вчера вечером был выкатан суши на 300 восьятлей",
180
+ "Оля вчера заказала в Joom на 1200 lei",
181
+ "Сегодня купили рассаду в Ярди Маркет на 280 лей",
182
+ "Вчера Влад оплатил в uiti credit 950 lei",
183
+ "Сегодня оплатил в U.T. Credit очередной платеж 1800 лей",
184
+ "Вчера заказал в iherb витамина на 420 лей",
185
+ "На прошлой неделе покупали в Ярмареку на 950,13 лей",
186
+ "Оля вчера была в Хипократис и оставила 650 lei",
187
+ "Сегодня я купил витамины в herb на 180 лей",
188
+ "Вчера купил хлеб в Граньер на 70 лей",
189
+ "Сегодня ходил в Global Store за техникой на 2100 лей",
190
+ "Вчера я оплатил интернет в Giganet 450,35 лей",
191
+ "Сегодня Оля купила хлеб Франзелуца на 80 петлей",
192
+ "вчера купил рыбу в эфилете на 420 лей",
193
+ "На прошлой неделе заплатил в Fidesco 1300 lei",
194
+ "Сегодня Влад был в Феличи и купил сыр на 95 лей",
195
+ "Вчера вечером купили fast food на 180 lei",
196
+ "Олечка вчера купила лекарство фармачия Familia на 240 лей",
197
+ "Я сегодня утром оплатил Eurotelicom 310 lei",
198
+ "Вчера Владислав оплатил энергоком 560 lei",
199
+ "Сегодня оплатил в Елика 420 лей",
200
+ "На следующей неделе в субботу хочу зайти в дёйнер-кебаб",
201
+ ]
202
+
203
 
204
  def get_embedding_model() -> SentenceTransformer:
205
  """Возвращает модель эмбеддингов (ленивая загрузка)."""
 
211
  return _MODEL
212
 
213
 
214
+ def get_whisper_pipeline() -> Any:
215
+ """Возвращает Whisper pipeline (ленивая загрузка)."""
216
+ global _WHISPER_MODEL, _WHISPER_PROCESSOR
217
 
218
  if _WHISPER_MODEL is None:
219
+ from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline
220
+
221
+ model_id = os.getenv("WHISPER_MODEL", "openai/whisper-large-v3-turbo")
222
+
223
+ model = AutoModelForSpeechSeq2Seq.from_pretrained(
224
+ model_id,
225
+ torch_dtype=torch.float32,
226
+ low_cpu_mem_usage=True,
227
+ use_safetensors=True,
228
+ )
229
+ model.to("cpu")
230
+
231
+ _WHISPER_PROCESSOR = AutoProcessor.from_pretrained(model_id)
232
 
233
+ _WHISPER_MODEL = pipeline(
234
+ "automatic-speech-recognition",
235
+ model=model,
236
+ tokenizer=_WHISPER_PROCESSOR.tokenizer,
237
+ feature_extractor=_WHISPER_PROCESSOR.feature_extractor,
238
+ torch_dtype=torch.float32,
239
+ device="cpu",
240
+ )
241
 
242
  return _WHISPER_MODEL
243
 
244
+
245
+ def build_whisper_prompt(suppliers: list[str], users: list[str], max_items: int = 25) -> str:
246
+ """Собирает подсказку для Whisper из поставщиков и пользователей."""
247
+ clean_suppliers = [item.strip() for item in suppliers if isinstance(item, str) and item.strip()][:max_items]
248
+ clean_users = [item.strip() for item in users if isinstance(item, str) and item.strip()][:max_items]
249
+
250
+ parts = ["Это русская голосовая запись о расходах."]
251
+ if clean_suppliers:
252
+ parts.append("Поставщики: " + ", ".join(clean_suppliers) + ".")
253
+ if clean_users:
254
+ parts.append("Пользователи: " + ", ".join(clean_users) + ".")
255
+
256
+ return " ".join(parts)
257
+
258
  class ExpenseTextExtractor:
259
  """
260
  Главный экстрактор данных о расходах.
 
372
  return normalized
373
 
374
 
375
+ def transcribe_audio_text(audio_path: str, suppliers: list[str] | None = None, users: list[str] | None = None) -> tuple[str, float]:
376
  """Транскрибирует аудио в текст. Возвращает (текст, время в секундах)."""
377
  mock_text = os.getenv("EXPENSE_VOICE_MOCK_TEXT")
378
  if mock_text:
 
380
 
381
  try:
382
  t0 = time.time()
383
+ pipe = get_whisper_pipeline()
384
+
385
+ generate_kwargs = {
386
+ "language": "russian",
387
+ "task": "transcribe",
388
+ }
389
+
390
+ prompt = build_whisper_prompt(suppliers or [], users or [])
391
+ if prompt and _WHISPER_PROCESSOR is not None:
392
+ try:
393
+ generate_kwargs["prompt_ids"] = _WHISPER_PROCESSOR.get_prompt_ids(prompt, return_tensors="pt")
394
+ print(f"[TIMINGS] whisper_prompt_enabled: suppliers={len(suppliers or [])}, users={len(users or [])}")
395
+ except Exception as prompt_error:
396
+ print(f"[WARN] Whisper prompt disabled: {prompt_error}")
397
+
398
+ result = pipe(audio_path, generate_kwargs=generate_kwargs)
399
+ text = result.get("text", "").strip()
400
  elapsed = round(time.time() - t0, 3)
401
  print(f"[TIMINGS] whisper_transcribe: {elapsed}s")
402
  if text:
403
  return text, elapsed
404
+ except Exception as e:
405
+ print(f"[ERROR] Whisper transcribe failed: {e}")
406
 
407
+ raise RuntimeError("Speech-to-text backend is unavailable.")
408
 
409
 
410
  def process_voice_request(audio_path: str, mode: str, payload: dict[str, Any]) -> dict[str, Any]:
 
415
  supplier_names = extract_names(context.get("suppliers"))
416
  user_names = extract_names(context.get("users"))
417
 
418
+ transcript, whisper_time = transcribe_audio_text(
419
+ audio_path,
420
+ suppliers=supplier_names,
421
+ users=user_names,
422
+ )
423
 
424
  if mode == "notes":
425
  notes = polish_notes_text(transcript)
 
502
  "message": "Voice processing API is running",
503
  "endpoints": {
504
  "POST /process-audio": "Process audio file",
505
+ "GET /health": "Health check",
506
+ "GET /test-data": "Run text-only extraction tests"
507
  }
508
  })
509
 
 
514
  return jsonify({"status": "ok"})
515
 
516
 
517
+ @app.get("/test-data")
518
+ def test_data():
519
+ """Тестирует извлечение данных из текста без использования Whisper."""
520
+ debug_supplier = (request.args.get("debug") or "").strip().lower() in {"1", "true", "yes"}
521
+ extractor = build_default_pipeline(suppliers=TEST_SUPPLIERS, users=TEST_USERS)
522
+
523
+ started = time.time()
524
+ results: list[dict[str, Any]] = []
525
+
526
+ for phrase in TEST_PHRASES:
527
+ item_started = time.time()
528
+ extracted = extractor.extract(
529
+ phrase,
530
+ reference_date=date.today().isoformat(),
531
+ debug_supplier=debug_supplier,
532
+ )
533
+ results.append({
534
+ "text": phrase,
535
+ "user": extracted.get("user"),
536
+ "supplier": extracted.get("supplier"),
537
+ "amount": extracted.get("amount"),
538
+ "date": extracted.get("date"),
539
+ "date_iso": extracted.get("date_iso"),
540
+ "processing_time": round(time.time() - item_started, 3),
541
+ **({"supplier_debug": extracted.get("supplier_debug")} if debug_supplier and extracted.get("supplier_debug") else {}),
542
+ })
543
+
544
+ return jsonify({
545
+ "status": "ok",
546
+ "mode": "text-only",
547
+ "reference_date": date.today().isoformat(),
548
+ "phrases_count": len(TEST_PHRASES),
549
+ "suppliers_count": len(TEST_SUPPLIERS),
550
+ "users_count": len(TEST_USERS),
551
+ "total_processing_time": round(time.time() - started, 3),
552
+ "results": results,
553
+ })
554
+
555
+
556
  @app.post("/process-audio")
557
  def process_audio():
558
  """Обработка аудио файла."""
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  flask==3.1.0
2
  gunicorn==23.0.0
3
- faster-whisper
 
4
  pymorphy3
5
  rapidfuzz
6
  dateparser
 
1
  flask==3.1.0
2
  gunicorn==23.0.0
3
+ transformers
4
+ accelerate
5
  pymorphy3
6
  rapidfuzz
7
  dateparser