<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nathan Roll</title>
    <description>The latest articles on DEV Community by Nathan Roll (@nathanroll).</description>
    <link>https://dev.to/nathanroll</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4114712%2F0fa832f5-db68-48fc-a850-2990a8d2deb5.png</url>
      <title>DEV Community: Nathan Roll</title>
      <link>https://dev.to/nathanroll</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nathanroll"/>
    <language>en</language>
    <item>
      <title>Build a local call-review panel with Python</title>
      <dc:creator>Nathan Roll</dc:creator>
      <pubDate>Tue, 08 Sep 2026 01:31:19 +0000</pubDate>
      <link>https://dev.to/nathanroll/build-a-local-call-review-panel-with-python-2dgh</link>
      <guid>https://dev.to/nathanroll/build-a-local-call-review-panel-with-python-2dgh</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the saved example
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;oruk-call-review
&lt;span class="nb"&gt;cd &lt;/span&gt;oruk-call-review
curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; call-review.py https://oruk.ai/examples/call-review.py
curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; response.json https://oruk.ai/samples/conversations/02-grocery-prices.oruk.json
curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; recording.wav https://oruk.ai/samples/conversations/02-grocery-prices.wav
curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; attribution.txt https://oruk.ai/examples/call-review-attribution.txt
python3 call-review.py &lt;span class="nt"&gt;--response&lt;/span&gt; response.json &lt;span class="nt"&gt;--audio&lt;/span&gt; recording.wav &lt;span class="nt"&gt;--output&lt;/span&gt; review.html &lt;span class="nt"&gt;--attribution-file&lt;/span&gt; attribution.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;review.html&lt;/code&gt; in your browser. Keep &lt;code&gt;recording.wav&lt;/code&gt; next to it. On Windows, use your Python launcher if &lt;code&gt;python3&lt;/code&gt; is not available.&lt;/p&gt;

&lt;p&gt;The 14.45-second grocery conversation comes from &lt;a href="https://huggingface.co/datasets/TheAgenticDataCompany/open-yap-1k" rel="noopener noreferrer"&gt;The Agentic Data Company's Open Yap 1K public sample&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://oruk.ai/examples/call-review.py" rel="noopener noreferrer"&gt;complete program&lt;/a&gt; is available under the &lt;a href="https://oruk.ai/examples/call-review.LICENSE.txt" rel="noopener noreferrer"&gt;MIT license&lt;/a&gt;. The &lt;a href="https://oruk.ai/samples/conversations/02-grocery-prices.oruk.json" rel="noopener noreferrer"&gt;saved JSON&lt;/a&gt; contains the model's original output, not substitute scores written for this tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the result before adding an API call
&lt;/h2&gt;

&lt;p&gt;The page has four passages. The first runs from 0.185 to 4.325 seconds and has the local label &lt;code&gt;speaker_0&lt;/code&gt;. Its transcript begins:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm sorry. The price of food has gone up so much.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The returned emotion annotation is &lt;code&gt;disappointed&lt;/code&gt;, with a score of 0.868 when rounded. The speaking-style annotation is &lt;code&gt;casual&lt;/code&gt;, with a rounded score of 0.869. The report displays them in separate lists below the words.&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Play passage 1&lt;/strong&gt; to replay that interval. Playback pauses near the returned end time. Choose &lt;strong&gt;Play from here without stopping&lt;/strong&gt; when you want to hear the surrounding context. Searching &lt;code&gt;frustrated&lt;/code&gt; should leave two passages visible; clearing the search restores all four. Filtering reads the existing response and does not call the model.&lt;/p&gt;

&lt;p&gt;There is a useful limitation in this example: its &lt;code&gt;words&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyze your own recording
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;ORUK_API_KEY&lt;/code&gt; in your environment. The live API uses the &lt;a href="https://oruk.ai/pricing" rel="noopener noreferrer"&gt;current subscription terms&lt;/a&gt;; the saved-data example above does not require a subscription.&lt;/p&gt;

&lt;p&gt;This command reuses the SDK's existing complete file example rather than introducing another upload implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="c"&gt;# macOS / Linux; on Windows use .venv\Scripts\activate&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;oruk&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.2.6
curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; analyze-file.py https://oruk.ai/examples/analyze-file.py

&lt;span class="c"&gt;# Set ORUK_API_KEY in the environment before this command.&lt;/span&gt;
python analyze-file.py my-recording.wav &lt;span class="nt"&gt;--diarize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; my-response.json &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  python call-review.py &lt;span class="nt"&gt;--response&lt;/span&gt; my-response.json &lt;span class="nt"&gt;--audio&lt;/span&gt; my-recording.wav &lt;span class="nt"&gt;--output&lt;/span&gt; my-review.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;--attribution-file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you already have a Python application, use this request-and-save step instead of the &lt;code&gt;analyze-file.py&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;oruk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Oruk&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Oruk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORUK_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-recording.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oruk-resonance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;diarize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-response.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;diarize=True&lt;/code&gt; makes the segments speaker turns. Leave &lt;code&gt;num_speakers&lt;/code&gt; unset unless you know the number of speakers. The labels belong to that recording; &lt;code&gt;speaker_0&lt;/code&gt; does not mean “customer,” “agent,” or a persistent identity. These SDK calls use the recorded-file API, not the separate realtime WebSocket preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the fields separate
&lt;/h2&gt;

&lt;p&gt;The report is built around a small data model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;speaker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Words:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Emotion scores:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emotions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Style scores:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;styles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;words&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;text&lt;/code&gt; is a model transcript and can contain transcription errors. &lt;code&gt;emotions&lt;/code&gt; and &lt;code&gt;styles&lt;/code&gt; contain selected acoustic model scores. They describe model support for how speech sounds; they do not reveal a person's private feelings or intent.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://oruk.ai/docs#labels" rel="noopener noreferrer"&gt;score interpretation reference&lt;/a&gt; before designing application thresholds.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replay from the browser's media clock
&lt;/h2&gt;

&lt;p&gt;The essential interaction is a seek followed by playback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;audio.currentTime&lt;/code&gt;, so it follows the recording rather than a separate elapsed-time timer. Overlapping intervals can both be active.&lt;/p&gt;

&lt;p&gt;These boundaries are useful for reviewing a passage. They are not sample-accurate editing cuts. Browser media clocks and seek precision impose limits.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle the failures a reviewer will encounter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invalid credentials or a rejected request:&lt;/strong&gt; the SDK CLI exits unsuccessfully, with the HTTP status, code, and request ID on stderr. An empty stdout file is not a successful response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporary service errors:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malformed saved data:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty segments or missing scores:&lt;/strong&gt; the page says what was not provided. The full transcript remains available even when there are no timed passages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wrong or missing recording:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Further reference: &lt;a href="https://oruk.ai/guides/speaker-diarization-python" rel="noopener noreferrer"&gt;speaker diarization&lt;/a&gt;, &lt;a href="https://oruk.ai/docs/sdks" rel="noopener noreferrer"&gt;SDK setup and errors&lt;/a&gt;, and &lt;a href="https://oruk.ai/docs#reference-analysis" rel="noopener noreferrer"&gt;analysis response fields&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
