0e75b24a7a
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 37e2622d4e |
feat(audiobook): advisory verify and span-native recast for span casts
Verify / recast now run span-natively (no text re-emission): a second differently-windowed pass marks lines agreed/disputed and records the other answer, without changing any speaker (~9 min vs ~49 min). Measured on the real pipeline, overruling disputed lines with a reasoning adjudication made them worse (66.5% -> 61.4%), so that step was removed; agreed lines were 83% right vs 48.5% for disputed ones, which is the signal used for confidence and review. Auto-repair no longer runs after a span cast; segments carry a spanId. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| c81ae28da2 |
feat(magazine): add figure/chart/table description generation and spoken narration
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
Adds a "Describe" action per figure in article review: sends the cropped
image to the configured vision engine (with fallback) for a type-aware
one- or two-sentence description -- a chart gets a prompt about what it
measures and its trend, a table about headings and what stands out, etc.
Editable before approval. A new "Read figure descriptions" toggle in the
reading controls speaks these where they occur; purely additive, off by
default, and a figure with no description behaves exactly as before.
New core/magazine.py describe_visual_region()/_chain() reuse the same
reliability guards as page OCR (repetition-loop detection, sentence-
boundary truncation). New routes: POST .../images/{id}/describe
(background job), PATCH .../images/{id} (manual edit), descriptions=1 on
the article-text endpoint.
Live-verified end to end against the ingested c't KI Wissen 2026
magazine. Also found and documented a real, config-dependent limitation:
the currently-configured primary vision engine is transcription-only and
returns empty content for any descriptive prompt (confirmed with three
different prompts) -- not a code defect, but description generation
needs a genuinely caption-capable engine reachable as the fallback.
251 tests passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|||
| 4249382ac6 |
feat(deploy): stand up correspondence-nuc as a second instance
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
The NFS blocker from TODO.md's deployment entry is resolved (export set up by the user on the DGX); picks up from there and completes the deployment end to end. - Mounted the DGX's TTS_Voices export read-write on the NUC at /mnt/dgx-tts-voices, persisted in /etc/fstab -- confirmed writable and confirmed the NUC instance sees the DGX's real voice library (344 voices), not a separate copy. - Deployed by rsyncing the working tree from the DGX (excluding .git, config/, logs/ and other local-only paths) and building the image directly on the NUC, rather than pulling ghcr.io/martin-b78/... -- that registry copy turned out to be both several versions stale and private with no credentials configured on the NUC, and the source was already right there to build from. - Found and fixed a real gap while wiring it up: vision_ocr_url and vision_ocr_fallback_url default to host.docker.internal, which resolves to whichever host the container is running on -- correct on the DGX itself, silently wrong on the NUC (would have pointed at the NUC's own nonexistent OCR engine). Pinned both explicitly to the DGX's LAN IP via environment overrides. - The container crash-looped on first start (sqlite3.OperationalError: unable to open database file) because docker compose had created config/ and logs/ root-owned under this session's root SSH access to the NUC, while the image's app user is uid 1000 -- chown -R 1000:1000 fixed it. - New docker-compose.nuc.yml, tracked here as the reviewable reference for that host (docker-compose.yml stays the DGX's own -- different build/network/socket-proxy shape). Deployed to the NUC as its own docker-compose.yml, since that host has no .git checkout to diff against, only the rsynced tree. Verified: container stable (no restart loop), HTTP 200, settings confirm all four engine URLs correctly resolve to the DGX, voice count matches the DGX exactly, restart policy is unless-stopped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| b458306ab9 |
fix(magazine): recover sparse charts, stabilize adjacent-figure snapping
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
Real failures found by re-inspecting a fresh re-ingest of c't KI Wissen
2026 after the previous figure-boundary-snapping release: a bar chart
was still truncated to roughly a third of its real height, and a
chart's top edge cut into an unrelated paragraph above it.
Root causes, each independently confirmed against the real page render
before fixing:
- A bar chart's bars/gridlines/axis labels rarely reach the "solid
ink" density a photo does (measured 0.25-0.5 vs the 0.55 threshold),
so the existing pass anchored only on the one fully-solid element
(the zero-line) and never grew further. Added a second, looser pass
that grows an anchor -- or the seed itself -- through any real mark
at all, fenced by the same blocked cells.
- Anchor selection ("which solid component does the seed overlap
most") was seed-size-dependent: a small seed touching only the
zero-line anchored there, but re-running the SAME function on the
box it had just produced -- now large enough to also touch a dense
axis-label strip -- picked that instead and shrank an already-good
box back down. Fixed by dilating the solid mask and labelling once,
BEFORE any seed is considered, so which components count as one
figure is a property of the page alone.
- A merged bounding box can enclose real text sitting between two
components even though neither component's own cells touched it (a
rectangle union, not the components' actual shape). Added
_trim_blocked_edges() to pull each edge back to the nearest text
wall found searching outward from the box's own centre.
- Two adjacent figures fencing each other's growth made each one's
result depend on the other's CURRENT box -- for two dark photos
sharing a caption strip, this never settled, swinging by over 100pt
call after call. snap_visual_bboxes() now re-seeds every visual on a
page from every other's last result for a bounded number of rounds,
deciding same-graphic ambiguity once up front (fenced against the
original seeds, before any iteration) so two disputed seeds can't
mutually fence each other into a tidy-looking split instead of being
recognised as the same disputed claim.
- A result is now only accepted once it reproduces itself when fed
back through the same rules (IoU-checked), and the minimum
seed/graphic overlap for repositioning a badly-offset box was
lowered slightly -- it was narrowly rejecting some real cases.
Verified against the full 204-page/147-figure document: idempotency
holds except for one confirmed non-convergent pair, which now lands on
the same deterministic answer every run instead of continuing to
drift.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|||
| 55f147d7a7 |
fix(magazine): derive figure boundaries from the page render
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
A vision OCR engine grounds a figure far less precisely than it
transcribes one. Over all 138 figure regions of a 204-page issue the
transcribed contents were right while the boxes were routinely offset,
undersized, or sitting on the neighbouring text column -- page 23's card
grid was reported at x 0.24-0.54 where the graphic runs 0.41-0.95.
snap_visual_bboxes() reduces the page render to an ink-coverage grid,
seeds a connected-component search with the engine's box, and re-joins
tiles a gutter apart so a card grid comes back as one figure. Text
blocks are excluded from the mask, which is what stops a figure
swallowing the column beside it. A candidate must be solidly inked or
framed on nearly all of its rows and columns (admitting a white-interior
screenshot), may not shrink below 30% of the seed, and reverts when two
figures land on the same graphic; anything else keeps the box it had.
Runs before the figure crop in both OCR paths, so the images shown
during playback are framed correctly too. Replaces refine_picture_
bounds(), which only recognised coloured panels and had no text fence.
Also adds "Fix figure boundaries" in review and POST
/api/magazine/{id}/figures/refine behind it, so an issue ingested before
this pass can be corrected without re-running OCR. Verified live against
c't KI Wissen 2026: 78 of 138 boxes corrected, the rest declined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|||
| c7b473910a |
docs: record public multi-user readiness as a deferred cross-app initiative
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
Three research passes into what multi-user hosting would require found the app has zero multi-tenancy today: one global settings/database/voice library, no browser-UI authentication at all, global (not per-user) concurrency limits, and no consent/audit mechanism for voice cloning. The user wants a single shared, interconnected login/profile system across all their self-hosted apps rather than a per-app login, extending the GoTrue+Postgres+PostgREST stack OurFlashcards already runs on the NUC — so this is recorded as a deferred, cross-repo initiative rather than implemented now. One policy decision was made and is captured for future implementation: voice cloning will require a consent attestation with an audit trail once accounts exist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
|||
| 813e06c914 |
feat: complete magazine P1 (images, play-all, karaoke, driving mode, sidebar exclusion) + OCR engines page + second OCR engine
Magazine P1:
- Images shown automatically during playback (assign_images_to_articles
links each figure to the article whose column it falls in; the client
shows the last image reached in reading order as playback advances).
- "Play all articles" queues every approved article and advances
automatically at the end of each rather than stopping.
- Word-level highlighting, explicitly documented as an ESTIMATE (no
forced-alignment/timestamp source exists): each word's share of the
sentence's real measured audio duration is proportional to its
character length. A checkbox turns it off; sentence highlighting
(measured, not estimated) is unaffected.
- Driving mode: full-screen overlay, two oversized controls, reuses
whatever is already playing/queued.
- Sidebars/pull-out boxes excluded from narration automatically
(mark_sidebars: an anomalously narrow column vs. the page's regular
grid). Fixed a real defect found while building this: a full-width
headline was left in the column-width analysis and could bridge two
genuinely separate columns into one, hiding a real sidebar column
entirely -- caught by a failing test before the fix.
Second OCR engine + fallback ladder:
- KDL Frontier Parser nano wired in via vision_ocr_page_chain (ordered
engine list, first usable result wins, tesseract last resort).
Confirmed live: ~3x faster than Baidu (~12s vs ~40s/page) but returns
plain text with NO coordinate markers, unlike Baidu's labeled regions.
_whole_page_region treats the whole response as one ambiguous
full-page block (gated by ocr_quality_score) rather than discarding
usable text just because it lacks layout structure.
OCR engines page + a real UI bug:
- The OCR & Document Parsing page had no .s-engines-page[data-page="ocr"]
markup at all -- renderLocalContainers already supported an OCR grid
generically, but $('dc-grid-ocr') was always null, so the card grid
silently never rendered. Added an Active OCR Engine panel mirroring
Active Language Model exactly, plus the Local grid -- both configured
engines now appear as ready-made cards, no "Add Custom" dialog needed.
- Fixed the audiobook pipeline's "Previous/Next" nav bar bleeding into
every .section-head in the document (confirmed live on the Engines
page: a "Cast Audiobook" button and "STEP 1/8" hint that made no sense
there). Changed from a blocklist (naming each unrelated section as
noticed) to an allowlist of the pipeline's three actual destinations.
Verified live end-to-end: play-all queue building and manual advance,
word-span markup, driving-mode toggle, OCR page showing both engines as
local cards, zero stray nav bars on the Engines page, no console errors.
20 new backend tests (156 total in the magazine module).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|||
| 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>
|
|||
| 736aeaaeac |
feat: add magazine projects with layout-aware article extraction
Magazines break the audiobook pipeline's assumptions: several unrelated articles share a page, columns run in parallel, adverts and sidebars interrupt body copy, and a feature routinely jumps twenty pages to its tail. Narrating page-by-page walks out of an article mid-sentence and into an advert, so the canonical object is the article, not the page. core/magazine.py reconstructs articles from a flat PDF: PyMuPDF extracts blocks with bboxes and font metrics, tesseract supplies a TSV fallback so scanned pages join the same column grouping, blocks are classified and ordered column-major, and articles are anchored by the parsed table of contents with "continued on page N" tails linked back. routes/magazine.py adds 13 endpoints. Ingestion is a restart-safe background job. Nothing is narrated until a human approves the boundaries; podcast generation refuses unapproved articles. Podcast mode renders an approved article as a 2-3 host discussion using the configured LLM and voices the user picks, alongside narration rather than instead of it. Verified end-to-end over HTTP on a synthetic 8-page issue: the feature came back as pages [3, 8] with its jump tail absorbed and the advert page excluded, and an image-only rendering recovered all 47 blocks via OCR. 44 new tests; full suite 90 passing. The Studio UI is not built — this is reachable only via the API. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |