mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(cli): add tts command for local text-to-speech via Kokoro-82M (#201)
* feat(cli): add `tts` command for local text-to-speech via Kokoro-82M Adds `hyperframes tts` — generate speech audio locally using Kokoro-82M (ONNX), no API key needed. Mirrors the transcribe command architecture. - New command: `hyperframes tts "text" --voice af_heart --output speech.wav` - 54 voices across 8 languages, ~5x realtime on CPU - Auto-downloads model (~311 MB) + voices (~27 MB) to ~/.cache/hyperframes/tts/ - Requires Python 3.8+ with kokoro-onnx installed - Extracted shared `downloadFile` utility from whisper/manager.ts with atomic .tmp→rename to prevent partial download corruption - Added hyperframes-tts skill with voice selection guide - Updated CLAUDE.md with TTS docs, voice table, and skill reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(tts): improve skill per skill-creator guidelines - Move trigger info from body to frontmatter description - Remove `trigger` field (not a valid frontmatter field) - Remove CLI flag docs Claude can derive from --help - Remove redundant voice tables (keep content-to-voice mapping) - Fix composition audio example to use actual <audio> element pattern - Keep non-obvious workflows: TTS+transcribe for captions, long scripts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(tts): add guidance for using external TTS sources Help users understand when to use cloud TTS (voice cloning, broader languages, higher quality) vs the built-in Kokoro model, and how external audio integrates into the same composition workflow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(tts): prioritize HeyGen API as recommended cloud TTS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(tts): remove external TTS section for now Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(tts): set required: false on input arg so --list works standalone Citty treats positional args as required by default unless explicitly set to required: false. Without this, `hyperframes tts --list` fails with "Missing required positional argument". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(tts): add --help examples and fix required:false for --list Add examples section to `tts --help` matching the pattern from other commands (transcribe, render, etc.). Fix citty positional arg requiring explicit `required: false` for --list to work standalone. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add CLI command checklist to CLAUDE.md Ensure new commands always get --help examples in help.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cb0b17062a
commit
7389c0c89b
@@ -0,0 +1,99 @@
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { downloadFile } from "../utils/download.js";
|
||||
|
||||
const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "tts");
|
||||
const MODELS_DIR = join(CACHE_DIR, "models");
|
||||
const VOICES_DIR = join(CACHE_DIR, "voices");
|
||||
|
||||
const DEFAULT_MODEL = "kokoro-v1.0";
|
||||
|
||||
const MODEL_URLS: Record<string, string> = {
|
||||
"kokoro-v1.0":
|
||||
"https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx",
|
||||
};
|
||||
|
||||
const VOICES_URL =
|
||||
"https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/voices-v1.0.bin";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Voices — Kokoro ships 54 voices across 8 languages. We expose a curated
|
||||
// default set and allow users to specify any valid Kokoro voice ID.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface VoiceInfo {
|
||||
id: string;
|
||||
label: string;
|
||||
language: string;
|
||||
gender: "female" | "male";
|
||||
}
|
||||
|
||||
export const BUNDLED_VOICES: VoiceInfo[] = [
|
||||
{ id: "af_heart", label: "Heart", language: "en-US", gender: "female" },
|
||||
{ id: "af_nova", label: "Nova", language: "en-US", gender: "female" },
|
||||
{ id: "af_sky", label: "Sky", language: "en-US", gender: "female" },
|
||||
{ id: "am_adam", label: "Adam", language: "en-US", gender: "male" },
|
||||
{ id: "am_michael", label: "Michael", language: "en-US", gender: "male" },
|
||||
{ id: "bf_emma", label: "Emma", language: "en-GB", gender: "female" },
|
||||
{ id: "bf_isabella", label: "Isabella", language: "en-GB", gender: "female" },
|
||||
{ id: "bm_george", label: "George", language: "en-GB", gender: "male" },
|
||||
];
|
||||
|
||||
export const DEFAULT_VOICE = "af_heart";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Ensure the Kokoro ONNX model is downloaded and cached.
|
||||
* Returns the path to the .onnx model file.
|
||||
*/
|
||||
export async function ensureModel(
|
||||
model: string = DEFAULT_MODEL,
|
||||
options?: { onProgress?: (message: string) => void },
|
||||
): Promise<string> {
|
||||
const modelPath = join(MODELS_DIR, `${model}.onnx`);
|
||||
if (existsSync(modelPath)) return modelPath;
|
||||
|
||||
const url = MODEL_URLS[model];
|
||||
if (!url) {
|
||||
throw new Error(
|
||||
`Unknown TTS model: ${model}. Available: ${Object.keys(MODEL_URLS).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
mkdirSync(MODELS_DIR, { recursive: true });
|
||||
options?.onProgress?.(`Downloading TTS model ${model} (~311 MB)...`);
|
||||
await downloadFile(url, modelPath);
|
||||
|
||||
if (!existsSync(modelPath)) {
|
||||
throw new Error(`Model download failed: ${model}`);
|
||||
}
|
||||
|
||||
return modelPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the Kokoro voices bundle is downloaded and cached.
|
||||
* Returns the path to the voices .bin file.
|
||||
*/
|
||||
export async function ensureVoices(options?: {
|
||||
onProgress?: (message: string) => void;
|
||||
}): Promise<string> {
|
||||
const voicesPath = join(VOICES_DIR, "voices-v1.0.bin");
|
||||
if (existsSync(voicesPath)) return voicesPath;
|
||||
|
||||
mkdirSync(VOICES_DIR, { recursive: true });
|
||||
options?.onProgress?.("Downloading voice data (~27 MB)...");
|
||||
await downloadFile(VOICES_URL, voicesPath);
|
||||
|
||||
if (!existsSync(voicesPath)) {
|
||||
throw new Error("Voice data download failed");
|
||||
}
|
||||
|
||||
return voicesPath;
|
||||
}
|
||||
|
||||
export { MODELS_DIR, VOICES_DIR, DEFAULT_MODEL };
|
||||
Reference in New Issue
Block a user