fix: index CW:28 body, validate API contracts, repair broken data pointer
opencode
fe038ce
- Convert CW/1/革命时期的爱情.txt from GB18030 to UTF-8 (203,548 -> 304,547
bytes, 102,538 characters, zero replacement characters, exact round-trip) and
drop its LFS attribute so the text ships as ordinary content.
- build_fulltext_db.py now decodes source TXT with the same strict UTF-8 then
strict GB18030 fallback as app.py. It previously used lenient UTF-8, so CW:28
was indexed as mojibake and its body could not be searched.
- /api/random accepts the canonical plural sources filter and returns 404 for an
unknown source instead of silently returning an unrelated record.
- /api/folders and /api/folders/.../contents return a JSON 404 for an unknown
source, matching /api/search/{source}.
- Unknown /api/* paths return a JSON 404 instead of the SPA HTML with HTTP 200.
- data/sources.json.gz: the LFS object has been absent from the Hub since the
first upload, so the file is rebuilt from the verified source contract in
data/sidebar/global.json and data/search_data.json.gz, and it i…
perf: avoid full-document decode in snippets and batch low-coverage wildcards
opencode
3f38b03
Snippet anchor and fallback queries called length(content), which makes FTS5
decode the entire document just to decide a trailing ellipsis. Request one
character past the window instead; the result is identical because substr()
returns the full remainder when it is shorter than the requested length.
Wildcard verification now batches rowid IN GLOB below 75% candidate coverage
and keeps the single full scan above it. Measured on both production indexes:
1.3-1.7x faster at 24-73% coverage, slower at 90%+.
Local in-memory A/B over seven alternating rounds: normal 手机 83->39 ms,
exact 手机 97->54 ms, exact 第*期 946->659 ms, exact 手*机 1057->708 ms.
Totals and IDs unchanged across 24 page payloads and 14 wildcard result sets.
fix: keep doubled dashes and ellipses inside one TTS clip
- Preview/snippet decode strict UTF-8, then strict GB18030 (CW/1/革命时期的爱情.txt is the only non-UTF-8 TXT of 47,250); UTF-8 files are unchanged.
- First clip may extend to the nearest clause mark within 32 characters; starts/seeks snap back to a clause start within 48; batch ends prefer clause marks before word boundaries. First clips ending on punctuation: 50% -> 87.5% on sampled CCRD text.
perf: play TTS clips from memory, long-poll parts and queue next batch early
- Bounded in-memory MP3 clip cache (48 clips/4 MiB, 4 ahead, 2 concurrent GETs, released on stop/page exit): sentence changes and next/previous/double-click on generated parts no longer wait for an HF round-trip; back-navigation plays held clips before cache_only confirms.
- Long-poll /tts/jobs/{id}?after=N; queue the following batch once the current one publishes.
- Retry client_busy/cancel_pending 429 after ~0.4 s instead of the unreadable 5 s Retry-After.
fix: retry TTS gateway errors and restore CW LFS attribute
- Treat non-JSON (HTML) 502/504 gateway responses as transient TTS errors.
- Report paused playback to mediaSession.
- Bump tts-client.js query version.
- Re-add LFS attribute for CW/1/革命时期的爱情.txt; removing it in 85ff6f28 made the Space serve the pointer text.
fix: align spoken sentences and reuse prepared audio on seeks
Do not merge multiple sentences in one source part; keep connective prefixes with their clauses and preserve lexical tones. Seek within live current/next jobs instead of stop/rebuilding them, with epochs and in-flight guards. Retain only two played/ready descriptors for completed-cache-only reopening; misses prepare just the target, never an old batch. Preserve pause, sleep, source Unicode, inline/audio cleanup and all search behavior.
feat: play first MP3 directly from the synthesis response
Request opt-in inline first audio on explicit play, validate bounded bytes against the original part and decode a temporary local MP3 Blob. Eliminate the first audio GET without pre-generating books. Revoke on clip end, navigation and stop; stale responses allocate nothing. Keep canonical GET and pause-aware decode-error fallback, immutable later parts and all native controls.
feat: read complete words with low-latency first phrases
Use bounded browser word and natural-clause boundaries, protected numbers and original UTF-16 plans rather than fixed-character cuts. First POST waits for published audio; synthesis is fixed at 1.2 independent of playback rate. Hide voice name and replace collapse chevron with a horizontal icon while preserving collapse/stop and preview independence.
fix: preserve native reader focus and bounded source markup
Normalize legacy WebView fallback highlights rather than accumulating per-clip text nodes. Keep active controls within the preview focus scope while another source loads and detach only before DOM replacement. Reopening controls preserves active speech; visible-start explicitly switches source. Exercise real long Unicode fallback DOM and actual first-control inline Tab escape.
feat: compact native preview reader with rolling long-text playback
One preview play icon, fixed female01, compact non-modal native controls and direct CORS audio. Start at visible source or double-clicked sentence; support locate, pitch/rate/sleep and pause-aware bounded prefetch. Remove document-wide 20k cap and iframe startup chain; first batch targets 80 codepoints, subsequent 480. Request opt-in pauses for unknown phones without changing originals.
test: walk full preview focus cycle including reader entry points
The new explicit speech buttons extend keyboard order. Retain close/fullscreen order checks and assert every visible Tab transition plus wrap instead of treating close/theme as the last focusable control. Production code and generated data unchanged.
Yesterday
feat: pilot explicit preview and selection reading with Hitokoto
Reuse the lazy independent CPU reader through a strict two-origin window/channel bridge. Preserve exact preview text and selection context, require explicit synthesis and active replacement, and keep search/filter/preview independent of the persistent player. No index/corpus/backend/model dependencies or SW cache-name changes.
fix(filters): use one stateful expand-collapse button