Releases: vyanhursky/joven
Release list
v1.0.0b8
Changed
- The app no longer bundles
epubcheck, and the download drops from 223 MB
to 201 MB on macOS. The jar needs a JVM the app cannot ship, so for the readers
the app exists for it was 32 MB that never ran — while the two Setup rows
explaining its absence were the most confusing thing on the page. epubcheck
remains a development and CI dependency, where it earns its place as the only
gate that validates the output as an EPUB rather than as a diff of the input;
a reader who wants the twelfth check installs it and Joven finds it onPATH. doctorreports epubcheck in one row instead of two. A separatejavarow
was right while the app shipped the jar and the JVM really was the missing
piece. Now it named something the reader never asked for, next to a second row
saying the same thing. Java appears only where it is genuinely what is missing:
a jar someone configured, with no runtime to run it.
Fixed
- The README said macOS ships a JVM. It ships a stub that prints "Unable to
locate a Java Runtime" and exits 1.
v1.0.0b7
Added
- A Quit button, top right of the page. The packaged app has no other off
switch on macOS — it is a console binary in a bundle with no Cocoa event loop,
so it has no Dock menu, ignores Cmd-Q, and is invisible to System Events, which
left Activity Monitor as the only way to stop it. Refuses while a job is
running, unless you confirm.
Fixed
- Launching Joven while it was already running died silently. Binding a taken
port raisedOSErrorfrom inside the server, and a Finder launch has no console
to print it to: the reader got a two-second bounce in the Dock and nothing else,
with the running instance invisible and the icon apparently broken. A second
launch now notices the first, reopens the page at it, and exits. A port taken by
something that is not Joven says so and names--port. Joven also stops setting
SO_REUSEADDRon Windows, where it means "bind a port that is already in use"
rather than Unix's "do not wait out TIME_WAIT" — two servers on 8770, and no way
to tell which one answers. - The install guide said quitting the app stops it, which on macOS there was no
way to do.
v1.0.0b6
Added
- A downloadable app —
Joven-windows-x64.zip,Joven-macos-arm64.dmg,
Joven-linux-x64.tar.gzon each release. Double-clicking it opens the browser
workflow: no Python, noPATH, no terminal. It carrieskepubifyand
epubcheckitself, so the only thing left to install is Ollama and its model.
About 220 MB to download, ~260 ms to start. Unsigned, so Windows and macOS warn
on first run — docs/install-app.md walks through it. joven doctor— checks the model server, the model,kepubify, Java and
epubcheck, and reports what each missing piece costs rather than only that it
is missing. Exits non-zero only when something stops a book being annotated.- An icon on the Windows and macOS builds, drawn from the README's horse by
packaging/make_icon.pyso there is only ever one copy of the art. Small sizes
get the strokes thickened first, because the horse is line work and shrinks to a
smudge otherwise. Linux keeps none: a tarball has nowhere to put one, so the PNG
ships in the bundle for whoever writes a.desktopfile. - A Setup tab in the browser UI, showing the same checks, and the page now
opens on it when something required is missing instead of on a drop zone that
cannot work. Where the model was never pulled it offers a Pull model button;
cancelling is safe, because Ollama keeps what it has and resumes.
Fixed
doctorreported Java andepubcheckas working on any Mac without a JDK.
macOS ships a/usr/bin/javastub on every install: it resolves like a real
JVM and exits 1 with "Unable to locate a Java Runtime". Sodoctorsaid
ready, promised twelve integrity checks, and the render ended in1 of 12 checks FAILED— the stub's message quoted at a reader who was told they did
not need Java. Joven now starts the JVM before believing in it, and a Mac with
no JDK gets the documentedjava — not foundand eleven checks that pass.- The macOS disk image had nothing to drag the app to. An app launched from
the read-only image cannot be approved — the first-run warning has a single
Done button and no way past it — so the window now carries an
Applicationsalias, and docs/install-app.md says to
use it and gives the System Settings approval steps that current macOS
actually requires. - The missing-Java hint named
wingeton every platform, including Linux and
macOS. setx JOVEN_EPUBCHECK_JARdoes not affect the terminal it is typed in; the
README now says so, and documents PowerShell'sActivate.ps1— the
extensionlessactivatebeside it is the bash script and silently does nothing.- A UI test asserted that a detect job was still running when its POST returned,
which the stub backend could beat about one run in six on Linux.
v1.0.0b5
Added
joven ui— the whole workflow in one local page: drop an EPUB, detect with
live progress and a Cancel that keeps every answer already given, review with
span editing, render and verify with one button, download the KEPUB, browse the
decision trace. Books live under~/.joven/books/<sha256>/with their sidecar,
trace and outputs, so the command line can pick up where the page left off.
See docs/browser-ui.md.joven strip, andrenderrefuses a book that already carries Joven
footnotes — rendering onto one doubled every marker while looking plausible.
inspectshows the marker count;detectwarns and carries on.- A progress bar on
detect: paragraphs, escalations, footnotes, last model
latency, time left. Hidden when stderr is not a terminal;--quietturns it off. detect --workers N. Paragraphs are scanned concurrently and finished in
book order, so the trace,--resumeand the sidecar are identical at any worker
count. Ollama needsOLLAMA_NUM_PARALLELto match or the requests queue.detect --backend openaifor any server speaking the OpenAI chat protocol —
llama.cpp, LM Studio, vLLM — with the same prompt, few-shots and JSON contract.
Strictjson_schemaand theenable_thinkingswitch are learned from the
server's first refusal rather than assumed.- Configuration.
joven.toml, a user config file andJOVEN_*variables, in
the order flag › environment › project file › user file › default.joven config
shows every value and its source. Unknown keys and wrong types are errors.
JOVEN_EPUBCHECK_JARis unchanged. joven reject,reset,status,diff, anddetect --href/--range.detect --ollama-url, alsoJOVEN_OLLAMA_URL.- Review page:
j/kmove between cards, Ctrl-Enter saves an edit, and the
review API accepts new spans.
Fixed
- The review page kept keyboard focus after a decision; the next
aused to hit
the first card on the page. renderandreviewwarn when a sidecar was detected from a different file.
Infrastructure
- mypy runs in CI with
lxml-stubs. - A GitHub release is created from every tag, with the wheel and sdist attached
and this file's section as the notes. A tag with no section fails before the
PyPI upload.
Measured
A Vintage edition of The Crossing (160,262 words, 13,878 segments) on an RTX
4070 Ti SUPER with qwen3:8b, Ollama at its defaults:
| one worker | --workers 4 |
|
|---|---|---|
| escalated to the LLM | 3,046 (22%) | 3,046 |
| footnotes | 735 | 734 |
| trace order | — | identical |
| wall clock | 15.2 min | 13.4 min |
The small gain is Ollama queueing the requests (OLLAMA_NUM_PARALLEL unset).
Cities of the Plain, 91,326 words, through the browser UI end to end: 2,272
escalations, 343 footnotes, 9 minutes with two workers, all 12 checks passed.
v1.0.0b4
Windows is a supported platform. Everything here was found by running the tool
on Windows 11 for the first time — the suite, the guard scripts, and a whole novel
through detect, render and verify. The README had said "Windows is
untested", and untested turned out to mean four bugs, one of them silent.
Fixed
-
External tools are run by resolved path, not by bare name.
shutil.which
honoursPATHEXT, so on Windows it resolvesepubcheck.CMD— but
CreateProcesscan only start a.exe, sosubprocess.run(["epubcheck", …])
raisedFileNotFoundErrorfor a tool that was installed and onPATH.
epubcheck_available()returned True and the run then died, which is the worst
shape a dependency check can take. It affects every.cmd/.batlauncher,
which is how epubcheck and most JVM tools arrive on Windows. Three integration
tests failed on it. -
epubcheck is found without a launcher at all. The official distribution is a
zip holdingepubcheck.jarand nothing else — no.bat, no.exe— so on
Windows there was never anything forPATHto find. This was the silent one:
epubcheck was reported missing, andverifyreported itSKIPPEDand passed,
so the single external gate on the output stopped running while the run still
looked clean.JOVEN_EPUBCHECK_JARnow points at the jar and it runs under
java -jar. -
The review server answers a refused POST instead of resetting the
connection.do_POSTsent 404 and 415 without reading the body it was
refusing. Unread bytes left in the socket make Windows reset rather than close,
so the client gotWinError 10053where the status code should have been — the
refusal arriving as a dropped connection rather than as a 415. Whether the reset
beat the response out of the buffer was a race, so this also flaked rather than
failing honestly. The content-type refusal that stops a page in another tab
writing to the sidecar is unchanged; it just says so now. -
Output is UTF-8 even when redirected. A redirected stream on Windows falls
back to the locale encoding, cp1252 on a stock install, somatrízwritten to a
file came back asmatr?z. Worse,joven addechoes the passage it matched, and
typer.echogiven a character outside cp1252 raisesUnicodeEncodeErrorrather
than degrading — after the sidecar has already been written. Accented Spanish is
safe, since cp1252 covers it, and The Crossing contains no character outside
it; that was luck rather than safety.
Documentation
- The install guide covers the Windows route, and calls out the epubcheck jar step
specifically, because skipping it does not fail loudly — it reportsSKIPPED
and passes. - Development covers
Scripts\rather thanbin/, and the reinstall-while-running
trap: Windows cannot replace an open file, sopip install -e .during a long
detectleaves a half-uninstalled package and a venv that looks fine until it
raisesModuleNotFoundError. - A
.gitattributessettles line endings in the repository rather than per clone.
Not cosmetic here: the load-bearing invariant compares bytes, so a stray\rin
a tracked fixture would look like the renderer corrupting the book.
Infrastructure
- CI runs the suite on
windows-latestat the Python floor and ceiling, five jobs
in total. The Windows epubcheck step deliberately creates no launcher, so CI
exercises the jar route a real user takes rather than a wrapper invented for its
own convenience.
Measured
A full run on Windows 11, against a different edition of The Crossing (149,995
words to the Knopf edition's 151,865) — RTX 4070 Ti SUPER, 32 GB, qwen3:8b:
| macOS, Knopf edition | Windows, this edition | |
|---|---|---|
| escalated to the LLM | 2,556 (21%) | 2,544 (21%) |
| footnotes produced | 726 | 731 |
| integrity checks | 12 of 12 | 12 of 12 |
| wall clock | 73 min | 12.2 min |
The escalation rate matches to within a percentage point and the footnote count to
within five on a book five thousand words shorter, which is the evidence that the
port changed behaviour nowhere. The wall clock is a GPU, not a platform: 0.3 s per
escalated call against 1.5 s on an M-series laptop.
v1.0.0b3
Everything here was found by running the tool against two books it had never
seen — All the Pretty Horses and Suttree — which is the first time it has been
asked to work on anything but the novel it was written for.
Fixed
-
The EPUB 2→3 upgrade no longer breaks a valid NCX.
read_packagetook the
firstdc:identifierand ignoredpackage/@unique-identifier, the attribute
whose only job is to say which identifier is the book's identity.
sync_ncx_identifierthen wrote that wrong value into the legacy NCX — so All
the Pretty Horses, whose OPF lists an ISBN first and its real identity second,
came out with an epubcheckNCX-001error it did not have going in. Books with a
singledc:identifier(The Crossing, Suttree) cannot show the difference,
which is why one book was not enough to catch it. -
Latin is no longer annotated as Spanish. A two-language detector cannot answer
"neither": asked about Latin it must choose, and it does not choose English.
Suttree — a novel with no Spanish in it — producedStabat Mater Dolorosa.at
SPANISH 0.94, above the accept threshold, which skips adjudication entirely and
becomes a confident, wrong footnote.The obvious fix is wrong. Adding Latin to the detector dilutes the distribution the
thresholds are tuned against:Dieciseis.drops from 1.00 to 0.54 and stops being
accepted, and it is a documented Tier-1 strength that the LLM gets wrong. So the
primary detector is untouched, and Latin is asked about separately as a veto on the
accept path — the one place where being wrong is unrecoverable. The dialogue tag
is stripped before the test, which is what makes it safe:Respóndele, he said.
ranks Latin 0.64 / Spanish 0.34 with the attribution attached and 0.00 / 1.00
without it. That is the same tag distorting a third measurement, for the same
reason it distorted the other two.Measured over 1,094 known-Spanish passages from two books: zero wrongly vetoed.
-
The model is told that Latin is not Spanish. The veto guards only the accept
path. Two of Suttree's three Latin passages escalated instead, and the model,
asked directly, agreed they were Spanish and translated them. The prompt and its
few-shot examples now cover this. -
--resumechecks the Tier-1 verdict before reusing an answer. Tier 1's two
outcomes call different prompts — an accept getstranslate("this is Spanish,
render it"), the band getsadjudicate("is this Spanish at all?") — and a
translate-only answer always says yes. Resuming across a Tier-1 change would
therefore hand a "yes" to a segment the new code wants adjudicated, quietly
reinstating the footnote the change existed to prevent. The Latin veto is exactly
such a change.
Documentation
- The install guide covers
uvitself and the~/.local/binPATH step. Following
the previous instructions on a clean machine got youjoven: command not found. - Results now cover three books, including the control run and the precision figure
it produced.
Measured
Re-running the Suttree control after these fixes: six false positives down to
two, in 177,257 words. One Latin passage was stopped by the Tier-1 veto, two by
the prompt, and Vag. went with them. Escalation rate and wall clock are unchanged
(2,548 calls, 63 minutes), so nothing was traded for it.
Still open
No suh.→ "No sir." — dialect English that the similarity veto misses at a 0.67
ratio, just under the 0.75 threshold.Ay.— genuinely ambiguous out of context; English here, Spanish elsewhere.
v1.0.0b2
Fixed
- Books using HTML named entities no longer fail to parse. XML defines five
entity names; XHTML in the wild uses the full HTML set, so any book containing
,—or’was refused withEntity 'nbsp' not defined
and a traceback. The Knopf edition of The Crossing happens to use none, which
is why one book was enough to hide this. Named entities are now resolved before
parsing, on both sides of the text-preservation comparison so the invariant is
unaffected. - Font obfuscation is no longer misreported as DRM.
META-INF/encryption.xml
is how the IDPF and Adobe font-obfuscation schemes declare themselves as well as
how real DRM does, and unencumbered trade EPUBs carry it routinely. Refusing on
the file's presence rejected those books with advice to strip DRM that was never
applied. The check now reads the encryption algorithm and refuses only genuine
encryption, naming the resources it cannot read. - An unparseable document reports an error and exits, instead of raising a
traceback out ofinspect,detectoradd.
Added
- Releases publish to PyPI from a tag, via Trusted Publishing — no API token
in repository secrets. The install path becomesuv tool install joven-ebook-annotatorinstead of a venv and an editable checkout. A guard fails
the build when a tag disagrees with the version inpyproject.toml, because
PyPI will not let a version number be reused. See
docs/releasing.md. joven detect --resume TRACE. A full run is 73 minutes and the sidecar was
written only at the end, so an interruption at minute 70 lost all of it. The
trace is now flushed per record and can be replayed: every model answer it holds
is reused and only unreached segments cost anything. Tier 1 and the suppression
gates still run over the whole book, so a resumed run reflects the current code;
recorded errors are retried rather than inherited.