From 834146de302fc75bd079461624d4a446ff5f77b4 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Tue, 29 Sep 2026 17:24:05 +0200 Subject: [PATCH] feat(magazine): synthesis state, MP3 length and inline play in the contents list Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + static/js/magazine.js | 56 +++++++++++++++++++++++++++++++++++++++++- static/style.css | 3 +++ tests/magazine.spec.js | 25 +++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b39351..b93877d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ### Fixed - **Voice benchmark failures now say why.** Designed voices are benchmarked through the Voice Design engine; when that service is down the row showed a bare "-" and the raw "Max retries exceeded" text. The error now names the service and the container to start, the Factor and WPM columns show `ERR` beside the time column, and a batch stops after three identical "unreachable" failures instead of retrying every remaining voice. ### Added +- **Magazine contents list shows synthesis state.** Each article row says "Synthesised · m:ss" (the MP3's exact length) or "Not synthesised" (approved, no MP3 yet), and has an inline play/pause button that streams the MP3 without opening the article. Icon plus words, screen-reader labelled, works at phone width. - `scripts/compare_magazines.py`: read-only comparison of two magazine folders (words, boxes, reading order, articles, quality numbers) for re-import checks. ### Changed (compare_magazines.py) diff --git a/static/js/magazine.js b/static/js/magazine.js index 9fa80a3..bd91469 100644 --- a/static/js/magazine.js +++ b/static/js/magazine.js @@ -88,6 +88,46 @@ function fmtDuration(seconds) { return m < 60 ? m + ' min' : Math.floor(m / 60) + ' h ' + (m % 60) + ' min'; } +// Exact clock time (m:ss, h:mm:ss) -- the estimate above is deliberately coarse, this one names a real MP3. +function fmtClock(seconds) { + const s = Math.max(0, Math.round(seconds || 0)); + const ss = String(s % 60).padStart(2, '0'); + return s >= 3600 + ? Math.floor(s / 3600) + ':' + String(Math.floor(s / 60) % 60).padStart(2, '0') + ':' + ss + : Math.floor(s / 60) + ':' + ss; +} + +// The contents list doubles as the playlist: one shared