mirror of
https://github.com/EDeev/api_processor.git
synced 2026-10-07 20:49:34 +03:00
- любой файл, кроме WAV 16 кГц моно, падал с NameError: вызывался несуществующий convert.convert_audio_to_wav; - из длинной записи оставалась только последняя фраза: бралось лишь FinalResult(), а Vosk отдаёт текст по фразам через Result(); - общий временный audio.wav в текущей папке — одновременные запросы перезаписывали друг другу аудио; теперь свой временный файл на запрос; - ffmpeg — из PATH (или FFMPEG_BINARY) вместо Windows-пути к ffmpeg.exe; - модель загружается один раз на процесс, а не на каждый запрос; - ошибка конвертации возвращается клиенту, а не печатается и не превращается в пустой текст.
81 lines
3 KiB
Python
81 lines
3 KiB
Python
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)
|