fix(subtitle_gen): stop millisecond rounding from overflowing timestamps

_ts_srt/_ts_vtt computed the seconds and millisecond fields independently:
`ms = int(round((seconds % 1) * 1000))`. When the fractional part is >= 0.9995
that rounds to 1000, emitting a malformed 4-digit `…,1000` value with no carry
into the seconds field (and, at 59.9999/3599.9999, no carry into minutes/hours).
For example 0.9999s became `00:00:00,1000` instead of `00:00:01,000`. ASR word
and segment end-times routinely land on such fractional boundaries, and the
resulting cue is rejected or mistimed by strict SRT/VTT parsers (ffmpeg
subtitles filter, VLC, browser WebVTT).

Decompose from a single rounded total-milliseconds value so the carry
propagates across all fields. Both formatters now share one `_hmsms` helper.
This commit is contained in:
0xDevNinja
2026-07-07 15:29:03 +05:30
parent de348f15e3
commit bcd8eb6e53
3 changed files with 68 additions and 11 deletions

View File

@@ -309,19 +309,27 @@ class SubtitleGen(BaseTool):
return "\n".join(lines)
@staticmethod
def _ts_srt(seconds: float) -> str:
def _hmsms(seconds: float) -> tuple[int, int, int, int]:
"""Decompose seconds into (h, m, s, ms), rounding to whole ms first.
Rounding to total milliseconds before splitting the fields lets the
carry propagate: 0.9995s+ must become the next second (…,000), not a
malformed 4-digit …,1000 with the seconds field left unincremented.
"""
total_ms = int(round(max(0.0, seconds) * 1000))
h, rem = divmod(total_ms, 3_600_000)
m, rem = divmod(rem, 60_000)
s, ms = divmod(rem, 1_000)
return h, m, s, ms
@classmethod
def _ts_srt(cls, seconds: float) -> str:
"""Format seconds as SRT timestamp: HH:MM:SS,mmm"""
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
ms = int(round((seconds % 1) * 1000))
h, m, s, ms = cls._hmsms(seconds)
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
@staticmethod
def _ts_vtt(seconds: float) -> str:
@classmethod
def _ts_vtt(cls, seconds: float) -> str:
"""Format seconds as VTT timestamp: HH:MM:SS.mmm"""
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
ms = int(round((seconds % 1) * 1000))
h, m, s, ms = cls._hmsms(seconds)
return f"{h:02d}:{m:02d}:{s:02d}.{ms:03d}"