feat(voice): 100% Google Gemini Multimodal STT with ffmpeg audio normalization and model fallback
This commit is contained in:
parent
587cb4b805
commit
487a2c7bcd
1 changed files with 17 additions and 32 deletions
|
|
@ -164,17 +164,16 @@ async def text_to_speech_bytes(text: str) -> bytes:
|
|||
return b""
|
||||
|
||||
async def transcribe_audio(audio_path: str) -> str:
|
||||
"""Двухконтурная транскрибация аудио:
|
||||
1. Основной контур: локальный Faster-Whisper (Large-v3) на MacMini M4 (порт 9001)
|
||||
- Быстро, приватно, без квот и без rate-limits от Google.
|
||||
- Поддерживает .ogg, .mp3, .m4a, .mp4, .wav.
|
||||
2. Резервный контур: Gemini Multimodal Audio API с перебором рабочих моделей.
|
||||
"""Распознавание речи через Gemini Multimodal Audio API:
|
||||
- Идеальное распознавание Google (длинные записи, шумы, речь на ходу, естественные мысли).
|
||||
- Автоматическая нормализация через ffmpeg для видеосообщений (.mp4) и голосовых (.ogg).
|
||||
- Умный перебор моделей при секундных задержках/лимитах сети.
|
||||
"""
|
||||
clean_path = audio_path
|
||||
tmp_wav = None
|
||||
|
||||
# Нормализация для видео/нестандартных форматов через ffmpeg
|
||||
if audio_path.endswith('.mp4') or audio_path.endswith('.ogg'):
|
||||
# Нормализация для видеосообщений и нестандартных контейнеров
|
||||
if audio_path.endswith(('.mp4', '.ogg', '.m4a', '.3gp')):
|
||||
tmp_wav = f"/tmp/norm_{os.getpid()}_{int(datetime.now().timestamp())}.wav"
|
||||
res = subprocess.run([
|
||||
'ffmpeg', '-y', '-i', audio_path, '-vn', '-ar', '16000', '-ac', '1', tmp_wav
|
||||
|
|
@ -182,31 +181,16 @@ async def transcribe_audio(audio_path: str) -> str:
|
|||
if res.returncode == 0 and os.path.exists(tmp_wav):
|
||||
clean_path = tmp_wav
|
||||
|
||||
# 1. Приоритет: локальный Whisper M4
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=25)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
with open(clean_path, 'rb') as f:
|
||||
form = aiohttp.FormData()
|
||||
form.add_field('file', f, filename=os.path.basename(clean_path), content_type='audio/wav')
|
||||
async with session.post("http://192.168.10.167:9001/transcribe", data=form) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json()
|
||||
text = (data.get('text') or "").strip()
|
||||
if text:
|
||||
logger.info(f"🎯 [Whisper M4] Распознано ({len(text)} симв): {text[:70]}...")
|
||||
if tmp_wav and os.path.exists(tmp_wav):
|
||||
os.remove(tmp_wav)
|
||||
return text
|
||||
else:
|
||||
logger.warning(f"Whisper M4 вернул HTTP {resp.status}, переход на Gemini...")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Whisper M4 недоступен ({e}), переход на Gemini...")
|
||||
models_to_try = [
|
||||
'gemini-2.5-flash',
|
||||
'gemini-flash-latest',
|
||||
'gemini-2.5-flash-lite',
|
||||
'gemini-3.5-transcribe'
|
||||
]
|
||||
|
||||
# 2. Резерв: Gemini Audio API
|
||||
for model_name in ['gemini-2.5-flash', 'gemini-3.5-transcribe', 'gemini-flash-latest', 'gemini-2.5-flash-lite']:
|
||||
for model_name in models_to_try:
|
||||
try:
|
||||
logger.info(f"Выгрузка {clean_path} в Gemini Audio API ({model_name})...")
|
||||
logger.info(f"🎙 [Gemini STT] Распознавание {clean_path} через {model_name}...")
|
||||
audio_file = genai.upload_file(path=clean_path)
|
||||
|
||||
prompt = (
|
||||
|
|
@ -224,12 +208,13 @@ async def transcribe_audio(audio_path: str) -> str:
|
|||
|
||||
if response and response.text and response.text.strip():
|
||||
text = response.text.strip()
|
||||
logger.info(f"🎯 [Gemini {model_name}] Распознано: {text[:70]}...")
|
||||
logger.info(f"🎯 [Gemini STT] Успешно распознано ({model_name}): {text[:70]}...")
|
||||
if tmp_wav and os.path.exists(tmp_wav):
|
||||
os.remove(tmp_wav)
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Gemini Audio API ({model_name}) сбой: {e}")
|
||||
logger.warning(f"⚠️ [Gemini STT] Ошибка на модели {model_name}: {e}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if tmp_wav and os.path.exists(tmp_wav):
|
||||
os.remove(tmp_wav)
|
||||
|
|
|
|||
Loading…
Reference in a new issue