Add a user-selectable playback mode stored in user_settings:
- 'stream' (default): /api/audio-stream starts playing within seconds,
saves to MinIO concurrently — low latency
- 'generate': queue runner task, poll until full audio is ready in
MinIO, then play via presigned URL — legacy behaviour
UI toggles in two places:
- AudioPlayer idle pill: compact '· Stream / · Generate' inline next
to voice name and estimated duration
- ListeningMode controls row: pill alongside Auto, Announce, Sleep;
disabled and grayed out for CF AI voices (batch-only, no streaming)
startPlayback() now branches on audioStore.audioMode for non-CF AI
voices; generate mode uses the same runner task + progress bar flow
as CF AI but without the preview clip.
PocketBase: audio_mode text field added to user_settings on
pb.libnovel.cc (live) and in pb-init-v3.sh (create block +
add_field migration line).
- Add `archived` bool to domain.BookMeta, pbBook, and Meilisearch bookDoc
- ArchiveBook / UnarchiveBook patch the PocketBase record; ListBooks filters
archived=false so hidden books disappear from all public responses
- Meilisearch: add `archived` as a filterable attribute; Search and Catalogue
always prepend `archived = false` to exclude archived books from results
- DeleteBook permanently removes the PocketBase record, all chapters_idx rows,
MinIO chapter objects, cover image, and the Meilisearch document
- New BookAdminStore interface with ArchiveBook, UnarchiveBook, DeleteBook
- Admin HTTP endpoints: PATCH /api/admin/books/{slug}/archive|unarchive,
DELETE /api/admin/books/{slug}
- PocketBase schema: archived field added to live pb.libnovel.cc and to
pb-init-v3.sh (both create block and add_field migration)
Split chapter text into ~1000-char sentence-boundary chunks before sending
to kokoro-fastapi. Each chunk is generated individually and the raw MP3 bytes
are concatenated. This prevents the EOF / timeout failures that occur when
the server receives a very large single request (e.g. a full PDF 'Full Text'
chapter). chunkText() breaks at sentence endings (. ! ? newlines) to preserve
natural speech flow.
- parsePDF: return all text as single 'Full Text' chapter (admin splits manually)
- parseEPUB: fix chapter numbering to use sequential counter not spine index
- Remove dead code: chaptersFromBookmarks, cleanChapterText, extractChaptersFromText, chapterHeadingRE; drop pdfcpu alias and regexp imports
- Backend: POST /api/admin/books/:slug/split-chapters endpoint — splits text on '---' dividers, optional '## Title' headers, writes chapters via WriteChapter
- UI: admin panel now shows for all admin users regardless of source_url; chapter split tool shown when book has single 'Full Text' chapter, pre-fills from MinIO content
Three fixes to PDF chapter extraction quality:
1. Page ordering: parse page number from pdfcpu filename (out_Content_page_N.txt)
instead of using lexicographic sort index — fixes chapters bleeding into each
other (e.g. Prologue text appearing inside Chapter 1).
2. Windows-1252 chars: map bytes 0x91-0x9F to proper Unicode (curly quotes U+2018/
U+2019/U+201C/U+201D, em-dash U+2014, etc.) instead of raw Latin-1 control
bytes that rendered as ◆ in the browser.
3. Chapter header cleanup: skip the first page of each bookmark range (decorative
title art page) and strip any run-on title fragment at the start of the first
body page (e.g. 'for New Journeys!I stood atop...' → 'I stood atop...'). The
remaining sentence truncation is a fundamental limitation of this PDF's
PUA-encoded body font (C2_1/Literata) — those glyphs cannot be decoded without
the publisher's private ToUnicode mapping.
Use PDF outline (bookmarks) for chapter titles and page ranges, then
extract text from per-page content streams via pdfcpu ExtractContent.
This avoids the indefinite hang caused by dslipak/pdf trying to resolve
custom font ToUnicode CMaps on publisher PDFs.
- parsePDF: decrypt → ExtractContent → Bookmarks → chaptersFromBookmarks
- chaptersFromBookmarks: flatten bookmark tree, skip front/back matter
(Cover, Insert, Title Page, Copyright, Appendix), assign page ranges
- extractTextFromContentStream: handle TJ arrays (concat literal strings,
skip hex glyph arrays and kerning numbers) + single Tj strings
- Falls back to paragraph-splitting when no bookmarks present
- Build verified; test PDF produces 9 chapters with proper titles
- parsePDF function restored in import.go (body was orphaned outside function)
- ParseImportFile() called at upload time with 3-min timeout; chapters stored as JSON in MinIO
- runner.go: prefer ChaptersKey path (read pre-parsed JSON) over BookImport.Import()
- ImportChapterStore interface added; store wired in runner/main.go
- HeartbeatTask and ReapStaleTasks now include import_tasks collection
- parseImportTask now returns ChaptersKey in domain.ImportTask
- asynq_runner.go handleImportTask passes ChaptersKey
- pb-init-v3.sh: chapters_key field added to import_tasks schema
- parsePDF now attempts to strip encryption via pdfcpu (empty user password)
before handing bytes to dslipak/pdf — fixes '256-bit encryption key' error
on publisher PDFs that use owner-only encryption (copy/print restrictions)
- Add pdfcpu v0.11.1 as direct dependency (was already indirect)
- docker-compose.yml minio-init: add 'imports' and 'translations' buckets
so a fresh deploy creates all required buckets
- Add ImportFileStore interface to bookstore package
- Add ImportFileStore field to backend.Dependencies
- Wire ImportFileStore: store in cmd/backend/main.go
- handlers_import.go: use s.deps.ImportFileStore.PutImportFile instead of
broken s.deps.Producer.(*storage.Store) type assertion (fails when Asynq active)
- pb-init-v3.sh: add import_tasks and notifications collection definitions
- Import task: persist object_key, author, cover_url, genres, summary,
book_status in PocketBase so the runner can fetch the file and write
book metadata on completion
- Runner poll mode: pass task.ObjectKey instead of empty string
- Runner: write BookMeta + UpsertBook in Meilisearch after chapter ingest
so imported books appear in catalogue and search
- Import UI: add author, cover URL, genres, summary, status fields; add
AI tasks panel (chapter names, description, image gen, tagline) after
import completes; add AI tasks button on each done task in the list
- Admin nav: add Notifications entry to sidebar (all 5 locales)
- Logout: delete user_sessions row on sign-out so sessions don't
accumulate as phantoms after each login/logout cycle
- docker-compose.yml: BODY_SIZE_LIMIT=52428800 (50MB) on UI service
— adapter-node was rejecting PDFs >512KB with 'Content-length exceeds limit'
- storage/import.go: add AnalyzeFile() public function using real PDF/EPUB parsers
- handlers_import.go: analyzeImportFile() now calls storage.AnalyzeFile() instead
of the file-size stub, so chapter count is accurate in the preview
The SvelteKit proxy was calling request.json() unconditionally,
consuming the body before forwarding. File uploads (multipart/form-data)
now use request.formData() and pass the FormData directly to backendFetch
so the fetch API sets the correct Content-Type boundary automatically.
The paraglide .gitignore uses '*' to ignore all generated files.
admin_nav_import.js was never force-added after the key was introduced
in v2.6.44, causing svelte-check to fail in CI with 'Cannot find module'.
- NewBookImporter now takes *Store instead of raw *minio.Client (same package access)
- Runner Dependencies gains ChapterIngester interface; storeImportedChapters no-op removed
- store.IngestChapters is now actually called after PDF/EPUB extraction
- BookImport and ChapterIngester wired in runner main.go
- CreateImportTask interface gains initiatorUserID param; threaded through store/asynq/handler
- domain.ImportTask gains InitiatorUserID field; parseImportTask populates it
- Runner notifies initiator (falls back to 'admin' when empty)
- UI: import task list polls every 3s while any task is pending/running
- UI: label[for] + input[id] fix for a11y warnings in admin import page
- Import page: add file upload with review step before committing
- Backend: add analyze endpoint to preview chapters before import
- Add notifications collection + API for admin alerts
- Add bell icon in header with notification dropdown (admin only)
- Runner creates notification on import completion
- Notifications link to /admin/import for easy review
- Add ImportTask/ImportResult types to domain.go
- Add TypeImportBook to asynqqueue for task routing
- Add CreateImportTask to producer and storage layers
- Add ClaimNextImportTask/FinishImportTask to Consumer interfaces
- Add import task handling to runner (polling + Asynq handler)
- Add BookImporter interface to bookstore for PDF/EPUB parsing
- Add backend API endpoints: POST/GET /api/admin/import
- Add SvelteKit UI at /admin/import with task list
- Add nav link in admin layout
Note: PDF/EPUB parsing is a placeholder - needs external library integration.
Cloudflare Workers AI changed the API for flux-2-dev, flux-2-klein-4b,
and flux-2-klein-9b to require multipart/form-data (instead of JSON) and
now returns {"image":"<base64>"} instead of raw PNG bytes.
- Add requiresMultipart() helper for the three FLUX.2 models
- callImageAPI builds multipart body for those models, JSON for others
- Parse {"image":"<base64>"} JSON response; fall back to raw bytes for legacy models
- Use "steps" field name (not "num_steps") in multipart forms per CF docs
- Book page: capture and display actual backend error message instead of blank 'Error'
The book_comments collection has a custom 'id' field (type: text,
required: true) distinct from PocketBase's system record ID.
Every comment POST was returning 400 validation_required because
the id field was never sent.
Fix: generate a 15-char hex ID via crypto.randomUUID() and include
it in the payload, matching PocketBase's own ID alphabet.
- Remove all emojis (flame icon in streak widget, 🔥 in stats footer,
✓ in completed badge) — they cheapened the overall feel
- Fix double carousel indicator: drop the animated progress sub-line
below the active dot; the expanding pill shape is sufficient signal.
Also removes the rAF animation loop and progressStart state.
- Remove 'X left' badge from Continue Reading shelf cards
- Remove 'X chapters ahead' text from hero card info row
Hand-authored the 12 missing .js message modules and updated _index.js
since npm/paraglide-js codegen cannot run in this environment.
These are equivalent to what 'npm run paraglide' would generate.
Going forward: run 'npm run paraglide' after adding keys to messages/*.json.
- Remove compact player mode (seekable bar inline was undifferentiated
from standard; didn't have voice/chapter/warm-up controls)
- Add 'float' player style: a fixed, draggable mini-overlay positioned
above the bottom mini-bar (bottom-right corner, z-55)
· Seek bar, skip ±15/30s, play/pause, time, speed indicator
· Drag handle (pointer capture) to reposition anywhere on screen
· Animated playing pulse dot in the title row
· Suppresses the standard non-idle inline block when active so
there is no duplicate UI
- PlayerStyle type: 'standard' | 'float' (was 'compact')
- Bump localStorage key to reader_layout_v2 so old 'compact' pref
does not pollute the new default
- Listening tab Style row now shows Standard / Float
- Admin layout: SVG icons, active highlight, divider between nav sections
- Scrape page: status filter pills with counts, text + status combined search
- Audio page: status filter pills, cancel jobs, retry failed jobs, mobile cards for cache tab
- Translation page: status filter pills (incl. cancelled), cancel + retry jobs, mobile cancel/retry cards, i18n for all labels
- AI Jobs page: fix concurrent cancel (Set instead of single slot), per-job cancel errors inline, full mobile card layout, i18n title/heading
- Text-gen page: tagline editable input + copy, warnings copy, i18n title/heading
- Book page: chapter cover Save button, audio monitor link, currentShelf pre-populated from server
- pocketbase.ts: add getBookShelf(), shelf field on UserLibraryEntry
- New API route: POST /api/admin/translation/bulk (proxy for translation retry)
- i18n: 15 new admin_translation_*, admin_ai_jobs_*, admin_text_gen_* keys across all 5 locales
- Wrap {#key} children in fade transition (out 100ms, in 180ms+60ms delay)
for a smooth cross-fade between pages with no added dependencies
- Halve navigation progress bar animation duration (8s → 4s) for
more realistic feedback on typical navigations
- Add prefers-reduced-motion media query to collapse all animation/transition
durations for users with accessibility needs
- Add content-visibility: auto on footer to skip browser paint of
off-screen content and improve rendering performance
- Listening tab: expose Speed, Auto-next, and Announce chapter controls
alongside the existing Player Style row (chapters/[n]/+page.svelte)
- AudioPlayer: add Warm up Ch. N button in standard player ready state
when autoNext is off; shows prefetching spinner and completion status
- Book page: fetch saved audioTime on mount; show Resume Ch. N button
(desktop + mobile) that sets autoStartChapter and navigates directly
Replace the two-row layout (toolbar + lonely 'Play narration' button)
with a single pill row:
- Large circular play button on the left (brand color, active:scale-95)
- 'Play narration' label + voice selector button + estimated duration
(based on word count at ~150wpm) in the centre
- Chapters icon button on the right
Voice panel drops inline below the pill when open.
Non-idle states (loading / generating / ready / other-chapter-playing)
keep the existing toolbar + status layout unchanged.
Also adds wordCount prop to AudioPlayer and passes it from the chapter page.
speechSynthesis is silently muted on iOS Safari and Chrome Android after
the audio session ends (onended), so chapter announcements never played.
Fix:
- Add GET /api/tts-announce backend endpoint: streams a short TTS clip
for arbitrary text without MinIO caching (backend/internal/backend/)
- Add GET /api/announce SvelteKit proxy route (no paywall)
- Add announceNavigatePending/announcePendingSlug/announcePendingChapter
to AudioStore
- Rewrite onended announce branch: sets audioStore.audioUrl to the
announcement clip URL so the persistent <audio> element plays it;
the next onended detects announceNavigatePending and navigates
- 10s safety timeout in case the clip fails to load/end
- Merged docker-backend, docker-runner, docker-ui, docker-caddy into a
single 'docker' job. Docker Hub is now authenticated once; the credential
in ~/.docker/config.json is reused by all four build-push-action steps.
Eliminates 3 redundant login, checkout, setup-buildx, and scheduler
round-trips. Builds still run sequentially within the job so inline layer
cache pushes don't race each other.
- docker-ui now downloads the plain ui-build artifact from check-ui directly
(previously it depended on upload-sourcemaps which produced ui-build-injected).
- release job now only needs: [docker] instead of the previous 5-job list.
- Entire upload-sourcemaps job commented out as requested. Re-enable by
uncommenting the job block and adding 'upload-sourcemaps' back to the
docker job's needs list (also swap ui-build → ui-build-injected artifact).
CI svelte-check failed because Paraglide generates per-key .js files that
must be committed — the compiler runs at build time in CI (svelte-kit sync)
but the output directory is tracked in git, so new keys added to messages/*.json
require the corresponding generated files to be committed manually when node
is not available locally.
Added:
- messages/profile_theme_forest.js
- messages/profile_theme_mono.js
- messages/profile_theme_cyber.js
- messages/_index.js: three new export lines
Also fixed a11y warn: added aria-label='Close history' to the icon-only
close button in the discover page history drawer.
Forest — deep green surfaces (#0a130d base) with green-400 accent.
Inspired by terminal emulators and dark mode IDE forest schemes.
Mono — near-black zinc-950 surface with pure white/zinc-100 accent.
Minimal high-contrast monochromatic palette for distraction-free reading.
Cyber — near-black blue surface with neon cyan-400 accent and dracula-palette
success/danger colors. Cyberpunk aesthetic with neon glow feel.
Changes:
- app.css: three new [data-theme] blocks with full token sets
- +layout.svelte: added forest/mono/cyber to THEMES array (before light themes)
- profile/+page.svelte: same additions with i18n label functions
- messages/*.json: translated names in all 5 locales (en/fr/ru/pt/id)
Root cause: the chapter picker overlay (fixed inset-0) was rendered as a
descendant of the AudioPlayer's wrapping div, which is itself inside a
'rounded-b-lg overflow-hidden' container on the chapter page. Chrome clips
position:fixed descendants when a parent has overflow:hidden + border-radius,
so the overlay was constrained to the parent's bounding box instead of
covering the full viewport.
Fix: moved the {#if showChapterPanel} block from inside the standard player
div to a top-level sibling at the end of the component template. Svelte
components support multiple root nodes, so the overlay is now a sibling of
all player containers — no ancestor overflow-hidden can clip it.
Also replaced the subtle chevron-down close button with a clear X (×) button
in the top-right of the header, making it obvious how to dismiss the panel.
Two issues causing announce to silently fail or permanently block navigation:
1. No hard timeout fallback on speechSynthesis.speak():
Chrome Android (and some desktop) silently drops utterances not triggered
within a user-gesture window. If both onend and onerror fail to fire (a
known browser bug), doNavigate() was never called and the chapter
transition was permanently lost. Added an 8-second setTimeout fallback
(safeNavigate) that forces navigation if the speech engine never resolves.
safeNavigate is idempotent — guarded by a 'navigated' flag so it only
fires once even if onend, onerror, and the timeout all fire.
2. audioStore.chapters only written inside startPlayback():
The onended handler reads audioStore.chapters to build the utterance text
(Chapter N — Title). If auto-next navigated to this chapter and the user
never manually pressed play (startPlayback was never called), chapters
held whatever the previous AudioPlayer had written — potentially stale or
empty on a book switch. Added a reactive $effect that keeps chapters in
sync whenever the prop changes, same pattern as nextChapter.
Bug 1 — Auto-next not transitioning:
audioExpanded defaulted to false on the new chapter page because
audioStore.chapter still held the old chapter number when the page script
initialized. The $effect only opened the panel when isPlaying was already
true — a circular dependency (can't play without the panel, panel only opens
when playing). Fix: also set audioExpanded=true when autoStartChapter targets
this chapter, both in the initial $state and in the reactive $effect.
Bug 2 — Resume starts at the end:
onended called saveAudioTime() which captured currentTime≈duration and fired a
PATCH 2 seconds later (after navigation had already completed). Next visit to
that chapter restored the end-of-file position. Fix: in onended, cancel the
debounced timer (clearTimeout) and immediately PATCH audioTime=0 for the
finished chapter, so it always resumes from the beginning on re-visit.
doAction() was fire-and-forgetting the POST but never updating the client-side
votedBooks array. History was only populated from SSR data.votedBooks (loaded
at page init), so any votes cast during the current session were invisible in
the drawer until a full page reload. Now we prepend/replace an entry in
votedBooks optimistically the moment a swipe action fires.
- Remove tab switcher, move history behind a modal drawer (clock icon in header with badge count)
- Increase card aspect ratio from 3/4.2 to 3/4.6 for more cover real estate
- Replace 5 small icon-only buttons with 3 large labeled buttons (Skip / Read Now / Like)
- Read Now is solid blue as the center primary CTA; Skip and Like use tinted bg with colored border
- Swipe indicators are larger (text-2xl, border-[3px], bg tint) for better visibility
- Remove swipe hint text to reclaim vertical space
- Larger title text on card (text-2xl)
- Added announce_chapter (bool) to the user_settings create block
- Added add_field migration line for existing installs
- Also backfilled missing user_settings fields in the create block
(theme, locale, font_family, font_size were already migrated but
absent from the create definition)
- Migrated live prod PocketBase (pb.libnovel.cc) — field confirmed present
When enabled, the Web Speech API speaks the upcoming chapter number and
title (e.g. 'Chapter 12 — The Final Battle') between auto-next chapters,
giving an audible cue before the next narration begins.
- AudioStore.announceChapter ( boolean, default false)
- PBUserSettings.announce_chapter persisted to PocketBase
- GET/PUT /api/settings includes announceChapter field
- +layout.server.ts loads + defaults the field
- +layout.svelte applies on load, saves in debounced PUT, and fires
SpeechSynthesisUtterance in onended before navigating (falls back to
immediate navigation if speechSynthesis is unavailable)
- ListeningMode: 'Announce' pill added to the Speed · Auto · Sleep row
Replace hardcoded bg-amber-500/text-zinc-900 with bg-(--color-brand)/text-(--color-surface)
to match the rest of the UI's button palette (both grid and list views).