Cookbook

End-to-end recipes for the patterns developers integrate most often. Each example uses the public /v1/asr endpoints and works for any supported language by swapping localeCode.

1. Transcribe a recorded file

The simplest recipe: send a WAV/MP3/OGG/WebM file to the multipart endpoint and read back the transcript.

import requests

with open("interview.mp3", "rb") as f:
    r = requests.post(
        "https://api.phonetico.ai/v1/asr/upload",
        headers={"Authorization": "Bearer pk_live_your_key_here"},
        files={"audio": f},
        data={"localeCode": "amh"},
    )

r.raise_for_status()
print(r.json()["transcript"])

2. Generate SRT or VTT captions

Ask for word timestamps, then group words into caption cues. The example below produces SRT cues capped at ~6 seconds and ~42 characters. Tweak to taste.

import requests, base64, datetime

with open("video.wav", "rb") as f:
    audio_b64 = base64.b64encode(f.read()).decode()

r = requests.post(
    "https://api.phonetico.ai/v1/asr",
    headers={"Authorization": "Bearer pk_live_your_key_here"},
    json={"audio": audio_b64, "localeCode": "amh", "wordTimestamps": True},
)
words = r.json()["wordTimestamps"]

def fmt(t):
    td = datetime.timedelta(seconds=t)
    return str(td)[:-3].replace(".", ",").rjust(12, "0")

cues, cur = [], []
for w in words:
    cur.append(w)
    span = cur[-1]["end"] - cur[0]["start"]
    text = " ".join(x["word"] for x in cur)
    if span > 6 or len(text) > 42:
        cues.append(cur); cur = []
if cur: cues.append(cur)

for i, cue in enumerate(cues, 1):
    print(f"{i}\n{fmt(cue[0]['start'])} --> {fmt(cue[-1]['end'])}")
    print(" ".join(w["word"] for w in cue) + "\n")

3. Voice-note app

Capture audio in the browser and POST it. Use MediaRecorder for the recording and the multipart endpoint for the upload. No transcoding step needed.

TypeScript
async function recordAndTranscribe(localeCode = "amh") {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const recorder = new MediaRecorder(stream);
  const chunks: Blob[] = [];

  recorder.ondataavailable = (e) => chunks.push(e.data);
  recorder.start();
  await new Promise((r) => setTimeout(r, 5000)); // record 5s
  recorder.stop();

  await new Promise<void>((r) => (recorder.onstop = () => r()));
  const blob = new Blob(chunks, { type: "audio/webm" });

  const form = new FormData();
  form.append("audio", blob, "note.webm");
  form.append("localeCode", localeCode);

  const r = await fetch("https://api.phonetico.ai/v1/asr/upload", {
    method: "POST",
    headers: { Authorization: "Bearer pk_live_your_key_here" },
    body: form,
  });
  const { transcript } = await r.json();
  return transcript;
}

4. Batch a folder of recordings

Concurrency-limited fan-out. Cap parallelism so you don't hit the rate limit; on 429, back off and retry.

import os, asyncio, httpx, pathlib

API = "https://api.phonetico.ai/v1/asr/upload"
KEY = "pk_live_your_key_here"
SEM = asyncio.Semaphore(8)  # tune to your rate limit

async def transcribe(client, path):
    async with SEM:
        for attempt in range(5):
            with open(path, "rb") as f:
                r = await client.post(
                    API,
                    headers={"Authorization": f"Bearer {KEY}"},
                    files={"audio": (path.name, f, "audio/wav")},
                    data={"localeCode": "amh"},
                    timeout=120,
                )
            if r.status_code == 429:
                await asyncio.sleep(2 ** attempt)
                continue
            r.raise_for_status()
            return path.name, r.json()["transcript"]

async def main():
    files = list(pathlib.Path("recordings").glob("*.wav"))
    async with httpx.AsyncClient() as client:
        results = await asyncio.gather(*(transcribe(client, p) for p in files))
    for name, text in results:
        print(name, "→", text[:80])

asyncio.run(main())

5. Click-to-seek transcript player

Render each word as a clickable span; on click, seek the audio element to that word's start. Keep the wordTimestamps array alongside the audio source.

TypeScript
// Assumes you've already fetched { transcript, wordTimestamps } from /v1/asr.
function TranscriptPlayer({
  audioUrl,
  words,
}: {
  audioUrl: string;
  words: { word: string; start: number; end: number }[];
}) {
  const ref = React.useRef<HTMLAudioElement>(null);
  const seek = (t: number) => {
    if (!ref.current) return;
    ref.current.currentTime = t;
    ref.current.play();
  };
  return (
    <div>
      <audio ref={ref} src={audioUrl} controls />
      <p className="leading-loose">
        {words.map((w, i) => (
          <span
            key={i}
            onClick={() => seek(w.start)}
            className="cursor-pointer hover:bg-gold-subtle"
          >
            {w.word}{" "}
          </span>
        ))}
      </p>
    </div>
  );
}

Synthesize text with a preset voice

The fastest TTS path: pick a voice from GET /v1/tts/voices and submit text. No reference audio, no attestation. Tigrinya only today; voice tir_phonetico_01 is seeded.

TypeScript
import { writeFileSync } from 'fs';

const resp = await fetch('https://api.phonetico.ai/v1/tts', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer pk_live_your_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    text: 'ምስ መኪና ስለዘላተሙኒ ውነይ ስሒተ።',
    language: 'tir',
    voice: { preset: 'tir_phonetico_01' },
    options: { qualityTier: 'balanced' },
  }),
});

const { audio } = await resp.json();
writeFileSync('out.wav', Buffer.from(audio, 'base64'));

Handle device-key quota cleanly

Mobile clients on a free-tier device key (dk_live_) get a monthly character cap (1,500 by default). When the cap is hit the call returns 429 with code: quota_exceeded. Route the user to your upgrade flow on that code, and treat 402 (developer-key balance) as a separate signal. Live caps come from GET /v1/tts/limits. Fetch on launch and trust the response over hardcoded copies.

TypeScript
async function synthesize(text: string): Promise<Blob | 'upgrade'> {
  const resp = await fetch('https://api.phonetico.ai/v1/tts', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer dk_live_your_device_key',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      text,
      language: 'tir',
      voice: { preset: 'tir_phonetico_01' },
      options: { qualityTier: 'fast' },
    }),
  });

  // Quota or premium-tier gates → route to upgrade UI.
  if (resp.status === 429 || resp.status === 403) {
    const { code } = await resp.json();
    if (
      code === 'quota_exceeded' ||
      code === 'daily_limit_reached' ||
      code === 'quality_tier_premium_only'
    ) {
      return 'upgrade';
    }
  }

  if (!resp.ok) throw new Error(`tts failed: ${resp.status}`);

  const { audio } = await resp.json();
  const bytes = Uint8Array.from(atob(audio), (c) => c.charCodeAt(0));
  return new Blob([bytes], { type: 'audio/wav' });
}

Synthesize text in a custom voice

Clone a voice from a short reference clip and synthesize Tigrinya text in that voice. The per-key rights attestation must be set first (API Keys); the first call from an unattested key returns 412.

TypeScript
import { readFileSync, writeFileSync } from 'fs';

const refAudio = readFileSync('voice-sample.wav').toString('base64');

const resp = await fetch('https://api.phonetico.ai/v1/tts', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer pk_live_your_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    text: 'ምስ መኪና ስለዘላተሙኒ ውነይ ስሒተ።',
    language: 'tir',
    voice: {
      reference: {
        audio: refAudio,
        transcript: 'exact transcript of voice-sample.wav',
        rightsAttestation: true,
      },
    },
    options: { qualityTier: 'fast' },
  }),
});

const { audio } = await resp.json();
writeFileSync('out.wav', Buffer.from(audio, 'base64'));

Quality tiers: fast (16 steps, 1× cost) for prototyping, balanced (32 steps, 2×) for production, quality (48 steps, 3×) for final renders. Switch via options.qualityTier.

Want a recipe added? Tell us what you're building.