fix(export): ship untagged file when provenance tagging fails

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-10-04 18:38:42 +02:00
parent 1fd79866f8
commit 2a06d0a488
4 changed files with 12 additions and 2 deletions

View File

@ -13,6 +13,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
- **Voice list rows no longer show the "Source unknown" badge** (it crowded the play buttons). The badge stays in the voice inspector, next to the Source & consent panel.
### Fixed
- **Voice archetype gallery broke `npm run check:frontend`**: its `say` helper collided with `magazine.js`; renamed to `archSay`.
- **Export failed with HTTP 500 when provenance tagging failed** (EPUB/ZIP/article MP3 with audio ffmpeg cannot tag); it now ships the untagged file and logs a warning.
- **Archetype `language` filter accepted only full names.** `GET /api/archetypes?language=de` returned 0 items; it now accepts ISO codes and names, case-insensitive (`de`, `DE`, `German`); unknown values return an empty list.
- **LibriVox search returned 502 "unavailable" after 20-30 s.** Cause: the LibriVox API is erratic (10-40 s for any request, measured from host and container; egress/DNS/TLS fine) and the old call asked for `extended=1` (all chapters of 15 books) with a 30 s timeout, mapping every failure to one generic message. Now the list is light (no `extended`), the first chapter is fetched per book on demand (`GET /api/voice-sources/librivox/book/{id}`, UI "Load preview" / on import), timeout 25 s with one retry and backoff, failures cached 30 s, distinct messages (timeout / unreachable / HTTP status / unreadable) and a privacy-safe log line (exception class and query length only). Host/redirect rules unchanged.
- **`export_provenance_default` missing from settings defaults.** It is now in the defaults (`true`), so `GET /api/settings` exposes it.

View File

@ -796,7 +796,7 @@ sent. Websites that redistribute commercial magazines without a licence are deli
- **Prerequisites:** an article with generated audio. No extra software.
- **Steps:** in the export row tick or untick "Mark as synthetic speech (tags)" (default on, remembered as a setting) and optionally "include voice name". Then download as usual.
- **Result:** the copy you download has tags `synthetic_speech`, `generator`, `generator_version`, `generated_date` (and `voice` if chosen). The stored audio is unchanged. To check a file: `python -m core.export_provenance file.mp3` or `POST /api/export/provenance` with the file as body.
- **Errors:** if tagging fails the download answers "Could not add provenance tags"; untick the box to export untagged.
- **Errors:** if tagging fails (e.g. ffmpeg cannot read the file) the download still succeeds with the untagged file and a server-log warning; untick the box to export untagged on purpose.
- **Mobile / accessibility:** plain labelled checkboxes. **Offline:** works offline. **Privacy:** no user names or paths; the voice name only if you tick it. Tags can be removed by anyone, so they are an honest label, not protection. An inaudible watermark and a spoken disclosure are not available yet.
### Errors

View File

@ -3311,7 +3311,8 @@ def _export_mp3(mp3: Path, tmpdir: Path, provenance: str, voice: str) -> Path:
try:
return ep.apply_tags(mp3, tmpdir / mp3.name, ep.build_tags(__version__, name, voice == "1"))
except (RuntimeError, OSError, __import__("subprocess").SubprocessError):
raise HTTPException(500, "Could not add provenance tags to the export")
logging.getLogger(__name__).warning("provenance tagging failed for %s; shipping the untagged file", mp3.name)
return mp3 # degrade gracefully: an untagged export beats a failed download
@router.post("/api/export/provenance")

View File

@ -279,6 +279,13 @@ class EndpointTests(unittest.TestCase):
self.assertEqual(z.namelist()[0], "mimetype")
self.assertIn("exact", z.read("OEBPS/content.opf").decode())
def test_provenance_tag_failure_ships_untagged_file(self):
from unittest import mock
self.seed()
with mock.patch("core.export_provenance.apply_tags", side_effect=RuntimeError("tagging failed")):
r = self.client.get(f"/api/magazine/{self.doc}/epub?provenance=1")
self.assertEqual(r.status_code, 200, r.text[:200])
def test_unapproved_or_silent_articles_are_not_packaged(self):
self.seed(approved=False)
self.assertEqual(self.client.get(f"/api/magazine/{self.doc}/epub").status_code, 404)