Add HyperFrames skills (from heygen-com/hyperframes, Apache-2.0)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXVsD1DgdzZLiPuwtKzMSY
This commit is contained in:
300 files changed
+49175
No files matched your search
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env node
|
||||
// HeyGen voice clones: `clone <audio> --name <n>`, `delete <voice_id>`, `list --prefix <p>` (references/tts.md).
|
||||
// The clone id works as `heygen-tts.mjs --voice <id>`. A refusal prints HeyGen's own message and exits 1.
|
||||
// Auth: same resolver as heygen-tts.mjs (lib/heygen.mjs).
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { extname } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { heygenAuthHeaders, heygenJSON, heygenMessage, loadEnvFromDir } from "./lib/heygen.mjs";
|
||||
import { isMainModule } from "./lib/main-module.mjs";
|
||||
|
||||
const MEDIA_TYPES = { ".mp3": "audio/mpeg", ".wav": "audio/wav" };
|
||||
const POLL_MS = 2_000;
|
||||
const DEADLINE_MS = 120_000;
|
||||
|
||||
async function clone(file, name, headers, { sleep, now }) {
|
||||
const mediaType = MEDIA_TYPES[extname(file).toLowerCase()];
|
||||
if (!mediaType) throw new Error(`unsupported audio file ${file}: use .mp3 or .wav`);
|
||||
const data = readFileSync(file).toString("base64");
|
||||
const created = await heygenJSON("/voices/clone", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: { voice_name: name, audio: { type: "base64", media_type: mediaType, data } },
|
||||
});
|
||||
const id = created.data.voice_clone_id;
|
||||
const deadline = now() + DEADLINE_MS;
|
||||
for (;;) {
|
||||
const { data: voice } = await heygenJSON(`/voices/${id}`, { headers });
|
||||
if (voice.status === "complete") return { voice_id: voice.voice_id ?? id };
|
||||
if (voice.status === "failed") throw new Error(voice.failure_message || "voice clone failed");
|
||||
if (now() >= deadline) throw new Error(`voice clone ${id} not ready after 120 s`);
|
||||
await sleep(POLL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
async function list(prefix, headers) {
|
||||
const mine = [];
|
||||
const seen = new Set();
|
||||
let token;
|
||||
do {
|
||||
if (token && seen.has(token)) throw new Error(`HeyGen returned page token ${token} twice`);
|
||||
if (token) seen.add(token);
|
||||
const query = new URLSearchParams({ type: "private", limit: "100" });
|
||||
if (token) query.set("token", token);
|
||||
const page = await heygenJSON(`/voices?${query}`, { headers });
|
||||
mine.push(...page.data.filter((v) => (v.name ?? "").startsWith(prefix)));
|
||||
token = page.has_more ? page.next_token : null;
|
||||
} while (token);
|
||||
// ponytail: the list response has no created_at, so one GET per match; bounded by the clone limit.
|
||||
return Promise.all(
|
||||
mine.map(async ({ voice_id, name }) => {
|
||||
const { data } = await heygenJSON(`/voices/${voice_id}`, { headers });
|
||||
return { voice_id, name, created_at: data.created_at ?? null };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function main(
|
||||
argv,
|
||||
{
|
||||
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
||||
now = Date.now,
|
||||
out = (s) => process.stdout.write(`${s}\n`),
|
||||
err = (s) => process.stderr.write(`${s}\n`),
|
||||
} = {},
|
||||
) {
|
||||
try {
|
||||
const { positionals, values } = parseArgs({
|
||||
args: argv,
|
||||
allowPositionals: true,
|
||||
options: { name: { type: "string" }, prefix: { type: "string", default: "" } },
|
||||
});
|
||||
const [command, arg] = positionals;
|
||||
if (command === "clone") {
|
||||
if (!arg || !values.name) throw new Error("usage: clone <audio-file> --name <name>");
|
||||
out(JSON.stringify(await clone(arg, values.name, heygenAuthHeaders(), { sleep, now })));
|
||||
} else if (command === "delete") {
|
||||
if (!arg) throw new Error("usage: delete <voice_id>");
|
||||
await heygenJSON(`/voices/${encodeURIComponent(arg)}`, {
|
||||
method: "DELETE",
|
||||
headers: heygenAuthHeaders(),
|
||||
});
|
||||
} else if (command === "list") {
|
||||
out(JSON.stringify(await list(values.prefix, heygenAuthHeaders())));
|
||||
} else {
|
||||
throw new Error("usage: heygen-voice.mjs clone|delete|list (see the header of this file)");
|
||||
}
|
||||
return 0;
|
||||
} catch (e) {
|
||||
err(heygenMessage(e));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMainModule(import.meta.url)) {
|
||||
loadEnvFromDir(process.cwd());
|
||||
process.exitCode = await main(process.argv.slice(2));
|
||||
}
|
||||
Reference in new issue
Block a user