Commit Graph

23 Commits

Author SHA1 Message Date
NilsBriggen ed1df8986f Bug-hunt sweep: fix 13 verified issues (correctness + perf)
CI / build-apk (push) Successful in 5m59s
CI / test (push) Successful in 17s
CI / deploy-web (push) Successful in 32s
From a multi-agent bug-hunt + adversarial verification pass (0 critical/
high; 15 mediums). Fixed 13; 2 deferred as bigger refactors.

Correctness:
- srs: "Hard" no longer overshoots "Good" for reviewed cards (reps>=2). It
  compounded ease AND x1.2; now grows x1.2 off the previous interval only.
  + regression test.
- enrich/dates: stop reading the modal verb "may" as the month May (phantom
  calendar events). "may" needs an ordinal/year/date-preposition now.
- enrich/bib: disambiguate colliding BibTeX keys (smith2020, smith2020a, …)
  — duplicates corrupted reference-manager imports.
- learn/glossary: junk-term guard used && (dead); now || so all-stopword
  terms like "there" are actually skipped.
- transcription/engineImpl.web: don't collapse Whisper's null end-timestamp
  to a zero-length [t,t] segment; estimate from the next chunk or window
  duration (fixes citation/seam anchors).
- transcription/pipeline: re-check the abort signal AFTER each chunk so
  Cancel works on single-chunk audio (it previously still saved).
- db/repo.native: use withExclusiveTransactionAsync for reassign /
  deleteCourse / upsertVectors / createFlashcards (withTransactionAsync is
  not isolated on a shared connection → interleaved/half-applied writes).
- db/repo.native: don't cache a rejected open/migrate promise — a transient
  first-open failure no longer bricks storage for the whole session.
- stores/transcriptsStore: sequence-guard refresh() so overlapping
  focus/typing/filter refreshes can't resolve out of order and show stale
  results.
- audio/wav: decode WAVE_FORMAT_EXTENSIBLE (0xFFFE) via its SubFormat GUID
  + add 24-bit PCM — common ffmpeg/Windows WAVs no longer hard-fail native
  import. + tests.

Performance / footprint:
- audio/decode.native: decode straight to 16kHz (decodeAudioData sampleRate
  hint) so the JS side never holds a full-rate buffer or runs the resample
  loop — big memory/OOM win on long lectures.
- models/catalog display: Settings model sizes are now backend-aware (the
  no-GPU WASM path pulls ~2x fp32 weights; was advertising ~half).

Feature gap:
- download: native exports were silent no-ops. New download.native.ts writes
  to cache + opens the share sheet (expo-sharing); transcript/ICS/Anki-CSV/
  BibTeX/RIS exports now work on device.

Deferred (bigger): lexical-search recall ceiling (needs a full-corpus rank
path on both repos); triple in-memory copy of the encoded file during
transcribe (media is keyed by a not-yet-existing transcript id).

Validated: tsc clean, 282 tests pass, web export clean (native deps not
bundled), arm64 APK compiles with expo-sharing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:05:17 +02:00
NilsBriggen 4bddc67e1a Web transcription: load an fp32 decoder on WASM (fixes MatMulNBits)
CI / test (push) Successful in 18s
CI / deploy-web (push) Successful in 35s
CI / build-apk (push) Successful in 6m46s
The WASM backend couldn't create a session — "TransposeDQWeightsFor
MatMulNBits Missing required scale" — so transcription failed to start on
mobile (which we route to WASM) and on any no-WebGPU device. Root cause:
the default quantized Whisper decoder (q8/q4, decoder_model_merged) uses
MatMulNBits ops that the onnxruntime-web bundled with transformers.js
4.2.0 cannot load on WASM. Reproduced and bisected in a browser harness:
across both Xenova and onnx-community repos, q8/q4/string-fp32 all fail on
WASM with this error, while an EXPLICIT per-file fp32 decoder
({ encoder_model: 'fp32', decoder_model_merged: 'fp32' }) loads and runs
(2/2 chunks, no error) on a no-GPU machine.

So WASM now requests that explicit fp32 decoder; WebGPU keeps fp16. Larger
download on the WASM path, but it actually loads and runs — q8 never did
on WASM. (4.2.0 is the latest transformers.js, so bumping isn't an option.)

tsc clean, 279 tests pass, web export OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:16:42 +02:00
NilsBriggen fa12817050 Transcription progress UX + use WASM on mobile web
CI / test (push) Successful in 19s
CI / deploy-web (push) Successful in 35s
CI / build-apk (push) Successful in 6m43s
Two issues behind "it just shows downloaded 100% / loads forever / crashes
after a couple seconds":

1. Progress was invisible during transcription. The 'transcribing' stage
   only fired AFTER the first (slow) chunk, so the UI sat at "Loading
   model… 100%" through the entire first inference and looked frozen.
   - pipeline now emits a transcribing kickoff (progress 0) BEFORE the
     first chunk and carries chunkIndex/chunkCount on every event.
   - transcribeStore threads those through; the Library job card shows a
     spinner, "Transcribing… N%", and "part i of N" so a long file's
     progress is legible and obviously advancing.

2. Web crash on mobile. We picked WebGPU + fp16 whenever navigator.gpu
   existed, but mobile WebGPU drivers crash on sustained fp16 Whisper
   inference (transcribes a chunk, then the GPU process dies). Mobile web
   now uses cross-origin-isolated multi-threaded WASM (slower but stable);
   desktop keeps WebGPU.

Native's "loads forever" was the same missing-feedback problem — it was
grinding through chunks with no UI signal; the chunk counter now shows it.

tsc clean, 279 tests pass (pipeline progress test updated for the kickoff
event), web export OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:05:54 +02:00
NilsBriggen e3ed03a471 Transcription: don't pass task/language to English-only models
CI / test (push) Successful in 18s
CI / deploy-web (push) Successful in 38s
CI / build-apk (push) Successful in 6m40s
Whisper ".en" models reject `task`/`language`; transformers.js throws
"Cannot specify `task` or `language` for an English-only model" if either
is passed — and the web engine passed `task: 'transcribe'` (plus a
possibly-undefined `language`) unconditionally. Since the default model is
tiny.en, web/PWA transcription failed for every default user.

Only add `task`/`language` for multilingual models now (translation also
requires a multilingual model), and never pass an explicit `undefined`
language (which trips the same check). Mirror the same gate on native
(whisper.rn) so a .en model is never asked to do a language/translate it
can't.

tsc clean, 279 tests pass, web export OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:39:24 +02:00
NilsBriggen 06519faea2 Native: download whisper model on demand + surface real errors
CI / test (push) Successful in 18s
CI / deploy-web (push) Successful in 33s
CI / build-apk (push) Successful in 6m40s
Two native-only fixes behind the "transcription failed [object Object]"
report on the APK:

1. Model download. engineImpl.native.loadModel assumed the ggml .bin was
   already on disk (the downloader was a TODO), so on a real device it
   rejected — and since in-app recording is web-only, no model was ever
   present. Now loadModel fetches the model from Hugging Face
   (ggerganov/whisper.cpp) into <documentDirectory>/models on first use,
   reporting 0..1 progress through the existing "Loading model… X%" UI.
   Downloads to a .part file and renames on success so an interrupted
   download can't leave a truncated model; if initWhisper still rejects,
   the file is deleted so the next try re-downloads cleanly. Model URLs
   verified (tiny.en 77.7MB / base.en 148MB / small.en 488MB).

2. Error surfacing. transcribeStore did `String(err)`, which renders a
   non-Error native rejection (whisper.rn / file system throw plain
   objects) as "[object Object]". New shared errorMessage() pulls
   message/reason/code (or JSON) out of whatever was thrown, so failures
   are actionable instead of opaque.

Progress downloads aren't in the new expo-file-system OO API yet, so the
fetch uses the still-supported expo-file-system/legacy resumable
downloader; native-only, not bundled on web.

Validated: tsc clean, 279 tests pass, web export unaffected (legacy not
in the web bundle), arm64 APK builds and the native JS bundle resolves
the legacy import. On-device download + transcribe still needs a real
phone to confirm end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:21:34 +02:00
NilsBriggen 7aa8b05971 Native audio: decode m4a/mp3/etc. (not just WAV)
CI / test (push) Successful in 23s
CI / deploy-web (push) Successful in 42s
CI / build-apk (push) Successful in 8m15s
On native, importing anything but WAV failed with "Only WAV is supported
on native" — ffmpeg-kit-react-native was retired in 2025 and a real
decoder was left as a follow-up. Since in-app recording is web-only, the
native app's only way to add audio is importing a phone recording — which
is m4a/aac — so transcription was effectively broken on the APK.

Add react-native-audio-api (Software Mansion, maintained) and route
non-WAV files through its standalone decodeAudioData(ArrayBuffer), which
uses platform codecs + bundled FFmpeg. WAV keeps its pure-JS fast path.
The decoded channels go through the existing toMono16k, mirroring the web
AudioContext.decodeAudioData path exactly. Bytes are read locally and
passed as an ArrayBuffer, so it doesn't matter whether the picker yields
file:// or content://. WAV is now sniffed by RIFF/WAVE content, not just
the extension.

The lib's config plugin is intentionally NOT enabled: it only sets up
playback (background audio mode, a media-playback foreground service, mic
permission), none of which decoding needs — and adding those would
contradict the app's on-device/no-extra-permissions stance.

Validated locally (arm64 Docker build): react-native-audio-api compiles
and autolinks against RN 0.85 / new arch; the arm64-v8a APK ships
libreact-native-audio-api.so + libav{codec,format,util}/libswresample.so
+ liboboe.so (48MB -> 58MB). tsc clean, 279 tests pass, web export does
NOT bundle the native dep. Runtime decode still needs an on-device check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:03:31 +02:00
NilsBriggen e256508c44 APK build: persistent ccache + Gradle cache + export-only target
CI / test (push) Successful in 23s
CI / deploy-web (push) Successful in 30s
CI / build-apk (push) Successful in 27m38s
The Android CI job recompiled all native C++ (whisper.cpp, reanimated,
worklets, gesture-handler) from scratch every push — ~41 min on the
shared runner — because nothing survived the `COPY . .` layer
invalidation, and `--load` exported the whole ~15GB image just to copy
one APK out.

Dockerfile (docker/android.Dockerfile):
- Install ccache and patch the NDK CMake toolchain file so every module's
  externalNativeBuild routes C/C++ compiles + links through ccache (the
  third-party modules don't honor ccache on their own). ccache dir is a
  persistent BuildKit cache mount, so objects survive across builds.
- Mount GRADLE_USER_HOME as a cache and pass --build-cache --parallel, so
  Kotlin/Java/resource/dex tasks and resolved deps persist too.
- Cache-mount the bun install dir; move the nodejs install into the cached
  toolchain layer (was reinstalling on every source change).
- New `apk` stage (FROM scratch) holding just the signed APK, for
  --output type=local extraction without loading the 15GB image.

CI (.gitea/workflows/ci.yml):
- build-apk now builds `--target apk --output type=local,dest=./out` and
  streams ./out/app-release.apk to /srv/wisp/wisp.apk — no --load, no
  docker create/cp, and the build image no longer piles up on the host.

Validated locally (arm64, real sign+assemble path):
  cold gradle 5m5s -> warm gradle 57s; 61/61 C++ compiles served from
  ccache (100% hit); 226/578 Gradle tasks from cache; export-only build
  60s wall, APK valid + v2/v3 signed. The first server build after this
  is still cold (~41 min) to populate the caches; pushes after that are
  much faster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:09:34 +02:00
NilsBriggen 108ac59cb1 Mobile UI: bottom tab navigation + auto-build APK on push
CI / test (push) Successful in 20s
CI / deploy-web (push) Successful in 27s
CI / build-apk (push) Successful in 42m14s
The Library crammed five text nav-links into a non-wrapping row with no
bottom navigation — unusable on phones. Restructure the primary screens
into a bottom tab bar:

- Add src/app/(tabs)/_layout.tsx: <Tabs> with 5 tabs (Library, Search,
  Study, Ask, Settings) with emoji icons and the accent active tint.
- Move index/search/study/ask/settings into the (tabs) route group; the
  parens keep URLs unchanged (/, /search, /study, /ask, /settings).
- Root _layout becomes a Stack hosting (tabs) (headerless) plus the
  secondary pushed screens (record, transcript/[id], courses, quiz,
  bibliography), each with a back-button header.
- Drop the per-screen <Stack.Screen> headers from the moved tabs (titles
  now come from Tabs); relocate Study's "Export Anki" action into the body.
- Library: remove the cramped header link-row and duplicate title; add a
  "nothing uploaded" subheader and a Courses link.

Verified at 375px: tab bar pinned to the bottom, 5 even tabs, navigation
between tabs works. tsc clean, web export OK, 279 tests pass.

CI: build-apk now runs automatically on every push to master (after
deploy-web), so the APK at /wisp.apk tracks master instead of being
frozen at a stale tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:40:32 +02:00
NilsBriggen 7611ada001 feat(phase5): optional generative RAG — ask your lectures, with citations
CI / test (push) Successful in 20s
CI / build-apk (push) Has been skipped
CI / deploy-web (push) Successful in 33s
Strictly opt-in, gated, with the deterministic features as the always-present floor:
- GenerationEngine: WebLLM (Qwen2.5-1.5B, WebGPU, CDN-loaded) + BYO-key cloud
  (OpenAI-compatible); native stub. Pure grounding prompt builder (4 tests).
- rag.askLectures: retrieve Phase-1 hits -> grounded prompt -> answer with
  citations; refuses when nothing relevant; falls back to search-only when no
  engine is available. Never sends raw audio/transcripts — only question + snippets.
- aiStore (BYO key persisted in localStorage on web), Ask screen (answer +
  tappable citation chips that jump to the audio + honest "verify" disclaimer),
  Settings AI section (engine status + bring-your-own-key form).

279 tests green, 0 tsc errors, web export builds. ROADMAP phases 0-5 complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:56:57 +02:00
NilsBriggen 97aee3e4b3 feat(phase4): enrichment — dates->calendar, citations->references, book/paper links
CI / test (push) Successful in 16s
CI / build-apk (push) Has been skipped
CI / deploy-web (push) Successful in 29s
All detection pure/on-device; lookups send only the id/topic (never transcript):
- src/lib/enrich pure modules: dates (absolute + relative, anchored to lectureDate),
  citations (DOI/arXiv/ISBN/author-year), ics (RFC5545), bib (BibTeX/RIS), links
  (legit + Anna's Archive/LibGen search URLs) — 60 tests.
- lookup.ts: CORS-friendly metadata clients (Crossref/OpenAlex/Open Library) +
  Wikipedia summary; AbortController timeouts, session cache, never throws.
- UI: transcript "Dates & references" (add to calendar .ics, look up references +
  open DOI/OpenLibrary/Google Books/Wikipedia/Anna's Archive/LibGen); per-course
  Bibliography screen with BibTeX/RIS export.

Not going to app stores, so Anna's Archive/LibGen links are included per request
(search URLs only — never fetched/proxied). 275 tests green, 0 tsc errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:48:13 +02:00
NilsBriggen 40858e0025 feat(phase3): learning helpers — summary, glossary, flashcards (SM-2), quizzes
CI / test (push) Successful in 16s
CI / build-apk (push) Has been skipped
CI / deploy-web (push) Successful in 30s
Deterministic, on-device, no model:
- src/lib/learn pure modules (tokenize, summary [TextRank-ish], glossary
  [definition-pattern + frequency], flashcards [cloze/Q-A], srs [SM-2], quiz
  [MCQ with distractors]) — 37 unit tests.
- Flashcard persistence: Dexie v4 + native v4 `flashcards` table; create/list/
  listDue/updateSrs/delete/counts; cascades (transcript delete, course->Unsorted).
- UI: transcript "Study aids" (generate summary+glossary, click-to-seek; create
  flashcards), Study screen (SM-2 review + Anki CSV export), per-lecture Quiz,
  library Study link with due-count badge.

215 tests green, 0 tsc errors, web export builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:37:42 +02:00
NilsBriggen 6d2f583136 feat(phase2): in-app live recording into courses (web)
CI / test (push) Successful in 17s
CI / build-apk (push) Has been skipped
CI / deploy-web (push) Successful in 28s
- useRecorder: MediaRecorder mic capture with elapsed timer, live input-level
  meter (AnalyserNode), pause/resume, and a screen Wake Lock; returns the
  recording as an ArrayBuffer that flows into the existing transcribe pipeline.
- Record screen: big timer + level meter + start/pause/resume/stop; on stop the
  pre-capture sheet collects title/course/date, then it transcribes + saves +
  indexes (reusing Phase 0/1). Home gets Record + Import buttons.
- Web-only for now; native realtime (whisper.rn), OPFS crash-recovery,
  tab-audio capture, and the per-course dashboard are follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:53:15 +02:00
NilsBriggen fc24c0875d feat(phase1): on-device semantic recall — embeddings, vector search, hybrid ranking
CI / test (push) Successful in 17s
CI / build-apk (push) Has been skipped
CI / deploy-web (push) Successful in 27s
Exam-time search over your own lectures, 100% on-device (vectors never leave it):
- EmbeddingEngine (transformers.js feature-extraction via the CDN loader,
  multilingual-e5-small 384-dim, e5 query/passage prefixes); native stub.
- Vector store in StorageRepo (Dexie v3 + native v3 segvecs): upsertVectors,
  brute-force cosine searchVectors (course-scoped), clearVectors, unembeddedIds.
  Cascades: re-embed on segment edit, reassign updates vector courseId, deletes cascade.
- Hybrid search: semantic candidates + lexical rank fused via reciprocal-rank-fusion
  (pure, tested); searchLectures() returns segment hits tagged semantic/lexical/both.
- embeddingStore: build-index/backfill with progress + embed-on-save (fire-and-forget).
- Search screen: query -> segment hits (snippet · course · time) -> tap jumps the
  transcript to that timestamp (seek + scroll-into-view). Per-course stats on Courses.

25 repo tests (incl. cosine ranking + course scoping), 13 search tests, 170 total green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:48:03 +02:00
NilsBriggen 8db59d4bbe feat(phase0): courses, migrations, persisted audio, stable segment IDs, course-aware UI
CI / test (push) Successful in 16s
CI / build-apk (push) Has been skipped
CI / deploy-web (push) Successful in 29s
Data layer (Zod-gated, behind StorageRepo; web Dexie v2 + native expo-sqlite
user_version migrations, both with no-data-loss segment-id backfill):
- Course entity + courseId/lectureDate/instructor/location/tags/lectureNumber on
  transcripts; listCourses/createCourse/updateCourse/deleteCourse, listByCourse,
  reassign. Stable per-segment ids assigned on create/update + backfilled.
- Persisted source audio (Dexie blob / expo-file-system) via putMedia/getMediaUrl;
  the editor replays it after reload.

UI/stores:
- Pre-capture sheet (title + course + lecture date + language) before transcribing.
- Course-aware library home (All/Unsorted/per-course filter) + Courses screen.
- coursesStore; transcriptsStore course filter + reassign; transcribeStore threads
  course/date and persists media.

16 web-repo tests incl. v1->v2 migration no-data-loss; 148 total green; web export OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 13:54:38 +02:00
NilsBriggen 30dd2d5b75 ci(apk): arm64-only, tag-triggered, persistent /wisp.apk via host mount
- android.Dockerfile: build arm64-v8a only (--max-workers=2) → ~48MB APK, ~4x
  less compile (the 4-ABI build timed out on the shared prod runner).
- ci.yml: build-apk runs only on version tags (v*), timeout 120m — normal pushes
  no longer peg the box; web still auto-deploys on every master push.
- wisp.compose.yml: mount /srv/wisp/wisp.apk read-only so the download survives
  web container rebuilds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 13:54:38 +02:00
NilsBriggen 7a1e6b06c1 fix(android): working signed release APK
CI / test (push) Successful in 22s
CI / deploy-web (push) Successful in 34s
CI / build-apk (push) Failing after 3h1m42s
- docker/android.Dockerfile: install nodejs (image ships Bun; ci-android-sign.sh
  patches build.gradle with a Node script).
- buffer polyfill: whisper.rn -> safe-buffer requires Node's `buffer`, absent in
  Metro/RN. Add `buffer` dep, alias it in metro.config.js, set the global in
  _layout. Web export unaffected (verified).

Validated locally end-to-end: assembleRelease BUILD SUCCESSFUL, 117MB APK across
all 4 ABIs with librnwhisper.so, signed (CN=Wisp, O=briggen.dev).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:59:08 +02:00
NilsBriggen 74a436b5ee docs: study-system roadmap (courses, on-device semantic recall, learning helpers, enrichment, optional RAG)
CI / test (push) Successful in 23s
CI / deploy-web (push) Successful in 7s
CI / build-apk (push) Failing after 6s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 21:10:36 +02:00
NilsBriggen ec90c1588c ci: build + serve signed Android APK on push to master
CI / test (push) Successful in 22s
CI / build-apk (push) Failing after 1m2s
CI / deploy-web (push) Successful in 29s
- docker/android.Dockerfile: full socket-safe build (JDK17 + SDK + NDK 27 +
  CMake + Bun) -> expo prebuild -> sign (BuildKit --secret) -> assembleRelease.
- ci.yml build-apk job: buildx build, extract APK via docker create/cp, copy
  into the running wisp container so it serves at /wisp.apk; artifact upload.
- app.json: set android.package = dev.briggen.wisp (required by prebuild).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:25:24 +02:00
NilsBriggen 1ec6f14f20 ci: trigger run after runner socket-mount fix
CI / test (push) Successful in 27s
CI / deploy-web (push) Successful in 30s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 18:44:20 +02:00
NilsBriggen 1ba172bdeb ci: run test on default image + install Bun (Node-less bun container broke checkout)
CI / test (push) Failing after 13s
CI / deploy-web (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 18:39:10 +02:00
NilsBriggen 84e7a56b13 CI: Gitea Actions web auto-deploy (runner-on-server) + signing prep
CI / test (push) Failing after 3s
CI / deploy-web (push) Has been skipped
- .gitea/workflows/ci.yml rewritten for a self-hosted runner co-located with
  the host Docker daemon + Traefik: push to master -> test -> deploy-web
  (docker compose up --build in place; no SSH, no registry). APK job documented
  as the next iteration (socket-safe build+extract; signing secrets already set).
- .gitignore: never commit the release keystore (*.keystore + wisp-release.keystore).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 18:27:26 +02:00
NilsBriggen 9f42ee2460 Build Wisp: on-device transcription studio (web + native, one codebase)
CI / test (push) Has been cancelled
CI / deploy-web (push) Has been cancelled
CI / build-apk (push) Has been cancelled
Private, offline speech-to-text that runs Whisper on the user's own device —
free, no account, no per-minute fees. Replaces Otter.ai / Rev.

- Pure, tested engine: chunking, overlap timestamp-stitching, exports
  (SRT/VTT/TXT/MD/JSON), WAV codec, resampler, job queue, model catalog (142 tests).
- Platform-abstracted TranscriptionEngine: transformers.js on web (loaded from
  CDN at runtime to dodge Metro's onnxruntime-web bundling limits), whisper.rn
  on native. Shared pipeline orchestrates decode -> chunk -> transcribe -> stitch.
- Cross-platform StorageRepo (Dexie web / expo-sqlite native), Zod-validated.
- UI: library + search, import, live-progress transcription, synced click-to-seek
  editor, multi-format export; model picker + privacy in settings.
- Web ships as a single-page PWA with COOP/COEP isolation for threaded WASM;
  Docker (nginx) image + Traefik compose for wisp.briggen.dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 17:54:21 +02:00
NilsBriggen 97996c9846 Initial commit
Generated by create-expo-app 4.0.0.
2026-06-13 17:05:52 +02:00