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.
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.
// 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.
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.
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.
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.