<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Junjun Wang]]></title><description><![CDATA[Junjun Wang]]></description><link>https://junjunwang.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 21 Oct 2026 23:22:00 GMT</lastBuildDate><atom:link href="https://junjunwang.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Designing Sentence-Level Navigation for a Subtitle-Based Language Learning Player]]></title><description><![CDATA[When you build a video player for language learners, the timeline scrubber quickly turns out to be the wrong tool. Learners do not think in seconds; they…]]></description><link>https://junjunwang.hashnode.dev/designing-sentence-level-navigation-for-a-subtitle-based-language-learning-player</link><guid isPermaLink="true">https://junjunwang.hashnode.dev/designing-sentence-level-navigation-for-a-subtitle-based-language-learning-player</guid><category><![CDATA[webdev]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[UX]]></category><category><![CDATA[Language Learning]]></category><dc:creator><![CDATA[王俊俊]]></dc:creator><pubDate>Mon, 19 Oct 2026 02:24:03 GMT</pubDate><content:encoded><![CDATA[<p>When you build a video player for language learners, the timeline scrubber quickly turns out to be the wrong tool. Learners do not think in seconds; they think in sentences. "Play that line again" is the most common action, and a seek bar makes it surprisingly hard. Here are the design decisions that mattered most when we built sentence-level navigation into a subtitle-driven player.</p>
<h2>1. Treat subtitle cues as the primary index</h2>
<p>Every subtitle cue (start, end, text) becomes a navigable unit. Instead of <code>currentTime</code>, the UI state revolves around <code>currentCueIndex</code>. Keyboard shortcuts map to cue operations:</p>
<ul>
<li><p>previous / next sentence</p>
</li>
<li><p>replay current sentence</p>
</li>
<li><p>loop current sentence N times</p>
</li>
<li><p>pause automatically at the end of each sentence ("auto-pause" mode)</p>
</li>
</ul>
<p>Finding the active cue on <code>timeupdate</code> is a binary search over sorted start times, which stays cheap even for long transcripts.</p>
<h2>2. Fix the cue boundaries before you trust them</h2>
<p>Speech-to-text output and many downloaded subtitle files do not align neatly to sentences. Cues often split mid-sentence or merge two speakers. A few practical rules:</p>
<ul>
<li><p>Merge cues shorter than ~0.8 s into a neighbour.</p>
</li>
<li><p>Split cues at sentence-final punctuation when a cue is longer than ~7 s.</p>
</li>
<li><p>Pad the start by 100â€“200 ms when looping, because ASR timestamps tend to clip the first consonant.</p>
</li>
</ul>
<h2>3. Bilingual subtitles need a stable pairing</h2>
<p>If you show a translation under the original line, pair translations to cue IDs, not to timestamps. Re-timing the original (step 2) should never desync the translation.</p>
<h2>4. Word-level interaction</h2>
<p>Click-to-look-up means tokenising each cue. For space-separated languages a regex tokenizer is fine; for Chinese and Japanese you need a segmenter. Store the cue ID alongside any saved word so a flashcard can replay the exact sentence clip later.</p>
<h2>5. Keep media handling simple</h2>
<p>Long videos and podcasts are where learners spend most time. Process audio in chunks for transcription, stream playback normally, and persist progress and saved words locally so a refresh never loses a session.</p>
<h2>Where this ended up</h2>
<p>These ideas power <a href="https://langscribe.online/en/ai-subtitles-language-learning/">LangScribe's AI subtitles for language learning</a>: it generates subtitles in about 99 languages, translates them into bilingual subtitles, and builds the whole player around sentence-by-sentence study, click-to-look-up words, and vocabulary cards with the original clip.</p>
<p>If you are building something similar, start with the cue index. Once "sentence" is a first-class concept in your state, almost every learner-facing feature becomes easier to build.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Cue-Centric Video Player in Vue 3 (HLS + Local Files)]]></title><description><![CDATA[I built a study player where the primary navigation is not the scrub bar—it is the subtitle cue list. Stack notes: Vue 3 + Vite; hls.js for HLS; native…]]></description><link>https://junjunwang.hashnode.dev/building-a-cue-centric-video-player-in-vue-3-hls-local-files</link><guid isPermaLink="true">https://junjunwang.hashnode.dev/building-a-cue-centric-video-player-in-vue-3-hls-local-files</guid><category><![CDATA[vue]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[王俊俊]]></dc:creator><pubDate>Sat, 17 Oct 2026 14:32:42 GMT</pubDate><content:encoded><![CDATA[<p>I built a study player where the primary navigation is not the scrub bar—it is the subtitle cue list.</p>
<p>Stack notes: Vue 3 + Vite; hls.js for HLS; native media for MP4/WebM and local File; Pinia; Dexie/IndexedDB for signed-out libraries. Cloudflare Workers (Hono) serve assets and API.</p>
<p>Cue list as the timeline: jump previous/next cue, replay current, loop current, A-B loop. Bilingual text is two strings on the active cue. Clicking a cue seeks; seeking highlights the row. Hard parts are HLS quirks, VFR, and uneven ASS timing.</p>
<p>Subtitles from three sources: import SRT/VTT/ASS; AI transcription audio chunks to Worker to timed cues; translation tracks. Word lookup is a separate cached call so it does not block playback.</p>
<p>Shipping lesson: if cue interactions feel slow, users never care about STT cleverness. Optimize the loop first.</p>
<p>I use this shape in <a href="https://langscribe.online/en/local-video-language-learning/">LangScribe</a> for local-file language study; play the film locally; optional AI on audio and text only.</p>
]]></content:encoded></item></channel></rss>