feat(subtitles): generate Jellyfin subtitles and widen halfwidth kana

- Whisper subtitle generation now works for Jellyfin playback. It reads the item's original file through toJellyfinOriginalStreamUrl and writes to a reused cache folder. For transcoded playback it uses Jellyfin's audio index
- generateJapaneseSubtitles accepts http(s) streams when an output path is set; subprocess errors redact URL tokens
- Add widenHalfwidthKana/narrowFullwidthKana and widen subtitle text at ingest (mpv sub-text, parsed cues, live primary text), so halfwidth and fullwidth duplicates merge into one cue
- Stats queries widen older stored lines, sentence search matches katakana in either width, and stats mining widens sentence/word
- Update Jellyfin, subtitle generation, and architecture docs; add changelog fragments
This commit is contained in:
sudacode committed 2026-10-13 22:30:50 -07:00
commit f08d9d1d53
30 files changed
+580 -51

No files matched your search

@@ -1,23 +1,12 @@
import { MergedToken, PartOfSpeech } from '../../../types';
import { widenHalfwidthKana } from '../../utils/halfwidth-kana';
const KATAKANA_TO_HIRAGANA_OFFSET = 0x60;
const KATAKANA_CODEPOINT_START = 0x30a1;
const KATAKANA_CODEPOINT_END = 0x30f6;
// No `u` flag: the range is entirely BMP so it changes nothing here, and
// Bun's unicode-mode matcher mis-handles this class next to certain ligatures.
const HALFWIDTH_KANA_RUN = /[\uff66-\uff9f]+/g;
// NFKC over the halfwidth kana only, never the whole string: it composes the
// voiced pairs (カ + ゙) into single characters so ガク compares equal to ガク
// instead of counting one character longer than the word it spells, but run
// over everything it would also rewrite unrelated text (① → 1, ㍑ → リットル).
function composeHalfwidthKana(text: string): string {
return text.replace(HALFWIDTH_KANA_RUN, (run) => run.normalize('NFKC'));
}
export function normalizeKana(text: string): string {
const raw = composeHalfwidthKana(text).trim();
const raw = widenHalfwidthKana(text).trim();
if (!raw) {
return '';
}