1
0
Fork 0
mirror of https://github.com/EDeev/api_processor.git synced 2026-10-07 20:49:34 +03:00
api_processor/api_app/services/vosk_recognizer.py
Egor Deev fc2f750cee Распознавание речи: исправлены ошибки
- любой файл, кроме WAV 16 кГц моно, падал с NameError: вызывался несуществующий
  convert.convert_audio_to_wav;
- из длинной записи оставалась только последняя фраза: бралось лишь FinalResult(), а Vosk
  отдаёт текст по фразам через Result();
- общий временный audio.wav в текущей папке — одновременные запросы перезаписывали друг
  другу аудио; теперь свой временный файл на запрос;
- ffmpeg — из PATH (или FFMPEG_BINARY) вместо Windows-пути к ffmpeg.exe;
- модель загружается один раз на процесс, а не на каждый запрос;
- ошибка конвертации возвращается клиенту, а не печатается и не превращается в пустой текст.
2026-10-06 00:38:57 +00:00

81 lines
3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import logging
import os
import tempfile
import threading
import wave
import ffmpeg
import vosk
from django.conf import settings
logger = logging.getLogger(__name__)
vosk.SetLogLevel(-1)
_model = None
_model_lock = threading.Lock()
class RecognitionError(Exception):
pass
def get_model():
"""Модель Vosk загружается один раз на процесс (раньше — на каждый запрос, это секунды и сотни МБ)"""
global _model
with _model_lock:
if _model is None:
if not os.path.isdir(settings.VOSK_MODEL_PATH):
raise RecognitionError(f"Модель Vosk не найдена: {settings.VOSK_MODEL_PATH}")
_model = vosk.Model(str(settings.VOSK_MODEL_PATH))
return _model
def convert_audio_to_wav(input_file, output_file):
"""WAV 16 кГц моно для Vosk, с шумоподавлением и нормализацией громкости"""
try:
(
ffmpeg
.input(input_file)
.output(output_file, format='wav', acodec='pcm_s16le', ar='16000', ac=1,
af='acompressor,afftdn,dynaudnorm,aresample=16000') # 16kHz для Vosk
.global_args('-loglevel', 'error')
.run(cmd=settings.FFMPEG_BINARY, overwrite_output=True, capture_stdout=True, capture_stderr=True)
)
except ffmpeg.Error as e:
raise RecognitionError("Не удалось прочитать аудиофайл") from e
def is_vosk_ready_wav(path) -> bool:
try:
with wave.open(path, "rb") as wf:
return wf.getnchannels() == 1 and wf.getsampwidth() == 2 and wf.getframerate() == 16000
except (wave.Error, EOFError):
return False
def recognize_speech(audio_path) -> str:
model = get_model()
# временный файл — свой на каждый запрос (раньше общий audio.wav в текущей папке:
# одновременные запросы перезаписывали друг другу аудио)
with tempfile.TemporaryDirectory() as tmp:
if not (audio_path.lower().endswith(".wav") and is_vosk_ready_wav(audio_path)):
wav_path = os.path.join(tmp, "audio.wav")
convert_audio_to_wav(audio_path, wav_path)
audio_path = wav_path
# Vosk отдаёт текст по фразам: раньше бралось только FinalResult(), и в длинной записи
# оставалась лишь последняя фраза
parts = []
with wave.open(audio_path, "rb") as wf:
recognizer = vosk.KaldiRecognizer(model, wf.getframerate())
while True:
data = wf.readframes(4000)
if not data:
break
if recognizer.AcceptWaveform(data):
parts.append(json.loads(recognizer.Result()).get("text", ""))
parts.append(json.loads(recognizer.FinalResult()).get("text", ""))
return " ".join(p for p in parts if p)