diff --git a/api_app/services/vosk_recognizer.py b/api_app/services/vosk_recognizer.py index da99f44..a476d24 100644 --- a/api_app/services/vosk_recognizer.py +++ b/api_app/services/vosk_recognizer.py @@ -1,51 +1,81 @@ -import os, wave, vosk, ffmpeg +import json +import logging +import os +import tempfile +import threading +import wave -MODEL_PATH = r"models/vosk-model-small-ru-0.22" -FFMPEG_PATH = r"models/ffmpeg/bin/ffmpeg.exe" +import ffmpeg +import vosk +from django.conf import settings -def convert_audio_to_wav(input_file, output_file, FFMPEG_PATH): +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', 'quiet') - .run(cmd=FFMPEG_PATH, overwrite_output=True) + .global_args('-loglevel', 'error') + .run(cmd=settings.FFMPEG_BINARY, overwrite_output=True, capture_stdout=True, capture_stderr=True) ) - print(f"Конвертация завершена: {output_file}") except ffmpeg.Error as e: - print("Ошибка при конвертации:", e.stderr.decode()) + raise RecognitionError("Не удалось прочитать аудиофайл") from e -vosk.SetLogLevel(-1) +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: - if not os.path.exists(MODEL_PATH): - print("Ошибка: Модель не найдена!") - return "" + model = get_model() - model = vosk.Model(MODEL_PATH) + # временный файл — свой на каждый запрос (раньше общий 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 - if audio_path.split('.')[-1] != "wav": - convert.convert_audio_to_wav(audio_path, "audio.wav", FFMPEG_PATH) - audio_path = "audio.wav" - else: + # Vosk отдаёт текст по фразам: раньше бралось только FinalResult(), и в длинной записи + # оставалась лишь последняя фраза + parts = [] with wave.open(audio_path, "rb") as wf: - if wf.getnchannels() != 1 or wf.getsampwidth() != 2 or wf.getframerate() != 16000: - convert.convert_audio_to_wav(audio_path, "audio.wav", FFMPEG_PATH) - audio_path = "audio.wav" + 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", "")) - - with wave.open(audio_path, "rb") as wf: # использование vosk - recognizer = vosk.KaldiRecognizer(model, wf.getframerate()) - while True: - data = wf.readframes(3200) - if not data: - break - recognizer.AcceptWaveform(data) - - if audio_path == "audio.wav": - os.remove(audio_path) - - return recognizer.FinalResult().split(": \"")[-1][:-3] + return " ".join(p for p in parts if p) diff --git a/media/uploads/audio.ogg b/examples/sample.ogg similarity index 100% rename from media/uploads/audio.ogg rename to examples/sample.ogg