Skip to content

Version 1.3.7 - #583

Merged
maboa merged 13 commits into
mainfrom
release/1.3.7
Aug 15, 2026
Merged

Version 1.3.7#583
maboa merged 13 commits into
mainfrom
release/1.3.7

Conversation

@maboa

@maboa maboa commented Aug 15, 2026

Copy link
Copy Markdown
Member

Exports that carry their captions, gap skipping that reaches the edges, and a card that stays clear of the navbar.

Fixes #561. Fixes #574. Fixes #577. Fixes #579. Fixes #580. Fixes #581.

Suite: 89 unit + 255 e2e green (1 known Playwright-WebKit OPFS skip).

maboa added 13 commits August 14, 2026 15:11
…RL (#561)

The page was never self-contained — its media is a separate file beside
it, and the bundle ships as one folder — so embedding the captions
bought nothing while carrying a percent-encoded copy of the whole track
inside the HTML. Files are also the only shape that extends: a
translated transcript wants one <track srclang> per language, which no
data: URL can express.

The .vtt now rides along whenever an interactive transcript is exported
without burned-in captions, and the modal says so. With the names
sanitised since #560, the page's media and caption references need no
encoding either — plain, readable filenames throughout.
Media served through a host application's own URL scheme (#547) is
fetched to embed it in a .hyperaudio file, and that fetch was
unguarded: when the handler could not produce the file — moved,
renamed, permissions changed, not ready yet — the raw TypeError escaped
as 'Failed to fetch' and the save was abandoned. Its sibling branch for
http(s) media has always caught, explained, and offered a link save.

Now both do. The wording differs deliberately: no server is involved
here, so it says the application could not provide the file rather than
blaming CORS. Same class of bug as #573 — an unguarded read of media
that can legitimately fail, surfacing as an opaque browser message.

The test declares a scheme with no handler, so the fetch genuinely
rejects, and asserts the explanation and that cancelling is a real
choice.
…last (#577)

Regions were only built BETWEEN kept words — regionStart derives from
prevKeptEnd, which is null until a kept word exists — so the lead-in was
never a candidate however long, and the trailing region existed only when
struck words followed the last kept word. The two gaps most likely to be
long were the two it ignored: the #371 fixture opens 2.24s before its
first word and ends 24.7s before the media does.

Both edges are now ordinary regions under the same threshold and edge
buffer. The tail is left alone when the duration is unknown rather than
guessed, and struck words in the lead-in are consumed by that region so
they cannot be cut twice.

PLAYBACK needed its own half: it jumps via checkStrikeThrus, which only
examined the bands BETWEEN kept sections, so correct sections alone
changed nothing a listener would notice. It now jumps to the first kept
section from before it, and lets the media end past the last one — which
is what Mark's testing caught, since my first round of tests exercised
only the export path.
WebKit's WebCodecs AudioEncoder picks HE-AAC for MONO input below 32 kHz
— a sharp, measured boundary — and the muxed track that results is
unreadable to AVFoundation: zero audio tracks, so the export is SILENT
in QuickTime, Safari and Finder preview while appearing to have
succeeded. Voice notes, telephony, voicemail and much interview audio
live below that line. Forcing the codec string to mp4a.40.2 does not
help; that was measured too, and the note is in the code so nobody
spends a day on it again.

Audio below 32 kHz is now resampled first, to a rate the source divides
exactly (16k -> 48k, 22.05k -> 44.1k) and clear of the cliff rather than
sitting on it. Upsampling cannot restore bandwidth the source never had,
nor lose any. If resampling fails the original buffer is used: a silent
export is bad, breaking one that used to work is worse. Applied on every
engine — sniffing for WebKit would risk guessing wrong about a variant,
and the early return means anything at 32 kHz or above pays nothing.

Both export paths funnel their buffers through ONE addAudio, used by the
direct branch AND the stretcher's emit, so no call site can be forgotten
— and the lift sits after SoundTouch, which keeps working on the smaller
source-rate buffers rather than three times the samples.

WHAT THE TESTS PROVE: the transformation, not the cure. They run in
Chromium, which does not exhibit the bug, and Playwright's WebKit is not
Safari's AVFoundation stack. File-level verification lives in the
WKWebView + AVAssetReader harness offered in the issue. Diagnosis and
measurements: maboa.
That route passed the live track's src straight into the page, and the
src is a data:text/vtt URL — a percent-encoded copy of the whole caption
track inside the HTML, and a shape no <track srclang> per language can
extend to. #561 fixed the media-export route; this is the other one.

The page and its .vtt now travel together in ONE archive. Two separate
downloads would let the browser de-duplicate the second to 'name (1).vtt'
and silently break the page's reference — the same hazard the media
export zips to avoid. With no captions the download stays a single HTML
file and the empty <track> is dropped, so the page never points at a
file that is not there; if JSZip fails to load, it falls back to that
same trackless page rather than failing the export.

The VTT checkbox's note is now conditional: the sidecar rides along with
an interactive transcript, but NOT when captions are burned into the
picture — the case where a blanket claim would be wrong, and the reason
a missing <track> looked like a bug.
At 1000px the compact band placed the card's top at 58px while the
navbar is 74px tall, so the navbar's grey band painted over the card's
top edge — the lost rounded corners — and over the ⓘ/copy buttons
pinned to it, which is why they looked faint and clipped. Constant, not
intermittent: what varied was whether a resize happened to repaint in an
order that briefly looked right. The 58px assumed a compact navbar
(#480) that the navbar has since outgrown.

Two changes, both about removing assumptions. --card-top is now measured
in responsive.js from the navbar's real bottom plus the 4px of canvas
the wide layout has always shown, re-measured on resize and by a
ResizeObserver, so a late-arriving toolbar control cannot strand it.
And every corner button — ⓘ, copy, and the touch-only undo/redo pair —
derives its top from that variable plus a per-band --text-inset, rather
than carrying its own constant per breakpoint: five hard-coded values
and two now-empty media blocks are gone.

The mobile band still computes its own --card-top in CSS (the player is
pinned under the navbar there), so the inline value is removed below
948px, where an inline property would outrank the media query. The CSS
constants survive only as pre-paint defaults, and the stale 58 became 78
so nothing flashes before the script runs.

Consequence, accepted deliberately: in the 949-1120px band the card
starts 20px lower than before, because those 20px were underneath the
navbar rather than visible.

Note for the record: I first blamed a z-index change of my own; Mark
doubted it, and the measurement proved him right — the constant was
stale long before.
@maboa
maboa merged commit 5152b27 into main Aug 15, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment