Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/app/video/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ class RenderOptions(BaseModel):

title_card: bool = True
chapter_screens: bool = False
# Speak each heading aloud. Off, headings are still shown (chapter screens,
# the on-video chapter name) and marked, just not narrated.
read_chapters: bool = True
embed_chapters: bool = True
thumbnail: bool = True
subtitles: SubtitleMode = "sidecar"
Expand Down
15 changes: 14 additions & 1 deletion backend/app/video/segmenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,17 @@ def flush(next_shot: Optional[Shot]) -> None:
result.shots.append(pending_card)
pending_card = None

# A heading with nothing spoken under it (read_chapters off) has no shot
# to hang its mark on; hand it to the shot that starts here instead.
carried_chapter = None
if (pending_chapter and open_shot is None
and next_shot is not None and next_shot.kind != "card"):
carried_chapter = pending_chapter
pending_text = []
pending_chapter = None
open_shot = next_shot
if carried_chapter and open_shot is not None and not open_shot.chapter:
open_shot.chapter = carried_chapter

for block in _flatten(blocks):
kind = _classify(block, options)
Expand Down Expand Up @@ -408,7 +416,8 @@ def flush(next_shot: Optional[Shot]) -> None:
# the words were no longer on screen.
pending_card = Shot(
kind="card", card_title=heading, chapter=heading,
card_kind="chapter", narration=_as_sentence(heading),
card_kind="chapter",
narration=_as_sentence(heading) if options.read_chapters else "",
label="chapter card",
)
# The card carries the chapter mark, so the section after
Expand Down Expand Up @@ -484,6 +493,10 @@ def flush(next_shot: Optional[Shot]) -> None:
label="after code"))
continue

if btype == "heading" and not options.read_chapters:
# The chapter mark was taken above; the words are just not read.
continue

text = _block_narration(block, options)
if text.strip():
spoken = _as_sentence(text)
Expand Down
28 changes: 28 additions & 0 deletions backend/tests/test_video_segmenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,34 @@ def test_headings_become_chapter_marks_and_are_still_narrated():
assert plan.shots[0].narration == "Chapter One."


def test_read_chapters_off_keeps_the_mark_but_drops_the_narration():
root = _media("a.png")
plan = _run([
{"id": "1", "type": "heading", "props": {"level": 1}, "content": _text("Chapter One")},
{"id": "2", "type": "image", "props": {"url": "/media/u1/a.png"}},
{"id": "3", "type": "paragraph", "content": _text("Body.")},
], media_root=root, options=RenderOptions(title_card=False, read_chapters=False))

assert plan.shots[0].chapter == "Chapter One"
assert "Chapter One" not in plan.shots[0].narration


def test_read_chapters_off_leaves_chapter_screens_silent_but_present():
root = _media("a.png")
plan = _run([
{"id": "1", "type": "image", "props": {"url": "/media/u1/a.png"}},
{"id": "2", "type": "paragraph", "content": _text("First.")},
{"id": "3", "type": "heading", "props": {"level": 2}, "content": _text("Next Up")},
{"id": "4", "type": "paragraph", "content": _text("Second.")},
], media_root=root,
options=RenderOptions(title_card=False, chapter_screens=True, read_chapters=False))

cards = [s for s in plan.shots if s.kind == "card"]
assert [c.card_title for c in cards] == ["Next Up"]
assert cards[0].narration == ""
assert cards[0].chapter == "Next Up"


def _with_chapter_screens():
return RenderOptions(title_card=False, chapter_screens=True)

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/videoGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export interface RenderOptions {
insert_into_note: boolean
title_card: boolean
chapter_screens: boolean
read_chapters: boolean
embed_chapters: boolean
thumbnail: boolean
subtitles: SubtitleMode
Expand Down Expand Up @@ -178,6 +179,7 @@ export const DEFAULT_RENDER_OPTIONS: RenderOptions = {
insert_into_note: true,
title_card: true,
chapter_screens: false,
read_chapters: true,
embed_chapters: true,
thumbnail: true,
subtitles: 'sidecar',
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/VideoGenModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export default function VideoGenModal({ noteId, noteTitle, diagramImages, onGene
return () => { cancelled = true; clearTimeout(timer) }
}, [
noteId, payload.speed, payload.title_card, payload.chapter_screens,
payload.read_chapters,
payload.narrate_code, payload.min_shot_seconds, payload.card_seconds,
payload.quotes.enabled,
payload.shot_end_pause_ms,
Expand Down Expand Up @@ -677,6 +678,11 @@ export default function VideoGenModal({ noteId, noteTitle, diagramImages, onGene
onChange={(e) => patch({ chapter_screens: e.target.checked })} />
Chapter screens
</label>
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input type="checkbox" checked={options.read_chapters}
onChange={(e) => patch({ read_chapters: e.target.checked })} />
Read chapters aloud
</label>
<label className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input type="checkbox" checked={options.embed_chapters}
onChange={(e) => patch({ embed_chapters: e.target.checked })} />
Expand Down
Loading