API voice.
La voix en texte, d'un fichier ou en direct, et le texte en voix. Pour les clés développeur d'un espace okto.it.
Adresse de l'API : https://voice.okto.it. Pour une app Expo ou React Native, le guide pas à pas reprend tout ceci avec du code prêt à copier.
La clé
Une clé développeur ressemble à ceci : okto_key_<12 caractères hexadécimaux>_<secret>.
- Elle se crée sur okto.it/space/keys (aussi par voice.okto.it/keys, qui y renvoie). Il faut être propriétaire ou gestionnaire de l'espace.
- Elle appartient à un espace. Ses appels sont payés par le crédit de cet espace.
- On coche les services qu'elle peut appeler. Pour l'instant :
voice. - Elle ne s'affiche qu'une fois, à la création. Perdue ? Révoquez-la et créez-en une autre.
Chaque requête la porte dans l'en-tête :
Authorization: Bearer okto_key_…
Jamais dans un dépôt git, jamais dans une page web publique. Dans une app mobile, elle peut être extraite : c'est acceptable pour un prototype, pas pour une app publiée. Voir la sécurité dans le guide Expo.
Transcrire : voix → texte
POST https://voice.okto.it/v1/transcribe
Envoyez le fichier audio dans le corps, de l'une de ces deux façons :
- Brut : le fichier tel quel, avec son type en
Content-Type:audio/mp4,audio/m4a,audio/aac,audio/mpeg,audio/wav,audio/webm,audio/oggouaudio/flac. - Formulaire :
multipart/form-data, le fichier dans le champfile.
Paramètres facultatifs
Dans l'adresse (?language=fr-FR&vocabulary=Kapsuls,UCA) ou comme champs du formulaire.
| Paramètre | Contenu |
|---|---|
language | Langue parlée, code BCP-47 (fr-FR, en-US…). Sans elle, la langue est détectée seule. |
vocabulary | Termes à bien reconnaître, séparés par des virgules : noms propres, produits, sigles. 100 au plus. |
Limites
20 Mo et 10 minutes d'audio par fichier. Au-delà : 413.
Exemples
curl "https://voice.okto.it/v1/transcribe?language=fr-FR&vocabulary=Kapsuls,UCA" \
-H "Authorization: Bearer $OKTO_KEY" \
-H "Content-Type: audio/mp4" \
--data-binary @note.m4a
curl https://voice.okto.it/v1/transcribe \
-H "Authorization: Bearer $OKTO_KEY" \
-F "file=@note.m4a;type=audio/mp4" \
-F "language=fr-FR" \
-F "vocabulary=Kapsuls,UCA"
Réponse
{ "text": "Bonjour à tous, on commence le cours.", "seconds": 12.4 }
seconds : la durée de l'audio, celle qui est facturée.
En direct : transcrire pendant qu'on parle
WS wss://voice.okto.it/v1/stream
Une connexion WebSocket reste ouverte : votre app envoie le micro au fil de l'eau, voice renvoie le texte au fil de la parole, provisoire puis définitif. Pour des sous-titres, une dictée, une commande vocale.
Ouvrir le flux
- Depuis un serveur : ouvrez
wss://voice.okto.it/v1/streamavec l'en-têteAuthorization: Bearer okto_key_…. Réglages dans l'adresse :?languages=fr-FR&vocabulary=Kapsuls,UCA&maxMinutes=30. - Depuis un navigateur ou une app : la clé reste sur votre serveur. Il demande une adresse de flux, puis la passe à la page ou à l'app, qui s'y connecte. L'adresse sert une seule fois et vaut une minute.
POST https://voice.okto.it/v1/stream
| Champ | Contenu |
|---|---|
languages | Langues parlées, codes BCP-47 (["fr-FR"]), 4 au plus. Sans elles, la langue est détectée phrase par phrase. |
vocabulary | Termes à bien reconnaître, 100 au plus. |
maxMinutes | Durée maximale du flux, 60 minutes au plus (c'est aussi la valeur par défaut). |
{ "id": "…", "url": "wss://voice.okto.it/v1/stream/…?key=…", "expiresIn": 60, "maxMinutes": 60 }
Envoyer l'audio
Des trames binaires : PCM 16 bits signé, little-endian, mono, 16 kHz, en temps réel, de 20 à 100 ms chacune. Envoyez tout, silences compris : voice repère lui-même les phrases. Messages texte possibles :
{"type":"stop"}: fin. Le texte de la dernière phrase arrive encore, puisend, puis la connexion se ferme.{"type":"pause"}: termine tout de suite la phrase en cours (bouton « parler » relâché).
Recevoir le texte
| Message | Contenu |
|---|---|
ready | Le flux est prêt : { id, rate: 16000, maxMinutes, languages }. |
text | { id, text, final, at } : une phrase. Le texte provisoire arrive environ toutes les demi-secondes, puis le définitif (final: true), avec le même id : remplacez le texte de la phrase, ne l'ajoutez pas. |
usage | { seconds } : parole décomptée depuis le début du flux. |
error | { status, message, action? } : mêmes codes que les erreurs HTTP. Refus à l'ouverture : la connexion se ferme ensuite avec le code 4000 + statut (4401, 4402, 4429…). |
end | { reason, seconds }, puis fermeture. reason : client (votre stop), limit (durée maximale), idle (plus aucune trame depuis 2 minutes), credit (crédit épuisé pendant le flux), refused, restart (maintenance : rouvrez un flux). |
Exemples
// La clé reste ici. Renvoyez `url` à la page ou à l'app qui va parler.
app.post("/api/voice-stream", async (req, res) => {
const r = await fetch("https://voice.okto.it/v1/stream", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.OKTO_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ languages: ["fr-FR"] }),
});
res.status(r.status).json(await r.json());
});
class Pcm16k extends AudioWorkletProcessor {
constructor() { super(); this.ratio = sampleRate / 16000; this.t = 0; this.buf = new Int16Array(320); this.n = 0; }
process(inputs) {
const ch = inputs[0] && inputs[0][0];
if (!ch) return true;
for (; this.t < ch.length; this.t += this.ratio) {
const i = Math.floor(this.t), a = ch[i], b = i + 1 < ch.length ? ch[i + 1] : a;
this.buf[this.n++] = Math.max(-1, Math.min(1, a + (b - a) * (this.t - i))) * 0x7fff;
if (this.n === 320) { this.port.postMessage(this.buf.buffer, [this.buf.buffer]); this.buf = new Int16Array(320); this.n = 0; }
}
this.t -= ch.length;
return true;
}
}
registerProcessor("pcm16k", Pcm16k);
const { url } = await (await fetch("/api/voice-stream", { method: "POST" })).json();
const ws = new WebSocket(url);
const phrases = new Map(); // id → texte, remplacé à chaque message
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.type === "text") { phrases.set(m.id, m.text); render([...phrases.values()].join(" ")); } // render : votre affichage
if (m.type === "error") console.warn(m.status, m.message);
};
await new Promise((ok) => (ws.onopen = ok));
const mic = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true } });
const ctx = new AudioContext();
await ctx.audioWorklet.addModule("/pcm16k.js");
const node = new AudioWorkletNode(ctx, "pcm16k");
node.port.onmessage = (e) => ws.readyState === WebSocket.OPEN && ws.send(e.data);
const mute = ctx.createGain(); mute.gain.value = 0; // le nœud doit aller jusqu'à la sortie pour tourner
ctx.createMediaStreamSource(mic).connect(node).connect(mute).connect(ctx.destination);
// Fin : couper le micro, puis laisser arriver la dernière phrase
function stop() { mic.getTracks().forEach((t) => t.stop()); ctx.close(); ws.send(JSON.stringify({ type: "stop" })); }
Décompte
Seule la parole est décomptée, au prix de la transcription : les silences et les bruits brefs ne partent pas chez le moteur et ne coûtent rien. Le décompte se fait minute par minute pendant le flux : si le crédit de l'espace s'épuise, vous recevez error (402, avec action), puis end avec reason: "credit". Trois flux ouverts en même temps par clé au plus.
Faire parler : texte → voix
POST https://voice.okto.it/v1/say
Corps JSON (Content-Type: application/json) :
| Champ | Contenu |
|---|---|
text | Le texte à dire, de 1 à 1000 caractères. Obligatoire. |
voice | Kore (par défaut) ou Aoede : voix féminines. Charon ou Puck : voix masculines. |
format | web (par défaut) : 24 kHz, pour une app ou une page. phone : 8 kHz, qualité téléphone. |
Exemples
curl https://voice.okto.it/v1/say \
-H "Authorization: Bearer $OKTO_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Bonjour, ici la voix okto.it.", "voice": "Kore" }' \
-o bonjour.wav
curl https://voice.okto.it/v1/say \
-H "Authorization: Bearer $OKTO_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Votre rendez-vous est confirmé.", "voice": "Charon", "format": "phone" }' \
-o confirme.wav
Réponse
200, Content-Type: audio/wav : un fichier WAV, PCM 16 bits mono, à 24 kHz (web) ou 8 kHz (phone).
voice ne garde rien. Chaque appel est une nouvelle synthèse, payée. Une phrase qui revient (accueil, consigne) se garde côté app et se rejoue.
/v1/say rend le fichier entier : comptez 2 s pour un mot, 5 s pour trois phrases. Pour répondre vite (assistant, dialogue), utilisez la voix en direct : le son commence en moins d'une seconde.
Faire parler en direct : le son au fil de l'eau
WS wss://voice.okto.it/v1/stream?mode=say
Le même WebSocket que la transcription en direct, dans l'autre sens : votre app envoie du texte, voice renvoie la voix pendant qu'elle se fabrique. Premier son en 0,7 à 0,9 s, quelle que soit la longueur du texte, et la voix se fabrique 3 à 4 fois plus vite qu'elle ne se joue : jouez-la au fur et à mesure, sans trou.
Ouvrir le flux
- Depuis un serveur :
wss://voice.okto.it/v1/stream?mode=say&voice=Koreavec l'en-têteAuthorization: Bearer okto_key_…. - Depuis un navigateur ou une app : votre serveur appelle
POST https://voice.okto.it/v1/streamavec{ "mode": "say", "voice": "Kore" }et passe l'adresse rendue à la page, comme pour la transcription.
voice : Kore (par défaut), Aoede, Charon ou Puck.
Envoyer le texte
| Message | Contenu |
|---|---|
{"type":"say","text":"…","id":"q1"} | Une phrase à dire, 1000 caractères au plus. id (facultatif) revient dans les messages de cette phrase. voice possible pour changer de voix. Les phrases sont dites dans l'ordre, une à la fois ; 20 en attente au plus. |
{"type":"cancel"} | Coupe la phrase en cours et oublie les suivantes : la personne a repris la parole. |
{"type":"stop"} | Dit les phrases en attente, puis end et fermeture. |
Recevoir la voix
| Message | Contenu |
|---|---|
ready | { id, mode: "say", rate: 24000, voice, maxMinutes }. |
audio_start | { id, rate: 24000 } : une phrase commence. |
| trames binaires | La voix : PCM 16 bits signé, little-endian, mono, 24 kHz, environ 60 ms par trame, dans l'ordre. |
audio_end | { id, seconds, cancelled? } : la phrase est complète (ou coupée par cancel). |
error | { id?, status, message, action? } : une phrase refusée (texte vide, file pleine, moteur indisponible…). 402 ou 423 : le flux se ferme ensuite. |
end | { reason, seconds }, puis fermeture : client, credit, refused, limit, idle (2 minutes sans message). |
Exemple : la page joue la voix au fil de l'eau
app.post("/api/voice-say", async (req, res) => {
const r = await fetch("https://voice.okto.it/v1/stream", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.OKTO_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ mode: "say", voice: "Kore" }),
});
res.status(r.status).json(await r.json());
});
const { url } = await (await fetch("/api/voice-say", { method: "POST" })).json();
const ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
const ctx = new AudioContext(); // à créer après un clic (règle des navigateurs)
let at = 0; // heure de fin du son déjà programmé
const playing = new Set();
ws.onmessage = (e) => {
if (typeof e.data !== "string") {
const pcm = new Int16Array(e.data);
const buf = ctx.createBuffer(1, pcm.length, 24000);
const ch = buf.getChannelData(0);
for (let i = 0; i < pcm.length; i++) ch[i] = pcm[i] / 0x8000;
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
at = Math.max(at, ctx.currentTime + 0.05);
src.start(at);
at += buf.duration;
playing.add(src);
src.onended = () => playing.delete(src);
return;
}
const m = JSON.parse(e.data);
if (m.type === "error") console.warn(m.status, m.message);
};
await new Promise((ok) => (ws.onopen = ok));
function say(text) { ws.send(JSON.stringify({ type: "say", text })); }
// La personne coupe la parole : voice arrête la phrase, la page arrête ce qui est déjà programmé
function interrupt() { ws.send(JSON.stringify({ type: "cancel" })); playing.forEach((s) => s.stop()); at = 0; }
Décompte
La voix produite, phrase par phrase, au prix de la synthèse (0,04 € par minute) ; une phrase coupée compte ce qui a déjà été produit. Avant chaque phrase, voice vérifie le crédit de l'espace : s'il manque, error 402 (avec action) puis end avec reason: "credit". Chaque phrase compte comme une requête dans le plafond de la clé.
Erreurs
En cas d'erreur, la réponse est en JSON, avec un message en français, à montrer tel quel si besoin :
{ "error": "message en français" }
| Code | Cause | Que faire |
|---|---|---|
400 | Paramètre invalide (texte vide ou trop long, voix inconnue, type de fichier non pris en charge…). | Corriger la requête. |
401 | Clé absente, invalide ou révoquée, ou service voice non coché sur la clé. | Vérifier la clé et ses services sur okto.it/space/keys. |
402 | Crédit de l'espace insuffisant. La réponse porte aussi action : l'adresse où recharger. | Recharger le crédit, ou envoyer la personne vers action. |
413 | Fichier trop grand (20 Mo) ou trop long (10 minutes). | Découper l'audio. |
415 | Format audio non reconnu. | Envoyer du m4a, mp4, aac, mp3, wav, webm, ogg ou flac, avec le bon Content-Type (ou un nom de fichier avec son extension). |
423 | Espace suspendu. | Voir avec la personne qui gère l'espace. |
429 | Trop de requêtes, ou plafond du jour de la clé atteint. | Ralentir et réessayer plus tard. |
502, 503 | Moteur de voix indisponible. | Réessayer après une courte pause. |
{ "error": "message en français", "action": "adresse où recharger le crédit" }
Prix et plafond
Décomptés du crédit de l'espace, à la seconde :
| Route | Prix | Ce qui compte |
|---|---|---|
/v1/transcribe | 0,02 € par minute | La durée de l'audio envoyé. |
/v1/stream | 0,02 € par minute | La durée de parole (les silences ne comptent pas). |
/v1/say | 0,04 € par minute | La durée de la voix produite. |
/v1/stream?mode=say | 0,04 € par minute | La durée de la voix produite, phrase par phrase. |
Plafond de sécurité : 30 minutes par clé et par jour, transcription (fichier et direct) et synthèse réunies. Atteint : 429 jusqu'au lendemain. Réglable sur demande, par le formulaire d'okto.it. Formules et recharges : okto.it/pricing.
Données
voice ne conserve ni l'audio ni le texte. Ils sont traités par le moteur Gemini de Google, en mode sans rétention et sans entraînement. Seules les durées sont comptées, pour la facturation et le plafond.
Votre app traite la voix de personnes : dites-leur quand le micro écoute, et ce que vous gardez de leurs paroles.