local_voice
local_voice
¶
Fully local realtime voice: Whisper → Ollama → Kokoro, nothing leaves.
The other two providers stream the raw microphone to Google or OpenAI; this one keeps the whole loop on the machine. It is honest about what that buys and what it costs: turn-based with barge-in rather than full duplex, first spoken word ~1.5–2.5 s after the user's last syllable (measured: STT ~1 s for a 4 s French utterance, qwen3.5:9b first token 0.47 s warm, Kokoro RTF 0.28) — against ~0.5 s for Gemini Live. In exchange: no key, no account, and the factory's local-only guard can finally let voice through instead of refusing it wholesale.
Structure: send_audio only buffers and detects turn boundaries; the
response pipeline (transcribe → stream tokens → speak sentence by sentence)
runs as a cancellable task, because barge-in is nothing more than cancelling
it. The three stages are injectable callables so the turn logic is testable
without a microphone, a model server or a vocoder.
Classes¶
LocalVoiceSession
¶
LocalVoiceSession(
*,
model: str = "",
voice: str = "",
instructions: str = "",
language: str = "",
api_key: Optional[str] = None,
enable_tools: bool = True,
max_tool_steps: int = 12,
allowed_tools: Optional[Sequence[str]] = None,
stt: Optional[Callable[[bytes], str]] = None,
llm: Optional[Callable[[List[dict]], Any]] = None,
tts: Optional[Callable[[str], bytes]] = None,
tool_executor: Optional[
Callable[[str, dict], dict]
] = None,
sur_echange: Optional[
Callable[[str, str], None]
] = None,
)
Bases: RealtimeVoiceSession
Turn-based local voice with barge-in, behind the realtime contract.
Source code in src/diapason/speech/realtime/local_voice.py
848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 | |
Functions:¶
classify_endpoint
¶
« complete », « hesitation » ou « neutral » pour une fin de tour.
Conservateur par construction : « complete » exige une ponctuation terminale — c'est le seul verdict qui RACCOURCIT l'attente, donc le seul qui puisse couper quelqu'un. « hesitation » ne fait qu'attendre plus, l'erreur y est bon marché. Tout le reste garde le délai normal.
Source code in src/diapason/speech/realtime/local_voice.py
speakable
¶
Strip what a voice cannot say; collapse the leftover whitespace.
Returns "" when nothing pronounceable is left — an emoji-only chunk leaves its punctuation behind ("👍👍." → "."), and a vocoder handed a bare period says "point" out loud.
Source code in src/diapason/speech/realtime/local_voice.py
french_now
¶
The machine's local date and time, spelled out in French.
Hand-rolled rather than strftime with a locale: setlocale is process-wide state and this runs inside a server thread pool. Injected into the system prompt at every turn — a model has no clock, and "quelle heure est-il" answered with "je ne peux pas lire l'heure" was a reported failure, on a machine that obviously knows.
Source code in src/diapason/speech/realtime/local_voice.py
french_today
¶
La date seule, sans l'heure — stable toute la journée.
L'horloge à la minute près, recollée au prompt à chaque appel, invalidait
le cache de préfixe d'Ollama dès que la minute changeait : les six mille
jetons de prompt et de schémas d'outils étaient relus en entier, plusieurs
secondes par tour. La voix dispose maintenant de current_time : quand
l'heure compte, le modèle la LIT — c'est plus juste qu'une heure figée au
début du tour, et le préfixe, lui, ne bouge plus qu'à minuit.
Source code in src/diapason/speech/realtime/local_voice.py
ollama_reachable
¶
True when the local model server answers. Never raises.
Source code in src/diapason/speech/realtime/local_voice.py
local_voice_readiness
¶
Return whether every local voice runtime component is available.
Keep this check cheap: model construction belongs to connect(), but a
missing optional extra or system phonemizer must disable Start instead of
letting the WebSocket claim readiness and fail a few seconds later.
Source code in src/diapason/speech/realtime/local_voice.py
polish_transcript
¶
Apply the user's dictation dictionary to a voice transcript.
The dictation path earns its accuracy partly AFTER Whisper: the personal dictionary fixes the words the recognizer keeps getting wrong ("App Store", proper nouns). Voice transcripts deserve the same corrections — same user, same vocabulary, same mistakes. bump_usage=False: voice hits must not skew the dictation dictionary's learning statistics.
Source code in src/diapason/speech/realtime/local_voice.py
mentions_assistant_name
¶
Vrai si un mot — ou DEUX mots adjacents recollés — ressemble au nom.
La transcription coupe le nom en deux : « Dia pasons, quelle heure… », constaté en session réelle. Un seul mot ne suffit donc pas ; les paires adjacentes se recollent avant la comparaison.
Source code in src/diapason/speech/realtime/local_voice.py
strip_assistant_name
¶
Retire l'appel initial — « Diapason, ouvre… » → « ouvre… ».