mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-29 00:15:32 -07:00
fix(youtube): preserve explicit 3000ms caption durations
- Distinguish generated rolling cues from plain-text cues - Keep explicit sound-cue spans intact
This commit is contained in:
@@ -125,11 +125,11 @@ test('convertYoutubeTimedTextToVtt leaves pop-on captions intact', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('convertYoutubeTimedTextToVtt keeps explicit sound-cue durations in rolling documents', () => {
|
test('convertYoutubeTimedTextToVtt keeps explicit 3000ms sound-cue durations in rolling documents', () => {
|
||||||
const result = convertYoutubeTimedTextToVtt(
|
const result = convertYoutubeTimedTextToVtt(
|
||||||
[
|
[
|
||||||
'<timedtext><body>',
|
'<timedtext><body>',
|
||||||
'<p t="20305" d="2020" w="1">[音楽]</p>',
|
'<p t="20305" d="3000" w="1">[音楽]</p>',
|
||||||
'<p t="26269" w="1" a="1">\n</p>',
|
'<p t="26269" w="1" a="1">\n</p>',
|
||||||
'<p t="26279" d="3000" w="1"><s ac="0">じゃあ、君からお願いします。</s></p>',
|
'<p t="26279" d="3000" w="1"><s ac="0">じゃあ、君からお願いします。</s></p>',
|
||||||
'</body></timedtext>',
|
'</body></timedtext>',
|
||||||
@@ -141,7 +141,7 @@ test('convertYoutubeTimedTextToVtt keeps explicit sound-cue durations in rolling
|
|||||||
[
|
[
|
||||||
'WEBVTT',
|
'WEBVTT',
|
||||||
'',
|
'',
|
||||||
'00:00:20.305 --> 00:00:22.325',
|
'00:00:20.305 --> 00:00:23.305',
|
||||||
'[音楽]',
|
'[音楽]',
|
||||||
'',
|
'',
|
||||||
'00:00:26.279 --> 00:00:29.279',
|
'00:00:26.279 --> 00:00:29.279',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ interface YoutubeTimedTextRow {
|
|||||||
startMs: number;
|
startMs: number;
|
||||||
durationMs: number;
|
durationMs: number;
|
||||||
text: string;
|
text: string;
|
||||||
|
isGenerated: boolean;
|
||||||
rollingWindow: YoutubeRollingWindow | null;
|
rollingWindow: YoutubeRollingWindow | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +153,8 @@ function extractYoutubeTimedTextDocument(xml: string): YoutubeTimedTextDocument
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inner = (match[2] ?? '').replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '');
|
const rawInner = match[2] ?? '';
|
||||||
|
const inner = rawInner.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '');
|
||||||
const text = decodeHtmlEntities(inner).trim();
|
const text = decodeHtmlEntities(inner).trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
continue;
|
continue;
|
||||||
@@ -162,6 +164,7 @@ function extractYoutubeTimedTextDocument(xml: string): YoutubeTimedTextDocument
|
|||||||
startMs,
|
startMs,
|
||||||
durationMs,
|
durationMs,
|
||||||
text,
|
text,
|
||||||
|
isGenerated: /<s\b/.test(rawInner),
|
||||||
rollingWindow: resolveRollingWindow(attrs, windowDefinitions),
|
rollingWindow: resolveRollingWindow(attrs, windowDefinitions),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -179,6 +182,10 @@ function findNextEventStartMs(eventStartsMs: number[], afterMs: number): number
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGeneratedRollingCue(row: YoutubeTimedTextRow, hasRollingWindowEvents: boolean): boolean {
|
||||||
|
return row.isGenerated && (row.rollingWindow !== null || hasRollingWindowEvents);
|
||||||
|
}
|
||||||
|
|
||||||
function formatVttTimestamp(ms: number): string {
|
function formatVttTimestamp(ms: number): string {
|
||||||
const totalMs = Math.max(0, Math.floor(ms));
|
const totalMs = Math.max(0, Math.floor(ms));
|
||||||
const hours = Math.floor(totalMs / 3_600_000);
|
const hours = Math.floor(totalMs / 3_600_000);
|
||||||
@@ -280,10 +287,11 @@ export function convertYoutubeTimedTextToVtt(xml: string): string {
|
|||||||
const row = rows[index]!;
|
const row = rows[index]!;
|
||||||
const nextRow = rows[index + 1];
|
const nextRow = rows[index + 1];
|
||||||
const unclampedEnd = row.startMs + row.durationMs;
|
const unclampedEnd = row.startMs + row.durationMs;
|
||||||
// YouTube uses exactly 3000ms as a placeholder for rolling speech rows.
|
// YouTube uses exactly 3000ms as a placeholder for generated rolling speech.
|
||||||
// Other durations are explicit, including short sound cues such as [音楽].
|
// Plain-text cues can explicitly use the same duration and must keep it.
|
||||||
const nextEventStart =
|
const nextEventStart =
|
||||||
hasRollingWindowEvents && row.durationMs === YOUTUBE_ROLLING_PLACEHOLDER_DURATION_MS
|
isGeneratedRollingCue(row, hasRollingWindowEvents) &&
|
||||||
|
row.durationMs === YOUTUBE_ROLLING_PLACEHOLDER_DURATION_MS
|
||||||
? findNextEventStartMs(eventStartsMs, row.startMs)
|
? findNextEventStartMs(eventStartsMs, row.startMs)
|
||||||
: undefined;
|
: undefined;
|
||||||
const clampedEnd =
|
const clampedEnd =
|
||||||
|
|||||||
Reference in New Issue
Block a user