fix(stt): add primary local Whisper M4 pipeline with ffmpeg audio extraction and working Gemini fallback models
This commit is contained in:
parent
2b6da541a4
commit
587cb4b805
1 changed files with 72 additions and 24 deletions
|
|
@ -163,29 +163,77 @@ async def text_to_speech_bytes(text: str) -> bytes:
|
||||||
logger.error(f"Ошибка Edge TTS: {e}")
|
logger.error(f"Ошибка Edge TTS: {e}")
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
def process_audio_with_gemini(audio_path: str) -> str:
|
async def transcribe_audio(audio_path: str) -> str:
|
||||||
"""Прямое транскрибирование аудио через Gemini Multimodal Audio API"""
|
"""Двухконтурная транскрибация аудио:
|
||||||
|
1. Основной контур: локальный Faster-Whisper (Large-v3) на MacMini M4 (порт 9001)
|
||||||
|
- Быстро, приватно, без квот и без rate-limits от Google.
|
||||||
|
- Поддерживает .ogg, .mp3, .m4a, .mp4, .wav.
|
||||||
|
2. Резервный контур: Gemini Multimodal Audio API с перебором рабочих моделей.
|
||||||
|
"""
|
||||||
|
clean_path = audio_path
|
||||||
|
tmp_wav = None
|
||||||
|
|
||||||
|
# Нормализация для видео/нестандартных форматов через ffmpeg
|
||||||
|
if audio_path.endswith('.mp4') or audio_path.endswith('.ogg'):
|
||||||
|
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
|
||||||
|
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
if res.returncode == 0 and os.path.exists(tmp_wav):
|
||||||
|
clean_path = tmp_wav
|
||||||
|
|
||||||
|
# 1. Приоритет: локальный Whisper M4
|
||||||
try:
|
try:
|
||||||
logger.info(f"Выгрузка {audio_path} в Gemini Multimodal Audio API...")
|
timeout = aiohttp.ClientTimeout(total=25)
|
||||||
audio_file = genai.upload_file(path=audio_path)
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
with open(clean_path, 'rb') as f:
|
||||||
prompt = (
|
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()
|
||||||
model = genai.GenerativeModel('gemini-2.5-flash')
|
text = (data.get('text') or "").strip()
|
||||||
response = model.generate_content([audio_file, prompt])
|
if text:
|
||||||
|
logger.info(f"🎯 [Whisper M4] Распознано ({len(text)} симв): {text[:70]}...")
|
||||||
try:
|
if tmp_wav and os.path.exists(tmp_wav):
|
||||||
genai.delete_file(name=audio_file.name)
|
os.remove(tmp_wav)
|
||||||
except Exception:
|
return text
|
||||||
pass
|
else:
|
||||||
|
logger.warning(f"Whisper M4 вернул HTTP {resp.status}, переход на Gemini...")
|
||||||
return response.text.strip() if response.text else ""
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка Gemini Audio API: {e}")
|
logger.warning(f"⚠️ Whisper M4 недоступен ({e}), переход на Gemini...")
|
||||||
return ""
|
|
||||||
|
# 2. Резерв: Gemini Audio API
|
||||||
|
for model_name in ['gemini-2.5-flash', 'gemini-3.5-transcribe', 'gemini-flash-latest', 'gemini-2.5-flash-lite']:
|
||||||
|
try:
|
||||||
|
logger.info(f"Выгрузка {clean_path} в Gemini Audio API ({model_name})...")
|
||||||
|
audio_file = genai.upload_file(path=clean_path)
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
"Точно транскрибируй эту аудиозапись на русском языке в текст. "
|
||||||
|
"Не добавляй никаких вводных слов, пояснений и комментариев, отдай исключительно чистый текст произнесенных слов."
|
||||||
|
)
|
||||||
|
|
||||||
|
model = genai.GenerativeModel(model_name)
|
||||||
|
response = model.generate_content([audio_file, prompt])
|
||||||
|
|
||||||
|
try:
|
||||||
|
genai.delete_file(name=audio_file.name)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if response and response.text and response.text.strip():
|
||||||
|
text = response.text.strip()
|
||||||
|
logger.info(f"🎯 [Gemini {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}")
|
||||||
|
|
||||||
|
if tmp_wav and os.path.exists(tmp_wav):
|
||||||
|
os.remove(tmp_wav)
|
||||||
|
return ""
|
||||||
|
|
||||||
async def execute_antigravity_agent(user_text: str) -> str:
|
async def execute_antigravity_agent(user_text: str) -> str:
|
||||||
"""Обработка команды и мыслей агентом Майа с систематизацией, Битрикс24 и правом исполнения bash-команд"""
|
"""Обработка команды и мыслей агентом Майа с систематизацией, Битрикс24 и правом исполнения bash-команд"""
|
||||||
|
|
@ -215,7 +263,7 @@ async def execute_antigravity_agent(user_text: str) -> str:
|
||||||
" Внутри <voice> пиши только чистый русский текст без звездочек, эмодзи и Markdown."
|
" Внутри <voice> пиши только чистый русский текст без звездочек, эмодзи и Markdown."
|
||||||
)
|
)
|
||||||
|
|
||||||
models_to_try = ['gemini-2.5-flash', 'gemini-3.5-flash', 'gemini-3.5-flash-lite']
|
models_to_try = ['gemini-2.5-flash', 'gemini-flash-latest', 'gemini-2.5-flash-lite', 'gemini-3.7-flash', 'gemini-2.5-pro']
|
||||||
last_error = None
|
last_error = None
|
||||||
|
|
||||||
for model_name in models_to_try:
|
for model_name in models_to_try:
|
||||||
|
|
@ -287,8 +335,8 @@ async def handle_voice_message(message: Message, bot: Bot):
|
||||||
file_info = await bot.get_file(file_id)
|
file_info = await bot.get_file(file_id)
|
||||||
await bot.download_file(file_info.file_path, local_audio_path)
|
await bot.download_file(file_info.file_path, local_audio_path)
|
||||||
|
|
||||||
# 1. Прямая расшифровка через Gemini Audio
|
# 1. Прямая расшифровка через локальный Whisper M4 (с фоллбэком на Gemini)
|
||||||
transcribed_text = process_audio_with_gemini(local_audio_path)
|
transcribed_text = await transcribe_audio(local_audio_path)
|
||||||
|
|
||||||
if os.path.exists(local_audio_path):
|
if os.path.exists(local_audio_path):
|
||||||
os.remove(local_audio_path)
|
os.remove(local_audio_path)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue