Voraussetzungen
- Python 3.11 oder neuer und uv
- Ein Venice-API-Schlüssel von venice.ai
- Ein Mikrofon und Lautsprecher, wenn du die volle Sprachschleife willst
uv sync installiert das Python-Paket, und unter Windows ist das alles, was du brauchst. macOS und Linux wollen zusätzlich die PortAudio-Bibliothek:
--text-only-Flag entgegen, das das Mikrofon komplett überspringt und trotzdem Chat und TTS durchspielt, sodass du auch auf einer Kiste ganz ohne Audio-Hardware mitmachen kannst.
Was wir bauen
Eine Gesprächsrunde besteht aus drei Requests:
Diese Modell-IDs sind ein Ausgangspunkt und keine feste Liste. Venice rotiert den Katalog, also löse sie zur Laufzeit über
GET /models?type=... und GET /models/traits auf, bevor du irgendetwas auslieferst. Siehe Deprecations dazu, wie das abläuft.
Wir halten den Quellbaum absichtlich klein:
venice.py ist der Teil, den du direkt in eine Web-App, einen Discord-Bot oder eine Telefon-Integration übernehmen kannst. audio.py ist die einzige Datei, die sich dafür interessiert, auf welcher Maschine sie läuft, und Venice bekommt nichts davon zu sehen — die API erhält auf dem Hinweg immer nur einen WAV-Blob und gibt auf dem Rückweg rohes PCM zurück.
Setup
Erstelle das Projekt und füge die Abhängigkeiten hinzu. Das OpenAI-SDK erledigt die ganze HTTP-Arbeit,python-dotenv hält den Schlüssel aus deiner Shell-History heraus, und sounddevice spricht mit Mikrofon und Lautsprechern:
.env.example, damit die Modellauswahl Konfiguration ist und nicht irgendwo im Code vergraben liegt:
.env und füge deinen Schlüssel ein.
Das SDK auf Venice ausrichten
Die Venice-API ist OpenAI-kompatibel, also verwenden wir den offiziellenopenai-Client und ändern die Base-URL. Das ist die gesamte Integration. Erstelle venice.py und beginne mit dem Client:
os.environ["VENICE_API_KEY"] werfen zu lassen. Ein KeyError-Traceback ist eine schlechte erste Erfahrung für etwas so Gewöhnliches wie einen fehlenden Schlüssel.
Noch ein Stück Aufräumarbeit, wo wir schon dabei sind. Das SDK wirft OpenAIError-Subklassen, und das nützliche Detail steckt im Response-Body, es lohnt sich also, das einmal auszupacken:
Den Nutzer hören
POST /audio/transcriptions nimmt eine Audiodatei entgegen und gibt Text zurück. Wir nehmen lokal 16-kHz-Mono-WAV auf, aber der Endpunkt akzeptiert die üblichen Formate, also mappen wir die Dateiendung auf einen MIME-Type, statt einen fest zu verdrahten:
Die Antwort streamen
Nun der Chat-Aufruf. Hier gibt es zwei Venice-spezifische Einstellungen, die einen echten Unterschied dafür machen, wie der Agent klingt:include_venice_system_prompt: False verhindert, dass Venice unserem System-Prompt seinen eigenen voranstellt. Bleibt das an, sind das grob siebzehnhundert zusätzliche Input-Tokens pro Aufruf und eine zweite Stimme, die dem Modell sagt, wie es sich verhalten soll. disable_thinking: True (mit reasoning.enabled: False für Modelle, die das neuere Feld lesen) verhindert, dass GLM sein Token-Budget für eine versteckte Gedankenkette ausgibt, bevor es irgendetwas sagt — was, wenn du auf eine hörbare Antwort wartest, Zeit ist, die du hören kannst.
Der Prompt selbst verdient seine Länge. Zwanzig Wörter zu verlangen lässt Antworten gesprochen statt geschrieben klingen, und „Omit detail rather than ending mid-sentence” ist das, was verhindert, dass ein hartes max_tokens-Limit mitten im Wort abschneidet. Markdown zu verbieten ist wichtiger, als man denkt: Ein TTS-Modell liest Sternchen bereitwillig laut vor.
Die Anweisung, die Nachricht des Nutzers als nicht vertrauenswürdig zu behandeln, leistet hier echte Arbeit. Transkribierte Sprache ist Nutzereingabe wie jede andere, und „ignoriere deine bisherigen Anweisungen” ist laut ausgesprochen genauso leicht wie getippt.
cancel-Event erlaubt dem Aufrufer, das Leeren des Streams zu stoppen, wenn der Nutzer Ctrl+C drückt, und das Schließen des Streams in einem finally-Block gibt die Verbindung frei, statt sie bis zum Timeout hängen zu lassen.
Sätze aufteilen, während sie eintreffen
Auf., ! und ? zu splitten bringt dich zu 90 % ans Ziel und blamiert dich dann beim ersten Mal, wenn das Modell „Dr. Smith” sagt. Also prüfen wir, ob das vor dem Punkt eine Abkürzung ist, bevor wir ihn als Grenze behandeln:
"Hello." ein fertiger Satz sein oder die erste Hälfte von "Hello.txt", und wir können es noch nicht wissen. Auf das Leerzeichen zu warten bedeutet, dass wir einen Satz nie zu früh abschneiden — um den Preis, den letzten festzuhalten, bis der Stream endet, was iter_sentences mit diesem abschließenden leftover-Flush erledigt.
Das ist ein naiver Splitter, und das ist in Ordnung. Es ist außerdem das eine Stück Logik hier, das sich billig per Unit-Test prüfen lässt, also lohnt es sich:
Die Antwort sprechen
POST /audio/speech ist der dritte und letzte Aufruf. Zwei Optionen lassen ihn schnell wirken:
response_format="pcm" liefert uns rohe, vorzeichenbehaftete 16-Bit-Little-Endian-Samples mit 24 kHz mono, die wir ohne Decodier-Schritt direkt an den Lautsprecher weiterreichen können. tts-kokoro verwendet ansonsten standardmäßig MP3, und ein MP3 zu decodieren heißt zu warten, bis genug von der Datei angekommen ist, bevor irgendetwas davon abgespielt werden kann. streaming: True ist das Venice-Flag, das Audio schon während der Synthese sendet statt erst, wenn der ganze Clip fertig ist.
resolve_voice ist absichtlich unspektakulär — es trimmt den String, fällt auf den Umgebungs-Default zurück und validiert nicht gegen eine Liste:
Prüfen, bevor du abspielst
Hier ist der eine Fallstrick, der dich vom Stuhl springen lässt. Rohes PCM hat keinen Header und keine Magic Bytes — wenn also eine Fehlerantwort in die Audio-Pipe geschrieben wird, spielt der Lautsprecher das JSON getreulich als Rauschausbruch bei voller Lautstärke ab. Prüfe also Status und Content-Type, bevor du den Body als Audio behandelst, und schnuppere als Absicherung am ersten Chunk:RIFF fängt eine WAV-Antwort ab und ID3 eine MP3 — beides bedeutet, dass das response_format nicht gegriffen hat. Der JSON-Check fängt einen Fehler-Body ab. Nichts davon ist clever, und alles davon ist der Unterschied zwischen einem lesbaren Fehler und einem erschrockenen Nutzer.
Aufnahme und Wiedergabe
Dieser Teil ist nicht Venice, also gehen wir zügig durch.audio.py öffnet einen PortAudio-Input-Stream, während der Nutzer spricht, und einen PortAudio-Output-Stream, um die Antwort abzuspielen, beides über sounddevice.
Wir importieren es lazy, damit eine fehlende native Bibliothek ein Satz wird statt eines OSError beim Start:
sounddevice meldet den zweiten als bloßen OSError direkt aus dem Import. Beide hier abzufangen ist das, was --text-only auf einer Maschine funktionieren lässt, die PortAudio überhaupt nicht laden kann.
Die Aufnahme ist ein Callback, der in eine Liste anhängt, mit einer harten Obergrenze, damit eine vergessene Session nicht grenzenlos wächst:
try/finally ist Absicht. Das innere verwandelt einen Abbruch in einen freundlichen AudioError, und das äußere stoppt und schließt den Stream auf jedem Weg nach draußen — einschließlich Abbruch —, weil ein RawInputStream, der nie geschlossen wird, das Mikrofon auch nach Ende der Runde weiter festhält. bytes(indata) kopiert, statt zu aliasen, denn PortAudio verwendet diesen Puffer für den nächsten Callback wieder.
Beachte, dass die Samples nie die Festplatte berühren. /audio/transcriptions braucht einen dateiförmigen Upload, aber „dateiförmig” heißt nur, dass ein WAV-Header dran sein muss, und den können wir im Speicher aufsetzen:
wave ist in der Standardbibliothek, und die Bytes gehen direkt an das file=-Argument, das wir vorhin eingerichtet haben.
Die Wiedergabe ist ein Stream pro Antwort, sodass aufeinanderfolgende Sätze als durchgehende Sprache ineinander übergehen, statt das Gerät jedes Mal neu zu starten:
_pending-Puffer ist das eine Detail hier, das dich beißt, wenn du es weglässt. HTTP-Chunk-Grenzen haben nichts mit Sample-Grenzen zu tun, ein 4096-Byte-Read kann dir also eine ungerade Anzahl Bytes reichen und ein 16-Bit-Sample in der Mitte teilen. Schreib das ans Gerät, und jedes folgende Sample ist byteverschoben, was wie das Audio-Äquivalent von Rauschen klingt. Also schreiben wir immer nur eine gerade Anzahl Bytes und tragen das übrige Byte in den nächsten Aufruf.
Die vollständige Klasse im Repo hat außerdem abort() für Ctrl+C — Gerät sofort stoppen, Gepuffertes verwerfen — und close() für den normalen Pfad, das das letzte Teil-Sample flusht (mit einem Null-Byte aufgefüllt) und dann wartet, bis das Gerät fertig abgespielt hat, was es schon hat. Diese beiden zu vertauschen bedeutet, entweder das letzte Wort jeder Antwort abzuschneiden oder eine nicht unterbrechen zu können.
PortAudio ist hier die Portabilitätsschicht, dieselbe
audio.py läuft also auf macOS, Windows und Linux. Nichts in venice.py weiß oder kümmert sich darum, welches davon.Stream und Wiedergabe überlappen
Hier zahlt sich das Streaming tatsächlich aus. Wenn wir den Chat-Stream leeren und Audio auf demselben Thread abspielen, blockiert die Wiedergabe die Schleife, und die verbleibenden Tokens des Modells liegen ungelesen in einem Socket-Puffer. Also leeren wir den Stream auf einem Seiten-Thread und reichen Sätze über eine Queue weiter:BaseException statt Exception bedeutet, dass ein KeyboardInterrupt innerhalb des Streams trotzdem den Aufrufer erreicht.
Nun die Runde selbst: Sätze abholen, jeden ausgeben und sein PCM an den Player füttern, sobald es ankommt.
raise_on_error=not failed bedeutet: Wenn die Runde ohnehin schon fehlschlägt, bauen wir die Wiedergabe leise ab, statt einen zweiten Fehler auf den eigentlichen zu stapeln.
Die Zeit bis zum ersten Audio auszugeben ist eine Kleinigkeit, die beim Tuning genuin nützlich ist. Es ist die Zahl, die der Nutzer spürt.
Die Prompt-Schleife
Alles, was übrig bleibt, ist einwhile True um input():
warmup-Aufruf verdient sein Geld. Er listet Modelle und sendet eine Ein-Wort-TTS-Probe, was die TLS-Verbindung aufbaut und Schlüssel und Stimme validiert, bevor die erste echte Runde des Nutzers beginnt statt währenddessen:
Ausführen
reset für ein neues Gespräch, q zum Beenden. Ctrl+C während einer Antwort stoppt die Wiedergabe und bringt dich zurück zum Prompt, statt das Programm zu beenden.
Ein paar Varianten:
AUDIO_SOURCE / AUDIO_SINK ein:
Was bei der Latenz zu erwarten ist
Die Pipeline besteht aus drei sequenziellen Requests, die Zahlen stapeln sich also ungefähr so:
Rechne mit etwa einer Sekunde bis zum ersten Audio auf einer guten Verbindung. Zwei Dinge dominieren diese Zahl: ob TTS mit dem ersten Satz startet oder auf die ganze Antwort wartet, und ob das Modell Tokens mit Denken verbrennt, bevor es spricht. Streaming auf Satzebene und
disable_thinking sind die zwei Änderungen hier, die du bemerken würdest, wenn du sie entfernst.
Wenn du es schneller willst, halte die Antworten kurz — der erste Satz ist es, der die gefühlte Reaktionsfähigkeit bestimmt — und probiere ein Chat-Modell der flash-Klasse. Mehr dazu steht in den LiveKit-Latenz-Anmerkungen.
Anmerkungen zur Privatsphäre
Es lohnt sich, explizit zu machen, was die Maschine verlässt, denn diese hier hat ein Mikrofon. Audio geht an Venice, um transkribiert zu werden, und Text kommt zurück, um gesprochen zu werden; beides fällt unter Venice’ Zero-Data-Retention-Richtlinie, und nach dem Request wird auf deren Seite nichts gespeichert. Lokal wird überhaupt nichts auf die Festplatte geschrieben — die Aufnahme wird in einer Liste zusammengesetzt, im Speicher in einen WAV-Header gepackt und dem Request übergeben, es gibt also keine Temp-Datei, die leaken oder aufgeräumt werden müsste. Der API-Schlüssel wird aus der Umgebung gelesen und nie ausgegeben. Die Gesprächshistorie lebt nur im Speicher und verschwindet, wenn du das Programm beendest oderreset tippst.
Siehe Privatsphäre für die Stufen pro Modell, wenn du eine stärkere Garantie als Zero Retention brauchst.
Zum Abschluss
Das Mitnehmenswerte: Ein Voice-Agent auf Venice ist drei OpenAI-kompatible Endpunkte, zwei davon gestreamt. Alles andere in diesem Projekt — der Satz-Splitter, die Audio-Streams, die Queue — existiert nur, damit sich diese drei Aufrufe wie ein Gespräch anfühlen.venice.py ist der Teil, der es wert ist, gestohlen zu werden. Tausche app.py gegen einen Web-Handler oder eine Telefon-Integration, und die API-Schicht ändert sich nicht.
Ein paar Dinge, die sich als Nächstes lohnen:
Gib ihm Tools
Füge dem Chat-Schritt Function Calling hinzu, und der Agent kann mitten im Gespräch Dinge nachschlagen.
Lass ihn suchen
Setze
enable_web_search in venice_parameters, und Antworten sind nicht mehr auf Trainingsdaten beschränkt.Klone eine Stimme
Tausche die Kokoro-Voice-ID gegen eine, die du selbst geklont hast.
Bring ihn in einen Raum
Übergib dieselben drei Schritte an LiveKit für VAD, Barge-in und Anrufe mit mehreren Teilnehmern.