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>
102 lines
4.8 KiB
Markdown
102 lines
4.8 KiB
Markdown
# 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.json` holds counts and a flat `idx` resume position.
|
|
- `kind: "magazine"` — adds `layout.json`, `articles.json`, `pages/`, `images/`,
|
|
`podcast/`, and a per-article `progress` map. See
|
|
[architecture/magazine-ingestion.md](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
|
|
|
|
- `gitea` backup remote is 157 commits behind `origin`; `main` tracks `gitea`.
|
|
- 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 on `c't KI Wissen 2026` and 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 qa` aggregate command yet; Python tests run via `unittest`,
|
|
browser tests via `playwright 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.md` and
|
|
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).
|