← Tous les manuels · Portail
↗ Ouvrir l'app

Fluent Voice — MANUAL

Rôle

Webapp de pratique orale d'anglais pour un ami de Matt (niveau B1). L'ami ouvre

un lien, appuie une fois sur le cercle, et il a une vraie conversation à l'oral

avec un partenaire IA bienveillant. Rien à installer, rien à taper, rien à lire.

Boucle mains libres :

```

micro navigateur -> POST /api/turn -> STT (lib/stt_client)

-> coach (coach.py, 1 appel LLM : réponse + notation)

-> TTS (lib/tts_client)

-> mp3 joué automatiquement -> le micro se réarme tout seul

```

La pédagogie reprend la compétence /fluent-speaking du repo

github.com/m98/fluent (communication avant grammaire, une question à la fois,

changement de sujet après 3-4 échanges, notation communication 0-5 / grammaire

0-3 / vocabulaire 0-2), adaptée à l'oral : **aucune correction n'est lue à voix

haute**. Le coach utilise un *recast* (il redit l'idée correctement dans sa

propre réponse, sans leçon de grammaire) et la notation reste silencieuse

jusqu'au récap de fin de session.

Infra

  • - URL publique : pas encore publiée (à faire via le skill expose-webapp).
  • - Service systemd : fluent-voice.service
  • - Port local : 127.0.0.1:8367 (uvicorn)
  • - Code : /root/workspace/apps/fluent-voice/
  • - Logs : /var/log/fluent-voice.log
  • - DB : SQLite /root/workspace/apps/fluent-voice/data/fluent.db (WAL)
  • ```bash

    systemctl status fluent-voice

    systemctl restart fluent-voice

    tail -f /var/log/fluent-voice.log

    ```

    Accès (pas de compte, un lien)

    Pas de login : la clé voyage dans le lien et le navigateur la garde en

    localStorage. Lien à envoyer à l'ami :

    ```

    https://<domaine>/?k=<FLUENT_VOICE_KEY>&u=<prenom>

    ```

  • - k = clé d'accès, dans /root/aios/.env sous FLUENT_VOICE_KEY.
  • - u = identifiant du learner (facultatif, défaut friend). C'est la clé de
  • sa mémoire de progression, un seul et même u d'une session à l'autre.

  • - La page JS retire aussitôt les paramètres de l'URL et rejoue la clé en
  • en-tête X-Fluent-Key sur chaque appel API.

  • - / reste public (sinon un retour sans la clé donnerait un 403 avant que le JS
  • puisse la relire) ; **tout /api/* est gardé**. Si FLUENT_VOICE_KEY est

    vide, l'app est entièrement ouverte.

    Endpoints

    MéthodeRouteRôle
    GET/health{"ok":true,"service":"fluent-voice"}, sans auth
    GET/l'app (HTML statique)
    POST/api/sessiondémarre une session, renvoie la phrase d'accueil + son audio
    POST/api/turnmultipart session_id + audio : un échange complet
    POST/api/nudgerelance parlée après un silence prolongé (audio mis en cache)
    POST/api/session/endrécap parlé + écrit, écrit les notes de progression
    GET/api/progress?learner=historique léger et points à travailler
    GET/api/audio/{token}.mp3audio en cache mémoire (TTL 30 min, jeton uuid4)

    Modèles et voix

  • - LLM : OpenAI gpt-4o-mini (JSON mode : réponse parlée + notation en un
  • seul appel), repli automatique sur Gemini gemini-flash-latest.

  • - STT : lib/stt_client.transcribe(..., language="en"). La langue est
  • forcée à l'anglais dans le code : config/preferences.yaml de l'AIOS

    pilote le français, il ne doit pas décider ici.

  • - TTS : lib/tts_client.synthesize(..., voice="nova"). L'app force
  • TTS_PROVIDER=openai + OPENAI_TTS_MODEL=tts-1 dans son propre process

    (le défaut AIOS est Gemini, qui repasse par ffmpeg : plus lent, et voix calée

    pour le français). Rien n'est modifié pour le reste de la flotte.

  • - Usage et télémétrie voix partent dans les logs AIOS habituels avec
  • caller="fluent_voice" (data/tts_usage.jsonl, data/stt_usage.jsonl).

    Variables d'environnement

    Toutes lues depuis /root/aios/.env (systemd donne un environnement vide, le

    serveur charge le .env lui-même). Aucun secret dans le code ni côté client.

    VariableDéfautRôle
    FLUENT_VOICE_KEY(vide = ouvert)clé d'accès du lien
    FLUENT_PORT8367port uvicorn
    FLUENT_TTS_VOICEnovavoix du coach
    FLUENT_TTS_MODELtts-1modèle TTS (tts-1-hd = plus beau, plus lent)
    FLUENT_TTS_PROVIDERopenaigemini pour basculer
    FLUENT_LLM_MODELgpt-4o-minimodèle du coach
    FLUENT_COACH_NAMEEmmaprénom du partenaire
    FLUENT_DB_PATHdata/fluent.dbbase SQLite
    Clés réutiliséesOPENAI_STT_API_KEY, GEMINI_TTS_API_KEY

    Suivi de progression (la mémoire légère)

    Trois tables dans fluent.db :

  • - sessions : une session (début, fin, nombre d'échanges, moyenne /10, récap).
  • - turns : un échange (ce qu'il a dit, ce que le coach a répondu, sujet,
  • communication/grammaire/vocabulaire, latence).

  • - learner_notes : la mémoire longue. kind = weakness | win | topic.
  • Une note identique n'est pas dupliquée, son seen_count est incrémenté :

    c'est ce qui fait remonter une faiblesse récurrente en tête de prompt.

    À la fin d'une session, un appel LLM produit le récap parlé et en extrait les

    notes. Elles sont réinjectées dans le prompt du coach à la session suivante

    (« points à travailler, sans jamais les nommer à voix haute » + sujets déjà

    couverts à éviter). Pas de répétition espacée type SM-2, volontairement.

    Session abandonnée (l'ami ferme l'onglet sans finir) : au démarrage de la

    session suivante, un thread de fond ferme les sessions ouvertes de plus de 10

    minutes et récupère leurs notes. Rien n'est perdu, et le démarrage n'attend pas.

    UI (ce que voit l'ami)

    Un seul point focal : le cercle. Ses états sont portés par la couleur et

    l'animation, jamais par du texte à lire.

    ÉtatCouleurSens
    idlegrisappuyer pour commencer
    speakingbleule coach parle
    listeningvert/menthe + niveau microà toi de parler
    thinkingambreça travaille
    pausedgristrop de silence, appuyer pour reprendre

    Détection de parole locale (WebAudio, seuil adaptatif au bruit de la pièce) :

    rien n'est envoyé tant que personne ne parle. Fin de tour après 1,2 s de

    silence, 45 s max par réponse, relance parlée après 15 s de silence, mise en

    pause après 30 s. Les sous-titres existent (bouton « Show text ») mais sont

    désactivés par défaut : l'app est faite pour être utilisée écran éteint.

    Le cercle sert aussi de télécommande : pendant que le coach parle, un appui

    coupe et redonne la parole ; pendant l'écoute, un appui veut dire « j'ai fini ».

    Sécurité

  • - Toutes les requêtes SQL sont paramétrées ; l'identifiant learner est réduit à
  • un slug [a-z0-9-_] avant toute requête.

  • - Comparaison de la clé en temps constant (hmac.compare_digest).
  • - CSP stricte sans unsafe-inline (le JS et le CSS sont des fichiers externes),
  • plus nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer,

    Permissions-Policy: microphone=(self). robots.txt interdit tout.

  • - Limites : 4 Mo par clip, 120 échanges par session, 60 tours / 10 min / IP,
  • 20 sessions / heure / IP. Jetons audio en uuid4 non devinables, TTL 30 min,

    gardés en mémoire (rien sur disque).

  • - Aucun secret côté client hors la clé d'accès, qui ne donne accès qu'à cette
  • app.

    Test

    ```bash

    suite complète : parle réellement à l'app avec une voix de synthèse,

    vérifie STT + coach + TTS + SQLite + récap + notes + cas limites

    cd /root/workspace/apps/fluent-voice

    export FLUENT_VOICE_KEY=$(grep '^FLUENT_VOICE_KEY=' /root/aios/.env | cut -d= -f2)

    python3 smoke_test.py # local

    python3 smoke_test.py https://<domaine> # après publication

    ```

    Coûte quelques centimes d'appels API. Sort en non-zéro à la première erreur.

    Pièges

  • - Latence : environ 4 s par tour (STT ~0,8 s + LLM ~2 s + TTS ~1,5 s). C'est
  • incompressible sans streaming ; l'UI passe en « thinking » instantanément pour

    que l'attente reste lisible. Ne pas passer en tts-1-hd sans accepter ~1 s de

    plus par tour.

  • - Le prompt système ne suffit pas à faire changer de sujet. L'instruction de
  • rotation a d'abord été mise dans le prompt système : le modèle l'ignorait

    systématiquement au profit de la règle « rebondis sur ce qu'il vient de dire »

    et creusait le même sujet indéfiniment. Elle est maintenant envoyée en message

    system après le tour du learner (coach._topic_change_directive), et

    elle est suivie. Ne pas la redéplacer dans le prompte système.

  • - L'étiquette de sujet est le compteur : le streak compte les tours
  • consécutifs portant le même libellé. Le prompt est réglé « exactitude d'abord,

    stabilité ensuite » : le libellé décrit le sujet de la question du coach, et

    doit être réécrit à l'identique tant que le sujet ne change pas. Trop flou, le

    compteur ne monte jamais à 4 et la rotation ne part pas ; trop collant (une

    version l'a fait), le libellé reste figé sur le premier sujet et force un

    changement alors que la conversation variait déjà toute seule. Si on retouche

    ce passage, refaire les deux tests : 4 sujets différents d'affilée (le libellé

    doit suivre) et 5 tours sur le même sujet (rotation au 5e).

  • - Autoplay iOS : la lecture est déverrouillée par static/silence.mp3 joué
  • dans le geste du premier tap. Sans ce fichier, plus aucun son sur iPhone.

  • - Onglet en arrière-plan : l'enregistrement est mis en pause
  • (visibilitychange), sinon on téléverse du bruit.

  • - Un pkill -f "python3 server.py" tue le shell qui le lance (le motif se
  • trouve dans sa propre ligne de commande). Passer par systemctl restart.

  • - La voix nova est féminine et anglophone ; changer FLUENT_TTS_VOICE change
  • aussi le personnage perçu, ajuster FLUENT_COACH_NAME en même temps.

    Reset des données d'un learner

    ```bash

    sqlite3 /root/workspace/apps/fluent-voice/data/fluent.db \

    "DELETE FROM turns WHERE session_id IN (SELECT id FROM sessions WHERE learner='friend');

    DELETE FROM sessions WHERE learner='friend';

    DELETE FROM learner_notes WHERE learner='friend';"

    ```

    Le learner smoketest est créé par smoke_test.py, il n'affecte pas l'ami.

    Reste à faire

  • - Publier via le skill expose-webapp (nginx + DNS + Let's Encrypt), puis
  • mettre l'URL en haut de ce manuel et l'ajouter au portail

    (apps/portail/index.html, règle « portail new-tab »).

  • - Envoyer à l'ami le lien avec ?k=...&u=<prenom> et lui dire d'ajouter la page
  • à son écran d'accueil (le manifest PWA est en place).

    CHANGELOG

  • - 2026-09-01 : création. Boucle vocale mains libres complète (STT AIOS →
  • coach LLM → TTS AIOS), suivi de session SQLite avec notes reportées d'une

    session à l'autre, récap parlé de fin, UI à un bouton avec détection de parole

    locale, garde d'accès par clé dans le lien, service systemd sur le port 8367,

    suite smoke_test.py de bout en bout.