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,51 @@
|
||||
import { createWriteStream, renameSync, unlinkSync } from "node:fs";
|
||||
import { get as httpsGet } from "node:https";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
|
||||
/**
|
||||
* Download a file from a URL, following redirects.
|
||||
* Uses atomic write (download to .tmp, rename on success) to prevent
|
||||
* corrupt partial files from persisting in the cache on interruption.
|
||||
*/
|
||||
export function downloadFile(url: string, dest: string): Promise<void> {
|
||||
const tmp = `${dest}.tmp`;
|
||||
return new Promise((resolve, reject) => {
|
||||
const follow = (u: string) => {
|
||||
httpsGet(u, (res) => {
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
const location = res.headers.location;
|
||||
if (location) {
|
||||
follow(location);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
const file = createWriteStream(tmp);
|
||||
pipeline(res, file)
|
||||
.then(() => {
|
||||
renameSync(tmp, dest);
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
try {
|
||||
unlinkSync(tmp);
|
||||
} catch {
|
||||
// ignore cleanup failure
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
}).on("error", (err) => {
|
||||
try {
|
||||
unlinkSync(tmp);
|
||||
} catch {
|
||||
// ignore cleanup failure
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
follow(url);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user