Frontend
Build a local call-review panel with Python
Nathan Roll Dev.to (EN Zone)
4 views
An audio analysis response is easier to inspect when you can hear the passage beside its transcript. This tutorial builds a local review page that lets you replay a speaker turn, search the words or annotations, and read vocal-expression scores separately from what was said.
We will start with an actual saved response and a licensed public recording. That path needs Python and a browser, but no API key, model download, or inference request. Then we will connect the same page to the Oruk Python SDK for your own recordings.
Disclosure: this example is maintained by Oruk, the API provider. The article and implementation were prepared with an AI coding agent and checked with automated tests and browser playback tests. The sample demonstrates an integration; it is not an accuracy evaluation or a customer case study.
Run the saved example
You need Python 3.10 or later, curl, and a current browser. The complete renderer uses Python's standard library. It generates an HTML file with a native audio player, plain JavaScript controls, and no third-party scripts or fonts.
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.txt
Open review.html in your browser. Keep recording.wav next to it. On Windows, use your Python launcher if python3 is not available.
The 14.45-second grocery conversation comes from The Agentic Data Company's Open Yap 1K public sample, licensed under CC BY 4.0. Oruk excerpted and downmixed it. The attribution file is included in the generated report; preserve it when sharing the sample. Other demo recordings can have different permissions.
The complete program is available under the MIT license. The saved JSON contains the model's original output, not substitute scores written for this tutorial.
Check the result before adding an API call
The page has four passages. The first runs from 0.185 to 4.325 seconds and has the local label speaker_0. Its transcript begins:
I'm sorry. The price of food has gone up so much.
The returned emotion annotation is disappointed, with a score of 0.868 when rounded. The speaking-style annotation is casual, with a rounded score of 0.869. The report displays them in separate lists below the words.
Press Play passage 1 to replay that interval. Playback pauses near the returned end time. Choose Play from here without stopping when you want to hear the surrounding context. Searching frustrated should leave two passages visible; clearing the search restores all four. Filtering reads the existing response and does not call the model.
There is a useful limitation in this example: its words arrays are empty. The response provides speaker-turn boundaries, not individual word timing. The page says that explicitly. If another response includes word timestamps, a details control exposes those returned values and lets you replay a word. It never assigns timestamps by dividing a passage's duration by its word count.
Analyze your own recording
For a fresh analysis, use an English recording you have permission to process, up to 30 MB and 60 minutes. Create a key through your Oruk account and set ORUK_API_KEY in your environment. The live API uses the current subscription terms; the saved-data example above does not require a subscription.
This command reuses the SDK's existing complete file example rather than introducing another upload implementation:
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 before this command.
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.html
The renderer runs only if analysis succeeds. It reads the saved file; it does not upload or analyze the recording again. Do not attach the grocery sample's attribution to your own audio. You can supply your own source note with --attribution-file.
If you already have a Python application, use this request-and-save step instead of the analyze-file.py command:
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,
)
Path("my-response.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
)
Choose the CLI or this snippet. Running both makes two logical requests. Unified analysis returns the transcript, emotion, and speaking style together; separate transcription and affect calls would process the audio separately.
diarize=True makes the segments speaker turns. Leave num_speakers unset unless you know the number of speakers. The labels belong to that recording; speaker_0 does not mean “customer,” “agent,” or a persistent identity. These SDK calls use the recorded-file API, not the separate realtime WebSocket preview.
Keep the fields separate
The report is built around a small data model:
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"))
for word in segment.get("words") or []:
print(word["word"], word["start"], word["end"])
text is a model transcript and can contain transcription errors. emotions and styles contain selected acoustic model scores. They describe model support for how speech sounds; they do not reveal a person's private feelings or intent.
Several labels can coexist. Their scores need not sum to one, and the numbers are not automatically calibrated probabilities. Emotion output includes the highest-scoring label when none meets its selection threshold. An empty style list means no style was returned, which does not establish absence. See the score interpretation reference before designing application thresholds.
The report keeps the returned order and values, displays three decimal places, and preserves the full score in the meter value and numeric tooltip. It does not turn a high score into a customer-risk verdict or rank a speaker's performance.
Replay from the browser's media clock
The essential interaction is a seek followed by playback:
audio.currentTime = start;
await audio.play();
The complete program also tracks the chosen end time, checks that boundary while playback is running, and pauses near it. It stops that animation-frame check when paused. Native seeking outside the selected interval releases the stop boundary. Highlighting uses audio.currentTime, so it follows the recording rather than a separate elapsed-time timer. Overlapping intervals can both be active.
These boundaries are useful for reviewing a passage. They are not sample-accurate editing cuts. Browser media clocks and seek precision impose limits.
The renderer escapes transcript text, labels, titles, and source notes before inserting them into HTML. It does not inject a tagged transcript as executable markup. Its Content Security Policy permits the generated script and style by hash, allows local media, and blocks network connections. The generated report has no analytics or API calls.
Handle the failures a reviewer will encounter
Invalid credentials or a rejected request: the SDK CLI exits unsuccessfully, with the HTTP status, code, and request ID on stderr. An empty stdout file is not a successful 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, which does not establish exactly-once processing. Python network and timeout errors propagate. Do not add an unbounded retry loop.
Malformed saved data: the renderer rejects API error objects, unsupported tasks, invalid intervals, and non-finite or out-of-range scores before writing the report. It refuses to overwrite its input files.
Empty segments or missing scores: the page says what was not provided. The full transcript remains available even when there are no timed passages.
The wrong or missing recording: the page reports loading/playback errors and warns if the audio duration differs from the response. Equal durations cannot prove two files belong together; use the exact recording that produced the response.
Keep the report's relative audio path intact when moving files. The HTML contains the transcript, even though it never contains the API key. Treat the report and recording under the same permissions and access controls.
This is a local review tool, not an authenticated multi-user application. A team version needs recording permissions, reviewer access, storage and retention rules, and representative evaluation before introducing any automatic triage threshold. The small example provides a place to inspect those decisions against the original audio.
Further reference: speaker diarization, SDK setup and errors, and analysis response fields.
Read original: https://dev.to/nathanroll/build-a-local-call-review-panel-with-python-2dgh
← Previous
After the Sprint: A 72-Hour Build Retrospective - Spoiler: It Wasn't Secure
Next →
Anne OS Kids: A Small Operating System with a Big Inspiration
Related
I Didn’t Have a Developer Website, So I Built One.
Frontend
0
DEV Community
Keeping a Legacy Cache Consistent During an Incremental Migration
Frontend
0
DEV Community
@supports named-feature() lets you branch on behavior, not syntax
Frontend
0
DEV Community
Anne OS Kids: A Small Operating System with a Big Inspiration
Frontend
3
Dev.to (EN Zone)
Comments0
No comments yet — be the first