Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hark

A private, installable, offline-first audiobook player. Import a chaptered MP3 directly, or give Hark a PDF, EPUB, DOCX, text, Markdown, or HTML document and Kestrel Fast narrates it on this device. One account is one solo library, with the same chapters and resume position on every device.

Your books never leave your devices. MP3s and source documents are read in the browser, and audio is stored in this device's own storage; the server only ever sees metadata — titles, chapters, progress, and playback history. There is no upload, no object storage, and streamed MP3 imports have no practical file-size limit beyond the device's free space, so a single 600-hour audiobook imports the same way a two-hour one does. Document conversion is deliberately bounded before parsing: PDF 96 MiB, EPUB/DOCX 48 MiB, text/Markdown 8 MiB, HTML 2 MiB, and two million extracted characters. If the MP3 carries an embedded read-along transcript, its text is book content too, so it is stored on the device alongside the audio and never included in any server request.

Your library reads from the device, too. Every book row, chapter, tag and position is mirrored into this device's IndexedDB, and the library screen reads only from there; the network syncs afterwards, in the background. The design contract for that is docs/local-first.md.

What it does

  • Import: parses an MP3 entirely in the browser — title/author/narrator, embedded cover art, and ID3/FFMETADATA chapters (a valid chapterless MP3 plays as one chapter with an honest diagnostic). Documents are extracted by format-specific local adapters, narrated by Kestrel Fast through WebGPU with a WASM fallback, and progressively encoded as MP3. The pinned model bundle is downloaded and integrity-checked once; its pinned browser runtime is cached with the offline shell, and source plus generated audio stay local.
  • Play: persistent player with chapters, scrubbing, configurable skip intervals, 0.5x–3x speed, sleep timer (presets, custom minutes, end of chapter), a 50-action playback history, finished/restart state, and lock-screen Media Session controls where the browser supports them.
  • Read along: books whose MP3 carries an Epub Listener transcript get a "Text" toggle in the player and a "Read-along" badge in the library. Tapping it swaps the cover for the book text, where the narrated sentence highlights and auto-scrolls and the exact spoken word is marked karaoke-style; tap a sentence to jump there. It works fully offline. Books without a transcript look and behave exactly as before. (Transcript, if present, is validated and size-capped in the browser; a malformed one is dropped and the audio still imports.)
  • Resume anywhere: positions, playback history, and library organization sync through the server with per-device monotonic sequences and deterministic conflict rules. Every write is journaled to a local outbox first, projected onto the local mirror second, and replayed against the server by its mutationId so a retry is a no-op rather than a double-apply. On a device that doesn't hold the audio yet, the player asks for the original source and verifies it by size and content fingerprint before attaching it.
  • Offline: the library screen reads this device's IndexedDB mirror, so search, filters, sort, the "On this device" facet and the continue card behave identically with the network off — there is no "am I online?" branch on the read path. The /library document itself is served from Cache Storage without touching the network, so a cold database and airplane mode cost what wifi costs: warm-launch p95 measures 291-370ms across fast, slow, 3000ms-cold-database and offline profiles — a 70-73ms spread — with zero server document hits and zero Postgres queries. Those recorded figures used a 4x CPU throttle whose fixed proof loop cost 16ms. Current runs calibrate each host to that same 16ms CPU budget, with a fresh browser process per launch, so a slow shared runner cannot silently turn 4x into a much harsher device. Imported or generated audio is served by the service worker with full seeking, and queued writes replay when the app is next open.
  • Organize: search, status and tag filters, an "On this device" facet, sort orders, grid/list views, collections with optional next-book autoplay, archive, and delete. There is one library screen: books whose audio is not on this device stay browsable, searchable, taggable and sortable, are marked as not on the device, and never look playable.
  • Own your data: JSON export of all metadata/progress and full account deletion (rows and this device's local data — the audio files were always yours). A first document narration downloads about 39 MB of public Kestrel weights plus a 24 MB pinned browser runtime; later narrations reuse the verified on-device bundle and cached runtime.

Local setup

  1. Install Node.js >= 20.19 and pnpm 9.
  2. cp .env.example .env.local and fill in the values (see below).
  3. pnpm install
  4. pnpm db:migrate
  5. pnpm devhttp://localhost:3000 (or pnpm build && pnpm start for the production build, which is what enables the service worker).

Environment variables

Variable Purpose
DATABASE_URL Postgres connection (Neon or any Postgres 15+). Never commit it.
BETTER_AUTH_SECRET Session signing secret.
BETTER_AUTH_URL The app's own origin, e.g. http://localhost:3000.

Optional: RESEND_API_KEY and MAIL_FROM enable password-reset email in production (see docs/operations.md); in development, reset mails are written to .data/mail/ instead.

Test database

Tests never touch the hosted database: its cold start makes runs slow and flaky, suites must work offline, and parallel runs against one shared remote database interfere. docker-compose.yml provides a local Postgres 18 matching the hosted server's major version and locale (builtin C.UTF-8), with the pg_trgm extension migration 0009 needs, published on 127.0.0.1:54329.

cp .env.test.example .env.test   # local-only, gitignored
node scripts/test-db.mjs         # generates the secrets, then starts, migrates and seeds

Everything test-related reads .env.test, never .env.local. Override the file with HARK_ENV_FILE=<path> or --env-file=<path>. scripts/lib/assert-local-database.mjs aborts the e2e config, the standalone test server, and this bootstrap script if DATABASE_URL ever points at a hosted provider such as Neon.

Commands

Command What it does
pnpm verify The complete executable local gate: quick checks plus iPhone WebKit, offline parity, sync integrity, the 24 automatable resume cells, and launch performance. Start the test database and install the pinned browsers first.
pnpm verify:quick The fast non-browser gate: format check, lint, typecheck, all Vitest suites, and a production build.
pnpm verify:browser Every executable Playwright gate, matching the browser matrix required by .github/workflows/ci.yml. The two real-iOS hidden-state gaps remain deliberately outside CI.
pnpm verify:kestrel-export Clean-room Kestrel graph reproduction: downloads the exact pinned public model weights into a temporary directory, uses the pinned Python/ONNX/NumPy recipe, and requires byte-for-byte matches with all committed ONNX graphs.
pnpm test Vitest suites (MP3 and transcript parsing contracts, service-worker range/navigation/shell logic, progress conflict policy, the outbox and the mirror, IndexedDB upgrades, playback).
pnpm test:idb-migrate Just the IndexedDB upgrade suite: every shipped version of both databases is opened from a fixture and carried forward, so downloads, transcripts and a pending deletion journal survive chapterline-offline-v1 v7 and outbox v4.
pnpm test:e2e:ios Production iPhone/WebKit flow: register, choose from Downloads, play, seek, relaunch, and play offline.
pnpm test:e2e:launch The launch benchmark in tests/perf/: warm launch to real library content over four networks (fast, slow, 3000ms cold database, offline), asserting p95, spread, server document hits and Postgres queries.
pnpm test:e2e:parity The parity project in tests/parity/, whose harness removes the network at the socket instead of through Playwright interception, because interception sits above the service worker and can be bypassed.
pnpm test:sync The data-integrity project in tests/sync/: outbox durability and coalescing, two-device convergence, progress conflicts, mirror/audio eviction recovery, lossless re-import, a seeded mutation fuzz across offline/online transitions, and one spec that makes its edits by clicking the real controls with the network off — the fuzz drives the engine directly, so only that one can prove the shipping UI uses it.
pnpm test:resume:ci The 24 resume-durability rows the pinned WebKit engine can execute honestly. It excludes only T1 hidden online/offline, because Playwright WebKit cannot produce a real hidden transition.
pnpm test:resume The full 26-row resume oracle, including the two T1 rows that intentionally fail as UNCOVERED unless the engine can genuinely background the page. Use it when evaluating a new engine or real-device bridge.
node scripts/test-db.mjs Starts the local Postgres container, migrates it, and seeds the e2e account. Subcommands up, reset, down, migrate, seed, guard, psql are also exposed as pnpm db:test:*.
pnpm db:migrate Applies ordered SQL migrations (idempotent; proven from an empty database).
node scripts/seed-perf.mjs <email> Seeds 1,000 books / ~60k rows onto an existing account for performance work.

Browser-level verification uses agent-browser against the production build, exercising the core flows (register, import, play, offline, resume) across phone, tablet, and desktop viewports.

Pull requests run verify:quick and all five executable browser gates as separate, required-capable GitHub checks. Each job provisions its own local Postgres and project-pinned browser binaries; failed browser jobs retain their screenshots and traces as artifacts. The resume job runs 24 proven cells, while the two real-iOS hidden-state cells stay explicit in docs/resume-durability-device-check.md instead of making CI permanently red. Protect main with Verify quick and every Browser (…) check before treating a green deployment as a merge gate.

Three limits are worth stating before reading a green run as more than it is. The launch benchmark measures in a Chromium persistent context with iPhone 15 emulation: in Playwright 1.61.1, WebKit's persistent context accepts cache.put() but returns nothing from cache.match(), in the page and in the service worker alike, so the app's worker cannot even install there. The harness probes WebKit first on every run and will switch back automatically when that is fixed; until then, WebKit PWA coverage comes from tests/e2e/iphone-pwa.spec.ts. The outbox drains only while the app is open, because iOS will not wake a closed PWA — an offline write reaches the server on the next foregrounded launch or reconnect, never before. And audio evicted by the OS is gone: it exists nowhere but the device, so the book stays visible with its metadata and asks for the original MP3 or document again.

Repository layout

All application code lives in src/: app/ (routes and API), components/ (UI), lib/ (the device mirror, outbox, storage, and playback), server/ (schema, queries, and sync), and domain/ (pure logic). Browser-driven suites live in tests/, migrations in drizzle/, and the directly maintained service worker in public/sw.js.

A tracked-text count is about 94k lines, but that is not 94k lines of production logic. Roughly 36.6k are Drizzle's required cumulative migration snapshots, 8.3k are the lockfile, 31k are src/ including co-located tests, and 15.9k are browser/verifier suites. Generated migration and dependency state is marked for GitHub in .gitattributes, not deleted or misclassified.

Local build and run output remains ignored: .next/, node_modules/, TypeScript build metadata, test reports, .data/, local env files, and .vercel/. See docs/repository-anatomy.md for the exact audit, required generated files, large authored files, and counting rules.

Documentation

  • docs/architecture.md — stack, boundaries, data rules, the local read model, the write path, and the launch path.
  • docs/local-first.md — the design contract for the device-authoritative library: what is mirrored, the outbox, pull, conflict rules, the launch path, eviction, and account lifecycle.
  • docs/operations.md — deployment, backups, troubleshooting, limitations.
  • docs/ios-pwa-testing.md — automated WebKit and physical-iPhone release gates.
  • docs/resume-durability-device-check.md — the remaining physical-device resume-position check and its observable pass/fail signal.
  • docs/repository-anatomy.md — tracked-line breakdown, generated-file policy, and large-file classification.
  • tests/perf/BASELINE.md — historical proven-red launch baseline and the later measured local-first result.

About

Private, installable offline-first audiobook PWA with on-device MP3 storage, chapter-aware playback, cross-device progress sync, and no audio uploads.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages