speech-api

Speech-to-text, text-to-speech, AI text analysis, translation, vision and account management — one backend, one key.

Base URL

https://speech-api.shahzaibkashan88.workers.dev

Every endpoint below is a path appended to this. GET /health needs no key and reports which features are on.

Authentication

Send an Authorization: Bearer … header on every request. Two token types work:

TokenLooks likeUse it for
API keysk_…Servers, scripts, integrations. Created in the admin console or via /admin/keys. Never embed in a public frontend or mobile app.
Access token (JWT)eyJ…Humans logging in. Get one from /auth/login; expires in 1 hour; renew with /auth/refresh.

Every key/user has a role: client can call every /v1/* endpoint and see its own usage; admin can also call /admin/*.

Requests & errors

JSON bodies need content-type: application/json. File uploads (audio, images) are multipart/form-data with the file in a field named file. Successful JSON responses include model and latency_ms. Binary endpoints (audio, image) return raw bytes with x-model / x-latency-ms headers.

Error shape
{ "error": "text is required" }               → 400
{ "error": "unauthorized — send Authorization: Bearer " } → 401
{ "error": "admin only" }                      → 403
{ "error": "analysis failed", "detail": "…" }  → 502 (model/upstream failure)
{ "error": "search not configured" }           → 503 (feature off)
Free-tier limit: 10,000 Workers AI Neurons/day shared across all endpoints. Roughly 1–2 hours of transcription or a few hundred LLM calls. Over the cap, calls fail until the daily reset.

Auth

POST/auth/register

Create a user account. The very first account ever registered becomes admin automatically; everyone after that needs an invite code from an admin. No auth header needed.

FieldTypeNotes
emailstringrequired
passwordstringmin 8 chars
invitestringrequired unless first user
Returns
{ "user": {"id", "email", "role"}, "api_key": "sk_…", "access_token", "refresh_token", "expires_in": 3600 }

POST/auth/login

Body {email, password}{user, access_token, refresh_token, expires_in}. 5 failed attempts locks the email for 15 minutes.

POST/auth/refresh  ·  POST/auth/logout

Both take {refresh_token}. Refresh returns a fresh token pair (and invalidates the old refresh token); logout just invalidates it.

GET/auth/me

Who the current token belongs to: {principal: {label, role, key_id}, user}. Handy for a "test my key" button.

GETPOST/auth/keys  ·  DELETE/auth/keys/:id

Manage your own API keys (JWT login required for create). POST body {label}; the key is returned once.

Speech

POST/v1/transcribe

Audio → text. Whisper large-v3-turbo. Urdu, English, Punjabi, Sindhi, Pashto and code-switched speech.

Field (multipart)Notes
fileaudio, ≤ 25 MB (mp3, wav, m4a, webm, ogg…)
languageoptional but always send ur for Urdu — auto-detect often picks Hindi script
Returns
{ "text": "…", "language": "ur", "duration": 42.3,
  "segments": [{ "start": 0, "end": 2.9, "text": "…", "words": [...] }] }
curl -X POST $URL/v1/transcribe -H "Authorization: Bearer $KEY" \
  -F file=@call.mp3 -F language=ur

POST/v1/synthesize Urdu off

Text → MP3 audio. English works now (Deepgram Aura). Urdu returns 503 until Azure Speech secrets are configured.

Field (JSON)Notes
text≤ 2,000 chars
languageen (default) or ur
voiceen: asteria, luna, stella, athena, hera, orion, arcas, perseus, angus, orpheus, helios, zeus · ur: uzma, asad
curl -X POST $URL/v1/synthesize -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" -d '{"text":"Hello farmer","voice":"orion"}' -o out.mp3

Call & transcript intelligence

POST/v1/analyze

The all-in-one call analyser. Send audio (it transcribes first) or a transcript. Tuned for Pakistani farmer / agri-advisor conversations.

InputFields
multipartfile, language?, context?
JSON{ text, language?, context? }

context steers the analysis — if it mentions "QA", "agent" or "call centre", the response also includes qa_scores.

Returns
{ "id": 12, "source": "text", "language": "ur", "transcript": "(audio only)",
  "analysis": {
    "summary": "…", "language_mix": ["Urdu","English"],
    "sentiment": { "label": "positive", "score": 0.8 },
    "intent": "purchase | dealer_enquiry | advisory | complaint | other",
    "entities": { "crops": [], "products": [], "pests_or_issues": [], "locations": [], "names": [], "phone_numbers": [] },
    "action_items": ["…"],
    "qa_scores": { "greeting": 5, "listening": 4, "resolution": 5, "closing": 4, "notes": "…" }   // only with QA context
  } }

POST/v1/qa-score

Dedicated contact-centre QA scoring against a rubric. Returns 1–5 per item with quoted evidence, an overall 0–100, strengths, improvements and compliance flags (wrong dosage, unsafe advice, rudeness, missed callback).

Field (JSON)Notes
textthe transcript
rubricoptional array of item names. Default: greeting, identity_verification, active_listening, product_knowledge, resolution, empathy_tone, closing
instructionsoptional extra guidance for the evaluator

Text intelligence

All take JSON with a text field (≤ 12,000 chars) and run on Llama 3.3 70B.

POST/v1/translate

FieldNotes
text≤ 5,000 chars
target_langur, en, pa, sd, ps, skr (Saraiki), bal, hno (Hindko), ur-roman, ar, hi
source_langoptional; auto-detects if omitted
enginellm (default, keeps agri terms) or m2m100 (faster, cheaper)
→ { "translated_text": "…", "source_lang": "Urdu", "target_lang": "en", "engine": "llm" }

POST/v1/summarize

{text, language?, length?}{summary, key_points[]}. length is free text like "2 sentences" or "one paragraph".

POST/v1/classify

{text, labels: ["…"], instructions?}{label, confidence, reason}. Picks exactly one of your labels.

POST/v1/extract

{text, schema: <JSON Schema object>, instructions?}{data: {…}} matching your schema. Missing fields come back as empty string / 0, never invented.

{ "text": "…", "schema": { "type": "object", "properties": {
    "crop": { "type": "string" }, "product": { "type": "string" }, "dose": { "type": "string" } } } }

POST/v1/moderate

{text}{flagged, categories[], severity, reason}. Categories: abuse, harassment, spam, scam, sexual, violence, self_harm, pii, off_topic. Ordinary farming questions are not flagged.

POST/v1/chat

Multi-turn assistant with server-side memory. Omit conversation_id on the first call; reuse the returned one to continue (last 20 turns kept).

FieldNotes
messagerequired
conversation_idoptional, from a previous reply
personaoptional system prompt — set who the assistant is and what context it has
max_tokensoptional, default 600
→ { "conversation_id": "c_…", "reply": "…", "turns": 2 }

POST/v1/chat/completions OpenAI-compatible

Drop-in for code already written against OpenAI / Groq / OpenRouter chat completions. Send {model, messages:[{role,content}], max_tokens?, temperature?}; model is accepted and ignored (always Llama 3.3 70B). Stateless — no memory between calls.

→ { "id": "chatcmpl_…", "object": "chat.completion", "model": "…",
    "choices": [{ "index": 0, "message": { "role": "assistant", "content": "…" }, "finish_reason": "stop" }] }

Vision

Image input for describe/ocr: multipart file, or JSON {image_url} or {image_base64, mime?}. ≤ 10 MB. Llama 3.2 11B Vision; expect ~10–15 s per image.

POST/v1/describe-image

Image → description. Default prompt is crop-aware: names the crop, visible symptoms, severity and what an agronomist should check. Override with prompt.

→ { "description": "…" }

POST/v1/ocr

Image → the text in it (Urdu or English script preserved). Labels, receipts, forms.

→ { "text": "…" }

POST/v1/generate-image

{prompt, steps?: 1–8} → image bytes (Flux Schnell, 1024×1024). Save the response body to a file.

curl -X POST $URL/v1/generate-image -H "Authorization: Bearer $KEY" \
  -H "content-type: application/json" -d '{"prompt":"healthy wheat field, illustration"}' -o out.png

Search / knowledge off

These return 503 until Vectorize is enabled. The code is in place; enabling it is a config step, not a rebuild.

POSTGET/v1/documents  ·  DELETE/v1/documents/:id

POST {id?, title?, kind?, language?, text} (≤ 200k chars) chunks and indexes the text for semantic search → {id, chunks}. GET lists your documents; DELETE removes one.

{query, top_k?, kind?, language?, answer?}{results:[{text, score, doc_id}], answer?}. With answer: true it also writes an answer grounded only in the matched passages.

Account & admin

GET/v1/usage

Your own key's totals per endpoint: calls, audio minutes, characters, average latency.

/admin/* admin role only

EndpointWhat it does
GET /admin/usage?days=30Usage for every key + a per-day series
GET · POST /admin/keys · DELETE /admin/keys/:idList all keys; create {label, role} (returned once); revoke
GET /admin/users · DELETE /admin/users/:idList / disable users (also revokes their keys and sessions)
GET · POST /admin/invitesList invites; create {role} → single-use code
GET /admin/analyses?since=&limit=Browse stored /v1/analyze results
GET /admin/health-checkLive test of D1, LLM, translate, embed, Vectorize, Urdu TTS, auth
POST /admin/accept-model-licenseOne-time acceptance for gated models (already done for vision & Flux)

These all have a visual UI in the admin console.

Usage examples

JavaScript (browser backend / Node)
const URL = "https://speech-api.shahzaibkashan88.workers.dev";
const KEY = process.env.SPEECH_API_KEY;

async function analyzeCall(transcript) {
  const r = await fetch(`${URL}/v1/analyze`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "content-type": "application/json" },
    body: JSON.stringify({ text: transcript, language: "ur", context: "call-centre agent QA" })
  });
  if (!r.ok) throw new Error((await r.json()).error);
  return (await r.json()).analysis;
}
Python
import requests
URL = "https://speech-api.shahzaibkashan88.workers.dev"
H = {"Authorization": f"Bearer {KEY}"}

# transcribe a file
with open("call.mp3", "rb") as f:
    t = requests.post(f"{URL}/v1/transcribe", headers=H, files={"file": f}, data={"language": "ur"}).json()

# keep a conversation going
c = requests.post(f"{URL}/v1/chat", headers=H, json={"message": "gandum kab bijain?"}).json()
c2 = requests.post(f"{URL}/v1/chat", headers=H, json={"message": "aur khaad?", "conversation_id": c["conversation_id"]}).json()
cURL — full round trip
URL=https://speech-api.shahzaibkashan88.workers.dev
KEY=sk_…

curl -s $URL/health
curl -s $URL/auth/me -H "Authorization: Bearer $KEY"
curl -s -X POST $URL/v1/translate -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"text":"sufaid makhi kapas per hai","target_lang":"en"}'
curl -s $URL/v1/usage -H "Authorization: Bearer $KEY"
Using it from an existing OpenAI-style client
// Anything that takes a base URL + key for OpenAI-compatible chat:
baseURL: "https://speech-api.shahzaibkashan88.workers.dev/v1"
apiKey:  "sk_…"
// then call chat.completions as usual — model name is ignored.
Timing to design for: transcribe ~1 s per 3 s of audio · text endpoints 1–5 s · chat 1–10 s · vision 10–15 s · analyze on audio = transcribe + ~5 s. Build your UI with a loading state, not an instant-response assumption.