{% extends "base.html" %} {% block header_extra %} {# Spec 30: language selector beside Active project - shows per-language progress #}
Applies to the current chapter β€” navigation, buttons and statuses stay English.
{% endblock %} {% block content %}

{{ total_utterances }} utterances

← Back to Chapters
⬇ WAV ⬇ MP3 Ready
{% if blank_count %}

⚠ {{ blank_count }} utterance{% if blank_count != 1 %}s{% endif %} with no text β€” flagged for review

Blank utterances are skipped at build time and their stray audio is no longer stitched into the chapter. Restore the text or delete the row (βœ•) to clear the flag.

{% endif %} {% if chapter_id|string == '1' %}

Personalization

Add a personalization watermark to the title page. This line is spoken at the end for tracing.

{% endif %} {% for audio in audio_files %} {% endfor %}
# Speaker Variation Text Audio
{{ audio.index }}
{% if audio.qa_issues %}
⚠
QA flag β€” fix then Recheck, or Accept as-is:
{% for iss in audio.qa_issues %}
[{{ iss.type }}] {% if iss.expected %} expected {{ iss.expected }}{% endif %} (conf {{ "%.2f"|format(iss.confidence) }})
{% if iss.evidence %}{{ iss.evidence }}
{% endif %} {% if iss.text %}β€œ{{ iss.text }}”{% endif %}
{% endfor %}
βœ“ Accepted {% endif %}
{% if audio.is_blank %}⚠ no text{% endif %}
{% if active_language != 'en' %} {% if audio.has_translation %} {{ audio.display_text }} βœ“ {{ active_language_name }} {% elif audio.translation_gap %} {{ audio.source_text or '(click to add text)' }} ⚠ {{ active_language_name }} gap: {{ audio.translation_gap_reason[:120] }} {% else %} {{ audio.source_text or '(click to add text)' }} ⚠ {{ active_language_name }} has not been translated yet {% endif %} {% else %} {{ audio.source_text or '(click to add text)' }} {% endif %}
{% if audio.filename %} {% else %} No audio {% endif %}

Add Utterance

function playUtterance(chapterDir, filename, index) { if (currentAudio) { currentAudio.pause(); currentAudio.currentTime = 0; } playAllStopped = true; const audio = document.getElementById('audio-' + index); if (audio) { currentAudio = audio; audio.play(); document.querySelectorAll('tbody tr').forEach(r => r.style.background = ''); document.getElementById('utterance-' + index).style.background = '#1e293b'; } } function playAllUtterances() { playAllStopped = false; const rows = Array.from(document.querySelectorAll('tbody tr')); if (rows.length === 0) return; playAllIndex = 0; document.getElementById('playback-status').textContent = 'Playing...'; document.querySelectorAll('tbody tr').forEach(r => r.style.background = ''); playNext(rows); } function playNext(rows) { if (playAllStopped || playAllIndex >= rows.length) { if (!playAllStopped) { document.getElementById('playback-status').textContent = 'Complete'; } return; } const row = rows[playAllIndex]; const index = row.id.replace('utterance-', ''); const audio = document.getElementById('audio-' + index); if (currentAudio) { currentAudio.pause(); currentAudio.currentTime = 0; } if (audio) { currentAudio = audio; row.style.background = '#1e293b'; document.getElementById('playback-status').textContent = 'Playing ' + (playAllIndex + 1) + '/' + rows.length + '...'; audio.onended = () => { row.style.background = ''; playAllIndex++; playNext(rows); }; audio.play(); } else { playAllIndex++; playNext(rows); } } function stopAllUtterances() { playAllStopped = true; if (currentAudio) { currentAudio.pause(); currentAudio.currentTime = 0; } document.querySelectorAll('tbody tr').forEach(r => r.style.background = ''); document.getElementById('playback-status').textContent = 'Stopped'; } function playChapterAudio() { if (chapterAudio) { chapterAudio.pause(); chapterAudio.currentTime = 0; } chapterAudio = new Audio('/api/chapter/{{ chapter_id }}/full-audio'); document.getElementById('playback-status').textContent = 'Playing chapter...'; document.getElementById('playChapterBtn').style.display = 'none'; document.getElementById('stopChapterBtn').style.display = 'inline-block'; chapterAudio.onended = () => { document.getElementById('playback-status').textContent = 'Chapter complete'; document.getElementById('playChapterBtn').style.display = 'inline-block'; document.getElementById('stopChapterBtn').style.display = 'none'; }; chapterAudio.onerror = () => { document.getElementById('playback-status').textContent = 'Error playing chapter'; document.getElementById('playChapterBtn').style.display = 'inline-block'; document.getElementById('stopChapterBtn').style.display = 'none'; }; chapterAudio.play(); } function stopChapterAudio() { if (chapterAudio) { chapterAudio.pause(); chapterAudio.currentTime = 0; chapterAudio = null; } document.getElementById('playback-status').textContent = 'Stopped'; document.getElementById('playChapterBtn').style.display = 'inline-block'; document.getElementById('stopChapterBtn').style.display = 'none'; } let buildPollTimer = null; let chapterAudio = null; function showChapterAudioButtons(show) { const display = show ? 'inline-block' : 'none'; const playBtn = document.getElementById('playChapterBtn'); const wavBtn = document.getElementById('downloadWavBtn'); const mp3Btn = document.getElementById('downloadMp3Btn'); if (playBtn) playBtn.style.display = display; if (wavBtn) wavBtn.style.display = display; if (mp3Btn) mp3Btn.style.display = display; } function buildChapter() { const btn = document.getElementById('buildBtn'); const statusDiv = document.getElementById('buildStatus'); const statusText = document.getElementById('buildStatusText'); const progressText = document.getElementById('buildProgress'); btn.disabled = true; btn.textContent = '⏳ Building...'; statusDiv.style.display = 'inline-block'; statusText.textContent = 'Starting build...'; progressText.textContent = ''; fetch('/api/chapter/{{ chapter_id }}/build', { method: 'POST' }) .then(r => r.json()) .then(data => { if (data.ok) { pollBuildStatus(); } else { btn.disabled = false; btn.textContent = 'πŸ“¦ Build Chapter'; statusText.textContent = 'Build failed: ' + (data.error || 'unknown'); } }).catch(() => { btn.disabled = false; btn.textContent = 'πŸ“¦ Build Chapter'; statusText.textContent = 'Request failed'; }); } function pollBuildStatus() { if (buildPollTimer) clearInterval(buildPollTimer); buildPollTimer = setInterval(() => { fetch('/api/chapter/{{ chapter_id }}/build-status') .then(r => r.json()) .then(data => { const statusText = document.getElementById('buildStatusText'); const progressText = document.getElementById('buildProgress'); const btn = document.getElementById('buildBtn'); const phase = data.phase || data.status || 'idle'; const detail = data.detail || ''; const elapsed = data.elapsed || ''; const failed = data.failed || []; const errors = data.errors || []; let label = detail || phase; if (elapsed) label += ' (' + elapsed + ')'; if (failed.length > 0) label += ' [' + failed.length + ' failed]'; statusText.textContent = label; if (data.progress > 0) { progressText.textContent = data.progress + '%'; } if (phase === 'complete' || phase === 'warning') { clearInterval(buildPollTimer); buildPollTimer = null; btn.disabled = false; btn.textContent = 'πŸ“¦ Build Chapter'; progressText.textContent = phase === 'complete' ? 'βœ“ Complete' : '⚠ Warnings'; showChapterAudioButtons(true); } else if (phase === 'error') { clearInterval(buildPollTimer); buildPollTimer = null; btn.disabled = false; btn.textContent = 'πŸ“¦ Build Chapter'; progressText.textContent = 'βœ— Failed'; if (errors.length > 0) { statusText.textContent += '\n' + errors.join('; '); } } }).catch(() => {}); }, 2000); setTimeout(() => { if (buildPollTimer) { clearInterval(buildPollTimer); buildPollTimer = null; const btn = document.getElementById('buildBtn'); btn.disabled = false; btn.textContent = 'πŸ“¦ Build Chapter'; document.getElementById('buildStatusText').textContent = 'Timed out'; } }, 300000); } {% endblock %}