Skip to content

feat(narration): generate speech with word timing and caption sidecars - #171

Open
DonIsmaelito wants to merge 3 commits into
browser-use:mainfrom
DonIsmaelito:submit/narration
Open

DonIsmaelito wants to merge 3 commits into
browser-use:mainfrom
DonIsmaelito:submit/narration

Conversation

@DonIsmaelito

@DonIsmaelito DonIsmaelito commented Sep 17, 2026

Copy link
Copy Markdown

Why

Creating a narrated video needs both speech audio and timing for its captions. This adds one command that generates them together from a script.

Changes

  • Add ElevenLabs narration with paragraph chunking, validated word timing, WAV and MP3 audio, and SRT captions.
  • Retain provider responses and generation settings for inspection.
  • Reuse only complete outputs with matching hashes and settings. Stage replacements before publishing, and require explicit overwrite for changed outputs.
  • Three focused commits cover generation, output protection and documentation. All 48 branch tests pass, including 32 narration cases with mocked requests and real FFmpeg encoding.

Limits

Requires an ElevenLabs account, an available voice and FFmpeg. Real generation may incur charges; live API generation and listening were not tested. Dry run makes no API calls.

Failed multi-request runs do not resume individual chunks. Cache checks still resolve the voice online, and multi-file publication is not atomic. Provider word timing needs human review and is not an independent transcription.

This PR targets main independently. It complements the narration guidance in #146 and does not replace transcription or the SRT editing pipeline in #41. No renderer changes, voices, media assets or fonts are bundled.

@DonIsmaelito
DonIsmaelito marked this pull request as ready for review September 17, 2026 22:29

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="helpers/narrate.py">

<violation number="1" location="helpers/narrate.py:123">
P2: When an HTML comment touches adjacent words, the parser concatenates those words and changes the narration text and timings. Replace comments with a space before paragraph normalization.</violation>

<violation number="2" location="helpers/narrate.py:222">
P2: When unknown bracketed text is spoken, this conversion removes it from the generated words and SRT even though `parse_script` passes it to ElevenLabs. Strip only known provider markup so captions retain ordinary bracketed text.</violation>

<violation number="3" location="helpers/narrate.py:257">
P2: When a chunk boundary inserts silence without punctuation, `write_srt()` keeps the previous cue open and appends the next chunk's words. Flush cues across timing gaps (or pass chunk boundaries into `write_srt()`) so captions do not span the deliberate inter-paragraph silence.</violation>

<violation number="4" location="helpers/narrate.py:511">
P2: When `.tts_metrics.json` is edited while the other artifacts remain unchanged, `cache_valid()` still reuses the output because it never authenticates the metrics artifact. Validate the metadata with a separate completion hash/signature or compare its settings fields to the current invocation before accepting a cache hit.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread helpers/narrate.py
previous_start, previous_end = start, end
text = "".join(chars)
# blank markup without changing the character positions used by provider timestamps
text = re.sub(r"<[^>]*>|\[[^\]]*\]", lambda match: " " * len(match.group()), text)

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When unknown bracketed text is spoken, this conversion removes it from the generated words and SRT even though parse_script passes it to ElevenLabs. Strip only known provider markup so captions retain ordinary bracketed text.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/narrate.py, line 222:

<comment>When unknown bracketed text is spoken, this conversion removes it from the generated words and SRT even though `parse_script` passes it to ElevenLabs. Strip only known provider markup so captions retain ordinary bracketed text.</comment>

<file context>
@@ -0,0 +1,722 @@
+        previous_start, previous_end = start, end
+    text = "".join(chars)
+    # blank markup without changing the character positions used by provider timestamps
+    text = re.sub(r"<[^>]*>|\[[^\]]*\]", lambda match: " " * len(match.group()), text)
+    words = []
+    for match in re.finditer(r"\S+", text):
</file context>
Fix with cubic

Comment thread helpers/narrate.py
Non v3 pause markers become SSML breaks. V3 keeps untimed pause tags and
rejects timed pauses. Headings and HTML comments are not spoken.
"""
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When an HTML comment touches adjacent words, the parser concatenates those words and changes the narration text and timings. Replace comments with a space before paragraph normalization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/narrate.py, line 123:

<comment>When an HTML comment touches adjacent words, the parser concatenates those words and changes the narration text and timings. Replace comments with a space before paragraph normalization.</comment>

<file context>
@@ -0,0 +1,722 @@
+    Non v3 pause markers become SSML breaks. V3 keeps untimed pause tags and
+    rejects timed pauses. Headings and HTML comments are not spoken.
+    """
+    text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
+    paragraphs: list[dict[str, Any]] = []
+    for raw in re.split(r"\n\s*\n", text.strip()):
</file context>
Suggested change
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
text = re.sub(r"<!--.*?-->", " ", text, flags=re.S)
Fix with cubic

Comment thread helpers/narrate.py
and not path.is_symlink()
and metrics.get("output_hashes", {}).get(suffix) == file_hash(path)
for suffix, path in paths.items()
if suffix != ".tts_metrics.json"

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When .tts_metrics.json is edited while the other artifacts remain unchanged, cache_valid() still reuses the output because it never authenticates the metrics artifact. Validate the metadata with a separate completion hash/signature or compare its settings fields to the current invocation before accepting a cache hit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/narrate.py, line 511:

<comment>When `.tts_metrics.json` is edited while the other artifacts remain unchanged, `cache_valid()` still reuses the output because it never authenticates the metrics artifact. Validate the metadata with a separate completion hash/signature or compare its settings fields to the current invocation before accepting a cache hit.</comment>

<file context>
@@ -0,0 +1,722 @@
+            and not path.is_symlink()
+            and metrics.get("output_hashes", {}).get(suffix) == file_hash(path)
+            for suffix, path in paths.items()
+            if suffix != ".tts_metrics.json"
+        )
+    except (OSError, ValueError, AttributeError, TypeError):
</file context>
Fix with cubic

Comment thread helpers/narrate.py
)
current = []

for word in words:

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a chunk boundary inserts silence without punctuation, write_srt() keeps the previous cue open and appends the next chunk's words. Flush cues across timing gaps (or pass chunk boundaries into write_srt()) so captions do not span the deliberate inter-paragraph silence.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/narrate.py, line 257:

<comment>When a chunk boundary inserts silence without punctuation, `write_srt()` keeps the previous cue open and appends the next chunk's words. Flush cues across timing gaps (or pass chunk boundaries into `write_srt()`) so captions do not span the deliberate inter-paragraph silence.</comment>

<file context>
@@ -0,0 +1,722 @@
+            )
+            current = []
+
+    for word in words:
+        proposed = " ".join(w["text"] for w in [*current, word])
+        if current and (len(current) >= max_words or len(proposed) > max_chars):
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant