Skip to content
Merged
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 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 lives in `~/.heygen/credentials` (mode `0600`) — no per-repo `.env` to manage. Browser OAuth is a `hyperframes auth login` feature. The separate [`heygen` CLI](https://github.com/heygen-com/heygen-cli) (its own install — there's no `npx heygen`) is API-key-only, so `heygen auth login` just stores a key you paste. Both read the same `~/.heygen/credentials`, so signing in with one carries to the other.

<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));
}
105 changes: 105 additions & 0 deletions packages/cli/src/commands/auth/status-guidance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* 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 = "npx 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.
* Both paths use `npx hyperframes` (zero-install via npm): browser OAuth
* (sign-in / sign-up) and `--api-key` both write `~/.heygen`. The separate
* `heygen` CLI shares that file but needs its own install (no `npx heygen`),
* so it's left to the docs — not dangled here as a command a fresh machine
* can't run. Names the local fallback so "no key" never reads as a failure,
* and never steers users toward a per-repo `.env`. Mirrors 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 sign up (browser OAuth, writes ~/.heygen — no per-repo .env):",
` ${c.accent("npx hyperframes auth login")} ${c.dim("# browser sign-in / sign-up")}`,
"",
"Or paste an existing HeyGen API key (get one at app.heygen.com/settings/api):",
` ${c.accent("npx hyperframes auth login --api-key")} ${c.dim("# paste at the prompt")}`,
"",
...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