Smoke test — verifies the Vitest + jsdom harness is wired correctly. Does NOT render the full app (that requires a running backend). Real component tests will be added in 3.x.
The app shell's primary navigation: a collapsible icon rail that expands on hover (desktop). On the immersive Player route it hides and is reachable via a floating menu button as a left drawer. Profile lives at the foot (not in the nav).
The active-profile control in the sidebar foot. Shows the current profile and opens a small dialog to set or clear it. Profile data is just a localStorage id (see ProfileContext); richer management lives on the Dashboard.
The task tray: a discrete floating dock (bottom-right) that surfaces the active profile's uploads and jobs from the TaskContext. It collapses to a small pill (hidden entirely when there is nothing to show) and expands into a panel with per-task progress, cancel, and dismiss. Reuses the shell's Sumi & Shu surface language so it reads as part of the chrome.
Context for the token bundling mode (Words / Grammar / Morphemes), a client-side view preference persisted to localStorage and sent to the server tokenizer on every request.
Profile-scoped operation settings: the curated Whisper transcription options, the default clean_audio flag, and the conversion options that the single-op and batch flows send to the server. Persisted per profile in localStorage. An unset Whisper field is omitted from requests, so the server keeps its own tuned default rather than being overridden.
This file defines the context for managing the state of the video player. It allows the player's state (e.g., loaded video, subtitles, settings) to persist across component mounts and unmounts, enabling navigating between pages.
Global task tracking: holds the active profile's jobs (server-side) and in-flight uploads (client-side), polls running jobs, and exposes submit / cancel / dismiss for the task tray and the features that enqueue work. The server is the source of truth, so active jobs survive navigation and reload (re-seeded on mount).
The demo replacement for @/shared/api/client, aliased in only for --mode demo. apiFetch answers reads from fixtures, routes the job and profile-collection writes to the simulator / session store, and the upload helpers fake progress. API_BASE is base-aware so bundled /api/... fixtures resolve under --base=/mirumoji/ on GitHub Pages.
The demo replacement for @/shared/dict/api, aliased in only for --mode demo. It re-exports the real dictionary client (whose calls run through the demo apiFetch) and overrides only dictAudioClipUrl, the one hardcoded /api literal that bypasses API_BASE, to be base-aware and point at the bundled static audio clip.
The demo job-lifecycle simulator. The sample's long-ops (generate_srt / transcribe / convert / fix_srt) are submitted for real by the reused UI, so this advances a submitted job queued -> running -> succeeded over a few of TaskContext's 2000 ms polls and attaches the canned result.
The request-to-fixture key scheme shared by the demo transport and scripts/generate_demo_data.py. Both sides MUST produce byte-identical keys, so keep this in lockstep with the Python key_of mirror.
The demo's in-memory write store. The hybrid policy lets harmless writes (save a clip, delete, edit the template) feel real for the session and reset on reload. Mutable profile collections start from their recorded GET fixtures and apply an overlay of session additions/deletions.
The demo replacement for @/shared/api/sse, aliased in only for --mode demo. Replays a recorded LLM stream (raw SSE body text) through the same frame semantics as the real client, with a small per-token delay for the live typewriter feel. An unrecorded stream degrades to a short notice rather than a failed request.
Demo variant of the dictionary display components, aliased in only for --mode demo. It re-exports the real components and overrides the click-through rows / kanji card so a link to an entry that was not captured in the fixtures renders disabled (muted, no click) instead of landing on an empty page. This one file gates the search results, the word / kanji views, and the word dialog at once.
Demo variant of the Dictionary hub, aliased in only for --mode demo. Free search has no fixtures, so the search control is disabled with a hint to use the curated suggestions on the landing view. The breadcrumb trail and routed <Outlet/> are kept intact so links between the captured word / kanji views still work.
Demo variant of the load-media popover, aliased in only for --mode demo. Loading a device file has no fixtures, so the "From device" pickers are dropped; the profile-media list (the captured sample and its derived files) is kept and loads from fixtures exactly as in the real app.
Demo variant of the sidebar profile control, aliased in only for --mode demo. The demo runs on one fixed profile, so this shows it read-only with no set / clear dialog.
Demo variant of the dashboard Profile tab, aliased in only for --mode demo. The demo runs on one fixed profile, so it shows it read-only with no set / clear controls.
Demo variant of the Text Analyzer, aliased in only for --mode demo. Arbitrary pasted text has no tokenization fixture, so it shows the captured sample sentence (read-only) tokenized from the recorded fixture.
Preloads the demo's sample episode into the player on startup, so the tour opens on a ready-to-watch video. Reads the committed sample.json descriptor; a no-op until the generator fills it.
The allowlist of dictionary entries captured in the demo fixtures, so demo views can disable links whose target has no fixture (rather than let a click land on an empty "Nothing Found" page).
Demo variant of the player context, aliased in only for --mode demo. It wraps the real provider so the sample-preloading bootstrap and the demo banner mount inside it, and re-exports everything else unchanged.
Demo variant of the profile context, aliased in only for --mode demo. It pins the single fixed demo profile in localStorage before anything reads it (both the context initializer and the fetch layer read localStorage directly), then re-exports the real provider unchanged.
The Dictionary hub layout: a single integrated search control (Japanese / English / Radicals modes fused to the input) above a breadcrumb trail that stitches the routed views together (search results, word entry, kanji detail, radical search). Every view is URL-addressed so back/forward, deep links, and the WordDialog's cross-link all work.
The Dictionary hub's kanji view: a large stroke-order drawing with replay / step / speed controls, the kanji's stats and readings, pronunciation clips (Kanji Alive), its radical components, words that use the kanji, and example sentences.
The Dictionary hub's radical search: pick radicals from an inventory grouped by stroke count, see every kanji containing all of them, and open a kanji's detail view. The picked set rides in the URL so the search is shareable and survives back/forward.
Persists the Dictionary hub's recent lookups (words and kanji) in localStorage, so the landing page can offer them back and the hub keeps a sense of state between visits.
The Dictionary hub's index view: an invitation when idle, wildcard results (entries / names / kanji tabs) for Japanese pattern searches, and entry rows for English reverse lookups. Rows navigate to the word / kanji views.
The Dictionary hub's word view: the full entry for one word — furigana headword with badges, numbered senses with readable tags (cross-references resolve on click), its kanji as clickable cards, and clickable example sentences with translations.
The Home page — a quick-start launcher: active-profile status, primary actions, a glance at recent clips/transcripts, and links to the Guide / repo / docs.
Player video API helpers. The long-running operations (SRT generation, MP4 convert, LLM fix) now run as jobs via the task tray; this only keeps the direct SRT-save helper.
A popover to load a video + subtitles, either from the device or from the active profile's stored files. Profile media is grouped by lineage, so a video's derived files (subtitles, a converted MP4) sit right under it, and every row loads on its own.
The slim player top bar: load media, subtitle style, furigana toggle, and the SRT actions (generate / convert / LLM fix). The long-running actions are submitted as jobs and tracked in the task tray, which loads each result back into the player when it finishes.
The subtitle-navigation panel: the full cue list with search, click-to-seek, and an active-cue highlight that auto-scrolls into view. Collapsible to a thin rail so the video can reclaim the width.
The player's video surface: composes the shared VideoPlayer and layers click-through overlay subtitles on top, anchored to the painted frame (computed by useVideoBox) rather than the element box.
Tracks the active subtitle cue from a video element's playback time. Binds to the element value (callback-ref) so the listener attaches exactly when it mounts, and drives updates from a requestAnimationFrame loop while playing — timeupdate alone fires sparsely and can stall, which froze the subtitle on a cue. setActiveIdx bails out when unchanged, so the rAF loop costs nothing until the cue actually changes.
Computes the painted video rectangle inside an object-contain video element (the element fills the stage, but the actual frame is letterboxed within it). Used to anchor overlay subtitles to the frame rather than the element box.
The Player page: a video stage with overlay subtitles, a collapsible subtitle-navigation panel, and a slim toolbar. Pulls media + style from the global contexts so state persists across navigation.
The Advanced tab: profile-scoped operation settings, split into Transcription (curated Whisper opts + clean-audio) and Conversion sub-tabs. Each field carries a tooltip and a faded placeholder showing the server default; an empty field is omitted from requests. A draft is edited locally and committed with Save (or Reset to defaults).
The Advanced panel's App Data sub-tab: a maintenance action that unregisters the service worker and clears its caches, for recovering a client stuck on a stale cached build. Profile and preferences are kept.
The batch operation dialog: pick one operation to run across the selected files. The options themselves come from the Advanced settings (Whisper / conversion) or the LLM Template (subtitle fix), so this is just a picker. Submitting drops a batch job into the Tasks tray.
The Files tab: the batch hub. An Upload dropdown adds files or a folder (tracked in the Tasks tray); files list in collapsible folder trays, and within each folder every source video nests its derived variants (generated / fixed SRTs, converted MP4) under it; and an Options dropdown runs a batch, deletes, or clears the current selection.
The LLM Template tab. Two sub-tabs: "Word Breakdown" (model + system message + prompt, used when clicking a word) and "Subtitle Fix" (model + system message, used by the player's Fix SRT). One Save persists the whole template.
The Tasks tab: the durable record of the profile's jobs. Lists the full job history (server) overlaid with the live task state, expands a batch job to its per-file children, applies a finished result, and deletes tasks. Files are the source of truth, so deleting a file cascades its jobs away server-side rather than leaving stale rows to detect here.
Groups profile files by lineage: each source video (or standalone file) becomes a head with its derived variants (generated / fixed SRTs, converted MP4) nested under it. Shared by the Files panel and the player's Load Media popover.
A transcribe-chat message: the audio renders as the themed AudioPlayer; a transcript renders as clickable furigana tokens; an LLM explanation renders as markdown — each in an aligned bubble.
This is the entry point of the application. It sets up the React application, including providers and routing. It also includes a fix for handling client-side routing on static hosting services like GitHub Pages.
A reusable Server-Sent Events client over fetch (an EventSource can't POST a JSON body), mirroring apiFetch (profile header, ApiError). Each data: frame is JSON; a named error event aborts with an ApiError, and the stream ends on the done event.
A small provider mark for the LLM provider picker / model chips. Providers with a brand SVG render it as a CSS mask tinted to the surrounding text color (so the monochrome marks stay visible on the dark theme and adopt disabled/active text colors); other providers fall back to a brand-tinted monogram or a generic icon.
Cross-browser recording of a MediaStream from an HTMLVideoElement. Uses native captureStream where available and falls back to a canvas-based approach (iOS Safari). Also picks a supported MIME type.
A selector for the token bundling mode (Words / Grammar / Morphemes), wired to BundleSettingsContext. The choice is a global view preference, so changing it re-tokenizes everywhere.
Shared presentational components for dictionary data (JMdict / JMnedict entries, kanji profiles, examples, morphology), reused by the WordDialog quick lookup and the Dictionary hub at different levels of interactivity: the hub makes examples, kanji, radicals, and cross-references clickable, while the dialog renders them inert.
The shared loading skeleton for a streamed LLM explanation: a heading bar over a few text lines, shown until the first token arrives. Used by the word dialog and the transcribe chat bubble so both present the same placeholder.
Renders a word with accurate furigana as ruby text, from the per-segment segmentation the server extracts (JmdictFurigana). Kana-only segments carry no annotation and render as plain text.
Draws a kanji stroke by stroke from its KanjiVG diagram (fetched as an inline SVG). Each stroke path is revealed by animating its dash offset in stroke order. Honors reduced motion by rendering the finished diagram statically, and falls back to the plain glyph when no stroke data exists.
Renders stitched JapaneseWords as clickable tokens with optional furigana. Shared by the text analyzer, subtitle player, and transcribe chat. Font size + color are inherited from the parent (so the subtitle style settings apply); the interaction accent is the theme's vermilion. No blanket underline — affordance is a subtle hover highlight.
Custom, on-theme video controls (the native ones can't be themed cross-browser). Renders over the bottom of the video: scrub bar, play/pause, volume, time, and playback speed. No fullscreen -> native fullscreen would drop the overlay subtitles. Reads/writes the
A styled, reusable video player: an object-contain video on a black stage with the on-theme VideoControls (auto-hiding while playing). Used on its own (e.g. clip preview) so a plain video matches the player's look instead of the unstyled native controls, and composed by the player feature, which layers its subtitle overlay on top via overlay.
A draggable word lookup: a streamed LLM nuance explanation + a dictionary reference organized into collapsible sections (Entry, Kanji, Examples, Grammar), with an optional "save clip" action when opened from a video and a deep link into the Dictionary hub. Shared by the player, text analyzer, and transcribe pages.
Screen-size hooks. useMediaQuery subscribes to a CSS media query. useIsMobile is the shared "below Tailwind's lg breakpoint" check used to switch between desktop and mobile component layouts.
A static glossary translating the Japanese grammatical terms that UniDic morphology uses (parts of speech, conjugation types and forms, word origins) into short English descriptions, surfaced as tooltips in the grammar breakdown.
Display helpers for jobs: human labels per operation type, the "apply result" action labels, the active-status test, and the status line (including batch progress + outcome summaries). Shared by the task tray and the dashboard Tasks tab.
Types for the async job system: server-side jobs and the client-side uploads that feed them, shared by the task tray and the features that enqueue work.
A hook that applies a finished job's result: loads an SRT or a converted video into the player (and routes there), or sends a transcript to the dashboard. Works for a single-op job and for a batch child (whose type is the single-op type). Shared by the task tray and the Tasks tab.
Platform detection for the few places that must branch on the host OS rather than the viewport. The viewport-based checks live in shared/hooks/useMediaQuery.
Shared responsive breakpoint definitions used by both the Tailwind preset (as named screens) and the runtime media-query hooks, so the CSS variants and the JS checks that mirror them never drift.
Tailwind preset that surfaces the "Sumi & Shu" design tokens (see tokens.css) as named utilities, e.g. bg-bg, text-ink-muted, border-ink/10, bg-shu/20, font-display, rounded-card.
A single-select segmented control: a row of pill options on a sunken track, the active one accented. Distinct from Tabs (section navigation) and Toggle (a single on/off switch).