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: 1 addition & 1 deletion skills/media-use/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Returns one line: `resolved <id> → <path> (<type>, <metadata>)`. All search no

| Type | One-line intent |
| ------- | ----------------------------------------------------------------------------------- |
| `bgm` | background music (HeyGen catalog, 10k+ tracks) |
| `bgm` | background music (HeyGen catalog or configured ACE-Step generation API) |
| `sfx` | sound effects (bundled 19-file library + catalog) |
| `image` | photos, backgrounds (HeyGen asset search, 75k+ vectors) |
| `icon` | icons, symbols (transparent) |
Expand Down
6 changes: 4 additions & 2 deletions skills/media-use/audio/references/bgm.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
One music bed per composition, produced by the shared audio engine (`scripts/audio.mjs` → `scripts/lib/bgm.mjs`). Two routes, chosen by the engine's one switch — whether a HeyGen credential is present:

- **HeyGen retrieval — the default when credentialed.** Search HeyGen's music catalog by mood, download the top track. No generation; same `~/.heygen` / `$HEYGEN_API_KEY` credential as TTS.
- **Local generation (Lyria → MusicGen) — the fallback when there is no credential** (or when asked for explicitly). Generate a WAV from a mood prompt. There is **no `npx hyperframes bgm` command**; the engine spawns `scripts/lyria-recipe.py` or an inline MusicGen script directly.
- **ACE-Step remote generation — the preferred generation path when `bgm.provider` or the user provider configuration selects `acestep`.** It submits the native asynchronous API, waits in the existing detached BGM flow, and freezes the completed MP3.
- **Local generation (Lyria → MusicGen) — the fallback when there is no credential or configured ACE-Step provider** (or when asked for explicitly). Generate a WAV from a mood prompt. There is **no `npx hyperframes bgm` command**; the engine spawns `scripts/lyria-recipe.py` or an inline MusicGen script directly.

> **Run the Preflight first — no credential is not a green light to silently generate locally.** Before generating, complete the sign-in **Preflight** (see `../SKILL.md` → Preflight): run `npx hyperframes auth status`, recommend signing in, and **STOP for the user's choice** (sign in for HeyGen's music library, or continue offline with local generation). This applies to a one-off "generate a BGM" request just as much as inside a full workflow.

## Driving it from the request

`audio_request.json` → `bgm: { mode?, query?, prompt? }`:
`audio_request.json` → `bgm: { mode?, provider?, query?, prompt? }`:

- **`mode`** — `retrieve | generate | none`. Omit for **auto** (retrieve when credentialed, else generate). An **explicit** `retrieve` is strict: no credential ⇒ skip, never a detached generate (so a caller with no `wait-bgm` step, e.g. product-launch, can't get a pending job it won't await).
- **`provider`** — `acestep` forces the configured ACE-Step endpoint for generation. Omit to use the user provider default and then the existing HeyGen/Lyria/MusicGen cascade.
- **`query`** — the mood, used for retrieval and as a fallback prompt seed (e.g. a storyboard's `music:` field, falling back to `message` → `arc` → `"calm cinematic underscore"`).
- **`prompt`** — an explicit full prompt for generation; omit and the engine infers one (see Mood inference). Optional `blob` / `archetype` / `arc` feed that inference.

Expand Down
2 changes: 2 additions & 0 deletions skills/media-use/audio/references/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Run `npx hyperframes auth status` to see what's configured and which engines a w
| Provider | Resolution order (first non-empty wins) | Local deps when used |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| **HeyGen** (TTS + BGM/SFX retrieval) | `$HEYGEN_API_KEY` → `$HYPERFRAMES_API_KEY` → `~/.heygen/credentials` (shared with heygen-cli; `$HEYGEN_CONFIG_DIR` overrides the dir; written by `hyperframes auth login`) | none (REST) |
| **ACE-Step** (BGM generation) | `~/.media/providers.json` `bgm.acestep.base_url`; optional key named by `api_key_env` (default `$ACESTEP_API_KEY`) | none (native REST API) |
| **ElevenLabs** (TTS fallback) | `$ELEVENLABS_API_KEY` | `pip install elevenlabs` |
| **Lyria** (BGM fallback) | `$GEMINI_API_KEY` → `$GOOGLE_API_KEY` | `pip install google-genai` |
| **Kokoro** (TTS, no key) | always — final voice fallback | `pip install kokoro-onnx soundfile` |
Expand All @@ -22,6 +23,7 @@ Each command downloads its own model on first run and caches it under `~/.cache/
- **TTS (ElevenLabs)** — same as HeyGen: API key + `ffmpeg`.
- **TTS (Kokoro)** — Kokoro-82M (~311 MB) + voices (~27 MB) in `tts/`. Requires Python 3.8+ with `kokoro-onnx` and `soundfile` (`pip install kokoro-onnx soundfile`). Non-English text also needs `espeak-ng` system-wide.
- **BGM (Lyria)** — needs `$GEMINI_API_KEY` or `$GOOGLE_API_KEY` + `pip install google-genai`. No local model cache.
- **BGM (ACE-Step)** — remote native API; no local dependency. Configure `bgm.default` as `acestep`, set `bgm.acestep.base_url`, and optionally set the named key environment variable.
- **BGM (MusicGen)** — `pip install transformers torch soundfile`. `facebook/musicgen-small` (~300 MB) cached under `~/.cache/huggingface/` on first run.
- **Transcribe** — Whisper model size depending on choice (75 MB – 3.1 GB) in `whisper/`, downloaded from HuggingFace on first use. `whisper.cpp` itself is NOT bundled: the CLI resolves it from PATH, installs via Homebrew (macOS), or builds it from source with git+cmake on first use (`$HYPERFRAMES_WHISPER_PATH` overrides).
- **Remove-background** — `u2net_human_seg` (~168 MB ONNX) in `background-removal/models/`. Peak inference RAM ~1.5 GB.
Expand Down
26 changes: 26 additions & 0 deletions skills/media-use/audio/scripts/acestep-recipe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env node
import { resolve } from "node:path";
import { parseArgs } from "node:util";
import { generateWithAceStep } from "../../scripts/lib/acestep-provider.mjs";
import { freezeUrl } from "../../scripts/lib/freeze.mjs";

const { values } = parseArgs({
options: {
output: { type: "string" },
duration: { type: "string", default: "30" },
prompt: { type: "string" },
},
strict: true,
});

if (!values.output || !values.prompt) {
console.error(
"usage: acestep-recipe.mjs --output <path> --duration <seconds> --prompt <description>",
);
process.exit(2);
}

const result = await generateWithAceStep(values.prompt, { duration: Number(values.duration) });
if (!result?.url) throw new Error("ACE-Step is not configured");
await freezeUrl(result.url, resolve(values.output), { headers: result.downloadHeaders });
console.log(`ACE-Step wrote ${values.output}`);
12 changes: 10 additions & 2 deletions skills/media-use/audio/scripts/audio.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// (credential present, NOT the CLI). This mirrors the table in ../SKILL.md:
//
// TTS : HeyGen REST → ElevenLabs → Kokoro (CLI)
// BGM : HeyGen retrieve → (no credential) Lyria/MusicGen generate
// BGM : HeyGen retrieve → configured ACE-Step → Lyria/MusicGen generate
// SFX : HeyGen retrieve → (no credential) bundled 19-file library
//
// ── audio_request.json (input) ────────────────────────────────────────────────
Expand Down Expand Up @@ -52,6 +52,7 @@ import {
withWordIds,
} from "./lib/tts.mjs";
import { generateBgmDetached, inferBgmPrompt, retrieveBgm } from "./lib/bgm.mjs";
import { configuredBgmProvider } from "../../scripts/lib/acestep-provider.mjs";
import { resolveSfx } from "./lib/sfx.mjs";
import { mapWithConcurrency } from "./lib/concurrency.mjs";

Expand Down Expand Up @@ -83,6 +84,7 @@ const requestPath = resolve(flag("request", join(hyperframesDir, "audio_request.
const outPath = resolve(flag("out", join(hyperframesDir, "audio_meta.json")));
const sfxLibDir = resolve(flag("sfx-lib", join(HERE, "..", "assets", "sfx")));
const lyriaRecipe = resolve(flag("lyria-recipe", join(HERE, "lyria-recipe.py")));
const acestepRecipe = resolve(flag("acestep-recipe", join(HERE, "acestep-recipe.mjs")));
const onlyArg = flag("only", "tts,bgm,sfx");
const only = new Set(
onlyArg
Expand Down Expand Up @@ -196,7 +198,11 @@ if (only.has("bgm")) {
// a pending job it can't await). Only the UNSET/auto default picks generate
// when HeyGen is absent.
const explicitMode = bgmModeOverride || request.bgm?.mode || null;
let mode = noBgm ? "none" : explicitMode || (heygenOK ? "retrieve" : "generate");
const requestedBgmProvider = request.bgm?.provider || configuredBgmProvider();
let mode = noBgm
? "none"
: explicitMode ||
(requestedBgmProvider === "acestep" ? "generate" : heygenOK ? "retrieve" : "generate");
if (mode === "retrieve" && !heygenOK) {
anomalies.push(
"bgm: retrieve requires a HeyGen credential — skipped (no generate fallback for an explicit retrieve)",
Expand Down Expand Up @@ -232,6 +238,8 @@ if (only.has("bgm")) {
durationS: totalDuration || 30,
hyperframesDir,
lyriaRecipe: existsSync(lyriaRecipe) ? lyriaRecipe : null,
acestepRecipe: existsSync(acestepRecipe) ? acestepRecipe : null,
provider: requestedBgmProvider,
seedSeconds,
hasVoice,
});
Expand Down
30 changes: 28 additions & 2 deletions skills/media-use/audio/scripts/lib/bgm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { spawn, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, openSync, closeSync } from "node:fs";
import { join } from "node:path";
import { downloadTo, searchSounds } from "./heygen.mjs";
import { aceStepConfig, configuredBgmProvider } from "../../../scripts/lib/acestep-provider.mjs";
import { pythonInvocation } from "./python.mjs";

const r3 = (x) => Number(x.toFixed(3));
Expand Down Expand Up @@ -115,17 +116,43 @@ export function generateBgmDetached({
durationS,
hyperframesDir,
lyriaRecipe,
acestepRecipe,
provider,
seedSeconds = 28,
hasVoice,
}) {
const rel = "assets/bgm/track.wav";
const useAceStep =
(provider || configuredBgmProvider()) === "acestep" &&
!!aceStepConfig().baseUrl &&
!!acestepRecipe &&
existsSync(acestepRecipe);
const rel = useAceStep ? "assets/bgm/track.mp3" : "assets/bgm/track.wav";
const abs = join(hyperframesDir, rel);
mkdirSync(join(hyperframesDir, "assets", "bgm"), { recursive: true });
const log = join(hyperframesDir, "assets", "bgm", `bgm-${Date.now()}.log`);
const targetS = Math.max(1, durationS);
const baseMeta = { path: rel, mode: null, volume: bgmDefaultVolume(hasVoice), pending: true };

const lyriaConfigured = !!lyriaKey() && !!lyriaRecipe && existsSync(lyriaRecipe);
const fd = openSync(log, "w");

if (useAceStep) {
const proc = spawn(
process.execPath,
[acestepRecipe, "--output", abs, "--duration", String(targetS), "--prompt", prompt],
{ detached: true, stdio: ["ignore", fd, fd] },
);
proc.unref();
closeSync(fd);
return {
...baseMeta,
mode: "detached-single",
provider: "acestep",
pid: proc.pid,
log,
target_duration_s: r3(targetS),
};
}

// Make a backend runnable: prefer Lyria when configured (install google-genai
// on demand), else ensure local MusicGen deps. Installs are synchronous here —
Expand All @@ -134,7 +161,6 @@ export function generateBgmDetached({
const useLyria = lyriaConfigured && pyOk(LYRIA_PY_PROBE);
if (!useLyria && !pyOk(BGM_PY_PROBE)) pipInstall(BGM_PY_DEPS);

const fd = openSync(log, "w");
if (useLyria) {
const { cmd, args } = pythonInvocation([
lyriaRecipe,
Expand Down
2 changes: 1 addition & 1 deletion skills/media-use/references/audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ node <SKILL_DIR>/audio/scripts/audio.mjs --request ./audio_request.json --out ./

- **Request** `{ provider?, lang?, speed?, lines: [{ id, text, sfx?: [names] }], bgm: { mode?, query?, prompt? } }`: `id` joins each line back to your model; `bgm.mode` = `retrieve | generate | none` (omit for auto). `--only tts,bgm,sfx` runs a subset and merges into an existing `--out`.
- **Output** `audio_meta.json` (id-keyed): `voices[].{path,duration_s,words[]}` (word timestamps for captions), `sfx[]`, `bgm`, `total_duration_s`.
- **HeyGen free-usage path**: HeyGen CLI auth unlocks TTS plus music/SFX retrieval. Local/provider-specific generators are explicit alternatives where installed; run `node <SKILL_DIR>/scripts/resolve.mjs --doctor` before assuming retrieval or TTS will work.
- **HeyGen free-usage path**: HeyGen CLI auth unlocks TTS plus music/SFX retrieval. A configured `acestep.remote` endpoint is the standard generated-BGM path; local/provider-specific generators remain explicit alternatives where installed. Run `node <SKILL_DIR>/scripts/resolve.mjs --doctor` before assuming retrieval or TTS will work.
- If BGM took the generate path (`bgm_pending: true`), run `audio/scripts/wait-bgm.mjs` before final render.

Single-shot helpers: `audio/scripts/heygen-tts.mjs` (one voice file). Transcription / background removal / captions use the `hyperframes` CLI (`transcribe`, `remove-background`), see the per-topic guides in `audio/references/` (`tts.md`, `bgm.md`, `sfx.md`, `transcribe.md`, `remove-background.md`, `captions/`).
Expand Down
22 changes: 21 additions & 1 deletion skills/media-use/references/resolve.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Returns one line: `resolved <id> → <path> (<type>, <metadata>)`

| Type | What it finds | Provider / cascade |
| ------- | -------------------------------- | ------------------------------------------------------------ |
| `bgm` | Background music | HeyGen audio catalog (10k+ tracks) |
| `bgm` | Background music | HeyGen catalog; optional ACE-Step remote generation |
| `sfx` | Sound effects | Bundled 19-file library + HeyGen catalog |
| `image` | Photos, backgrounds | HeyGen asset search (75k+ vectors) |
| `icon` | Icons, symbols | HeyGen asset search (type=icon) |
Expand Down Expand Up @@ -65,6 +65,7 @@ node <SKILL_DIR>/scripts/resolve.mjs --type lut --intent "teal orange blockbuste
| `--for` | Analyze a local image/video and add measured adjust suggestions (`grade` only) |
| `--local-only` | Offline: skip every network provider (cache + local only) |
| `--provider` | Force one generator (e.g. `codex`, `mflux`, `kokoro`, `heygen`) |
| `--duration` | Generated BGM length in seconds, 10-600 (default 30) |
| `--adopt` | Bulk-import existing assets/ into manifest |
| `--doctor` | Check local CLI dependencies; no manifest changes |
| `--stats` | Print local usage stats from `.media/` and `~/.media`; no manifest changes |
Expand Down Expand Up @@ -103,6 +104,25 @@ The deterministic floor still runs automatically: an identical (case/whitespace-
4. Search via provider (HeyGen audio catalog, HeyGen asset search), or resolve color locally
5. Freeze file to `.media/<type>/`, register in manifest, regenerate `index.md`, auto-promote to `~/.media/`

### ACE-Step remote generation

`acestep.remote` is the supported self-hosted BGM generator. Configure it in `~/.media/providers.json`:

```json
{
"version": 1,
"bgm": {
"default": "acestep",
"acestep": {
"base_url": "https://music.example.ts.net",
"api_key_env": "ACESTEP_API_KEY"
}
}
}
```

The key is optional and is read only from the named environment variable. The provider submits the native `/release_task` request, polls `/query_result`, downloads `/v1/audio`, and then follows the same freeze, ledger, and global-cache path as every other provider. Use `--provider acestep --duration 45` to force it for one resolve. `--local-only` always blocks it because a Tailnet service is still a network provider.

Steps 1 and 3 are the **deterministic floor**: they only auto-reuse an exact-normalized match, never a fuzzy one. Semantic reuse ("close enough") is the agent's explicit call via [Reuse before you resolve](#reuse-before-you-resolve) — it never happens automatically. The agent gets back **one line**; candidates, scores, provenance stay on disk.

## Adopt existing projects
Expand Down
3 changes: 2 additions & 1 deletion skills/media-use/references/setup-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ see the ladder and override.

| Type | Provider / path |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| bgm/sfx | heygen catalog free-usage path |
| bgm | heygen catalog free-usage path; optional self-hosted `acestep.remote` generation configured in `~/.media/providers.json` |
| sfx | heygen catalog free-usage path |
| image | heygen search free-usage path; optional local mflux; codex `image_gen` upsell |
| voice | heygen tts free-usage path; optional local **Kokoro** (free, on-device) |
| icon | heygen asset search free-usage path |
Expand Down
Loading