Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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 docs/deploy/cloud.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The credential is **shared with the [`heygen` CLI](https://github.com/heygen-com
3. `~/.heygen/credentials`

<Note>
Point the CLI at a different backend with `HEYGEN_API_URL` (default `https://api.heygen.com`). Use `hyperframes auth refresh` to force-refresh an OAuth token before a long job; `hyperframes auth logout` clears the stored credential.
Point the CLI at a different backend with `HEYGEN_API_URL` (default `https://api.heygen.com`). Use `hyperframes auth refresh` to force-refresh an OAuth token before a long job; `hyperframes auth logout` clears the stored credential. For the keys voice, music, and capture use across the skills — and the fully local fallback — see [Authentication & API keys](/guides/authentication).
</Note>

## How a cloud render flows
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"group": "Guides",
"pages": [
"guides/pipeline",
"guides/authentication",
"guides/video-components",
"guides/html-in-canvas",
"guides/website-to-video",
Expand Down
88 changes: 88 additions & 0 deletions docs/guides/authentication.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: Authentication & API keys
description: "Sign in to HeyGen, and how the keys for voice, music, and capture resolve across the CLI and skills — including the priority order and the fully local fallback."
---

HyperFrames uses a HeyGen credential for premium voiceover (TTS) and the music / sound-effects library. Other providers are optional, and **everything runs without any key** — voice and music fall back to fully local engines. This page covers signing in, the keys each capability uses, and the order they resolve.

## Sign in

Signing in is the same OAuth step as creating an account — new users land on the sign-up screen.

<Steps>
<Step title="Sign in">
The default flow opens your browser for OAuth and captures the token on a loopback port:

```bash
npx hyperframes auth login
# ✓ Signed in.
```

For CI or headless machines, save a long-lived API key instead:

```bash
npx hyperframes auth login --api-key # hidden-input prompt
echo "$HEYGEN_API_KEY" | npx hyperframes auth login --api-key # from stdin
```
</Step>
<Step title="Confirm what's configured">
```bash
npx hyperframes auth status
```

Shows the active credential's source and verified identity, and — when you're signed out — which local engines voice and music will use. Add `--json` for `{ configured, recommended_action, offline_engines }` in scripts.
</Step>
</Steps>

The credential is **shared with the [`heygen` CLI](https://github.com/heygen-com/heygen-cli)** — sign in with either (`hyperframes auth login` or `heygen auth login`) and the other picks up the session. It lives in `~/.heygen/credentials` (mode `0600`), so there's no per-repo `.env` to manage.

<Tip>
No account needed to try HyperFrames. With no credential, voice uses **Kokoro** and music uses **MusicGen**, both fully local and offline — see [Working offline](#working-offline).
</Tip>

## How credentials resolve

The HeyGen credential drives TTS and music / SFX **retrieval**. It resolves first-match-wins:

1. `HEYGEN_API_KEY` — environment variable
2. `HYPERFRAMES_API_KEY` — alias, for parity with other tools
3. `~/.heygen/credentials` — written by `hyperframes auth login` (or `heygen auth login`)

Point at a different config directory with `HEYGEN_CONFIG_DIR`, or a different backend with `HEYGEN_API_URL`.

## Keys by capability

Each capability picks the **first available provider** in order; the last is always a local engine that needs no key. Cloud providers below the HeyGen line need their own key *and* a local Python dependency.

| Capability | Provider order | Key(s) — first match wins | Local dependency |
|------------|----------------|---------------------------|------------------|
| **Voice (TTS)** | HeyGen → ElevenLabs → Kokoro | `HEYGEN_API_KEY` → `HYPERFRAMES_API_KEY` → `~/.heygen` · then `ELEVENLABS_API_KEY` | Kokoro: `pip install kokoro-onnx soundfile` |
| **Music (BGM)** | HeyGen library → Lyria → MusicGen | HeyGen credential (above) · then `GEMINI_API_KEY` → `GOOGLE_API_KEY` | MusicGen: `pip install transformers torch soundfile numpy` |
| **Sound effects** | HeyGen library → bundled library | HeyGen credential (above) | bundled — no deps |
| **Capture descriptions** | OpenRouter → Gemini | `OPENROUTER_API_KEY` → `GEMINI_API_KEY` | — (optional; for [website-to-video](/guides/website-to-video)) |

Run `npx hyperframes doctor` to check which local dependencies are installed. The media skills also run `hyperframes auth status` as a preflight before generating, so you always know whether a run will use HeyGen or a local engine before it starts.

## Working offline

No key configured is a normal state, not an error. The workflow runs entirely on local models:

- **Voice** — Kokoro-82M (54 voices), with Whisper for word-level caption alignment.
- **Music** — MusicGen (`facebook/musicgen-small`).
- **Sound effects** — a bundled library.

Local engines are free and offline; HeyGen gives higher-quality voices and a professionally produced music library. Sign in any time to switch a project from local to HeyGen.

## Environment variables

| Variable | Used for |
|----------|----------|
| `HEYGEN_API_KEY` | HeyGen credential — voice + music/SFX retrieval. Highest priority. |
| `HYPERFRAMES_API_KEY` | Alias for `HEYGEN_API_KEY`. |
| `HEYGEN_API_URL` | API base URL (default `https://api.heygen.com`). |
| `HEYGEN_CONFIG_DIR` | Credentials directory (default `~/.heygen`). |
| `ELEVENLABS_API_KEY` | ElevenLabs TTS, used when no HeyGen credential is present. |
| `GEMINI_API_KEY` / `GOOGLE_API_KEY` | Lyria music generation (and capture descriptions). |
| `OPENROUTER_API_KEY` | Capture descriptions; takes priority over Gemini for that step. |

See the [`hyperframes auth`](/packages/cli#hyperframes-auth) command reference for subcommand details, and [Cloud rendering](/deploy/cloud) for using the same credential to render in HeyGen's cloud.
2 changes: 2 additions & 0 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,8 @@ hyperframes auth logout --yes # skip the confirmation prompt
| `HEYGEN_API_URL` | API base URL (default `https://api.heygen.com`). |
| `HEYGEN_CONFIG_DIR` | Credentials directory (default `~/.heygen`). |

For the keys other capabilities use — ElevenLabs and Gemini for voice/music fallback, OpenRouter/Gemini for capture — and how the skills prioritize them, see [Authentication & API keys](/guides/authentication).

## hyperframes cloud

Render a HyperFrames composition on HeyGen's hosted cloud — no local Chrome, no local ffmpeg, no AWS to manage. Sign in once with `hyperframes auth login` and the same credential drives every `cloud` subcommand.
Expand Down
50 changes: 50 additions & 0 deletions packages/cli/src/audio/providers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { decideMusic, decideVoice, KOKORO_PIP, MUSICGEN_PIP } from "./providers.js";

describe("decideVoice — mirrors the skill's heygen → elevenlabs → kokoro order", () => {
it("prefers HeyGen when configured", () => {
const r = decideVoice({ hasHeygen: true, elevenlabs: true, kokoro: true });
expect(r.engine).toBe("heygen");
expect(r.ready).toBe(true);
});

it("falls to ElevenLabs only when key + module are both present", () => {
expect(decideVoice({ hasHeygen: false, elevenlabs: true, kokoro: true }).engine).toBe(
"elevenlabs",
);
});

it("falls to Kokoro when no cloud provider is usable", () => {
expect(decideVoice({ hasHeygen: false, elevenlabs: false, kokoro: true }).engine).toBe(
"kokoro",
);
});

it("flags Kokoro as not-ready with a pip hint when deps are missing", () => {
const r = decideVoice({ hasHeygen: false, elevenlabs: false, kokoro: false });
expect(r.engine).toBe("kokoro");
expect(r.ready).toBe(false);
expect(r.setupHint).toBe(KOKORO_PIP);
});

it("omits the hint when Kokoro is ready", () => {
expect(
decideVoice({ hasHeygen: false, elevenlabs: false, kokoro: true }).setupHint,
).toBeUndefined();
});
});

describe("decideMusic — mirrors the skill's heygen → lyria → musicgen order", () => {
it("prefers HeyGen, then Lyria, then MusicGen", () => {
expect(decideMusic({ hasHeygen: true, lyria: true, musicgen: true }).engine).toBe("heygen");
expect(decideMusic({ hasHeygen: false, lyria: true, musicgen: true }).engine).toBe("lyria");
expect(decideMusic({ hasHeygen: false, lyria: false, musicgen: true }).engine).toBe("musicgen");
});

it("flags MusicGen as not-ready with a pip hint when deps are missing", () => {
const r = decideMusic({ hasHeygen: false, lyria: false, musicgen: false });
expect(r.engine).toBe("musicgen");
expect(r.ready).toBe(false);
expect(r.setupHint).toBe(MUSICGEN_PIP);
});
});
102 changes: 102 additions & 0 deletions packages/cli/src/audio/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Which voice / music engine a workflow will actually use, and whether
* its local dependencies are present. Mirrors the resolution order the
* hyperframes-media skill scripts use, so `auth status` and `doctor`
* report the same engine the render pipeline would pick:
*
* voice: HeyGen Starfish → ElevenLabs (key + `elevenlabs`) → Kokoro (local)
* music: HeyGen library → Lyria (key + `google.genai`) → MusicGen (local)
*
* The decision is split from the probing: `decide*` is pure (unit-tested
* without spawning Python); `gather*` collects the live facts.
*/

import { hasPythonModules } from "../tts/python.js";

/** Python import names probed for each local engine. */
export const KOKORO_MODULES = ["kokoro_onnx", "soundfile"];
export const MUSICGEN_MODULES = ["transformers", "torch", "soundfile", "numpy"];

/** pip one-liners shown when a local engine's deps are missing. */
export const KOKORO_PIP = "pip install kokoro-onnx soundfile";
export const MUSICGEN_PIP = "pip install transformers torch soundfile numpy";

export type VoiceEngine = "heygen" | "elevenlabs" | "kokoro";
export type MusicEngine = "heygen" | "lyria" | "musicgen";

export interface EngineReadiness<E> {
engine: E;
/** Human label, e.g. "Kokoro". */
label: string;
/** A local engine (no account needed) vs a cloud provider keyed by env. */
local: boolean;
/** Usable right now: cloud key present, or local deps installed. */
ready: boolean;
/** Shown when `ready` is false — how to make it ready. */
setupHint?: string;
}

export interface VoiceFacts {
hasHeygen: boolean;
/** ELEVENLABS_API_KEY set AND the `elevenlabs` module importable. */
elevenlabs: boolean;
/** Kokoro's local deps importable. */
kokoro: boolean;
}

export interface MusicFacts {
hasHeygen: boolean;
/** A Gemini/Google key set AND `google.genai` importable. */
lyria: boolean;
/** MusicGen's local deps importable. */
musicgen: boolean;
}

export function decideVoice(f: VoiceFacts): EngineReadiness<VoiceEngine> {
if (f.hasHeygen) return { engine: "heygen", label: "HeyGen Starfish", local: false, ready: true };
if (f.elevenlabs) return { engine: "elevenlabs", label: "ElevenLabs", local: false, ready: true };
return {
engine: "kokoro",
label: "Kokoro",
local: true,
ready: f.kokoro,
...(f.kokoro ? {} : { setupHint: KOKORO_PIP }),
};
}

export function decideMusic(f: MusicFacts): EngineReadiness<MusicEngine> {
if (f.hasHeygen) return { engine: "heygen", label: "HeyGen library", local: false, ready: true };
if (f.lyria) return { engine: "lyria", label: "Lyria (Gemini)", local: false, ready: true };
return {
engine: "musicgen",
label: "MusicGen",
local: true,
ready: f.musicgen,
...(f.musicgen ? {} : { setupHint: MUSICGEN_PIP }),
};
}

/** Collect live voice facts. Skips Python probes when HeyGen is configured. */
function gatherVoiceFacts(hasHeygen: boolean): VoiceFacts {
if (hasHeygen) return { hasHeygen, elevenlabs: false, kokoro: false };
const elevenlabs = Boolean(process.env["ELEVENLABS_API_KEY"]) && hasPythonModules(["elevenlabs"]);
const kokoro = hasPythonModules(KOKORO_MODULES);
return { hasHeygen, elevenlabs, kokoro };
}

/** Collect live music facts. Skips Python probes when HeyGen is configured. */
function gatherMusicFacts(hasHeygen: boolean): MusicFacts {
if (hasHeygen) return { hasHeygen, lyria: false, musicgen: false };
const hasLyriaKey = Boolean(process.env["GEMINI_API_KEY"] || process.env["GOOGLE_API_KEY"]);
const lyria = hasLyriaKey && hasPythonModules(["google.genai"]);
const musicgen = hasPythonModules(MUSICGEN_MODULES);
return { hasHeygen, lyria, musicgen };
}

export function resolveVoice(hasHeygen: boolean): EngineReadiness<VoiceEngine> {
return decideVoice(gatherVoiceFacts(hasHeygen));
}

export function resolveMusic(hasHeygen: boolean): EngineReadiness<MusicEngine> {
return decideMusic(gatherMusicFacts(hasHeygen));
}
103 changes: 103 additions & 0 deletions packages/cli/src/commands/auth/status-guidance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Onboarding guidance shown by `auth status` when nothing is configured.
*
* Kept separate from `status.ts` so the wording is pure (it depends only
* on colors, not on the credential resolver / API client / system probe)
* and can be unit-tested without booting the whole CLI dependency graph.
* Environment detection lives in `status.ts`; this module only renders.
*/

import { c } from "../../ui/colors.js";

export interface UnconfiguredContext {
/** A human can act on guidance now — a TTY, or a coding agent driving the CLI. */
interactive: boolean;
}

/** The local engine a workflow will fall back to, and whether it's ready. */
export interface OfflineEngineLine {
capability: "voice" | "music";
/** Engine label, e.g. "Kokoro" / "MusicGen". */
label: string;
/** Deps installed (local) or key present (cloud) — usable right now. */
ready: boolean;
/** How to make it ready, shown when `ready` is false. */
setupHint?: string;
}

/** The recommended first step; sign-in and sign-up are the same OAuth flow. */
const RECOMMENDED_ACTION = "hyperframes auth login";

/**
* Render the "what offline will use" block from probed engine readiness.
* Falls back to a generic one-liner when readiness wasn't probed (e.g. a
* caller that didn't want to spawn Python).
*/
function offlineEngineLines(engines?: OfflineEngineLine[]): string[] {
if (!engines || engines.length === 0) {
return [
c.dim("Prefer offline? Just continue — local engines (Kokoro · MusicGen) need no account."),
];
}
const lines = ["Prefer offline? Workflows will use these local engines:"];
for (const e of engines) {
const cap = e.capability.padEnd(5);
if (e.ready) {
lines.push(` ${cap} → ${e.label} ${c.success("✓ ready")}`);
} else {
lines.push(` ${cap} → ${e.label} ${c.warn("⚠ deps missing")}`);
if (e.setupHint) lines.push(` ${c.dim(e.setupHint)}`);
}
}
if (engines.some((e) => !e.ready)) {
lines.push(c.dim(" (or run `hyperframes doctor` to check the local toolchain)"));
}
return lines;
}

/**
* Human guidance for an unconfigured machine — registration-first.
* Recommends signing in via either CLI — `hyperframes auth login` (always
* available here) or `heygen auth login` (if the HeyGen CLI is installed);
* both are the same OAuth login, create an account, and share `~/.heygen`.
* Names the local fallback so "no key" never reads as a failure, and never
* steers users toward a per-repo `.env`. Mirrors the canonical wording in
* the hyperframes-media skill's Preflight section.
*/
export function buildUnconfiguredLines(
ctx: UnconfiguredContext,
engines?: OfflineEngineLine[],
): string[] {
if (!ctx.interactive) {
return [
c.warn("Not signed in to HeyGen (non-interactive)."),
c.dim(
"Set HEYGEN_API_KEY to use HeyGen, or workflows fall back to local engines (Kokoro voice · MusicGen music).",
),
];
}
return [
c.warn("Not signed in to HeyGen — voice & music will use local engines (free, offline)."),
"",
"Sign in or create an account — either CLI works (same shared login, no per-repo .env):",
` ${c.accent("hyperframes auth login")} ${c.dim("# always available via this repo's CLI")}`,
` ${c.accent("heygen auth login")} ${c.dim("# if you use the HeyGen CLI")}`,
` ${c.accent("hyperframes auth login --api-key")} ${c.dim("# paste an existing key instead")}`,
"",
...offlineEngineLines(engines),
];
}

/** Machine-readable form of the unconfigured guidance for `--json`. */
export function buildUnconfiguredJson(
ctx: UnconfiguredContext,
engines?: OfflineEngineLine[],
): Record<string, unknown> {
return {
configured: false,
interactive: ctx.interactive,
recommended_action: RECOMMENDED_ACTION,
fallback: "local",
...(engines ? { offline_engines: engines } : {}),
};
}
Loading
Loading