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>
4.8 KiB
Project map
Living index of where things are and what state they are in. Update in the same commit as the change it describes.
Current phase
Magazine projects, P0 and P1 complete (frontend reachable at Studio → Magazines). Audiobook/Rehearser/voice-library features are mature and in daily use.
Deployment
Self-hosted Docker on the DGX Spark, port 7890, image
ghcr.io/martin-b78/tts-voice-creator-clone-and-design-2.
Python and static files are bind-mounted, so code changes need only a container
recreate; rebuild and push only when Dockerfile or requirements.txt change.
Layout
| Path | Role |
|---|---|
server.py |
FastAPI app, router registration, auth middleware |
core/ |
domain logic — config, database, audio, voice, routing, magazine |
routes/ |
HTTP boundary, one module per feature area |
static/js/ |
frontend, one module per feature area (no build step for most) |
tests/ |
test_*.py (unittest) and *.spec.js (Playwright) |
architecture/ |
SOPs for subsystems whose design is not obvious from code |
workflows/ |
ComfyUI graphs for character portrait generation |
Feature areas
| Area | Server | Frontend | State |
|---|---|---|---|
| Voice library / clone / design | routes/library.py, core/voice.py |
library.js |
mature |
| Read Aloud (books, PDFs) | routes/reader.py |
reader.js |
mature |
| Audiobook casting | routes/conversation.py |
audiobook*.js |
mature |
| Rehearser | routes/rehearsals_db.py |
rehearser-*.js |
mature |
| Studio (phase wizard) | — | studio.js |
mature |
| TTS engines / routing | routes/tts.py, core/routing.py |
engines.js |
mature |
| STT | routes/stt.py |
— | mature |
| Magazines | core/magazine.py, routes/magazine.py |
magazine.js, s-magazine.html |
P0 + P1 |
Data model
Documents (books and magazines alike) live under
<CONFIG_DIR>/reader_library/<doc_id>/, discriminated by meta.json → kind:
kind: "pdf"/ text — Read Aloud book. Text is extracted in the browser and not persisted;meta.jsonholds counts and a flatidxresume position.kind: "magazine"— addslayout.json,articles.json,pages/,images/,podcast/, and a per-articleprogressmap. See architecture/magazine-ingestion.md.
Settings, presets, routes and the SQLite database also live under CONFIG_DIR,
which is the writable volume.
Environment
| Variable | Purpose |
|---|---|
CONFIG_DIR |
writable state (settings, library, database) |
VOICES_DIR / OUTPUT_DIR |
voice WAV library |
TTS_CONTAINER_NAMES |
containers restarted after a voice change |
DOCKER_HOST |
points at the socket proxy; the raw socket is not mounted |
llm_url / llm_model / llm_api_key |
settings.json, OpenAI-compatible LLM |
Pending decisions
- Container registry: ghcr costs quota against a private package (≈427 MB per push, 500 MB free on the Free plan). Local registry vs. ghcr vs. no re-pull is unresolved — the compose files currently point at ghcr.
Known limitations
giteabackup remote is 157 commits behindorigin;maintracksgitea.- Magazine sidebar exclusion is a narrow-column geometry heuristic
(
mark_sidebars), not certain — review always shows every sidebar block. - Magazine word-level highlighting is a character-length estimate against real measured audio duration, not true forced alignment.
- No formal cross-engine OCR accuracy benchmark exists (two engines wired: ocr-baidu-unlimited-ocr primary, KDL Frontier Parser nano fallback).
- Figure boundaries are re-derived from the page render (
snap_visual_bboxes) because vision OCR grounds figures loosely; it corrects roughly 100 of the 147 figure regions onc't KI Wissen 2026and deliberately declines the rest. A pale chart or line drawing on white is below its ink threshold and keeps the engine's box — draw those by hand in review. The pass is idempotent (safe to re-run via "Fix figure boundaries" any number of times), including the one confirmed pair that cannot mathematically converge — two adjacent figures sharing a dark caption strip settle on the same answer every run rather than continuing to drift between two states. - No
npm run qaaggregate command yet; Python tests run viaunittest, browser tests viaplaywright test. - The app has no authentication for the browser UI and no per-user data
isolation — single-tenant/LAN-trust only (one global settings/database/voice
library, no accounts). See
architecture/multi-tenant-and-identity.mdand TODO.md's "Deferred — public multi-user readiness" section.
Next logical step
Magazine P2 per TODO.md: image descriptions, table/chart handling, EPUB 3 Media Overlay export, and a formal OCR-engine benchmark (see architecture/magazine-ingestion.md).