"""Transcription par OpenRouter — même famille Gemini, autre guichet. Comme `moteur/gemini.py`, ce moteur N'EST PAS local : l'audio part chez OpenRouter, qui le relaie à Google. L'encodage, la consigne, le repli local et la lecture de la clé sont partagés avec le moteur Gemini direct ; seuls l'adresse, l'authentification et le format de requête (compatible OpenAI, partie `input_audio`) diffèrent. """ import base64 import json from core import clean_transcript from moteur.gemini import ( CONSIGNE, DELAI_RESEAU_S, ErreurGemini, audio_en_wav, lire_cle, poster_json, ) URL_API = "https://openrouter.ai/api/v1/chat/completions" SERVICE_TROUSSEAU = "zonza-openrouter" def lire_cle_openrouter(trousseau=None, environ=None): """La clé du trousseau `zonza-openrouter`, sinon OPENROUTER_API_KEY.""" return lire_cle(trousseau, environ, service=SERVICE_TROUSSEAU, variable="OPENROUTER_API_KEY") def _envoyer(url, entetes, corps, delai_s): return poster_json(url, entetes, corps, delai_s) class TranscripteurOpenRouter: """Implémentation du contrat Transcripteur via OpenRouter.""" def __init__(self, modele, langue, amorce, sample_rate=16000, lire_cle=lire_cle_openrouter, envoyer=_envoyer): self.modele = modele self.langue = langue self.amorce = amorce or "" self.sample_rate = sample_rate self._lire_cle = lire_cle self._envoyer = envoyer def _cle(self): cle = self._lire_cle() if not cle: raise ErreurGemini( "aucune clé OpenRouter : lancer `security add-generic-password " f"-s {SERVICE_TROUSSEAU} -a zonza -w` puis relancer Zonza" ) return cle def warmup(self): """Rien à charger : on vérifie seulement la clé, sans appel payant.""" self._cle() def transcrire(self, audio): wav = base64.b64encode(audio_en_wav(audio, self.sample_rate)).decode("ascii") corps = { "model": self.modele, "messages": [ {"role": "system", "content": CONSIGNE.format( langue=self.langue, amorce=self.amorce)}, {"role": "user", "content": [ {"type": "input_audio", "input_audio": {"data": wav, "format": "wav"}}, {"type": "text", "text": "Transcris cet enregistrement."}, ]}, ], } entetes = {"Authorization": f"Bearer {self._cle()}", "X-Title": "Zonza"} reponse = self._envoyer(URL_API, entetes, corps, DELAI_RESEAU_S) # OpenRouter peut rendre un 200 portant un objet `error` quand le # fournisseur en aval tombe : ce n'est PAS une transcription vide. if "error" in reponse: erreur = reponse["error"] raise ErreurGemini(f"OpenRouter : {erreur.get('message', erreur)}") choix = reponse.get("choices") or [] if not choix: raise ErreurGemini(f"réponse sans transcription : {json.dumps(reponse)[:300]}") return clean_transcript(choix[0].get("message", {}).get("content") or "")