tts-voice-creator-clone-and.../static/sections/s-magazine.html
mARTin-B78 71a398b82f feat: add magazine reader frontend (P1)
Studio -> Magazines: library, contents tree grouped by section with
confidence/pages/listening-time/audio status, the Stage D review
screen (tick/untick layout blocks, correct title/author, approve or
reject), a reflowed article view with click-to-seek sentence
highlighting, a linked original-page view showing the source bounding
box, and a podcast panel (2-3 host pickers, script generation,
turn-by-turn playback).

Wired into the existing lazy-module system (module-manifest.json,
loader.js, nav.js) rather than a separate app. Reuses /api/tts-preview
for both narration and podcast turns -- no new audio pipeline.

Fixes found building this:
- The pane toggle used role="tab" without the full ARIA tabs pattern
  (aria-selected, roving tabindex, arrow-key nav) -- fails WCAG 2.2 AA
  and hid the buttons from role-based tooling. Replaced with a button
  group + aria-pressed, matching the plain-click interaction it
  actually has.
- Print layout cues ("(continued on page 8)") were being sent to TTS
  verbatim -- narration would have spoken the parenthetical aloud.
  Stripped from the narration copy only; layout.json keeps the
  original for the review screen.
- has_podcast now comes from the articles listing so opening an
  article without a generated script skips a doomed GET instead of a
  routine 404 on every open.

Verified against the real API contract with Playwright (library,
review pre-selection, approval, sentence highlighting + PDF-page
linking, podcast approval gate, zero console errors across the flow)
and manually end-to-end against a real ingested fixture, screenshots
included. Full suite: 94 Python tests, 33/34 desktop Playwright tests
(one unrelated pre-existing flaky DOM-count assertion).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:00:58 +02:00

120 lines
6.0 KiB
HTML

<div class="section-head">
<span class="section-icon"><span class="mdi mdi-newspaper-variant-outline"></span></span>
<div class="section-title">
<h2>Magazines</h2>
<p>Listen to a magazine one article at a time &mdash; from its true beginning to its true end, without adverts, sidebars or a neighbouring piece spliced into the middle.</p>
</div>
</div>
<!-- ── Library ──────────────────────────────────────────────────────── -->
<div id="mag-view-library">
<div class="card mag-new">
<h3>Add a magazine or paper</h3>
<div class="mag-new-grid">
<label for="mag-new-title">Title
<input id="mag-new-title" type="text" placeholder="Scientific American" autocomplete="off">
</label>
<label for="mag-new-issue">Issue
<input id="mag-new-issue" type="text" placeholder="September 2026" autocomplete="off">
</label>
<label for="mag-new-file">PDF
<input id="mag-new-file" type="file" accept="application/pdf">
</label>
<button type="button" class="btn primary" onclick="magCreate()">
<span class="mdi mdi-plus"></span> Add magazine
</button>
</div>
<p class="muted">Scanned issues work too &mdash; pages without selectable text are read with OCR.</p>
</div>
<h3>Your magazines</h3>
<div id="mag-library-list" class="mag-grid"></div>
</div>
<!-- ── Contents ─────────────────────────────────────────────────────── -->
<div id="mag-view-contents" hidden>
<div class="mag-bar">
<button type="button" class="btn-sm" onclick="magShow('library')">
<span class="mdi mdi-arrow-left"></span> All magazines
</button>
<h3 id="mag-doc-title">Magazine</h3>
<div class="mag-bar-actions">
<button type="button" class="btn-sm" onclick="magIngest()">
<span class="mdi mdi-refresh"></span> Re-run detection
</button>
</div>
</div>
<div id="mag-ingest-status" class="mag-status" hidden></div>
<div id="mag-contents"></div>
</div>
<!-- ── Review (Stage D) ─────────────────────────────────────────────── -->
<div id="mag-view-review" hidden>
<div class="mag-bar">
<button type="button" class="btn-sm" onclick="magBackToContents()">
<span class="mdi mdi-arrow-left"></span> Contents
</button>
<h3>Review article boundaries</h3>
</div>
<div id="mag-review"></div>
</div>
<!-- ── Reading ──────────────────────────────────────────────────────── -->
<div id="mag-view-read" hidden>
<div class="mag-bar">
<button type="button" class="btn-sm" onclick="magBackToContents()">
<span class="mdi mdi-arrow-left"></span> Contents
</button>
<div class="mag-read-id">
<h3 id="mag-read-title">Article</h3>
<span id="mag-read-by" class="muted"></span>
<span id="mag-read-pages" class="muted"></span>
</div>
</div>
<div class="mag-controls">
<label for="mag-voice" class="mag-voice-pick">Narrator
<select id="mag-voice"></select>
</label>
<button type="button" class="btn primary" id="mag-play" onclick="magPlay()"
aria-label="Play narration">
<span class="mdi mdi-play"></span> Listen
</button>
<span id="mag-read-progress" class="muted" aria-live="polite"></span>
<div class="mag-pane-tabs" role="group" aria-label="Reading view">
<button type="button" class="btn-sm active" data-mag-pane="reflow"
aria-pressed="true" onclick="magSetPane('reflow')">Article</button>
<button type="button" class="btn-sm" data-mag-pane="pdf"
aria-pressed="false" onclick="magSetPane('pdf')">Original page</button>
</div>
</div>
<div id="mag-pane-reflow" class="mag-pane">
<div id="mag-article" class="mag-article"></div>
</div>
<div id="mag-pane-pdf" class="mag-pane" hidden>
<div class="mag-page-wrap">
<img id="mag-page-img" alt="">
<div id="mag-page-box" class="mag-page-box" hidden></div>
</div>
<p id="mag-page-label" class="muted"></p>
</div>
<!-- Podcast mode -->
<details class="card mag-podcast">
<summary><span class="mdi mdi-podcast"></span> Podcast mode &mdash; discuss this article</summary>
<p class="muted">Two or three hosts discuss the article for someone who has not read it. The script is written by the configured LLM from the article text; it will not invent facts, but it is a generated discussion, not the author&rsquo;s words.</p>
<div class="mag-hosts">
<label for="mag-host-a">Host A<select id="mag-host-a"></select></label>
<label for="mag-host-b">Host B<select id="mag-host-b"></select></label>
<label for="mag-host-c">Host C <span class="muted">(optional)</span><select id="mag-host-c"></select></label>
</div>
<div class="mag-podcast-actions">
<button type="button" class="btn" id="mag-podcast-gen" onclick="magGeneratePodcast()">Generate script</button>
<button type="button" class="btn primary" id="mag-podcast-play" onclick="magPlayPodcast()" hidden>Play podcast</button>
</div>
<div id="mag-podcast-script"></div>
</details>
</div>