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_…
Gardez la clé secrète

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/ogg ou audio/flac.
  • Formulaire : multipart/form-data, le fichier dans le champ file.

Paramètres facultatifs

Dans l'adresse (?language=fr-FR&vocabulary=Kapsuls,UCA) ou comme champs du formulaire.

ParamètreContenu
languageLangue parlée, code BCP-47 (fr-FR, en-US…). Sans elle, la langue est détectée seule.
vocabularyTermes à 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/stream avec l'en-tête Authorization: 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

ChampContenu
languagesLangues parlées, codes BCP-47 (["fr-FR"]), 4 au plus. Sans elles, la langue est détectée phrase par phrase.
vocabularyTermes à bien reconnaître, 100 au plus.
maxMinutesDuré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, puis end, puis la connexion se ferme.
  • {"type":"pause"} : termine tout de suite la phrase en cours (bouton « parler » relâché).

Recevoir le texte

MessageContenu
readyLe 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) :

ChampContenu
textLe texte à dire, de 1 à 1000 caractères. Obligatoire.
voiceKore (par défaut) ou Aoede : voix féminines. Charon ou Puck : voix masculines.
formatweb (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).

Mettez en cache

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=Kore avec l'en-tête Authorization: Bearer okto_key_….
  • Depuis un navigateur ou une app : votre serveur appelle POST https://voice.okto.it/v1/stream avec { "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

MessageContenu
{"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

MessageContenu
ready{ id, mode: "say", rate: 24000, voice, maxMinutes }.
audio_start{ id, rate: 24000 } : une phrase commence.
trames binairesLa 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" }
CodeCauseQue faire
400Paramètre invalide (texte vide ou trop long, voix inconnue, type de fichier non pris en charge…).Corriger la requête.
401Clé 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.
402Cré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.
413Fichier trop grand (20 Mo) ou trop long (10 minutes).Découper l'audio.
415Format 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).
423Espace suspendu.Voir avec la personne qui gère l'espace.
429Trop de requêtes, ou plafond du jour de la clé atteint.Ralentir et réessayer plus tard.
502, 503Moteur 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 :

RoutePrixCe qui compte
/v1/transcribe0,02 € par minuteLa durée de l'audio envoyé.
/v1/stream0,02 € par minuteLa durée de parole (les silences ne comptent pas).
/v1/say0,04 € par minuteLa durée de la voix produite.
/v1/stream?mode=say0,04 € par minuteLa 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.