Moteur OpenRouter (google/gemini-3.5-flash-lite), par defaut
Meme consigne, meme encodage, meme repli local que le moteur Gemini direct ; seuls l'adresse, l'authentification (Bearer) et le format de requete (input_audio, compatible OpenAI) changent. Cle : trousseau zonza-openrouter, puis OPENROUTER_API_KEY. Un 200 portant un objet error est traite comme un echec, pas comme une transcription vide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""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 "")
|
||||
Reference in New Issue
Block a user