feat(voice): transcribe Telegram voice notes with SpeechRecognition

Add a voice message handler that downloads OGG audio, converts it to
WAV via pydub/ffmpeg, and transcribes it with Google Speech Recognition
(pt-BR) before feeding the text into the normal message pipeline.

Also extract shared text processing into process_text_message, guard
against updates without a text attribute, switch the default AI provider
to openrouter with deepseek-v4-flash, and document the ffmpeg system
requirement in DEPLOY.md.
This commit is contained in:
2026-10-25 17:27:42 -03:00
parent 6b3b6541a7
commit 25288d2c0f
73 changed files with 139973 additions and 38 deletions
+86 -3
View File
@@ -1068,7 +1068,7 @@ def update_profile_with_preference(text: str) -> Dict[str, Any]:
return {"section": section, "field": field, "content": content, "profile": profile}
async def handle_profile_update_if_triggered(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
user_message = update.message.text.strip()
user_message = (getattr(update.message, "text", None) or "").strip()
content = _extract_profile_update_content(user_message)
if not content:
@@ -3239,6 +3239,68 @@ async def listfiles_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("\n".join(lines))
async def _transcribe_voice_file(ogg_path: str) -> Optional[str]:
wav_path = ogg_path.replace(".ogg", ".wav")
try:
from pydub import AudioSegment
audio = AudioSegment.from_ogg(ogg_path)
audio.export(wav_path, format="wav")
except Exception:
return None
def _recognize():
import speech_recognition as sr
r = sr.Recognizer()
with sr.AudioFile(wav_path) as source:
audio_data = r.record(source)
return r.recognize_google(audio_data, language="pt-BR")
try:
text = await asyncio.to_thread(_recognize)
except Exception:
text = None
finally:
for path in (ogg_path, wav_path):
try:
os.remove(path)
except Exception:
pass
return text
async def voice_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
voice = getattr(update.message, "voice", None)
if not voice:
await update.message.reply_text("Não detectei mensagem de voz.")
print("[VOICE] update.message.voice is None")
return
print("[VOICE] received voice message")
await update.message.reply_text("🎤 Recebi seu áudio, processando...")
try:
file = await context.bot.get_file(voice.file_id)
ogg_path = f"/tmp/voice_{voice.file_id}.ogg"
await file.download_to_drive(ogg_path)
print("[VOICE] downloaded to", ogg_path)
except Exception as e:
print("[VOICE] download error:", e)
await update.message.reply_text("Erro ao baixar o áudio. Tente novamente.")
return
text = await _transcribe_voice_file(ogg_path)
print("[VOICE] transcription result:", repr(text))
if text:
await update.message.reply_text(f"🎤 Transcrevi: \"{text}\"")
await process_text_message(update, context, text)
else:
await update.message.reply_text("Não consegui entender o áudio. Pode enviar como texto?")
async def handle_file_upload(update: Update, context: ContextTypes.DEFAULT_TYPE):
await auto_detect_chat_id(update)
@@ -3736,58 +3798,78 @@ async def onboarding_command(update: Update, context: ContextTypes.DEFAULT_TYPE)
await start_onboarding(update, context)
async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
print("[MSG] handler called")
await auto_detect_chat_id(update)
user_message = update.message.text.strip()
user_message = (update.message.text or "").strip()
print("[MSG] text=", repr(user_message))
await process_text_message(update, context, user_message)
async def process_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE, user_message: str) -> None:
print("[PROCESS] text=", repr(user_message))
if not user_message:
print("[PROCESS] empty message, returning")
return
global is_onboarding, is_newconfig, is_configagentprofile, is_edit_user_profile, is_edit_agent_profile, is_activity_crud, is_agendamento, is_useragenda, is_configia
if is_configia:
handled = await handle_configia_message(update, context)
print("[PROCESS] configia handled=", handled)
if handled:
return
if is_activity_crud:
if await handle_activity_crud_answer(update, context):
handled = await handle_activity_crud_answer(update, context)
print("[PROCESS] activity_crud handled=", handled)
if handled:
return
if is_onboarding:
handled = await handle_onboarding_answer(update, context)
print("[PROCESS] onboarding handled=", handled)
if handled:
return
if is_newconfig:
handled = await handle_newconfig_answer(update, context)
print("[PROCESS] newconfig handled=", handled)
if handled:
return
if is_configagentprofile:
handled = await handle_configagentprofile_answer(update, context)
print("[PROCESS] configagentprofile handled=", handled)
if handled:
return
if is_edit_user_profile:
handled = await handle_edit_user_profile_answer(update, context)
print("[PROCESS] edit_user_profile handled=", handled)
if handled:
return
if is_edit_agent_profile:
handled = await handle_edit_agent_profile_answer(update, context)
print("[PROCESS] edit_agent_profile handled=", handled)
if handled:
return
if is_agendamento:
handled = await handle_agendamento_answer(update, context)
print("[PROCESS] agendamento handled=", handled)
if handled:
return
if is_useragenda:
handled = await handle_useragenda_answer(update, context)
print("[PROCESS] useragenda handled=", handled)
if handled:
return
profile_updated = await handle_profile_update_if_triggered(update, context)
print("[PROCESS] profile_updated=", profile_updated)
if profile_updated:
return
add_to_context("user", user_message)
print("[PROCESS] added context, getting AI response...")
# Check if user query references an uploaded file
file_context = ""
@@ -4060,6 +4142,7 @@ def main():
app_instance.add_handler(CommandHandler("uploadfile", uploadfile_command))
app_instance.add_handler(CommandHandler("listfiles", listfiles_command))
app_instance.add_handler(CallbackQueryHandler(configia_callback, pattern=r"^configia_(provider|model)_"))
app_instance.add_handler(MessageHandler(filters.VOICE, voice_message_handler))
app_instance.add_handler(MessageHandler(filters.Document.ALL, handle_file_upload))
app_instance.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, message_handler))