{% extends "base.html" %} {% from "fragments/export_menu.html" import export_menu %} {% from "fragments/outline_fallback.html" import outline_fallback %} {% block title %}Voxint — transcript {{ run.id.hex[:8] }}{% endblock %} {# Canonical parent, no origin tracking: transcripts belong to Runs. #} {% block cb_breadcrumb %}runs / {{ run.id.hex[:8] }}{% endblock %} {% block body %}

← run

Transcript {{ run.id.hex[:8] }}

{{ run.media_item.source_path }}

{# Read mode (issue #65): plain-anchor toggles, no JS. Each link preserves the current text variant so switching views never loses the reading position. The reading-view entry deliberately opens timestamp-free (the shareable "reading copy"); a within-mode toggle brings timestamps back. #} {# Translation toggle (issue #133): one link per current generation. A fresh generation links to the interleaved view; a stale one links to the run page card, where Re-translate lives. Rendered on the CORRECTED variant only — translations pair with the reviewed text, so raw/enhanced views never advertise a toggle whose links would silently switch the rendition (review finding). Absent in reading view and when the run has no translations. #} {% if not read and text.value == "corrected" and translation_ctx.views %} {% endif %} {% if not read and translation_ctx.note %}

{{ translation_ctx.note }}

{% endif %} {% if not read and translation_ctx.active %}

Machine-translated to {{ translation_ctx.active.label }} {%- if translation_ctx.active.source_language %} from {{ language_label(translation_ctx.active.source_language) }}{% endif %} by {{ translation_ctx.active.model }}, {{ translation_ctx.active.completed_at.strftime("%Y-%m-%d %H:%M") }} — a working rendition, not a certified translation.

{% endif %} {{ export_menu(run, text, translation_ctx.fresh) }} {% if (read and not read_rows) or (not read and not lines) %}

No transcript segments for this run.

{% elif read %} {# Shared reading paragraphs: Python decides grouping, clocks and markers; Jinja autoescape keeps hostile transcript text literal. #}
{% for row in read_rows %}

{% if row.clock %}{{ row.clock }} {% endif %}{% if row.speaker is not none %}{{ row.speaker }}: {% endif %}{% for run in row.runs %}{% if not loop.first %} {% endif %}{% if run.marker %}{{ run.marker }} {% endif %}{{ run.text }}{% endfor %}

{% endfor %}
{% else %} {# Island mount point (issue #48): the transcript-player island renders the audio-synced view over this fallback once hydrated. The server-rendered loop below is fully functional with JS disabled, htmx off, or the asset route misconfigured — the same "degrades to plain HTML" guarantee the rest of the console has. Islands are leaf nodes htmx does not swap. #}
{# Navigable outline (issue #87): the same INERT JS-off / pre-hydration list the review workbench shows, from the shared macro. The transcript-player island root replaces this markup (OutlinePanel + player) on its first render; without JS it stays a readable, non-interactive list. #} {{ outline_fallback(island_props.outline) }} {% for ln in lines[:200] %} {% set idx = palette.get(ln.diarization_label) %} {% set uncertain = ln.confidence is not none and ln.confidence < low_confidence_threshold %}

[{{ "%.2f"|format(ln.start_seconds) }}–{{ "%.2f"|format(ln.end_seconds) }}] {% if uncertain %}uncertain{% endif %} {% if ln.diarization_label %}{{ ln.diarization_label }}{% endif %} {% if ln.speaker != ln.diarization_label %}{{ ln.speaker }}:{% endif %} {{ ln.text }} {% if translation_ctx.active and translation_ctx.active.texts[loop.index0] %} {{ translation_ctx.active.texts[loop.index0] }} {% endif %}

{% endfor %} {% if lines|length > 200 %}

{{ lines|length - 200 }} more segments. Enable JavaScript for the full transcript.

{% endif %}
{% endif %} {% endblock %}