b3385ca3f8
Manson asked whether the training videos could be in other languages. A real dub means re-rendering every video per language: translated Romance-language speech runs 15-25% longer than English and these are slide videos with fixed beat timings, so swapped audio drifts off what is on screen. That turns 15 files into 75 and makes every future lesson edit a five-way job. Captions keep ONE video and add small text tracks beside it, so editing a lesson re-captions that lesson only. tools/captions.mjs: ffmpeg pulls the audio, ElevenLabs Scribe transcribes with word timings (the same STT we already use to verify voiceovers), words are grouped into SENTENCES, and only then translated. Translating cue-by-cue is why most auto-captions read badly at cue boundaries. Translation goes through our own /api/public/translate, so every phrase caches forever in translations.json and costs once across the site. The STT response is cached on disk because it costs money; never pay for it twice. Language choice is evidence, not instinct. The translation cache shows real member demand: Italian and French far ahead, then Spanish, then GERMAN - which beats Portuguese by more than double, the opposite of what we assumed. Proof of concept is Italian on the 5-minute overview. Captions are deliberately NOT on by default. An English reader does not want them forced over the picture; someone who already switched the site to Italian almost certainly does. public/vtt-lang.js shows the track matching their 🌐 choice and leaves the player's CC button to do the rest. Two bugs this caught in my own code, both found by reading the output: - the line wrapper truncated each cue to two lines and SILENTLY DELETED the overflow, so "the whole plan fits in one sentence" shipped as "fits in" then "sentence". It now chunks by the real wrapped line count and never drops a word. - a one or two word tail ("sentence." alone on screen) folds back into the previous cue. The suite asserts all 780 transcript words survive into the English track. Also: .vtt had no Content-Type mapping, so it served as octet-stream and browsers silently ignore such a track. qa/captions-e2e.mjs (12 assertions) reads the parsed cues back out of the player rather than trusting the markup, which is the only way to catch that class of failure. profiles-unit 28 and gate-e2e 33 still green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
34 lines
1.6 KiB
JavaScript
34 lines
1.6 KiB
JavaScript
// Match video captions to the language the member already chose with the 🌐 button.
|
|
//
|
|
// Deliberately NOT on by default for English. Someone reading English does not want
|
|
// captions forced over the picture; someone who switched the site to Italian almost
|
|
// certainly does. So: if their chosen language has a track, show it. Otherwise show
|
|
// nothing and leave the player's own CC button to do its job.
|
|
//
|
|
// Marty + Manson, 2026-09-17. See tools/captions.mjs for how the tracks are built.
|
|
(function () {
|
|
function pick() {
|
|
var lang = '';
|
|
try { lang = String(localStorage.getItem('rmc.lang') || '').toLowerCase(); } catch (e) {}
|
|
var vids = document.querySelectorAll('video');
|
|
for (var i = 0; i < vids.length; i++) {
|
|
var tt = vids[i].textTracks;
|
|
if (!tt || !tt.length) continue;
|
|
var wanted = null;
|
|
for (var j = 0; j < tt.length; j++) {
|
|
if (tt[j].kind !== 'captions' && tt[j].kind !== 'subtitles') continue;
|
|
// never leave a stale track showing when the member switches language
|
|
tt[j].mode = 'disabled';
|
|
if (lang && lang !== 'en' && String(tt[j].language || '').toLowerCase() === lang) wanted = tt[j];
|
|
}
|
|
if (wanted) wanted.mode = 'showing';
|
|
}
|
|
}
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', pick);
|
|
else pick();
|
|
// translate.js writes rmc.lang when the member picks a language; re-run so the
|
|
// captions follow without a reload, and cover players rendered later.
|
|
window.addEventListener('storage', function (e) { if (!e || e.key === 'rmc.lang') pick(); });
|
|
window.RMCCaptions = { refresh: pick };
|
|
})();
|