Skip to content
Open
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
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.venv/
__pycache__/
*.pyc
inference_logs/
proof/frames/
proof/narration/
proof/segments/
proof/work/
proof/chrome-profile/
proof/*.mp4
proof/*.srt
proof/*-check/
*.egg-info/
.python-version
42 changes: 42 additions & 0 deletions proof/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Proof movie — Hermes Function Calling

A rebuildable movie that proves the function-calling **toolkit** works on
this machine. It does **not** load Hermes-2-Pro (that needs a GPU and
several gigabytes of weights). Yahoo Finance also 429'd from this network,
so the live tool shown is `code_interpreter` rather than a faked stock quote.

Pipeline is the proving-it-works skill (`prime-radiant-inc/proving-it-works`):
terminal capture via ttyd + tmux + Chrome, local narration, burned-in
subtitles, then `check-movie`.

## What the movie shows

| Scene | Real code path |
|---|---|
| tools | `functions.get_openai_tools()` |
| parse | `utils.validate_and_extract_tool_calls()` |
| validate | `validator.validate_function_call_schema()` |
| execute | `functions.code_interpreter.invoke(...)` |

## Rebuild

```bash
# once: lightweight venv (no torch / flash-attn)
uv venv .venv
uv pip install --python .venv/bin/python \
langchain==0.1.9 pydantic==2.6.2 jsonschema==4.21.1 \
yfinance==0.2.36 pandas==2.2.0 beautifulsoup4 requests art pyyaml \
pillow websockets

# ttyd + uv on PATH, Google Chrome installed
proof/make-movie.sh
```

`proof/scripts/` are the proving-it-works helpers (MIT), vendored so the
movie can be re-cut without a plugin install. Title/end cards are PNG
stills from `render_cards.py` (PIL) so assemble does not depend on a
browser.

Frames, narration wavs, and the encoded movie stay out of git — they are
scratch. `scenes.yaml`, `show.py`, `film.py`, `render_cards.py`,
`cards/*.png`, and `make-movie.sh` are the source of truth.
Binary file added proof/cards/end.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added proof/cards/title.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
219 changes: 219 additions & 0 deletions proof/film.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""Film a tmux+ttyd terminal of the Hermes toolkit via headless Chrome.

Adapted from proving-it-works examples/film-terminal.py: real characters in
a real shell, screenshotted from the canvas ttyd draws. Software GL is
required or headless Chrome paints that canvas black.
"""

from __future__ import annotations

import asyncio
import base64
import json
import subprocess
import sys
import time
import urllib.request
from pathlib import Path

HERE = Path(__file__).resolve().parent
FPS = 2.0
DEBUG_PORT = 7222
SESSION = "hermes-demo"
CHROME = "/usr/bin/google-chrome"


def pane_command() -> str:
r = subprocess.run(
["tmux", "display-message", "-p", "-t", SESSION, "#{pane_current_command}"],
capture_output=True,
text=True,
)
return r.stdout.strip()


def wait_for_shell(timeout: float = 120) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
if pane_command() in ("bash", "sh", "zsh"):
return
time.sleep(0.4)
raise SystemExit("shell never came back; refusing to type into a running program")


def send(keys: str, enter: bool = True) -> None:
cmd = ["tmux", "send-keys", "-t", SESSION, keys]
if enter:
cmd.append("Enter")
subprocess.run(cmd, check=True, capture_output=True)


def send_and_wait(keys: str, timeout: float = 120) -> None:
"""Type a command, wait for it to start, then wait for the shell to return."""
send(keys)
deadline = time.time() + 8
while time.time() < deadline and pane_command() in ("bash", "sh", "zsh"):
time.sleep(0.1)
wait_for_shell(timeout=timeout)


async def cdp(ws, mid, method, params=None):
await ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
while True:
msg = json.loads(await ws.recv())
if msg.get("id") == mid:
return msg.get("result", {})


async def shoot(ws, outdir: Path, stop: asyncio.Event, counter: list[int]) -> None:
import websockets # noqa: F401 — imported by caller

mid = 1000
while not stop.is_set():
mid += 1
try:
res = await asyncio.wait_for(
cdp(ws, mid, "Page.captureScreenshot", {"format": "png"}), timeout=3
)
data = res.get("data")
if data:
(outdir / f"f{counter[0]:05d}.png").write_bytes(base64.b64decode(data))
counter[0] += 1
except asyncio.TimeoutError:
pass
await asyncio.sleep(1 / FPS)


async def preflight(ws) -> None:
wait_for_shell()
send("echo PREFLIGHT_OK")
await asyncio.sleep(1.5)
res = await cdp(ws, 900, "Page.captureScreenshot", {"format": "png"})
png = base64.b64decode(res["data"])
from io import BytesIO

from PIL import Image

im = Image.open(BytesIO(png)).convert("L")
px = list(im.getdata())
lit = sum(1 for v in px if v > 90) / len(px)
if lit < 0.002:
raise SystemExit(
f"preflight failed: terminal renders blank ({lit:.4%} lit pixels). "
"Check software GL flags before filming."
)
print(f"preflight ok: {lit:.2%} of pixels lit")
send("clear")
await asyncio.sleep(0.8)


async def beats_tools() -> None:
await asyncio.sleep(1.2)
send_and_wait("python proof/show.py tools")
await asyncio.sleep(5)


async def beats_parse() -> None:
await asyncio.sleep(1.0)
send("clear")
await asyncio.sleep(0.6)
send_and_wait("python proof/show.py parse")
await asyncio.sleep(5)


async def beats_validate() -> None:
await asyncio.sleep(1.0)
send("clear")
await asyncio.sleep(0.6)
send_and_wait("python proof/show.py validate")
await asyncio.sleep(6)


async def beats_execute() -> None:
await asyncio.sleep(1.0)
send("clear")
await asyncio.sleep(0.6)
send_and_wait("python proof/show.py execute")
await asyncio.sleep(6)


BEATS = {
"tools": beats_tools,
"parse": beats_parse,
"validate": beats_validate,
"execute": beats_execute,
}


async def main(segment: str, url: str) -> None:
import websockets

if segment not in BEATS:
raise SystemExit(f"unknown segment {segment!r}; expected {sorted(BEATS)}")

outdir = HERE / "frames" / segment
outdir.mkdir(parents=True, exist_ok=True)
for old in outdir.glob("*.png"):
old.unlink()

# Drop a leftover debug Chrome so this take owns DEBUG_PORT.
subprocess.run(
["pkill", "-f", f"--remote-debugging-port={DEBUG_PORT}"],
capture_output=True,
)
time.sleep(0.4)

profile = HERE / "chrome-profile"
profile.mkdir(exist_ok=True)
chrome = subprocess.Popen(
[
CHROME,
f"--remote-debugging-port={DEBUG_PORT}",
"--headless=new",
"--user-data-dir=" + str(profile),
"--window-size=1280,800",
"--hide-scrollbars",
"--no-sandbox",
"--force-device-scale-factor=2",
"--use-gl=angle",
"--use-angle=swiftshader",
"--enable-unsafe-swiftshader",
"about:blank",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
try:
time.sleep(3)
tabs = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{DEBUG_PORT}/json").read())
ws_url = [t for t in tabs if t["type"] == "page"][0]["webSocketDebuggerUrl"]
async with websockets.connect(ws_url, max_size=40 * 1024 * 1024) as ws:
await cdp(ws, 1, "Page.enable")
await cdp(ws, 3, "Page.navigate", {"url": url})
await asyncio.sleep(4)
subprocess.run(
["tmux", "refresh-client", "-t", SESSION], capture_output=True
)
await asyncio.sleep(1)
await preflight(ws)

stop = asyncio.Event()
counter = [0]
task = asyncio.create_task(shoot(ws, outdir, stop, counter))
await BEATS[segment]()
stop.set()
await task
print(f"{segment}: {counter[0]} frames -> {outdir}")
finally:
chrome.terminate()
try:
chrome.wait(timeout=5)
except Exception:
chrome.kill()


if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit("usage: film.py <tools|parse|validate|execute> <ttyd-url>")
asyncio.run(main(sys.argv[1], sys.argv[2]))
55 changes: 55 additions & 0 deletions proof/make-movie.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Rebuild the Hermes Function Calling proof movie.
# Requires: ffmpeg, ffprobe, uv, ttyd, Google Chrome, the repo .venv
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PROOF="$ROOT/proof"
SKILL="$PROOF/scripts"
SESSION=hermes-demo
TTYD_PORT=7681
export PATH="$HOME/.local/bin:$ROOT/.venv/bin:$PATH"

cd "$ROOT"

if [[ ! -x "$ROOT/.venv/bin/python" ]]; then
echo "missing $ROOT/.venv — create it and pip-install langchain, pydantic, jsonschema, yfinance, pandas, art, pyyaml, pillow, websockets" >&2
exit 1
fi

# --- terminal session served over HTTP ---------------------------------
tmux has-session -t "$SESSION" 2>/dev/null || \
tmux new-session -d -s "$SESSION" -x 125 -y 34 -- bash -l

# Point the pane at the repo with a short prompt.
tmux send-keys -t "$SESSION" "cd $ROOT" Enter
tmux send-keys -t "$SESSION" "export PATH=$ROOT/.venv/bin:\$HOME/.local/bin:\$PATH" Enter
tmux send-keys -t "$SESSION" "export PYTHONUNBUFFERED=1 PYTHONWARNINGS=ignore" Enter
tmux send-keys -t "$SESSION" "export PS1='$ '" Enter
sleep 0.4

if ! curl -fsS "http://127.0.0.1:${TTYD_PORT}/" >/dev/null 2>&1; then
ttyd -p "$TTYD_PORT" \
-t fontSize=17 \
-t 'fontFamily=DejaVu Sans Mono,monospace' \
-t 'theme={"background":"#101014","foreground":"#e8e6e1"}' \
tmux attach -t "$SESSION" >/tmp/ttyd-hermes.log 2>&1 &
# wait until it answers
for _ in $(seq 1 30); do
curl -fsS "http://127.0.0.1:${TTYD_PORT}/" >/dev/null 2>&1 && break
sleep 0.3
done
fi

python "$PROOF/render_cards.py"
python "$PROOF/film.py" tools "http://127.0.0.1:${TTYD_PORT}/"
python "$PROOF/film.py" parse "http://127.0.0.1:${TTYD_PORT}/"
python "$PROOF/film.py" validate "http://127.0.0.1:${TTYD_PORT}/"
python "$PROOF/film.py" execute "http://127.0.0.1:${TTYD_PORT}/"

"$SKILL/narrate" "$PROOF/scenes.yaml" "$PROOF/narration/"
"$SKILL/assemble" "$PROOF/scenes.yaml" "$PROOF/silent-cut.mp4"
"$SKILL/make-subtitles" "$PROOF/narration/manifest.json" "$PROOF/movie.srt" \
--offsets-json "$PROOF/segments/offsets.json"
"$SKILL/burn-subtitles" "$PROOF/silent-cut.mp4" "$PROOF/movie.srt" "$PROOF/movie.mp4"
"$SKILL/check-movie" "$PROOF/movie.mp4"
Loading