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
22 changes: 20 additions & 2 deletions src/xbrain/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,27 @@ def _video_digest_lines(source: ContentSourceSuccess, strings: Strings) -> list[
return [f"> {strings.silent_video}", ""]
heading = source.title or source.url
lines = [f"## {strings.video_digest_header}: {heading}", ""]
if not source.digest:
# Fallback (no long-form digest yet): the raw transcript + frame embeds
# render inline, exactly as before — so this render change is safe to ship
# before any digest exists (an empty `digest` is the default).
if has_text:
lines += [source.text, ""]
lines += _slide_embed_lines(source.frames)
return lines
# With a digest, it is the readable headline of the section; the raw transcript
# + frame slides are demoted into a collapsible `<details>` below — the evidence
# stays in the note without the 40-frame wall of noise up top. Blank lines around
# the inner content let Obsidian render the markdown/embeds inside the HTML block.
lines += [source.digest, ""]
evidence: list[str] = []
if has_text:
lines += [source.text, ""]
lines += _slide_embed_lines(source.frames)
evidence += [source.text, ""]
evidence += _slide_embed_lines(source.frames)
if evidence:
lines += ["<details>", f"<summary>{strings.video_evidence_header}</summary>", ""]
lines += evidence
lines += ["</details>", ""]
return lines


Expand Down
3 changes: 3 additions & 0 deletions src/xbrain/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Strings:
also_relevant: str # "También relevante" / "Also relevant"
video_digest_header: str # "Resumen del vídeo" / "Video digest" (#44)
silent_video: str # the one-line no-speech marker (#44)
video_evidence_header: str # collapsible label for the raw frames + transcript


_STRINGS: dict[str, Strings] = {
Expand All @@ -43,6 +44,7 @@ class Strings:
also_relevant="Also relevant",
video_digest_header="Video digest",
silent_video="🔇 Silent video (no speech detected).",
video_evidence_header="Frames + transcript",
),
"Spanish": Strings(
topics_label="Temas",
Expand All @@ -52,6 +54,7 @@ class Strings:
also_relevant="También relevante",
video_digest_header="Resumen del vídeo",
silent_video="🔇 Vídeo sin voz (sin transcripción).",
video_evidence_header="Frames y transcripción",
),
}

Expand Down
7 changes: 7 additions & 0 deletions src/xbrain/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,13 @@ class ContentSourceSuccess(BaseModel):
# one-time backward-compatible additive churn as `has_speech`/`language`. Each
# `VideoFrame` embeds into the note like a downloaded photo (see `VideoFrame`).
frames: list[VideoFrame] = Field(default_factory=list)
# Long-form readable digest of an `x_video` source — a "what it is · key
# points · why it matters" synthesis of the transcript (`text`) + `frames`,
# written by `xbrain video-digest`. Optional + additive (defaults to `""`), so
# every EXISTING record LOADS unchanged — a pre-digest `x_video` source (and
# every article source) simply carries an empty digest, and `generate` falls
# back to rendering the raw transcript + frames. `""` = "no digest yet".
digest: str = ""
# Ordered body blocks (text + inline images) for `kind="x_article"` sources
# captured as a structured Article (#39). Optional + additive (defaults to
# `[]`), so every EXISTING record LOADS unchanged — a pre-#39 `x_article`
Expand Down
56 changes: 56 additions & 0 deletions tests/test_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ def _video_item(
has_speech: bool = True,
title: str | None = "The Talk",
frames: list | None = None,
digest: str = "",
) -> Item:
"""An enriched video bookmark carrying an `x_video` transcript source (#44)."""
return Item(
Expand Down Expand Up @@ -603,6 +604,7 @@ def _video_item(
text=text,
has_speech=has_speech,
frames=frames or [],
digest=digest,
)
],
),
Expand Down Expand Up @@ -653,6 +655,60 @@ def test_generate_video_digest_spanish_header(tmp_path: Path):
assert "Cuerpo de la transcripción." in note


def test_generate_renders_video_digest_prose_when_present(tmp_path: Path):
"""When the `x_video` source carries a long-form `digest`, it renders as the
body of the `## Video digest` section — the readable headline of the video."""
generate(
{"7": _video_item(text="raw transcript body", digest="This talk explains scaling laws.")},
tmp_path,
)
note = next((tmp_path / "items").glob("*.md")).read_text(encoding="utf-8")
assert "## Video digest: The Talk" in note
assert "This talk explains scaling laws." in note


def test_generate_video_digest_demotes_raw_to_details_when_digest_present(tmp_path: Path):
"""With a digest, the raw transcript + frame embeds are demoted into a collapsible
`<details>` below the digest prose — evidence kept, noise hidden. The digest prose
sits ABOVE the `<details>`, the transcript + embeds INSIDE it."""
generate(
{
"7": _video_item(
text="raw transcript body", frames=_frames(), digest="A concise digest."
)
},
tmp_path,
)
note = next((tmp_path / "items").glob("*.md")).read_text(encoding="utf-8")
assert "<details>" in note and "</details>" in note
digest_idx = note.index("A concise digest.")
details_idx = note.index("<details>")
assert digest_idx < details_idx # digest prose is the headline, above the raw dump
inside = note[details_idx : note.index("</details>")]
assert "raw transcript body" in inside
assert "![[_media/7/frames/0.png]]" in inside


def test_generate_video_digest_no_details_when_digest_absent(tmp_path: Path):
"""Fallback: with no digest, the section is unchanged — transcript rendered inline,
no `<details>` wrapper. Safe to ship the render change before any digest exists."""
generate({"7": _video_item(text="inline transcript body")}, tmp_path)
note = next((tmp_path / "items").glob("*.md")).read_text(encoding="utf-8")
assert "inline transcript body" in note
assert "<details>" not in note


def test_generate_video_digest_evidence_label_is_localised(tmp_path: Path):
"""The `<details>` summary label follows the configured output language."""
generate(
{"7": _video_item(text="cuerpo", digest="Un resumen conciso.")},
tmp_path,
output_language="Spanish",
)
note = next((tmp_path / "items").glob("*.md")).read_text(encoding="utf-8")
assert "transcripción" in note.lower() # the ES evidence label


# ----------------------------------------------------------- x_video slide frames (#44 PR4)


Expand Down
1 change: 1 addition & 0 deletions tests/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def test_strings_dataclass_has_exactly_the_expected_fields() -> None:
"also_relevant",
"video_digest_header",
"silent_video",
"video_evidence_header",
}


Expand Down
Loading