Python · Recorded audio · Updated September 17, 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.
Record whether a passage helped
Write the review task at the top of the panel before rating passages, for example locating unanswered billing questions. For each passage, choose Useful, Not useful, or Uncertain, and optionally explain why. Leave passages you have not assessed at Not reviewed. A usefulness judgment concerns your task; it does not establish that an emotion label is correct.
Press Start review timer when you begin assessing a passage and pause it when you stop. Starting another passage, filtering the active passage out, or hiding the browser page pauses the timer. You can resume it later. This records elapsed review time while the page is visible, not verified attention or time spent in another application. If you never start a passage’s timer, its exported duration stays null.
At the bottom, choose what the review represents. Use Demonstration or software fixture for the supplied sample and test runs. Use Authorized reviewer pilot only for recordings you have permission to review, with the review task stated. The default is Unverified / not specified; selecting a pilot does not verify consent or independence.
Download review JSON saves every passage, including unreviewed ones and passages hidden by search. Each record contains its original interval, usefulness, reason and recorded duration. The file also includes the saved response’s SHA-256, returned model and model revision when present. A missing revision stays null. Read the version 1 JSON schema and field and evaluation notes.
The tool keeps edits in page memory and downloads the file locally. It sends no review data anywhere and omits audio, transcripts and speaker IDs from the export. Your free-text notes are included, so avoid names and private details. Check the downloaded file before closing or reloading; the page does not restore unsaved reviews.
For a pilot, agree on the task and which recordings will be reviewed before looking at model output. Keep uncertain and unreviewed passages in the report rather than dropping them. A before/after claim also needs a comparable baseline and consistent timing rules. These exports provide observations for that evaluation; they do not calculate an improvement or grant permission to publish customer results.
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 https://oruk.ai/sdk/oruk-0.2.15-py3-none-any.whl
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: The Python SDK 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.