feat(watch): refonte pro page lecture + dedup transcript
- Supprime le hint debug 'touche s' (raccourci conserve, tooltip discret) - server/transcript.mjs: dedupeTranscriptLines collapse les auto-captions a fenetre glissante (12 lignes -> 3 sur l'exemple signale), applique dans parseJson3/parseVtt/parseXmlCaptions + filet avant cache API - watch: layout pro (grille 1fr/360px, carte chaine+actions, labels FR, panneaus description/download, sidebar A suivre sticky, a11y/focus) - transcript interactif: recherche, clic timestamp -> seek, suivi auto + surlignage ligne active, copie, retry - tests: 4 nouveaux cas dedupe (23/23 pass), build prod OK
This commit is contained in:
@@ -13,7 +13,10 @@ import {
|
||||
parseVtt,
|
||||
parseTrackText,
|
||||
parseXmlCaptions,
|
||||
dedupeTranscriptLines,
|
||||
orderedTracks,
|
||||
translatedFallbacks,
|
||||
firstPerLanguage,
|
||||
looksLikeHtmlError,
|
||||
capLines,
|
||||
normalizeTranscriptProvider,
|
||||
@@ -151,6 +154,84 @@ describe('orderedTracks + parseTrackText + XML captions', () => {
|
||||
const v = parseTrackText('WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nHello\n', 'vtt');
|
||||
assert.equal(v.length, 1);
|
||||
});
|
||||
it('translatedFallbacks appends &tlang= candidates from other languages only', () => { const cands = [
|
||||
{ track: { url: 'https://x/t?lang=en-US', ext: 'json3' }, lang: 'en-us' },
|
||||
{ track: { url: 'https://x/t?lang=fr', ext: 'json3' }, lang: 'fr' },
|
||||
];
|
||||
const t = translatedFallbacks(cands, 'en');
|
||||
assert.equal(t.length, 1);
|
||||
assert.equal(t[0].lang, 'en');
|
||||
assert.match(t[0].track.url, /tlang=en/);
|
||||
// Same primary language sources are skipped, not re-translated.
|
||||
assert.equal(translatedFallbacks(cands, 'fr').length, 1);
|
||||
assert.equal(translatedFallbacks(cands, '').length, 0);
|
||||
});
|
||||
it('firstPerLanguage keeps one candidate per language in order, bounded', () => {
|
||||
const cands = [
|
||||
{ track: { url: 'https://x/a' }, lang: 'en-US' },
|
||||
{ track: { url: 'https://x/b' }, lang: 'en' },
|
||||
{ track: { url: 'https://x/c' }, lang: 'fr' },
|
||||
{ track: { url: 'https://x/d' }, lang: null },
|
||||
];
|
||||
const out = firstPerLanguage(cands, 10).map((c) => c.track.url);
|
||||
assert.deepEqual(out, ['https://x/a', 'https://x/c', 'https://x/d']);
|
||||
assert.equal(firstPerLanguage(cands, 2).length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dedupeTranscriptLines (rolling-window auto-captions)', () => {
|
||||
it('collapses prefix extensions and exact duplicates', () => {
|
||||
const lines = dedupeTranscriptLines([
|
||||
{ t: 0, dur: 1, text: 'Tac.' },
|
||||
{ t: 1, dur: 1, text: 'Tac.' },
|
||||
{ t: 1, dur: 2, text: 'Tac. David Prou est avec nous. David,' },
|
||||
{ t: 2, dur: 1, text: 'David Prou est avec nous. David,' },
|
||||
{ t: 2, dur: 2, text: 'David Prou est avec nous. David, bonjour.' },
|
||||
{ t: 3, dur: 1, text: 'bonjour.' },
|
||||
{ t: 3, dur: 2, text: 'bonjour. Bonjour Benois.' },
|
||||
]);
|
||||
assert.ok(lines.length < 7, `expected fewer lines, got ${lines.length}`);
|
||||
assert.ok(lines.length >= 1);
|
||||
const joined = lines.map((l) => l.text).join(' ');
|
||||
assert.match(joined, /David Prou/);
|
||||
assert.match(joined, /Benois/);
|
||||
// No word lost, no exact consecutive duplicates remain.
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
assert.notEqual(lines[i].text.trim().toLowerCase(), lines[i - 1].text.trim().toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
it('merges sliding partial overlaps without fragments', () => {
|
||||
const lines = dedupeTranscriptLines([
|
||||
{ t: 6, dur: 2, text: "Alors ce roman québécois c'était ça ou" },
|
||||
{ t: 6, dur: 2, text: "Alors ce roman québécois c'était ça ou mourir ?" },
|
||||
{ t: 7, dur: 1, text: 'mourir ?' },
|
||||
]);
|
||||
assert.equal(lines.length, 1);
|
||||
assert.match(lines[0].text, /mourir/);
|
||||
});
|
||||
|
||||
it('parseJson3 dedupes rolling events end-to-end', () => {
|
||||
const lines = parseJson3({
|
||||
events: [
|
||||
{ tStartMs: 0, dDurationMs: 1000, segs: [{ utf8: 'Tac.' }] },
|
||||
{ tStartMs: 1000, dDurationMs: 1000, segs: [{ utf8: 'Tac.' }] },
|
||||
{ tStartMs: 1000, dDurationMs: 2000, segs: [{ utf8: 'Tac. David Prou est avec nous.' }] },
|
||||
{ tStartMs: 2000, dDurationMs: 1000, segs: [{ utf8: 'David Prou est avec nous.' }] },
|
||||
],
|
||||
});
|
||||
assert.equal(lines.length, 1);
|
||||
assert.match(lines[0].text, /David Prou/);
|
||||
});
|
||||
|
||||
it('keeps distinct sentences on separate cues', () => {
|
||||
const lines = dedupeTranscriptLines([
|
||||
{ t: 0, dur: 2, text: 'Bonjour à tous.' },
|
||||
{ t: 10, dur: 2, text: 'On parle intelligence artificielle.' },
|
||||
{ t: 20, dur: 2, text: 'Merci beaucoup.' },
|
||||
]);
|
||||
assert.equal(lines.length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('capLines + normalizeTranscriptProvider', () => { it('truncates very long transcripts', () => {
|
||||
|
||||
Reference in New Issue
Block a user