# Create Transcription Job
Source: https://docs.fennec-asr.com/api-reference/create-transcription-job
api-reference/openapi.json post /api/v1/transcribe
Accept an audio file, stream it into GridFS (async), and queue a job
with the resulting GridFS file ID (as a string).
# Create Transcription Job From Url
Source: https://docs.fennec-asr.com/api-reference/create-transcription-job-from-url
post /api/v1/transcribe/url
# Get Transcription Status
Source: https://docs.fennec-asr.com/api-reference/get-transcription-status
get /api/v1/transcribe/status/{job_id}
# Contextual Correction
Source: https://docs.fennec-asr.com/essentials/contextual-correction
Leverage Fennec to correct domain-specific jargon, names, and other contextual errors in your transcripts, achieving near-human accuracy.
Even the best ASR models can struggle with specific terminology, such as company names, project codenames, or technical jargon. The Contextual Correction feature addresses this by performing a secondary analysis pass on your transcript.
By providing a small amount of "context," you can guide the AI to fix these specific errors, dramatically improving the accuracy and professionalism of your final transcript.
## How It Works
This feature is controlled by two parameters you send in your `/transcribe` request. When enabled, the initial transcript is chunked and analyzed by the AI, which uses your provided context to identify and fix errors.
Set this to `true` to enable the secondary AI correction pass. If `false`, this feature is skipped entirely.
A string of text providing hints to the AI. This is where you should include correct spellings of names, products, or technical terms that the ASR might mishear.
**Performance Impact:** Enabling contextual correction adds processing time (latency) and increases the cost of the transcription. Use it when accuracy for specific terms is critical.
## How to Use It
Using the feature is as simple as adding two fields to your `multipart/form-data` request.
### Python SDK Example
```python theme={null}
from fennec_asr import FennecASRClient
asr_client = FennecASRClient(api_key="YOUR_API_KEY")
transcription = asr_client.transcribe_file(
file_path="bartholomew.mp3",
context="The company they are talking about is called Bartholomew Bros, correct any potential misinterpretations in the script.",
apply_contextual_correction=True
)
print(transcription)
```
### Example Result
Contextual correction can fix errors that would be impossible to resolve without external knowledge.
**Transcript:**
```text theme={null}
Alright team, let's sync up on the Q3 project deliverables for Bahamian bureaus. The primary goal is to finalize the user interface mockups by Wednesday.
```
**Context Provided:** `The company they are talking about is called Bartholomew Bros, correct any potential misinterpretations in the script.`
**Transcript:**
```text theme={null}
Alright team, let's sync up on the Q3 project deliverables for Bartholomew Bros. The primary goal is to finalize the user interface mockups by Wednesday.
```
### Deeper Use Cases
This feature is also very useful for dynamic formatting work and subtle adjustments to anything in the text:
**Transcript:**
```text theme={null}
The Fennec fox is a small fox native to the deserts of North Africa, ranging from Western Sahara and Mauritania to the Sinai Peninsula.
Its most distinctive feature is its unusually large ears, which serve to dissipate heat and listen for underground prey.
The fennec is the smallest fox species. Its coat, ears, and kidney functions have adapted to the desert environment with high temperatures and little water.
The Fennec fox mainly eats insects, small mammals, and birds. It has a lifespan of up to fourteen years in captivity and about ten years in the wild.
Pups are preyed upon by the pharaoh eagle owl. Both adults and pups may possibly fall prey to jackals and striped hyenas.
Fennec families dig out burrows in the sand for habitation and protection, which can be as large as 120 meters squared, and adjoin the burrows of other families.
Precise population figures are not known, but are estimated from the frequency of sightings.
These indicate that the fennec fox is currently not threatened by extinction.
Knowledge of social interactions is limited to information gathered from captive animals.
The fennec fox is commonly trapped for exhibition or sale in North Africa, and it is considered an exotic pet in some parts of the world.
```
**Context Provided:** `De-capitalize all proper nouns, unless they are the start of a sentence`
**Transcript:**
```text theme={null}
The fennec fox is a small fox native to the deserts of north africa, ranging from western sahara and mauritania to the sinai peninsula.
Its most distinctive feature is its unusually large ears, which serve to dissipate heat and listen for underground prey.
The fennec is the smallest fox species. Its coat, ears, and kidney functions have adapted to the desert environment with high temperatures and little water.
The fennec fox mainly eats insects, small mammals, and birds. It has a lifespan of up to fourteen years in captivity and about ten years in the wild.
pups are preyed upon by the pharaoh eagle owl. Both adults and pups may possibly fall prey to jackals and striped hyenas.
fennec families dig out burrows in the sand for habitation and protection, which can be as large as 120 meters squared, and adjoin the burrows of other families.
Precise population figures are not known, but are estimated from the frequency of sightings.
These indicate that the fennec fox is currently not threatened by extinction.
Knowledge of social interactions is limited to information gathered from captive animals.
The fennec fox is commonly trapped for exhibition or sale in north africa, and it is considered an exotic pet in some parts of the world.
```
### Add the parameters to your request
Include context and apply\_contextual\_correction in the data part of your call.
```python A Full Example (quickstart_context.py) theme={null}
import os
import time
import requests
import json
BASE_URL = "https://api.fennec-asr.com/api/v1"
API_KEY = "YOUR_API_KEY_HERE"
AUDIO_PATH = "sample.mp3"
POLL_INTERVAL_S = 3
context_string = "The company they are talking about is called Bartholomew Bros, correct any potential misinterpretations in the script."
apply_correction_flag = True
def transcribe_with_context():
headers = {"X-API-Key": API_KEY}
with open(AUDIO_PATH, "rb") as audio_file:
# 3. Prepare the request data.
files = {"audio": (os.path.basename(AUDIO_PATH), audio_file, "audio/mpeg")}
form_data = {
"context": context_string,
"apply_contextual_correction": apply_correction_flag
}
print("--- Submitting Transcription with Contextual Correction ---")
print(f"Applying Correction: {apply_correction_flag}")
print(f"Context: '{context_string}'")
print("---------------------------------------------------------")
try:
submit_response = requests.post(
f"{BASE_URL}/transcribe",
headers=headers,
files=files,
data=form_data # <-- Add the context and flag here
)
submit_response.raise_for_status()
job_id = submit_response.json().get("job_id")
print(f"✅ Job submitted successfully! Job ID: {job_id}\n")
# Polling logic remains the same...
status_url = f"{BASE_URL}/transcribe/status/{job_id}"
while True:
status_response = requests.get(status_url, headers=headers)
status_response.raise_for_status()
data = status_response.json()
status = data.get("status")
if status == "completed":
print("\n🎉 Transcription Complete!")
print("-" * 25)
print(data.get("transcript"))
print("-" * 25)
break
elif status == "failed":
print("\n❌ Transcription failed. Error:", data.get("transcript"))
break
else:
print(f" Current status: '{status}'... waiting.")
time.sleep(POLL_INTERVAL_S)
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
transcribe_with_context()
```
```ts A Full Example (quickstart_context.ts) theme={null}
#!/usr/bin/env node
"use strict";
const BASE = "https://api.fennec-asr.com/api/v1";
const KEY = process.env.ASR_API_KEY;
const file = process.argv[2] || "sample.mp3";
const CONTEXT =
"The company they are talking about is called Bartholomew Bros, correct any potential misinterpretations in the script.";
const APPLY = true;
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
(async () => {
if (!KEY) throw new Error("Set ASR_API_KEY");
const { readFile } = require("fs/promises");
const { basename } = require("path");
const form = new FormData();
form.append(
"audio",
new Blob([await readFile(file)], { type: "audio/mpeg" }),
basename(file)
);
form.append("context", CONTEXT);
form.append("apply_contextual_correction", String(APPLY));
const sub = await fetch(`${BASE}/transcribe`, {
method: "POST",
headers: { "X-API-Key": KEY },
body: form,
duplex: "half",
}).then((r) => r.json());
if (!sub?.job_id) throw new Error("No job_id");
for (;;) {
const s = await fetch(`${BASE}/transcribe/status/${sub.job_id}`, {
headers: { "X-API-Key": KEY },
}).then((r) => r.json());
if (s.status === "completed") return void console.log(s.transcript || "");
if (s.status === "failed") {
console.error("Failed:", s.transcript || "");
process.exit(1);
}
await wait(3000);
}
})().catch((e) => {
console.error(e.message || e);
process.exit(1);
});
```
```python URL Sample Script (quickstart_context_url.py) theme={null}
import time
import json
import requests
BASE_URL = "https://api.fennec-asr.com/api/v1"
API_KEY = "YOUR_API_KEY_HERE" # <-- put your API key here
AUDIO_URL = "https://upload.wikimedia.org/wikipedia/commons/3/3f/En-History_of_Corpus_Christi%2C_Texas.ogg"
CONTEXT_STRING = "The city being referred to is Corpus Christi. Make sure any potential confusions are corrected."
APPLY_CORRECTION_FLAG = True # set to False to disable
# Polling
POLL_INTERVAL_SECONDS = 3
MAX_WAIT_SECONDS = 300
def submit_job():
headers = {"Content-Type": "application/json", "X-API-Key": API_KEY}
payload = {
"audio": AUDIO_URL,
"context": CONTEXT_STRING,
"apply_contextual_correction": APPLY_CORRECTION_FLAG,
}
print("--- Submitting URL Transcription with Contextual Correction ---")
print(f"Audio URL: {AUDIO_URL}")
print(f"Applying Correction: {APPLY_CORRECTION_FLAG}")
print(f"Context: '{CONTEXT_STRING}'")
print("---------------------------------------------------------------")
resp = requests.post(f"{BASE_URL}/transcribe/url",
headers=headers,
data=json.dumps(payload),
timeout=60)
resp.raise_for_status()
job_id = resp.json().get("job_id")
if not job_id:
raise RuntimeError("No job_id returned from submit endpoint.")
print(f"✅ Job submitted successfully! Job ID: {job_id}\n")
return job_id
def poll_job(job_id):
headers = {"X-API-Key": API_KEY}
status_url = f"{BASE_URL}/transcribe/status/{job_id}"
start = time.monotonic()
while time.monotonic() - start < MAX_WAIT_SECONDS:
print("Polling for status...")
try:
resp = requests.get(status_url, headers=headers, timeout=30)
if resp.status_code != 200:
print(f" Error polling status: HTTP {resp.status_code}")
time.sleep(POLL_INTERVAL_SECONDS)
continue
data = resp.json()
status = data.get("status")
print(f" Current status: '{status}'")
if status == "completed":
return data
if status == "failed":
return data
except requests.exceptions.RequestException as e:
print(f" Polling error: {e}")
time.sleep(POLL_INTERVAL_SECONDS)
return {"status": "timeout", "transcript": None}
def main():
try:
job_id = submit_job()
result = poll_job(job_id)
status = result.get("status")
if status == "completed":
transcript = result.get("transcript") or ""
print("\n🎉 Transcription complete!")
print("-" * 60)
print(transcript)
print("-" * 60)
if SAVE_TO:
with open(SAVE_TO, "w", encoding="utf-8") as f:
f.write(transcript)
print(f"💾 Transcript saved to: {SAVE_TO}")
elif status == "failed":
print("\n❌ Transcription failed.")
print("Error:", result.get("transcript"))
else:
print("\n⏰ Polling timed out.")
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
if __name__ == "__main__":
main()
```
```ts URL Sample Script (quickstart_context_url.ts) theme={null}
#!/usr/bin/env node
"use strict";
const BASE = "https://api.fennec-asr.com/api/v1";
const KEY = process.env.ASR_API_KEY;
const url =
process.argv[2] ||
"https://upload.wikimedia.org/wikipedia/commons/3/3f/En-History_of_Corpus_Christi%2C_Texas.ogg";
const CONTEXT =
"The city being referred to is Corpus Christi. Make sure any potential confusions are corrected.";
const APPLY = true; // set to false to disable
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
(async () => {
if (!KEY) throw new Error("Set ASR_API_KEY");
const payload = {
audio: url,
context: CONTEXT,
apply_contextual_correction: APPLY,
};
const sub = await fetch(`${BASE}/transcribe/url`, {
method: "POST",
headers: {
"X-API-Key": KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).then((r) => r.json());
if (!sub?.job_id) throw new Error("No job_id");
for (;;) {
const s = await fetch(`${BASE}/transcribe/status/${sub.job_id}`, {
headers: { "X-API-Key": KEY },
}).then((r) => r.json());
if (s.status === "completed") return void console.log(s.transcript || "");
if (s.status === "failed") {
console.error("Failed:", s.transcript || "");
process.exit(1);
}
await wait(3000);
}
})().catch((e) => {
console.error(e.message || e);
process.exit(1);
});
```
## Tips for Writing Effective Context
* **Be Specific:** Instead of writing "The book is called 'Agoraphobia in Romanesque Society'" in the context field, write "The book being talked about is 'Agoraphobia in Romanesque Society', please ensure that anything that seems like it could be a misinterpretation of the title is replaced with this book title. Also, anytime someone says an abbreviation of the title (ARS) or what sounds like an abbreviation, replace it with the title."
* **Provide Correct Spellings:** Include a list of correctly spelled names and jargon, e.g., "Key terms to replace in this script: OAuth 2.0, Kubernetes, FastAPI, Pydantic."
* **Keep it Concise:** A few clear sentences are more effective than a long, rambling paragraph. Specific examples are helpful as well.
# Diarization (Speaker Labels)
Source: https://docs.fennec-asr.com/essentials/diarization
Automatically identify and label different speakers in your audio. Fennec can distinguish who said what and when, even assigning names with context.
Diarization is the process of partitioning an audio stream into segments according to speaker identity. In simple terms, it answers the question, **"Who spoke when?"**
Fennec's diarization not only separates speakers but can also use AI to assign specific names to them, transforming a raw transcript into a structured, easy-to-read dialogue.
## How It Works
Use these parameters in your `/transcribe` request. Supplying known speaker names helps the model map voices to real people.
Set this to `true` to enable speaker diarization. The transcript will be returned with speaker labels (e.g., `[SPEAKER_00]`, `[SPEAKER_01]`).
Provide a short sentence listing the speakers (e.g., "The two speakers are Marv Esserman and the host, Ally Holt"). When `diarize` is enabled, the AI uses this text plus voice cues to replace generic labels like `[SPEAKER_00]` with the actual names.
Formatting & Performance: Enabling diarization increases processing time and cost. The `formatting` parameter is ignored when `diarize` is `true` because diarization dictates the output format.
## How to Use It
Adding `diarize` and (optionally) `speaker_recognition_context` is all you need.
### Python SDK Example
```python quickstart_sdk.py theme={null}
from fennec_asr import FennecASRClient
asr_client = FennecASRClient(api_key="YOUR_API_KEY")
transcription = asr_client.transcribe_file(
file_path="interview_session.mp3",
diarize=True,
speaker_recognition_context="The two speakers are Marv Esserman, the guest, and the host, Ally Holt."
)
print(transcription)
```
### Example Result
Without diarization, a conversation is a wall of text. With diarization and speaker context, it becomes a readable script.
**Transcript:**
```text theme={null}
What's up, everybody, and welcome to the podcast. Today, we've got a great show, because we'll be sitting down with Marv Esserman, the mastermind behind the new hit single Decorations. Marv, first off, congrats on the success of Decorations. That track's been everywhere lately. What inspired it? Thanks, Allie. It actually came from a pretty raw place. I was staring at these old holiday lights I never took down, and it just hit me. They were like metaphors for all the stuff I never let go of. So the song kind of wrote itself after that. That's wild. And the bridge in the song? Pure emotion? You layer this subtle synth hum beneath the vocals that feels like... like a memory almost. Yeah, that hum is a field recording of my childhood homes heater. I wanted the song to feel like nostalgia, not just talk about it. That's art. You've got listeners crying in their cars and they don't even know why. What can we expect next? I'm going darker next. More stripped down, less perfection, more feeling. It's about chasing the ghosts in your own house, you know. You heard it here first, folks? Marv is just getting started. Until next time, stay curious and keep listening.
```
**Transcript:**
```text theme={null}
[SPEAKER_00]: What's up, everybody, and welcome to the podcast. Today, we've got a great show, because we'll be sitting down with Marv Esserman, the mastermind behind the new hit single Decorations. Marv, first off, congrats on the success of Decorations. That track's been everywhere lately. What inspired it?
[SPEAKER_01]: Thanks, Allie. It actually came from a pretty raw place. I was staring at these old holiday lights I never took down, and it just hit me. They were like metaphors for all the stuff I never let go of. So the song kind of wrote itself after that.
[SPEAKER_00]: That's wild. And the bridge in the song? Pure emotion? You layer this subtle synth hum beneath the vocals that feels like... like a memory almost.
[SPEAKER_01]: Yeah, that hum is a field recording of my childhood homes heater. I wanted the song to feel like nostalgia, not just talk about it.
[SPEAKER_00]: That's art. You've got listeners crying in their cars and they don't even know why. What can we expect next?
[SPEAKER_01]: I'm going darker next. More stripped down, less perfection, more feeling. It's about chasing the ghosts in your own house, you know.
[SPEAKER_00]: You heard it here first, folks? Marv is just getting started. Until next time, stay curious and keep listening.
```
**Context Provided:** `The two speakers are Marv Esserman, the guest, and the host, Ally Holt.`
**Transcript:**
```text theme={null}
[Ally Holt]: What's up, everybody, and welcome to the podcast. Today, we've got a great show, because we'll be sitting down with Marv Esserman, the mastermind behind the new hit single Decorations. Marv, first off, congrats on the success of Decorations. That track's been everywhere lately. What inspired it?
[Marv Esserman]: Thanks, Allie. It actually came from a pretty raw place. I was staring at these old holiday lights I never took down, and it just hit me. They were like metaphors for all the stuff I never let go of. So the song kind of wrote itself after that.
[Ally Holt]: That's wild. And the bridge in the song? Pure emotion? You layer this subtle synth hum beneath the vocals that feels like... like a memory almost.
[Marv Esserman]: Yeah, that hum is a field recording of my childhood homes heater. I wanted the song to feel like nostalgia, not just talk about it.
[Ally Holt]: That's art. You've got listeners crying in their cars and they don't even know why. What can we expect next?
[Marv Esserman]: I'm going darker next. More stripped down, less perfection, more feeling. It's about chasing the ghosts in your own house, you know.
[Ally Holt]: You heard it here first, folks? Marv is just getting started. Until next time, stay curious and keep listening.
```
**Add the parameters to your request** — include `diarize` and `speaker_recognition_context` in the form/body.
```python quickstart_diarize.py theme={null}
import os
import time
import requests
BASE_URL = "https://api.fennec-asr.com/api/v1"
API_KEY = "YOUR_API_KEY_HERE"
AUDIO_PATH = "sample.mp3"
POLL_INTERVAL_S = 3
speaker_context = "The two speakers are Marv Esserman and the host, Ally Holt"
enable_diarization = True
def transcribe_with_diarization():
headers = {"X-API-Key": API_KEY}
with open(AUDIO_PATH, "rb") as audio_file:
files = {"audio": (os.path.basename(AUDIO_PATH), audio_file, "audio/mpeg")}
form_data = {
"diarize": enable_diarization,
"speaker_recognition_context": speaker_context,
}
print("--- Submitting Transcription with Diarization ---")
print(f"Enable Diarization: {enable_diarization}")
print(f"Speaker Context: '{speaker_context}'")
print("-------------------------------------------------")
try:
submit_response = requests.post(
f"{BASE_URL}/transcribe",
headers=headers,
files=files,
data=form_data,
timeout=60,
)
submit_response.raise_for_status()
job_id = submit_response.json().get("job_id")
print(f"✅ Job submitted successfully! Job ID: {job_id}")
status_url = f"{BASE_URL}/transcribe/status/{job_id}"
while True:
status_response = requests.get(status_url, headers=headers, timeout=30)
status_response.raise_for_status()
data = status_response.json()
status = data.get("status")
if status == "completed":
print("🎉 Transcription Complete!")
print("-" * 25)
print(data.get("transcript"))
print("-" * 25)
break
elif status == "failed":
print("❌ Transcription failed. Error:", data.get("transcript"))
break
else:
print(f" Current status: '{status}'... waiting.")
time.sleep(POLL_INTERVAL_S)
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
transcribe_with_diarization()
```
```typescript quickstart_diarize.ts theme={null}
#!/usr/bin/env node
"use strict";
const BASE = "https://api.fennec-asr.com/api/v1";
const KEY = process.env.ASR_API_KEY;
const file = process.argv[2] || "sample.mp3";
const SPEAKER_CONTEXT = "The two speakers are Marv Esserman and the host, Ally Holt";
const DIARIZE = true;
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
(async () => {
if (!KEY) throw new Error("Set ASR_API_KEY");
const { readFile } = await import("fs/promises");
const { basename } = await import("path");
const form = new FormData();
form.append(
"audio",
new Blob([await readFile(file)], { type: "audio/mpeg" }),
basename(file)
);
form.append("diarize", String(DIARIZE));
form.append("speaker_recognition_context", SPEAKER_CONTEXT);
const sub = await fetch(`${BASE}/transcribe`, {
method: "POST",
headers: { "X-API-Key": KEY },
body: form,
duplex: "half",
}).then((r) => r.json());
if (!sub?.job_id) throw new Error("No job_id");
for (;;) {
const s = await fetch(`${BASE}/transcribe/status/${sub.job_id}`, {
headers: { "X-API-Key": KEY },
}).then((r) => r.json());
if (s.status === "completed") return void console.log(s.transcript || "");
if (s.status === "failed") {
console.error("Failed:", s.transcript || "");
process.exit(1);
}
await wait(3000);
}
})().catch((e) => {
console.error(e.message || e);
process.exit(1);
});
```
```python quickstart_diarize_url.py theme={null}
import time
import json
import requests
BASE_URL = "https://api.fennec-asr.com/api/v1"
API_KEY = "YOUR_API_KEY_HERE"
AUDIO_URL = "https://ondemand.npr.org/anon.npr-mp3/npr/atc/2025/08/20250821_atc_at_83_years_old_harrison_ford_is_still_experiencing_firsts.mp3"
SPEAKER_CONTEXT = "This is a conversation between Harrison Ford and Rachel Martin, Intro speaker is Juana Summers"
ENABLE_DIARIZATION = True
POLL_INTERVAL_SECONDS = 3
MAX_WAIT_SECONDS = 300
def submit_job():
headers = {"Content-Type": "application/json", "X-API-Key": API_KEY}
payload = {
"audio": AUDIO_URL,
"diarize": ENABLE_DIARIZATION,
"speaker_recognition_context": SPEAKER_CONTEXT,
}
print("--- Submitting URL Transcription with Diarization ---")
print(f"Audio URL: {AUDIO_URL}")
print(f"Enable Diarization: {ENABLE_DIARIZATION}")
print(f"Speaker Context: '{SPEAKER_CONTEXT}'")
print("-----------------------------------------------------")
resp = requests.post(
f"{BASE_URL}/transcribe/url",
headers=headers,
data=json.dumps(payload),
timeout=60,
)
resp.raise_for_status()
job_id = resp.json().get("job_id")
if not job_id:
raise RuntimeError("No job_id returned from submit endpoint.")
print(f"✅ Job submitted successfully! Job ID: {job_id}")
return job_id
def poll_job(job_id):
headers = {"X-API-Key": API_KEY}
status_url = f"{BASE_URL}/transcribe/status/{job_id}"
start = time.monotonic()
while time.monotonic() - start < MAX_WAIT_SECONDS:
print("Polling for status...")
try:
resp = requests.get(status_url, headers=headers, timeout=30)
resp.raise_for_status()
data = resp.json()
status = data.get("status")
print(f" Current status: '{status}'")
if status in ("completed", "failed"):
return data
except requests.exceptions.RequestException as e:
print(f" Polling error: {e}")
time.sleep(POLL_INTERVAL_SECONDS)
return {"status": "timeout", "transcript": None}
def main():
try:
job_id = submit_job()
result = poll_job(job_id)
status = result.get("status")
if status == "completed":
print("🎉 Transcription complete!")
print("-" * 60)
print(result.get("transcript") or "")
print("-" * 60)
elif status == "failed":
print("❌ Transcription failed. Error:", result.get("transcript"))
else:
print("⏰ Polling timed out.")
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
main()
```
```typescript quickstart_diarize_url.ts theme={null}
#!/usr/bin/env node
"use strict";
const BASE = "https://api.fennec-asr.com/api/v1";
const KEY = process.env.ASR_API_KEY;
const url =
process.argv[2] ||
"https://ondemand.npr.org/anon.npr-mp3/npr/atc/2025/08/20250821_atc_at_83_years_old_harrison_ford_is_still_experiencing_firsts.mp3";
const SPEAKER_CONTEXT = "This is a conversation between Harrison Ford and Rachel Martin, Intro speaker is Juana Summers";
const DIARIZE = true;
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
(async () => {
if (!KEY) throw new Error("Set ASR_API_KEY");
const payload = {
audio: url,
diarize: DIARIZE,
speaker_recognition_context: SPEAKER_CONTEXT,
};
const sub = await fetch(`${BASE}/transcribe/url`, {
method: "POST",
headers: {
"X-API-Key": KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).then((r) => r.json());
if (!sub?.job_id) throw new Error("No job_id");
for (;;) {
const s = await fetch(`${BASE}/transcribe/status/${sub.job_id}`, {
headers: { "X-API-Key": KEY },
}).then((r) => r.json());
if (s.status === "completed") return void console.log(s.transcript || "");
if (s.status === "failed") {
console.error("Failed:", s.transcript || "");
process.exit(1);
}
await wait(3000);
}
})().catch((e) => {
console.error(e.message || e);
process.exit(1);
});
```
## Tips for Writing Effective Speaker Context
* **Be specific:** Provide full names and roles if possible. For example, `The interviewer is Dr. Anya Sharma, and the patient's name is Ben Carter.`
* **List all speakers:** Try to list all known speakers to give the AI the best chance of correctly identifying everyone.
* **Clarity is key:** The AI uses this text to make an intelligent assignment. The clearer and more descriptive your context, the more accurate the final named speaker labels will be.
# Automatic Formatting
Source: https://docs.fennec-asr.com/essentials/formatting
Automatically add paragraphs and line breaks to your transcripts to improve readability.
Raw ASR output is often a dense wall of text, making it difficult to read and understand the flow of a conversation. The Automatic Formatting feature solves this by intelligently inserting newlines and double newlines into your transcript based on the duration of pauses between spoken words.
This transforms a hard-to-read block of text into a well-structured, easy-to-scan document.
## How It Works
The feature is controlled by a single `formatting` parameter, which accepts a JSON string containing one or both of the following keys:
The pause duration (in seconds) required to insert a single newline (`\n`). This is ideal for shorter, conversational breaks.
The pause duration (in seconds) required to insert a double newline (`\n\n`), effectively creating a new paragraph. This is useful for marking a change in topic or speaker.
The `formatting` parameter must be sent as a **JSON-formatted string** within your `multipart/form-data` request, not as a raw JSON object. We'll show you how to do this below.
## How to Use It
To use this feature, you'll add the `formatting` field to your request.
## SDK Example
```python quickstart_format.py theme={null}
from fennec_asr import FennecASRClient
asr_client = FennecASRClient(api_key="YOUR_API_KEY")
formatting_options = {
"newline_pause_threshold": 0.8,
"double_newline_pause_threshold": 1.5
}
transcription = asr_client.transcribe_file(
file_path="sample.mp3",
formatting=formatting_options,
)
print(transcription)
```
## Example Result
Applying formatting makes a huge difference in readability.
```text theme={null}
Alright team let's sync up on the Q3 project deliverables for Fennec aural. The primary goal is to finalize the user interface mockups by Wednesday. I've finished the preliminary analysis for the core features and have the numbers ready. We need to ensure that the new design is both intuitive and accessible. I'm reviewing the data on the acting ink report now. Great, pull them up. We need to finalize the presentation by tomorrow. The client expects a full walkthrough.
```
```text theme={null}
Alright team let's sync up on the Q3 project deliverables for Fennec aural. The primary goal is to finalize the user interface mockups by Wednesday.
I've finished the preliminary analysis for the core features and have the numbers ready. We need to ensure that the new design is both intuitive and accessible.
I'm reviewing the data on the acting ink report now.
Great, pull them up. We need to finalize the presentation by tomorrow. The client expects a full walkthrough.
```
### 1. Define your formatting rules
In your script, create a dictionary with your desired pause thresholds.
```python theme={null}
# Define the formatting rules in a Python dictionary
# Add a newline for any pause over 0.8 seconds
# Add a new paragraph for any pause over 1.5 seconds
formatting_options = {
"newline_pause_threshold": 0.8,
"double_newline_pause_threshold": 1.5
}
```
```ts theme={null}
// 1) Define formatting rules
// Add a newline for any pause over 0.8 seconds
// Add a new paragraph for any pause over 1.5 seconds
export const formattingOptions = {
newline_pause_threshold: 0.8,
double_newline_pause_threshold: 1.5,
};
```
### 2. Convert the rules to a JSON string
Use Python's json library to serialize the dictionary into a string.
```python theme={null}
import json
# The API expects a JSON string, so we serialize the dictionary
formatting_string = json.dumps(formatting_options)
```
```ts theme={null}
// 2) Serialize to a JSON string (what the API expects)
import type { } from "node"; // keeps this as TS; safe to remove if not needed
import { formattingOptions } from "./formattingOptions"; // or inline the object from above
export const formattingString: string = JSON.stringify(formattingOptions);
```
### 3. Send the request
Pass the serialized string in the data part of your requests.post call.
```python A Full Example (quickstart_format.py) theme={null}
import os
import time
import requests
import json
BASE_URL = "https://api.fennec-asr.com/api/v1"
API_KEY = "YOUR_API_KEY_HERE"
AUDIO_PATH = "sample.mp3"
POLL_INTERVAL_S = 3
# 1. Define your formatting rules as a Python dictionary.
formatting_options = {
"newline_pause_threshold": 0.8,
"double_newline_pause_threshold": 1.5
}
def transcribe_with_formatting():
headers = {"X-API-Key": API_KEY}
with open(AUDIO_PATH, "rb") as audio_file:
files = {"audio": (os.path.basename(AUDIO_PATH), audio_file, "audio/mpeg")}
form_data = {
"formatting": json.dumps(formatting_options)
}
print("--- Submitting Transcription with Formatting ---")
print(f"Formatting Rules: {form_data['formatting']}")
print("----------------------------------------------")
try:
# Add the 'data' parameter to send the new form field.
submit_response = requests.post(
f"{BASE_URL}/transcribe",
headers=headers,
files=files,
data=form_data # <-- Add the formatting rules here
)
submit_response.raise_for_status()
job_id = submit_response.json().get("job_id")
print(f"✅ Job submitted successfully! Job ID: {job_id}\n")
status_url = f"{BASE_URL}/transcribe/status/{job_id}"
while True:
status_response = requests.get(status_url, headers=headers)
status_response.raise_for_status()
data = status_response.json()
status = data.get("status")
if status == "completed":
print("\n🎉 Transcription Complete!")
print("-" * 25)
print(data.get("transcript"))
print("-" * 25)
break
elif status == "failed":
print("\n❌ Transcription failed. Error:", data.get("transcript"))
break
else:
print(f" Current status: '{status}'... waiting.")
time.sleep(POLL_INTERVAL_S)
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
transcribe_with_formatting()
```
```ts theme={null}
#!/usr/bin/env node
"use strict";
// 3) Full CLI script mirroring your Python example
const BASE = "https://api.fennec-asr.com/api/v1";
const KEY = process.env.ASR_API_KEY;
const file = process.argv[2] || "sample.mp3";
const formattingOptions = {
newline_pause_threshold: 0.8,
double_newline_pause_threshold: 1.5,
};
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
(async () => {
if (!KEY) throw new Error("Set ASR_API_KEY");
const { readFile } = require("fs/promises");
const { basename } = require("path");
// Build multipart form with audio + formatting JSON string
const form = new FormData();
form.append(
"audio",
new Blob([await readFile(file)], { type: "audio/mpeg" }),
basename(file)
);
form.append("formatting", JSON.stringify(formattingOptions));
// Submit job
const sub = await fetch(`${BASE}/transcribe`, {
method: "POST",
headers: { "X-API-Key": KEY },
body: form,
duplex: "half",
}).then((r) => r.json());
if (!sub?.job_id) throw new Error("No job_id");
// Poll until completed/failed
for (;;) {
const s = await fetch(`${BASE}/transcribe/status/${sub.job_id}`, {
headers: { "X-API-Key": KEY },
}).then((r) => r.json());
if (s.status === "completed") return void console.log(s.transcript || "");
if (s.status === "failed") {
console.error("Failed:", s.transcript || "");
process.exit(1);
}
await wait(3000);
}
})().catch((e) => {
console.error(e.message || e);
process.exit(1);
});
```
```python URL Sample Script (quickstart_formatting_url.py) theme={null}
import time
import json
import requests
BASE_URL = "https://api.fennec-asr.com/api/v1"
API_KEY = "YOUR_API_KEY_HERE"
AUDIO_URL = "https://upload.wikimedia.org/wikipedia/commons/3/3f/En-History_of_Corpus_Christi%2C_Texas.ogg"
POLL_INTERVAL_SECONDS = 5
MAX_WAIT_SECONDS = 300
formatting_options = {
"newline_pause_threshold": 0.6,
"double_newline_pause_threshold": 0.8
}
def submit_job():
headers = {"Content-Type": "application/json", "X-API-Key": API_KEY}
# The 'formatting' value MUST be a JSON-formatted string.
payload = {
"audio": AUDIO_URL,
"formatting": json.dumps(formatting_options)
}
print("--- Submitting URL Transcription with Formatting ---")
print(f"Audio URL: {AUDIO_URL}")
print(f"Formatting: {payload['formatting']}")
print("----------------------------------------------------")
resp = requests.post(f"{BASE_URL}/transcribe/url",
headers=headers,
json=payload,
timeout=60)
resp.raise_for_status()
job_id = resp.json().get("job_id")
if not job_id:
raise RuntimeError("No job_id returned from submit endpoint.")
print(f"✅ Job submitted successfully! Job ID: {job_id}\n")
return job_id
def poll_job(job_id):
headers = {"X-API-Key": API_KEY}
status_url = f"{BASE_URL}/transcribe/status/{job_id}"
start = time.monotonic()
while time.monotonic() - start < MAX_WAIT_SECONDS:
print("Polling for status...")
try:
resp = requests.get(status_url, headers=headers, timeout=30)
if resp.status_code != 200:
print(f" Error polling status: HTTP {resp.status_code}")
time.sleep(POLL_INTERVAL_SECONDS)
continue
data = resp.json()
status = data.get("status")
print(f" Current status: '{status}'")
if status == "completed":
return data
if status == "failed":
return data
except requests.exceptions.RequestException as e:
print(f" Polling error: {e}")
time.sleep(POLL_INTERVAL_SECONDS)
return {"status": "timeout", "transcript": None}
def main():
try:
job_id = submit_job()
result = poll_job(job_id)
status = result.get("status")
if status == "completed":
transcript = result.get("transcript") or ""
print("\n🎉 Transcription complete!")
print("-" * 60)
print(transcript)
print("-" * 60)
elif status == "failed":
print("\n❌ Transcription failed.")
print("Error:", result.get("transcript"))
else:
print("\n⏰ Polling timed out.")
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
if __name__ == "__main__":
main()
```
```ts URL Sample Script (quickstart_formatting_url.ts theme={null}
#!/usr/bin/env node
"use strict";
const BASE = "https://api.fennec-asr.com/api/v1";
const KEY = process.env.ASR_API_KEY;
const AUDIO_URL =
process.argv[2] ||
"https://upload.wikimedia.org/wikipedia/commons/3/3f/En-History_of_Corpus_Christi%2C_Texas.ogg";
const POLL_INTERVAL_MS = 5000;
const MAX_WAIT_MS = 300000;
const formattingOptions = {
newline_pause_threshold: 0.6,
double_newline_pause_threshold: 0.8,
};
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
(async () => {
if (!KEY) throw new Error("Set ASR_API_KEY");
// The 'formatting' value MUST be a JSON-formatted string.
const payload = {
audio: AUDIO_URL,
formatting: JSON.stringify(formattingOptions),
};
console.log("--- Submitting URL Transcription with Formatting ---");
console.log(`Audio URL: ${AUDIO_URL}`);
console.log(`Formatting: ${payload.formatting}`);
console.log("----------------------------------------------------");
const sub = await fetch(`${BASE}/transcribe/url`, {
method: "POST",
headers: {
"X-API-Key": KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).then(async (r) => {
if (!r.ok) {
throw new Error(`Submit failed: HTTP ${r.status}`);
}
return r.json();
});
if (!sub?.job_id) throw new Error("No job_id returned from submit endpoint.");
console.log(`✅ Job submitted successfully! Job ID: ${sub.job_id}\n`);
const start = Date.now();
const statusUrl = `${BASE}/transcribe/status/${sub.job_id}`;
for (;;) {
console.log("Polling for status...");
try {
const s = await fetch(statusUrl, {
headers: { "X-API-Key": KEY },
}).then(async (r) => {
if (!r.ok) {
console.log(` Error polling status: HTTP ${r.status}`);
return null;
}
return r.json();
});
if (s) {
const status = s.status;
console.log(` Current status: '${status}'`);
if (status === "completed") {
const transcript: string = s.transcript || "";
console.log("\n🎉 Transcription complete!");
console.log("-".repeat(60));
console.log(transcript);
console.log("-".repeat(60));
return;
}
if (status === "failed") {
console.error("\n❌ Transcription failed.");
console.error("Error:", s.transcript || "");
process.exit(1);
}
}
} catch (e: any) {
console.log(` Polling error: ${e?.message || e}`);
}
if (Date.now() - start >= MAX_WAIT_MS) {
console.error("\n⏰ Polling timed out.");
process.exit(1);
}
await wait(POLL_INTERVAL_MS);
}
})().catch((e) => {
console.error(e.message || e);
process.exit(1);
});
```
# Thought Detection
Source: https://docs.fennec-asr.com/essentials/thought-detection
Build smart voice agents that let your users speak freely with no unwanted interruptions.
**Experimental Feature**: This feature is currently in testing, so stability is not guaranteed.
Traditional Voice Activity Detection (VAD) is excellent for segmenting speech based on pauses, but it can often break a single cohesive idea into multiple smaller transcripts if the speaker pauses to think. Many LLMs are hot to interrupt and respond to a user when a transcription is sent even when turn skipping tools are enabled.
The **Thought Detection** feature solves this by analyzing the semantic and vocal content of the speech in real-time to determine when a user has finished expressing a complete thought, leading to less interruptions and a better voice assistant interaction.
## How It Works
1. Enable Thought Detection by sending a JSON start message on the WebSocket that includes `detect_thoughts: true`. You can also tune `end_thought_eagerness` and `force_complete_time` here.
2. As you stream audio, the server transcribes it internally but **does not** immediately send back a transcript after every pause. Instead, it buffers these transcripts and keeps them as one longer string.
3. Only when the model determines a thought is complete does the server send a single message of type `complete_thought` containing the full text of that idea.
The moment a thought is considered “complete” is tunable. Use `end_thought_eagerness` to make the detector more or less willing to close a thought, and `force_complete_time` to set a drop-dead timeout (in seconds) that will force emission of the current buffered thought after silence. This is helpful if a finished turn is incorrectly identified as an unfinished turn.
### Enabling the Feature
**Start Message Fields**
```json theme={null}
{
"type": "start",
"sample_rate": 16000,
"channels": 1,
"detect_thoughts": true,
"end_thought_eagerness": "medium", // "low" | "medium" | "high"
"force_complete_time": 2.0, // seconds, will force complete thought so users aren't stranded (min 1, max 60)
"context": "optional correction context string", // enables server-side transcript correction if provided
"vad": { "...": "unchanged VAD settings" }
}
```
### Tuning When a Thought Ends
These fields belong in the initial start message you send after opening the WebSocket.
`end_thought_eagerness` (string, default: "medium")
Controls how aggressively the model closes a thought.
Allowed values: "low", "medium", "high".
`force_complete_time` (number, default: 2.0)
A drop-dead timer in seconds. If the model has not marked the current thought complete and the user has gone silent, the server will force-emit the buffered text once this many seconds have elapsed.
Range: 1.0–60.0 seconds. Values outside this range are rejected.
Note: You still enable the feature with detect\_thoughts: true. These extra fields only tune when the thought ends.
## Getting Started
### Install the SDK mic addon
```python theme={null}
pip install fennec-asr[mic]
```
### Python SDK Example
```python An SDK Example (mic_ws_continuous_thought_detection_sdk.py) theme={null}
import os, asyncio
from dotenv import load_dotenv
from fennec_asr import Realtime
from fennec_asr.mic import stream_microphone
load_dotenv()
API_KEY = os.getenv("FENNEC_API_KEY")
SAMPLE_RATE = 16000
CHANNELS = 1
CHUNK_MS = 32
SINGLE_UTTERANCE = False
DETECT_THOUGHTS = True
VAD = {
"threshold": 0.45,
"min_silence_ms": 100,
"speech_pad_ms": 200,
"final_silence_s": 0.1,
"start_trigger_ms": 36,
"min_voiced_ms": 48,
"min_chars": 1,
"min_words": 1,
"amp_extend": 1200,
"force_decode_ms": 0,
"debug": False,
}
async def main():
if not API_KEY:
raise RuntimeError("Set FENNEC_API_KEY")
rt = (
Realtime(API_KEY, sample_rate=SAMPLE_RATE, channels=CHANNELS, detect_thoughts=DETECT_THOUGHTS)
.on("open", lambda: print("✅ ready"))
.on("thought", lambda t: print("🧠", t))
.on("error", lambda e: print("❌", e))
.on("close", lambda: print("👋 closed"))
)
rt._start_msg["single_utterance"] = SINGLE_UTTERANCE
rt._start_msg["detect_thoughts"] = True
rt._start_msg["end_thought_eagerness"] = "medium" # "low" | "medium" | "high"
rt._start_msg["force_complete_time"] = 2.0 # 1–60 seconds
rt._start_msg["vad"] = VAD
async with rt:
await stream_microphone(rt, samplerate=SAMPLE_RATE, channels=CHANNELS, chunk_ms=CHUNK_MS)
if __name__ == "__main__":
asyncio.run(main())
```
Don't want to use the SDK? Here are some full code samples:
This client script includes a simple `ENABLE_THOUGHT_DETECTION` flag. When set to `true`, it automatically adjusts the WebSocket URL and the VAD settings, then listens for the specific `complete_thought` message from the server.
```python A Full Example (mic_ws_thought_detection.py) theme={null}
import asyncio
import json
import signal
import sounddevice as sd
import websockets
import httpx
import os
ENABLE_THOUGHT_DETECTION = True
WS_BASE = "wss://api.fennec-asr.com/api/v1/transcribe/stream"
HTTP_TOKEN_URL = "https://api.fennec-asr.com/api/v1/transcribe/streaming-token"
API_KEY = os.getenv("FENNEC_API_KEY") or "YOUR_API_KEY"
SAMPLE_RATE = 16_000
CHANNELS = 1
DTYPE = "int16"
CHUNK_MS = 100
FRAMES_PER_CHUNK = int(SAMPLE_RATE * (CHUNK_MS / 1000.0))
START_MSG = {
"type": "start",
"sample_rate": 16000,
"channels": 1,
"single_utterance": False,
"detect_thoughts": True,
"end_thought_eagerness": "medium", # "low" | "medium" | "high"
"force_complete_time": 2.0, # 1–60 seconds
# "context": "optional correction context string",
"vad": {
"threshold": 0.45,
"min_silence_ms": 400,
"speech_pad_ms": 200,
"final_silence_s": 0,
"start_trigger_ms": 30,
"min_voiced_ms": 100,
"min_chars": 1,
"min_words": 1,
"amp_extend": 1200
}
}
shutdown_event = asyncio.Event()
def _handle_sigint(*_):
shutdown_event.set()
async def audio_sender(ws, stream):
print("\n🎙️ Streaming mic… speak a full thought and pause. (Ctrl+C to stop)", flush=True)
while not shutdown_event.is_set():
try:
data, _ = await asyncio.to_thread(stream.read, FRAMES_PER_CHUNK)
await ws.send(bytes(data))
except websockets.exceptions.ConnectionClosed:
break
try:
if not ws.closed:
await ws.send('{"type":"eos"}')
except websockets.exceptions.ConnectionClosed:
pass
async def message_receiver(ws):
"""Listens for messages and handles them based on the selected mode."""
async for msg in ws:
try:
data = json.loads(msg)
if data.get("type") == "complete_thought":
text = data.get("text", "")
print(f"\n✅ Thought Complete: {text}", flush=True)
except Exception:
# Ignore non-JSON messages
pass
async def fetch_streaming_token() -> str:
"""
Exchanges your API key for a short-lived streaming token.
Send the token via ?streaming_token=... in the WS URL.
"""
if not API_KEY or API_KEY == "YOUR_API_KEY":
raise RuntimeError("Set FENNEC_API_KEY (or replace API_KEY).")
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(
HTTP_TOKEN_URL,
headers={"X-API-Key": API_KEY, "content-type": "application/json"},
json={}, # body not required; header is what matters
)
resp.raise_for_status()
data = resp.json()
token = data.get("token")
if not token:
raise RuntimeError(f"Token endpoint returned no token: {data}")
return token
async def main():
print("Fetching streaming token…")
token = await fetch_streaming_token()
WEBSOCKET_URL = f"{WS_BASE}?streaming_token={token}"
print(f"Connecting to: {WEBSOCKET_URL}")
try:
async with websockets.connect(
WEBSOCKET_URL,
max_size=None,
ping_interval=5
) as ws:
await ws.send(json.dumps(START_MSG))
print("✅ WebSocket connected and configured.")
with sd.RawInputStream(
samplerate=SAMPLE_RATE, channels=CHANNELS, dtype=DTYPE, blocksize=FRAMES_PER_CHUNK
) as stream:
sender = asyncio.create_task(audio_sender(ws, stream))
receiver = asyncio.create_task(message_receiver(ws))
await asyncio.wait([sender, receiver], return_when=asyncio.FIRST_COMPLETED)
except Exception as e:
print(f"❌ An error occurred: {e}")
if __name__ == "__main__":
if sd.query_devices(kind='input'):
signal.signal(signal.SIGINT, _handle_sigint)
asyncio.run(main())
else:
print("\n❌ No input microphone found.")
```
```ts A Full Example (mic_ws_thought_detection.ts) theme={null}
#!/usr/bin/env node
"use strict";
/**
* Streams microphone audio to the ASR WebSocket API with "thought detection".
* Prints a message whenever a complete thought is detected. (Ctrl+C to stop)
*
* Requires:
* - Node 18+ (for global fetch/Blob/FormData/AbortSignal)
* - npm i ws mic
* - Set your API key below or via env (FENNEC_API_KEY)
*/
const ENABLE_THOUGHT_DETECTION = true;
const WS_BASE = "wss://api.fennec-asr.com/api/v1/transcribe/stream";
const HTTP_TOKEN_URL = "https://api.fennec-asr.com/api/v1/transcribe/streaming-token";
// Prefer env var if available; falls back to placeholder to mirror the Python example.
const API_KEY = process.env.FENNEC_API_KEY || "YOUR_API_KEY";
if (!API_KEY || API_KEY === "YOUR_API_KEY") {
console.warn("⚠️ Using placeholder API key. Set FENNEC_API_KEY for real requests.");
}
let WEBSOCKET_URL = WS_BASE;
// --- Audio Configuration ---
const SAMPLE_RATE: number = 16_000;
const CHANNELS: number = 1;
const CHUNK_MS: number = 100;
const DTYPE_BYTES: number = 2; // int16 -> 2 bytes/sample
const FRAMES_PER_CHUNK: number = Math.floor(SAMPLE_RATE * (CHUNK_MS / 1000));
const CHUNK_BYTES: number = FRAMES_PER_CHUNK * CHANNELS * DTYPE_BYTES;
// --- WebSocket Start Message (mirrors Python) ---
const START_MSG = {
type: "start",
sample_rate: 16000,
channels: 1,
single_utterance: false,
detect_thoughts: true,
end_thought_eagerness: "medium", // "low" | "medium" | "high"
force_complete_time: 2.0, // 1–60 seconds
// context: "optional correction context string",
vad: {
threshold: 0.45,
min_silence_ms: 400,
speech_pad_ms: 200,
final_silence_s: 0,
start_trigger_ms: 30,
min_voiced_ms: 100,
min_chars: 1,
min_words: 1,
amp_extend: 1200
}
};
(async () => {
const WebSocket = require("ws");
const mic = require("mic");
// Exchange API key -> short-lived streaming token
const resp = await fetch(HTTP_TOKEN_URL, {
method: "POST",
headers: { "X-API-Key": API_KEY, "content-type": "application/json" },
body: JSON.stringify({}),
});
if (!resp.ok) {
const text = await resp.text().catch(() => "");
throw new Error(`Token request failed: ${resp.status} ${text}`);
}
const data = await resp.json().catch(() => ({}));
const token = data?.token as string | undefined;
if (!token) throw new Error("Token endpoint returned no token");
WEBSOCKET_URL = `${WS_BASE}?streaming_token=${encodeURIComponent(token)}`;
console.log(`Connecting to: ${WEBSOCKET_URL}`);
const ws = new (WebSocket as any)(WEBSOCKET_URL, {
perMessageDeflate: false,
maxPayload: 0,
});
// Graceful shutdown
let shuttingDown = false;
const shutdown = (micInstance?: any) => {
if (shuttingDown) return;
shuttingDown = true;
try {
if (ws.readyState === WebSocket.OPEN) ws.send('{"type":"eos"}');
} catch {}
try {
micInstance?.stop();
} catch {}
try {
ws.close();
} catch {}
};
process.on("SIGINT", () => shutdown());
ws.on("open", async () => {
try {
await ws.send(JSON.stringify(START_MSG));
console.log("✅ WebSocket connected and configured.");
// Configure mic: raw 16kHz, mono, 16-bit LE PCM
const micInstance = mic({
rate: String(SAMPLE_RATE),
channels: String(CHANNELS),
bitwidth: "16",
encoding: "signed-integer",
endian: "little",
fileType: "raw",
exitOnSilence: 0,
device: process.env.MIC_DEVICE || undefined, // optional override
});
const micStream = micInstance.getAudioStream();
let buffer: Buffer = Buffer.alloc(0);
console.log("\n🎙️ Streaming mic… speak a full thought and pause. (Ctrl+C to stop)");
micStream.on("data", (chunk: Buffer) => {
buffer = Buffer.concat([buffer, chunk]);
while (buffer.length >= CHUNK_BYTES && ws.readyState === WebSocket.OPEN) {
const slice = buffer.subarray(0, CHUNK_BYTES);
ws.send(slice); // binary PCM chunk
buffer = buffer.subarray(CHUNK_BYTES);
}
});
micStream.on("error", (err: any) => {
console.error("Mic error:", err?.message || err);
});
ws.on("message", (msg: Buffer) => {
try {
const data = JSON.parse(msg.toString("utf8"));
if (data?.type === "complete_thought") {
const text: string = data.text || "";
console.log(`\n✅ Thought Complete: ${text}`);
}
} catch {
// ignore non-JSON
}
});
ws.on("close", () => shutdown(micInstance));
ws.on("error", (e: any) => {
console.error("WebSocket error:", e?.message || e);
shutdown(micInstance);
});
// Try starting mic; if it immediately errors, surface a helpful message.
try {
micInstance.start();
} catch (e: any) {
console.error("\n❌ Failed to access microphone. Ensure an input device is available.");
shutdown(micInstance);
}
} catch (e: any) {
console.error("Startup error:", e?.message || e);
shutdown();
}
});
ws.on("error", (e: any) => {
console.error("WebSocket error (pre-open):", e?.message || e);
});
})();
```
## Example Interaction
Here is how the experience differs when speaking the same sentence: "I was thinking about the quarterly report... and it seems like the numbers for Q3 are a bit lower than we expected."
The application receives multiple, fragmented transcripts based purely on pauses.
**Received Transcripts:**
```text theme={null}
I was thinking about the quarterly report
```
```text theme={null}
and it seems like the numbers for Q3 are a bit lower than we expected.
```
The application receives a single, semantically complete transcript after the user finishes their entire point.
**Received Transcript:**
```text theme={null}
✅ Thought Complete: I was thinking about the quarterly report, and it seems like the numbers for Q3 are a bit lower than we expected.
```
### Why is this useful?
Thought Detection helps AI voice agents to be more intuitive and realistic. It's annoying for users to be interrupted by an overly enthusiastic LLM, and this is a simple solution to eliminate that from the start.
It's also useful for live transcriptions that have beautiful spacing out of the box. No more blocks of text!
# Live Transcription
Source: https://docs.fennec-asr.com/essentials/websockets
Stream audio directly from any source and receive low-latency transcripts in real-time using our WebSocket endpoint, with fine-grained VAD controls.
For applications requiring immediate feedback, AI voice agents, the WebSocket endpoint provides real-time transcription. You can stream raw audio and receive transcripts back as soon as a speaker pauses, all with sub-250 ms latency.
This endpoint's behavior is primarily controlled by Voice Activity Detection (VAD), which intelligently segments the audio stream into utterances based on speech and silence.
## How It Works
1. **Get a streaming token (required):**
Make an HTTP POST to `/api/v1/transcribe/streaming-token` with your API key in the `X-API-Key` header.
The response is `{ "token": "" }`. Tokens are short-lived; fetch a new one for each connection.
2. **Connect:**
Open a WebSocket to
`wss://api.fennec-asr.com/api/v1/transcribe/stream?streaming_token=`
3. **Configure:**
Send a single JSON `{"type":"start", ...}` message (audio + VAD + optional features).
4. **Stream:**
Send raw audio as binary messages.
5. **Receive:**
Read JSON messages (finalized transcripts, and optionally VAD/utterance events).
6. **Close:**
Send `{"type":"eos"}` to gracefully end the stream.
### Get your API Key
Your API key is used to grab your streaming token. Follow [this link](https://app.fennec-asr.com) to grab your first API key.
***
The following example uses your machine's microphone as the audio source.
### Install the SDK mic addon
```bash theme={null}
pip install fennec-asr[mic]
```
### Python SDK Example
```python An SDK Example (mic_ws_continuous_sdk.py) theme={null}
import os, asyncio
from fennec_asr import Realtime
from fennec_asr.mic import stream_microphone
API_KEY = "YOUR_API_KEY"
SAMPLE_RATE = 16000
CHANNELS = 1
CHUNK_MS = 32
SINGLE_UTTERANCE = False
DETECT_THOUGHTS = False
# Note: This is a very aggressive VAD setting for high speed transcription.
VAD = {
"threshold": 0.5, # VAD sensitivity. Lower is more sensitive. 0.5 is a good baseline.
"min_silence_ms": 100, # Milliseconds of silence to trigger an End-of-Speech event.
"speech_pad_ms": 200, # Milliseconds of audio to include before speech starts.
"final_silence_s": 0.1, # Additional silence to wait for at the end.
"start_trigger_ms": 36, # How many ms of speech are needed to start transcribing.
"min_voiced_ms": 48, # Utterance is discarded if it has less than this much voiced audio.
"min_chars": 1, # Discard transcript if it has fewer characters than this.
"min_words": 1, # Discard transcript if it has fewer words than this.
"amp_extend": 1200, # A non-speech noise can extend utterance, but not start it.
"force_decode_ms": 0, # Forces a transcription after a set amount of ms have elapsed
"debug": False, # show debugging messages
}
async def main():
if not API_KEY:
raise RuntimeError("Set FENNEC_API_KEY")
rt = (
Realtime(API_KEY, sample_rate=SAMPLE_RATE, channels=CHANNELS, detect_thoughts=DETECT_THOUGHTS)
.on("open", lambda: print("✅ ready"))
.on("final", lambda t: print("📝", t))
.on("error", lambda e: print("❌", e))
)
rt._start_msg["single_utterance"] = SINGLE_UTTERANCE
rt._start_msg["vad"] = VAD
async with rt:
await stream_microphone(rt, samplerate=SAMPLE_RATE, channels=CHANNELS, chunk_ms=CHUNK_MS)
if __name__ == "__main__":
asyncio.run(main())
```
```python A Full Example (mic_ws_continuous.py) theme={null}
import asyncio
import json
import signal
import sounddevice as sd
import websockets
# --- API Configuration ---
# 1) Fetch a short-lived streaming token over HTTPS using your API key.
# 2) Use that token to authenticate the WebSocket connection via the URL query string.
HTTP_TOKEN_URL = "https://api.fennec-asr.com/api/v1/transcribe/streaming-token"
WS_BASE = "wss://api.fennec-asr.com/api/v1/transcribe/stream"
API_KEY = os.getenv("FENNEC_API_KEY") or "YOUR_API_KEY_HERE"
# --- Audio Configuration ---
SAMPLE_RATE = 16_000
CHANNELS = 1
DTYPE = "int16"
CHUNK_MS = 100
FRAMES_PER_CHUNK = int(SAMPLE_RATE * (CHUNK_MS / 1000.0))
# --- WebSocket Start Message ---
# This dictionary is sent once upon connection to configure the VAD.
# Note: This is a very aggressive VAD setting for high speed transcription.
START_MSG = {
"type": "start",
"sample_rate": 16000,
"channels": 1,
"single_utterance": False, # Keep connection open after each transcript
"vad": {
"threshold": 0.5, # VAD sensitivity. Lower is more sensitive. 0.5 is a good baseline.
"min_silence_ms": 100, # Milliseconds of silence to trigger an End-of-Speech event.
"speech_pad_ms": 200, # Milliseconds of audio to include before speech starts.
"final_silence_s": 0.1, # Additional silence to wait for at the end.
"start_trigger_ms": 36, # How many ms of speech are needed to start transcribing.
"min_voiced_ms": 48, # Utterance is discarded if it has less than this much voiced audio.
"min_chars": 1, # Discard transcript if it has fewer characters than this.
"min_words": 1, # Discard transcript if it has fewer words than this.
"amp_extend": 1200, # A non-speech noise can extend utterance, but not start it.
"force_decode_ms": 0, # Forces a transcription after a set amount of ms have elapsed
"debug": False, # show debugging messages
}
}
# --- Graceful Shutdown ---
shutdown_event = asyncio.Event()
def _handle_sigint(*_):
print("\nStopping…", flush=True)
shutdown_event.set()
async def fetch_streaming_token() -> str:
"""
Exchanges your API key for a short-lived streaming token.
Send the token via ?streaming_token=... in the WS URL.
"""
if not API_KEY or API_KEY == "YOUR_API_KEY_HERE":
raise RuntimeError("Set FENNEC_API_KEY (or replace API_KEY).")
async with httpx.AsyncClient(timeout=10) as client:
# The ASR API reads your key from the X-API-Key header and returns {"token": ""}
resp = await client.post(
HTTP_TOKEN_URL,
headers={"X-API-Key": API_KEY, "content-type": "application/json"},
json={}, # body not required; header is what matters
)
resp.raise_for_status()
data = resp.json()
token = data.get("token")
if not token:
raise RuntimeError(f"Token endpoint returned no token: {data}")
return token
async def audio_sender(ws, stream):
"""Reads from the mic and sends audio chunks to the WebSocket."""
print("\n🎙️ Streaming mic… speak, and pause to see the transcript. (Ctrl+C to stop)", flush=True)
while not shutdown_event.is_set():
try:
data, _ = await asyncio.to_thread(stream.read, FRAMES_PER_CHUNK)
await ws.send(bytes(data))
except websockets.exceptions.ConnectionClosed:
break
try:
await ws.send('{"type":"eos"}')
except websockets.exceptions.ConnectionClosed:
pass
async def message_receiver(ws):
"""Listens for messages from the server and prints them."""
async for msg in ws:
try:
data = json.loads(msg)
if text := data.get("text"):
print(text, flush=True)
except Exception:
# Ignore non-JSON messages
pass
async def main():
"""Main function to connect and stream microphone audio."""
print("Fetching streaming token…")
token = await fetch_streaming_token()
WEBSOCKET_URL = f"{WS_BASE}?streaming_token={token}"
print(f"Connecting to: {WEBSOCKET_URL}")
try:
async with websockets.connect(
WEBSOCKET_URL,
max_size=None,
ping_interval=5,
) as ws:
# Send the configuration message.
await ws.send(json.dumps(START_MSG))
print("✅ WebSocket connected. Sent VAD configuration.")
with sd.RawInputStream(
samplerate=SAMPLE_RATE,
channels=CHANNELS,
dtype=DTYPE,
blocksize=FRAMES_PER_CHUNK,
) as stream:
# Run the sender and receiver concurrently.
sender_task = asyncio.create_task(audio_sender(ws, stream))
receiver_task = asyncio.create_task(message_receiver(ws))
await asyncio.wait([sender_task, receiver_task], return_when=asyncio.FIRST_COMPLETED)
except Exception as e:
print(f"❌ An unexpected error occurred: {e}")
if __name__ == "__main__":
if not sd.query_devices(kind='input'):
print("\n❌ No input microphone found.")
else:
signal.signal(signal.SIGINT, _handle_sigint)
asyncio.run(main())
```
```ts theme={null}
#!/usr/bin/env node
"use strict";
/**
* Streams microphone audio to the ASR WebSocket API with VAD configuration,
* printing finalized transcripts as they arrive. (Ctrl+C to stop)
*
* Auth flow:
* 1) POST your API key to the HTTPS token endpoint to get a short-lived streaming token.
* 2) Connect to the WebSocket using ?streaming_token=... in the URL (no header needed).
*
* Requires:
* - Node 18+ (for global fetch)
* - npm i ws mic dotenv
* - A working system recorder (arecord/sox/CoreAudio depending on OS)
* - .env with FENNEC_API_KEY=your_key
*/
require("dotenv").config();
const HTTP_TOKEN_URL = "https://api.fennec-asr.com/api/v1/transcribe/streaming-token";
const WS_BASE = "wss://api.fennec-asr.com/api/v1/transcribe/stream";
const KEY = process.env.FENNEC_API_KEY;
if (!KEY) {
console.error("Set FENNEC_API_KEY in your environment (or .env).");
process.exit(1);
}
// --- Audio Configuration ---
const SAMPLE_RATE = 16_000;
const CHANNELS = 1;
const CHUNK_MS = 100;
const DTYPE_BYTES = 2; // int16 -> 2 bytes/sample
const FRAMES_PER_CHUNK = Math.floor(SAMPLE_RATE * (CHUNK_MS / 1000));
const CHUNK_BYTES = FRAMES_PER_CHUNK * CHANNELS * DTYPE_BYTES;
// --- Start Message (matches the Python example) ---
const START_MSG = {
type: "start",
sample_rate: 16000,
channels: 1,
single_utterance: false, // Keep connection open after each transcript
vad: {
threshold: 0.5, // Lower => more sensitive
min_silence_ms: 100, // Silence to trigger end-of-speech
speech_pad_ms: 200, // Audio to include before speech starts
final_silence_s: 0.1, // Extra silence to wait at the end
start_trigger_ms: 36, // How many ms of speech to start transcribing
min_voiced_ms: 48, // Discard utterances with less voiced audio
min_chars: 1,
min_words: 1,
amp_extend: 1200, // Non-speech noise can extend utterance, not start it
force_decode_ms: 0, // Force a transcription after N ms
debug: false,
},
};
(async () => {
const WebSocket = require("ws");
const mic = require("mic");
async function fetchStreamingToken() {
// The ASR API reads your key from the X-API-Key header and returns { token: "" }
const resp = await fetch(HTTP_TOKEN_URL, {
method: "POST",
headers: {
"X-API-Key": KEY,
"content-type": "application/json",
},
body: JSON.stringify({}), // body not required; header is what matters
});
if (!resp.ok) {
const text = await resp.text().catch(() => "");
throw new Error(`Token request failed: ${resp.status} ${text}`);
}
const data = await resp.json();
if (!data?.token) throw new Error("Token endpoint returned no token");
return data.token;
}
console.log("Fetching streaming token…");
const token = await fetchStreamingToken();
const WEBSOCKET_URL = `${WS_BASE}?streaming_token=${encodeURIComponent(token)}`;
console.log(`Connecting to: ${WEBSOCKET_URL}`);
const ws = new WebSocket(WEBSOCKET_URL, {
perMessageDeflate: false,
maxPayload: 0,
});
// Graceful shutdown helpers
let shuttingDown = false;
const shutdown = (micInstance) => {
if (shuttingDown) return;
shuttingDown = true;
console.log("\nStopping…");
try {
if (ws.readyState === WebSocket.OPEN) ws.send('{"type":"eos"}');
} catch {}
try {
micInstance?.stop();
} catch {}
try {
ws.close();
} catch {}
};
process.on("SIGINT", () => shutdown());
ws.on("open", () => {
(async () => {
try {
await ws.send(JSON.stringify(START_MSG));
console.log("✅ WebSocket connected. Sent VAD configuration.");
// Configure mic capture (raw signed 16-bit little-endian, mono, 16 kHz)
const micInstance = mic({
rate: String(SAMPLE_RATE),
channels: String(CHANNELS),
bitwidth: "16",
encoding: "signed-integer",
endian: "little",
device: process.env.MIC_DEVICE || undefined, // optional override
fileType: "raw",
exitOnSilence: 0,
});
const micStream = micInstance.getAudioStream();
let buffer = Buffer.alloc(0);
console.log(
"\n🎙️ Streaming mic… speak, and pause to see the transcript. (Ctrl+C to stop)"
);
micStream.on("data", (chunk) => {
// Accumulate mic bytes and send fixed-size chunks
buffer = Buffer.concat([buffer, chunk]);
while (buffer.length >= CHUNK_BYTES && ws.readyState === WebSocket.OPEN) {
const slice = buffer.subarray(0, CHUNK_BYTES);
ws.send(slice); // binary audio payload
buffer = buffer.subarray(CHUNK_BYTES);
}
});
micStream.on("error", (err) => {
console.error("Mic error:", err?.message || err);
});
ws.on("message", (msg) => {
try {
const data = JSON.parse(msg.toString("utf8"));
if (data?.text) {
console.log(data.text);
}
} catch {
// Ignore non-JSON messages
}
});
ws.on("close", () => shutdown(micInstance));
ws.on("error", (e) => {
console.error("WebSocket error:", e?.message || e);
shutdown(micInstance);
});
micInstance.start();
} catch (e) {
console.error("Startup error:", e?.message || e);
shutdown();
}
})();
});
ws.on("error", (e) => {
console.error("WebSocket error (pre-open):", e?.message || e);
});
})();
```
## Understanding VAD
Voice Activity Detection acts as the Fennec's "ears," listening for speech and silence to intelligently segment the continuous audio stream into meaningful chunks, or "utterances." Properly tuning the VAD settings is the most critical step to tailor the transcription behavior for your specific application.
All VAD settings are passed inside the vad dictionary within your initial start message.
### Core VAD Parameters
These parameters control the fundamental timing and sensitivity of the speech detection.
VAD Sensitivity. This float between 0.0 and 1.0 determines how loud a sound must be to be considered speech. Lower values (e.g., 0.3) are more sensitive and can pick up quieter speakers or whispers, but may also misclassify background noise as speech. Higher values (e.g., 0.7) are less sensitive and are ideal for ignoring background noise in loud environments. 0.5 is a balanced starting point.
End-of-Speech Trigger. This is the most important parameter for controlling the latency-vs-completeness tradeoff. It defines the duration of silence (in milliseconds) the system will wait for before finalizing an utterance and sending the transcript. A low value (250) results in fast, short transcripts. A high value (1000) results in longer, more complete sentences but increases the perceived delay.
Pre-Speech Audio Buffer. This setting captures a small amount of audio (in milliseconds) from before the VAD officially detected the start of speech. It is crucial for preventing the first syllable or word of an utterance from being cut off (e.g., ensuring "Okay, let's start" isn't transcribed as "kay, let's start").
Post-Utterance Silence. This specifies an extra duration of silence (in seconds) to append to the end of a finalized utterance before it's sent for transcription. This can sometimes provide the ASR model with more non-speech context, which can help in correctly punctuating the very end of a sentence, but it will directly add to the overall latency. Use with caution.
### Advanced Filtering and Control Parameters
These parameters provide fine-grained control over what constitutes a valid utterance, helping to filter out noise and unwanted segments.
Minimum Speech to Start. An utterance will not begin until the VAD detects at least this many milliseconds of continuous speech. This helps prevent very short vocal tics or brief background noises from incorrectly starting a new transcription segment.
Minimum Voiced Audio per Utterance. After an utterance is segmented, the system checks if it contains at least this much voiced audio. If not, the entire segment is discarded. This is a powerful tool for filtering out non-speech sounds like coughs, door slams, or clicks that might otherwise be long enough to form a segment.
Minimum Transcript Character Length. After a segment is transcribed, the system checks the character count of the resulting text. If it's less than this value, the transcript is discarded. This is a post-processing filter useful for eliminating transcripts of filler words like "a" or "uh".
Minimum Transcript Word Count. Similar to min\_chars, but works on the word count. Setting this to 2 would discard single-word utterances (e.g., "Okay", "Right").
Non-Speech Utterance Extension. This allows a non-speech sound (below the threshold) to extend a currently active speech utterance for up to this many milliseconds. However, such a sound cannot start a new utterance. This is useful for cases where a speaker's voice trails off or a quiet background noise occurs mid-sentence, preventing the utterance from cutting off prematurely.
Maximum Utterance Duration. If this is set to a value greater than 0 (e.g., 15000 for 15 seconds), it acts as a safety net. The system will automatically finalize and transcribe an utterance after it reaches this duration, even if the speaker has not paused. This prevents infinitely long transcripts from users who don't pause naturally.
Enable Debug Logging. If set to true, the server will send additional diagnostic messages along with the transcripts. This is useful for development and fine-tuning VAD settings, but should be disabled in production.
### VAD Configuration Examples
Here are three fully-configured START\_MSG examples tailored for different real-world situations.
This profile is balanced for users dictating notes or emails. It favors creating more complete sentences over instant responsiveness.
```python theme={null}
START_MSG = {
"type": "start",
"sample_rate": 16000,
"channels": 1,
"single_utterance": False,
"vad": {
"threshold": 0.45,
"min_silence_ms": 800, # Longer silence to allow for pauses mid-sentence
"speech_pad_ms": 200,
"start_trigger_ms": 36,
"min_voiced_ms": 100,
"min_chars": 2,
"min_words": 1,
"force_decode_ms": 60000 # Force a transcript after 60s for long-winded speakers
}
}
```
**Rationale:**
* `min_silence_ms` is high (`800`) to give users time to think between clauses without breaking the sentence.
* `force_decode_ms` is enabled as a fallback, ensuring that even if a user talks for a long time, they will get a transcript back every 20 seconds.
* `threshold` is slightly more sensitive (`0.45`) for more natural-sounding dictation.
This profile is hardened for use-cases like a call center or public kiosk where significant background noise is expected. The goal is to reject as much non-speech audio as possible.
```python theme={null}
START_MSG = {
"type": "start",
"sample_rate": 16000,
"channels": 1,
"single_utterance": False,
"vad": {
"threshold": 0.65, # High threshold to ignore background chatter
"min_silence_ms": 400,
"speech_pad_ms": 150,
"start_trigger_ms": 100, # Requires a clear, intentional start to speech
"min_voiced_ms": 250, # Very high requirement for voiced audio
"min_chars": 1,
"min_words": 1,
"amp_extend": 0, # Disable extension from quiet sounds
"force_decode_ms": 0
}
}
```
**Rationale:**
* `threshold` is set high (`0.65`) to make the VAD less sensitive, effectively ignoring lower-volume background sounds.
* `start_trigger_ms` and `min_voiced_ms` are both significantly increased to ensure only strong, clear speech from the primary speaker creates an utterance.
* `min_chars` and `min_words` are increased to filter out partial words or phrases picked up from background conversations.
* `amp_extend` is disabled (`0`) to prevent background hum or noise from incorrectly keeping an utterance alive.
Please know that building with VAD is an iterative process, and giving your end user indirect control of these parameters can enhance the experience for their specific situation.
# Transcribe from a file
Source: https://docs.fennec-asr.com/transcribe-from-a-file
## Get started in five minutes
### Step 1: Get your API Key
Your API key is used to authenticate your requests. Follow [this link](https://app.fennec-asr.com/dashboard/api-keys) to grab your first API key.
### Step 2: Grab an audio file
Grab an audio file of your choice, or download [this audio file](https://static.wixstatic.com/mp3/748cf5_b8310da2c11b432f86f14922a89bec02.mp3)
and save it as `sample.mp3` in the same directory where you will save the Python script below.
Our API works best with common audio formats like MP3, WAV, M4A, OGG, WEBM, and FLAC.
### Step 3: Install the SDK
```python theme={null}
pip install fennec-asr
```
### Step 4: Transcribe!
```python theme={null}
from fennec_asr import FennecASRClient
print(FennecASRClient(api_key="YOUR_API_KEY").transcribe_file(r"sample.mp3"))
```
Don't want to use the SDK? See the example code below:
```python quickstart.py theme={null}
import os
import time
import requests
BASE_URL = "https://api.fennec-asr.com/api/v1"
API_KEY = "YOUR_API_KEY_HERE" # 👈 Paste your API key here
AUDIO_PATH = "sample.mp3"
POLL_INTERVAL_S = 3
def transcribe_audio():
if not os.path.exists(AUDIO_PATH):
print(f"❌ Error: Audio file not found at '{AUDIO_PATH}'.")
print(" Please make sure the audio file is in the same directory as this script.")
return
if "YOUR_API_KEY_HERE" in API_KEY:
print("❌ Error: Please replace 'YOUR_API_KEY_HERE' with your actual API key.")
return
headers = {"X-API-Key": API_KEY}
with open(AUDIO_PATH, "rb") as audio_file:
files = {"audio": (os.path.basename(AUDIO_PATH), audio_file, "audio/mpeg")}
print(f"Submitting '{AUDIO_PATH}' for transcription...")
try:
# 1. Submit the job
submit_response = requests.post(f"{BASE_URL}/transcribe", headers=headers, files=files)
submit_response.raise_for_status()
job_id = submit_response.json().get("job_id")
print(f"✅ Job submitted successfully! Job ID: {job_id}\n")
# 2. Poll for the result
status_url = f"{BASE_URL}/transcribe/status/{job_id}"
while True:
status_response = requests.get(status_url, headers=headers)
status_response.raise_for_status()
data = status_response.json()
status = data.get("status")
if status == "completed":
print("\n🎉 Transcription Complete!")
print("-" * 25)
print(data.get("transcript"))
print("-" * 25)
break
elif status == "failed":
print("\n❌ Transcription failed.")
print("Error:", data.get("transcript"))
break
else:
print(f" Current status: '{status}'... waiting.")
time.sleep(POLL_INTERVAL_S)
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
if e.response:
print(f"Response Body: {e.response.text}")
# --- Run the script ---
if __name__ == "__main__":
transcribe_audio()
```
```ts quickstart.ts theme={null}
#!/usr/bin/env node
"use strict";
const BASE = "https://api.fennec-asr.com/api/v1";
const KEY = process.env.ASR_API_KEY;
const file = process.argv[2] || "sample.mp3";
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
(async () => {
if (!KEY) throw new Error("Set ASR_API_KEY");
const { readFile } = require("fs/promises");
const { basename } = require("path");
const form = new FormData();
form.append("audio", new Blob([await readFile(file)], { type: "audio/mpeg" }), basename(file));
const sub = await fetch(`${BASE}/transcribe`, {
method: "POST",
headers: { "X-API-Key": KEY },
body: form,
duplex: "half",
}).then((r) => r.json());
if (!sub?.job_id) throw new Error("No job_id");
for (;;) {
const s = await fetch(`${BASE}/transcribe/status/${sub.job_id}`, {
headers: { "X-API-Key": KEY },
}).then((r) => r.json());
if (s.status === "completed") return void console.log(s.transcript || "");
if (s.status === "failed") {
console.error("Failed:", s.transcript || "");
process.exit(1);
}
await wait(3000);
}
})().catch((e) => {
console.error(e.message || e);
process.exit(1);
});
```
## Next Steps
Now that you've completed your first transcription, you can explore the API's more powerful features.
Submit a public URL for transcription instead of uploading a file.
Provide hints and jargon to the AI to get more accurate results for specialized audio.
Automatically add newlines and paragraphs to your transcript based on speech pauses.
Explore all endpoints, parameters, and schemas in detail.
# Transcribe from a URL
Source: https://docs.fennec-asr.com/transcribe-from-a-url
### Step 1: Get your API Key
Your API key is used to authenticate your requests. Follow [this link](https://app.fennec-asr.com/dashboard/api-keys) to grab your first API key.
### Step 2: Install the SDK
```python theme={null}
pip install fennec-asr
```
Let's write a simple script to send a URL to the API.
### Step 3: Transcribe!
```python theme={null}
from fennec_asr import FennecASRClient
print(FennecASRClient(api_key="YOUR_API_KEY").transcribe_url("https://upload.wikimedia.org/wikipedia/commons/3/3f/En-History_of_Corpus_Christi%2C_Texas.ogg"))
```
Don't want to use the SDK? See the example code below:
```python quickstart.py theme={null}
import requests
import json
import time
BASE_URL = "https://api.fennec-asr.com/api/v1"
API_KEY = "YOUR_API_KEY_HERE"
AUDIO_URL = "https://upload.wikimedia.org/wikipedia/commons/3/3f/En-History_of_Corpus_Christi%2C_Texas.ogg"
POLL_INTERVAL_SECONDS = 3
MAX_WAIT_SECONDS = 300
headers = {"Content-Type": "application/json", "X-API-Key": API_KEY}
payload = {"audio": AUDIO_URL, "context": "testing with a python script"}
def transcribe_audio_url():
try:
# 1. Submit the job
print("Submitting transcription job...")
submit_response = requests.post(f"{BASE_URL}/transcribe/url", headers=headers, data=json.dumps(payload))
submit_response.raise_for_status()
job_info = submit_response.json()
job_id = job_info.get("job_id")
print(f"✅ Job submitted successfully! Job ID: {job_id}\n")
# 2. Poll for the result
start_time = time.time()
while time.time() - start_time < MAX_WAIT_SECONDS:
print("Polling for status...")
status_url = f"{BASE_URL}/transcribe/status/{job_id}"
status_response = requests.get(status_url, headers=headers)
if status_response.status_code == 200:
status_data = status_response.json()
job_status = status_data.get("status")
print(f" Current status: '{job_status}'")
if job_status == "completed":
print("\n🎉 Transcription complete!")
print("Transcript:", status_data.get("transcript"))
break
elif job_status == "failed":
print("\n❌ Transcription failed.")
print("Error:", status_data.get("transcript")) # Assuming error is stored in transcript field
break
else:
print(f" Error polling status: {status_response.status_code}")
time.sleep(POLL_INTERVAL_SECONDS)
else:
print("\n⏰ Polling timed out.")
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
# --- Run the script ---
if __name__ == "__main__":
transcribe_audio_url()
```
```ts quickstart.ts theme={null}
#!/usr/bin/env node
"use strict";
const BASE = "https://api.fennec-asr.com/api/v1";
const KEY = process.env.ASR_API_KEY;
const AUDIO = process.argv[2] || "https://upload.wikimedia.org/wikipedia/commons/3/3f/En-History_of_Corpus_Christi%2C_Texas.ogg";
const INTERVAL = 3000, MAX = 300000;
const wait = (ms) => new Promise(r => setTimeout(r, ms));
(async () => {
if (!KEY) throw new Error("Set ASR_API_KEY");
const submit = await fetch(`${BASE}/transcribe/url`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": KEY },
body: JSON.stringify({ audio: AUDIO, context: "testing with a node script" }),
});
if (!submit.ok) throw new Error(`Submit ${submit.status}: ${await submit.text()}`);
const { job_id } = await submit.json();
if (!job_id) throw new Error("No job_id");
const start = Date.now();
for (;;) {
const res = await fetch(`${BASE}/transcribe/status/${job_id}`, { headers: { "X-API-Key": KEY } });
if (!res.ok) throw new Error(`Status ${res.status}: ${await res.text()}`);
const s = await res.json();
if (s.status === "completed") return void console.log(s.transcript || "");
if (s.status === "failed") { console.error("Failed:", s.transcript || ""); process.exit(1); }
if (Date.now() - start > MAX) { console.error("Timed out."); process.exit(1); }
await wait(INTERVAL);
}
})().catch(e => { console.error(e.message || e); process.exit(1); });
```
## Next Steps
Now that you've completed your first transcription, you can explore the API's more powerful features.
Provide hints and jargon to the AI to get more accurate results for specialized audio.
Automatically add newlines and paragraphs to your transcript based on speech pauses.
Explore all endpoints, parameters, and schemas in detail.