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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions skills/faceless-explainer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: faceless-explainer
description: "turn arbitrary text — an article, notes, a topic, a brief — into a faceless explainer video, up to ~3 min (sweet spot 30-90s), where every visual is invented (typography, abstract graphics, diagrams, data-viz) rather than captured. There is no URL, no website capture, and no real assets. Use this skill for topic explainers, concept breakdowns, how-tos, listicles, and narrative explainers. Do not use it for a product launch/promo (use /product-launch-video), a tour of a real website (use /website-to-video), a GitHub PR (use /pr-to-video), captions on existing footage (use /embedded-captions), or a short unnarrated motion graphic (use /motion-graphics). If the intent is unclear, route through /hyperframes first."
---

> **media-use**: Before sourcing audio/images, call `/media-use` to resolve BGM/SFX/images from the HeyGen catalog. Run `--adopt` first to register existing assets. See `/media-use` skill.

# Faceless Explainer to HyperFrames

Use this skill to turn a body of text into an explainer video: pick a design system, plan a teaching story, and build it frame by frame in HyperFrames. **Faceless** means every visual is invented downstream — there is no capture step and no real asset inventory.
Expand Down
2 changes: 2 additions & 0 deletions skills/general-video/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ description: >
metadata: { "tags": "orchestrator, general-video, fallback, freeform, composition-authoring" }
---

> **media-use**: Before sourcing audio/images, call `/media-use` to resolve BGM/SFX/images from the HeyGen catalog. Run `--adopt` first to register existing assets. See `/media-use` skill.

# general-video — general video workflow

> **Confirm the route before you build.** This is the **fallback** for custom composition authoring. If the input clearly fits a specialized workflow, prefer it: marketed product → `/product-launch-video`; general site → `/website-to-video`; topic explainer → `/faceless-explainer`; GitHub PR → `/pr-to-video`; existing footage → `/embedded-captions` · `/graphic-overlays`; short unnarrated motion graphic → `/motion-graphics`; Remotion port → `/remotion-to-hyperframes`. **Out of scope**: live / at-render-time data, NLE-style editing of a finished video, or producing footage HyperFrames can't capture. Unsure? **Read `/hyperframes` first.**
Expand Down
1 change: 1 addition & 0 deletions skills/hyperframes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Atomic capabilities you load **on demand** — not full video workflows. For "ma
| **Animate** — atomic motion, scene blueprints, transitions, runtime adapters (GSAP / Lottie / Three.js / Anime.js / CSS / WAAPI / TypeGPU) | `/hyperframes-animation` |
| **Creative direction** — `frame.md` / `design.md`, palettes, typography, narration, beat planning, audio-reactive | `/hyperframes-creative` |
| **Media** — TTS voiceover, background music, transcription, background removal, captions | `/hyperframes-media` |
| **Media resolve** — find + freeze BGM, SFX, images, icons from HeyGen catalog into `.media/` with manifest tracking | `/media-use` |
| **CLI dev loop** — init, lint, validate, inspect, preview, render, publish, doctor | `/hyperframes-cli` |
| **Install registry blocks / components** (`hyperframes add`) | `/hyperframes-registry` |

Expand Down
114 changes: 114 additions & 0 deletions skills/media-use/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
name: media-use
description: Agent Media OS — resolve any media need (BGM, SFX, image, icon) into a frozen local file + ledger record. One verb (`resolve`) handles the full cascade: project cache, global cache, HeyGen catalog search, freeze, register. Keeps search noise on disk, hands the agent a path. Use when a composition needs background music, sound effects, images, or icons.
---

# media-use

Resolve media needs into frozen local files. One verb, four types, zero context noise.

## When to use

Call `resolve` whenever a composition needs media — background music, sound effects, images, or icons. media-use searches the HeyGen catalog, downloads the best match, freezes it locally, and registers it in a manifest. The agent gets back one line; all search noise stays on disk.

## Resolve

```bash
node <SKILL_DIR>/scripts/resolve.mjs --type <type> --intent "<description>" --project <dir>
```

Returns one line: `resolved <id> → <path> (<type>, <metadata>)`

### Types

| Type | What it finds | Provider |
| ------- | ------------------- | ---------------------------------------- |
| `bgm` | Background music | HeyGen audio catalog (10k+ tracks) |
| `sfx` | Sound effects | Bundled 19-file library + HeyGen catalog |
| `image` | Photos, backgrounds | HeyGen asset search (75k+ vectors) |
| `icon` | Icons, logos | HeyGen asset search (type=icon) |

### Examples

```bash
# Background music
node <SKILL_DIR>/scripts/resolve.mjs --type bgm --intent "upbeat tech launch" --project .
# → resolved bgm_001 → .media/audio/bgm/bgm_001.mp3 (bgm, 25s)

# Sound effect
node <SKILL_DIR>/scripts/resolve.mjs --type sfx --intent "whoosh" --project .
# → resolved sfx_001 → .media/audio/sfx/sfx_001.mp3 (sfx, 0.57s)

# Image
node <SKILL_DIR>/scripts/resolve.mjs --type image --intent "gradient tech background" --project .
# → resolved image_001 → .media/images/image_001.jpg (image)

# Icon
node <SKILL_DIR>/scripts/resolve.mjs --type icon --intent "rocket" --project .
# → resolved icon_001 → .media/images/icon_001.svg (icon, transparent)
```

### Flags

| Flag | Description |
| --------------- | ------------------------------------------ |
| `--type, -t` | Media type: bgm, sfx, image, icon |
| `--intent, -i` | What you need (natural language) |
| `--entity, -e` | Entity name for cache matching (optional) |
| `--project, -p` | Project directory (default: .) |
| `--adopt` | Bulk-import existing assets/ into manifest |
| `--json` | Output JSON instead of one-line result |

## How it works

1. Check project `.media/manifest.jsonl` for exact-prompt match
2. Scan existing `assets/` directory for unregistered files matching the need
3. Check global cache `~/.media/` for reusable asset
4. Search via provider (HeyGen audio catalog, HeyGen asset search)
5. Freeze file to `.media/<type>/`, register in manifest, regenerate `index.md`

The agent gets back **one line**. Candidates, scores, provenance stay on disk.

## Adopt existing projects

Most HyperFrames projects already have assets in `assets/`. media-use adopts them:

```bash
node <SKILL_DIR>/scripts/resolve.mjs --adopt --project .
# → adopted 9 assets from assets/
# bgm_001 → assets/bgm/mango-fizz.mp3 (bgm, 146.6s)
# image_001 → assets/images/avatar.jpg (image, 400×400)
```

`ffprobe` extracts real duration and dimensions. During resolve, unregistered files in `assets/` matching the intent are adopted on the fly.

## Reading the inventory

After resolve or adopt, read `.media/index.md` for the full inventory:

```
# .media · 4 assets

id type dur dims path description
bgm_001 bgm 25s — .media/audio/bgm/bgm_001.mp3 upbeat tech launch
sfx_001 sfx 0.6s — .media/audio/sfx/sfx_001.mp3 whoosh
image_001 image — 1920×1080 .media/images/image_001.jpg gradient tech background
icon_001 icon — svg .media/images/icon_001.svg rocket
```

## Cross-project reuse

Assets are cached automatically on resolve. Subsequent resolves for the same prompt hit the global cache at `~/.media/` — no re-download, no provider call. Promote an asset explicitly with `organize --promote <id>` to make it reusable across all projects.

## Files

- `.media/manifest.jsonl` — machine SSOT, one JSON record per line
- `.media/index.md` — agent-readable table (id, type, dur, dims, path, description)
- `~/.media/` — global cross-project reuse cache (content-addressed, SHA-256)

## CLI tools used

| Tool | Purpose | Required? |
| --------- | ------------------------------------------ | ------------- |
| `ffprobe` | Probe duration, dimensions, codec on adopt | Yes |
| `heygen` | Audio catalog, asset search | For providers |
20 changes: 20 additions & 0 deletions skills/media-use/scripts/lib/bgm-provider.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { heygenSearch } from "./heygen-search.mjs";

export const bgmProvider = {
async search(intent) {
const results = heygenSearch("audio sounds list", intent, { type: "music" });
if (!results) return null;
const best = results[0];
return {
url: best.audio_url,
source: "search",
ext: ".mp3",
metadata: {
description: best.description || intent,
duration: best.duration || null,
provider: "heygen.audio.sounds",
provenance: { track_id: best.id, score: best.score, query: intent },
},
};
},
};
54 changes: 54 additions & 0 deletions skills/media-use/scripts/lib/brand-provider.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";

function findDesignSpec(projectDir) {
for (const name of ["frame.md", "design.md", "DESIGN.md"]) {
const p = join(projectDir, name);
if (existsSync(p)) return { path: p, name };
}
return null;
}

function parseFrontmatter(content) {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return null;
const yaml = match[1];
const tokens = {};
for (const line of yaml.split("\n")) {
const m = line.match(/^\s*(\w[\w-]*):\s*(.+)/);
if (m) tokens[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
}
return tokens;
}

function extractColors(tokens) {
const colors = [];
for (const [k, v] of Object.entries(tokens)) {
if (typeof v === "string" && /^#[0-9a-fA-F]{3,8}$/.test(v)) {
colors.push({ name: k, hex: v });
}
}
return colors;
}

export const brandProvider = {
async search(intent, { projectDir } = {}) {
if (!projectDir) return null;
const spec = findDesignSpec(projectDir);
if (!spec) return null;
const content = readFileSync(spec.path, "utf8");
const tokens = parseFrontmatter(content);
if (!tokens) return null;
const colors = extractColors(tokens);
return {
localPath: spec.path,
source: "local",
ext: ".md",
metadata: {
description: "Brand tokens from " + spec.name,
provider: "design_spec",
provenance: { file: spec.name, colors, font: tokens.font || tokens.typography || null, logo: tokens.logo || null },
},
};
},
};
16 changes: 16 additions & 0 deletions skills/media-use/scripts/lib/heygen-search.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { execSync } from "node:child_process";

export function heygenSearch(subcommand, query, { type, limit = 5, minScore } = {}) {
try {
const q = query.replace(/'/g, "'\\''");
const parts = [`heygen --x-source media-use ${subcommand} --query '${q}'`];
if (type) parts.push(`--type ${type}`);
parts.push(`--limit ${limit}`);
if (minScore != null) parts.push(`--min-score ${minScore}`);
const out = execSync(parts.join(" "), { encoding: "utf8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"] });
const data = JSON.parse(out)?.data;
return Array.isArray(data) && data.length > 0 ? data : null;
} catch {
return null;
}
}
41 changes: 41 additions & 0 deletions skills/media-use/scripts/lib/image-provider.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { heygenSearch } from "./heygen-search.mjs";

export const imageProvider = {
async search(intent) {
const results = heygenSearch("asset search list", intent, { type: "image" });
if (!results) return null;
const best = results[0];
return {
url: best.url,
source: "search",
ext: ".jpg",
metadata: {
description: intent,
width: best.width || null,
height: best.height || null,
transparent: best.is_transparent || false,
provider: "heygen.asset.search",
provenance: { asset_id: best.id, score: best.score },
},
};
},
};

export const iconProvider = {
async search(intent) {
const results = heygenSearch("asset search list", intent, { type: "icon", minScore: 0.2 });
if (!results) return null;
const best = results[0];
return {
url: best.url,
source: "search",
ext: ".svg",
metadata: {
description: intent,
transparent: true,
provider: "heygen.asset.search",
provenance: { asset_id: best.id, score: best.score, type: "icon" },
},
};
},
};
25 changes: 25 additions & 0 deletions skills/media-use/scripts/lib/providers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { sfxProvider } from "./sfx-provider.mjs";
import { imageProvider, iconProvider } from "./image-provider.mjs";
import { bgmProvider } from "./bgm-provider.mjs";
import { brandProvider } from "./brand-provider.mjs";

const STUB = { async search() { return null; } };

const registry = {
bgm: { ...bgmProvider, type: "bgm" },
sfx: { ...sfxProvider, type: "sfx" },
voice: { ...STUB, type: "voice" },
image: { ...imageProvider, type: "image" },
icon: { ...iconProvider, type: "icon" },
brand: { ...brandProvider, type: "brand" },
};

export function getProvider(type) {
const p = registry[type];
if (!p) throw new Error(`unknown media type: ${type}`);
return p;
}

export function listTypes() {
return Object.keys(registry);
}
20 changes: 20 additions & 0 deletions skills/media-use/scripts/lib/sfx-provider.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { heygenSearch } from "./heygen-search.mjs";

export const sfxProvider = {
async search(intent) {
const results = heygenSearch("audio sounds list", intent, { type: "sound_effects", minScore: 0.4 });
if (!results) return null;
const best = results[0];
return {
url: best.audio_url,
source: "search",
ext: ".mp3",
metadata: {
description: best.description || best.name || intent,
duration: best.duration || null,
provider: "heygen.audio.sounds",
provenance: { track_id: best.id, score: best.score, query: intent },
},
};
},
};
Loading
Loading