Initial import of NousResearch/hermes-agent
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker / shell lint / Lint Dockerfile (hadolint) (push) Has been cancelled
Docker / shell lint / Lint docker/ shell scripts (shellcheck) (push) Has been cancelled
Docker Build and Publish / build-amd64 (push) Has been cancelled
Docker Build and Publish / build-arm64 (push) Has been cancelled
Lint (ruff + ty) / ruff + ty diff (push) Has been cancelled
Lint (ruff + ty) / ruff enforcement (blocking) (push) Has been cancelled
Lint (ruff + ty) / Windows footguns (blocking) (push) Has been cancelled
Nix Lockfile Fix / auto-fix-main (push) Has been cancelled
Nix Lockfile Fix / fix (push) Has been cancelled
Nix / nix (macos-latest) (push) Has been cancelled
Nix / nix (ubuntu-latest) (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
Build Skills Index / build-index (push) Has been cancelled
Tests / test (1) (push) Has been cancelled
Tests / test (2) (push) Has been cancelled
Tests / test (3) (push) Has been cancelled
Tests / test (4) (push) Has been cancelled
Tests / test (5) (push) Has been cancelled
Tests / test (6) (push) Has been cancelled
Tests / e2e (push) Has been cancelled
uv.lock check / uv lock --check (push) Has been cancelled
Docker Build and Publish / merge (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
Tests / save-durations (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker / shell lint / Lint Dockerfile (hadolint) (push) Has been cancelled
Docker / shell lint / Lint docker/ shell scripts (shellcheck) (push) Has been cancelled
Docker Build and Publish / build-amd64 (push) Has been cancelled
Docker Build and Publish / build-arm64 (push) Has been cancelled
Lint (ruff + ty) / ruff + ty diff (push) Has been cancelled
Lint (ruff + ty) / ruff enforcement (blocking) (push) Has been cancelled
Lint (ruff + ty) / Windows footguns (blocking) (push) Has been cancelled
Nix Lockfile Fix / auto-fix-main (push) Has been cancelled
Nix Lockfile Fix / fix (push) Has been cancelled
Nix / nix (macos-latest) (push) Has been cancelled
Nix / nix (ubuntu-latest) (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
Build Skills Index / build-index (push) Has been cancelled
Tests / test (1) (push) Has been cancelled
Tests / test (2) (push) Has been cancelled
Tests / test (3) (push) Has been cancelled
Tests / test (4) (push) Has been cancelled
Tests / test (5) (push) Has been cancelled
Tests / test (6) (push) Has been cancelled
Tests / e2e (push) Has been cancelled
uv.lock check / uv lock --check (push) Has been cancelled
Docker Build and Publish / merge (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
Tests / save-durations (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: blender-mcp
|
||||
description: Control Blender directly from Hermes via socket connection to the blender-mcp addon. Create 3D objects, materials, animations, and run arbitrary Blender Python (bpy) code. Use when user wants to create or modify anything in Blender.
|
||||
version: 1.0.0
|
||||
requires: Blender 4.3+ (desktop instance required, headless not supported)
|
||||
author: alireza78a
|
||||
tags: [blender, 3d, animation, modeling, bpy, mcp]
|
||||
platforms: [linux, macos, windows]
|
||||
---
|
||||
|
||||
# Blender MCP
|
||||
|
||||
Control a running Blender instance from Hermes via socket on TCP port 9876.
|
||||
|
||||
## Setup (one-time)
|
||||
|
||||
### 1. Install the Blender addon
|
||||
|
||||
curl -sL https://raw.githubusercontent.com/ahujasid/blender-mcp/main/addon.py -o ~/Desktop/blender_mcp_addon.py
|
||||
|
||||
In Blender:
|
||||
Edit > Preferences > Add-ons > Install > select blender_mcp_addon.py
|
||||
Enable "Interface: Blender MCP"
|
||||
|
||||
### 2. Start the socket server in Blender
|
||||
|
||||
Press N in Blender viewport to open sidebar.
|
||||
Find "BlenderMCP" tab and click "Start Server".
|
||||
|
||||
### 3. Verify connection
|
||||
|
||||
nc -z -w2 localhost 9876 && echo "OPEN" || echo "CLOSED"
|
||||
|
||||
## Protocol
|
||||
|
||||
Plain UTF-8 JSON over TCP -- no length prefix.
|
||||
|
||||
Send: {"type": "<command>", "params": {<kwargs>}}
|
||||
Receive: {"status": "success", "result": <value>}
|
||||
{"status": "error", "message": "<reason>"}
|
||||
|
||||
## Available Commands
|
||||
|
||||
| type | params | description |
|
||||
|-------------------------|-------------------|---------------------------------|
|
||||
| execute_code | code (str) | Run arbitrary bpy Python code |
|
||||
| get_scene_info | (none) | List all objects in scene |
|
||||
| get_object_info | object_name (str) | Details on a specific object |
|
||||
| get_viewport_screenshot | (none) | Screenshot of current viewport |
|
||||
|
||||
## Python Helper
|
||||
|
||||
Use this inside execute_code tool calls:
|
||||
|
||||
import socket, json
|
||||
|
||||
def blender_exec(code: str, host="localhost", port=9876, timeout=15):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect((host, port))
|
||||
s.settimeout(timeout)
|
||||
payload = json.dumps({"type": "execute_code", "params": {"code": code}})
|
||||
s.sendall(payload.encode("utf-8"))
|
||||
buf = b""
|
||||
while True:
|
||||
try:
|
||||
chunk = s.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
try:
|
||||
json.loads(buf.decode("utf-8"))
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
except socket.timeout:
|
||||
break
|
||||
s.close()
|
||||
return json.loads(buf.decode("utf-8"))
|
||||
|
||||
## Common bpy Patterns
|
||||
|
||||
### Clear scene
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.delete()
|
||||
|
||||
### Add mesh objects
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, location=(0, 0, 0))
|
||||
bpy.ops.mesh.primitive_cube_add(size=2, location=(3, 0, 0))
|
||||
bpy.ops.mesh.primitive_cylinder_add(radius=0.5, depth=2, location=(-3, 0, 0))
|
||||
|
||||
### Create and assign material
|
||||
mat = bpy.data.materials.new(name="MyMat")
|
||||
mat.use_nodes = True
|
||||
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||||
bsdf.inputs["Base Color"].default_value = (R, G, B, 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = 0.3
|
||||
bsdf.inputs["Metallic"].default_value = 0.0
|
||||
obj.data.materials.append(mat)
|
||||
|
||||
### Keyframe animation
|
||||
obj.location = (0, 0, 0)
|
||||
obj.keyframe_insert(data_path="location", frame=1)
|
||||
obj.location = (0, 0, 3)
|
||||
obj.keyframe_insert(data_path="location", frame=60)
|
||||
|
||||
### Render to file
|
||||
bpy.context.scene.render.filepath = "/tmp/render.png"
|
||||
bpy.context.scene.render.engine = 'CYCLES'
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Must check socket is open before running (nc -z localhost 9876)
|
||||
- Addon server must be started inside Blender each session (N-panel > BlenderMCP > Connect)
|
||||
- Break complex scenes into multiple smaller execute_code calls to avoid timeouts
|
||||
- Render output path must be absolute (/tmp/...) not relative
|
||||
- shade_smooth() requires object to be selected and in object mode
|
||||
@@ -0,0 +1,362 @@
|
||||
---
|
||||
name: concept-diagrams
|
||||
description: Generate flat, minimal light/dark-aware SVG diagrams as standalone HTML files, using a unified educational visual language with 9 semantic color ramps, sentence-case typography, and automatic dark mode. Best suited for educational and non-software visuals — physics setups, chemistry mechanisms, math curves, physical objects (aircraft, turbines, smartphones, mechanical watches), anatomy, floor plans, cross-sections, narrative journeys (lifecycle of X, process of Y), hub-spoke system integrations (smart city, IoT), and exploded layer views. If a more specialized skill exists for the subject (dedicated software/cloud architecture, hand-drawn sketches, animated explainers, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback with a clean educational look. Ships with 15 example diagrams.
|
||||
version: 0.1.0
|
||||
author: v1k22 (original PR), ported into hermes-agent
|
||||
license: MIT
|
||||
dependencies: []
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [diagrams, svg, visualization, education, physics, chemistry, engineering]
|
||||
related_skills: [architecture-diagram, excalidraw, generative-widgets]
|
||||
---
|
||||
|
||||
# Concept Diagrams
|
||||
|
||||
Generate production-quality SVG diagrams with a unified flat, minimal design system. Output is a single self-contained HTML file that renders identically in any modern browser, with automatic light/dark mode.
|
||||
|
||||
## Scope
|
||||
|
||||
**Best suited for:**
|
||||
- Physics setups, chemistry mechanisms, math curves, biology
|
||||
- Physical objects (aircraft, turbines, smartphones, mechanical watches, cells)
|
||||
- Anatomy, cross-sections, exploded layer views
|
||||
- Floor plans, architectural conversions
|
||||
- Narrative journeys (lifecycle of X, process of Y)
|
||||
- Hub-spoke system integrations (smart city, IoT networks, electricity grids)
|
||||
- Educational / textbook-style visuals in any domain
|
||||
- Quantitative charts (grouped bars, energy profiles)
|
||||
|
||||
**Look elsewhere first for:**
|
||||
- Dedicated software / cloud infrastructure architecture with a dark tech aesthetic (consider `architecture-diagram` if available)
|
||||
- Hand-drawn whiteboard sketches (consider `excalidraw` if available)
|
||||
- Animated explainers or video output (consider an animation skill)
|
||||
|
||||
If a more specialized skill is available for the subject, prefer that. If none fits, this skill can serve as a general-purpose SVG diagram fallback — the output will carry the clean educational aesthetic described below, which is a reasonable default for almost any subject.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Decide on the diagram type (see Diagram Types below).
|
||||
2. Lay out components using the Design System rules.
|
||||
3. Write the full HTML page using `templates/template.html` as the wrapper — paste your SVG where the template says `<!-- PASTE SVG HERE -->`.
|
||||
4. Save as a standalone `.html` file (for example `~/my-diagram.html` or `./my-diagram.html`).
|
||||
5. User opens it directly in a browser — no server, no dependencies.
|
||||
|
||||
Optional: if the user wants a browsable gallery of multiple diagrams, see "Local Preview Server" at the bottom.
|
||||
|
||||
Load the HTML template:
|
||||
```
|
||||
skill_view(name="concept-diagrams", file_path="templates/template.html")
|
||||
```
|
||||
|
||||
The template embeds the full CSS design system (`c-*` color classes, text classes, light/dark variables, arrow marker styles). The SVG you generate relies on these classes being present on the hosting page.
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
### Philosophy
|
||||
|
||||
- **Flat**: no gradients, drop shadows, blur, glow, or neon effects.
|
||||
- **Minimal**: show the essential. No decorative icons inside boxes.
|
||||
- **Consistent**: same colors, spacing, typography, and stroke widths across every diagram.
|
||||
- **Dark-mode ready**: all colors auto-adapt via CSS classes — no per-mode SVG.
|
||||
|
||||
### Color Palette
|
||||
|
||||
9 color ramps, each with 7 stops. Put the class name on a `<g>` or shape element; the template CSS handles both modes.
|
||||
|
||||
| Class | 50 (lightest) | 100 | 200 | 400 | 600 | 800 | 900 (darkest) |
|
||||
|------------|---------------|---------|---------|---------|---------|---------|---------------|
|
||||
| `c-purple` | #EEEDFE | #CECBF6 | #AFA9EC | #7F77DD | #534AB7 | #3C3489 | #26215C |
|
||||
| `c-teal` | #E1F5EE | #9FE1CB | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 | #04342C |
|
||||
| `c-coral` | #FAECE7 | #F5C4B3 | #F0997B | #D85A30 | #993C1D | #712B13 | #4A1B0C |
|
||||
| `c-pink` | #FBEAF0 | #F4C0D1 | #ED93B1 | #D4537E | #993556 | #72243E | #4B1528 |
|
||||
| `c-gray` | #F1EFE8 | #D3D1C7 | #B4B2A9 | #888780 | #5F5E5A | #444441 | #2C2C2A |
|
||||
| `c-blue` | #E6F1FB | #B5D4F4 | #85B7EB | #378ADD | #185FA5 | #0C447C | #042C53 |
|
||||
| `c-green` | #EAF3DE | #C0DD97 | #97C459 | #639922 | #3B6D11 | #27500A | #173404 |
|
||||
| `c-amber` | #FAEEDA | #FAC775 | #EF9F27 | #BA7517 | #854F0B | #633806 | #412402 |
|
||||
| `c-red` | #FCEBEB | #F7C1C1 | #F09595 | #E24B4A | #A32D2D | #791F1F | #501313 |
|
||||
|
||||
#### Color Assignment Rules
|
||||
|
||||
Color encodes **meaning**, not sequence. Never cycle through colors like a rainbow.
|
||||
|
||||
- Group nodes by **category** — all nodes of the same type share one color.
|
||||
- Use `c-gray` for neutral/structural nodes (start, end, generic steps, users).
|
||||
- Use **2-3 colors per diagram**, not 6+.
|
||||
- Prefer `c-purple`, `c-teal`, `c-coral`, `c-pink` for general categories.
|
||||
- Reserve `c-blue`, `c-green`, `c-amber`, `c-red` for semantic meaning (info, success, warning, error).
|
||||
|
||||
Light/dark stop mapping (handled by the template CSS — just use the class):
|
||||
- Light mode: 50 fill + 600 stroke + 800 title / 600 subtitle
|
||||
- Dark mode: 800 fill + 200 stroke + 100 title / 200 subtitle
|
||||
|
||||
### Typography
|
||||
|
||||
Only two font sizes. No exceptions.
|
||||
|
||||
| Class | Size | Weight | Use |
|
||||
|-------|------|--------|-----|
|
||||
| `th` | 14px | 500 | Node titles, region labels |
|
||||
| `ts` | 12px | 400 | Subtitles, descriptions, arrow labels |
|
||||
| `t` | 14px | 400 | General text |
|
||||
|
||||
- **Sentence case always.** Never Title Case, never ALL CAPS.
|
||||
- Every `<text>` MUST carry a class (`t`, `ts`, or `th`). No unclassed text.
|
||||
- `dominant-baseline="central"` on all text inside boxes.
|
||||
- `text-anchor="middle"` for centered text in boxes.
|
||||
|
||||
**Width estimation (approx):**
|
||||
- 14px weight 500: ~8px per character
|
||||
- 12px weight 400: ~6.5px per character
|
||||
- Always verify: `box_width >= (char_count × px_per_char) + 48` (24px padding each side)
|
||||
|
||||
### Spacing & Layout
|
||||
|
||||
- **ViewBox**: `viewBox="0 0 680 H"` where H = content height + 40px buffer.
|
||||
- **Safe area**: x=40 to x=640, y=40 to y=(H-40).
|
||||
- **Between boxes**: 60px minimum gap.
|
||||
- **Inside boxes**: 24px horizontal padding, 12px vertical padding.
|
||||
- **Arrowhead gap**: 10px between arrowhead and box edge.
|
||||
- **Single-line box**: 44px height.
|
||||
- **Two-line box**: 56px height, 18px between title and subtitle baselines.
|
||||
- **Container padding**: 20px minimum inside every container.
|
||||
- **Max nesting**: 2-3 levels deep. Deeper gets unreadable at 680px width.
|
||||
|
||||
### Stroke & Shape
|
||||
|
||||
- **Stroke width**: 0.5px on all node borders. Not 1px, not 2px.
|
||||
- **Rect rounding**: `rx="8"` for nodes, `rx="12"` for inner containers, `rx="16"` to `rx="20"` for outer containers.
|
||||
- **Connector paths**: MUST have `fill="none"`. SVG defaults to `fill: black` otherwise.
|
||||
|
||||
### Arrow Marker
|
||||
|
||||
Include this `<defs>` block at the start of **every** SVG:
|
||||
|
||||
```xml
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5"
|
||||
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
</defs>
|
||||
```
|
||||
|
||||
Use `marker-end="url(#arrow)"` on lines. The arrowhead inherits the line color via `context-stroke`.
|
||||
|
||||
### CSS Classes (Provided by the Template)
|
||||
|
||||
The template page provides:
|
||||
|
||||
- Text: `.t`, `.ts`, `.th`
|
||||
- Neutral: `.box`, `.arr`, `.leader`, `.node`
|
||||
- Color ramps: `.c-purple`, `.c-teal`, `.c-coral`, `.c-pink`, `.c-gray`, `.c-blue`, `.c-green`, `.c-amber`, `.c-red` (all with automatic light/dark mode)
|
||||
|
||||
You do **not** need to redefine these — just apply them in your SVG. The template file contains the full CSS definitions.
|
||||
|
||||
---
|
||||
|
||||
## SVG Boilerplate
|
||||
|
||||
Every SVG inside the template page starts with this exact structure:
|
||||
|
||||
```xml
|
||||
<svg width="100%" viewBox="0 0 680 {HEIGHT}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5"
|
||||
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Diagram content here -->
|
||||
|
||||
</svg>
|
||||
```
|
||||
|
||||
Replace `{HEIGHT}` with the actual computed height (last element bottom + 40px).
|
||||
|
||||
### Node Patterns
|
||||
|
||||
**Single-line node (44px):**
|
||||
```xml
|
||||
<g class="node c-blue">
|
||||
<rect x="100" y="20" width="180" height="44" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="190" y="42" text-anchor="middle" dominant-baseline="central">Service name</text>
|
||||
</g>
|
||||
```
|
||||
|
||||
**Two-line node (56px):**
|
||||
```xml
|
||||
<g class="node c-teal">
|
||||
<rect x="100" y="20" width="200" height="56" rx="8" stroke-width="0.5"/>
|
||||
<text class="th" x="200" y="38" text-anchor="middle" dominant-baseline="central">Service name</text>
|
||||
<text class="ts" x="200" y="56" text-anchor="middle" dominant-baseline="central">Short description</text>
|
||||
</g>
|
||||
```
|
||||
|
||||
**Connector (no label):**
|
||||
```xml
|
||||
<line x1="200" y1="76" x2="200" y2="120" class="arr" marker-end="url(#arrow)"/>
|
||||
```
|
||||
|
||||
**Container (dashed or solid):**
|
||||
```xml
|
||||
<g class="c-purple">
|
||||
<rect x="40" y="92" width="600" height="300" rx="16" stroke-width="0.5"/>
|
||||
<text class="th" x="66" y="116">Container label</text>
|
||||
<text class="ts" x="66" y="134">Subtitle info</text>
|
||||
</g>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram Types
|
||||
|
||||
Choose the layout that fits the subject:
|
||||
|
||||
1. **Flowchart** — CI/CD pipelines, request lifecycles, approval workflows, data processing. Single-direction flow (top-down or left-right). Max 4-5 nodes per row.
|
||||
2. **Structural / Containment** — Cloud infrastructure nesting, system architecture with layers. Large outer containers with inner regions. Dashed rects for logical groupings.
|
||||
3. **API / Endpoint Map** — REST routes, GraphQL schemas. Tree from root, branching to resource groups, each containing endpoint nodes.
|
||||
4. **Microservice Topology** — Service mesh, event-driven systems. Services as nodes, arrows for communication patterns, message queues between.
|
||||
5. **Data Flow** — ETL pipelines, streaming architectures. Left-to-right flow from sources through processing to sinks.
|
||||
6. **Physical / Structural** — Vehicles, buildings, hardware, anatomy. Use shapes that match the physical form — `<path>` for curved bodies, `<polygon>` for tapered shapes, `<ellipse>`/`<circle>` for cylindrical parts, nested `<rect>` for compartments. See `references/physical-shape-cookbook.md`.
|
||||
7. **Infrastructure / Systems Integration** — Smart cities, IoT networks, multi-domain systems. Hub-spoke layout with central platform connecting subsystems. Semantic line styles (`.data-line`, `.power-line`, `.water-pipe`, `.road`). See `references/infrastructure-patterns.md`.
|
||||
8. **UI / Dashboard Mockups** — Admin panels, monitoring dashboards. Screen frame with nested chart/gauge/indicator elements. See `references/dashboard-patterns.md`.
|
||||
|
||||
For physical, infrastructure, and dashboard diagrams, load the matching reference file before generating — each one provides ready-made CSS classes and shape primitives.
|
||||
|
||||
---
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before finalizing any SVG, verify ALL of the following:
|
||||
|
||||
1. Every `<text>` has class `t`, `ts`, or `th`.
|
||||
2. Every `<text>` inside a box has `dominant-baseline="central"`.
|
||||
3. Every connector `<path>` or `<line>` used as arrow has `fill="none"`.
|
||||
4. No arrow line crosses through an unrelated box.
|
||||
5. `box_width >= (longest_label_chars × 8) + 48` for 14px text.
|
||||
6. `box_width >= (longest_label_chars × 6.5) + 48` for 12px text.
|
||||
7. ViewBox height = bottom-most element + 40px.
|
||||
8. All content stays within x=40 to x=640.
|
||||
9. Color classes (`c-*`) are on `<g>` or shape elements, never on `<path>` connectors.
|
||||
10. Arrow `<defs>` block is present.
|
||||
11. No gradients, shadows, blur, or glow effects.
|
||||
12. Stroke width is 0.5px on all node borders.
|
||||
|
||||
---
|
||||
|
||||
## Output & Preview
|
||||
|
||||
### Default: standalone HTML file
|
||||
|
||||
Write a single `.html` file the user can open directly. No server, no dependencies, works offline. Pattern:
|
||||
|
||||
```python
|
||||
# 1. Load the template
|
||||
template = skill_view("concept-diagrams", "templates/template.html")
|
||||
|
||||
# 2. Fill in title, subtitle, and paste your SVG
|
||||
html = template.replace(
|
||||
"<!-- DIAGRAM TITLE HERE -->", "SN2 reaction mechanism"
|
||||
).replace(
|
||||
"<!-- OPTIONAL SUBTITLE HERE -->", "Bimolecular nucleophilic substitution"
|
||||
).replace(
|
||||
"<!-- PASTE SVG HERE -->", svg_content
|
||||
)
|
||||
|
||||
# 3. Write to a user-chosen path (or ./ by default)
|
||||
write_file("./sn2-mechanism.html", html)
|
||||
```
|
||||
|
||||
Tell the user how to open it:
|
||||
|
||||
```
|
||||
# macOS
|
||||
open ./sn2-mechanism.html
|
||||
# Linux
|
||||
xdg-open ./sn2-mechanism.html
|
||||
```
|
||||
|
||||
### Optional: local preview server (multi-diagram gallery)
|
||||
|
||||
Only use this when the user explicitly wants a browsable gallery of multiple diagrams.
|
||||
|
||||
**Rules:**
|
||||
- Bind to `127.0.0.1` only. Never `0.0.0.0`. Exposing diagrams on all network interfaces is a security hazard on shared networks.
|
||||
- Pick a free port (do NOT hard-code one) and tell the user the chosen URL.
|
||||
- The server is optional and opt-in — prefer the standalone HTML file first.
|
||||
|
||||
Recommended pattern (lets the OS pick a free ephemeral port):
|
||||
|
||||
```bash
|
||||
# Put each diagram in its own folder under .diagrams/
|
||||
mkdir -p .diagrams/sn2-mechanism
|
||||
# ...write .diagrams/sn2-mechanism/index.html...
|
||||
|
||||
# Serve on loopback only, free port
|
||||
cd .diagrams && python3 -c "
|
||||
import http.server, socketserver
|
||||
with socketserver.TCPServer(('127.0.0.1', 0), http.server.SimpleHTTPRequestHandler) as s:
|
||||
print(f'Serving at http://127.0.0.1:{s.server_address[1]}/')
|
||||
s.serve_forever()
|
||||
" &
|
||||
```
|
||||
|
||||
If the user insists on a fixed port, use `127.0.0.1:<port>` — still never `0.0.0.0`. Document how to stop the server (`kill %1` or `pkill -f "http.server"`).
|
||||
|
||||
---
|
||||
|
||||
## Examples Reference
|
||||
|
||||
The `examples/` directory ships 15 complete, tested diagrams. Browse them for working patterns before writing a new diagram of a similar type:
|
||||
|
||||
| File | Type | Demonstrates |
|
||||
|------|------|--------------|
|
||||
| `hospital-emergency-department-flow.md` | Flowchart | Priority routing with semantic colors |
|
||||
| `feature-film-production-pipeline.md` | Flowchart | Phased workflow, horizontal sub-flows |
|
||||
| `automated-password-reset-flow.md` | Flowchart | Auth flow with error branches |
|
||||
| `autonomous-llm-research-agent-flow.md` | Flowchart | Loop-back arrows, decision branches |
|
||||
| `place-order-uml-sequence.md` | Sequence | UML sequence diagram style |
|
||||
| `commercial-aircraft-structure.md` | Physical | Paths, polygons, ellipses for realistic shapes |
|
||||
| `wind-turbine-structure.md` | Physical cross-section | Underground/above-ground separation, color coding |
|
||||
| `smartphone-layer-anatomy.md` | Exploded view | Alternating left/right labels, layered components |
|
||||
| `apartment-floor-plan-conversion.md` | Floor plan | Walls, doors, proposed changes in dotted red |
|
||||
| `banana-journey-tree-to-smoothie.md` | Narrative journey | Winding path, progressive state changes |
|
||||
| `cpu-ooo-microarchitecture.md` | Hardware pipeline | Fan-out, memory hierarchy sidebar |
|
||||
| `sn2-reaction-mechanism.md` | Chemistry | Molecules, curved arrows, energy profile |
|
||||
| `smart-city-infrastructure.md` | Hub-spoke | Semantic line styles per system |
|
||||
| `electricity-grid-flow.md` | Multi-stage flow | Voltage hierarchy, flow markers |
|
||||
| `ml-benchmark-grouped-bar-chart.md` | Chart | Grouped bars, dual axis |
|
||||
|
||||
Load any example with:
|
||||
```
|
||||
skill_view(name="concept-diagrams", file_path="examples/<filename>")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: What to Use When
|
||||
|
||||
| User says | Diagram type | Suggested colors |
|
||||
|-----------|--------------|------------------|
|
||||
| "show the pipeline" | Flowchart | gray start/end, purple steps, red errors, teal deploy |
|
||||
| "draw the data flow" | Data pipeline (left-right) | gray sources, purple processing, teal sinks |
|
||||
| "visualize the system" | Structural (containment) | purple container, teal services, coral data |
|
||||
| "map the endpoints" | API tree | purple root, one ramp per resource group |
|
||||
| "show the services" | Microservice topology | gray ingress, teal services, purple bus, coral workers |
|
||||
| "draw the aircraft/vehicle" | Physical | paths, polygons, ellipses for realistic shapes |
|
||||
| "smart city / IoT" | Hub-spoke integration | semantic line styles per subsystem |
|
||||
| "show the dashboard" | UI mockup | dark screen, chart colors: teal, purple, coral for alerts |
|
||||
| "power grid / electricity" | Multi-stage flow | voltage hierarchy (HV/MV/LV line weights) |
|
||||
| "wind turbine / turbine" | Physical cross-section | foundation + tower cutaway + nacelle color-coded |
|
||||
| "journey of X / lifecycle" | Narrative journey | winding path, progressive state changes |
|
||||
| "layers of X / exploded" | Exploded layer view | vertical stack, alternating labels |
|
||||
| "CPU / pipeline" | Hardware pipeline | vertical stages, fan-out to execution ports |
|
||||
| "floor plan / apartment" | Floor plan | walls, doors, proposed changes in dotted red |
|
||||
| "reaction mechanism" | Chemistry | atoms, bonds, curved arrows, transition state, energy profile |
|
||||
@@ -0,0 +1,43 @@
|
||||
# Dashboard Patterns
|
||||
|
||||
Building blocks for UI/dashboard mockups inside a concept diagram — admin panels, monitoring dashboards, control interfaces, status displays.
|
||||
|
||||
## Pattern
|
||||
|
||||
A "screen" is a rounded dark rect inside a lighter "frame" rect, with chart/gauge/indicator elements nested on top.
|
||||
|
||||
```xml
|
||||
<!-- Monitor frame -->
|
||||
<rect class="dashboard" x="0" y="0" width="200" height="120" rx="8"/>
|
||||
<!-- Screen -->
|
||||
<rect class="screen" x="10" y="10" width="180" height="85" rx="4"/>
|
||||
<!-- Mini bar chart -->
|
||||
<rect class="screen-content" x="18" y="18" width="50" height="35" rx="2"/>
|
||||
<rect class="screen-chart" x="22" y="38" width="8" height="12"/>
|
||||
<rect class="screen-chart" x="33" y="32" width="8" height="18"/>
|
||||
<!-- Gauge -->
|
||||
<circle class="screen-bar" cx="100" cy="35" r="12"/>
|
||||
<text x="100" y="39" text-anchor="middle" fill="#E8E6DE" style="font-size:8px">78%</text>
|
||||
<!-- Status indicators -->
|
||||
<circle cx="35" cy="74" r="6" fill="#97C459"/> <!-- green = ok -->
|
||||
<circle cx="75" cy="74" r="6" fill="#EF9F27"/> <!-- amber = warning -->
|
||||
<circle cx="115" cy="74" r="6" fill="#E24B4A"/> <!-- red = alert -->
|
||||
```
|
||||
|
||||
## CSS
|
||||
|
||||
```css
|
||||
.dashboard { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1.5; }
|
||||
.screen { fill: #1a1a18; }
|
||||
.screen-content { fill: #2C2C2A; }
|
||||
.screen-chart { fill: #5DCAA5; }
|
||||
.screen-bar { fill: #7F77DD; }
|
||||
.screen-alert { fill: #E24B4A; }
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Dashboard screens stay dark in both light and dark mode — they represent actual monitor glass.
|
||||
- Keep on-screen text small (`font-size:8px` or `10px`) and high-contrast (near-white fill on dark).
|
||||
- Use the status triad green/amber/red consistently — OK / warning / alert.
|
||||
- A single dashboard usually sits on top of an infrastructure hub diagram as a unified view (see `examples/smart-city-infrastructure.md`).
|
||||
@@ -0,0 +1,144 @@
|
||||
# Infrastructure Patterns
|
||||
|
||||
Reusable shapes and line styles for infrastructure / systems-integration diagrams (smart cities, IoT networks, industrial systems, multi-domain architectures).
|
||||
|
||||
## Layout pattern: hub-spoke
|
||||
|
||||
- **Central hub**: Hexagon or circle representing the integration platform
|
||||
- **Radiating connections**: Data lines from hub to each subsystem with connection dots
|
||||
- **Subsystem sections**: Each system (power, water, transport) in its own region
|
||||
- **Dashboard on top**: Optional UI mockup showing a unified view (see `dashboard-patterns.md`)
|
||||
|
||||
```xml
|
||||
<!-- Central hub (hexagon) -->
|
||||
<polygon class="iot-hex" points="0,-45 39,-22 39,22 0,45 -39,22 -39,-22"/>
|
||||
|
||||
<!-- Data lines with connection dots -->
|
||||
<path class="data-line" d="M 321 248 L 200 248 L 120 380" stroke-dasharray="4 3"/>
|
||||
<circle cx="321" cy="248" r="4" fill="#7F77DD"/>
|
||||
```
|
||||
|
||||
## Semantic line styles
|
||||
|
||||
Use a dedicated CSS class per subsystem so every diagram reads the same way:
|
||||
|
||||
```css
|
||||
.data-line { stroke: #7F77DD; stroke-width: 2; fill: none; stroke-dasharray: 4 3; }
|
||||
.power-line { stroke: #EF9F27; stroke-width: 2; fill: none; }
|
||||
.water-pipe { stroke: #378ADD; stroke-width: 4; stroke-linecap: round; fill: none; }
|
||||
.road { stroke: #888780; stroke-width: 8; stroke-linecap: round; fill: none; }
|
||||
```
|
||||
|
||||
## Power systems
|
||||
|
||||
**Solar panel (angled):**
|
||||
```xml
|
||||
<polygon class="solar-panel" points="0,25 35,8 38,12 3,29"/>
|
||||
<line class="solar-frame" x1="12" y1="22" x2="24" y2="13"/>
|
||||
```
|
||||
|
||||
**Wind turbine:**
|
||||
```xml
|
||||
<polygon class="wind-tower" points="20,70 30,70 28,25 22,25"/>
|
||||
<circle class="wind-hub" cx="25" cy="18" r="5"/>
|
||||
<ellipse class="wind-blade" cx="25" cy="5" rx="3" ry="13"/>
|
||||
<ellipse class="wind-blade" cx="14" cy="26" rx="3" ry="13" transform="rotate(-120, 25, 18)"/>
|
||||
<ellipse class="wind-blade" cx="36" cy="26" rx="3" ry="13" transform="rotate(120, 25, 18)"/>
|
||||
```
|
||||
|
||||
**Battery with charge level:**
|
||||
```xml
|
||||
<rect class="battery" x="0" y="0" width="45" height="65" rx="5"/>
|
||||
<rect x="10" y="-6" width="10" height="8" rx="2" fill="#27500A"/> <!-- terminal -->
|
||||
<rect class="battery-level" x="5" y="12" width="35" height="48" rx="3"/> <!-- fill level -->
|
||||
```
|
||||
|
||||
**Power pylon:**
|
||||
```xml
|
||||
<polygon class="pylon" points="30,0 35,0 40,60 25,60"/>
|
||||
<line x1="15" y1="10" x2="45" y2="10" stroke="#5F5E5A" stroke-width="3"/>
|
||||
<circle cx="18" cy="10" r="3" fill="#FAEEDA" stroke="#854F0B"/> <!-- insulator -->
|
||||
```
|
||||
|
||||
## Water systems
|
||||
|
||||
**Reservoir/dam:**
|
||||
```xml
|
||||
<polygon class="reservoir-wall" points="0,60 10,0 70,0 80,60"/>
|
||||
<polygon class="water" points="12,10 68,10 68,55 75,55 75,58 5,58 5,55 12,55"/>
|
||||
<!-- Wave effect -->
|
||||
<path d="M 15 25 Q 25 22 35 25 Q 45 28 55 25" fill="none" stroke="#378ADD" opacity="0.5"/>
|
||||
```
|
||||
|
||||
**Treatment tank:**
|
||||
```xml
|
||||
<ellipse class="treatment-tank" cx="35" cy="45" rx="30" ry="18"/>
|
||||
<rect class="treatment-tank" x="5" y="20" width="60" height="25"/>
|
||||
<!-- Bubbles -->
|
||||
<circle cx="20" cy="32" r="2" fill="#378ADD" opacity="0.6"/>
|
||||
```
|
||||
|
||||
**Pipe with joint and valve:**
|
||||
```xml
|
||||
<path class="pipe" d="M 80 85 L 110 85"/>
|
||||
<circle class="pipe-joint" cx="110" cy="85" r="8"/>
|
||||
<circle class="valve" cx="95" cy="85" r="6"/>
|
||||
```
|
||||
|
||||
## Transport systems
|
||||
|
||||
**Road with lane markings:**
|
||||
```xml
|
||||
<line class="road" x1="0" y1="50" x2="170" y2="50"/>
|
||||
<line class="road-mark" x1="10" y1="50" x2="160" y2="50"/>
|
||||
```
|
||||
|
||||
**Traffic light:**
|
||||
```xml
|
||||
<rect class="traffic-light" x="0" y="0" width="14" height="32" rx="3"/>
|
||||
<circle class="light-red" cx="7" cy="8" r="4"/>
|
||||
<circle class="light-off" cx="7" cy="16" r="4"/>
|
||||
<circle class="light-green" cx="7" cy="24" r="4"/>
|
||||
```
|
||||
|
||||
**Bus:**
|
||||
```xml
|
||||
<rect class="bus" x="0" y="0" width="55" height="28" rx="6"/>
|
||||
<rect class="bus-window" x="5" y="5" width="12" height="12" rx="2"/>
|
||||
<circle cx="14" cy="30" r="6" fill="#2C2C2A"/> <!-- wheel -->
|
||||
<circle cx="14" cy="30" r="3" fill="#5F5E5A"/> <!-- hubcap -->
|
||||
```
|
||||
|
||||
## Full CSS block (add to the host page or inline <style>)
|
||||
|
||||
```css
|
||||
/* Power */
|
||||
.solar-panel { fill: #3C3489; stroke: #534AB7; stroke-width: 0.5; }
|
||||
.wind-tower { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1; }
|
||||
.wind-blade { fill: #F1EFE8; stroke: #888780; stroke-width: 0.5; }
|
||||
.battery { fill: #27500A; stroke: #3B6D11; stroke-width: 1.5; }
|
||||
.battery-level { fill: #97C459; }
|
||||
.power-line { stroke: #EF9F27; stroke-width: 2; fill: none; }
|
||||
|
||||
/* Water */
|
||||
.reservoir-wall { fill: #B4B2A9; stroke: #5F5E5A; stroke-width: 1; }
|
||||
.water { fill: #85B7EB; stroke: #378ADD; stroke-width: 0.5; }
|
||||
.pipe { fill: none; stroke: #378ADD; stroke-width: 4; stroke-linecap: round; }
|
||||
.pipe-joint { fill: #185FA5; stroke: #0C447C; stroke-width: 1; }
|
||||
.valve { fill: #0C447C; stroke: #185FA5; stroke-width: 1; }
|
||||
|
||||
/* Transport */
|
||||
.road { stroke: #888780; stroke-width: 8; fill: none; stroke-linecap: round; }
|
||||
.road-mark { stroke: #F1EFE8; stroke-width: 1; stroke-dasharray: 6 4; fill: none; }
|
||||
.traffic-light { fill: #444441; stroke: #2C2C2A; stroke-width: 0.5; }
|
||||
.light-red { fill: #E24B4A; }
|
||||
.light-green { fill: #97C459; }
|
||||
.light-off { fill: #2C2C2A; }
|
||||
.bus { fill: #E1F5EE; stroke: #0F6E56; stroke-width: 1.5; }
|
||||
```
|
||||
|
||||
## Reference examples
|
||||
|
||||
- `examples/smart-city-infrastructure.md` — hub-spoke with multiple subsystems
|
||||
- `examples/electricity-grid-flow.md` — voltage hierarchy, flow markers
|
||||
- `examples/wind-turbine-structure.md` — cross-section with legend
|
||||
@@ -0,0 +1,42 @@
|
||||
# Physical Shape Cookbook
|
||||
|
||||
Guidance for drawing physical objects (vehicles, buildings, hardware, mechanical systems, anatomy) — when rectangles aren't enough.
|
||||
|
||||
## Shape selection
|
||||
|
||||
| Physical form | SVG element | Example use |
|
||||
|---------------|-------------|-------------|
|
||||
| Curved bodies | `<path>` with Q/C curves | Fuselage, tanks, pipes |
|
||||
| Tapered/angular shapes | `<polygon>` | Wings, fins, wedges |
|
||||
| Cylindrical/round | `<ellipse>`, `<circle>` | Engines, wheels, buttons |
|
||||
| Linear structures | `<line>` | Struts, beams, connections |
|
||||
| Internal sections | `<rect>` inside parent | Compartments, rooms |
|
||||
| Dashed boundaries | `stroke-dasharray` | Hidden parts, fuel tanks |
|
||||
|
||||
## Layering approach
|
||||
|
||||
1. Draw outer structure first (fuselage, frame, hull)
|
||||
2. Add internal sections on top (cabins, compartments)
|
||||
3. Add detail elements (engines, wheels, controls)
|
||||
4. Add leader lines with labels
|
||||
|
||||
## Semantic CSS classes (instead of c-* ramps)
|
||||
|
||||
For physical diagrams, define component-specific classes directly rather than applying `c-*` color classes. This makes each part self-documenting and lets you keep a restrained palette:
|
||||
|
||||
```css
|
||||
.fuselage { fill: #F1EFE8; stroke: #5F5E5A; stroke-width: 1; }
|
||||
.wing { fill: #E6F1FB; stroke: #185FA5; stroke-width: 1; }
|
||||
.engine { fill: #FAECE7; stroke: #993C1D; stroke-width: 1; }
|
||||
```
|
||||
|
||||
Add these to a local `<style>` inside the SVG (or extend the host page's `<style>` block). The light-mode/dark-mode pattern still works — use the CSS variables from the template (`var(--bg-secondary)`, `var(--border)`, `var(--text-primary)`) if you want dark-mode awareness.
|
||||
|
||||
## Reference examples
|
||||
|
||||
Look at these example files for working physical-diagram patterns:
|
||||
|
||||
- `examples/commercial-aircraft-structure.md` — fuselage curves + tapered wings + ellipse engines
|
||||
- `examples/wind-turbine-structure.md` — underground foundation, tubular tower, nacelle cutaway
|
||||
- `examples/smartphone-layer-anatomy.md` — exploded-view stack with alternating labels
|
||||
- `examples/apartment-floor-plan-conversion.md` — walls, doors, windows, proposed changes
|
||||
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Concept Diagram</title>
|
||||
<style>
|
||||
:root {
|
||||
--text-primary: #1a1a18;
|
||||
--text-secondary: #5f5e5a;
|
||||
--text-tertiary: #88877f;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f6f5f0;
|
||||
--bg-tertiary: #eeedeb;
|
||||
--border: rgba(0,0,0,0.15);
|
||||
--border-hover: rgba(0,0,0,0.3);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-primary: #e8e6de;
|
||||
--text-secondary: #b4b2a9;
|
||||
--text-tertiary: #888780;
|
||||
--bg-primary: #1a1a18;
|
||||
--bg-secondary: #2c2c2a;
|
||||
--bg-tertiary: #3d3d3a;
|
||||
--border: rgba(255,255,255,0.15);
|
||||
--border-hover: rgba(255,255,255,0.3);
|
||||
}
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
max-width: 780px;
|
||||
width: 100%;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||
}
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
svg { width: 100%; height: auto; }
|
||||
|
||||
/* === SVG Design System Classes === */
|
||||
|
||||
/* Text classes */
|
||||
.t { font-family: system-ui, -apple-system, sans-serif; font-size: 14px; fill: var(--text-primary); }
|
||||
.ts { font-family: system-ui, -apple-system, sans-serif; font-size: 12px; fill: var(--text-secondary); }
|
||||
.th { font-family: system-ui, -apple-system, sans-serif; font-size: 14px; fill: var(--text-primary); font-weight: 500; }
|
||||
|
||||
/* Neutral box */
|
||||
.box { fill: var(--bg-secondary); stroke: var(--border); stroke-width: 0.5px; }
|
||||
|
||||
/* Arrow */
|
||||
.arr { stroke: var(--text-secondary); stroke-width: 1.5px; fill: none; }
|
||||
|
||||
/* Leader line */
|
||||
.leader { stroke: var(--text-tertiary); stroke-width: 0.5px; stroke-dasharray: 4 3; fill: none; }
|
||||
|
||||
/* Clickable node */
|
||||
.node { cursor: pointer; transition: opacity 0.15s; }
|
||||
.node:hover { opacity: 0.82; }
|
||||
|
||||
/* === Color Ramp Classes (light mode) === */
|
||||
.c-purple > rect, .c-purple > circle, .c-purple > ellipse { fill: #EEEDFE; stroke: #534AB7; }
|
||||
.c-purple > .th, .c-purple > text.th { fill: #3C3489; }
|
||||
.c-purple > .ts, .c-purple > text.ts { fill: #534AB7; }
|
||||
.c-purple > .t, .c-purple > text.t { fill: #3C3489; }
|
||||
|
||||
.c-teal > rect, .c-teal > circle, .c-teal > ellipse { fill: #E1F5EE; stroke: #0F6E56; }
|
||||
.c-teal > .th, .c-teal > text.th { fill: #085041; }
|
||||
.c-teal > .ts, .c-teal > text.ts { fill: #0F6E56; }
|
||||
.c-teal > .t, .c-teal > text.t { fill: #085041; }
|
||||
|
||||
.c-coral > rect, .c-coral > circle, .c-coral > ellipse { fill: #FAECE7; stroke: #993C1D; }
|
||||
.c-coral > .th, .c-coral > text.th { fill: #712B13; }
|
||||
.c-coral > .ts, .c-coral > text.ts { fill: #993C1D; }
|
||||
.c-coral > .t, .c-coral > text.t { fill: #712B13; }
|
||||
|
||||
.c-pink > rect, .c-pink > circle, .c-pink > ellipse { fill: #FBEAF0; stroke: #993556; }
|
||||
.c-pink > .th, .c-pink > text.th { fill: #72243E; }
|
||||
.c-pink > .ts, .c-pink > text.ts { fill: #993556; }
|
||||
.c-pink > .t, .c-pink > text.t { fill: #72243E; }
|
||||
|
||||
.c-gray > rect, .c-gray > circle, .c-gray > ellipse { fill: #F1EFE8; stroke: #5F5E5A; }
|
||||
.c-gray > .th, .c-gray > text.th { fill: #444441; }
|
||||
.c-gray > .ts, .c-gray > text.ts { fill: #5F5E5A; }
|
||||
.c-gray > .t, .c-gray > text.t { fill: #444441; }
|
||||
|
||||
.c-blue > rect, .c-blue > circle, .c-blue > ellipse { fill: #E6F1FB; stroke: #185FA5; }
|
||||
.c-blue > .th, .c-blue > text.th { fill: #0C447C; }
|
||||
.c-blue > .ts, .c-blue > text.ts { fill: #185FA5; }
|
||||
.c-blue > .t, .c-blue > text.t { fill: #0C447C; }
|
||||
|
||||
.c-green > rect, .c-green > circle, .c-green > ellipse { fill: #EAF3DE; stroke: #3B6D11; }
|
||||
.c-green > .th, .c-green > text.th { fill: #27500A; }
|
||||
.c-green > .ts, .c-green > text.ts { fill: #3B6D11; }
|
||||
.c-green > .t, .c-green > text.t { fill: #27500A; }
|
||||
|
||||
.c-amber > rect, .c-amber > circle, .c-amber > ellipse { fill: #FAEEDA; stroke: #854F0B; }
|
||||
.c-amber > .th, .c-amber > text.th { fill: #633806; }
|
||||
.c-amber > .ts, .c-amber > text.ts { fill: #854F0B; }
|
||||
.c-amber > .t, .c-amber > text.t { fill: #633806; }
|
||||
|
||||
.c-red > rect, .c-red > circle, .c-red > ellipse { fill: #FCEBEB; stroke: #A32D2D; }
|
||||
.c-red > .th, .c-red > text.th { fill: #791F1F; }
|
||||
.c-red > .ts, .c-red > text.ts { fill: #A32D2D; }
|
||||
.c-red > .t, .c-red > text.t { fill: #791F1F; }
|
||||
|
||||
/* === Dark mode overrides === */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.c-purple > rect, .c-purple > circle, .c-purple > ellipse { fill: #3C3489; stroke: #AFA9EC; }
|
||||
.c-purple > .th, .c-purple > text.th { fill: #CECBF6; }
|
||||
.c-purple > .ts, .c-purple > text.ts { fill: #AFA9EC; }
|
||||
|
||||
.c-teal > rect, .c-teal > circle, .c-teal > ellipse { fill: #085041; stroke: #5DCAA5; }
|
||||
.c-teal > .th, .c-teal > text.th { fill: #9FE1CB; }
|
||||
.c-teal > .ts, .c-teal > text.ts { fill: #5DCAA5; }
|
||||
|
||||
.c-coral > rect, .c-coral > circle, .c-coral > ellipse { fill: #712B13; stroke: #F0997B; }
|
||||
.c-coral > .th, .c-coral > text.th { fill: #F5C4B3; }
|
||||
.c-coral > .ts, .c-coral > text.ts { fill: #F0997B; }
|
||||
|
||||
.c-pink > rect, .c-pink > circle, .c-pink > ellipse { fill: #72243E; stroke: #ED93B1; }
|
||||
.c-pink > .th, .c-pink > text.th { fill: #F4C0D1; }
|
||||
.c-pink > .ts, .c-pink > text.ts { fill: #ED93B1; }
|
||||
|
||||
.c-gray > rect, .c-gray > circle, .c-gray > ellipse { fill: #444441; stroke: #B4B2A9; }
|
||||
.c-gray > .th, .c-gray > text.th { fill: #D3D1C7; }
|
||||
.c-gray > .ts, .c-gray > text.ts { fill: #B4B2A9; }
|
||||
|
||||
.c-blue > rect, .c-blue > circle, .c-blue > ellipse { fill: #0C447C; stroke: #85B7EB; }
|
||||
.c-blue > .th, .c-blue > text.th { fill: #B5D4F4; }
|
||||
.c-blue > .ts, .c-blue > text.ts { fill: #85B7EB; }
|
||||
|
||||
.c-green > rect, .c-green > circle, .c-green > ellipse { fill: #27500A; stroke: #97C459; }
|
||||
.c-green > .th, .c-green > text.th { fill: #C0DD97; }
|
||||
.c-green > .ts, .c-green > text.ts { fill: #97C459; }
|
||||
|
||||
.c-amber > rect, .c-amber > circle, .c-amber > ellipse { fill: #633806; stroke: #EF9F27; }
|
||||
.c-amber > .th, .c-amber > text.th { fill: #FAC775; }
|
||||
.c-amber > .ts, .c-amber > text.ts { fill: #EF9F27; }
|
||||
|
||||
.c-red > rect, .c-red > circle, .c-red > ellipse { fill: #791F1F; stroke: #F09595; }
|
||||
.c-red > .th, .c-red > text.th { fill: #F7C1C1; }
|
||||
.c-red > .ts, .c-red > text.ts { fill: #F09595; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1><!-- DIAGRAM TITLE HERE --></h1>
|
||||
<p class="subtitle"><!-- OPTIONAL SUBTITLE HERE --></p>
|
||||
<!-- PASTE SVG HERE -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
name: hyperframes
|
||||
description: Create HTML-based video compositions, animated title cards, social overlays, captioned talking-head videos, audio-reactive visuals, and shader transitions using HyperFrames. HTML is the source of truth for video. Use when the user wants a rendered MP4/WebM from an HTML composition, wants to animate text/logos/charts over media, needs captions synced to audio, wants TTS narration, or wants to convert a website into a video.
|
||||
version: 1.0.0
|
||||
author: heygen-com
|
||||
license: Apache-2.0
|
||||
platforms: [linux, macos, windows]
|
||||
prerequisites:
|
||||
commands: [node, ffmpeg, npx]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [creative, video, animation, html, gsap, motion-graphics]
|
||||
related_skills: [manim-video, meme-generation]
|
||||
category: creative
|
||||
requires_toolsets: [terminal]
|
||||
---
|
||||
|
||||
# HyperFrames
|
||||
|
||||
HTML is the source of truth for video. A composition is an HTML file with `data-*` attributes for timing, a GSAP timeline for animation, and CSS for appearance. The HyperFrames engine captures the page frame-by-frame and encodes to MP4/WebM with FFmpeg.
|
||||
|
||||
**Complement to `manim-video`:** Use `manim-video` for mathematical/geometric explainers (equations, 3B1B-style). Use `hyperframes` for motion-graphics, talking-head with captions, product tours, social overlays, shader transitions, and anything driven by real video/audio media.
|
||||
|
||||
## When to Use
|
||||
|
||||
- User asks for a rendered video from text, a script, or a website
|
||||
- Animated title cards, lower thirds, or typographic intros
|
||||
- Captioned narration video (TTS + captions synced to waveform)
|
||||
- Audio-reactive visuals (beat sync, spectrum bars, pulsing glow)
|
||||
- Scene-to-scene transitions (crossfade, wipe, shader warp, flash-through-white)
|
||||
- Social overlays (Instagram/TikTok/YouTube style)
|
||||
- Website-to-video pipeline (capture a URL, produce a promo)
|
||||
- Any HTML/CSS/JS animation that must render deterministically to a video file
|
||||
|
||||
Do **not** use this skill for:
|
||||
- Pure math/equation animation (→ `manim-video`)
|
||||
- Image generation or memes (→ `meme-generation`, image models)
|
||||
- Live video conferencing or streaming
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
npx hyperframes init my-video # scaffold a project
|
||||
cd my-video
|
||||
npx hyperframes lint # validate before preview/render
|
||||
npx hyperframes preview # live-reload browser preview (port 3002)
|
||||
npx hyperframes render --output final.mp4 # render to MP4
|
||||
npx hyperframes doctor # diagnose environment issues
|
||||
```
|
||||
|
||||
Render flags: `--quality draft|standard|high` · `--fps 24|30|60` · `--format mp4|webm` · `--docker` (reproducible) · `--strict`.
|
||||
|
||||
Full CLI reference: [references/cli.md](references/cli.md).
|
||||
|
||||
## Setup (one-time)
|
||||
|
||||
```bash
|
||||
bash "$(dirname "$(find ~/.hermes/skills -path '*/hyperframes/SKILL.md' 2>/dev/null | head -1)")/scripts/setup.sh"
|
||||
```
|
||||
|
||||
The script:
|
||||
1. Verifies Node.js >= 22 and FFmpeg are installed (prints fix instructions if not).
|
||||
2. Installs the `hyperframes` CLI globally (`npm install -g hyperframes@>=0.4.2`).
|
||||
3. Pre-caches `chrome-headless-shell` via Puppeteer — **required** for best-quality rendering via Chrome's `HeadlessExperimental.beginFrame` capture path.
|
||||
4. Runs `npx hyperframes doctor` and reports the result.
|
||||
|
||||
See [references/troubleshooting.md](references/troubleshooting.md) if setup fails.
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Plan before writing HTML
|
||||
|
||||
Before touching code, articulate at a high level:
|
||||
- **What** — narrative arc, key moments, emotional beats
|
||||
- **Structure** — compositions, tracks (video/audio/overlays), durations
|
||||
- **Visual identity** — colors, fonts, motion character (explosive / cinematic / fluid / technical)
|
||||
- **Hero frame** — for each scene, the moment when the most elements are simultaneously visible. This is the static layout you'll build first.
|
||||
|
||||
**Visual Identity Gate (HARD-GATE).** Before writing ANY composition HTML, a visual identity must be defined. Do NOT write compositions with default or generic colors (`#333`, `#3b82f6`, `Roboto` are tells that this step was skipped). Check in order:
|
||||
|
||||
1. **`DESIGN.md` at project root?** → Use its exact colors, fonts, motion rules, and "What NOT to Do" constraints.
|
||||
2. **User named a style** (e.g. "Swiss Pulse", "dark and techy", "luxury brand")? → Generate a minimal `DESIGN.md` with `## Style Prompt`, `## Colors` (3-5 hex with roles), `## Typography` (1-2 families), `## What NOT to Do` (3-5 anti-patterns).
|
||||
3. **None of the above?** → Ask 3 questions before writing any HTML:
|
||||
- Mood? (explosive / cinematic / fluid / technical / chaotic / warm)
|
||||
- Light or dark canvas?
|
||||
- Any brand colors, fonts, or visual references?
|
||||
|
||||
Then generate a `DESIGN.md` from the answers. Every composition must trace its palette and typography back to `DESIGN.md` or explicit user direction.
|
||||
|
||||
### 2. Scaffold
|
||||
|
||||
```bash
|
||||
npx hyperframes init my-video --non-interactive
|
||||
```
|
||||
|
||||
Templates: `blank`, `warm-grain`, `play-mode`, `swiss-grid`, `vignelli`, `decision-tree`, `kinetic-type`, `product-promo`, `nyt-graph`. Pass `--example <name>` to pick one, `--video clip.mp4` or `--audio track.mp3` to seed with media.
|
||||
|
||||
### 3. Layout before animation
|
||||
|
||||
Write the static HTML+CSS for the **hero frame first** — no GSAP yet. The `.scene-content` container must fill the scene (`width:100%; height:100%; padding:Npx`) with `display:flex` + `gap`. Use padding to push content inward — never `position: absolute; top: Npx` on a content container (content overflows when taller than the remaining space).
|
||||
|
||||
Only after the hero frame looks right, add `gsap.from()` entrances (animate **to** the CSS position) and `gsap.to()` exits (animate **from** it).
|
||||
|
||||
See [references/composition.md](references/composition.md) for the full data-attribute schema and composition rules.
|
||||
|
||||
### 4. Animate with GSAP
|
||||
|
||||
Every composition must:
|
||||
- Register its timeline: `window.__timelines["<composition-id>"] = tl`
|
||||
- Start paused: `gsap.timeline({ paused: true })` — the player controls playback
|
||||
- Use finite `repeat` values (no `repeat: -1` — breaks the capture engine). Calculate: `repeat: Math.ceil(duration / cycleDuration) - 1`.
|
||||
- Be deterministic — no `Math.random()`, `Date.now()`, or wall-clock logic. Use a seeded PRNG if you need pseudo-randomness.
|
||||
- Build synchronously — no `async`/`await`, `setTimeout`, or Promises around timeline construction.
|
||||
|
||||
See [references/gsap.md](references/gsap.md) for the core GSAP API (tweens, eases, stagger, timelines).
|
||||
|
||||
### 5. Transitions between scenes
|
||||
|
||||
Multi-scene compositions require transitions. Rules:
|
||||
1. **Always use a transition between scenes** — no jump cuts.
|
||||
2. **Always use entrance animations** on every scene element (`gsap.from(...)`).
|
||||
3. **Never use exit animations** except on the final scene — the transition IS the exit.
|
||||
4. The final scene may fade out.
|
||||
|
||||
Use `npx hyperframes add <transition-name>` to install shader transitions (`flash-through-white`, `liquid-wipe`, etc.). Full list: `npx hyperframes add --list`.
|
||||
|
||||
### 6. Audio, captions, TTS, audio-reactive, highlighting
|
||||
|
||||
- **Audio:** always a separate `<audio>` element (video is `muted playsinline`).
|
||||
- **TTS:** `npx hyperframes tts "Script text" --voice af_nova --output narration.wav`. List voices with `--list`. Voice ID first letter encodes language (`a`/`b`=English, `e`=Spanish, `f`=French, `j`=Japanese, `z`=Mandarin, etc.) — the CLI auto-infers the phonemizer locale; pass `--lang` only to override. Non-English phonemization requires `espeak-ng` installed system-wide.
|
||||
- **Captions:** `npx hyperframes transcribe narration.wav` → word-level transcript. Pick style from the transcript tone (hype / corporate / tutorial / storytelling / social — see the table in `references/features.md`). **Language rule:** never use `.en` whisper models unless the audio is confirmed English — `.en` translates non-English audio instead of transcribing it. Every caption group MUST have a hard `tl.set(el, { opacity: 0, visibility: "hidden" }, group.end)` kill after its exit tween — otherwise groups leak visible into later ones.
|
||||
- **Audio-reactive visuals:** pre-extract audio bands (bass / mid / treble) and sample per-frame inside the timeline with a `for` loop of `tl.call(draw, [], f / fps)` — a single long tween does NOT react to audio. Map bass → `scale` (pulse), treble → `textShadow`/`boxShadow` (glow), overall amplitude → `opacity`/`y`/`backgroundColor`. Avoid equalizer-bar clichés — let content guide the visual, audio drive its behavior.
|
||||
- **Marker-style highlighting:** highlight, circle, burst, scribble, sketchout effects for text emphasis are deterministic CSS+GSAP — see `references/features.md#marker-highlighting`. Fully seekable, no animated SVG filters.
|
||||
- **Scene transitions:** every multi-scene composition MUST use transitions (no jump cuts). Pick from CSS primitives (push slide, blur crossfade, zoom through, staggered blocks) or shader transitions (`flash-through-white`, `liquid-wipe`, `cross-warp-morph`, `chromatic-split`, etc.) via `npx hyperframes add`. Mood and energy tables live in `references/features.md#transitions`. Do not mix CSS and shader transitions in the same composition.
|
||||
|
||||
### 7. Lint, validate, inspect, preview, render
|
||||
|
||||
```bash
|
||||
npx hyperframes lint # catches missing data-composition-id, overlapping tracks, unregistered timelines
|
||||
npx hyperframes validate # WCAG contrast audit at 5 timestamps
|
||||
npx hyperframes inspect # visual layout audit — overflow, off-frame elements, occluded text
|
||||
npx hyperframes preview # live browser preview
|
||||
npx hyperframes render --quality draft --output draft.mp4 # fast iteration
|
||||
npx hyperframes render --quality high --output final.mp4 # final delivery
|
||||
```
|
||||
|
||||
`hyperframes validate` samples background pixels behind every text element and warns on contrast ratios below 4.5:1 (or 3:1 for large text). `hyperframes inspect` is the layout-side companion — runs the page at multiple timestamps and flags issues that a static lint can't see (a caption that wraps past the safe area only at 4.5s, a card that overflows when its title is the longest variant, an element that ends up behind a transition shader). Run `inspect` especially on compositions with speech bubbles, cards, captions, or tight typography.
|
||||
|
||||
### 8. Website-to-video (if the user gives a URL)
|
||||
|
||||
Use the 7-step capture-to-video workflow in [references/website-to-video.md](references/website-to-video.md): capture → DESIGN.md → SCRIPT.md → storyboard → composition → render → deliver.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **`HeadlessExperimental.beginFrame' wasn't found`** — Chromium 147+ removed this protocol. Ensure you're on `hyperframes@>=0.4.2` (auto-detects and falls back to screenshot mode). Escape hatch: `export PRODUCER_FORCE_SCREENSHOT=true`. See [hyperframes#294](https://github.com/heygen-com/hyperframes/issues/294) and [references/troubleshooting.md](references/troubleshooting.md).
|
||||
- **System Chrome (not `chrome-headless-shell`)** — renders hang for 120s then timeout. Run `npx puppeteer browsers install chrome-headless-shell` (setup.sh does this). `hyperframes doctor` reports which binary will be used.
|
||||
- **`repeat: -1` anywhere** — breaks the capture engine. Always compute a finite repeat count.
|
||||
- **`gsap.set()` on clip elements that enter later** — the element doesn't exist at page load. Use `tl.set(selector, vars, timePosition)` inside the timeline instead, at or after the clip's `data-start`.
|
||||
- **`<br>` inside content text** — forced breaks don't know the rendered font width, so natural wrap + `<br>` double-breaks. Use `max-width` to let text wrap. Exception: short display titles where each word is deliberately on its own line.
|
||||
- **Animating `visibility` or `display`** — GSAP can't tween these. Use `autoAlpha` (handles both visibility and opacity).
|
||||
- **Calling `video.play()` or `audio.play()`** — the framework owns playback. Never call these yourself.
|
||||
- **Building timelines async** — the capture engine reads `window.__timelines` synchronously after page load. Never wrap timeline construction in `async`, `setTimeout`, or a Promise.
|
||||
- **Standalone `index.html` wrapped in `<template>`** — hides all content from the browser. Only **sub-compositions** loaded via `data-composition-src` use `<template>`.
|
||||
- **Using video for audio** — always muted `<video>` + separate `<audio>`.
|
||||
|
||||
## Verification
|
||||
|
||||
Before and after rendering:
|
||||
|
||||
1. **Lint + validate + inspect pass:** `npx hyperframes lint --strict && npx hyperframes validate && npx hyperframes inspect` (lint catches structural issues, validate catches contrast, inspect catches visual layout / overflow issues — see troubleshooting.md if warnings appear).
|
||||
2. **Animation choreography** — for new compositions or significant animation changes, run the animation map. `npx hyperframes init` copies the skill scripts into the project, so the path is project-local:
|
||||
```bash
|
||||
node skills/hyperframes/scripts/animation-map.mjs <composition-dir> \
|
||||
--out <composition-dir>/.hyperframes/anim-map
|
||||
```
|
||||
Outputs a single `animation-map.json` with per-tween summaries, ASCII Gantt timeline, stagger detection, dead zones (>1s with no animation), element lifecycles, and flags (`offscreen`, `collision`, `invisible`, `paced-fast` <0.2s, `paced-slow` >2s). Scan summaries and flags — fix or justify each. Skip on small edits.
|
||||
3. **File exists + non-zero:** `ls -lh final.mp4`.
|
||||
4. **Duration matches `data-duration`:** `ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 final.mp4`.
|
||||
5. **Visual check:** extract a mid-composition frame: `ffmpeg -i final.mp4 -ss 00:00:05 -vframes 1 preview.png`.
|
||||
6. **Audio present if expected:** `ffprobe -v error -show_streams -select_streams a -of default=nw=1:nk=1 final.mp4 | head -1`.
|
||||
|
||||
If `hyperframes render` fails, run `npx hyperframes doctor` and attach its output when reporting.
|
||||
|
||||
## References
|
||||
|
||||
- [composition.md](references/composition.md) — data attributes, timeline contract, non-negotiable rules, typography/asset rules
|
||||
- [cli.md](references/cli.md) — every CLI command (init, capture, lint, validate, inspect, preview, render, transcribe, tts, doctor, browser, info, upgrade, benchmark)
|
||||
- [gsap.md](references/gsap.md) — GSAP core API for HyperFrames (tweens, eases, stagger, timelines, matchMedia)
|
||||
- [features.md](references/features.md) — captions, TTS, audio-reactive, marker highlighting, transitions (load on demand)
|
||||
- [website-to-video.md](references/website-to-video.md) — 7-step capture-to-video workflow
|
||||
- [troubleshooting.md](references/troubleshooting.md) — OpenClaw fix, env vars, common render errors
|
||||
@@ -0,0 +1,185 @@
|
||||
# HyperFrames CLI
|
||||
|
||||
Everything runs through `npx hyperframes` (or the globally-installed `hyperframes` after `npm install -g hyperframes`). Requires Node.js >= 22 and FFmpeg.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Scaffold** — `npx hyperframes init my-video` (or `npx hyperframes capture <url>` if starting from a website)
|
||||
2. **Write** — author HTML composition (see `composition.md`)
|
||||
3. **Lint** — `npx hyperframes lint`
|
||||
4. **Validate** — `npx hyperframes validate` (WCAG contrast audit)
|
||||
5. **Inspect** — `npx hyperframes inspect` (visual layout audit)
|
||||
6. **Preview** — `npx hyperframes preview`
|
||||
7. **Render** — `npx hyperframes render`
|
||||
|
||||
Always lint before preview/render — catches missing `data-composition-id`, overlapping tracks, and unregistered timelines.
|
||||
|
||||
## init — Scaffold a Project
|
||||
|
||||
```bash
|
||||
npx hyperframes init my-video # interactive wizard
|
||||
npx hyperframes init my-video --example warm-grain # pick an example template
|
||||
npx hyperframes init my-video --video clip.mp4 # seed with a video file
|
||||
npx hyperframes init my-video --audio track.mp3 # seed with an audio file
|
||||
npx hyperframes init my-video --non-interactive # skip prompts (CI / agent use)
|
||||
```
|
||||
|
||||
Templates: `blank`, `warm-grain`, `play-mode`, `swiss-grid`, `vignelli`, `decision-tree`, `kinetic-type`, `product-promo`, `nyt-graph`.
|
||||
|
||||
`init` creates the correct file structure, copies media, transcribes audio with Whisper, and installs authoring skills. Use it instead of creating files by hand.
|
||||
|
||||
## capture — Website → Editable Components
|
||||
|
||||
```bash
|
||||
npx hyperframes capture https://example.com # → captures/example.com/
|
||||
npx hyperframes capture https://stripe.com -o stripe-video # custom output dir
|
||||
npx hyperframes capture https://example.com --json # machine-readable output
|
||||
npx hyperframes capture https://example.com --skip-assets # skip images/SVGs
|
||||
```
|
||||
|
||||
Captures the site into `captures/<hostname>/capture/` by default, producing `capture/screenshots/`, `capture/assets/`, `capture/extracted/` (tokens.json, visible-text.txt, fonts.json), and a self-contained snapshot.
|
||||
|
||||
All downstream steps (DESIGN.md, SCRIPT.md, STORYBOARD, composition) read from the `capture/` subfolder — see `website-to-video.md`.
|
||||
|
||||
## lint
|
||||
|
||||
```bash
|
||||
npx hyperframes lint # current directory
|
||||
npx hyperframes lint ./my-project # specific project
|
||||
npx hyperframes lint --verbose # include info-level findings
|
||||
npx hyperframes lint --json # machine-readable output
|
||||
```
|
||||
|
||||
Lints `index.html` and all files in `compositions/`. Reports errors (must fix), warnings (should fix), and info (only with `--verbose`).
|
||||
|
||||
## validate
|
||||
|
||||
```bash
|
||||
npx hyperframes validate # WCAG contrast audit at 5 timestamps
|
||||
npx hyperframes validate --no-contrast # skip while iterating
|
||||
```
|
||||
|
||||
Seeks to 5 timestamps, screenshots the page, samples background pixels behind every text element, and warns on contrast ratios below 4.5:1 (normal text) or 3:1 (large text — 24px+, or 19px+ bold). Run before final render.
|
||||
|
||||
## inspect
|
||||
|
||||
```bash
|
||||
npx hyperframes inspect # visual layout audit at 5 timestamps
|
||||
npx hyperframes inspect ./my-project # specific project
|
||||
npx hyperframes inspect --json # agent-readable findings
|
||||
npx hyperframes inspect --samples 15 # denser timeline sweep
|
||||
npx hyperframes inspect --at 1.5,4,7.25 # explicit hero-frame timestamps
|
||||
```
|
||||
|
||||
Use this after `lint` and `validate`, especially for compositions with speech bubbles, cards, captions, or tight typography. Reports overflow, off-frame elements, occluded text, contrast warnings, and per-timestamp layout summaries — catches issues that pure timeline lint can't see (e.g., a caption that wraps past the safe area only at a specific timestamp).
|
||||
|
||||
`npx hyperframes layout` is a compatibility alias for the same visual inspection pass.
|
||||
|
||||
## preview
|
||||
|
||||
```bash
|
||||
npx hyperframes preview # serve current directory (port 3002)
|
||||
npx hyperframes preview --port 4567 # custom port
|
||||
```
|
||||
|
||||
Hot-reloads on file changes. Opens the Studio in your browser automatically.
|
||||
|
||||
## render
|
||||
|
||||
```bash
|
||||
npx hyperframes render # standard MP4
|
||||
npx hyperframes render --output final.mp4 # named output
|
||||
npx hyperframes render --quality draft # fast iteration
|
||||
npx hyperframes render --fps 60 --quality high # final delivery
|
||||
npx hyperframes render --format webm # transparent WebM
|
||||
npx hyperframes render --docker # byte-identical reproducible render
|
||||
```
|
||||
|
||||
| Flag | Options | Default | Notes |
|
||||
| -------------- | ----------------------- | ------------------------------ | --------------------------- |
|
||||
| `--output` | path | `renders/<name>_<timestamp>.mp4` | Output path |
|
||||
| `--fps` | 24, 30, 60 | 30 | 60fps doubles render time |
|
||||
| `--quality` | `draft`, `standard`, `high` | standard | draft for iterating |
|
||||
| `--format` | `mp4`, `webm` | mp4 | WebM supports transparency |
|
||||
| `--workers` | 1–8 or `auto` | auto | Each spawns Chrome |
|
||||
| `--docker` | flag | off | Reproducible output |
|
||||
| `--gpu` | flag | off | GPU-accelerated encoding |
|
||||
| `--strict` | flag | off | Fail on lint errors |
|
||||
| `--strict-all` | flag | off | Fail on errors AND warnings |
|
||||
|
||||
**Quality guidance:** `draft` while iterating, `standard` for review, `high` for final delivery.
|
||||
|
||||
## transcribe
|
||||
|
||||
```bash
|
||||
npx hyperframes transcribe audio.mp3
|
||||
npx hyperframes transcribe video.mp4 --model medium.en --language en
|
||||
npx hyperframes transcribe subtitles.srt # import existing
|
||||
npx hyperframes transcribe subtitles.vtt
|
||||
npx hyperframes transcribe openai-response.json
|
||||
```
|
||||
|
||||
Produces word-level timings suitable for caption components. First run downloads the Whisper model (cached after).
|
||||
|
||||
## tts
|
||||
|
||||
```bash
|
||||
npx hyperframes tts "Text here" --voice af_nova --output narration.wav
|
||||
npx hyperframes tts script.txt --voice bf_emma
|
||||
npx hyperframes tts "La reunión empieza a las nueve" --voice ef_dora --output es.wav
|
||||
npx hyperframes tts "Hello there" --voice af_heart --lang fr-fr --output accented.wav
|
||||
npx hyperframes tts --list # show all voices
|
||||
```
|
||||
|
||||
Uses Kokoro (local, no API key). Voice ID first letter encodes language: `a` American English, `b` British English, `e` Spanish, `f` French, `h` Hindi, `i` Italian, `j` Japanese, `p` Brazilian Portuguese, `z` Mandarin. The CLI auto-infers the phonemizer locale from that prefix — pass `--lang` only to override (e.g. stylized accents). Valid `--lang` codes: `en-us`, `en-gb`, `es`, `fr-fr`, `hi`, `it`, `pt-br`, `ja`, `zh`. Non-English phonemization requires `espeak-ng` installed system-wide (`apt-get install espeak-ng` / `brew install espeak-ng`).
|
||||
|
||||
## doctor
|
||||
|
||||
```bash
|
||||
npx hyperframes doctor
|
||||
```
|
||||
|
||||
Verifies environment:
|
||||
- Node.js >= 22
|
||||
- FFmpeg present on PATH
|
||||
- Available RAM (renders are memory-hungry — 4 GB minimum)
|
||||
- Chrome binary resolution (`chrome-headless-shell` preferred over system Chrome)
|
||||
- Current `hyperframes` version
|
||||
|
||||
Run this **first** when a render fails. See `troubleshooting.md` for interpreting the output.
|
||||
|
||||
## browser
|
||||
|
||||
```bash
|
||||
npx hyperframes browser --install # install the bundled chrome-headless-shell
|
||||
npx hyperframes browser --path # print the resolved browser binary path
|
||||
npx hyperframes browser --clean # clear the bundled browser cache
|
||||
```
|
||||
|
||||
## info
|
||||
|
||||
```bash
|
||||
npx hyperframes info
|
||||
```
|
||||
|
||||
Prints version, Node version, FFmpeg version, OS, and resolved browser path — useful in bug reports.
|
||||
|
||||
## upgrade
|
||||
|
||||
```bash
|
||||
npx hyperframes upgrade -y
|
||||
```
|
||||
|
||||
Check for and install updates. Run this if you hit `HeadlessExperimental.beginFrame` errors — the auto-detect fix shipped in `hyperframes@0.4.2` (commit 4c72ba4, March 2026).
|
||||
|
||||
## Other
|
||||
|
||||
```bash
|
||||
npx hyperframes compositions # list compositions in the project
|
||||
npx hyperframes docs # open documentation in browser
|
||||
npx hyperframes benchmark . # benchmark render performance
|
||||
npx hyperframes add <block> # install a block/component from the catalog
|
||||
npx hyperframes add --list # browse the catalog
|
||||
```
|
||||
|
||||
Popular catalog blocks: `flash-through-white` (shader transition), `instagram-follow` (social overlay), `data-chart` (animated chart), `lower-third` (talking-head overlay). See [hyperframes.heygen.com/catalog](https://hyperframes.heygen.com/catalog).
|
||||
@@ -0,0 +1,129 @@
|
||||
# Composition Authoring
|
||||
|
||||
HTML structure, data attributes, timeline contract, and non-negotiable rules.
|
||||
|
||||
## Root Structure
|
||||
|
||||
Standalone `index.html` — the top-level composition. **Does NOT use `<template>`**. Put the `data-composition-id` div directly in `<body>`.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<div
|
||||
id="stage"
|
||||
data-composition-id="root"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<!-- clips go here -->
|
||||
<video id="clip-1" data-start="0" data-duration="5" data-track-index="0" src="intro.mp4" muted playsinline></video>
|
||||
<img id="logo" data-start="2" data-duration="3" data-track-index="1" src="logo.png" />
|
||||
<audio id="music" data-start="0" data-duration="10" data-track-index="2" data-volume="0.5" src="music.wav"></audio>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.from("#logo", { opacity: 0, y: 40, duration: 0.6 }, 2);
|
||||
window.__timelines["root"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Sub-compositions loaded via `data-composition-src` **DO** use `<template>`:
|
||||
|
||||
```html
|
||||
<template id="my-comp-template">
|
||||
<div data-composition-id="my-comp" data-width="1920" data-height="1080">
|
||||
<!-- content + scoped <style> + <script> with window.__timelines["my-comp"] -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Load from the root: `<div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>`
|
||||
|
||||
## Data Attributes
|
||||
|
||||
### All clips
|
||||
|
||||
| Attribute | Required | Values |
|
||||
| ------------------ | --------------------------------- | ------------------------------------------------------ |
|
||||
| `id` | Yes | Unique identifier |
|
||||
| `data-start` | Yes | Seconds, or clip ID reference (`"el-1"`, `"intro + 2"`) |
|
||||
| `data-duration` | Required for img/div/compositions | Seconds. Video/audio defaults to media duration. |
|
||||
| `data-track-index` | Yes | Integer. Same-track clips cannot overlap. |
|
||||
| `data-media-start` | No | Trim offset into source (seconds) |
|
||||
| `data-volume` | No | 0–1 (default 1) |
|
||||
|
||||
`data-track-index` controls timeline layout only — **not** visual layering. Use CSS `z-index` for layering.
|
||||
|
||||
### Composition clips
|
||||
|
||||
| Attribute | Required | Values |
|
||||
| ---------------------------- | -------- | -------------------------------------------- |
|
||||
| `data-composition-id` | Yes | Unique composition ID |
|
||||
| `data-start` | Yes | Start time (root composition: `"0"`) |
|
||||
| `data-duration` | Yes | Takes precedence over GSAP timeline duration |
|
||||
| `data-width` / `data-height` | Yes | Pixel dimensions (1920x1080 or 1080x1920) |
|
||||
| `data-composition-src` | No | Path to external HTML file |
|
||||
|
||||
## Timeline Contract
|
||||
|
||||
- Every timeline starts `{ paused: true }` — the player controls playback.
|
||||
- Register every timeline: `window.__timelines["<composition-id>"] = tl`.
|
||||
- Duration comes from `data-duration`, not from the GSAP timeline length.
|
||||
- Framework auto-nests sub-timelines — do NOT manually add them.
|
||||
- Never create empty tweens just to set duration.
|
||||
|
||||
## Non-Negotiable Rules
|
||||
|
||||
1. **Deterministic.** No `Math.random()`, `Date.now()`, or time-based logic. Use a seeded PRNG (e.g. mulberry32) if you need pseudo-randomness.
|
||||
2. **GSAP only on visual properties.** `opacity`, `x`, `y`, `scale`, `rotation`, `color`, `backgroundColor`, `borderRadius`, transforms. Never animate `visibility`, `display`, or call `video.play()`/`audio.play()`.
|
||||
3. **No property conflicts across timelines.** Never animate the same property on the same element from multiple timelines simultaneously.
|
||||
4. **No `repeat: -1`.** Infinite-repeat tweens break the capture engine. Compute `repeat: Math.ceil(duration / cycleDuration) - 1`.
|
||||
5. **Synchronous timeline construction.** Never build timelines inside `async`/`await`, `setTimeout`, or Promises. The capture engine reads `window.__timelines` synchronously after page load. Fonts are embedded by the compiler — no need to wait for load.
|
||||
6. **Root composition has no `<template>` wrapper.** Only sub-compositions use `<template>`.
|
||||
7. **Video is always `muted playsinline`.** Audio is always a separate `<audio>` element — even if it's the same source file.
|
||||
8. **Content containers use padding, not absolute positioning.** `.scene-content { width: 100%; height: 100%; padding: Npx; display: flex; flex-direction: column; gap: Npx; box-sizing: border-box }`. Absolute-positioned content containers overflow. Reserve `position: absolute` for decoratives only.
|
||||
|
||||
## Scene Transitions
|
||||
|
||||
Multi-scene compositions MUST follow all of these:
|
||||
|
||||
1. **Always use a transition between scenes.** No jump cuts.
|
||||
2. **Always use entrance animations** on every scene element. Every element animates IN via `gsap.from(...)`. No element may appear fully-formed.
|
||||
3. **Never use exit animations** (except on the final scene). This means NO `gsap.to()` that animates `opacity` to 0, `y` offscreen, etc. The transition IS the exit. Outgoing scene content must be fully visible at the moment the transition starts.
|
||||
4. **Final scene only:** may fade elements out. This is the only scene where `gsap.to(..., { opacity: 0 })` is allowed.
|
||||
|
||||
## Typography and Assets
|
||||
|
||||
- **Fonts:** write the `font-family` you want in CSS — the compiler embeds supported fonts automatically. Unsupported fonts produce a compiler warning.
|
||||
- Add `crossorigin="anonymous"` to external media.
|
||||
- For dynamic text sizing, use `window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })`.
|
||||
- All project files live at the project root alongside `index.html`. Sub-compositions reference assets with `../`.
|
||||
- For rendered video: 60px+ headlines, 20px+ body, 16px+ data labels. `font-variant-numeric: tabular-nums` on number columns. Avoid full-screen linear gradients on dark backgrounds (H.264 banding — use radial or solid + localized glow).
|
||||
|
||||
## Animation Guardrails
|
||||
|
||||
- Offset the first animation 0.1–0.3s (not `t=0`).
|
||||
- Vary eases across entrance tweens — at least 3 different eases per scene.
|
||||
- Don't repeat an entrance pattern within a scene.
|
||||
|
||||
## Never Do
|
||||
|
||||
1. Forget `window.__timelines` registration.
|
||||
2. Use video for audio — always muted video + separate `<audio>`.
|
||||
3. Nest video inside a timed div — use a non-timed wrapper.
|
||||
4. Use `data-layer` (use `data-track-index`) or `data-end` (use `data-duration`).
|
||||
5. Animate video element dimensions — animate a wrapper div instead.
|
||||
6. Call `play`/`pause`/`seek` on media — framework owns playback.
|
||||
7. Create a top-level container without `data-composition-id`.
|
||||
8. Use `repeat: -1` on any timeline or tween.
|
||||
9. Build timelines asynchronously.
|
||||
10. Use `gsap.set()` on elements from later scenes — they don't exist in the DOM at page load. Use `tl.set(selector, vars, timePosition)` inside the timeline at or after the clip's `data-start`.
|
||||
11. Use `<br>` in content text — causes unwanted extra breaks when the text wraps naturally. Use `max-width` instead. Exception: short display titles (e.g., "THE\nIMMORTAL\nGAME") where each word is deliberately on its own line.
|
||||
@@ -0,0 +1,289 @@
|
||||
# HyperFrames Feature Reference
|
||||
|
||||
Load this file when a composition needs captions, TTS narration, audio-reactive visuals, marker-style text highlighting, or scene transitions. All patterns here are deterministic (no `Math.random()`, no `Date.now()`, no runtime audio analysis) and live on the same GSAP timeline as the rest of the composition.
|
||||
|
||||
## Captions
|
||||
|
||||
### Language Rule (Non-Negotiable)
|
||||
|
||||
**Never use `.en` whisper models unless the audio is confirmed English.** `.en` models TRANSLATE non-English audio into English instead of transcribing it.
|
||||
|
||||
- User says the language → `npx hyperframes transcribe audio.mp3 --model small --language <code>` (no `.en`)
|
||||
- User confirms English → `--model small.en`
|
||||
- Language unknown → `--model small` (auto-detects)
|
||||
|
||||
### Style Detection
|
||||
|
||||
If the user doesn't specify a caption style, detect it from the transcript tone:
|
||||
|
||||
| Tone | Font mood | Animation | Color | Size |
|
||||
| ------------ | ------------------------ | ---------------------------------- | --------------------------- | ------- |
|
||||
| Hype / launch | Heavy condensed, 800-900 | Scale-pop, `back.out(1.7)`, 0.1-0.2s | Bright on dark | 72-96px |
|
||||
| Corporate | Clean sans, 600-700 | Fade+slide, `power3.out`, 0.3s | White / neutral + muted accent | 56-72px |
|
||||
| Tutorial | Mono / clean sans, 500-600 | Typewriter or fade, 0.4-0.5s | High contrast, minimal | 48-64px |
|
||||
| Storytelling | Serif / elegant, 400-500 | Slow fade, `power2.out`, 0.5-0.6s | Warm muted tones | 44-56px |
|
||||
| Social | Rounded sans, 700-800 | Bounce, `elastic.out`, word-by-word | Playful, colored pills | 56-80px |
|
||||
|
||||
### Word Grouping
|
||||
|
||||
- High energy: 2-3 words, quick turnover.
|
||||
- Conversational: 3-5 words, natural phrases.
|
||||
- Measured / calm: 4-6 words.
|
||||
|
||||
Break on sentence boundaries, 150ms+ pauses, or a max word count.
|
||||
|
||||
### Positioning
|
||||
|
||||
- Landscape (1920x1080): bottom 80-120px, centered.
|
||||
- Portrait (1080x1920): ~600-700px from bottom, centered.
|
||||
- Never cover the subject's face. `position: absolute` (never relative). One caption group visible at a time.
|
||||
|
||||
### Text Overflow Prevention
|
||||
|
||||
Use the runtime helper so captions never overflow:
|
||||
|
||||
```js
|
||||
const result = window.__hyperframes.fitTextFontSize(group.text.toUpperCase(), {
|
||||
fontFamily: "Outfit",
|
||||
fontWeight: 900,
|
||||
maxWidth: 1600, // 1600 landscape, 900 portrait
|
||||
});
|
||||
el.style.fontSize = result.fontSize + "px";
|
||||
```
|
||||
|
||||
When per-word styling uses `scale > 1.0`, compute `maxWidth = safeWidth / maxScale` to leave headroom. Container needs `overflow: visible` (not `hidden` — hidden clips scaled emphasis words and glow).
|
||||
|
||||
### Caption Exit Guarantee
|
||||
|
||||
Every group MUST have a hard kill after its exit tween — otherwise groups leak into later ones:
|
||||
|
||||
```js
|
||||
tl.to(groupEl, { opacity: 0, scale: 0.95, duration: 0.12, ease: "power2.in" }, group.end - 0.12);
|
||||
tl.set(groupEl, { opacity: 0, visibility: "hidden" }, group.end); // deterministic kill
|
||||
```
|
||||
|
||||
### Per-Word Styling
|
||||
|
||||
Scan the transcript for words that deserve distinct treatment:
|
||||
|
||||
- Brand / product names — larger, unique color.
|
||||
- ALL CAPS — scale boost, flash, accent color.
|
||||
- Numbers / statistics — bold weight, accent color.
|
||||
- Emotional keywords — exaggerated animation (overshoot, bounce).
|
||||
- Call-to-action — highlight, underline, color pop.
|
||||
|
||||
## TTS (Kokoro-82M)
|
||||
|
||||
Local, no API key. Runs on CPU. Model downloads on first use (~311 MB + ~27 MB voices, cached in `~/.cache/hyperframes/tts/`).
|
||||
|
||||
### Voice Selection
|
||||
|
||||
| Content type | Voice | Why |
|
||||
| ------------- | ----------------------- | --------------------------- |
|
||||
| Product demo | `af_heart` / `af_nova` | Warm, professional |
|
||||
| Tutorial | `am_adam` / `bf_emma` | Neutral, easy to follow |
|
||||
| Marketing | `af_sky` / `am_michael` | Energetic or authoritative |
|
||||
| Documentation | `bf_emma` / `bm_george` | Clear British English |
|
||||
| Casual | `af_heart` / `af_sky` | Approachable, natural |
|
||||
|
||||
Run `npx hyperframes tts --list` for all 54 voices across 8 languages.
|
||||
|
||||
### Multilingual Phonemization
|
||||
|
||||
Voice ID first letter encodes language: `a`=American English, `b`=British English, `e`=Spanish, `f`=French, `h`=Hindi, `i`=Italian, `j`=Japanese, `p`=Brazilian Portuguese, `z`=Mandarin. The CLI auto-infers the phonemizer locale from that prefix — you don't need `--lang` when voice and text match.
|
||||
|
||||
```bash
|
||||
npx hyperframes tts "La reunión empieza a las nueve" --voice ef_dora --output es.wav
|
||||
npx hyperframes tts "今日はいい天気ですね" --voice jf_alpha --output ja.wav
|
||||
```
|
||||
|
||||
Pass `--lang` only to override auto-detection (e.g. stylized accents):
|
||||
|
||||
```bash
|
||||
npx hyperframes tts "Hello there" --voice af_heart --lang fr-fr --output accented.wav
|
||||
```
|
||||
|
||||
Valid `--lang` codes: `en-us`, `en-gb`, `es`, `fr-fr`, `hi`, `it`, `pt-br`, `ja`, `zh`. Non-English phonemization requires `espeak-ng` installed system-wide (`apt-get install espeak-ng` / `brew install espeak-ng`).
|
||||
|
||||
### Speed
|
||||
|
||||
- `0.7-0.8` — tutorial, complex content
|
||||
- `1.0` — natural (default)
|
||||
- `1.1-1.2` — intros, upbeat content
|
||||
- `1.5+` — rarely appropriate
|
||||
|
||||
### TTS + Captions Workflow
|
||||
|
||||
```bash
|
||||
npx hyperframes tts script.txt --voice af_heart --output narration.wav
|
||||
npx hyperframes transcribe narration.wav # → transcript.json (word-level)
|
||||
```
|
||||
|
||||
## Audio-Reactive Visuals
|
||||
|
||||
Drive visuals from music, voice, or sound. Any GSAP-tweenable property can respond to pre-extracted audio data.
|
||||
|
||||
### Data format
|
||||
|
||||
```js
|
||||
const AUDIO_DATA = {
|
||||
fps: 30,
|
||||
totalFrames: 900,
|
||||
frames: [{ bands: [0.82, 0.45, 0.31, /* ... */] }, /* ... */],
|
||||
};
|
||||
```
|
||||
|
||||
`frames[i].bands[]` are frequency band amplitudes, 0-1. Index 0 = bass, higher indices = treble. Each band is normalized independently across the full track.
|
||||
|
||||
### Mapping audio to visuals
|
||||
|
||||
| Audio signal | Visual property | Effect |
|
||||
| ---------------------- | --------------------------------- | -------------------------- |
|
||||
| Bass (`bands[0]`) | `scale` | Pulse on beat |
|
||||
| Treble (`bands[12-14]`)| `textShadow`, `boxShadow` | Glow intensity |
|
||||
| Overall amplitude | `opacity`, `y`, `backgroundColor` | Breathe, lift, color shift |
|
||||
| Mid-range (`bands[4-8]`)| `borderRadius`, `width` | Shape morphing |
|
||||
|
||||
Any GSAP-tweenable property works — `clipPath`, `filter`, SVG attributes, CSS custom properties. Let content guide the visual and let audio drive its behavior. **Never add** equalizer bars, spectrum analyzers, waveform displays, rainbow cycling, or generic particle systems — they look cheap.
|
||||
|
||||
### Sampling pattern (required)
|
||||
|
||||
Audio reactivity needs per-frame sampling via a `for` loop of `tl.call()`, NOT a single tween. A single long tween does NOT react to audio:
|
||||
|
||||
```js
|
||||
for (let f = 0; f < AUDIO_DATA.totalFrames; f++) {
|
||||
tl.call(
|
||||
((frame) => () => draw(frame))(AUDIO_DATA.frames[f]),
|
||||
[],
|
||||
f / AUDIO_DATA.fps,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Gotchas
|
||||
|
||||
- **textShadow on a container** with semi-transparent children (e.g. inactive caption words at `rgba(255,255,255,0.3)`) renders a visible glow rectangle behind every child. Apply the glow to active words individually, not to the container.
|
||||
- **Subtlety for text** — 3-6% scale variation, soft glow. Heavy pulsing makes text unreadable.
|
||||
- **Go bigger on non-text** — backgrounds and shapes can handle 10-30% swings.
|
||||
- **Deterministic only** — pre-extracted audio data, no Web Audio API, no runtime analysis.
|
||||
|
||||
## Marker-Style Highlighting
|
||||
|
||||
Deterministic CSS + GSAP implementations of the classic "highlight / circle / burst / scribble / sketchout" drawing modes for emphasizing text. Fully seekable — no animated SVG filters, no JS timers.
|
||||
|
||||
### Highlight (yellow marker sweep)
|
||||
|
||||
```html
|
||||
<span class="mh-highlight-wrap">
|
||||
<span class="mh-highlight-bar" id="hl-1"></span>
|
||||
<span class="mh-highlight-text">highlighted text</span>
|
||||
</span>
|
||||
```
|
||||
|
||||
```css
|
||||
.mh-highlight-wrap { position: relative; display: inline; }
|
||||
.mh-highlight-bar {
|
||||
position: absolute; inset: 0 -6px;
|
||||
background: #fdd835; opacity: 0.35;
|
||||
transform: scaleX(0); transform-origin: left center;
|
||||
border-radius: 3px; z-index: 0;
|
||||
}
|
||||
.mh-highlight-text { position: relative; z-index: 1; }
|
||||
```
|
||||
|
||||
```js
|
||||
tl.to("#hl-1", { scaleX: 1, duration: 0.5, ease: "power2.out" }, 0.6);
|
||||
```
|
||||
|
||||
Multi-line: apply to `.mh-highlight-bar` with `stagger: 0.3`.
|
||||
|
||||
### Circle
|
||||
|
||||
Hand-drawn ellipse around a word. Use a positioned `::before` with `border-radius: 50%`, slight rotation, and `clip-path` to avoid covering the letters. Animate `clip-path` or `stroke-dashoffset` on an inline SVG circle.
|
||||
|
||||
### Burst
|
||||
|
||||
Short radiating lines around a word. Render 6-12 small `<span>` elements positioned in a radial pattern; animate `scaleY` from 0.
|
||||
|
||||
### Scribble
|
||||
|
||||
A chaotic overlay created by animating `stroke-dashoffset` on an inline SVG `<path>` with a `d` attribute describing a zig-zag. Seed values, never `Math.random()`.
|
||||
|
||||
### Sketchout
|
||||
|
||||
A rough rectangle outline. Two `<rect>`s with slight `transform` offsets, animated via `stroke-dashoffset`.
|
||||
|
||||
All five modes tween CSS transforms or `stroke-dashoffset` only — both tween cleanly, are deterministic, and seek correctly.
|
||||
|
||||
## Scene Transitions
|
||||
|
||||
Every multi-scene composition MUST use transitions. No jump cuts.
|
||||
|
||||
### Energy → primary transition
|
||||
|
||||
| Energy | CSS primary | Shader primary | Accent | Duration | Easing |
|
||||
| ------------------------------------ | ---------------------------- | ------------------------------------ | ------------------------------ | --------- | ------------------------ |
|
||||
| **Calm** (wellness, brand, luxury) | Blur crossfade, focus pull | Cross-warp morph, thermal distortion | Light leak, circle iris | 0.5-0.8s | `sine.inOut`, `power1` |
|
||||
| **Medium** (corporate, SaaS) | Push slide, staggered blocks | Whip pan, cinematic zoom | Squeeze, vertical push | 0.3-0.5s | `power2`, `power3` |
|
||||
| **High** (promos, sports, launch) | Zoom through, overexposure | Ridged burn, glitch, chromatic split | Staggered blocks, gravity drop | 0.15-0.3s | `power4`, `expo` |
|
||||
|
||||
Pick ONE primary (60-70% of scene changes) plus 1-2 accents. Never use a different transition for every scene.
|
||||
|
||||
### Mood → transition type
|
||||
|
||||
| Mood | Transitions |
|
||||
| ------------------------ | --------------------------------------------------------------------------- |
|
||||
| Warm / inviting | Light leak, blur crossfade, focus pull, film burn · _Shader:_ thermal distortion, cross-warp morph |
|
||||
| Cold / clinical | Squeeze, zoom out, blinds, shutter, grid dissolve · _Shader:_ gravitational lens |
|
||||
| Editorial / magazine | Push slide, vertical push, diagonal split, shutter · _Shader:_ whip pan |
|
||||
| Tech / futuristic | Grid dissolve, staggered blocks, blinds · _Shader:_ glitch, chromatic split |
|
||||
| Tense / edgy | Glitch, VHS, chromatic aberration, ripple · _Shader:_ ridged burn, domain warp |
|
||||
| Playful / fun | Elastic push, 3D flip, circle iris, morph circle · _Shader:_ swirl vortex, ripple waves |
|
||||
| Dramatic / cinematic | Zoom through, gravity drop, overexposure · _Shader:_ cinematic zoom, gravitational lens |
|
||||
| Premium / luxury | Focus pull, blur crossfade, color dip to black · _Shader:_ cross-warp morph |
|
||||
| Retro / analog | Film burn, light leak, VHS, clock wipe · _Shader:_ light leak |
|
||||
|
||||
### Presets
|
||||
|
||||
| Preset | Duration | Easing |
|
||||
| ---------- | -------- | ----------------- |
|
||||
| `snappy` | 0.2s | `power4.inOut` |
|
||||
| `smooth` | 0.4s | `power2.inOut` |
|
||||
| `gentle` | 0.6s | `sine.inOut` |
|
||||
| `dramatic` | 0.5s | `power3.in` → out |
|
||||
| `instant` | 0.15s | `expo.inOut` |
|
||||
| `luxe` | 0.7s | `power1.inOut` |
|
||||
|
||||
### Install a shader transition
|
||||
|
||||
```bash
|
||||
npx hyperframes add flash-through-white
|
||||
npx hyperframes add --list
|
||||
```
|
||||
|
||||
### CSS vs shader
|
||||
|
||||
- **CSS transitions** animate scene containers with opacity, transforms, `clip-path`, and filters. Simpler to set up.
|
||||
- **Shader transitions** composite both scene textures per-pixel on a WebGL canvas — can warp, dissolve, and morph in ways CSS cannot. Import from `@hyperframes/shader-transitions` instead of writing raw GLSL.
|
||||
|
||||
Don't mix CSS and shader transitions in the same composition — once a composition uses shader transitions, the WebGL canvas replaces DOM-based scene switching for every transition.
|
||||
|
||||
### Shader-compatible CSS rules
|
||||
|
||||
Shader transitions capture DOM scenes to WebGL textures via html2canvas. The canvas 2D pipeline doesn't match CSS exactly:
|
||||
|
||||
1. No `transparent` keyword in gradients — use the target color at zero alpha: `rgba(200,117,51,0)` not `transparent`. (Canvas interpolates `transparent` as `rgba(0,0,0,0)` creating dark fringes.)
|
||||
2. No gradient backgrounds on elements thinner than 4px. Use solid `background-color` on thin accent lines.
|
||||
3. No CSS variables (`var()`) on elements visible during capture — html2canvas doesn't reliably resolve custom properties. Use literal color values.
|
||||
4. Mark uncapturable decoratives with `data-no-capture` — they stay on the live DOM but are absent from the shader texture.
|
||||
5. No gradient opacity below 0.15 — renders differently in canvas vs CSS.
|
||||
6. Every `.scene` div must have explicit `background-color`, AND pass the same color as `bgColor` in the `init()` config. Without either, the texture renders as black.
|
||||
|
||||
These rules only apply to shader transition compositions. CSS-only compositions have no restrictions.
|
||||
|
||||
### Don't
|
||||
|
||||
- Mix CSS and shader transitions in one composition.
|
||||
- Use exit animations on any scene except the final scene — the transition IS the exit.
|
||||
- Introduce a new transition type every scene — pick one primary + 1-2 accents.
|
||||
- Use transitions that create visible geometric repetition (grids, hex cells, uniform dots) — they look artificial regardless of the math behind them. Prefer organic noise (FBM, domain warping).
|
||||
@@ -0,0 +1,136 @@
|
||||
# GSAP for HyperFrames
|
||||
|
||||
GSAP is the animation engine for all HyperFrames compositions. Load from CDN inside the composition:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
```
|
||||
|
||||
## Core Tween Methods
|
||||
|
||||
- **`gsap.to(targets, vars)`** — animate from current state to `vars`. Most common.
|
||||
- **`gsap.from(targets, vars)`** — animate from `vars` to current state (entrances).
|
||||
- **`gsap.fromTo(targets, fromVars, toVars)`** — explicit start and end.
|
||||
- **`gsap.set(targets, vars)`** — apply immediately (duration 0). Don't use on clip elements that enter later — use `tl.set(selector, vars, time)` inside the timeline instead.
|
||||
|
||||
Always use **camelCase** property names (`backgroundColor`, `rotationX`, not `background-color`).
|
||||
|
||||
## Common vars
|
||||
|
||||
- **`duration`** — seconds (default 0.5).
|
||||
- **`delay`** — seconds before start.
|
||||
- **`ease`** — `"power1.out"` (default), `"power3.inOut"`, `"back.out(1.7)"`, `"elastic.out(1, 0.3)"`, `"none"`, `"expo.out"`, `"circ.inOut"`.
|
||||
- **`stagger`** — number `0.1` or object: `{ amount: 0.3, from: "center" }`, `{ each: 0.1, from: "random" }`.
|
||||
- **`overwrite`** — `false` (default), `true`, or `"auto"`.
|
||||
- **`repeat`** — number (never `-1` in HyperFrames). **`yoyo`** — alternates direction with repeat.
|
||||
- **`onComplete`**, **`onStart`**, **`onUpdate`** — callbacks.
|
||||
- **`immediateRender`** — default `true` for `from()`/`fromTo()`. Set `false` on later tweens targeting the same property+element to avoid overwrite surprises.
|
||||
|
||||
## Transforms
|
||||
|
||||
Prefer GSAP's transform aliases over raw CSS `transform`:
|
||||
|
||||
| GSAP property | Equivalent |
|
||||
| --------------------------- | -------------------------- |
|
||||
| `x`, `y`, `z` | translateX/Y/Z (px) |
|
||||
| `xPercent`, `yPercent` | translateX/Y (%) |
|
||||
| `scale`, `scaleX`, `scaleY` | scale |
|
||||
| `rotation` | rotate (deg) |
|
||||
| `rotationX`, `rotationY` | 3D rotate |
|
||||
| `skewX`, `skewY` | skew |
|
||||
| `transformOrigin` | transform-origin |
|
||||
|
||||
- **`autoAlpha`** — prefer over `opacity`. At 0, also sets `visibility: hidden`.
|
||||
- **CSS variables** — `"--hue": 180`.
|
||||
- **Directional rotation** — `"360_cw"`, `"-170_short"`, `"90_ccw"`.
|
||||
- **`clearProps`** — `"all"` or comma-separated; removes inline styles on complete.
|
||||
- **Relative values** — `"+=20"`, `"-=10"`, `"*=2"`.
|
||||
|
||||
## Function-based Values
|
||||
|
||||
```js
|
||||
gsap.to(".item", {
|
||||
x: (i, target, targets) => i * 50,
|
||||
stagger: 0.1,
|
||||
});
|
||||
```
|
||||
|
||||
## Easing
|
||||
|
||||
Built-in eases: `power1` through `power4`, `back`, `bounce`, `circ`, `elastic`, `expo`, `sine`. Each has `.in`, `.out`, `.inOut`.
|
||||
|
||||
Rule of thumb:
|
||||
- Entrances: `power3.out`, `expo.out`, `back.out(1.4)`
|
||||
- Exits: `power2.in`, `expo.in`
|
||||
- Scrubbed sections: `none` (linear)
|
||||
- Vary eases across entrance tweens within a scene — at least 3 different eases.
|
||||
|
||||
## Defaults
|
||||
|
||||
```js
|
||||
gsap.defaults({ duration: 0.6, ease: "power2.out" });
|
||||
```
|
||||
|
||||
## Timelines (HyperFrames primary pattern)
|
||||
|
||||
```js
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const tl = gsap.timeline({ paused: true, defaults: { duration: 0.6, ease: "power2.out" } });
|
||||
|
||||
tl.from(".title", { y: 50, opacity: 0 }, 0.3);
|
||||
tl.from(".subtitle", { y: 30, opacity: 0 }, 0.5);
|
||||
tl.from(".cta", { scale: 0.8, opacity: 0, ease: "back.out(1.7)" }, 0.8);
|
||||
|
||||
window.__timelines["root"] = tl;
|
||||
```
|
||||
|
||||
### Position parameter
|
||||
|
||||
Third argument to `.from()` / `.to()` / `.add()`:
|
||||
|
||||
- Absolute seconds: `0.5`, `2.1`.
|
||||
- Relative to end: `">+0.2"` (0.2s after previous), `"<"` (same time as previous), `"<+0.3"` (0.3s after previous's start).
|
||||
- Named labels: `tl.addLabel("act2", 5); tl.from(".x", { y: 30 }, "act2");`
|
||||
|
||||
### Nesting
|
||||
|
||||
HyperFrames auto-nests sub-composition timelines. **Do not** manually `tl.add(subTl)` — the framework wires sub-timelines into the parent at the sub-composition's `data-start`.
|
||||
|
||||
### Playback
|
||||
|
||||
The player controls playback. Don't call `tl.play()`, `tl.pause()`, or `tl.reverse()` at construction time. `{ paused: true }` is required.
|
||||
|
||||
## Stagger
|
||||
|
||||
```js
|
||||
// even distribution
|
||||
tl.from(".card", { opacity: 0, y: 40, stagger: 0.1 });
|
||||
|
||||
// control total amount
|
||||
tl.from(".card", { opacity: 0, stagger: { amount: 0.6, from: "center" } });
|
||||
|
||||
// deterministic "random" stagger (HyperFrames compositions must be deterministic)
|
||||
tl.from(".dot", { opacity: 0, stagger: { each: 0.05, from: "random" } });
|
||||
```
|
||||
|
||||
`stagger.from`: `"start"` | `"end"` | `"center"` | `"edges"` | `"random"` | index | `[x, y]` for grid.
|
||||
|
||||
## Performance
|
||||
|
||||
- Animate transforms (`x`, `y`, `scale`, `rotation`, `opacity`) — cheap, GPU-accelerated.
|
||||
- Avoid animating `width`, `height`, `top`, `left`, `margin` — causes layout thrash.
|
||||
- Avoid box-shadow or filter animations on large elements — expensive.
|
||||
- `will-change` is rarely needed; GSAP handles promotion.
|
||||
|
||||
## gsap.matchMedia (rarely needed in HyperFrames)
|
||||
|
||||
Compositions have fixed dimensions (`data-width`/`data-height`), so responsive breakpoints don't apply. You may still use `matchMedia` for `prefers-reduced-motion` when authoring UI previews, but it's not used in rendered video output.
|
||||
|
||||
## Don't Do
|
||||
|
||||
- `repeat: -1` anywhere — breaks the capture engine.
|
||||
- `Math.random()`, `Date.now()`, performance.now()` inside tween values — non-deterministic.
|
||||
- `async` / `setTimeout` / `Promise` around timeline construction — the capture engine reads `window.__timelines` synchronously.
|
||||
- Animate `visibility` or `display` directly — use `autoAlpha`.
|
||||
- `gsap.set()` on clip elements that enter later in the timeline — they don't exist in the DOM at page-load. Use `tl.set(sel, vars, time)` inside the timeline.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Troubleshooting
|
||||
|
||||
## `HeadlessExperimental.beginFrame' wasn't found` (first thing to check)
|
||||
|
||||
**Symptom:** `npx hyperframes render` fails with:
|
||||
|
||||
```
|
||||
✗ Render failed
|
||||
Protocol error (HeadlessExperimental.beginFrame):
|
||||
'HeadlessExperimental.beginFrame' wasn't found
|
||||
```
|
||||
|
||||
**Cause:** Chromium 147+ removed the `HeadlessExperimental.beginFrame` CDP command. This affected sandbox environments (e.g., OpenClaw, some containerized agent hosts) that ship modern Chromium as the system browser. See [hyperframes#294](https://github.com/heygen-com/hyperframes/issues/294).
|
||||
|
||||
**Fix (permanent — preferred):** upgrade.
|
||||
|
||||
```bash
|
||||
npx hyperframes upgrade -y
|
||||
# or
|
||||
npm install -g hyperframes@latest
|
||||
```
|
||||
|
||||
`hyperframes >= 0.4.2` auto-detects whether the resolved browser supports `beginFrame` (checks for `chrome-headless-shell` in the binary path) and falls back to screenshot capture mode when it doesn't. Commit [`4c72ba4`](https://github.com/heygen-com/hyperframes/commit/4c72ba4a36ec2bd6733f7b9cb2a9e63f9fb234b9) (March 2026) shipped this auto-detect.
|
||||
|
||||
**Fix (escape hatch — if you can't upgrade):**
|
||||
|
||||
```bash
|
||||
export PRODUCER_FORCE_SCREENSHOT=true
|
||||
npx hyperframes render
|
||||
```
|
||||
|
||||
This forces screenshot mode regardless of the binary. Screenshot mode is slightly slower but visually identical.
|
||||
|
||||
**Fix (prevent — recommended):** install `chrome-headless-shell` so the engine can use the fast BeginFrame path:
|
||||
|
||||
```bash
|
||||
npx puppeteer browsers install chrome-headless-shell
|
||||
# or let the CLI do it
|
||||
npx hyperframes browser --install
|
||||
```
|
||||
|
||||
`scripts/setup.sh` runs this automatically.
|
||||
|
||||
## `npx hyperframes render` hangs for 120s then times out
|
||||
|
||||
**Cause:** the resolved browser is system Chrome (e.g., `/usr/bin/google-chrome`) and doesn't support the BeginFrame path, but auto-detect also missed it (older `hyperframes` version).
|
||||
|
||||
**Fix:**
|
||||
1. Check which binary is being used: `npx hyperframes browser --path`
|
||||
2. If it's system Chrome, either:
|
||||
- Install `chrome-headless-shell`: `npx hyperframes browser --install`, OR
|
||||
- Set the escape hatch: `export PRODUCER_FORCE_SCREENSHOT=true`, OR
|
||||
- Upgrade: `npx hyperframes upgrade -y`
|
||||
|
||||
## `ffmpeg: command not found`
|
||||
|
||||
Install FFmpeg via your system package manager:
|
||||
|
||||
| OS / distro | Command |
|
||||
| --------------- | ----------------------------------- |
|
||||
| Ubuntu / Debian | `sudo apt-get install -y ffmpeg` |
|
||||
| Fedora / RHEL | `sudo dnf install -y ffmpeg` |
|
||||
| Arch | `sudo pacman -S ffmpeg` |
|
||||
| macOS | `brew install ffmpeg` |
|
||||
| Windows | `winget install Gyan.FFmpeg` |
|
||||
|
||||
Verify: `ffmpeg -version`.
|
||||
|
||||
## `Node version X is not supported`
|
||||
|
||||
HyperFrames requires Node.js >= 22. Check with `node --version`.
|
||||
|
||||
- **nvm:** `nvm install 22 && nvm use 22`
|
||||
- **Homebrew (macOS):** `brew install node@22 && brew link --overwrite node@22`
|
||||
- **apt:** follow [nodesource](https://github.com/nodesource/distributions) for Node 22 LTS.
|
||||
|
||||
## `ENOSPC: no space left on device` or OOM kills during render
|
||||
|
||||
Renders are memory- and disk-hungry. Minimums:
|
||||
|
||||
- **RAM:** 4 GB free (8 GB recommended for 60fps / `--quality high`)
|
||||
- **Disk:** 2 GB free scratch space — frames are written to `/tmp` during capture
|
||||
|
||||
Mitigations:
|
||||
- Lower quality: `--quality draft`.
|
||||
- Lower fps: `--fps 24`.
|
||||
- Lower worker count: `--workers 1`.
|
||||
- Set `TMPDIR` to a volume with more space: `export TMPDIR=/mnt/scratch`.
|
||||
|
||||
## Lint passes but the render is blank / black frames
|
||||
|
||||
Check the browser console in `preview` — usually:
|
||||
- A timeline was registered with the wrong key (`__timelines["typo"]` instead of `__timelines["root"]`).
|
||||
- The root composition was wrapped in `<template>` (only sub-compositions use `<template>`).
|
||||
- A script tag failed to load — check Network tab in preview.
|
||||
|
||||
Run `npx hyperframes lint --verbose` to see info-level findings.
|
||||
|
||||
## Contrast warnings from `hyperframes validate`
|
||||
|
||||
```
|
||||
⚠ WCAG AA contrast warnings (3):
|
||||
· .subtitle "secondary text" — 2.67:1 (need 4.5:1, t=5.3s)
|
||||
```
|
||||
|
||||
- **Dark backgrounds:** brighten the failing color until it clears 4.5:1 (normal text) or 3:1 (large text — 24px+ or 19px+ bold).
|
||||
- **Light backgrounds:** darken it.
|
||||
- Stay within the palette family — don't invent a new color, adjust the existing one.
|
||||
- Skip the check temporarily with `--no-contrast` if iterating rapidly, but clear it before delivery.
|
||||
|
||||
## `Font family 'X' not supported by compiler`
|
||||
|
||||
The compiler embeds a curated set of web-safe + open-source fonts. If a font isn't supported, either:
|
||||
- Swap to a supported alternative from the warning.
|
||||
- Register a custom font via `@font-face` pointing to a `.woff2` in the project directory (the compiler embeds referenced `@font-face` files).
|
||||
|
||||
## Video plays back muted or with no audio
|
||||
|
||||
Check:
|
||||
- The `<video>` element has `muted playsinline` (required — browser autoplay policy).
|
||||
- Audio is a **separate** `<audio>` element, not the video element.
|
||||
- Audio `data-volume` is set (defaults to 1).
|
||||
- The audio file is at the expected path — compositions load relative to their own directory.
|
||||
|
||||
## Docker render fails on Linux with rootless Docker
|
||||
|
||||
Add `--privileged` or pass `--cap-add=SYS_ADMIN`:
|
||||
|
||||
```bash
|
||||
npx hyperframes render --docker --docker-args "--cap-add=SYS_ADMIN"
|
||||
```
|
||||
|
||||
The headless browser needs namespace permissions for sandboxing.
|
||||
|
||||
## Bug reports
|
||||
|
||||
Include `npx hyperframes info` output + the full error log. File at [github.com/heygen-com/hyperframes](https://github.com/heygen-com/hyperframes/issues).
|
||||
@@ -0,0 +1,145 @@
|
||||
# Website to Video
|
||||
|
||||
Capture a website, produce a professional video from it. Use when the user provides a URL and wants a video — social ad, product tour, 30-second promo, etc.
|
||||
|
||||
The workflow has 7 steps. Each produces an artifact that gates the next. **Do not skip steps** — each artifact prevents a downstream failure mode.
|
||||
|
||||
## Step 1: Capture & Understand
|
||||
|
||||
```bash
|
||||
npx hyperframes capture https://example.com -o example-video
|
||||
```
|
||||
|
||||
Produces `example-video/capture/` with:
|
||||
- `capture/screenshots/` — above-the-fold + section screenshots (up to `--max-screenshots`)
|
||||
- `capture/assets/` — logos, hero images, background video (if any)
|
||||
- `capture/extracted/tokens.json` — colors, fonts, and spacing tokens
|
||||
- `capture/extracted/visible-text.txt` — extracted headings, paragraphs, CTAs
|
||||
- `capture/extracted/fonts.json` — font families and stacks detected in computed styles
|
||||
- `capture/asset-descriptions.md` — auto-generated asset catalog
|
||||
|
||||
All subsequent steps read from the `capture/` subfolder — `capture/extracted/tokens.json`, `capture/assets/hero.png`, etc. Never strip the `capture/` prefix when referencing these files.
|
||||
|
||||
**Gate:** Print a site summary — name, top 3 colors, primary + display fonts, hero asset path, one-sentence vibe. Keep it in your context — don't re-capture.
|
||||
|
||||
## Step 2: Write DESIGN.md
|
||||
|
||||
Small brand reference at the project root. 6 sections, ~90 lines. This is the cheat sheet — not the creative plan.
|
||||
|
||||
```markdown
|
||||
# DESIGN
|
||||
|
||||
## Brand
|
||||
- Name: Example Co.
|
||||
- One-line mission: "…"
|
||||
|
||||
## Colors
|
||||
- Background: #0B0F14
|
||||
- Primary: #00E0A4 (accent, CTA)
|
||||
- Secondary: #7A8B9B (body text)
|
||||
- Text: #FFFFFF
|
||||
|
||||
## Typography
|
||||
- Display: "Inter Tight", 700, tight letter-spacing
|
||||
- Body: "Inter", 400
|
||||
|
||||
## Motion
|
||||
- Mood: precise, technical, confident
|
||||
- Eases: `power3.out` for entrances, `expo.in` for exits
|
||||
|
||||
## Assets
|
||||
- Logo: `capture/assets/logo.svg`
|
||||
- Hero image: `capture/assets/hero.png`
|
||||
|
||||
## What NOT to Do
|
||||
- No purple, no pastels, no serif body
|
||||
- No playful/bubbly eases (`elastic`, `bounce`)
|
||||
- No drop shadows on text
|
||||
```
|
||||
|
||||
**Gate:** `DESIGN.md` exists in the project directory.
|
||||
|
||||
## Step 3: Write SCRIPT.md
|
||||
|
||||
Narration script. Story backbone. **Scene durations come from the narration, not from guessing.**
|
||||
|
||||
```markdown
|
||||
# SCRIPT
|
||||
|
||||
## Scene 1 — Hook (0:00–0:04)
|
||||
"What if your dashboards wrote themselves?"
|
||||
|
||||
## Scene 2 — Problem (0:04–0:11)
|
||||
"Teams spend hours stitching together queries, charts, and callouts — every Monday."
|
||||
|
||||
## Scene 3 — Solution (0:11–0:22)
|
||||
"Example Co. watches your data streams and proposes the dashboard you'd have built — in seconds."
|
||||
|
||||
## Scene 4 — CTA (0:22–0:28)
|
||||
"Try it free at example.com."
|
||||
```
|
||||
|
||||
Run `npx hyperframes tts SCRIPT.md --voice af_nova --output narration.wav` to generate TTS audio. Note the exact duration — that's the video's duration.
|
||||
|
||||
**Gate:** `SCRIPT.md` + `narration.wav` exist and durations match the plan (±0.3s).
|
||||
|
||||
## Step 4: Storyboard
|
||||
|
||||
Text-only scene plan: for each scene, describe the hero frame — what's on screen at the scene's most-visible moment.
|
||||
|
||||
```markdown
|
||||
# STORYBOARD
|
||||
|
||||
## Scene 1 (0:00–0:04) — Hook
|
||||
Hero frame: giant "WHAT IF YOUR DASHBOARDS WROTE THEMSELVES?" in display font, centered, on near-black. Logo top-left at 40% opacity.
|
||||
Entrance: each word staggers in, 0.08s apart.
|
||||
Transition out: flash-through-white into Scene 2.
|
||||
```
|
||||
|
||||
One paragraph per scene. Do NOT skip this step — it's where you catch narrative gaps before writing HTML.
|
||||
|
||||
**Gate:** `STORYBOARD.md` exists. Each scene has: hero frame, entrance, transition.
|
||||
|
||||
## Step 5: Composition
|
||||
|
||||
Write `index.html` scene-by-scene:
|
||||
- Each scene is a `<div class="scene scene-N">` positioned absolutely, full-bleed.
|
||||
- Static HTML+CSS for the hero frame first (no GSAP).
|
||||
- Layer the narration `<audio>` at `data-start="0"` on a high track index.
|
||||
- Add a transitions component (`flash-through-white`, `liquid-wipe`, etc.) between each scene.
|
||||
- THEN add GSAP entrances (`gsap.from()`), no exits — transitions own the exit.
|
||||
- Register `window.__timelines["root"] = tl`.
|
||||
|
||||
Install transitions as needed:
|
||||
|
||||
```bash
|
||||
npx hyperframes add flash-through-white
|
||||
```
|
||||
|
||||
## Step 6: Render
|
||||
|
||||
```bash
|
||||
npx hyperframes lint --strict # must pass
|
||||
npx hyperframes validate # WCAG contrast audit
|
||||
npx hyperframes render --quality draft --output draft.mp4
|
||||
```
|
||||
|
||||
Watch the draft. Note issues in a `REVIEW.md` bullet list (scene, timestamp, issue). Fix, re-render.
|
||||
|
||||
When happy:
|
||||
|
||||
```bash
|
||||
npx hyperframes render --quality high --output final.mp4
|
||||
```
|
||||
|
||||
## Step 7: Deliver
|
||||
|
||||
- Report file path + duration + file size to the user.
|
||||
- If the user wants a vertical cut, re-render with a 9:16 composition (`data-width="1080" data-height="1920"`) — typically requires a separate `index-vertical.html` with tighter typography and re-stacked scene layout.
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
- **Skipped DESIGN.md** → colors drift scene-to-scene; output feels like "AI slides."
|
||||
- **Skipped STORYBOARD.md** → scenes overlap or hero frames collide with transitions.
|
||||
- **Exit animations** before transitions → empty frames when the transition fires.
|
||||
- **Narration longer than `data-duration`** → audio clips mid-sentence. Update the composition's `data-duration` to match the TTS output length + 0.5s buffer.
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
# HyperFrames setup for Hermes.
|
||||
#
|
||||
# Verifies Node >= 22 and FFmpeg, installs the `hyperframes` CLI globally,
|
||||
# pre-caches `chrome-headless-shell`, and runs `hyperframes doctor`.
|
||||
#
|
||||
# Pins `hyperframes@>=0.4.2` so the OpenClaw/Chromium-147 fix from
|
||||
# https://github.com/heygen-com/hyperframes/issues/294 (commit 4c72ba4)
|
||||
# is always present — the engine auto-detects `HeadlessExperimental.beginFrame`
|
||||
# support and falls back to screenshot capture otherwise.
|
||||
#
|
||||
# Idempotent: safe to re-run.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MIN_NODE_MAJOR=22
|
||||
MIN_HYPERFRAMES_VERSION="0.4.2"
|
||||
|
||||
red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||
yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
|
||||
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||
|
||||
bold "==> HyperFrames setup"
|
||||
|
||||
# --- 1. Node.js --------------------------------------------------------------
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
red "✗ Node.js is not installed."
|
||||
echo " Install Node.js >= ${MIN_NODE_MAJOR} (nvm, Homebrew, or your package manager) and re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node_version="$(node --version | sed 's/^v//')"
|
||||
node_major="$(echo "$node_version" | cut -d. -f1)"
|
||||
if [ "$node_major" -lt "$MIN_NODE_MAJOR" ]; then
|
||||
red "✗ Node.js ${node_version} is too old. HyperFrames requires Node.js >= ${MIN_NODE_MAJOR}."
|
||||
echo " Upgrade with 'nvm install ${MIN_NODE_MAJOR} && nvm use ${MIN_NODE_MAJOR}' or your package manager."
|
||||
exit 1
|
||||
fi
|
||||
green "✓ Node.js ${node_version}"
|
||||
|
||||
# --- 2. FFmpeg ---------------------------------------------------------------
|
||||
|
||||
if ! command -v ffmpeg >/dev/null 2>&1; then
|
||||
red "✗ FFmpeg is not installed."
|
||||
case "$(uname -s)" in
|
||||
Linux*) echo " sudo apt-get install -y ffmpeg # Debian/Ubuntu"
|
||||
echo " sudo dnf install -y ffmpeg # Fedora/RHEL";;
|
||||
Darwin*) echo " brew install ffmpeg";;
|
||||
MINGW*|MSYS*|CYGWIN*) echo " winget install Gyan.FFmpeg";;
|
||||
*) echo " See https://ffmpeg.org/download.html";;
|
||||
esac
|
||||
exit 1
|
||||
fi
|
||||
green "✓ FFmpeg $(ffmpeg -version 2>&1 | head -1 | awk '{print $3}')"
|
||||
|
||||
# --- 3. npm ------------------------------------------------------------------
|
||||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
red "✗ npm is not installed (should ship with Node.js)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 4. Install / upgrade hyperframes CLI -----------------------------------
|
||||
|
||||
bold "==> Installing hyperframes CLI (>= ${MIN_HYPERFRAMES_VERSION})"
|
||||
|
||||
current_hyperframes=""
|
||||
if command -v hyperframes >/dev/null 2>&1; then
|
||||
current_hyperframes="$(hyperframes --version 2>/dev/null | tail -1 | sed 's/^v//')"
|
||||
fi
|
||||
|
||||
if [ -n "$current_hyperframes" ]; then
|
||||
yellow " Found hyperframes ${current_hyperframes}"
|
||||
fi
|
||||
|
||||
# Always install/upgrade to >= MIN version.
|
||||
# Using 'latest' so we pick up any newer auto-detect/capture fixes.
|
||||
if ! npm install -g "hyperframes@latest" >/dev/null 2>&1; then
|
||||
red "✗ npm install -g hyperframes@latest failed."
|
||||
echo " Try: sudo npm install -g hyperframes@latest"
|
||||
echo " Or use a user-scoped npm prefix: npm config set prefix ~/.npm-global && export PATH=\"\$HOME/.npm-global/bin:\$PATH\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
installed_version="$(hyperframes --version 2>/dev/null | tail -1 | sed 's/^v//')"
|
||||
green "✓ hyperframes ${installed_version} installed globally"
|
||||
|
||||
# Sanity-check minimum version.
|
||||
version_ge() {
|
||||
# version_ge A B → true if A >= B
|
||||
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -1)" = "$2" ]
|
||||
}
|
||||
if ! version_ge "$installed_version" "$MIN_HYPERFRAMES_VERSION"; then
|
||||
red "✗ hyperframes ${installed_version} is below required minimum ${MIN_HYPERFRAMES_VERSION}."
|
||||
echo " Try 'npm install -g hyperframes@latest' or 'sudo npm install -g hyperframes@latest'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 5. Pre-cache chrome-headless-shell --------------------------------------
|
||||
#
|
||||
# Chromium 147+ removed HeadlessExperimental.beginFrame. System Chrome (e.g.
|
||||
# /usr/bin/google-chrome) can't render with the fast path, so the engine
|
||||
# auto-detects and falls back to screenshot mode — but BeginFrame mode is
|
||||
# faster and produces higher-quality output. Install chrome-headless-shell
|
||||
# up front so the engine picks it over system Chrome.
|
||||
|
||||
bold "==> Pre-caching chrome-headless-shell (for best render quality)"
|
||||
|
||||
if ! npx --yes puppeteer browsers install chrome-headless-shell >/dev/null 2>&1; then
|
||||
yellow "⚠ Could not pre-install chrome-headless-shell."
|
||||
yellow " Rendering will still work via screenshot-mode fallback (slower)."
|
||||
yellow " If you hit HeadlessExperimental.beginFrame errors:"
|
||||
yellow " export PRODUCER_FORCE_SCREENSHOT=true"
|
||||
yellow " See references/troubleshooting.md."
|
||||
else
|
||||
green "✓ chrome-headless-shell installed"
|
||||
fi
|
||||
|
||||
# --- 6. Doctor ---------------------------------------------------------------
|
||||
|
||||
bold "==> Running hyperframes doctor"
|
||||
|
||||
if hyperframes doctor; then
|
||||
green "✓ HyperFrames is ready"
|
||||
echo
|
||||
echo " Scaffold a project: npx hyperframes init my-video"
|
||||
echo " Preview: npx hyperframes preview"
|
||||
echo " Render: npx hyperframes render"
|
||||
else
|
||||
yellow "⚠ hyperframes doctor reported issues."
|
||||
yellow " See references/troubleshooting.md or re-run 'hyperframes doctor'."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
name: kanban-video-orchestrator
|
||||
description: Plan, set up, and monitor a multi-agent video production pipeline backed by Hermes Kanban. Use when the user wants to make ANY video — narrative film, product/marketing, music video, explainer, ASCII/terminal art, abstract/generative loop, comic, 3D, real-time/installation — and the work warrants decomposition into specialized profiles (writer, designer, animator, renderer, voice, editor, etc.) coordinated through a kanban board. Performs adaptive discovery to scope the brief, designs an appropriate team for the requested style, generates the setup script that creates Hermes profiles + initial kanban task, then helps monitor execution and intervene when tasks stall or fail. Routes scenes to whichever Hermes rendering / audio / design skill fits each beat (`ascii-video`, `manim-video`, `p5js`, `comfyui`, `touchdesigner-mcp`, `blender-mcp`, `pixel-art`, `baoyu-comic`, `claude-design`, `excalidraw`, `songsee`, `heartmula`, …) plus external APIs for TTS, image-gen, and image-to-video as needed.
|
||||
version: 1.0.0
|
||||
author: [SHL0MS, alt-glitch]
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [video, kanban, multi-agent, orchestration, production-pipeline]
|
||||
related_skills: [kanban-orchestrator, kanban-worker, ascii-video, manim-video, p5js, comfyui, touchdesigner-mcp, blender-mcp, pixel-art, ascii-art, songwriting-and-ai-music, heartmula, songsee, spotify, youtube-content, claude-design, excalidraw, architecture-diagram, concept-diagrams, baoyu-comic, baoyu-infographic, humanizer, gif-search, meme-generation]
|
||||
credits: |
|
||||
The single-project workspace layout, profile-config patching pattern,
|
||||
SOUL.md-per-profile model, TEAM.md task-graph convention, and
|
||||
`--workspace dir:<path>` discipline are adapted from alt-glitch's
|
||||
original multi-agent video pipeline at
|
||||
https://github.com/NousResearch/kanban-video-pipeline.
|
||||
---
|
||||
|
||||
# Kanban Video Orchestrator
|
||||
|
||||
Wrap any video request — from a 15-second product teaser to a 5-minute narrative
|
||||
short to a music video to an ASCII loop — in a Hermes Kanban pipeline that
|
||||
decomposes the work to specialized agent profiles.
|
||||
|
||||
This skill does **not** render anything itself. It is a meta-pipeline that:
|
||||
|
||||
1. **Scopes** the request through targeted discovery
|
||||
2. **Designs** an appropriate team (which roles, which tools per role) based on the style
|
||||
3. **Generates** a setup script that creates Hermes profiles, project workspace, and the initial kanban task
|
||||
4. **Hands off** to the director profile, which decomposes via the kanban
|
||||
5. **Monitors** execution, helps intervene when tasks stall or fail
|
||||
|
||||
The actual rendering happens inside the kanban once it's running, via whichever
|
||||
existing skills + tools fit the scenes — `ascii-video`, `manim-video`, `p5js`,
|
||||
`comfyui`, `touchdesigner-mcp`, `blender-mcp`, `songwriting-and-ai-music`,
|
||||
`heartmula`, external APIs, or plain Python with PIL + ffmpeg.
|
||||
|
||||
## When NOT to use this skill
|
||||
|
||||
- The video is one continuous procedural project that needs no specialists. Just write the code directly.
|
||||
- The user wants a quick one-shot conversion (e.g. "convert this mp4 to a GIF") — use ffmpeg directly.
|
||||
- The output is a static image, GIF, or audio-only artifact — use the matching specific skill (`ascii-art`, `gifs`, `meme-generation`, `songwriting-and-ai-music`).
|
||||
- The work fits a single existing skill cleanly (e.g. a pure ASCII video — just use `ascii-video`).
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
DISCOVER → BRIEF → TEAM DESIGN → SETUP → EXECUTE → MONITOR
|
||||
```
|
||||
|
||||
### Step 1 — Discover (ask the right questions)
|
||||
|
||||
The discovery process is **adaptive**: ask only what is actually needed. Always
|
||||
start with three questions to identify the broad shape:
|
||||
|
||||
- **What is the video?** (one-sentence brief)
|
||||
- **How long?** (5-30s teaser / 30-90s short / 90s-3min explainer / 3-10min film / longer)
|
||||
- **What aspect ratio + target platform?** (1:1 / 9:16 / 16:9; X, IG, YouTube, internal, etc.)
|
||||
|
||||
From the answer, classify the style category. The style determines which
|
||||
follow-up questions to ask. **Do not ask all questions at once.** Ask 2-4 at a
|
||||
time, listen, then proceed. Make reasonable assumptions whenever the user
|
||||
implies an answer.
|
||||
|
||||
For complete intake patterns and per-style question banks, see
|
||||
**[references/intake.md](references/intake.md)**.
|
||||
|
||||
### Step 2 — Brief
|
||||
|
||||
Once enough is known, produce a structured `brief.md` using the template in
|
||||
`assets/brief.md.tmpl`. Stages:
|
||||
|
||||
1. **Concept** — the one-sentence pitch + emotional north star
|
||||
2. **Scope** — duration, aspect, platform, deadline
|
||||
3. **Style** — visual references, brand constraints, tone
|
||||
4. **Scenes** — beat-by-beat breakdown (durations, content, target tool)
|
||||
5. **Audio** — narration / music / SFX / silent (per scene if needed)
|
||||
6. **Deliverables** — file format, resolution, optional alternates (vertical cut, GIF, etc.)
|
||||
|
||||
Show the brief to the user for confirmation before designing the team. **The
|
||||
brief is the contract** — every downstream task references it.
|
||||
|
||||
### Step 3 — Team design
|
||||
|
||||
Pick role archetypes from the library that fit this video. **Compose, don't
|
||||
clone.** Most videos need 4-7 profiles. The director is always present; the
|
||||
rest are picked by what the brief actually requires.
|
||||
|
||||
For the role library and per-style team compositions, see
|
||||
**[references/role-archetypes.md](references/role-archetypes.md)**.
|
||||
|
||||
For mapping role → which Hermes skills + toolsets it loads, see
|
||||
**[references/tool-matrix.md](references/tool-matrix.md)**.
|
||||
|
||||
### Step 4 — Setup
|
||||
|
||||
Generate a setup script (`setup.sh`) and run it. The script:
|
||||
|
||||
1. Creates the project workspace (`~/projects/video-pipeline/<slug>/`)
|
||||
2. Copies any provided assets into `taste/`, `audio/`, `assets/`
|
||||
3. Creates each Hermes profile via `hermes profile create --clone`
|
||||
4. Writes per-profile `SOUL.md` (personality + role definition)
|
||||
5. Configures profile YAML (toolsets, always_load skills, cwd)
|
||||
6. Writes `brief.md`, `TEAM.md`, and `taste/` content
|
||||
7. Fires the initial `hermes kanban create` task assigned to the director
|
||||
|
||||
Use `scripts/bootstrap_pipeline.py` to generate setup.sh from a brief +
|
||||
team-design JSON. See **[references/kanban-setup.md](references/kanban-setup.md)**
|
||||
for the setup script structure, profile config patterns, and the critical
|
||||
"shared workspace" rule.
|
||||
|
||||
### Step 5 — Execute
|
||||
|
||||
Run `setup.sh`. Then provide the user with monitoring commands:
|
||||
|
||||
```bash
|
||||
hermes kanban watch --tenant <project-tenant> # live events
|
||||
hermes kanban list --tenant <project-tenant> # board snapshot
|
||||
hermes dashboard # visual board UI
|
||||
```
|
||||
|
||||
The director profile takes over from here, decomposing the work and routing
|
||||
tasks to specialist profiles via the kanban toolset.
|
||||
|
||||
### Step 6 — Monitor and intervene
|
||||
|
||||
Stay engaged — the kanban runs autonomously but a stuck task or bad output
|
||||
needs human (or AI) judgment.
|
||||
|
||||
Monitoring patterns: poll `kanban list` periodically, inspect any RUNNING task
|
||||
that exceeds its expected duration with `kanban show <id>`, and check
|
||||
heartbeats. When a worker's output fails review, the standard interventions are:
|
||||
|
||||
1. Comment on the worker's task with specific feedback (`kanban_comment`)
|
||||
2. Create a re-run task with the original as parent
|
||||
3. Adjust the brief's scope and let the director re-decompose
|
||||
|
||||
For diagnostic patterns, intervention recipes, and the "task is stuck"
|
||||
playbook, see **[references/monitoring.md](references/monitoring.md)**.
|
||||
|
||||
## Reference: worked examples
|
||||
|
||||
Six concrete pipelines covering very different video styles — narrative film,
|
||||
product/marketing, music video, math/algorithm explainer, ASCII video, real-time
|
||||
installation — showing how the same workflow yields very different teams and
|
||||
task graphs. See **[references/examples.md](references/examples.md)**.
|
||||
|
||||
## Critical rules
|
||||
|
||||
1. **Discovery before action.** Never start generating a brief or team without
|
||||
asking at least the three baseline questions. A bad brief cascades through
|
||||
the entire pipeline.
|
||||
|
||||
2. **Match the team to the video.** Don't reuse the same 4-profile setup for
|
||||
every job. A music video that doesn't have a beat-analysis profile will
|
||||
misfire. A narrative film that doesn't have a writer profile will produce
|
||||
incoherent scenes. See `references/role-archetypes.md`.
|
||||
|
||||
3. **One workspace per project.** All profiles for a given video share the same
|
||||
`dir:` workspace. Tasks pass artifacts via shared filesystem and structured
|
||||
handoffs. **Every** `kanban_create` call passes
|
||||
`workspace_kind="dir"` + `workspace_path="<absolute project path>"`.
|
||||
|
||||
4. **Tenant every project.** Use a project-specific tenant
|
||||
(`--tenant <project-slug>`). Keeps the dashboard scoped and prevents
|
||||
cross-pollination with other ongoing kanbans.
|
||||
|
||||
5. **Respect existing skills.** When a scene fits an existing skill, the
|
||||
relevant renderer should load that skill via `--skill <name>` on its task
|
||||
or `always_load` in its profile. Do not re-derive what a skill already
|
||||
provides.
|
||||
|
||||
6. **The director never executes.** Even with the full `kanban + terminal +
|
||||
file` toolset, the director's `SOUL.md` rules forbid it from executing
|
||||
work itself. It decomposes and routes only — every concrete task becomes
|
||||
a `hermes kanban create` call to a specialist profile. The
|
||||
`kanban-orchestrator` skill spells this out further.
|
||||
|
||||
7. **Don't over-decompose.** A 30-second product video does NOT need 20 tasks.
|
||||
Aim for the smallest task graph that still parallelizes well and exposes the
|
||||
right human-review gates.
|
||||
|
||||
8. **Verify API keys BEFORE firing.** External APIs (TTS, image-gen,
|
||||
image-to-video) need keys in `~/.hermes/.env` or the user's secret store.
|
||||
A worker that hits a missing-key error wastes a task slot. The setup
|
||||
script's `check_key` helper aborts cleanly if a required key is missing.
|
||||
|
||||
## File map
|
||||
|
||||
```
|
||||
SKILL.md ← this file (workflow + rules)
|
||||
references/
|
||||
intake.md ← discovery question banks per style
|
||||
role-archetypes.md ← role library (writer, designer, animator, …)
|
||||
tool-matrix.md ← skill + toolset mapping per role
|
||||
kanban-setup.md ← setup script structure & profile config
|
||||
monitoring.md ← watch + intervene patterns
|
||||
examples.md ← six worked pipelines
|
||||
assets/
|
||||
brief.md.tmpl ← brief skeleton
|
||||
setup.sh.tmpl ← setup script skeleton
|
||||
soul.md.tmpl ← profile personality skeleton
|
||||
scripts/
|
||||
bootstrap_pipeline.py ← generate setup.sh from brief + team JSON
|
||||
monitor.py ← polling + intervention helpers
|
||||
```
|
||||
@@ -0,0 +1,79 @@
|
||||
# Video Brief — {{TITLE}}
|
||||
|
||||
> Slug: `{{SLUG}}` · Tenant: `{{TENANT}}` · Project workspace: `{{WORKSPACE}}`
|
||||
|
||||
## 1. Concept
|
||||
|
||||
**One-line pitch.** {{ONE_LINE_PITCH}}
|
||||
|
||||
**Emotional north star.** {{EMOTIONAL_NORTH_STAR}}
|
||||
*(What should the viewer feel walking away?)*
|
||||
|
||||
## 2. Scope
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Duration | {{DURATION_S}} seconds |
|
||||
| Aspect ratio | {{ASPECT}} |
|
||||
| Resolution | {{RESOLUTION}} |
|
||||
| Frame rate | {{FPS}} fps |
|
||||
| Target platforms | {{PLATFORMS}} |
|
||||
| Deadline | {{DEADLINE}} |
|
||||
| Quality bar | {{QUALITY_BAR}} *(rough draft / polished / archival)* |
|
||||
|
||||
## 3. Style
|
||||
|
||||
**Visual references.** {{VISUAL_REFS}}
|
||||
|
||||
**Tone.** {{TONE}}
|
||||
|
||||
**Brand constraints.** {{BRAND_CONSTRAINTS}}
|
||||
*(colors, typography, motion language; or "n/a")*
|
||||
|
||||
**Aesthetic rules.**
|
||||
{{AESTHETIC_RULES}}
|
||||
|
||||
## 4. Scenes
|
||||
|
||||
Beat-by-beat breakdown. Each scene gets a row.
|
||||
|
||||
| # | Time | Content | Target tool / skill | Audio | Notes |
|
||||
|---|------|---------|---------------------|-------|-------|
|
||||
| 1 | 0:00–0:0X | {{SCENE_1_CONTENT}} | {{SCENE_1_TOOL}} | {{SCENE_1_AUDIO}} | {{SCENE_1_NOTES}} |
|
||||
| 2 | 0:0X–0:0Y | ... | ... | ... | ... |
|
||||
|
||||
## 5. Audio
|
||||
|
||||
**Approach.** {{AUDIO_APPROACH}}
|
||||
*(narration / music-only / synced to track / silent / mixed)*
|
||||
|
||||
**Voiceover.** {{VO_DETAILS}}
|
||||
*(provider, voice, language, script source — "n/a" if no VO)*
|
||||
|
||||
**Music.** {{MUSIC_DETAILS}}
|
||||
*(provided track path / commission via Suno / commission via heartmula /
|
||||
license-free / "n/a")*
|
||||
|
||||
**SFX.** {{SFX_DETAILS}}
|
||||
*(generated, library, or "n/a")*
|
||||
|
||||
## 6. Deliverables
|
||||
|
||||
| Format | Resolution | Notes |
|
||||
|--------|-----------|-------|
|
||||
| {{PRIMARY_FORMAT}} | {{PRIMARY_RES}} | The main output |
|
||||
| {{ALT_FORMAT_1}} | {{ALT_RES_1}} | {{ALT_NOTES_1}} |
|
||||
|
||||
**Final filename.** `output/final.mp4`
|
||||
*(plus optional `output/final-9x16.mp4`, `output/captions.srt`, etc.)*
|
||||
|
||||
## 7. Constraints
|
||||
|
||||
- API keys required: {{API_KEYS_REQUIRED}}
|
||||
- External dependencies: {{EXT_DEPS}}
|
||||
- Source assets to incorporate: {{SOURCE_ASSETS}}
|
||||
|
||||
---
|
||||
|
||||
**This brief is the contract. The director and every downstream profile read
|
||||
it. If the brief changes, the kanban must be re-fired — don't edit live.**
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Video Pipeline Setup — {{TITLE}}
|
||||
#
|
||||
# Generated by kanban-video-orchestrator skill.
|
||||
#
|
||||
# Slug: {{SLUG}}
|
||||
# Workspace: {{WORKSPACE}}
|
||||
# Tenant: {{TENANT}}
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_SLUG="{{SLUG}}"
|
||||
WORKSPACE="$HOME/projects/video-pipeline/${PROJECT_SLUG}"
|
||||
TENANT="{{TENANT}}"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 1. Verify required API keys
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
echo "═══ Checking required API keys ═══"
|
||||
|
||||
check_key() {
|
||||
local var="$1"
|
||||
local kc_account="${2:-hermes}"
|
||||
local kc_service="${3:-$1}"
|
||||
if grep -q "^${var}=" "$HOME/.hermes/.env" 2>/dev/null && \
|
||||
[ -n "$(grep "^${var}=" "$HOME/.hermes/.env" | cut -d= -f2-)" ]; then
|
||||
echo " ✓ ${var} (env)"
|
||||
return 0
|
||||
fi
|
||||
if command -v security >/dev/null 2>&1 && \
|
||||
security find-generic-password -a "${kc_account}" -s "${kc_service}" -w >/dev/null 2>&1; then
|
||||
echo " ✓ ${var} (Keychain ${kc_account}/${kc_service})"
|
||||
return 0
|
||||
fi
|
||||
echo " ✗ ${var} not set in ~/.hermes/.env or Keychain (${kc_account}/${kc_service})"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Customize this list per project — only check keys actually used:
|
||||
{{KEY_CHECKS}}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 2. Create project workspace
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
echo "═══ Creating project workspace ═══"
|
||||
mkdir -p "$WORKSPACE"/{taste,audio/{voiceover,sfx},assets,scenes,checkpoints,tools,output}
|
||||
{{SCENE_DIRS}}
|
||||
echo " ✓ $WORKSPACE"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 3. Create Hermes profiles
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
echo "═══ Creating Hermes profiles ═══"
|
||||
|
||||
{{PROFILE_CREATE_COMMANDS}}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 4. Configure profiles (toolsets, skills, cwd)
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
echo "═══ Configuring profiles ═══"
|
||||
|
||||
configure_profile() {
|
||||
local profile="$1"
|
||||
local toolsets_json="$2" # JSON array string, e.g. '["kanban","terminal","file"]'
|
||||
local skills_json="$3" # JSON array string, e.g. '["kanban-worker","ascii-video"]'
|
||||
python3 - "$profile" "$toolsets_json" "$skills_json" "$WORKSPACE" <<'PY'
|
||||
"""Patch a Hermes profile config.yaml using PyYAML so we don't depend on the
|
||||
exact default-config string format. Validates the patch took effect and exits
|
||||
non-zero if anything's off."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("ERROR: PyYAML required. pip install pyyaml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
profile, toolsets_json, skills_json, workspace = sys.argv[1:5]
|
||||
toolsets = json.loads(toolsets_json)
|
||||
skills = json.loads(skills_json)
|
||||
|
||||
p = os.path.expanduser(f"~/.hermes/profiles/{profile}/config.yaml")
|
||||
if not os.path.exists(p):
|
||||
print(f" ✗ profile config not found: {p}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(p) as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
|
||||
# Apply our changes — only the keys we actually want to set.
|
||||
cfg["toolsets"] = toolsets
|
||||
cfg.setdefault("skills", {})
|
||||
cfg["skills"]["always_load"] = skills
|
||||
|
||||
# Note: we do NOT touch cfg["approvals"] — that's a security-sensitive
|
||||
# setting (manual confirmation of tool calls). Workspace cwd is overridden
|
||||
# per-task by `--workspace dir:<path>` on `hermes kanban create`, so we
|
||||
# don't need to mutate cfg["terminal"]["cwd"] either.
|
||||
|
||||
with open(p, "w") as f:
|
||||
yaml.safe_dump(cfg, f, sort_keys=False)
|
||||
|
||||
# Validate
|
||||
with open(p) as f:
|
||||
after = yaml.safe_load(f)
|
||||
errors = []
|
||||
if after.get("toolsets") != toolsets:
|
||||
errors.append(f"toolsets mismatch: {after.get('toolsets')!r}")
|
||||
if after.get("skills", {}).get("always_load") != skills:
|
||||
errors.append(f"skills.always_load mismatch: {after.get('skills', {}).get('always_load')!r}")
|
||||
if errors:
|
||||
print(f" ✗ {profile}: " + "; ".join(errors), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
if [ $? -ne 0 ]; then
|
||||
echo " ✗ failed to configure ${profile}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ ${profile}"
|
||||
}
|
||||
|
||||
{{PROFILE_CONFIG_COMMANDS}}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 5. Write SOUL.md per profile
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
echo "═══ Writing profile personalities ═══"
|
||||
|
||||
{{SOUL_WRITES}}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 6. Copy brief, TEAM.md, and any provided assets
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
echo "═══ Writing brief + taste ═══"
|
||||
|
||||
cat > "$WORKSPACE/brief.md" <<'BRIEF_EOF'
|
||||
{{BRIEF_CONTENTS}}
|
||||
BRIEF_EOF
|
||||
|
||||
cat > "$WORKSPACE/TEAM.md" <<'TEAM_EOF'
|
||||
{{TEAM_CONTENTS}}
|
||||
TEAM_EOF
|
||||
|
||||
{{TASTE_WRITES}}
|
||||
|
||||
{{ASSET_COPIES}}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 7. Fire the initial kanban task
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
echo "═══ Firing initial kanban task ═══"
|
||||
|
||||
hermes kanban create "Direct production of {{TITLE}}" \
|
||||
--assignee director \
|
||||
--workspace dir:"$WORKSPACE" \
|
||||
--tenant "$TENANT" \
|
||||
--priority 2 \
|
||||
--max-runtime 4h \
|
||||
--body "$(cat <<EOF
|
||||
Read brief.md, TEAM.md, and taste/.
|
||||
|
||||
Decompose into the team graph defined in TEAM.md.
|
||||
|
||||
All child tasks MUST use:
|
||||
workspace_kind="dir"
|
||||
workspace_path="$WORKSPACE"
|
||||
tenant="$TENANT"
|
||||
|
||||
Do not execute the work yourself — route every concrete subtask to the
|
||||
appropriate profile via kanban_create.
|
||||
EOF
|
||||
)"
|
||||
|
||||
echo ""
|
||||
echo "═══ Setup complete ═══"
|
||||
echo ""
|
||||
echo "Monitor with:"
|
||||
echo " hermes kanban watch --tenant $TENANT"
|
||||
echo " hermes kanban list --tenant $TENANT"
|
||||
echo " hermes dashboard"
|
||||
echo ""
|
||||
echo "Workspace: $WORKSPACE"
|
||||
@@ -0,0 +1,38 @@
|
||||
# {{ROLE_NAME}}
|
||||
|
||||
You are the **{{ROLE_NAME}}** for this video production.
|
||||
|
||||
## Project context
|
||||
|
||||
- **Brief:** read `brief.md` in your CWD
|
||||
- **Team graph:** read `TEAM.md` in your CWD
|
||||
- **Style spec:** read `taste/brand-guide.md` and `taste/emotional-dna.md` in
|
||||
your CWD
|
||||
|
||||
## What you do
|
||||
|
||||
{{ROLE_RESPONSIBILITIES}}
|
||||
|
||||
## Inputs you read
|
||||
|
||||
{{INPUTS_READ}}
|
||||
|
||||
## Outputs you produce
|
||||
|
||||
{{OUTPUTS_PRODUCED}}
|
||||
|
||||
## Tools and skills available
|
||||
|
||||
- **Toolsets:** {{TOOLSETS}}
|
||||
- **Skills loaded:** {{SKILLS}}
|
||||
- **External APIs / CLIs:** {{EXTERNAL_TOOLS}}
|
||||
|
||||
## Rules
|
||||
|
||||
{{ROLE_RULES}}
|
||||
|
||||
{{COMMON_RULES}}
|
||||
|
||||
## Common reference commands
|
||||
|
||||
{{COMMON_COMMANDS}}
|
||||
@@ -0,0 +1,227 @@
|
||||
# Worked Examples
|
||||
|
||||
Six concrete pipelines covering different video styles. Each shows the team
|
||||
composition, task graph, and skill/tool choices the orchestrator would make
|
||||
for that brief. **These are illustrative, not templates** — adapt to the
|
||||
actual brief.
|
||||
|
||||
## Example 1 — Narrative short film (text-to-image → image-to-video → cut)
|
||||
|
||||
**Brief:** A 90-second noir-style short. A detective walks through a rainy
|
||||
city. Voiceover narration. AI-generated visuals.
|
||||
|
||||
**Team:**
|
||||
- `director` — vision, decomposition, approval
|
||||
- `writer` — script + voiceover copy (loads `humanizer` for natural voice)
|
||||
- `storyboarder` — beat-by-beat shot list (loads `excalidraw`)
|
||||
- `image-generator` — generates each shot's still via local ComfyUI workflows
|
||||
(loads `comfyui`)
|
||||
- `image-to-video-generator` — animates each still (Runway/Kling, OR
|
||||
ComfyUI's AnimateDiff/WAN workflows via `comfyui`)
|
||||
- `voice-talent` — narration via ElevenLabs
|
||||
- `audio-mixer` — VO + ambient pad
|
||||
- `editor` — assembly + transitions
|
||||
- `reviewer` — final QA
|
||||
|
||||
**Task graph:**
|
||||
```
|
||||
T0 director decompose
|
||||
T1 writer script + voiceover.md (parent: T0)
|
||||
T2 storyboarder shot list with framing per beat (parent: T1)
|
||||
T3 image-generator one still per shot (~12 shots) (parent: T2)
|
||||
T4 image-to-video animate each still (parent: T3)
|
||||
T5 voice-talent generate narration audio (parent: T1)
|
||||
T6 audio-mixer mix VO + ambient (parent: T5)
|
||||
T7 editor cut + transitions + audio mux (parents: T4, T6)
|
||||
T8 reviewer final QA (parent: T7)
|
||||
```
|
||||
|
||||
**Key choices:**
|
||||
- Local ComfyUI via `comfyui` skill is preferred over external API for
|
||||
cost/control — but external APIs are fine if ComfyUI isn't installed
|
||||
- `editor` profile is ffmpeg-only, no Hermes skill required beyond
|
||||
`kanban-worker`
|
||||
- Storyboarder produces `storyboard.excalidraw` alongside the markdown
|
||||
|
||||
## Example 2 — Product / marketing teaser
|
||||
|
||||
**Brief:** A 30-second product teaser for a developer tool. Shows code +
|
||||
terminal + UI screen recordings, voiceover, CTA at end. Square 1:1.
|
||||
|
||||
**Team:**
|
||||
- `director`
|
||||
- `copywriter` — taglines, voiceover script, CTA (loads `humanizer`)
|
||||
- `concept-artist` — style frames (loads `claude-design` for UI mockups)
|
||||
- `renderer-motion-graphics` — animated UI sequences (Remotion CLI)
|
||||
- `renderer-ascii` — terminal-style demo scenes (loads `ascii-video`)
|
||||
- `voice-talent` — VO via ElevenLabs
|
||||
- `editor` — assembly + brand-color treatment
|
||||
- `audio-mixer` — VO + light music bed
|
||||
- `captioner` — burned subtitles for muted-autoplay platforms
|
||||
- `masterer` — produces 1:1 + 9:16 + 16:9 variants
|
||||
|
||||
**Task graph:**
|
||||
```
|
||||
T0 director decompose
|
||||
T1 copywriter copy.md + cta + vo script (parent: T0)
|
||||
T2 concept-artist visual-spec.md + style frames (parent: T1)
|
||||
T3a renderer-motion-graphics scene 1: UI sequence (parent: T2)
|
||||
T3b renderer-ascii scene 2: terminal demo (parent: T2)
|
||||
T3c renderer-motion-graphics scene 3: feature highlight (parent: T2)
|
||||
T3d renderer-motion-graphics scene 4: CTA card (parent: T2)
|
||||
T4 voice-talent narration (parent: T1)
|
||||
T5 audio-mixer VO + music bed (parent: T4)
|
||||
T6 editor cut + transitions (parents: T3*, T5)
|
||||
T7 captioner SRT + burned subtitles (parent: T6)
|
||||
T8 masterer 1:1, 9:16, 16:9 variants (parent: T7)
|
||||
```
|
||||
|
||||
**Key choices:**
|
||||
- Multiple specialized renderers (motion-graphics + ASCII) coexist
|
||||
- Captioner is included because muted autoplay is the norm on social
|
||||
- `claude-design` skill for UI mockups maps directly to the product video idiom
|
||||
|
||||
## Example 3 — Music video (synced to provided track)
|
||||
|
||||
**Brief:** A 3-minute music video for a provided lo-fi hip-hop track. Visuals
|
||||
should pulse with the beat. Generative + ASCII hybrid. Vertical 9:16.
|
||||
|
||||
**Team:**
|
||||
- `director`
|
||||
- `music-supervisor` — analyze track, emit `audio/beats.json` (loads `songsee`)
|
||||
- `storyboarder` — beat-aligned shot list (loads `excalidraw`)
|
||||
- `renderer-ascii` — ASCII scenes synced to bass kicks (loads `ascii-video`)
|
||||
- `renderer-p5js` — generative particle scenes synced to highs (loads `p5js`)
|
||||
- `editor` — beat-cut assembly using `beats.json`
|
||||
- `reviewer` — sync QA
|
||||
|
||||
**Task graph:**
|
||||
```
|
||||
T0 director decompose
|
||||
T1 music-supervisor analyze track → beats.json + spectrogram (parent: T0)
|
||||
T2 storyboarder shot list aligned to beats (parents: T1, T0)
|
||||
T3a renderer-ascii scene 1: bass-driven ASCII (parent: T2)
|
||||
T3b renderer-p5js scene 2: high-end particle field (parent: T2)
|
||||
... (more scenes)
|
||||
T4 editor cut to beats + mux track (parents: T3*, T1)
|
||||
T5 reviewer sync QA + final approval (parent: T4)
|
||||
```
|
||||
|
||||
**Key choices:**
|
||||
- `music-supervisor` runs FIRST — `beats.json` gates the renderers
|
||||
- `editor` uses `beats.json` directly to align cuts to bass kicks
|
||||
- No voice-talent — music is the audio
|
||||
- Two specialized renderers (`ascii-video` + `p5js`) for visual variety
|
||||
|
||||
## Example 4 — Math/algorithm explainer
|
||||
|
||||
**Brief:** A 2-minute explainer of an algorithm. 3Blue1Brown-style. Animated
|
||||
diagrams, equations, narration. Square 1:1.
|
||||
|
||||
**Team:**
|
||||
- `director`
|
||||
- `writer` — narration script (loads `humanizer`)
|
||||
- `cinematographer` — visual spec (loads `manim-video`)
|
||||
- `renderer-manim` — all animated scenes (loads `manim-video`)
|
||||
- `voice-talent` — narration via ElevenLabs
|
||||
- `editor` — assembly + audio mux
|
||||
- `captioner` — burned subtitles
|
||||
|
||||
**Task graph:**
|
||||
```
|
||||
T0 director decompose
|
||||
T1 writer script + narration (parent: T0)
|
||||
T2 cinematographer visual spec for all scenes (parent: T1)
|
||||
T3a-Tn renderer-manim scenes 1..N (parents: T2)
|
||||
T4 voice-talent narration audio (parent: T1)
|
||||
T5 editor cut + mux (parents: T3*, T4)
|
||||
T6 captioner SRT + burn (parent: T5)
|
||||
```
|
||||
|
||||
**Key choices:**
|
||||
- `manim-video` skill drives both the cinematographer (visual language) and
|
||||
the renderer (actual scene production)
|
||||
- The `manim-video` skill's reference docs (animation-design-thinking,
|
||||
scene-planning, equations) auto-load when needed via the renderer's pinned skill
|
||||
|
||||
## Example 5 — ASCII video, music-track-only
|
||||
|
||||
**Brief:** A 60-second pure-ASCII video reactive to an existing track. No
|
||||
voiceover, no other tools. Square 1:1.
|
||||
|
||||
**Team:**
|
||||
- `director`
|
||||
- `music-supervisor` — track analysis (loads `songsee`)
|
||||
- `renderer-ascii` — all visuals (loads `ascii-video`)
|
||||
- `editor` — assembly + audio mux
|
||||
|
||||
**Task graph:**
|
||||
```
|
||||
T0 director decompose
|
||||
T1 music-supervisor analyze track (parent: T0)
|
||||
T2a renderer-ascii scene 1 (parents: T1, T0)
|
||||
T2b renderer-ascii scene 2 (parents: T1, T0)
|
||||
T2c renderer-ascii scene 3 (parents: T1, T0)
|
||||
T3 editor stitch + mux audio (parents: T2*)
|
||||
```
|
||||
|
||||
**Key choices:**
|
||||
- Minimal team (4 profiles) for a focused single-tool project
|
||||
- No reviewer — short experimental piece, director approves directly
|
||||
- All scenes run through one `renderer-ascii` profile because the `ascii-video`
|
||||
skill covers everything
|
||||
|
||||
This example illustrates the rule: **don't over-decompose**. Three scenes
|
||||
through one renderer is fine. Don't spawn three renderer profiles.
|
||||
|
||||
## Example 6 — Real-time / installation art
|
||||
|
||||
**Brief:** A 2-minute audio-reactive visual for a gallery installation. Driven
|
||||
by an audio input feed. TouchDesigner-based. 16:9 4K.
|
||||
|
||||
**Team:**
|
||||
- `director`
|
||||
- `cinematographer` — visual language spec (loads `touchdesigner-mcp`)
|
||||
- `renderer-touchdesigner` — all visuals + record-to-disk
|
||||
(loads `touchdesigner-mcp`)
|
||||
- `audio-mixer` — final loudness pass on the captured audio (optional if
|
||||
pre-mixed source)
|
||||
- `editor` — assemble final clip from TouchDesigner recording
|
||||
- `reviewer` — visual QA
|
||||
|
||||
**Task graph:**
|
||||
```
|
||||
T0 director decompose
|
||||
T1 cinematographer TD operator graph spec (parent: T0)
|
||||
T2 renderer-touchdesigner build TD network + record output (parent: T1)
|
||||
T3 editor trim + audio mux (parent: T2)
|
||||
T4 reviewer final QA (parent: T3)
|
||||
```
|
||||
|
||||
**Key choices:**
|
||||
- `touchdesigner-mcp` controls a running TouchDesigner instance — the
|
||||
cinematographer designs the operator graph, renderer builds it
|
||||
- Output is a recording from the running TD network, not a render-to-frames
|
||||
process; editor mostly just trims
|
||||
|
||||
## Pattern recognition
|
||||
|
||||
When the user describes a video, look for these signals to map to an example:
|
||||
|
||||
- **Plot, characters, scripted dialogue** → Example 1 (narrative)
|
||||
- **Specific product, CTA, brand colors, voiceover** → Example 2 (marketing)
|
||||
- **Track file provided, "synced to music"** → Example 3 (music video)
|
||||
- **"Explain how X works", math/algorithm/concept walkthrough** → Example 4 (manim explainer)
|
||||
- **Terminal aesthetic, ASCII, retro pixel** → Example 5 (ASCII)
|
||||
- **"Audio-reactive", "real-time", "installation"** → Example 6 (TouchDesigner)
|
||||
- **Comic-style narrative** → use `renderer-comic` (`baoyu-comic` skill)
|
||||
- **Retro game / pixel-art aesthetic** → use `renderer-pixel` (`pixel-art` skill)
|
||||
- **3D scene, photoreal environment** → use `renderer-3d` (`blender-mcp`)
|
||||
- **Generative art, particle system, shader** → use `renderer-p5js` (`p5js`)
|
||||
- **AI-generated photoreal stills + animation** → use `renderer-comfyui`
|
||||
(`comfyui`) for both stills and image-to-video
|
||||
- **"video about how the system works", recursive demo** → composable from
|
||||
any of the above; the recursion is a rendering technique, not a style
|
||||
|
||||
The actual team should be derived from the specific brief — these examples are
|
||||
starting points, not endpoints.
|
||||
@@ -0,0 +1,166 @@
|
||||
# Intake — Discovery Question Banks
|
||||
|
||||
The discovery process is **adaptive**. Always start with three baseline
|
||||
questions to identify the broad style category, then drill into a per-style
|
||||
question bank. Ask 2-4 questions at a time, listen, then proceed. Make
|
||||
reasonable assumptions whenever the user implies an answer.
|
||||
|
||||
## Tier 0 — Baseline (always ask)
|
||||
|
||||
1. **What is the video?** — One-sentence pitch
|
||||
2. **How long?** — Approximate duration
|
||||
3. **Aspect ratio + target platform?** — 16:9 / 9:16 / 1:1 / 4:5; X, IG, YouTube, internal, etc.
|
||||
|
||||
From these answers, classify the style category and pick the relevant Tier 1
|
||||
follow-ups. **Do not** continue asking until you have at least these three.
|
||||
|
||||
## Style classification
|
||||
|
||||
Map the brief to one of these archetypes (or a hybrid):
|
||||
|
||||
| Archetype | Tells |
|
||||
|-----------|-------|
|
||||
| **Narrative film** | Plot, characters, scenes-with-events, dialogue, location |
|
||||
| **Product / marketing** | A specific product or feature being shown / sold; CTA at end |
|
||||
| **Music video** | A specific track exists; visuals sync to music |
|
||||
| **Explainer / educational** | A concept being taught; voiceover-driven |
|
||||
| **Tutorial / changelog** | Software demo, terminal-heavy, technical |
|
||||
| **ASCII / terminal art** | Retro terminal aesthetic explicit, character-grid |
|
||||
| **Abstract / loop** | Generative, no plot, often perfect-loop |
|
||||
| **Documentary / interview cut** | Real footage, transcription-driven |
|
||||
| **Real-time / installation** | Audio-reactive, gallery installation, VJ output |
|
||||
|
||||
If ambiguous, **ask** which category fits — don't guess. Hybrids are common
|
||||
(e.g., a product video with a narrative arc); decompose into the dominant
|
||||
mode + secondary modifiers.
|
||||
|
||||
**Recursive / meta** ("a video that shows its own production") is a
|
||||
*rendering technique*, not a separate style — compose it from any of the
|
||||
above by adding a two-pass render step where pass 2 uses pass 1's output as
|
||||
texture inside the final scene.
|
||||
|
||||
## Tier 1 — Per-style follow-ups
|
||||
|
||||
### Narrative film
|
||||
|
||||
- **Setting / world?** — When and where the story takes place
|
||||
- **Characters?** — How many, archetypes, who carries dialogue
|
||||
- **Beat list or full script?** — Has the user written the story or do we draft it
|
||||
- **Dialogue language?** — Spoken lines, on-screen subs only, silent
|
||||
- **Visual generation approach?** — Text-to-image (FAL/Midjourney/Imagen) →
|
||||
image-to-video (Runway/Kling), 3D animation (Blender), 2D animation,
|
||||
procedural, or hybrid
|
||||
- **Voice approach?** — TTS (which voice), recorded VO, no dialogue
|
||||
- **Music / score?** — Commissioned (via `songwriting-and-ai-music` Suno
|
||||
prompts, or local `heartmula`), licensed track provided, silent
|
||||
|
||||
### Product / marketing
|
||||
|
||||
- **Product?** — Name, what it does, key feature being shown
|
||||
- **Target audience?** — Who's watching, what they care about
|
||||
- **CTA?** — Visit URL, install, sign up, etc.
|
||||
- **Tone?** — Serious, playful, technical, premium, edgy
|
||||
- **Brand assets available?** — Logo files, color palette, fonts, existing footage
|
||||
- **Animation style?** — Motion graphics (Remotion / AE-style), screen recording,
|
||||
generative, illustrated
|
||||
- **Voiceover?** — Yes (which voice / language) or text-only
|
||||
- **Music?** — Track provided, license-free needed, custom-composed
|
||||
|
||||
### Music video
|
||||
|
||||
- **Track file?** — Path to the audio (essential — we'll analyze BPM + beats)
|
||||
- **Track length to use?** — Full song or a section
|
||||
- **Genre / energy?** — Tells what visual rhythm and density to use
|
||||
- **Lyric / narrative content?** — Are there lyrics to render on screen,
|
||||
or is it purely visual?
|
||||
- **Visual reference style?** — Existing music videos / artists for reference
|
||||
- **Performer footage?** — None, has clips, will provide
|
||||
- **Visual generation approach?** — Per-beat generative, edit-driven cuts of stock
|
||||
footage, illustrated, hybrid
|
||||
|
||||
### Explainer / educational
|
||||
|
||||
- **What concept is being taught?** — One-sentence concept, key takeaway
|
||||
- **Audience expertise?** — Beginner / intermediate / expert
|
||||
- **Diagram density?** — Heavy math / formulas / code / abstract concepts
|
||||
- **Voiceover?** — TTS / recorded / on-screen text only
|
||||
- **Tool preference?** — `manim-video` (math), `p5js` (generative),
|
||||
Remotion (UI motion graphics), `comfyui` (AI-generated visuals),
|
||||
`ascii-video` (technical/retro), hybrid
|
||||
- **Pacing?** — Fast and dense (3Blue1Brown) or slow and contemplative
|
||||
|
||||
### Tutorial / changelog / software demo
|
||||
|
||||
- **Software being demonstrated?** — Name, what it does
|
||||
- **Demo script?** — Sequence of commands / screens to show
|
||||
- **Terminal-only or with GUI?**
|
||||
- **Voiceover for narration?**
|
||||
- **Diagram support needed?** — Often these benefit from a diagram skill
|
||||
alongside the screen-capture/render step (`excalidraw`,
|
||||
`architecture-diagram`, `concept-diagrams`)
|
||||
|
||||
### ASCII / terminal art
|
||||
|
||||
- **Source material?** — Generative / driven by audio / converting existing
|
||||
video / static image starting point
|
||||
- **Color palette?** — Brand-driven (gold/black/blue), Matrix green, full
|
||||
rainbow, monochrome
|
||||
- **Audio reactivity?** — None / loose mood / tight beat sync / FFT-driven
|
||||
- **Character set?** — ASCII only / Unicode block-drawing / mystic glyphs
|
||||
- **Loop or narrative?** — Perfect loop or one-shot
|
||||
|
||||
### Abstract / loop
|
||||
|
||||
- **Mood / emotion?** — One word that captures the feel
|
||||
- **Motion type?** — Zoom-into-itself, particle drift, wave, geometric, organic
|
||||
- **Loop required?** — Perfect loop (Droste-style) or just satisfying ending
|
||||
- **Audio?** — Silent, ambient pad, beat-synced
|
||||
|
||||
### Documentary / interview cut
|
||||
|
||||
- **Source footage?** — Provided clips, length per clip
|
||||
- **Transcript / subtitles?** — Provided or to be generated
|
||||
- **Story structure?** — Chronological / thematic / arc
|
||||
- **B-roll approach?** — Generated, stock library, none
|
||||
|
||||
### Real-time / installation
|
||||
|
||||
- **Output environment?** — Gallery wall, projector, screen, web embed
|
||||
- **Audio source?** — Live audio input, pre-recorded track, both
|
||||
- **Reactivity tightness?** — Mood-level (loose) vs. tight beat-sync vs. live
|
||||
parameter control
|
||||
- **Tool preference?** — `touchdesigner-mcp` for full TD operator graphs;
|
||||
`p5js` for web-canvas; `comfyui` for generative-AI fed by audio features
|
||||
|
||||
## Tier 2 — Always ask near the end
|
||||
|
||||
- **Brand assets path?** — Where logo / color palette / fonts / music library lives
|
||||
- **Output format requirements?** — Codec preference, target file size, accepted
|
||||
alternates (vertical cut, GIF, audio-only)
|
||||
- **Deadline?** — Affects task `max_runtime_seconds` and acceptable scope
|
||||
- **Quality bar?** — Rough draft for review / polished final / archival
|
||||
- **Existing footage / assets to reuse?** — Anything that should appear, not just inform
|
||||
|
||||
## Reasonable assumption defaults
|
||||
|
||||
When the user under-specifies, fill in these defaults rather than asking:
|
||||
|
||||
| Question | Default |
|
||||
|----------|---------|
|
||||
| Frame rate | 30 fps for X / IG; 60 fps for tutorials/explainers; 24 fps for narrative film |
|
||||
| Resolution | 1080×1080 for square, 1920×1080 for 16:9, 1080×1920 for 9:16 |
|
||||
| Codec | H.264 / yuv420p, CRF 18 |
|
||||
| Audio codec | AAC 192 kbps |
|
||||
| Voice | Provider's mid-range neutral voice unless brand calls for distinctive timbre |
|
||||
| Music | Silent (require user to specify if music is wanted) |
|
||||
| Captions | On for explainer/tutorial; off for narrative/abstract unless requested |
|
||||
| Quality bar | Polished final unless user says draft |
|
||||
|
||||
State the assumption explicitly: *"Assuming 30fps and AAC audio unless you say otherwise — proceed?"*
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **Asking 10 questions at once.** Maximum 4 per turn.
|
||||
- **Asking for things the brief already implies.** If the user said "music video for my track," do not ask "is there a track?"
|
||||
- **Failing to classify before drilling in.** Tier-1 questions depend on classification; mixing them up wastes turns.
|
||||
- **Treating "make a video" as enough to proceed.** Always confirm the three baseline questions.
|
||||
@@ -0,0 +1,276 @@
|
||||
# Kanban Setup — Project Bootstrap & Profile Configuration
|
||||
|
||||
Once the brief is locked and the team is designed, the next step is producing
|
||||
the actual `setup.sh` that creates the project workspace, configures Hermes
|
||||
profiles, and fires the initial kanban task.
|
||||
|
||||
This file documents the patterns. The companion script
|
||||
`scripts/bootstrap_pipeline.py` automates most of it from a structured input
|
||||
JSON.
|
||||
|
||||
> **Credit:** the single-project-workspace layout, profile-config patching
|
||||
> approach, SOUL.md-per-profile convention, and `--workspace dir:<path>` rule
|
||||
> are adapted from alt-glitch's original multi-agent video pipeline:
|
||||
> [NousResearch/kanban-video-pipeline](https://github.com/NousResearch/kanban-video-pipeline).
|
||||
> This skill generalizes those patterns across video styles and replaces the
|
||||
> string-replacement config patcher with a PyYAML-based one.
|
||||
|
||||
## Project workspace structure
|
||||
|
||||
Every video project gets one workspace under `~/projects/video-pipeline/<slug>/`:
|
||||
|
||||
```
|
||||
~/projects/video-pipeline/<slug>/
|
||||
├── brief.md ← the contract; all tasks reference
|
||||
├── TEAM.md ← team composition + task graph (director reads this)
|
||||
├── taste/
|
||||
│ ├── brand-guide.md ← color, typography, motion rules
|
||||
│ ├── emotional-dna.md ← what the piece should FEEL like
|
||||
│ └── style-frames/ ← optional: visual references
|
||||
├── audio/
|
||||
│ ├── track.mp3 ← provided music (if any)
|
||||
│ ├── voiceover/ ← per-line TTS clips
|
||||
│ └── sfx/ ← sound effects
|
||||
├── assets/
|
||||
│ ├── logos/
|
||||
│ ├── fonts/
|
||||
│ └── existing-footage/ ← reusable provided clips
|
||||
├── scenes/
|
||||
│ ├── scene-01/
|
||||
│ │ ├── VISUAL_SPEC.md ← cinematographer's per-scene spec
|
||||
│ │ ├── render.py ← renderer's code (or sketch.html, etc.)
|
||||
│ │ ├── checkpoints/ ← preview frames for QA
|
||||
│ │ └── clip.mp4 ← the deliverable for this scene
|
||||
│ ├── scene-02/...
|
||||
│ └── ...
|
||||
├── checkpoints/ ← global review frames
|
||||
├── tools/ ← optional project-local helpers
|
||||
└── output/
|
||||
├── final.mp4 ← stitched + audio
|
||||
├── final-noaudio.mp4
|
||||
├── final-9x16.mp4 ← optional: vertical alternate
|
||||
└── captions.srt ← optional: subtitle file
|
||||
```
|
||||
|
||||
**The slug** is derived from the brief title: lowercase, hyphen-separated.
|
||||
Example: `q3-product-teaser`, `ascii-mood-loop`, `interview-cut-2026-q1`.
|
||||
|
||||
## The setup.sh script
|
||||
|
||||
The setup script does six things in order:
|
||||
|
||||
1. **Create workspace tree** — all directories above
|
||||
2. **Create profiles** — `hermes profile create <name> --clone`
|
||||
3. **Configure profiles** — patch each profile's
|
||||
`~/.hermes/profiles/<name>/config.yaml` to set toolsets, always_load skills,
|
||||
and `cwd`
|
||||
4. **Write SOUL.md per profile** — the personality + role definition
|
||||
5. **Copy any provided assets + write `brief.md`, `TEAM.md`, and `taste/`**
|
||||
6. **Fire the initial kanban task** — `hermes kanban create` assigned to the director
|
||||
|
||||
See `assets/setup.sh.tmpl` for the skeleton.
|
||||
|
||||
### Profile creation pattern
|
||||
|
||||
```bash
|
||||
hermes profile create director --clone 2>/dev/null || true
|
||||
```
|
||||
|
||||
The `--clone` flag clones from the active profile (preserving model, base
|
||||
config). The `|| true` makes the script idempotent — re-running won't error if
|
||||
the profile already exists.
|
||||
|
||||
### Profile config patching
|
||||
|
||||
Each profile has a YAML config at `~/.hermes/profiles/<name>/config.yaml`. The
|
||||
setup script edits exactly two keys:
|
||||
|
||||
1. `toolsets:` — replace the default with the role's required toolsets
|
||||
2. `skills.always_load:` — list the role's must-load skills (may be empty)
|
||||
|
||||
**Do NOT** modify `approvals.mode` (controls user-confirmation of tool calls
|
||||
— a security setting that must stay as the user configured it). **Do NOT**
|
||||
modify `terminal.cwd` — the kanban dispatcher overrides cwd per-task via
|
||||
`--workspace dir:<path>`, so the profile's cwd is irrelevant to the kanban
|
||||
work and changing it could break the user's interactive use of the profile.
|
||||
|
||||
Use **PyYAML**, not string replacement, so the patch is robust against
|
||||
default-config schema drift:
|
||||
|
||||
```bash
|
||||
configure_profile() {
|
||||
local profile="$1"
|
||||
local toolsets_json="$2" # JSON array, e.g. '["kanban","terminal","file"]'
|
||||
local skills_json="$3" # JSON array, e.g. '["kanban-worker","ascii-video"]'
|
||||
python3 - "$profile" "$toolsets_json" "$skills_json" <<'PY'
|
||||
import json, os, sys, yaml
|
||||
profile, ts_json, sk_json = sys.argv[1:4]
|
||||
p = os.path.expanduser(f"~/.hermes/profiles/{profile}/config.yaml")
|
||||
with open(p) as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
cfg["toolsets"] = json.loads(ts_json)
|
||||
cfg.setdefault("skills", {})["always_load"] = json.loads(sk_json)
|
||||
with open(p, "w") as f:
|
||||
yaml.safe_dump(cfg, f, sort_keys=False)
|
||||
PY
|
||||
}
|
||||
```
|
||||
|
||||
PyYAML must be installed in the user's Python (it ships with most Hermes
|
||||
installs). If absent: `pip install pyyaml`.
|
||||
|
||||
The setup script should also **validate** the patch by re-reading the file
|
||||
and comparing — see `assets/setup.sh.tmpl` for the validation pattern.
|
||||
|
||||
### SOUL.md per profile
|
||||
|
||||
Each profile gets a `SOUL.md` at `~/.hermes/profiles/<name>/SOUL.md` that
|
||||
defines its role, voice, and rules. See `assets/soul.md.tmpl` for the
|
||||
template. Customize per role and per project.
|
||||
|
||||
The director's SOUL.md should be the most opinionated — its voice flavors
|
||||
the entire production. **Critical content for the director's SOUL.md:**
|
||||
|
||||
- **Anti-temptation rules:** "Do not execute the work yourself. For every
|
||||
concrete task, create a kanban task and assign it. Decompose, route, comment,
|
||||
approve — that's the whole job." (The `kanban-orchestrator` skill provides
|
||||
the deeper playbook; load it.)
|
||||
- **Decomposition steps:** Read `brief.md`, `TEAM.md`, `taste/`. Use the team
|
||||
graph in `TEAM.md` to fan out tasks.
|
||||
- **The workspace_path rule** (see below).
|
||||
|
||||
Other profiles' SOUL.md is briefer; mostly mechanical: who you are, what you
|
||||
read, what you produce, what skills/tools to use, where to write outputs.
|
||||
Most non-director profiles should `always_load: kanban-worker` for the
|
||||
deeper-than-baseline kanban guidance.
|
||||
|
||||
### Initial kanban task
|
||||
|
||||
The final action of setup.sh is firing the kanban:
|
||||
|
||||
```bash
|
||||
hermes kanban create "Direct production of <video title>" \
|
||||
--assignee director \
|
||||
--workspace dir:"$HOME/projects/video-pipeline/${PROJECT_SLUG}" \
|
||||
--tenant ${PROJECT_SLUG} \
|
||||
--priority 2 \
|
||||
--max-runtime 4h \
|
||||
--body "$(cat <<EOF
|
||||
Read brief.md, TEAM.md, and taste/.
|
||||
Decompose into the team graph defined in TEAM.md.
|
||||
All child tasks MUST use:
|
||||
workspace_kind="dir"
|
||||
workspace_path="$HOME/projects/video-pipeline/${PROJECT_SLUG}"
|
||||
tenant="${PROJECT_SLUG}"
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
The `--workspace dir:<path>` flag is **critical** — it tells the kanban that
|
||||
all child tasks share this workspace. Skipping or using `worktree` will
|
||||
isolate profiles and break artifact sharing.
|
||||
|
||||
## The TEAM.md file
|
||||
|
||||
Alongside `brief.md`, write a `TEAM.md` that the director reads. It documents
|
||||
the team composition + task graph the orchestrator should follow. This
|
||||
removes ambiguity and prevents the director from inventing extra steps.
|
||||
|
||||
Example structure (for an ASCII video with a music supervisor and editor):
|
||||
|
||||
```markdown
|
||||
# Team & Task Graph — <video title>
|
||||
|
||||
## Team
|
||||
|
||||
- `director` (this profile) — vision, decomposition, approval
|
||||
- `cinematographer` — visual spec, quality review (loads `ascii-video`)
|
||||
- `renderer-ascii` — ASCII scenes (loads `ascii-video`)
|
||||
- `music-supervisor` — track analysis (loads `songsee`)
|
||||
- `voice-talent` — narration (uses ElevenLabs API)
|
||||
- `audio-mixer` — final mix (ffmpeg)
|
||||
- `editor` — assembly (ffmpeg)
|
||||
- `reviewer` — final QA gate
|
||||
|
||||
## Task Graph
|
||||
|
||||
T0: this task — decompose
|
||||
│
|
||||
├── T1: cinematographer "Design visual language" (parent: T0)
|
||||
│ │
|
||||
│ ├── T2a: renderer-ascii "Scene 1 — title card" (parent: T1)
|
||||
│ ├── T2b: renderer-ascii "Scene 2 — main beat" (parent: T1)
|
||||
│ ├── T2c: renderer-ascii "Scene 3 — outro" (parent: T1)
|
||||
│
|
||||
├── T3: music-supervisor "Analyze track + emit beats.json" (parent: T0)
|
||||
│
|
||||
├── T4: voice-talent "Generate narration" (parent: T0)
|
||||
│
|
||||
├── T5: audio-mixer "Mix VO + bg music" (parents: T3, T4)
|
||||
│
|
||||
├── T6: editor "Assemble cut + mux audio" (parents: T2*, T5)
|
||||
│
|
||||
└── T7: reviewer "Final QA" (parent: T6)
|
||||
```
|
||||
|
||||
The director turns this into actual `kanban_create` calls.
|
||||
|
||||
## API-key prerequisites check
|
||||
|
||||
Before firing the kanban, verify required keys are available. Check both
|
||||
`~/.hermes/.env` and macOS Keychain (if on macOS):
|
||||
|
||||
```bash
|
||||
check_key() {
|
||||
local var="$1"
|
||||
local kc_account="$2"
|
||||
local kc_service="$3"
|
||||
if grep -q "^${var}=" ~/.hermes/.env 2>/dev/null && \
|
||||
[ -n "$(grep "^${var}=" ~/.hermes/.env | cut -d= -f2-)" ]; then
|
||||
return 0
|
||||
fi
|
||||
if command -v security >/dev/null 2>&1 && \
|
||||
security find-generic-password -a "${kc_account}" -s "${kc_service}" -w >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
echo "ERROR: ${var} not set in ~/.hermes/.env or Keychain (${kc_account}/${kc_service})"
|
||||
return 1
|
||||
}
|
||||
|
||||
check_key ELEVENLABS_API_KEY hermes ELEVENLABS_API_KEY || exit 1
|
||||
check_key OPENROUTER_API_KEY hermes OPENROUTER_API_KEY || exit 1
|
||||
# ...
|
||||
```
|
||||
|
||||
If a key is missing, the script aborts with a clear message rather than
|
||||
firing a kanban that will hit credential errors mid-execution.
|
||||
|
||||
## Critical rules
|
||||
|
||||
1. **`workspace_kind="dir"` + `workspace_path="<absolute>"` on every kanban_create.** Otherwise profiles can't share artifacts.
|
||||
|
||||
2. **Tenant every task.** `--tenant <project-slug>` keeps the dashboard scoped
|
||||
and prevents cross-pollination with other ongoing kanbans.
|
||||
|
||||
3. **Idempotency keys.** For tasks that should not duplicate on re-run (e.g.,
|
||||
setup creating profiles), use the `idempotency_key` argument or check
|
||||
existence first.
|
||||
|
||||
4. **`max_runtime_seconds` per task.** Renderers that get stuck eat compute.
|
||||
Standard defaults:
|
||||
- Renderer task: 1800s (30min)
|
||||
- Editor task: 600s (10min)
|
||||
- Voice-talent task: 300s (5min)
|
||||
- Image-generator task: 600s (10min)
|
||||
- Image-to-video-generator task: 900s (15min)
|
||||
|
||||
5. **Heartbeats for long renders.** Tasks expected to run >5min should emit
|
||||
`kanban_heartbeat` periodically with progress. Renderers should report
|
||||
frame counts; the editor should report assembly progress.
|
||||
|
||||
6. **The `audio/` and `taste/` dirs are populated BEFORE firing the kanban.**
|
||||
Don't ask the director's pipeline to source these — copy at setup time.
|
||||
|
||||
7. **`brief.md` is read-only after setup.** If the brief changes during
|
||||
execution, that's a significant pivot — re-fire the kanban rather than edit
|
||||
live.
|
||||
@@ -0,0 +1,180 @@
|
||||
# Monitoring — Watch the Pipeline + Intervene
|
||||
|
||||
After `setup.sh` fires the kanban, the work runs autonomously. The role of
|
||||
this skill in the execution phase is to help the user (and the AI overseeing
|
||||
the session) detect problems early and intervene effectively.
|
||||
|
||||
## Live monitoring commands
|
||||
|
||||
```bash
|
||||
# Live event stream — task spawns, status changes, heartbeats, completions
|
||||
hermes kanban watch --tenant <project-slug>
|
||||
|
||||
# Snapshot of the board
|
||||
hermes kanban list --tenant <project-slug>
|
||||
hermes kanban list --tenant <project-slug> --json # machine-readable
|
||||
|
||||
# Per-status counts + oldest-ready age
|
||||
hermes kanban stats --tenant <project-slug>
|
||||
|
||||
# Visual dashboard (browser)
|
||||
hermes dashboard
|
||||
|
||||
# Inspect a specific task (includes comments + events)
|
||||
hermes kanban show <task-id>
|
||||
|
||||
# Follow a single task's event stream
|
||||
hermes kanban tail <task-id>
|
||||
```
|
||||
|
||||
Verify available subcommands with `hermes kanban --help` — the kanban CLI
|
||||
ships with `init / create / list / show / assign / link / unlink / claim /
|
||||
comment / complete / block / unblock / archive / tail / dispatch / watch /
|
||||
stats / heartbeat / log / runs / context / gc`.
|
||||
|
||||
The companion `scripts/monitor.py` polls the kanban via the CLI and surfaces
|
||||
common issues (stuck tasks, missing heartbeats, repeated retries, dependency
|
||||
deadlocks).
|
||||
|
||||
## What to watch for
|
||||
|
||||
### Healthy pipeline indicators
|
||||
|
||||
- Tasks transition `READY → RUNNING → DONE` in roughly the expected order
|
||||
- Renderers emit periodic `kanban_heartbeat` events with progress (e.g. "frame
|
||||
240/720")
|
||||
- Each task's runtime is well under its `max_runtime_seconds` cap
|
||||
- No task accumulates more than 1 retry
|
||||
- Dependency arrows resolve (children unblock as parents complete)
|
||||
|
||||
### Warning signs
|
||||
|
||||
| Symptom | Likely cause | Action |
|
||||
|---------|--------------|--------|
|
||||
| Task RUNNING but no heartbeat in 2+ min | Worker stuck, infinite loop, blocked on input | `hermes kanban show <id>` — read the worker's last events. The dispatcher SIGTERMs tasks that exceed their `max-runtime`; if you need to stop one earlier, `hermes kanban block <id>` then `hermes kanban archive <id>`, and create a re-run task. |
|
||||
| Same task retried 2+ times | Reproducible failure (missing key, bad spec, broken tool) | `hermes kanban show <id>` to read failure events. Fix root cause before re-running. |
|
||||
| RUNNING longer than max_runtime | Task is slow but progressing OR genuinely stuck | Check heartbeats with `hermes kanban tail <id>`. If progressing, the dispatcher will SIGTERM eventually anyway — raise `max-runtime` on a re-created task. |
|
||||
| Child task READY but parents still RUNNING for >2× expected | Cascade slow, dependency miswired | Check the dependency graph. Inspect the parent: sometimes it completed but its handoff fields (summary, metadata) were empty so the child has nothing to consume. |
|
||||
| New tasks not appearing | Director is hung in decomposition | Inspect director task with `kanban show`. Often a malformed `kanban_create` call. |
|
||||
| Specialist tasks completing instantly | Decomposition created tasks without bodies | Director didn't pass enough context. Re-create with explicit body content. |
|
||||
| Tasks created but never picked up | Profile not running, or tenant mismatch, or dispatcher not running | Check `hermes profile list` (profile exists?), `hermes status` (gateway/dispatcher up?), and verify tenant. |
|
||||
| Specific renderer task fails → review note → renderer redoes → fails again | Brief is asking for the impossible | Pivot the brief, not the renderer. |
|
||||
|
||||
## Intervention recipes
|
||||
|
||||
### Rejecting bad output
|
||||
|
||||
When a renderer ships a clip that doesn't pass review:
|
||||
|
||||
```bash
|
||||
# 1. Comment on the renderer's task with specific feedback
|
||||
hermes kanban comment <renderer-task-id> "Scene 3 looks too sparse \
|
||||
— increase visual density. Tighten color palette to brand spec."
|
||||
|
||||
# 2. Create a re-render task with the original as parent
|
||||
hermes kanban create "Scene 3 — re-render with feedback" \
|
||||
--assignee renderer-ascii \
|
||||
--parent <renderer-task-id> \
|
||||
--workspace dir:"$HOME/projects/video-pipeline/<slug>" \
|
||||
--tenant <slug> \
|
||||
--skill ascii-video \
|
||||
--max-runtime 30m
|
||||
```
|
||||
|
||||
### Adding a new dependency mid-flight
|
||||
|
||||
When the editor needs an asset that wasn't originally planned (e.g., a captions
|
||||
file):
|
||||
|
||||
```bash
|
||||
# 1. Create the new task and capture its id
|
||||
NEW_TASK_ID=$(hermes kanban create "Generate SRT captions from voiceover" \
|
||||
--assignee captioner \
|
||||
--workspace dir:"$HOME/projects/video-pipeline/<slug>" \
|
||||
--tenant <slug> \
|
||||
--json | python3 -c "import json,sys;print(json.load(sys.stdin)['id'])")
|
||||
|
||||
# 2. Wire it as a parent of the editor's task with `kanban link`
|
||||
hermes kanban link "$NEW_TASK_ID" <editor-task-id>
|
||||
```
|
||||
|
||||
`kanban link` takes `parent_id child_id` (parent first). Use `kanban unlink`
|
||||
to remove a dependency.
|
||||
|
||||
### Stopping a worker that's stuck
|
||||
|
||||
The kanban dispatcher will SIGTERM (then SIGKILL) any task that exceeds its
|
||||
`--max-runtime` automatically. To stop one sooner:
|
||||
|
||||
```bash
|
||||
# Mark blocked so the dispatcher leaves it alone, then archive
|
||||
hermes kanban block <task-id>
|
||||
hermes kanban archive <task-id>
|
||||
|
||||
# Diagnose what happened
|
||||
hermes kanban show <task-id> # task body, comments, recent events
|
||||
hermes kanban tail <task-id> # follow the live event stream
|
||||
hermes kanban log <task-id> # worker process log
|
||||
```
|
||||
|
||||
After stopping, decide: fix root cause + re-create the task, or skip and
|
||||
adjust dependent tasks.
|
||||
|
||||
### Pivoting the brief
|
||||
|
||||
If during execution the user wants something fundamentally different:
|
||||
|
||||
1. Cancel the active director task and all RUNNING children
|
||||
2. Edit `brief.md` and `TEAM.md`
|
||||
3. Re-fire the initial `hermes kanban create` for the director
|
||||
|
||||
Don't try to "edit while running" — the kanban's audit trail makes a clean
|
||||
pivot more legible than mid-stream changes.
|
||||
|
||||
## Periodic check-in script
|
||||
|
||||
A simple polling pattern for hands-off monitoring:
|
||||
|
||||
```bash
|
||||
while true; do
|
||||
clear
|
||||
hermes kanban list --tenant <slug>
|
||||
echo "---"
|
||||
hermes kanban stats --tenant <slug>
|
||||
sleep 30
|
||||
done
|
||||
```
|
||||
|
||||
For a live event feed, run `hermes kanban watch --tenant <slug>` in a
|
||||
separate terminal — it streams task lifecycle events as they happen.
|
||||
|
||||
For automated intervention (auto-restart stuck tasks, auto-create re-render on
|
||||
review failure), see the `scripts/monitor.py` patterns.
|
||||
|
||||
## When to call it done
|
||||
|
||||
The pipeline is finished when:
|
||||
|
||||
1. All RENDER tasks complete and pass review
|
||||
2. The editor's `output/final.mp4` exists and `ffprobe` confirms expected
|
||||
duration + streams
|
||||
3. The reviewer (if present) has approved
|
||||
4. Optional masterer variants exist
|
||||
|
||||
At this point, present the final.mp4 path to the user along with any review
|
||||
notes. Do NOT delete the workspace — the user may want to iterate on a single
|
||||
scene without re-running the whole pipeline.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **Tenant mismatches.** A task created with the wrong tenant won't appear in
|
||||
monitoring. Always pass `--tenant <slug>` consistently.
|
||||
- **Profile process not running.** Tasks queue indefinitely in READY if no
|
||||
worker for that profile is online. Check `hermes profile list` and start
|
||||
any missing profiles.
|
||||
- **Workspace permissions.** All profiles need read+write to the workspace
|
||||
directory. `chmod -R u+rw <workspace>` if any worker reports permission
|
||||
errors.
|
||||
- **Audio/visual sync.** The editor's clip stitching must match the
|
||||
renderer's actual output durations. Don't hardcode scene durations in
|
||||
the editor — read from the renderer's handoff metadata.
|
||||
@@ -0,0 +1,298 @@
|
||||
# Role Archetypes
|
||||
|
||||
The library of role archetypes for video production. **Compose a team from this
|
||||
list, don't clone a fixed roster.** Most videos need 4-7 profiles. The director
|
||||
is always present; everything else is conditional on the brief.
|
||||
|
||||
Each role's profile name is by convention `kebab-case` (e.g. `creative-director`,
|
||||
`image-generator`). Multiple instances of the same role get descriptive suffixes
|
||||
when they need different focus (e.g., `renderer-ascii`, `renderer-3d`).
|
||||
|
||||
For toolset + skill mapping per role, see [tool-matrix.md](tool-matrix.md).
|
||||
|
||||
## Always present
|
||||
|
||||
### director
|
||||
|
||||
The vision-holder. Reads the brief and brand guide, decomposes into a task
|
||||
graph, comments to steer creative direction, approves the final cut.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-orchestrator`. The kanban plugin auto-injects baseline
|
||||
orchestration guidance for free; `kanban-orchestrator` is the deeper
|
||||
decomposition playbook. Add `creative-ideation` if the brief is wide-open
|
||||
and needs framing help.
|
||||
- **Personality:** Tied to the brand voice — see `assets/soul.md.tmpl`
|
||||
|
||||
The director has the same toolset as everyone else, but its `SOUL.md` rules
|
||||
**forbid** execution. The "decompose, don't execute" discipline is enforced
|
||||
by personality + the kanban-orchestrator skill, not by missing tools.
|
||||
|
||||
## Pre-production roles
|
||||
|
||||
Pick based on what the brief needs.
|
||||
|
||||
### writer / screenwriter
|
||||
|
||||
Writes scripts, dialogue, voiceover copy, narration. Use for any video with
|
||||
spoken or written words beyond a tagline.
|
||||
|
||||
- **Toolsets:** kanban, file
|
||||
- **Skills:** `kanban-worker`, `humanizer` (post-process to strip AI-tells)
|
||||
- **Outputs:** `script.md`, `narration.md`, `dialogue/scene-NN.md`
|
||||
|
||||
### copywriter
|
||||
|
||||
Like `writer` but specifically for marketing copy: taglines, CTAs, voiceover
|
||||
scripts for product videos.
|
||||
|
||||
- **Toolsets:** kanban, file
|
||||
- **Skills:** `kanban-worker`, `humanizer`
|
||||
- **Outputs:** `copy.md`
|
||||
|
||||
### concept-artist / visual-designer
|
||||
|
||||
Develops the visual identity: mood board, style frames, color palette
|
||||
rationale, typography choices. Produces a `visual-spec.md` that all generators
|
||||
follow. Often produces still reference frames using image-generation APIs or
|
||||
local skills.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker` plus any project-specific design skill —
|
||||
`claude-design` (UI/web), `sketch` (quick mockup variants),
|
||||
`popular-web-designs` (matching known web aesthetic), `pixel-art` (retro),
|
||||
`ascii-art` (terminal/retro), `excalidraw` (hand-drawn frames),
|
||||
`design-md` (text-based design docs)
|
||||
- **Outputs:** `visual-spec.md`, `taste/style-frames/*.png`
|
||||
|
||||
### storyboarder
|
||||
|
||||
Maps the brief to a beat-by-beat shot list with timing. Critical for narrative
|
||||
film and music video. Often pairs with a diagramming tool.
|
||||
|
||||
- **Toolsets:** kanban, file
|
||||
- **Skills:** `kanban-worker` plus a diagram skill — `excalidraw` (sketch),
|
||||
`architecture-diagram` (technical/system), `concept-diagrams` (educational/
|
||||
scientific)
|
||||
- **Outputs:** `storyboard.md` with one row per scene/shot, optional
|
||||
storyboard sketches
|
||||
|
||||
### cinematographer / dp
|
||||
|
||||
Designs the visual language: framing, color, motion, transitions. Reviews
|
||||
generator output for visual consistency. Hands off per-scene `VISUAL_SPEC.md`.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file, video, vision
|
||||
- **Skills:** `kanban-worker` plus the visual skill that matches the project
|
||||
(e.g., `ascii-video` for ASCII work, `manim-video` for explainers,
|
||||
`touchdesigner-mcp` for real-time visuals, etc.)
|
||||
- **Outputs:** `scenes/scene-NN/VISUAL_SPEC.md`, review comments on renderer
|
||||
tasks
|
||||
- **Reviews via:** `video_analyze` (sends full clip to multimodal LLM for
|
||||
native review), `vision_analyze` for spot-checking frames, ffprobe summaries
|
||||
|
||||
## Production roles
|
||||
|
||||
### renderer (generic)
|
||||
|
||||
A worker that produces visual content for one or more scenes. Loaded with
|
||||
whichever creative skill fits the scene's style. Multiple renderers can run in
|
||||
parallel, each pinned to a different skill via `always_load` in their profile
|
||||
or `--skill` on the task.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** one creative skill (see specialized variants below)
|
||||
- **Outputs:** `scenes/scene-NN/clip.mp4`
|
||||
|
||||
### Specialized renderer variants
|
||||
|
||||
When scenes need very different tools, create specialized renderer profiles
|
||||
instead of overloading one. Each loads a different creative skill.
|
||||
|
||||
| Variant | Skill | Best for |
|
||||
|---------|-------|----------|
|
||||
| `renderer-ascii` | `ascii-video` | Terminal aesthetic, retro pixel, audio-reactive grid, video-to-ASCII conversion |
|
||||
| `renderer-manim` | `manim-video` | Math, algorithms, 3Blue1Brown-style explainers, equation derivations |
|
||||
| `renderer-p5js` | `p5js` | Generative art, particles, shaders, organic motion, web-canvas content |
|
||||
| `renderer-comfyui` | `comfyui` | AI-generated stills + video using local ComfyUI workflows (img-to-img, img-to-video, etc.) |
|
||||
| `renderer-touchdesigner` | `touchdesigner-mcp` | Real-time, audio-reactive, installation art, VJ-style content |
|
||||
| `renderer-3d` | `blender-mcp` *(optional)* | 3D modeling, animation, photoreal environments, character animation |
|
||||
| `renderer-pixel` | `pixel-art` | Retro game aesthetic with era-correct palettes |
|
||||
| `renderer-comic` | `baoyu-comic` | Knowledge-comic style narrative scenes |
|
||||
| `renderer-meme` | `meme-generation` *(optional)* | Meme-style stills for satirical/social content |
|
||||
| `renderer-procedural` | (none — Python with PIL + ffmpeg directly) | Custom procedural content where no skill fits |
|
||||
| `renderer-video` | (external image-to-video API: Runway / Kling / Luma) | Animating still images in narrative film |
|
||||
| `renderer-motion-graphics` | (external — Remotion CLI) | Motion graphics, kinetic typography, UI animations |
|
||||
|
||||
For external-API renderers, the profile holds the API client logic; only
|
||||
`kanban-worker` is loaded, plus the terminal toolset and the API key.
|
||||
|
||||
### image-generator
|
||||
|
||||
Specifically for text-to-image generation. Often produces stills that go to
|
||||
`renderer-video` for animation.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`, optionally `comfyui` (drives a local
|
||||
ComfyUI install for image generation)
|
||||
- **External APIs (alternative to local ComfyUI):** FAL, Replicate, OpenAI
|
||||
Images, Midjourney
|
||||
- **Outputs:** `scenes/scene-NN/stills/*.png`
|
||||
|
||||
### image-to-video-generator
|
||||
|
||||
Takes still images and animates them via Runway/Kling/Luma APIs, or via
|
||||
ComfyUI's image-to-video workflows locally. Almost always follows
|
||||
`image-generator` in narrative film pipelines.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`, optionally `comfyui` (for local image-to-video
|
||||
workflows like AnimateDiff or WAN)
|
||||
- **External APIs:** Runway, Kling, Luma, Pika
|
||||
- **Outputs:** `scenes/scene-NN/clip.mp4`
|
||||
|
||||
### music-supervisor
|
||||
|
||||
Sources, analyzes, and prepares the music track. For music videos, also
|
||||
produces a beat/BPM map and key-moment timestamps. Uses `songsee` for
|
||||
spectrograms when the editor or renderer needs a visual reference of the
|
||||
audio's energy.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`, `songsee` (audio visualization), plus one of:
|
||||
- `songwriting-and-ai-music` — when commissioning lyrics + Suno prompts
|
||||
- `heartmula` — when generating music with the open-source local model
|
||||
- `spotify` — when sourcing existing tracks
|
||||
- **Outputs:** `audio/track.mp3`, `audio/beats.json`, optional
|
||||
`audio/track-spectrogram.png`
|
||||
|
||||
### voice-talent / narrator
|
||||
|
||||
Generates voiceover audio. Calls a TTS API directly; no Hermes skill required
|
||||
beyond `kanban-worker`. The user can also supply pre-recorded VO instead of
|
||||
generation.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`
|
||||
- **External APIs:** ElevenLabs, OpenAI TTS, etc.
|
||||
- **Outputs:** `audio/voiceover/line-NN.mp3`, `audio/voiceover/timeline.mp3`
|
||||
|
||||
### foley / sfx-designer
|
||||
|
||||
Sound effects and ambient design. Often optional unless the brief calls for
|
||||
sound design specifically.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`, `songsee` for audio-feature visualization when
|
||||
designing to a track
|
||||
- **Outputs:** `audio/sfx/*.mp3`
|
||||
|
||||
## Post-production roles
|
||||
|
||||
### editor
|
||||
|
||||
Assembles the final cut from clips. Uses ffmpeg for stitching, fades,
|
||||
transitions. Reviews each clip for pacing and quality before assembly.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`
|
||||
- **External tools:** ffmpeg, ffprobe
|
||||
- **Outputs:** `output/final.mp4`, `output/final-noaudio.mp4`
|
||||
|
||||
### colorist
|
||||
|
||||
Color grading. Usually optional — if the renderers already produce
|
||||
brand-consistent output and the editor just stitches, the colorist is overkill.
|
||||
Worth including for narrative film with hero shots.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`
|
||||
- **Outputs:** `output/final-graded.mp4`
|
||||
|
||||
### audio-mixer
|
||||
|
||||
Mixes voiceover + music + SFX into a final audio track. Sets levels, ducks
|
||||
music under VO, normalizes loudness (LUFS).
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`
|
||||
- **External tools:** ffmpeg with `loudnorm` filter, optional `sox`
|
||||
- **Outputs:** `audio/final-mix.mp3`
|
||||
|
||||
### captioner
|
||||
|
||||
Burns subtitles into the video, generates SRT, handles accessibility. Can also
|
||||
generate captions from audio via Whisper.
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`
|
||||
- **External tools:** Whisper (CLI or API), ffmpeg subtitle filters
|
||||
- **Outputs:** `output/captions.srt`, `output/final-captioned.mp4`
|
||||
|
||||
### masterer
|
||||
|
||||
Final encode + format variants. Produces deliverables for each platform target
|
||||
(square for IG, vertical for TikTok, full HD for YouTube, etc.).
|
||||
|
||||
- **Toolsets:** kanban, terminal, file
|
||||
- **Skills:** `kanban-worker`
|
||||
- **Outputs:** `output/final-1080.mp4`, `output/final-9x16.mp4`, etc.
|
||||
|
||||
## QA roles
|
||||
|
||||
### reviewer
|
||||
|
||||
A neutral quality gate. Reads the brief, watches the cut, comments
|
||||
specifically on what's off (pacing, sync, brand alignment, technical
|
||||
quality). Distinct from the cinematographer (who reviews visuals during
|
||||
production) and the editor (who reviews for assembly).
|
||||
|
||||
- **Toolsets:** kanban, terminal, file, video, vision
|
||||
- **Skills:** `kanban-worker`
|
||||
- **Review tools:** `video_analyze` (native clip review via multimodal LLM),
|
||||
`vision_analyze` (frame/thumbnail review), ffprobe
|
||||
- **Outputs:** `review-notes.md`, comments on tasks
|
||||
|
||||
### brand-cop
|
||||
|
||||
Reviews specifically for brand compliance — colors, typography, voice. Use
|
||||
when the brand guidelines are detailed and a generic reviewer might miss
|
||||
violations.
|
||||
|
||||
- **Toolsets:** kanban, file
|
||||
- **Skills:** `kanban-worker`
|
||||
- **Outputs:** comments + `brand-review.md`
|
||||
|
||||
## Composing teams — heuristics
|
||||
|
||||
- **Always:** director + at least one renderer + editor.
|
||||
- **Add writer** if scripted dialogue / narration / on-screen text exceeds a
|
||||
tagline.
|
||||
- **Add storyboarder** if the brief has more than 5 distinct beats and the
|
||||
director hasn't already laid out a beat list.
|
||||
- **Add cinematographer** if multiple renderer instances need consistent
|
||||
visual language. (For a single-tool video, the renderer's own skill spec
|
||||
is enough.)
|
||||
- **Add image-generator + image-to-video-generator pair** for narrative film
|
||||
with photorealistic visuals.
|
||||
- **Add music-supervisor** when music is provided and rhythm matters
|
||||
(music videos always; explainers sometimes).
|
||||
- **Add voice-talent** for any voiceover / narrative dialogue.
|
||||
- **Add audio-mixer** when there are 2+ audio sources (VO + music, music + SFX).
|
||||
- **Add captioner** for accessibility-priority projects (explainer, tutorial,
|
||||
any platform that defaults to muted playback).
|
||||
- **Add reviewer** for high-stakes projects. Skip for quick experimental loops.
|
||||
- **Add masterer** when multiple platform deliverables are needed.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **One renderer doing everything.** If scenes use very different tools
|
||||
(ASCII + 3D + motion graphics), use specialized renderer variants. The
|
||||
renderer loads ONE creative skill at a time; mixing styles in a single
|
||||
renderer causes thrashing.
|
||||
- **A separate profile per scene.** No. Profiles are per-role, not per-scene.
|
||||
Eight scenes use one or two renderer profiles, not eight.
|
||||
- **A "general" profile that does everything.** Worse than no specialization.
|
||||
The kanban routing breaks down if every task fits every profile.
|
||||
- **No reviewer for important deliverables.** Saves an hour of pipeline time
|
||||
but ships flaws.
|
||||
@@ -0,0 +1,317 @@
|
||||
# Tool Matrix — Skills + Toolsets per Role
|
||||
|
||||
Maps each role archetype to the Hermes skills it should `always_load` and the
|
||||
toolsets it needs. Only references skills that ship in the public hermes-agent
|
||||
repository (under `skills/` or `optional-skills/`). External APIs and CLIs are
|
||||
called from the terminal toolset; they don't appear in `always_load`.
|
||||
|
||||
## Hermes skills relevant to video production
|
||||
|
||||
### Visual / rendering skills (`hermes-agent/skills/creative/`)
|
||||
|
||||
| Skill | What it does | Best fit for |
|
||||
|-------|--------------|--------------|
|
||||
| `ascii-video` | Production pipeline for ASCII art video — generative, audio-reactive, video-to-ASCII | Renderer for ASCII / terminal / retro pixel content; cinematographer for ASCII projects |
|
||||
| `ascii-art` | Static ASCII art generation | Concept artist for ASCII style frames; secondary tool for ASCII renderer |
|
||||
| `manim-video` | Manim CE animations — math, algorithms, 3Blue1Brown-style explainers | Renderer for math, algorithm walkthroughs, technical concept explainers |
|
||||
| `p5js` | p5.js sketches — generative art, shaders, interactive, 3D | Renderer for generative art, particle systems, organic motion, web-canvas content |
|
||||
| `comfyui` | Generate images, video, audio with ComfyUI workflows (image-to-image, image-to-video, etc.) | image-generator, image-to-video-generator, or general renderer for AI-generated content |
|
||||
| `touchdesigner-mcp` | Control a running TouchDesigner instance — real-time visuals, audio-reactive installation art, VJ | Renderer for real-time/audio-reactive content; installation art; live performance |
|
||||
| `blender-mcp` *(optional)* | Control Blender 4.3+ via MCP — 3D modeling, animation, rendering | Renderer for 3D scenes, photoreal environments, character animation |
|
||||
| `pixel-art` | Pixel art with era palettes (NES, Game Boy, PICO-8) | Renderer for retro game aesthetic; concept artist for pixel-style frames |
|
||||
| `baoyu-comic` | Knowledge-comic generation (educational, biography, tutorial) | Renderer for comic-style narrative; explainer in panel form |
|
||||
| `baoyu-infographic` | Infographic generation | Renderer for data-driven explainer scenes |
|
||||
| `meme-generation` *(optional)* | Generate meme images by overlaying text on templates | Generator for satirical/social content; meme-style stills |
|
||||
|
||||
### Design / pre-production skills (`hermes-agent/skills/creative/`)
|
||||
|
||||
| Skill | What it does | Best fit for |
|
||||
|-------|--------------|--------------|
|
||||
| `claude-design` | Design one-off HTML artifacts (landing, deck, prototype) | Concept artist for product video style frames; storyboarder for UI-heavy content |
|
||||
| `design-md` | Design markdown docs | Concept artist documenting visual specs |
|
||||
| `popular-web-designs` | Reference patterns for popular web designs | Concept artist; cinematographer when matching a known UI aesthetic |
|
||||
| `sketch` | Throwaway HTML mockups (2-3 design variants to compare) | Concept artist exploring directions; storyboarder for UI flows |
|
||||
| `excalidraw` | Excalidraw-style hand-drawn diagrams | Storyboarder; concept artist for sketch-style frames |
|
||||
| `architecture-diagram` | Software architecture diagrams | Storyboarder for technical content; explainer scenes about systems |
|
||||
| `concept-diagrams` *(optional)* | Flat, minimal SVG diagrams (educational visual language; physics, chemistry, math, anatomy, etc.) | Renderer / storyboarder for explainer scenes with clean educational diagrams |
|
||||
| `pretext` | Mathematical/scientific content authoring | Writer / cinematographer for technical-explainer pretexts |
|
||||
| `creative-ideation` | Constraint-driven project ideation | Director / cinematographer when the brief is wide-open and needs framing |
|
||||
| `humanizer` | Strip AI-isms from text, add real voice | Writer / copywriter post-process to avoid AI-tells in scripts and VO copy |
|
||||
|
||||
### Audio / media skills (`hermes-agent/skills/creative/` + `skills/media/`)
|
||||
|
||||
| Skill | What it does | Best fit for |
|
||||
|-------|--------------|--------------|
|
||||
| `songwriting-and-ai-music` | Songwriting craft + Suno prompt patterns | Music supervisor when commissioning a track via Suno |
|
||||
| `heartmula` | Open-source music generation (Apache-2.0, Suno-like) | Music supervisor generating bespoke tracks without external APIs |
|
||||
| `songsee` | Spectrograms, mel/chroma/MFCC of audio files | Music supervisor analyzing tracks; foley-designer designing to a beat; editor visualizing a mix |
|
||||
| `spotify` | Spotify control — play, search, queue, manage playlists | Music supervisor sourcing existing tracks; reference research |
|
||||
| `youtube-content` | Fetch transcripts + transform to chapters/summaries/posts | Documentary cut, content adaptation, research for explainers |
|
||||
| `gif-search` | Find existing GIFs | Editor / concept artist sourcing references |
|
||||
| `gifs` | GIF tooling | Masterer producing GIF deliverables |
|
||||
|
||||
### Kanban infrastructure (`hermes-agent/skills/devops/`)
|
||||
|
||||
| Skill | What it does | When to load |
|
||||
|-------|--------------|--------------|
|
||||
| `kanban-orchestrator` | Decomposition playbook + anti-temptation rules for orchestrator profiles | Director only |
|
||||
| `kanban-worker` | Pitfalls, examples, edge cases for kanban workers (deeper than auto-injected guidance) | Any profile — load when handling tricky multi-step workflows |
|
||||
|
||||
The kanban plugin auto-injects baseline orchestration guidance into every
|
||||
worker's system prompt — the `kanban_create` fan-out pattern, claim/handoff
|
||||
lifecycle, and the "decompose, don't execute" rule for orchestrators.
|
||||
`kanban-orchestrator` and `kanban-worker` are deeper playbooks loaded when a
|
||||
profile needs them.
|
||||
|
||||
## External tools (called from terminal toolset)
|
||||
|
||||
These are **not** Hermes skills but external CLIs / APIs that profiles invoke.
|
||||
They don't appear in `always_load`; instead the role's terminal commands hit
|
||||
them directly.
|
||||
|
||||
| Tool | What it does | Profile that uses it |
|
||||
|------|--------------|----------------------|
|
||||
| `ffmpeg` | Video / audio encode, splice, mux | renderer, editor, audio-mixer, masterer |
|
||||
| `ffprobe` | Inspect media | All media-touching profiles |
|
||||
| Whisper (CLI or API) | Speech-to-text for captions | captioner |
|
||||
| Text-to-image API (FAL / Replicate / OpenAI / Midjourney) | Stills generation | image-generator (alternative to local `comfyui`) |
|
||||
| Image-to-video API (Runway / Kling / Luma / Pika) | Animate stills | image-to-video-generator |
|
||||
| Text-to-speech API (ElevenLabs / OpenAI TTS / etc.) | Voiceover generation | voice-talent |
|
||||
| Suno API or web | Track composition (paired with `songwriting-and-ai-music`) | music-supervisor |
|
||||
| Remotion CLI (`npx remotion render`) | React-based motion graphics | renderer-motion-graphics |
|
||||
| Manim CE (`manim`) | Math animation render (driven by `manim-video` skill's recipes) | renderer-manim |
|
||||
| Blender (`blender -b`) | 3D rendering (alternative to `blender-mcp`) | renderer-3d |
|
||||
|
||||
## Built-in Hermes tools for media review
|
||||
|
||||
These are native Hermes tools — not invoked via terminal but through their own
|
||||
toolsets. Enable them per-profile by adding the toolset to the profile config.
|
||||
|
||||
| Tool | Toolset | What it does | Profile that uses it |
|
||||
|------|---------|--------------|----------------------|
|
||||
| `video_analyze` | `video` (opt-in — `hermes tools enable video`) | Native video understanding — sends full clip to a multimodal LLM (Gemini via OpenRouter) for review without frame extraction. Supports mp4, webm, mov, avi, mkv. 50 MB cap. Model: `AUXILIARY_VIDEO_MODEL` env → `AUXILIARY_VISION_MODEL` fallback. | reviewer, cinematographer, editor |
|
||||
| `vision_analyze` | `vision` (core — enabled by default) | Image/frame analysis — review stills, thumbnails, exported frames. Already available to all profiles without opt-in. | reviewer, cinematographer, concept-artist |
|
||||
|
||||
## Standard toolset configurations per role
|
||||
|
||||
### director
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- terminal
|
||||
- file
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-orchestrator
|
||||
```
|
||||
|
||||
The director's terminal access is conventional but the SOUL.md rules forbid
|
||||
execution. Audit logs catch violations.
|
||||
|
||||
### writer / copywriter
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- file
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-worker
|
||||
- humanizer # post-process scripts to strip AI-tells
|
||||
```
|
||||
|
||||
No terminal — writers don't need it.
|
||||
|
||||
### concept-artist
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- terminal
|
||||
- file
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-worker
|
||||
# plus one or more (style-dependent):
|
||||
# - claude-design (UI / web product video)
|
||||
# - sketch (quick mockup variants)
|
||||
# - excalidraw (hand-drawn frames)
|
||||
# - ascii-art (ASCII style frames)
|
||||
# - pixel-art (retro/game aesthetic)
|
||||
# - popular-web-designs (matching known web aesthetic)
|
||||
# - design-md (text-based design docs)
|
||||
```
|
||||
|
||||
### storyboarder
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- file
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-worker
|
||||
# one of:
|
||||
# - excalidraw (sketch storyboards)
|
||||
# - architecture-diagram (technical/system content)
|
||||
# - concept-diagrams (educational / scientific content)
|
||||
```
|
||||
|
||||
### cinematographer
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- terminal
|
||||
- file
|
||||
- video # video_analyze — review full clips natively
|
||||
- vision # vision_analyze — review stills / exported frames
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-worker
|
||||
# the visual skill that matches the project, e.g.:
|
||||
# - ascii-video (ASCII projects)
|
||||
# - manim-video (math/explainer)
|
||||
# - p5js (generative)
|
||||
# - comfyui (AI-generated visuals)
|
||||
# - blender-mcp (3D)
|
||||
# - touchdesigner-mcp (real-time/installation)
|
||||
```
|
||||
|
||||
### renderer (specialized variants)
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- terminal
|
||||
- file
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-worker
|
||||
# ONE skill per renderer variant (or empty for external-API renderers):
|
||||
# - ascii-video (renderer-ascii)
|
||||
# - manim-video (renderer-manim)
|
||||
# - p5js (renderer-p5js)
|
||||
# - comfyui (renderer-comfyui — img/video AI gen)
|
||||
# - touchdesigner-mcp (renderer-touchdesigner)
|
||||
# - blender-mcp (renderer-3d)
|
||||
# - pixel-art (renderer-pixel)
|
||||
# - baoyu-comic (renderer-comic)
|
||||
# - meme-generation (renderer-meme)
|
||||
```
|
||||
|
||||
For external-API renderers (image-to-video-generator using Runway, voice-talent
|
||||
using ElevenLabs, renderer-motion-graphics using Remotion), `always_load` only
|
||||
contains `kanban-worker` — the role's work is API-driven and the API key +
|
||||
terminal commands suffice.
|
||||
|
||||
For multi-skill renderer setups (rare — usually one variant per skill is
|
||||
cleaner) use `--skill <name>` on individual `kanban_create` calls to override
|
||||
which skill loads for that specific task.
|
||||
|
||||
### image-generator / image-to-video-generator / voice-talent
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- terminal
|
||||
- file
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-worker
|
||||
# for image-generator that drives ComfyUI locally:
|
||||
# - comfyui
|
||||
env_required:
|
||||
# populate based on the chosen API:
|
||||
- FAL_KEY # or REPLICATE_API_TOKEN, OPENAI_API_KEY for image-gen
|
||||
- RUNWAY_API_KEY # or KLING_API_KEY, LUMA_API_KEY for image-to-video
|
||||
- ELEVENLABS_API_KEY # or OPENAI_API_KEY for TTS
|
||||
```
|
||||
|
||||
If the user's setup has ComfyUI installed locally, the `comfyui` skill can
|
||||
replace the external image-gen API entirely (cheaper, more control, supports
|
||||
custom workflows for image-to-video too).
|
||||
|
||||
### music-supervisor
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- terminal
|
||||
- file
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-worker
|
||||
- songsee # spectrograms / audio analysis
|
||||
# plus (depending on what the project needs):
|
||||
# - songwriting-and-ai-music (commissioning Suno tracks)
|
||||
# - heartmula (commissioning open-source local generation)
|
||||
# - spotify (sourcing existing tracks)
|
||||
```
|
||||
|
||||
### editor / audio-mixer / captioner / masterer
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- terminal
|
||||
- file
|
||||
- video # video_analyze — editor reviews assembled cuts natively
|
||||
- vision # vision_analyze — spot-check frames
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-worker
|
||||
```
|
||||
|
||||
These are mostly ffmpeg-driven; no special skill needed beyond `kanban-worker`.
|
||||
For captioner add Whisper invocation patterns to the SOUL.md.
|
||||
|
||||
### reviewer / brand-cop
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- kanban
|
||||
- terminal # for media inspection (ffprobe, etc.)
|
||||
- file
|
||||
- video # video_analyze — review full clips natively
|
||||
- vision # vision_analyze — review stills / exported frames
|
||||
skills:
|
||||
always_load:
|
||||
- kanban-worker
|
||||
```
|
||||
|
||||
## API key requirements
|
||||
|
||||
Track these in the project setup. The setup script should verify each required
|
||||
key is present in `~/.hermes/.env` (or macOS Keychain) before firing the kanban.
|
||||
|
||||
| Service | Env var | Used by |
|
||||
|---------|---------|---------|
|
||||
| ElevenLabs | `ELEVENLABS_API_KEY` | voice-talent |
|
||||
| OpenAI | `OPENAI_API_KEY` | image-generator (DALL-E), voice-talent (TTS) |
|
||||
| OpenRouter | `OPENROUTER_API_KEY` | reviewer, cinematographer, editor (`video_analyze` routes through `AUXILIARY_VIDEO_MODEL` → OpenRouter) |
|
||||
| FAL | `FAL_KEY` | image-generator (FAL flux models) |
|
||||
| Replicate | `REPLICATE_API_TOKEN` | image-generator (alternate provider) |
|
||||
| Runway | `RUNWAY_API_KEY` | image-to-video-generator |
|
||||
| Kling | `KLING_API_KEY` | image-to-video-generator (alternate) |
|
||||
| Luma | `LUMA_API_KEY` | image-to-video-generator (alternate) |
|
||||
| Suno | `SUNO_API_KEY` | music-supervisor (paired with `songwriting-and-ai-music`) |
|
||||
| Spotify | `SPOTIFY_CLIENT_ID` + `SPOTIFY_CLIENT_SECRET` | music-supervisor (paired with `spotify` skill) |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` | every Hermes profile (Claude) |
|
||||
|
||||
If a key is missing, prompt the user to add it. Storage methods, in order of
|
||||
preference: macOS Keychain → `~/.hermes/.env` → environment variable.
|
||||
|
||||
## Skill version pinning
|
||||
|
||||
If a specific skill version is desired, pass it via the per-task
|
||||
`--skill <name>=<version>` flag. The default is whatever's installed.
|
||||
|
||||
## Adding a new skill to the matrix
|
||||
|
||||
When a new Hermes-public video skill ships:
|
||||
|
||||
1. Add a row to the relevant table at the top of this file
|
||||
2. If it warrants a specialized renderer variant, add to `role-archetypes.md`
|
||||
3. Update relevant per-style examples in `examples.md`
|
||||
+501
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bootstrap a video production kanban from a structured plan JSON.
|
||||
|
||||
Reads a plan.json describing the team + brief, expands templates from
|
||||
../assets/, and writes a setup.sh that creates Hermes profiles and fires the
|
||||
initial kanban task.
|
||||
|
||||
Profile-config patching, SOUL.md-per-profile, TEAM.md task-graph convention,
|
||||
and the `hermes kanban create --workspace dir:` initial-task pattern are
|
||||
adapted from alt-glitch's NousResearch/kanban-video-pipeline.
|
||||
|
||||
Usage:
|
||||
bootstrap_pipeline.py plan.json [--out setup.sh]
|
||||
|
||||
The plan.json schema is documented inline below — see the `validate_plan`
|
||||
function. A minimal example:
|
||||
|
||||
{
|
||||
"title": "Q3 Product Teaser",
|
||||
"slug": "q3-product-teaser",
|
||||
"tenant": "q3-product-teaser",
|
||||
"duration_s": 30,
|
||||
"aspect": "1:1",
|
||||
"resolution": "1080x1080",
|
||||
"fps": 30,
|
||||
"team": [
|
||||
{
|
||||
"profile": "director",
|
||||
"role": "director",
|
||||
"toolsets": ["kanban", "terminal", "file"],
|
||||
"skills": [],
|
||||
"responsibilities": "...",
|
||||
"inputs": "brief.md, TEAM.md, taste/",
|
||||
"outputs": "kanban tasks for the team"
|
||||
},
|
||||
...
|
||||
],
|
||||
"scenes": [
|
||||
{"n": 1, "time": "0:00-0:08", "content": "...", "tool": "renderer-ascii"},
|
||||
...
|
||||
],
|
||||
"audio": {"approach": "voiceover + music bed", "vo": "ElevenLabs Lily",
|
||||
"music": "license-free", "sfx": "n/a"},
|
||||
"deliverables": [
|
||||
{"format": "mp4", "resolution": "1080x1080", "notes": "primary"}
|
||||
],
|
||||
"api_keys_required": ["ELEVENLABS_API_KEY", "OPENROUTER_API_KEY"],
|
||||
"brief_extra": {
|
||||
"concept_one_liner": "...",
|
||||
"emotional_north_star": "...",
|
||||
"visual_refs": "...",
|
||||
"tone": "...",
|
||||
"brand_constraints": "..."
|
||||
}
|
||||
}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
|
||||
|
||||
|
||||
def load_template(name: str) -> str:
|
||||
return (ASSETS_DIR / name).read_text()
|
||||
|
||||
|
||||
PROFILE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")
|
||||
SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]+$")
|
||||
|
||||
|
||||
def validate_plan(plan: dict) -> list[str]:
|
||||
"""Return a list of validation error strings; empty list = valid."""
|
||||
errors = []
|
||||
required_top = ["title", "slug", "tenant", "duration_s", "aspect",
|
||||
"resolution", "fps", "team", "scenes", "audio",
|
||||
"deliverables"]
|
||||
for k in required_top:
|
||||
if k not in plan:
|
||||
errors.append(f"missing required key: {k}")
|
||||
|
||||
if "team" in plan:
|
||||
if not isinstance(plan["team"], list) or not plan["team"]:
|
||||
errors.append("team must be a non-empty list")
|
||||
else:
|
||||
roles = [t.get("role") for t in plan["team"]]
|
||||
if "director" not in roles:
|
||||
errors.append("team must include a director role")
|
||||
seen_profiles = set()
|
||||
for i, t in enumerate(plan["team"]):
|
||||
for k in ["profile", "role", "toolsets", "skills",
|
||||
"responsibilities"]:
|
||||
if k not in t:
|
||||
errors.append(f"team[{i}] missing {k}")
|
||||
# Profile name must match Hermes's regex (lowercase
|
||||
# alphanumeric + hyphens + underscores, up to 64 chars).
|
||||
if "profile" in t:
|
||||
if not PROFILE_NAME_RE.match(t["profile"]):
|
||||
errors.append(
|
||||
f"team[{i}].profile {t['profile']!r} must match "
|
||||
f"[a-z0-9][a-z0-9_-]{{0,63}} per Hermes profile rules"
|
||||
)
|
||||
if t["profile"] in seen_profiles:
|
||||
errors.append(
|
||||
f"team[{i}].profile {t['profile']!r} is duplicated"
|
||||
)
|
||||
seen_profiles.add(t["profile"])
|
||||
# Toolsets / skills must be lists, not strings.
|
||||
if "toolsets" in t and not isinstance(t["toolsets"], list):
|
||||
errors.append(
|
||||
f"team[{i}].toolsets must be a list of strings"
|
||||
)
|
||||
if "skills" in t and not isinstance(t["skills"], list):
|
||||
errors.append(
|
||||
f"team[{i}].skills must be a list of strings"
|
||||
)
|
||||
|
||||
if "slug" in plan:
|
||||
if not SLUG_RE.match(plan["slug"]):
|
||||
errors.append("slug must be lowercase, hyphenated, "
|
||||
"starting with [a-z0-9]")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def render_brief(plan: dict) -> str:
|
||||
"""Render brief.md from the plan."""
|
||||
tmpl = load_template("brief.md.tmpl")
|
||||
extra = plan.get("brief_extra", {})
|
||||
|
||||
# Scene table rows
|
||||
scene_rows = []
|
||||
for s in plan["scenes"]:
|
||||
scene_rows.append(
|
||||
f"| {s.get('n', '?')} | {s.get('time', '?')} | "
|
||||
f"{s.get('content', '')} | {s.get('tool', '')} | "
|
||||
f"{s.get('audio', '')} | {s.get('notes', '')} |"
|
||||
)
|
||||
scene_table = "\n".join(scene_rows) if scene_rows else "_(none yet)_"
|
||||
|
||||
# Deliverable rows
|
||||
deliv_rows = []
|
||||
for d in plan["deliverables"]:
|
||||
deliv_rows.append(
|
||||
f"| {d.get('format', '?')} | {d.get('resolution', '?')} | "
|
||||
f"{d.get('notes', '')} |"
|
||||
)
|
||||
deliv_table = "\n".join(deliv_rows) if deliv_rows else "_(none)_"
|
||||
|
||||
# Replacements (single-pass)
|
||||
replacements = {
|
||||
"TITLE": plan["title"],
|
||||
"SLUG": plan["slug"],
|
||||
"TENANT": plan["tenant"],
|
||||
"WORKSPACE": f"~/projects/video-pipeline/{plan['slug']}",
|
||||
"ONE_LINE_PITCH": extra.get("concept_one_liner", "_(TBD)_"),
|
||||
"EMOTIONAL_NORTH_STAR": extra.get("emotional_north_star", "_(TBD)_"),
|
||||
"DURATION_S": str(plan["duration_s"]),
|
||||
"ASPECT": plan["aspect"],
|
||||
"RESOLUTION": plan["resolution"],
|
||||
"FPS": str(plan["fps"]),
|
||||
"PLATFORMS": extra.get("platforms", "_(TBD)_"),
|
||||
"DEADLINE": extra.get("deadline", "_(none)_"),
|
||||
"QUALITY_BAR": extra.get("quality_bar", "polished"),
|
||||
"VISUAL_REFS": extra.get("visual_refs", "_(none)_"),
|
||||
"TONE": extra.get("tone", "_(TBD)_"),
|
||||
"BRAND_CONSTRAINTS": extra.get("brand_constraints", "_(none)_"),
|
||||
"AESTHETIC_RULES": extra.get("aesthetic_rules", "_(TBD)_"),
|
||||
"AUDIO_APPROACH": plan["audio"].get("approach", "_(TBD)_"),
|
||||
"VO_DETAILS": plan["audio"].get("vo", "_(n/a)_"),
|
||||
"MUSIC_DETAILS": plan["audio"].get("music", "_(n/a)_"),
|
||||
"SFX_DETAILS": plan["audio"].get("sfx", "_(n/a)_"),
|
||||
"PRIMARY_FORMAT": plan["deliverables"][0]["format"],
|
||||
"PRIMARY_RES": plan["deliverables"][0]["resolution"],
|
||||
"ALT_FORMAT_1": (plan["deliverables"][1]["format"]
|
||||
if len(plan["deliverables"]) > 1 else "_(none)_"),
|
||||
"ALT_RES_1": (plan["deliverables"][1]["resolution"]
|
||||
if len(plan["deliverables"]) > 1 else ""),
|
||||
"ALT_NOTES_1": (plan["deliverables"][1].get("notes", "")
|
||||
if len(plan["deliverables"]) > 1 else ""),
|
||||
"API_KEYS_REQUIRED": ", ".join(plan.get("api_keys_required", [])) or "none",
|
||||
"EXT_DEPS": extra.get("ext_deps", "ffmpeg, Python 3.11+"),
|
||||
"SOURCE_ASSETS": extra.get("source_assets", "_(none)_"),
|
||||
}
|
||||
out = tmpl
|
||||
for k, v in replacements.items():
|
||||
out = out.replace("{{" + k + "}}", str(v))
|
||||
|
||||
# Scene + deliv tables: replace the placeholder row in the template
|
||||
out = re.sub(
|
||||
r"\|\s*1\s*\|\s*0:00–0:0X.+?\n\|\s*2\s*\|.+?\n",
|
||||
scene_table + "\n",
|
||||
out, flags=re.DOTALL,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def render_team_md(plan: dict) -> str:
|
||||
"""Render TEAM.md from the team list + scene → tool mapping."""
|
||||
lines = [f"# Team & Task Graph — {plan['title']}", "", "## Team", ""]
|
||||
for t in plan["team"]:
|
||||
skills = (
|
||||
f"loads `{', '.join(t['skills'])}`"
|
||||
if t["skills"] else "no skills required"
|
||||
)
|
||||
lines.append(
|
||||
f"- `{t['profile']}` — {t['responsibilities']} ({skills})"
|
||||
)
|
||||
lines.extend(["", "## Task Graph", "", "```"])
|
||||
|
||||
# Build a simple task graph based on conventions
|
||||
profiles_by_role = {t["role"]: t["profile"] for t in plan["team"]}
|
||||
director = profiles_by_role.get("director", "director")
|
||||
lines.append(f"T0 {director} — decompose")
|
||||
|
||||
next_id = 1
|
||||
parents_for_renderer: list[str] = ["T0"]
|
||||
|
||||
if "cinematographer" in profiles_by_role:
|
||||
cid = f"T{next_id}"
|
||||
lines.append(
|
||||
f"{cid:5} {profiles_by_role['cinematographer']} — visual spec for all scenes (parent: T0)"
|
||||
)
|
||||
parents_for_renderer = [cid]
|
||||
next_id += 1
|
||||
|
||||
if "music-supervisor" in profiles_by_role:
|
||||
cid = f"T{next_id}"
|
||||
lines.append(
|
||||
f"{cid:5} {profiles_by_role['music-supervisor']} — track analysis + beats.json (parent: T0)"
|
||||
)
|
||||
next_id += 1
|
||||
ms_id = cid
|
||||
else:
|
||||
ms_id = None
|
||||
|
||||
# Scenes
|
||||
scene_ids = []
|
||||
for s in plan["scenes"]:
|
||||
cid = f"T{next_id}"
|
||||
renderer_profile = s.get("tool") or "renderer"
|
||||
# Lookup the actual profile name
|
||||
for t in plan["team"]:
|
||||
if t["role"] == renderer_profile or t["profile"] == renderer_profile:
|
||||
renderer_profile = t["profile"]
|
||||
break
|
||||
parents = parents_for_renderer + ([ms_id] if ms_id else [])
|
||||
parent_str = ", ".join(parents)
|
||||
lines.append(
|
||||
f"{cid:5} {renderer_profile} — scene {s.get('n', '?')}: "
|
||||
f"{s.get('content', '')[:50]} (parents: {parent_str})"
|
||||
)
|
||||
scene_ids.append(cid)
|
||||
next_id += 1
|
||||
|
||||
# VO + audio mix
|
||||
if "voice-talent" in profiles_by_role:
|
||||
vo_id = f"T{next_id}"
|
||||
lines.append(f"{vo_id:5} {profiles_by_role['voice-talent']} — narration (parent: T0)")
|
||||
next_id += 1
|
||||
else:
|
||||
vo_id = None
|
||||
|
||||
if "audio-mixer" in profiles_by_role:
|
||||
am_id = f"T{next_id}"
|
||||
am_parents = [p for p in [ms_id, vo_id] if p]
|
||||
lines.append(
|
||||
f"{am_id:5} {profiles_by_role['audio-mixer']} — mix audio (parents: {', '.join(am_parents)})"
|
||||
)
|
||||
next_id += 1
|
||||
else:
|
||||
am_id = None
|
||||
|
||||
# Editor
|
||||
if "editor" in profiles_by_role:
|
||||
ed_id = f"T{next_id}"
|
||||
ed_parents = scene_ids + [p for p in [am_id, vo_id, ms_id] if p and p not in scene_ids]
|
||||
lines.append(
|
||||
f"{ed_id:5} {profiles_by_role['editor']} — assemble + mux (parents: {', '.join(ed_parents)})"
|
||||
)
|
||||
next_id += 1
|
||||
else:
|
||||
ed_id = None
|
||||
|
||||
# Captioner
|
||||
if "captioner" in profiles_by_role and ed_id:
|
||||
cap_id = f"T{next_id}"
|
||||
lines.append(
|
||||
f"{cap_id:5} {profiles_by_role['captioner']} — SRT + burn (parent: {ed_id})"
|
||||
)
|
||||
next_id += 1
|
||||
last = cap_id
|
||||
else:
|
||||
last = ed_id
|
||||
|
||||
# Reviewer
|
||||
if "reviewer" in profiles_by_role and last:
|
||||
rv_id = f"T{next_id}"
|
||||
lines.append(
|
||||
f"{rv_id:5} {profiles_by_role['reviewer']} — final QA (parent: {last})"
|
||||
)
|
||||
|
||||
lines.append("```")
|
||||
lines.extend([
|
||||
"",
|
||||
"## Per-task workspace requirement",
|
||||
"",
|
||||
f"All `kanban_create` calls MUST pass:",
|
||||
f"```",
|
||||
f'workspace_kind="dir"',
|
||||
f'workspace_path="$HOME/projects/video-pipeline/{plan["slug"]}"',
|
||||
f'tenant="{plan["tenant"]}"',
|
||||
f"```",
|
||||
])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_setup_sh(plan: dict, brief_md: str, team_md: str) -> str:
|
||||
"""Render setup.sh from the plan."""
|
||||
tmpl = load_template("setup.sh.tmpl")
|
||||
|
||||
# API key checks
|
||||
key_checks = []
|
||||
for key in plan.get("api_keys_required", []):
|
||||
key_checks.append(f'check_key {key} hermes {key} || exit 1')
|
||||
key_checks_str = "\n".join(key_checks) if key_checks else "# (no API keys required)"
|
||||
|
||||
# Scene dirs
|
||||
scene_dir_lines = []
|
||||
for s in plan["scenes"]:
|
||||
n = s.get("n", "?")
|
||||
scene_dir_lines.append(f'mkdir -p "$WORKSPACE/scenes/scene-{n:02d}"/checkpoints')
|
||||
scene_dirs = "\n".join(scene_dir_lines) if scene_dir_lines else ""
|
||||
|
||||
# Profile create
|
||||
profile_creates = []
|
||||
for t in plan["team"]:
|
||||
profile_creates.append(
|
||||
f'hermes profile create {t["profile"]} --clone 2>/dev/null || true'
|
||||
)
|
||||
|
||||
# Profile config — emit JSON arrays so the bash function can pass them
|
||||
# safely through to the Python YAML patcher.
|
||||
profile_configs = []
|
||||
for t in plan["team"]:
|
||||
ts_json = json.dumps(t["toolsets"])
|
||||
sk_json = json.dumps(t["skills"])
|
||||
# Use single-quoted bash strings; JSON only contains "/[/], no single
|
||||
# quotes, so this is safe.
|
||||
profile_configs.append(
|
||||
f"configure_profile {t['profile']!r} {ts_json!r} {sk_json!r}"
|
||||
)
|
||||
|
||||
# SOUL writes — uses heredocs per profile
|
||||
soul_writes = []
|
||||
for t in plan["team"]:
|
||||
soul_writes.append(
|
||||
f'cat > "$HOME/.hermes/profiles/{t["profile"]}/SOUL.md" <<\'SOUL_EOF\'\n'
|
||||
f"{render_soul_md(t, plan)}\n"
|
||||
f"SOUL_EOF\n"
|
||||
f'echo " ✓ SOUL.md for {t["profile"]}"'
|
||||
)
|
||||
|
||||
# Taste writes (placeholder; real content optional)
|
||||
taste_writes = (
|
||||
'cat > "$WORKSPACE/taste/brand-guide.md" <<\'TASTE_EOF\'\n'
|
||||
'# Brand Guide\n\n'
|
||||
'_(Populate with project-specific colors, typography, motion rules)_\n'
|
||||
'TASTE_EOF\n'
|
||||
'cat > "$WORKSPACE/taste/emotional-dna.md" <<\'DNA_EOF\'\n'
|
||||
'# Emotional DNA\n\n'
|
||||
'_(What this piece should FEEL like — populate from the brief.)_\n'
|
||||
'DNA_EOF'
|
||||
)
|
||||
|
||||
# Asset copies — leave empty by default; user fills in
|
||||
asset_copies = "# Add cp/rsync commands here for any provided assets"
|
||||
|
||||
out = tmpl
|
||||
out = out.replace("{{TITLE}}", plan["title"])
|
||||
out = out.replace("{{SLUG}}", plan["slug"])
|
||||
out = out.replace("{{TENANT}}", plan["tenant"])
|
||||
out = out.replace("{{WORKSPACE}}", f"~/projects/video-pipeline/{plan['slug']}")
|
||||
out = out.replace("{{KEY_CHECKS}}", key_checks_str)
|
||||
out = out.replace("{{SCENE_DIRS}}", scene_dirs)
|
||||
out = out.replace("{{PROFILE_CREATE_COMMANDS}}", "\n".join(profile_creates))
|
||||
out = out.replace("{{PROFILE_CONFIG_COMMANDS}}", "\n".join(profile_configs))
|
||||
out = out.replace("{{SOUL_WRITES}}", "\n".join(soul_writes))
|
||||
out = out.replace("{{BRIEF_CONTENTS}}", brief_md)
|
||||
out = out.replace("{{TEAM_CONTENTS}}", team_md)
|
||||
out = out.replace("{{TASTE_WRITES}}", taste_writes)
|
||||
out = out.replace("{{ASSET_COPIES}}", asset_copies)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def render_soul_md(team_member: dict, plan: dict) -> str:
|
||||
"""Render a profile's SOUL.md from a team member dict + plan context."""
|
||||
tmpl = load_template("soul.md.tmpl")
|
||||
role = team_member["role"]
|
||||
|
||||
common_rules = (
|
||||
"- **Read the brief and team graph** before doing anything else.\n"
|
||||
"- **Pass `workspace_kind=\"dir\"` and `workspace_path` on every "
|
||||
"`kanban_create` call.** This keeps the team in one shared workspace.\n"
|
||||
f"- **Use tenant `{plan['tenant']}`** on every kanban call.\n"
|
||||
"- **Write outputs to predictable paths.** Other profiles depend on "
|
||||
"your filename conventions.\n"
|
||||
"- **Emit heartbeats** during long-running work. Renderers should "
|
||||
"report frame counts; editors should report assembly progress.\n"
|
||||
)
|
||||
|
||||
if role == "director":
|
||||
common_rules += (
|
||||
"- **Do not execute the work yourself.** For every concrete task, "
|
||||
"create a kanban task and assign it to the appropriate profile.\n"
|
||||
"- **Decompose, route, comment, approve — that's the whole job.**\n"
|
||||
"- **Read TEAM.md** for the canonical task graph. Do not invent "
|
||||
"new roles unless the brief truly demands it.\n"
|
||||
"- **Load the `kanban-orchestrator` skill** for the deeper "
|
||||
"decomposition playbook beyond the auto-injected baseline.\n"
|
||||
)
|
||||
|
||||
common_commands = (
|
||||
"```bash\n"
|
||||
"# Inspect a clip\n"
|
||||
"ffprobe -v quiet -show_entries format=duration -show_entries "
|
||||
"stream=codec_name,width,height,r_frame_rate <file.mp4>\n"
|
||||
"\n"
|
||||
"# Extract a frame for QA\n"
|
||||
"ffmpeg -y -i <input.mp4> -vf \"select='eq(n,30)'\" -vsync vfr <out.png>\n"
|
||||
"```"
|
||||
)
|
||||
|
||||
out = tmpl
|
||||
out = out.replace("{{ROLE_NAME}}", role)
|
||||
out = out.replace("{{ROLE_RESPONSIBILITIES}}", team_member["responsibilities"])
|
||||
out = out.replace("{{INPUTS_READ}}", team_member.get("inputs", "_(see brief)_"))
|
||||
out = out.replace("{{OUTPUTS_PRODUCED}}", team_member.get("outputs", "_(see brief)_"))
|
||||
out = out.replace("{{TOOLSETS}}", ", ".join(team_member["toolsets"]))
|
||||
out = out.replace(
|
||||
"{{SKILLS}}",
|
||||
", ".join(team_member["skills"]) if team_member["skills"] else "(none)"
|
||||
)
|
||||
out = out.replace(
|
||||
"{{EXTERNAL_TOOLS}}",
|
||||
team_member.get("external_tools", "ffmpeg, ffprobe (via terminal)")
|
||||
)
|
||||
out = out.replace(
|
||||
"{{ROLE_RULES}}",
|
||||
team_member.get("role_rules", "_(see TEAM.md and brief.md)_")
|
||||
)
|
||||
out = out.replace("{{COMMON_RULES}}", common_rules)
|
||||
out = out.replace("{{COMMON_COMMANDS}}", common_commands)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("plan_json", help="Path to plan.json")
|
||||
ap.add_argument("--out", default="setup.sh",
|
||||
help="Output path for setup.sh (default: ./setup.sh)")
|
||||
ap.add_argument("--brief-out", default=None,
|
||||
help="Write brief.md alongside (default: skipped)")
|
||||
ap.add_argument("--team-out", default=None,
|
||||
help="Write TEAM.md alongside (default: skipped)")
|
||||
args = ap.parse_args()
|
||||
|
||||
plan = json.loads(Path(args.plan_json).read_text())
|
||||
errors = validate_plan(plan)
|
||||
if errors:
|
||||
print("Plan validation failed:", file=sys.stderr)
|
||||
for e in errors:
|
||||
print(f" - {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
brief = render_brief(plan)
|
||||
team = render_team_md(plan)
|
||||
setup = render_setup_sh(plan, brief, team)
|
||||
|
||||
Path(args.out).write_text(setup)
|
||||
os.chmod(args.out, 0o755)
|
||||
print(f"Wrote {args.out}")
|
||||
|
||||
if args.brief_out:
|
||||
Path(args.brief_out).write_text(brief)
|
||||
print(f"Wrote {args.brief_out}")
|
||||
if args.team_out:
|
||||
Path(args.team_out).write_text(team)
|
||||
print(f"Wrote {args.team_out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Monitor a running video-production kanban. Polls `hermes kanban list` and
|
||||
`events` for a tenant and surfaces issues (stuck tasks, missing heartbeats,
|
||||
repeated retries, dependency deadlocks).
|
||||
|
||||
Usage:
|
||||
monitor.py --tenant <project-slug> [--interval 30]
|
||||
|
||||
Outputs a periodic snapshot to stdout. Sends alerts via stderr when issues
|
||||
are detected. Designed to run alongside the kanban — kill with Ctrl-C when
|
||||
you're satisfied (or scripted to stop on completion).
|
||||
|
||||
This is best-effort observability. It does not auto-restart tasks; intervention
|
||||
decisions should remain human/AI-overseen.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def hermes_available() -> bool:
|
||||
return shutil.which("hermes") is not None
|
||||
|
||||
|
||||
def kanban_list(tenant: str) -> list[dict]:
|
||||
"""Returns parsed task rows. Falls back to plain stdout parsing if JSON
|
||||
output isn't supported by the installed hermes CLI."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["hermes", "kanban", "list", "--tenant", tenant, "--json"],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if out.returncode == 0 and out.stdout.strip().startswith("["):
|
||||
return json.loads(out.stdout)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
pass
|
||||
# Fallback: textual parse of `hermes kanban list`
|
||||
out = subprocess.run(
|
||||
["hermes", "kanban", "list", "--tenant", tenant],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
rows = []
|
||||
for line in out.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "STATUS" in line.upper():
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) >= 4 and parts[0].startswith("t_"):
|
||||
rows.append({
|
||||
"id": parts[0],
|
||||
"status": parts[1] if len(parts) > 1 else "?",
|
||||
"assignee": parts[2] if len(parts) > 2 else "?",
|
||||
"title": " ".join(parts[3:]) if len(parts) > 3 else "",
|
||||
"started_at": None,
|
||||
"heartbeat_at": None,
|
||||
"max_runtime_s": None,
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def kanban_show(task_id: str) -> dict | None:
|
||||
out = subprocess.run(
|
||||
["hermes", "kanban", "show", task_id, "--json"],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
try:
|
||||
return json.loads(out.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def detect_issues(tasks: list[dict]) -> list[str]:
|
||||
"""Return a list of issue strings, one per concern."""
|
||||
now = datetime.now()
|
||||
issues: list[str] = []
|
||||
by_status = defaultdict(list)
|
||||
for t in tasks:
|
||||
by_status[t.get("status", "?")].append(t)
|
||||
|
||||
# Stuck tasks: RUNNING with no heartbeat in 2 min
|
||||
for t in by_status.get("running", []) + by_status.get("RUNNING", []):
|
||||
hb = t.get("heartbeat_at")
|
||||
if not hb:
|
||||
continue
|
||||
try:
|
||||
hb_dt = datetime.fromisoformat(str(hb).rstrip("Z"))
|
||||
except ValueError:
|
||||
continue
|
||||
if now - hb_dt > timedelta(minutes=2):
|
||||
issues.append(
|
||||
f"STUCK: {t['id']} ({t.get('assignee', '?')}) — "
|
||||
f"no heartbeat in {(now - hb_dt).total_seconds():.0f}s"
|
||||
)
|
||||
|
||||
# Tasks exceeding max_runtime
|
||||
for t in by_status.get("running", []) + by_status.get("RUNNING", []):
|
||||
started = t.get("started_at")
|
||||
max_rt = t.get("max_runtime_s")
|
||||
if not started or not max_rt:
|
||||
continue
|
||||
try:
|
||||
started_dt = datetime.fromisoformat(str(started).rstrip("Z"))
|
||||
except ValueError:
|
||||
continue
|
||||
elapsed = (now - started_dt).total_seconds()
|
||||
if elapsed > max_rt:
|
||||
issues.append(
|
||||
f"OVERTIME: {t['id']} ({t.get('assignee', '?')}) — "
|
||||
f"running {elapsed:.0f}s, cap was {max_rt}s"
|
||||
)
|
||||
|
||||
# Repeated retries
|
||||
for t in tasks:
|
||||
retries = t.get("retries", 0)
|
||||
if retries and retries >= 2:
|
||||
issues.append(
|
||||
f"FLAPPING: {t['id']} ({t.get('assignee', '?')}) — "
|
||||
f"retried {retries}× — fix root cause before next run"
|
||||
)
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def snapshot(tenant: str) -> tuple[list[dict], list[str]]:
|
||||
tasks = kanban_list(tenant)
|
||||
issues = detect_issues(tasks)
|
||||
return tasks, issues
|
||||
|
||||
|
||||
def print_snapshot(tasks: list[dict], issues: list[str]):
|
||||
counts = defaultdict(int)
|
||||
for t in tasks:
|
||||
counts[str(t.get("status", "?")).lower()] += 1
|
||||
|
||||
print(f"\n[{datetime.now().strftime('%H:%M:%S')}] "
|
||||
f"Total: {len(tasks)} | "
|
||||
+ " | ".join(f"{k}: {v}" for k, v in sorted(counts.items())))
|
||||
|
||||
for t in tasks:
|
||||
bar = "✓" if str(t.get("status", "")).lower() == "done" else \
|
||||
"▶" if str(t.get("status", "")).lower() == "running" else \
|
||||
"·" if str(t.get("status", "")).lower() == "ready" else \
|
||||
"✗" if str(t.get("status", "")).lower() == "failed" else "?"
|
||||
print(f" {bar} {t.get('id', '?'):14} {t.get('assignee', '?'):20} "
|
||||
f"{t.get('title', '')[:60]}")
|
||||
|
||||
if issues:
|
||||
print("\n ⚠ ISSUES:", file=sys.stderr)
|
||||
for i in issues:
|
||||
print(f" {i}", file=sys.stderr)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--tenant", required=True,
|
||||
help="Project tenant slug to monitor")
|
||||
ap.add_argument("--interval", type=int, default=30,
|
||||
help="Poll interval in seconds (default: 30)")
|
||||
ap.add_argument("--once", action="store_true",
|
||||
help="Print one snapshot and exit (no polling loop)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not hermes_available():
|
||||
print("ERROR: 'hermes' CLI not found in PATH", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.once:
|
||||
tasks, issues = snapshot(args.tenant)
|
||||
print_snapshot(tasks, issues)
|
||||
sys.exit(0 if not issues else 2)
|
||||
|
||||
print(f"Monitoring tenant '{args.tenant}' every {args.interval}s. "
|
||||
"Ctrl-C to exit.")
|
||||
try:
|
||||
while True:
|
||||
tasks, issues = snapshot(args.tenant)
|
||||
print_snapshot(tasks, issues)
|
||||
time.sleep(args.interval)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
# Meme Generation Examples
|
||||
|
||||
## Example 1: Debugging at 2 AM
|
||||
|
||||
**Topic:** debugging production at 2 AM
|
||||
**Template:** this-is-fine
|
||||
|
||||
```bash
|
||||
python generate_meme.py this-is-fine /tmp/meme.png "PRODUCTION IS DOWN" "This is fine"
|
||||
```
|
||||
|
||||
## Example 2: Developer Priorities
|
||||
|
||||
**Topic:** choosing between writing tests and shipping features
|
||||
**Template:** drake
|
||||
|
||||
```bash
|
||||
python generate_meme.py drake /tmp/meme.png "Writing unit tests" "Shipping straight to prod"
|
||||
```
|
||||
|
||||
## Example 3: Exam Stress
|
||||
|
||||
**Topic:** final exam preparation
|
||||
**Template:** two-buttons
|
||||
|
||||
```bash
|
||||
python generate_meme.py two-buttons /tmp/meme.png "Study everything" "Sleep" "Me at midnight"
|
||||
```
|
||||
|
||||
## Example 4: Escalating Solutions
|
||||
|
||||
**Topic:** fixing a CSS bug
|
||||
**Template:** expanding-brain
|
||||
|
||||
```bash
|
||||
python generate_meme.py expanding-brain /tmp/meme.png "Reading the docs" "Stack Overflow" "!important on everything" "Deleting the stylesheet"
|
||||
```
|
||||
|
||||
## Example 5: Hot Take
|
||||
|
||||
**Topic:** tabs vs spaces
|
||||
**Template:** change-my-mind
|
||||
|
||||
```bash
|
||||
python generate_meme.py change-my-mind /tmp/meme.png "Tabs are just thicc spaces"
|
||||
```
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
name: meme-generation
|
||||
description: Generate real meme images by picking a template and overlaying text with Pillow. Produces actual .png meme files.
|
||||
version: 2.0.0
|
||||
author: adanaleycio
|
||||
license: MIT
|
||||
platforms: [linux, macos, windows]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [creative, memes, humor, images]
|
||||
related_skills: [ascii-art, generative-widgets]
|
||||
category: creative
|
||||
---
|
||||
|
||||
# Meme Generation
|
||||
|
||||
Generate actual meme images from a topic. Picks a template, writes captions, and renders a real .png file with text overlay.
|
||||
|
||||
## When to Use
|
||||
|
||||
- User asks you to make or generate a meme
|
||||
- User wants a meme about a specific topic, situation, or frustration
|
||||
- User says "meme this" or similar
|
||||
|
||||
## Available Templates
|
||||
|
||||
The script supports **any of the ~100 popular imgflip templates** by name or ID, plus 10 curated templates with hand-tuned text positioning.
|
||||
|
||||
### Curated Templates (custom text placement)
|
||||
|
||||
| ID | Name | Fields | Best for |
|
||||
|----|------|--------|----------|
|
||||
| `this-is-fine` | This is Fine | top, bottom | chaos, denial |
|
||||
| `drake` | Drake Hotline Bling | reject, approve | rejecting/preferring |
|
||||
| `distracted-boyfriend` | Distracted Boyfriend | distraction, current, person | temptation, shifting priorities |
|
||||
| `two-buttons` | Two Buttons | left, right, person | impossible choice |
|
||||
| `expanding-brain` | Expanding Brain | 4 levels | escalating irony |
|
||||
| `change-my-mind` | Change My Mind | statement | hot takes |
|
||||
| `woman-yelling-at-cat` | Woman Yelling at Cat | woman, cat | arguments |
|
||||
| `one-does-not-simply` | One Does Not Simply | top, bottom | deceptively hard things |
|
||||
| `grus-plan` | Gru's Plan | step1-3, realization | plans that backfire |
|
||||
| `batman-slapping-robin` | Batman Slapping Robin | robin, batman | shutting down bad ideas |
|
||||
|
||||
### Dynamic Templates (from imgflip API)
|
||||
|
||||
Any template not in the curated list can be used by name or imgflip ID. These get smart default text positioning (top/bottom for 2-field, evenly spaced for 3+). Search with:
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/generate_meme.py" --search "disaster"
|
||||
```
|
||||
|
||||
## Procedure
|
||||
|
||||
### Mode 1: Classic Template (default)
|
||||
|
||||
1. Read the user's topic and identify the core dynamic (chaos, dilemma, preference, irony, etc.)
|
||||
2. Pick the template that best matches. Use the "Best for" column, or search with `--search`.
|
||||
3. Write short captions for each field (8-12 words max per field, shorter is better).
|
||||
4. Find the skill's script directory:
|
||||
```
|
||||
SKILL_DIR=$(dirname "$(find ~/.hermes/skills -path '*/meme-generation/SKILL.md' 2>/dev/null | head -1)")
|
||||
```
|
||||
5. Run the generator:
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/generate_meme.py" <template_id> /tmp/meme.png "caption 1" "caption 2" ...
|
||||
```
|
||||
6. Return the image with `MEDIA:/tmp/meme.png`
|
||||
|
||||
### Mode 2: Custom AI Image (when image_generate is available)
|
||||
|
||||
Use this when no classic template fits, or when the user wants something original.
|
||||
|
||||
1. Write the captions first.
|
||||
2. Use `image_generate` to create a scene that matches the meme concept. Do NOT include any text in the image prompt — text will be added by the script. Describe only the visual scene.
|
||||
3. Find the generated image path from the image_generate result URL. Download it to a local path if needed.
|
||||
4. Run the script with `--image` to overlay text, choosing a mode:
|
||||
- **Overlay** (text directly on image, white with black outline):
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/generate_meme.py" --image /path/to/scene.png /tmp/meme.png "top text" "bottom text"
|
||||
```
|
||||
- **Bars** (black bars above/below with white text — cleaner, always readable):
|
||||
```bash
|
||||
python "$SKILL_DIR/scripts/generate_meme.py" --image /path/to/scene.png --bars /tmp/meme.png "top text" "bottom text"
|
||||
```
|
||||
Use `--bars` when the image is busy/detailed and text would be hard to read on top of it.
|
||||
5. **Verify with vision** (if `vision_analyze` is available): Check the result looks good:
|
||||
```
|
||||
vision_analyze(image_url="/tmp/meme.png", question="Is the text legible and well-positioned? Does the meme work visually?")
|
||||
```
|
||||
If the vision model flags issues (text hard to read, bad placement, etc.), try the other mode (switch between overlay and bars) or regenerate the scene.
|
||||
6. Return the image with `MEDIA:/tmp/meme.png`
|
||||
|
||||
## Examples
|
||||
|
||||
**"debugging production at 2 AM":**
|
||||
```bash
|
||||
python generate_meme.py this-is-fine /tmp/meme.png "SERVERS ARE ON FIRE" "This is fine"
|
||||
```
|
||||
|
||||
**"choosing between sleep and one more episode":**
|
||||
```bash
|
||||
python generate_meme.py drake /tmp/meme.png "Getting 8 hours of sleep" "One more episode at 3 AM"
|
||||
```
|
||||
|
||||
**"the stages of a Monday morning":**
|
||||
```bash
|
||||
python generate_meme.py expanding-brain /tmp/meme.png "Setting an alarm" "Setting 5 alarms" "Sleeping through all alarms" "Working from bed"
|
||||
```
|
||||
|
||||
## Listing Templates
|
||||
|
||||
To see all available templates:
|
||||
```bash
|
||||
python generate_meme.py --list
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Keep captions SHORT. Memes with long text look terrible.
|
||||
- Match the number of text arguments to the template's field count.
|
||||
- Pick the template that fits the joke structure, not just the topic.
|
||||
- Do not generate hateful, abusive, or personally targeted content.
|
||||
- The script caches template images in `scripts/.cache/` after first download.
|
||||
|
||||
## Verification
|
||||
|
||||
The output is correct if:
|
||||
- A .png file was created at the output path
|
||||
- Text is legible (white with black outline) on the template
|
||||
- The joke lands — caption matches the template's intended structure
|
||||
- File can be delivered via MEDIA: path
|
||||
@@ -0,0 +1 @@
|
||||
.cache/
|
||||
@@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a meme image by overlaying text on a template.
|
||||
|
||||
Usage:
|
||||
python generate_meme.py <template_id_or_name> <output_path> <text1> [text2] [text3] [text4]
|
||||
|
||||
Example:
|
||||
python generate_meme.py drake /tmp/meme.png "Writing tests" "Shipping to prod and hoping"
|
||||
python generate_meme.py "Disaster Girl" /tmp/meme.png "Top text" "Bottom text"
|
||||
python generate_meme.py --list # show curated templates
|
||||
python generate_meme.py --search "distracted" # search all imgflip templates
|
||||
|
||||
Templates with custom text positioning are in templates.json (10 curated).
|
||||
Any of the ~100 popular imgflip templates can also be used by name or ID —
|
||||
unknown templates get smart default text positioning based on their box_count.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests as _requests
|
||||
except ImportError:
|
||||
_requests = None
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
TEMPLATES_FILE = SCRIPT_DIR / "templates.json"
|
||||
CACHE_DIR = SCRIPT_DIR / ".cache"
|
||||
IMGFLIP_API = "https://api.imgflip.com/get_memes"
|
||||
IMGFLIP_CACHE_FILE = CACHE_DIR / "imgflip_memes.json"
|
||||
IMGFLIP_CACHE_MAX_AGE = 86400 # 24 hours
|
||||
|
||||
|
||||
def _fetch_url(url: str, timeout: int = 15) -> bytes:
|
||||
"""Fetch URL content, using requests if available, else urllib."""
|
||||
if _requests is not None:
|
||||
resp = _requests.get(url, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
return resp.content
|
||||
import urllib.request
|
||||
return urllib.request.urlopen(url, timeout=timeout).read()
|
||||
|
||||
|
||||
def load_curated_templates() -> dict:
|
||||
"""Load templates with hand-tuned text field positions."""
|
||||
with open(TEMPLATES_FILE) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _default_fields(box_count: int) -> list:
|
||||
"""Generate sensible default text field positions for unknown templates."""
|
||||
if box_count <= 0:
|
||||
box_count = 2
|
||||
if box_count == 1:
|
||||
return [{"name": "text", "x_pct": 0.5, "y_pct": 0.5, "w_pct": 0.90, "align": "center"}]
|
||||
if box_count == 2:
|
||||
return [
|
||||
{"name": "top", "x_pct": 0.5, "y_pct": 0.08, "w_pct": 0.95, "align": "center"},
|
||||
{"name": "bottom", "x_pct": 0.5, "y_pct": 0.92, "w_pct": 0.95, "align": "center"},
|
||||
]
|
||||
# 3+: evenly space vertically
|
||||
fields = []
|
||||
for i in range(box_count):
|
||||
y = 0.08 + (0.84 * i / (box_count - 1)) if box_count > 1 else 0.5
|
||||
fields.append({
|
||||
"name": f"text{i+1}",
|
||||
"x_pct": 0.5,
|
||||
"y_pct": round(y, 2),
|
||||
"w_pct": 0.90,
|
||||
"align": "center",
|
||||
})
|
||||
return fields
|
||||
|
||||
|
||||
def fetch_imgflip_templates() -> list:
|
||||
"""Fetch popular meme templates from imgflip API. Cached for 24h."""
|
||||
import time
|
||||
|
||||
CACHE_DIR.mkdir(exist_ok=True)
|
||||
# Check cache
|
||||
if IMGFLIP_CACHE_FILE.exists():
|
||||
age = time.time() - IMGFLIP_CACHE_FILE.stat().st_mtime
|
||||
if age < IMGFLIP_CACHE_MAX_AGE:
|
||||
with open(IMGFLIP_CACHE_FILE) as f:
|
||||
return json.load(f)
|
||||
|
||||
try:
|
||||
data = json.loads(_fetch_url(IMGFLIP_API))
|
||||
memes = data.get("data", {}).get("memes", [])
|
||||
with open(IMGFLIP_CACHE_FILE, "w") as f:
|
||||
json.dump(memes, f)
|
||||
return memes
|
||||
except Exception as e:
|
||||
# If fetch fails and we have stale cache, use it
|
||||
if IMGFLIP_CACHE_FILE.exists():
|
||||
with open(IMGFLIP_CACHE_FILE) as f:
|
||||
return json.load(f)
|
||||
print(f"Warning: could not fetch imgflip templates: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def _slugify(name: str) -> str:
|
||||
"""Convert a template name to a slug for matching."""
|
||||
return name.lower().replace(" ", "-").replace("'", "").replace("\"", "")
|
||||
|
||||
|
||||
def resolve_template(identifier: str) -> dict:
|
||||
"""Resolve a template by curated ID, imgflip name, or imgflip ID.
|
||||
|
||||
Returns dict with: name, url, fields, source.
|
||||
"""
|
||||
curated = load_curated_templates()
|
||||
|
||||
# 1. Exact curated ID match
|
||||
if identifier in curated:
|
||||
tmpl = curated[identifier]
|
||||
return {**tmpl, "source": "curated"}
|
||||
|
||||
# 2. Slugified curated match
|
||||
slug = _slugify(identifier)
|
||||
for tid, tmpl in curated.items():
|
||||
if _slugify(tmpl["name"]) == slug or tid == slug:
|
||||
return {**tmpl, "source": "curated"}
|
||||
|
||||
# 3. Search imgflip templates
|
||||
imgflip_memes = fetch_imgflip_templates()
|
||||
slug_lower = slug.lower()
|
||||
id_lower = identifier.strip()
|
||||
|
||||
for meme in imgflip_memes:
|
||||
meme_slug = _slugify(meme["name"])
|
||||
# Check curated first for this imgflip template (custom positioning)
|
||||
for tid, ctmpl in curated.items():
|
||||
if _slugify(ctmpl["name"]) == meme_slug:
|
||||
if meme_slug == slug_lower or meme["id"] == id_lower:
|
||||
return {**ctmpl, "source": "curated"}
|
||||
|
||||
if meme_slug == slug_lower or meme["id"] == id_lower or slug_lower in meme_slug:
|
||||
return {
|
||||
"name": meme["name"],
|
||||
"url": meme["url"],
|
||||
"fields": _default_fields(meme.get("box_count", 2)),
|
||||
"source": "imgflip",
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_template_image(url: str) -> Image.Image:
|
||||
"""Download a template image, caching it locally."""
|
||||
CACHE_DIR.mkdir(exist_ok=True)
|
||||
# Use URL hash as cache key
|
||||
cache_name = url.split("/")[-1]
|
||||
cache_path = CACHE_DIR / cache_name
|
||||
|
||||
# Always cache as PNG to avoid JPEG/RGBA conflicts
|
||||
cache_path = cache_path.with_suffix(".png")
|
||||
|
||||
if cache_path.exists():
|
||||
return Image.open(cache_path).convert("RGBA")
|
||||
|
||||
data = _fetch_url(url)
|
||||
img = Image.open(BytesIO(data)).convert("RGBA")
|
||||
img.save(cache_path, "PNG")
|
||||
return img
|
||||
|
||||
|
||||
def find_font(size: int) -> ImageFont.FreeTypeFont:
|
||||
"""Find a bold font for meme text. Tries Impact, then falls back."""
|
||||
candidates = [
|
||||
"/usr/share/fonts/truetype/msttcorefonts/Impact.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/liberation-sans/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/dejavu-sans/DejaVuSans-Bold.ttf",
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
"/System/Library/Fonts/SFCompact.ttf",
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
return ImageFont.truetype(path, size)
|
||||
except (OSError, IOError):
|
||||
continue
|
||||
# Last resort: Pillow default
|
||||
try:
|
||||
return ImageFont.truetype("DejaVuSans-Bold", size)
|
||||
except (OSError, IOError):
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def _wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int) -> str:
|
||||
"""Word-wrap text to fit within max_width pixels. Never breaks mid-word."""
|
||||
words = text.split()
|
||||
if not words:
|
||||
return text
|
||||
lines = []
|
||||
current_line = words[0]
|
||||
for word in words[1:]:
|
||||
test_line = current_line + " " + word
|
||||
if font.getlength(test_line) <= max_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
lines.append(current_line)
|
||||
current_line = word
|
||||
lines.append(current_line)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def draw_outlined_text(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
text: str,
|
||||
x: int,
|
||||
y: int,
|
||||
font_size: int,
|
||||
max_width: int,
|
||||
align: str = "center",
|
||||
):
|
||||
"""Draw white text with black outline, auto-scaled to fit max_width."""
|
||||
# Auto-scale: reduce font size until text fits reasonably
|
||||
size = font_size
|
||||
while size > 12:
|
||||
font = find_font(size)
|
||||
wrapped = _wrap_text(text, font, max_width)
|
||||
bbox = draw.multiline_textbbox((0, 0), wrapped, font=font, align=align)
|
||||
text_w = bbox[2] - bbox[0]
|
||||
line_count = wrapped.count("\n") + 1
|
||||
# Accept if width fits and not too many lines
|
||||
if text_w <= max_width * 1.05 and line_count <= 4:
|
||||
break
|
||||
size -= 2
|
||||
else:
|
||||
font = find_font(size)
|
||||
wrapped = _wrap_text(text, font, max_width)
|
||||
|
||||
# Measure total text block
|
||||
bbox = draw.multiline_textbbox((0, 0), wrapped, font=font, align=align)
|
||||
text_w = bbox[2] - bbox[0]
|
||||
text_h = bbox[3] - bbox[1]
|
||||
|
||||
# Center horizontally at x, vertically at y
|
||||
tx = x - text_w // 2
|
||||
ty = y - text_h // 2
|
||||
|
||||
# Draw outline (black border)
|
||||
outline_range = max(2, font.size // 18)
|
||||
for dx in range(-outline_range, outline_range + 1):
|
||||
for dy in range(-outline_range, outline_range + 1):
|
||||
if dx == 0 and dy == 0:
|
||||
continue
|
||||
draw.multiline_text(
|
||||
(tx + dx, ty + dy), wrapped, font=font, fill="black", align=align
|
||||
)
|
||||
# Draw main text (white)
|
||||
draw.multiline_text((tx, ty), wrapped, font=font, fill="white", align=align)
|
||||
|
||||
|
||||
def _overlay_on_image(img: Image.Image, texts: list, fields: list) -> Image.Image:
|
||||
"""Overlay meme text directly on an image using field positions."""
|
||||
draw = ImageDraw.Draw(img)
|
||||
w, h = img.size
|
||||
base_font_size = max(16, min(w, h) // 12)
|
||||
|
||||
for i, field in enumerate(fields):
|
||||
if i >= len(texts):
|
||||
break
|
||||
text = texts[i].strip()
|
||||
if not text:
|
||||
continue
|
||||
fx = int(field["x_pct"] * w)
|
||||
fy = int(field["y_pct"] * h)
|
||||
fw = int(field["w_pct"] * w)
|
||||
draw_outlined_text(draw, text, fx, fy, base_font_size, fw, field.get("align", "center"))
|
||||
return img
|
||||
|
||||
|
||||
def _add_bars(img: Image.Image, texts: list) -> Image.Image:
|
||||
"""Add black bars with white text above/below the image.
|
||||
|
||||
Distributes texts across bars: first text on top bar, last text on
|
||||
bottom bar, any middle texts overlaid on the image center.
|
||||
"""
|
||||
w, h = img.size
|
||||
bar_font_size = max(20, w // 16)
|
||||
font = find_font(bar_font_size)
|
||||
padding = bar_font_size // 2
|
||||
|
||||
top_text = texts[0].strip() if texts else ""
|
||||
bottom_text = texts[-1].strip() if len(texts) > 1 else ""
|
||||
middle_texts = [t.strip() for t in texts[1:-1]] if len(texts) > 2 else []
|
||||
|
||||
def _measure_bar(text: str) -> int:
|
||||
if not text:
|
||||
return 0
|
||||
wrapped = _wrap_text(text, font, int(w * 0.92))
|
||||
bbox = ImageDraw.Draw(Image.new("RGB", (1, 1))).multiline_textbbox(
|
||||
(0, 0), wrapped, font=font, align="center"
|
||||
)
|
||||
return (bbox[3] - bbox[1]) + padding * 2
|
||||
|
||||
top_h = _measure_bar(top_text)
|
||||
bottom_h = _measure_bar(bottom_text)
|
||||
new_h = h + top_h + bottom_h
|
||||
|
||||
canvas = Image.new("RGB", (w, new_h), (0, 0, 0))
|
||||
canvas.paste(img.convert("RGB"), (0, top_h))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
if top_text:
|
||||
wrapped = _wrap_text(top_text, font, int(w * 0.92))
|
||||
bbox = draw.multiline_textbbox((0, 0), wrapped, font=font, align="center")
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
tx = (w - tw) // 2
|
||||
ty = (top_h - th) // 2
|
||||
draw.multiline_text((tx, ty), wrapped, font=font, fill="white", align="center")
|
||||
|
||||
if bottom_text:
|
||||
wrapped = _wrap_text(bottom_text, font, int(w * 0.92))
|
||||
bbox = draw.multiline_textbbox((0, 0), wrapped, font=font, align="center")
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
tx = (w - tw) // 2
|
||||
ty = top_h + h + (bottom_h - th) // 2
|
||||
draw.multiline_text((tx, ty), wrapped, font=font, fill="white", align="center")
|
||||
|
||||
# Overlay any middle texts centered on the image
|
||||
if middle_texts:
|
||||
mid_fields = _default_fields(len(middle_texts))
|
||||
# Shift y positions to account for top bar offset
|
||||
for field in mid_fields:
|
||||
field["y_pct"] = (top_h + field["y_pct"] * h) / new_h
|
||||
field["w_pct"] = 0.90
|
||||
_overlay_on_image(canvas, middle_texts, mid_fields)
|
||||
|
||||
return canvas
|
||||
|
||||
|
||||
def generate_meme(template_id: str, texts: list[str], output_path: str) -> str:
|
||||
"""Generate a meme from a template and save it. Returns the path."""
|
||||
tmpl = resolve_template(template_id)
|
||||
|
||||
if tmpl is None:
|
||||
print(f"Unknown template: {template_id}", file=sys.stderr)
|
||||
print("Use --list to see curated templates or --search to find imgflip templates.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
fields = tmpl["fields"]
|
||||
print(f"Using template: {tmpl['name']} ({tmpl['source']}, {len(fields)} fields)", file=sys.stderr)
|
||||
|
||||
img = get_template_image(tmpl["url"])
|
||||
img = _overlay_on_image(img, texts, fields)
|
||||
|
||||
output = Path(output_path)
|
||||
if output.suffix.lower() in {".jpg", ".jpeg"}:
|
||||
img = img.convert("RGB")
|
||||
img.save(str(output), quality=95)
|
||||
return str(output)
|
||||
|
||||
|
||||
def generate_from_image(
|
||||
image_path: str, texts: list[str], output_path: str, use_bars: bool = False
|
||||
) -> str:
|
||||
"""Generate a meme from a custom image (e.g. AI-generated). Returns the path."""
|
||||
img = Image.open(image_path).convert("RGBA")
|
||||
print(f"Custom image: {img.size[0]}x{img.size[1]}, {len(texts)} text(s), mode={'bars' if use_bars else 'overlay'}", file=sys.stderr)
|
||||
|
||||
if use_bars:
|
||||
result = _add_bars(img, texts)
|
||||
else:
|
||||
fields = _default_fields(len(texts))
|
||||
result = _overlay_on_image(img, texts, fields)
|
||||
|
||||
output = Path(output_path)
|
||||
if output.suffix.lower() in {".jpg", ".jpeg"}:
|
||||
result = result.convert("RGB")
|
||||
result.save(str(output), quality=95)
|
||||
return str(output)
|
||||
|
||||
|
||||
def list_templates():
|
||||
"""Print curated templates with custom positioning."""
|
||||
templates = load_curated_templates()
|
||||
print(f"{'ID':<25} {'Name':<30} {'Fields':<8} Best for")
|
||||
print("-" * 90)
|
||||
for tid, tmpl in sorted(templates.items()):
|
||||
fields = len(tmpl["fields"])
|
||||
print(f"{tid:<25} {tmpl['name']:<30} {fields:<8} {tmpl['best_for']}")
|
||||
print(f"\n{len(templates)} curated templates with custom text positioning.")
|
||||
print("Use --search to find any of the ~100 popular imgflip templates.")
|
||||
|
||||
|
||||
def search_templates(query: str):
|
||||
"""Search imgflip templates by name."""
|
||||
imgflip_memes = fetch_imgflip_templates()
|
||||
curated = load_curated_templates()
|
||||
curated_slugs = {_slugify(t["name"]) for t in curated.values()}
|
||||
query_lower = query.lower()
|
||||
|
||||
matches = []
|
||||
for meme in imgflip_memes:
|
||||
if query_lower in meme["name"].lower():
|
||||
slug = _slugify(meme["name"])
|
||||
has_custom = "curated" if slug in curated_slugs else "default"
|
||||
matches.append((meme["name"], meme["id"], meme.get("box_count", 2), has_custom))
|
||||
|
||||
if not matches:
|
||||
print(f"No templates found matching '{query}'")
|
||||
return
|
||||
|
||||
print(f"{'Name':<40} {'ID':<12} {'Fields':<8} Positioning")
|
||||
print("-" * 75)
|
||||
for name, mid, boxes, positioning in matches:
|
||||
print(f"{name:<40} {mid:<12} {boxes:<8} {positioning}")
|
||||
print(f"\n{len(matches)} template(s) found. Use the name or ID as the first argument.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: generate_meme.py <template_id_or_name> <output_path> <text1> [text2] ...")
|
||||
print(" generate_meme.py --image <path> [--bars] <output_path> <text1> [text2] ...")
|
||||
print(" generate_meme.py --list # curated templates")
|
||||
print(" generate_meme.py --search <query> # search all imgflip templates")
|
||||
sys.exit(1)
|
||||
|
||||
if sys.argv[1] == "--list":
|
||||
list_templates()
|
||||
sys.exit(0)
|
||||
|
||||
if sys.argv[1] == "--search":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: generate_meme.py --search <query>")
|
||||
sys.exit(1)
|
||||
search_templates(sys.argv[2])
|
||||
sys.exit(0)
|
||||
|
||||
if sys.argv[1] == "--image":
|
||||
# Custom image mode: --image <path> [--bars] <output> <text1> ...
|
||||
args = sys.argv[2:]
|
||||
if len(args) < 3:
|
||||
print("Usage: generate_meme.py --image <image_path> [--bars] <output_path> <text1> ...")
|
||||
sys.exit(1)
|
||||
image_path = args.pop(0)
|
||||
use_bars = False
|
||||
if args and args[0] == "--bars":
|
||||
use_bars = True
|
||||
args.pop(0)
|
||||
if len(args) < 2:
|
||||
print("Need at least: output_path and one text argument")
|
||||
sys.exit(1)
|
||||
output_path = args.pop(0)
|
||||
result = generate_from_image(image_path, args, output_path, use_bars=use_bars)
|
||||
print(f"Meme saved to: {result}")
|
||||
sys.exit(0)
|
||||
|
||||
if len(sys.argv) < 4:
|
||||
print("Need at least: template_id_or_name, output_path, and one text argument")
|
||||
sys.exit(1)
|
||||
|
||||
template_id = sys.argv[1]
|
||||
output_path = sys.argv[2]
|
||||
texts = sys.argv[3:]
|
||||
|
||||
result = generate_meme(template_id, texts, output_path)
|
||||
print(f"Meme saved to: {result}")
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"this-is-fine": {
|
||||
"name": "This is Fine",
|
||||
"url": "https://i.imgflip.com/wxica.jpg",
|
||||
"best_for": "chaos, denial, pretending things are okay",
|
||||
"fields": [
|
||||
{"name": "top", "x_pct": 0.5, "y_pct": 0.08, "w_pct": 0.95, "align": "center"},
|
||||
{"name": "bottom", "x_pct": 0.5, "y_pct": 0.92, "w_pct": 0.95, "align": "center"}
|
||||
]
|
||||
},
|
||||
"drake": {
|
||||
"name": "Drake Hotline Bling",
|
||||
"url": "https://i.imgflip.com/30b1gx.jpg",
|
||||
"best_for": "rejecting one thing, preferring another",
|
||||
"fields": [
|
||||
{"name": "reject", "x_pct": 0.73, "y_pct": 0.25, "w_pct": 0.45, "align": "center"},
|
||||
{"name": "approve", "x_pct": 0.73, "y_pct": 0.75, "w_pct": 0.45, "align": "center"}
|
||||
]
|
||||
},
|
||||
"distracted-boyfriend": {
|
||||
"name": "Distracted Boyfriend",
|
||||
"url": "https://i.imgflip.com/1ur9b0.jpg",
|
||||
"best_for": "distraction, shifting priorities, temptation",
|
||||
"fields": [
|
||||
{"name": "distraction", "x_pct": 0.18, "y_pct": 0.90, "w_pct": 0.30, "align": "center"},
|
||||
{"name": "current", "x_pct": 0.55, "y_pct": 0.90, "w_pct": 0.30, "align": "center"},
|
||||
{"name": "person", "x_pct": 0.82, "y_pct": 0.90, "w_pct": 0.30, "align": "center"}
|
||||
]
|
||||
},
|
||||
"two-buttons": {
|
||||
"name": "Two Buttons",
|
||||
"url": "https://i.imgflip.com/1g8my4.jpg",
|
||||
"best_for": "impossible choice, dilemma between two options",
|
||||
"fields": [
|
||||
{"name": "left_button", "x_pct": 0.30, "y_pct": 0.20, "w_pct": 0.28, "align": "center"},
|
||||
{"name": "right_button", "x_pct": 0.62, "y_pct": 0.12, "w_pct": 0.28, "align": "center"},
|
||||
{"name": "person", "x_pct": 0.5, "y_pct": 0.85, "w_pct": 0.90, "align": "center"}
|
||||
]
|
||||
},
|
||||
"expanding-brain": {
|
||||
"name": "Expanding Brain",
|
||||
"url": "https://i.imgflip.com/1jwhww.jpg",
|
||||
"best_for": "escalating irony, increasingly absurd ideas",
|
||||
"fields": [
|
||||
{"name": "level1", "x_pct": 0.25, "y_pct": 0.12, "w_pct": 0.45, "align": "center"},
|
||||
{"name": "level2", "x_pct": 0.25, "y_pct": 0.38, "w_pct": 0.45, "align": "center"},
|
||||
{"name": "level3", "x_pct": 0.25, "y_pct": 0.63, "w_pct": 0.45, "align": "center"},
|
||||
{"name": "level4", "x_pct": 0.25, "y_pct": 0.88, "w_pct": 0.45, "align": "center"}
|
||||
]
|
||||
},
|
||||
"change-my-mind": {
|
||||
"name": "Change My Mind",
|
||||
"url": "https://i.imgflip.com/24y43o.jpg",
|
||||
"best_for": "strong or ironic opinion, controversial take",
|
||||
"fields": [
|
||||
{"name": "statement", "x_pct": 0.58, "y_pct": 0.78, "w_pct": 0.35, "align": "center"}
|
||||
]
|
||||
},
|
||||
"woman-yelling-at-cat": {
|
||||
"name": "Woman Yelling at Cat",
|
||||
"url": "https://i.imgflip.com/345v97.jpg",
|
||||
"best_for": "argument, blame, misunderstanding",
|
||||
"fields": [
|
||||
{"name": "woman", "x_pct": 0.27, "y_pct": 0.10, "w_pct": 0.50, "align": "center"},
|
||||
{"name": "cat", "x_pct": 0.76, "y_pct": 0.10, "w_pct": 0.44, "align": "center"}
|
||||
]
|
||||
},
|
||||
"one-does-not-simply": {
|
||||
"name": "One Does Not Simply",
|
||||
"url": "https://i.imgflip.com/1bij.jpg",
|
||||
"best_for": "something that sounds easy but is actually hard",
|
||||
"fields": [
|
||||
{"name": "top", "x_pct": 0.5, "y_pct": 0.08, "w_pct": 0.95, "align": "center"},
|
||||
{"name": "bottom", "x_pct": 0.5, "y_pct": 0.92, "w_pct": 0.95, "align": "center"}
|
||||
]
|
||||
},
|
||||
"grus-plan": {
|
||||
"name": "Gru's Plan",
|
||||
"url": "https://i.imgflip.com/26jxvs.jpg",
|
||||
"best_for": "a plan that backfires, unexpected consequence",
|
||||
"fields": [
|
||||
{"name": "step1", "x_pct": 0.5, "y_pct": 0.05, "w_pct": 0.45, "align": "center"},
|
||||
{"name": "step2", "x_pct": 0.5, "y_pct": 0.30, "w_pct": 0.45, "align": "center"},
|
||||
{"name": "step3", "x_pct": 0.5, "y_pct": 0.55, "w_pct": 0.45, "align": "center"},
|
||||
{"name": "realization", "x_pct": 0.5, "y_pct": 0.80, "w_pct": 0.45, "align": "center"}
|
||||
]
|
||||
},
|
||||
"batman-slapping-robin": {
|
||||
"name": "Batman Slapping Robin",
|
||||
"url": "https://i.imgflip.com/9ehk.jpg",
|
||||
"best_for": "shutting down a bad idea, correcting someone",
|
||||
"fields": [
|
||||
{"name": "robin", "x_pct": 0.28, "y_pct": 0.08, "w_pct": 0.50, "align": "center"},
|
||||
{"name": "batman", "x_pct": 0.72, "y_pct": 0.08, "w_pct": 0.50, "align": "center"}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user