From 3e333763fba6ae8b5a649bc4153eff3b2365486e Mon Sep 17 00:00:00 2001 From: javis-bot Date: Thu, 11 Jun 2026 18:41:25 +0900 Subject: [PATCH] fix(stream): enable captions only for real Korean tracks, skip auto-generated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The broadcast subtitle rule treated any ko* track as Korean, so YouTube's auto-generated (자동 생성) Korean track would switch captions on. Match only human-authored tracks (kind !== 'asr', vss_id not 'a.*') and pass the full track object to setOption so YouTube selects the manual track rather than the same-languageCode ASR one. Captions stay off when only an auto track exists. Co-Authored-By: Claude Opus 4.7 --- bot/scripts/stream-test/README.md | 3 +- bot/scripts/stream-test/broadcast-helper.mjs | 32 ++++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/bot/scripts/stream-test/README.md b/bot/scripts/stream-test/README.md index e928b08..afee9f9 100644 --- a/bot/scripts/stream-test/README.md +++ b/bot/scripts/stream-test/README.md @@ -29,7 +29,8 @@ real browsing session captured from the X display. ad" instantly, closes overlay ads, fast-forwards unskippable ads (seek-to-end + 16x + mute) and RESTORES the pre-ad muted/playbackRate when the ad ends; and (2) applies the subtitle rule per video: captions OFF by default, Korean ON - when the video offers a Korean track. Run it alongside the broadcast; it + only when the video offers a real (human-authored) Korean track; YouTube's + auto-generated Korean track is ignored. Run it alongside the broadcast; it reconnects across Chrome restarts. ## Run diff --git a/bot/scripts/stream-test/broadcast-helper.mjs b/bot/scripts/stream-test/broadcast-helper.mjs index 86c52ab..67a58ff 100644 --- a/bot/scripts/stream-test/broadcast-helper.mjs +++ b/bot/scripts/stream-test/broadcast-helper.mjs @@ -5,7 +5,9 @@ // so they clear in ~1s. The pre-ad muted/playbackRate are SAVED and // RESTORED when the ad ends, so the main video is never left muted/fast. // 2. Applies the subtitle rule per video: captions OFF by default, but a -// Korean track is turned ON when the video offers one. Runs once per video. +// real (human-authored) Korean track is turned ON when the video offers +// one. YouTube's auto-generated (자동 생성) Korean track does NOT count and +// is left off. Runs once per video. // Self-contained: no extension, no network/hosts changes. Reconnects across // Chrome restarts. // @@ -44,29 +46,41 @@ const WATCH = `(() => { return adShowing; }; + // A "real" Korean caption track is a human-authored/uploaded Korean track, + // NOT YouTube's auto-speech-recognition (자동 생성) track. ASR tracks report + // kind === 'asr' and a vss_id like 'a.ko'; manual tracks have an empty kind + // and a vss_id like '.ko'. We only ever turn captions on for a real track. + const isRealKo = (t) => !!t && /^ko/i.test(t.languageCode || '') + && t.kind !== 'asr' && !/^a\./.test(t.vss_id || ''); + const capTick = (adShowing) => { if (adShowing) return; // don't touch captions while an ad plays const p = document.getElementById('movie_player'); if (!p || !p.getOption || !p.getVideoData) return; const vid = p.getVideoData().video_id; if (!vid) return; - // Decide the desired state once per video (off, or Korean if offered). + // Decide the desired state once per video: ON only if a real (non-ASR) + // Korean track is offered, otherwise OFF. if (capWant[vid] === undefined) { capTries[vid] = (capTries[vid] || 0) + 1; let tracks = []; try { p.loadModule('captions'); tracks = p.getOption('captions', 'tracklist') || []; } catch {} - if (tracks.length) capWant[vid] = tracks.find((t) => /^ko/i.test(t.languageCode || '')) ? 'ko' : 'off'; + if (tracks.length) capWant[vid] = tracks.some(isRealKo) ? 'ko' : 'off'; else if (capTries[vid] > 16) capWant[vid] = 'off'; // no tracks: keep it off else return; // tracklist not ready yet } - // Enforce it every tick so YouTube cannot silently re-enable captions. - let curLc = ''; - try { const c = p.getOption('captions', 'track'); curLc = (c && c.languageCode) || ''; } catch {} + // Enforce it every tick so YouTube cannot silently re-enable captions or + // swap our manual Korean track for the auto-generated one. + let curTrack = null; + try { curTrack = p.getOption('captions', 'track'); } catch {} + const curLc = (curTrack && curTrack.languageCode) || ''; if (capWant[vid] === 'ko') { - if (!/^ko/i.test(curLc)) { + if (!isRealKo(curTrack)) { // not on the real Korean track (off, or ASR) -> set it let tracks = []; try { tracks = p.getOption('captions', 'tracklist') || []; } catch {} - const ko = tracks.find((t) => /^ko/i.test(t.languageCode || '')); - if (ko) { try { p.setOption('captions', 'track', { languageCode: ko.languageCode }); } catch {} } + const ko = tracks.find(isRealKo); + // Pass the full track object so YouTube selects this exact manual track + // and not the same-languageCode auto-generated one. + if (ko) { try { p.setOption('captions', 'track', ko); } catch {} } } } else if (curLc) { // want off but a track is on -> turn it off try { p.setOption('captions', 'track', {}); } catch {}