diff --git a/docs/launch/demo_video.md b/docs/launch/demo_video.md index f2f99f1..51a34d2 100644 --- a/docs/launch/demo_video.md +++ b/docs/launch/demo_video.md @@ -22,12 +22,27 @@ Recording command: uv run python examples/launch_demo.py ``` +Generated assets: + +- [synaptic-memory-demo.mp4](synaptic-memory-demo.mp4) +- [synaptic-memory-demo.gif](synaptic-memory-demo.gif) + +Preview: + +![Synaptic Memory demo](synaptic-memory-demo.gif) + Fast verification command: ```bash uv run python examples/launch_demo.py --no-pause ``` +Regenerate the MP4/GIF from this server: + +```bash +uv run python docs/launch/render_demo_video.py +``` + Optional recording tools: ```bash diff --git a/docs/launch/promotion_playbook.md b/docs/launch/promotion_playbook.md index d13ab22..7422b6e 100644 --- a/docs/launch/promotion_playbook.md +++ b/docs/launch/promotion_playbook.md @@ -48,12 +48,12 @@ Must-have: quickstart. - GitHub topics and description. - GitHub release notes. -- 60-second demo plan and recording command: `docs/launch/demo_video.md`. +- 60-second generated demo and recording command: `docs/launch/demo_video.md`. - Show HN draft: `docs/launch/show_hn_draft.md`. Nice-to-have: -- Exported terminal GIF or short MP4 from `examples/launch_demo.py`. +- Edited voiceover version of the generated terminal GIF/MP4. - Architecture image showing documents/SQL -> graph -> EvidenceSearch -> MCP agent tools. - Short Korean post for GeekNews / LinkedIn. diff --git a/docs/launch/render_demo_video.py b/docs/launch/render_demo_video.py new file mode 100644 index 0000000..d60f023 --- /dev/null +++ b/docs/launch/render_demo_video.py @@ -0,0 +1,255 @@ +"""Render the launch terminal demo to MP4/GIF using ffmpeg. + +This avoids external terminal-recording tools. It runs +``examples/launch_demo.py --no-pause``, turns the output into a terminal-style +ASS subtitle track, and renders: + +- ``docs/launch/synaptic-memory-demo.mp4`` +- ``docs/launch/synaptic-memory-demo.gif`` + +Requires ``ffmpeg`` with ``ass``, ``drawtext``, ``palettegen``, and +``paletteuse`` filters. +""" + +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +LAUNCH_DIR = ROOT / "docs" / "launch" +MP4_PATH = LAUNCH_DIR / "synaptic-memory-demo.mp4" +GIF_PATH = LAUNCH_DIR / "synaptic-memory-demo.gif" +ASS_PATH = LAUNCH_DIR / "synaptic-memory-demo.ass" +PALETTE_PATH = LAUNCH_DIR / "synaptic-memory-demo-palette.png" +FONT_PATH = Path("/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf") + + +SCENES = [ + { + "title": "Synaptic Memory", + "lines": [ + "Graph memory for RAG agents", + "Documents + SQL rows + feedback", + "Default path: no LLM calls at indexing time", + ], + "hold": 1.0, + }, + { + "title": "1. Build a tiny mixed graph", + "lines": [ + "Created graph: 4 nodes", + "Sources: 2 policy docs + 2 support ticket rows", + ], + "hold": 1.2, + }, + { + "title": "2. Search and record retrieval", + "lines": [ + "event_id: ", + "1. ticket:T-1001 score=0.660 source=ticket", + "2. Refund Policy score=0.435 source=policy/refunds.md", + "3. Shipping Policy score=0.210 source=policy/shipping.md", + ], + "hold": 1.2, + }, + { + "title": "3. Feed back that the evidence helped", + "lines": [ + "Recorded explicit positive feedback for the top evidence.", + ], + "hold": 1.2, + }, + { + "title": "4. Inspect memory health metadata", + "lines": [ + "memory_events: 2", + "retrieval_events: 2", + "memory_scores: 2", + "health_signals: 0", + "No raw provenance was appended to Node.content.", + "Swap the backend to PostgreSQL/Kuzu/Qdrant when the corpus grows.", + ], + "hold": 2.0, + }, +] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--mp4-only", action="store_true", help="Skip GIF export.") + args = parser.parse_args() + + ffmpeg = _require("ffmpeg") + uv = _require("uv") + _validate_demo_output(uv) + + duration = _write_ass(ASS_PATH) + _render_mp4(duration, ffmpeg) + if not args.mp4_only: + _render_gif(ffmpeg) + print(f"rendered: {MP4_PATH}") + if not args.mp4_only: + print(f"rendered: {GIF_PATH}") + + +def _require(name: str) -> str: + resolved = shutil.which(name) + if resolved is None: + raise SystemExit(f"missing required command: {name}") + return resolved + + +def _validate_demo_output(uv: str) -> None: + result = subprocess.run( # noqa: S603 - fixed command for local launch rendering. + [uv, "run", "python", "examples/launch_demo.py", "--no-pause"], + cwd=ROOT, + text=True, + capture_output=True, + check=True, + ) + normalized = re.sub(r"event_id: [0-9a-f]+", "event_id: ", result.stdout) + required = [ + "Created graph: 4 nodes", + "ticket:T-1001", + "Refund Policy", + "Recorded explicit positive feedback", + "memory_scores: 2", + ] + missing = [needle for needle in required if needle not in normalized] + if missing: + raise SystemExit(f"demo output missing expected lines: {missing}") + + +def _write_ass(path: Path) -> float: + events: list[str] = [] + t = 0.0 + for scene in SCENES: + visible = [scene["title"]] + for line in scene["lines"]: + start = t + t += 1.0 + visible.append(line) + events.append(_dialogue(start, t, visible)) + end = t + float(scene["hold"]) + events[-1] = _dialogue(t - 1.0, end, visible) + t = end + + content = "\n".join( + [ + "[Script Info]", + "ScriptType: v4.00+", + "PlayResX: 1280", + "PlayResY: 720", + "ScaledBorderAndShadow: yes", + "", + "[V4+ Styles]", + "Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour," + "BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,ScaleY,Spacing,Angle," + "BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,Encoding", + "Style: Terminal,Liberation Mono,30,&H00E5E7EB,&H000000FF,&H00111827," + "&H00111827,0,0,0,0,100,100,0,0,1,0,0,7,80,60,95,1", + "", + "[Events]", + "Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text", + *events, + "", + ] + ) + path.write_text(content, encoding="utf-8") + return t + + +def _dialogue(start: float, end: float, lines: list[str]) -> str: + text = r"\N".join(_escape_ass(line) for line in lines) + return f"Dialogue: 0,{_ts(start)},{_ts(end)},Terminal,,0,0,0,,{text}" + + +def _escape_ass(text: str) -> str: + return text.replace("\\", r"\\").replace("{", r"\{").replace("}", r"\}") + + +def _ts(seconds: float) -> str: + centis = round(seconds * 100) + h, rem = divmod(centis, 360000) + m, rem = divmod(rem, 6000) + s, cs = divmod(rem, 100) + return f"{h}:{m:02d}:{s:02d}.{cs:02d}" + + +def _render_mp4(duration: float, ffmpeg: str) -> None: + font = FONT_PATH + ass = _ffmpeg_path(ASS_PATH) + draw_filter = ( + "drawbox=x=44:y=42:w=1192:h=636:color=0x111827@0.96:t=fill," + "drawbox=x=44:y=42:w=1192:h=636:color=0x334155@1:t=2," + "drawbox=x=44:y=42:w=1192:h=42:color=0x1f2937@1:t=fill," + f"drawtext=fontfile='{font}':text='Synaptic Memory launch demo':" + "x=74:y=53:fontsize=20:fontcolor=0xCBD5E1," + f"ass='{ass}'" + ) + subprocess.run( # noqa: S603 - fixed ffmpeg invocation over repo-local generated inputs. + [ + ffmpeg, + "-y", + "-f", + "lavfi", + "-i", + f"color=c=0x070B12:s=1280x720:r=30:d={duration:.2f}", + "-vf", + draw_filter, + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + str(MP4_PATH), + ], + cwd=ROOT, + check=True, + ) + + +def _render_gif(ffmpeg: str) -> None: + scale = "fps=12,scale=960:-1:flags=lanczos" + subprocess.run( # noqa: S603 - fixed ffmpeg invocation over generated MP4. + [ + ffmpeg, + "-y", + "-i", + str(MP4_PATH), + "-vf", + f"{scale},palettegen", + str(PALETTE_PATH), + ], + cwd=ROOT, + check=True, + ) + subprocess.run( # noqa: S603 - fixed ffmpeg invocation over generated MP4/palette. + [ + ffmpeg, + "-y", + "-i", + str(MP4_PATH), + "-i", + str(PALETTE_PATH), + "-lavfi", + f"{scale} [x]; [x][1:v] paletteuse", + str(GIF_PATH), + ], + cwd=ROOT, + check=True, + ) + PALETTE_PATH.unlink(missing_ok=True) + + +def _ffmpeg_path(path: Path) -> str: + return str(path).replace("\\", "\\\\").replace(":", "\\:") + + +if __name__ == "__main__": + main() diff --git a/docs/launch/synaptic-memory-demo.ass b/docs/launch/synaptic-memory-demo.ass new file mode 100644 index 0000000..4834b56 --- /dev/null +++ b/docs/launch/synaptic-memory-demo.ass @@ -0,0 +1,28 @@ +[Script Info] +ScriptType: v4.00+ +PlayResX: 1280 +PlayResY: 720 +ScaledBorderAndShadow: yes + +[V4+ Styles] +Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,Encoding +Style: Terminal,Liberation Mono,30,&H00E5E7EB,&H000000FF,&H00111827,&H00111827,0,0,0,0,100,100,0,0,1,0,0,7,80,60,95,1 + +[Events] +Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text +Dialogue: 0,0:00:00.00,0:00:01.00,Terminal,,0,0,0,,Synaptic Memory\NGraph memory for RAG agents +Dialogue: 0,0:00:01.00,0:00:02.00,Terminal,,0,0,0,,Synaptic Memory\NGraph memory for RAG agents\NDocuments + SQL rows + feedback +Dialogue: 0,0:00:02.00,0:00:04.00,Terminal,,0,0,0,,Synaptic Memory\NGraph memory for RAG agents\NDocuments + SQL rows + feedback\NDefault path: no LLM calls at indexing time +Dialogue: 0,0:00:04.00,0:00:05.00,Terminal,,0,0,0,,1. Build a tiny mixed graph\NCreated graph: 4 nodes +Dialogue: 0,0:00:05.00,0:00:07.20,Terminal,,0,0,0,,1. Build a tiny mixed graph\NCreated graph: 4 nodes\NSources: 2 policy docs + 2 support ticket rows +Dialogue: 0,0:00:07.20,0:00:08.20,Terminal,,0,0,0,,2. Search and record retrieval\Nevent_id: +Dialogue: 0,0:00:08.20,0:00:09.20,Terminal,,0,0,0,,2. Search and record retrieval\Nevent_id: \N1. ticket:T-1001 score=0.660 source=ticket +Dialogue: 0,0:00:09.20,0:00:10.20,Terminal,,0,0,0,,2. Search and record retrieval\Nevent_id: \N1. ticket:T-1001 score=0.660 source=ticket\N2. Refund Policy score=0.435 source=policy/refunds.md +Dialogue: 0,0:00:10.20,0:00:12.40,Terminal,,0,0,0,,2. Search and record retrieval\Nevent_id: \N1. ticket:T-1001 score=0.660 source=ticket\N2. Refund Policy score=0.435 source=policy/refunds.md\N3. Shipping Policy score=0.210 source=policy/shipping.md +Dialogue: 0,0:00:12.40,0:00:14.60,Terminal,,0,0,0,,3. Feed back that the evidence helped\NRecorded explicit positive feedback for the top evidence. +Dialogue: 0,0:00:14.60,0:00:15.60,Terminal,,0,0,0,,4. Inspect memory health metadata\Nmemory_events: 2 +Dialogue: 0,0:00:15.60,0:00:16.60,Terminal,,0,0,0,,4. Inspect memory health metadata\Nmemory_events: 2\Nretrieval_events: 2 +Dialogue: 0,0:00:16.60,0:00:17.60,Terminal,,0,0,0,,4. Inspect memory health metadata\Nmemory_events: 2\Nretrieval_events: 2\Nmemory_scores: 2 +Dialogue: 0,0:00:17.60,0:00:18.60,Terminal,,0,0,0,,4. Inspect memory health metadata\Nmemory_events: 2\Nretrieval_events: 2\Nmemory_scores: 2\Nhealth_signals: 0 +Dialogue: 0,0:00:18.60,0:00:19.60,Terminal,,0,0,0,,4. Inspect memory health metadata\Nmemory_events: 2\Nretrieval_events: 2\Nmemory_scores: 2\Nhealth_signals: 0\NNo raw provenance was appended to Node.content. +Dialogue: 0,0:00:19.60,0:00:22.60,Terminal,,0,0,0,,4. Inspect memory health metadata\Nmemory_events: 2\Nretrieval_events: 2\Nmemory_scores: 2\Nhealth_signals: 0\NNo raw provenance was appended to Node.content.\NSwap the backend to PostgreSQL/Kuzu/Qdrant when the corpus grows. diff --git a/docs/launch/synaptic-memory-demo.gif b/docs/launch/synaptic-memory-demo.gif new file mode 100644 index 0000000..e5aaf09 Binary files /dev/null and b/docs/launch/synaptic-memory-demo.gif differ diff --git a/docs/launch/synaptic-memory-demo.mp4 b/docs/launch/synaptic-memory-demo.mp4 new file mode 100644 index 0000000..60ef75c Binary files /dev/null and b/docs/launch/synaptic-memory-demo.mp4 differ