Python · Recorded audio · September 7, 2026
Build a local call-review panel with Python
An analysis response is easier to inspect when you can hear the passage beside its transcript. This example turns saved Oruk JSON into a local HTML page: replay a speaker turn, search the words or annotations, and read vocal-expression scores separately from what was said.
The renderer uses Python’s standard library and a browser’s native audio player. Start with an existing public recording and its actual saved response: no API key, model download, or new inference request is needed for that path. Then reuse the same page for a recording analyzed with the Python SDK.
Run the saved example first
You need Python 3.10 or later, curl, and a current browser. Run these commands in a terminal, then open review.html in your browser. Keep recording.wav next to it. On Windows, use your Python launcher if python3 is not available.
mkdir oruk-call-review
cd oruk-call-review
curl --fail --location -o call-review.py https://oruk.ai/examples/call-review.py
curl --fail --location -o response.json https://oruk.ai/samples/conversations/02-grocery-prices.oruk.json
curl --fail --location -o recording.wav https://oruk.ai/samples/conversations/02-grocery-prices.wav
curl --fail --location -o attribution.txt https://oruk.ai/examples/call-review-attribution.txt
python3 call-review.py --response response.json --audio recording.wav --output review.html --attribution-file attribution.txtThe sample is a 14.45-second conversation about grocery prices from The Agentic Data Company’s Open Yap 1K public sample, used under CC BY 4.0. Oruk excerpted and downmixed the audio. Preserve the attribution when sharing it. These saved model outputs demonstrate integration; they are not human labels or an accuracy evaluation.
What you should see
The report contains four timed passages. The first runs from 0.185 to 4.325 seconds and is labeled speaker_0. Its transcript starts “I’m sorry. The price of food has gone up so much.” The returned emotion annotation is disappointed, score 0.868 when rounded, and its speaking-style annotation is casual, score 0.869.
Press Play passage 1 to replay that interval. The player pauses near the returned end time. Use Play from here without stopping for context. Search frustrated to show the two passages carrying that returned annotation; clear the search to restore all four. Nothing is re-scored when you filter.
This response has empty words arrays. It supplies turn boundaries, not individual word timing. The page keeps the words readable and says that word timings were not returned. If another analysis response includes word timestamps, a details control exposes those exact values and lets you replay a word. The example never spreads a turn’s duration evenly across its text.
Analyze your own recording once
Use an English recording you have permission to process, up to 30 MB and 60 minutes. Create an API key through your account and keep it in ORUK_API_KEY on your machine or server. API access follows the current plan terms; the saved-example path above does not require a subscription.
python3 -m venv .venv
# macOS / Linux; on Windows use .venv\Scripts\activate
source .venv/bin/activate
python -m pip install oruk==0.2.6
curl --fail --location -o analyze-file.py https://oruk.ai/examples/analyze-file.py
# Set ORUK_API_KEY in the environment, then analyze your own English recording:
python analyze-file.py my-recording.wav --diarize > my-response.json && \
python call-review.py --response my-response.json --audio my-recording.wav --output my-review.htmlThe command reuses the existing complete SDK workflow, including structured errors. The second command runs only if analysis succeeds. It does not upload the audio again. Do not attach the public sample’s attribution to your own recording; pass your own source note if needed.
If you are adding this to an existing Python application, the request and save step are:
import json
import os
from pathlib import Path
from oruk import Oruk
with Oruk(api_key=os.environ["ORUK_API_KEY"]) as client:
result = client.analyze(
"my-recording.wav",
model="oruk-resonance",
diarize=True,
)
# Save only after the request succeeds. The SDK key stays in this process.
Path("my-response.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
)Choose the CLI or the snippet; running both makes two logical requests. analyze returns transcript, emotion, and speaking style together. Separate transcription and affect calls would process the file separately. diarize=True makes segments speaker turns; leave num_speakers unset unless you know the count. This is the recorded-file workflow, not the realtime WebSocket preview.
Keep words and vocal expression separate
for segment in result["segments"]:
print(segment["start"], segment["end"], segment.get("speaker"))
print("Words:", segment.get("text"))
print("Emotion scores:", segment.get("emotions"))
print("Style scores:", segment.get("styles"))
# Only use word-level timing when it is actually returned.
for word in segment.get("words") or []:
print(word["word"], word["start"], word["end"])text is the model’s transcript. emotions and styles contain selected acoustic model scores. We show two distinct lists under each transcript rather than treating a tagged transcript as HTML or turning one high label into a verdict about the speaker.
Scores lie between zero and one, but are not automatically calibrated probabilities of private feelings. Several labels can be returned and need not sum to one. Emotion output includes the highest-scoring label when none clears its selection threshold; an empty style list means no style was returned. It does not prove a style is absent. The page preserves the returned values, shows three decimals, and retains the exact score in the meter and number tooltip. See score interpretation.
Speaker labels are local to the response. speaker_0 does not mean customer, agent, or a persistent person. Overlapping passages can both be highlighted while the audio plays. The app does not assign roles, decide who is angry, or rank anyone’s performance.
Replay from the actual media clock
The browser seeks by setting audio.currentTime to the returned start. It highlights intervals containing the current playback position and checks the selected end while playing. Pausing stops the boundary-check loop; seeking outside the selected interval releases the stop boundary. Native controls remain available throughout. Browser media timing is suitable for review, but these are not sample-accurate editing cuts.
The generated HTML escapes transcript text, speaker labels, titles, and annotations before inserting them. It uses no remote JavaScript, fonts, or analytics, and its Content Security Policy blocks network connections. The audio stays a relative local file reference. If you move the report, preserve that path. A changed file duration triggers a warning, but matching duration alone cannot prove you chose the right recording.
Handle failures before a reviewer trusts the page
- Authentication or invalid input: the SDK CLI exits unsuccessfully and reports status, code, and request ID to stderr. Do not treat the resulting empty stdout file as an analysis response.
- Temporary service errors: SDK 0.2.6 retries HTTP 429, 500, 502, 503, and 504 up to twice. It reuses a request ID for tracing; that does not establish exactly-once processing. Python network and timeout errors propagate. Avoid wrapping the CLI in an unbounded retry loop.
- Malformed saved JSON: the renderer rejects API error objects, unsupported tasks, invalid intervals, and non-finite or out-of-range scores before writing a page. It refuses to overwrite the response or audio file.
- No segments or labels: missing values remain visible as missing. An empty segment list leaves the full transcript available without invented times.
- Missing audio or blocked playback: the page reports the problem and leaves the native player available. Convert your own unsupported format locally if needed; the supplied WAV is browser-compatible.
The HTML contains the transcript even though the API key never enters it. Keep reports under the same access controls as recordings. This example is a local review tool, not an authenticated multi-user application. A team version needs recording permissions, storage and retention rules, reviewer access, and evaluation on representative audio before introducing any triage threshold.
