🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@marswave/listenhub-cli

Package Overview
Dependencies
Maintainers
5
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@marswave/listenhub-cli - npm Package Compare versions

Comparing version
0.0.3
to
0.0.4
+1285
dist/cli.mjs
#!/usr/bin/env node
import { Command, Option } from "commander";
import process from "node:process";
import { ListenHubClient, ListenHubError } from "@marswave/listenhub-sdk";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import http from "node:http";
import ora from "ora";
import { access, readFile, stat } from "node:fs/promises";
//#region source/_shared/output.ts
function printJson(data) {
console.log(JSON.stringify(data, null, 2));
}
function printDetail(label, rows) {
console.log(`\u2713 ${label}\n`);
for (const [key, value] of rows) if (value !== void 0) console.log(` ${key.padEnd(10)} ${String(value)}`);
}
function printTable(headers, rows) {
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
console.log(" " + headers.map((h, i) => h.padEnd(widths[i])).join(" "));
for (const row of rows) console.log(" " + row.map((c, i) => c.padEnd(widths[i])).join(" "));
}
var CliTimeoutError = class extends Error {
constructor(message) {
super(message);
this.name = "CliTimeoutError";
}
};
var CliAuthError = class extends Error {
constructor(message) {
super(message);
this.name = "CliAuthError";
}
};
function handleError(error, json) {
if (json) {
const message = error instanceof ListenHubError ? {
error: error.message,
code: error.code,
requestId: error.requestId
} : {
error: error instanceof Error ? error.message : String(error),
code: "UNKNOWN"
};
console.error(JSON.stringify(message, null, 2));
} else {
const message = error instanceof Error ? error.message : String(error);
console.error(`\u2717 Error: ${message}`);
}
if (error instanceof CliAuthError || error instanceof ListenHubError && (error.status === 401 || error.status === 403)) process.exit(2);
if (error instanceof CliTimeoutError) process.exit(3);
process.exit(1);
}
//#endregion
//#region source/_shared/credentials.ts
function getConfigDir() {
const xdg = process.env["XDG_CONFIG_HOME"];
return path.join(xdg ?? path.join(os.homedir(), ".config"), "listenhub");
}
function getCredentialsPath() {
return path.join(getConfigDir(), "credentials.json");
}
async function loadCredentials() {
const filePath = getCredentialsPath();
try {
const raw = fs.readFileSync(filePath, "utf8");
return JSON.parse(raw);
} catch {
return;
}
}
async function saveCredentials(creds) {
const dir = getConfigDir();
fs.mkdirSync(dir, { recursive: true });
const filePath = getCredentialsPath();
const temporaryPath = `${filePath}.tmp.${process.pid}`;
fs.writeFileSync(temporaryPath, JSON.stringify(creds, null, " "), { mode: 384 });
fs.renameSync(temporaryPath, filePath);
}
async function deleteCredentials() {
const filePath = getCredentialsPath();
try {
fs.unlinkSync(filePath);
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
throw error;
}
}
//#endregion
//#region source/auth/login-server.ts
const loginTimeoutMs = 300 * 1e3;
async function startCallbackServer() {
let resolveCode;
let rejectCode;
const codePromise = new Promise((resolve, reject) => {
resolveCode = resolve;
rejectCode = reject;
});
const timeout = setTimeout(() => {
rejectCode(/* @__PURE__ */ new Error("Login timed out after 5 minutes. Please try again."));
}, loginTimeoutMs);
const server = http.createServer((request, response) => {
const url = new URL(request.url, "http://localhost");
const code = url.searchParams.get("code");
const error = url.searchParams.get("error");
if (error) {
const description = url.searchParams.get("error_description") ?? error;
response.writeHead(200, { "Content-Type": "text/plain" });
response.end(`Login failed: ${description}`);
clearTimeout(timeout);
rejectCode(/* @__PURE__ */ new Error(`OAuth error: ${description}`));
return;
}
if (code) {
response.writeHead(200, { "Content-Type": "text/html" });
response.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
clearTimeout(timeout);
resolveCode({ code });
} else {
response.writeHead(400, { "Content-Type": "text/plain" });
response.end("Missing code parameter");
}
});
await new Promise((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
const { port } = server.address();
return {
port,
waitForCode: async () => codePromise,
close() {
clearTimeout(timeout);
server.close();
}
};
}
//#endregion
//#region source/auth/auth.ts
async function runLogin() {
const server = await startCallbackServer();
try {
const client = new ListenHubClient();
const { sessionId, authUrl } = await client.connectInit({ callbackPort: server.port });
console.error("Opening browser for login...");
const { default: open } = await import("open");
await open(authUrl);
const { code } = await server.waitForCode();
const tokens = await client.connectToken({
sessionId,
code
});
await saveCredentials({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: Date.now() + tokens.expiresIn * 1e3
});
const user = await new ListenHubClient({ accessToken: tokens.accessToken }).getCurrentUser();
console.log(`\u2713 Logged in as ${user.nickname || user.email || "user"}`);
} finally {
server.close();
}
}
async function runLogout() {
const creds = await loadCredentials();
if (creds?.refreshToken) try {
await new ListenHubClient({ accessToken: creds.accessToken }).revoke({ refreshToken: creds.refreshToken });
} catch {
console.error("Warning: remote revoke failed, local credentials cleared");
}
await deleteCredentials();
console.log("✓ Logged out");
}
async function runStatus(json) {
const creds = await loadCredentials();
if (!creds) {
if (json) console.log(JSON.stringify({ loggedIn: false }));
else console.log("Not logged in");
process.exit(1);
}
try {
const user = await new ListenHubClient({ accessToken: creds.accessToken }).getCurrentUser();
const expiresAt = new Date(creds.expiresAt).toISOString();
if (json) console.log(JSON.stringify({
loggedIn: true,
user: user.nickname,
email: user.email,
expiresAt
}, null, 2));
else {
console.log(`\u2713 Logged in as ${user.nickname || "user"}`);
console.log(` Email: ${user.email}`);
console.log(` Expires at: ${expiresAt}`);
}
} catch {
if (json) console.log(JSON.stringify({
loggedIn: false,
error: "Token expired or invalid"
}));
else console.log("Not logged in (token expired or invalid)");
process.exit(1);
}
}
//#endregion
//#region source/auth/_cli.ts
function register$9(program) {
const auth = program.command("auth").description("Manage authentication");
auth.command("login").description("Log in via browser OAuth").action(async () => {
try {
await runLogin();
} catch (error) {
handleError(error, false);
}
});
auth.command("logout").description("Log out and revoke tokens").action(async () => {
try {
await runLogout();
} catch (error) {
handleError(error, false);
}
});
auth.command("status").description("Show current login status").option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await runStatus(options.json);
} catch (error) {
handleError(error, options.json);
}
});
}
//#endregion
//#region source/_shared/client.ts
const refreshBufferMs = 6e4;
let refreshPromise;
async function ensureFreshCredentials() {
const creds = await loadCredentials();
if (!creds) throw new CliAuthError("Not logged in. Run `listenhub auth login` first.");
if (creds.expiresAt - Date.now() >= refreshBufferMs) return;
const tokens = await new ListenHubClient({ accessToken: creds.accessToken }).refresh({ refreshToken: creds.refreshToken });
await saveCredentials({
...creds,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: Date.now() + tokens.expiresIn * 1e3
});
}
async function getClient() {
refreshPromise ??= ensureFreshCredentials().finally(() => {
refreshPromise = void 0;
});
await refreshPromise;
const creds = await loadCredentials();
if (!creds) throw new CliAuthError("Not logged in. Run `listenhub auth login` first.");
return new ListenHubClient({ accessToken: creds.accessToken });
}
//#endregion
//#region source/creation/creation.ts
async function getCreation(client, episodeId, json) {
const detail = await client.getCreation(episodeId);
if (json) {
printJson(detail);
return;
}
printDetail("Creation details", [
["ID:", detail.id],
["Type:", detail.generationType],
["Status:", detail.processStatus],
["Language:", detail.language],
["Created:", new Date(detail.createdAt).toISOString()]
]);
}
async function deleteCreations(client, ids, json) {
await client.deleteCreations({ ids });
if (json) printJson({ deleted: ids });
else console.log(`\u2713 Deleted ${String(ids.length)} creation(s)`);
}
//#endregion
//#region source/creation/_cli.ts
function register$8(program) {
const cmd = program.command("creation").description("Manage creations");
cmd.command("get <id>").description("Get creation details").option("-j, --json", "Output JSON", false).action(async (id, options) => {
try {
await getCreation(await getClient(), id, options.json);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("delete <id...>").description("Delete one or more creations").option("-j, --json", "Output JSON", false).action(async (ids, options) => {
try {
await deleteCreations(await getClient(), ids, options.json);
} catch (error) {
handleError(error, options.json);
}
});
}
//#endregion
//#region source/_shared/language.ts
const cjkRegex = /[\u4E00-\u9FFF\u3400-\u4DBF]/v;
const kanaRegex = /[\u3040-\u309F\u30A0-\u30FF]/v;
function inferLanguage(text) {
if (!text) return "en";
if (kanaRegex.test(text)) return "ja";
if (cjkRegex.test(text)) return "zh";
return "en";
}
//#endregion
//#region source/_shared/polling.ts
const pollIntervalMs = 1e4;
const defaultTimeoutS = 300;
async function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function pollUntilDone(client, episodeId, options) {
const timeoutS = options.timeout ?? defaultTimeoutS;
const maxAttempts = Math.ceil(timeoutS / (pollIntervalMs / 1e3));
const spinner = options.json ? void 0 : ora({ text: `${options.label ?? "Creating"}... (1/${maxAttempts})` }).start();
for (let i = 0; i < maxAttempts; i++) {
if (i > 0) await sleep(pollIntervalMs);
const detail = await client.getCreation(episodeId);
if (detail.processStatus === "success") {
spinner?.succeed(`${options.label ?? "Created"} successfully`);
return detail;
}
if (detail.processStatus === "fail") {
spinner?.fail("Creation failed");
throw new Error(`Creation failed (code: ${detail.failCode})`);
}
if (spinner) spinner.text = `${options.label ?? "Creating"}... (${String(i + 2)}/${maxAttempts})`;
}
spinner?.fail("Timed out");
throw new CliTimeoutError(`Timed out after ${timeoutS}s`);
}
async function pollImageUntilDone(client, imageId, options) {
const timeoutS = options.timeout ?? 120;
const maxAttempts = Math.ceil(timeoutS / (pollIntervalMs / 1e3));
const spinner = options.json ? void 0 : ora({ text: `Creating image... (1/${maxAttempts})` }).start();
for (let i = 0; i < maxAttempts; i++) {
if (i > 0) await sleep(pollIntervalMs);
const item = await client.getAIImage(imageId);
if (item.status === "success") {
spinner?.succeed("Image created successfully");
return item;
}
if (item.status === "fail") {
spinner?.fail("Image creation failed");
throw new Error("Image creation failed");
}
if (spinner) spinner.text = `Creating image... (${String(i + 2)}/${maxAttempts})`;
}
spinner?.fail("Timed out");
throw new CliTimeoutError(`Timed out after ${timeoutS}s`);
}
async function pollMusicTaskUntilDone(client, taskId, options) {
const timeoutS = options.timeout ?? 600;
const maxAttempts = Math.ceil(timeoutS / (pollIntervalMs / 1e3));
const spinner = options.json ? void 0 : ora({ text: `Creating music... (1/${maxAttempts})` }).start();
for (let i = 0; i < maxAttempts; i++) {
if (i > 0) await sleep(pollIntervalMs);
const task = await client.getMusicTask(taskId);
if (task.status === "success") {
spinner?.succeed("Music created successfully");
return task;
}
if (task.status === "failed") {
spinner?.fail("Music creation failed");
throw new Error(`Music creation failed${task.errorMessage ? `: ${task.errorMessage}` : ""}`);
}
if (spinner) spinner.text = `Creating music... (${String(i + 2)}/${maxAttempts})`;
}
spinner?.fail("Timed out");
throw new CliTimeoutError(`Timed out after ${timeoutS}s`);
}
const lyricsIntervalMs = 5e3;
async function pollLyricsTaskUntilDone(client, taskId, options) {
const timeoutS = options.timeout ?? 120;
const maxAttempts = Math.ceil(timeoutS / (lyricsIntervalMs / 1e3));
const spinner = options.json ? void 0 : ora({ text: `Creating lyrics... (1/${maxAttempts})` }).start();
for (let i = 0; i < maxAttempts; i++) {
if (i > 0) await sleep(lyricsIntervalMs);
const task = await client.getLyricsTask(taskId);
if (task.status === "success") {
spinner?.succeed("Lyrics generated successfully");
return task;
}
if (task.status === "failed") {
spinner?.fail("Lyrics generation failed");
throw new Error(`Lyrics generation failed${task.errorMessage ? `: ${task.errorMessage}` : ""}`);
}
if (spinner) spinner.text = `Creating lyrics... (${String(i + 2)}/${maxAttempts})`;
}
spinner?.fail("Timed out");
throw new CliTimeoutError(`Timed out after ${timeoutS}s`);
}
//#endregion
//#region source/_shared/sources.ts
function buildSources(urls, texts) {
const sources = [];
for (const uri of urls ?? []) sources.push({
type: "url",
uri
});
for (const content of texts ?? []) sources.push({
type: "text",
content
});
return sources;
}
//#endregion
//#region source/_shared/speaker-resolver.ts
const defaultSpeakers = {
zh: ["CN-Man-Beijing-V2", "gaoqing3-bfb5c88a"],
en: ["cozy-man-english", "travel-girl-english"],
ja: ["tianzhongdunzi-5d612542", "1shenguhaoshivocals-c002bc47"]
};
async function resolveSpeakers(client, options) {
if (options.speakerIds?.length) return options.speakerIds;
if (!options.speakerNames?.length) {
const defaults = defaultSpeakers[options.language] ?? defaultSpeakers.en;
const count = options.count ?? defaults.length;
return defaults.slice(0, count);
}
const { items } = await client.listSpeakers({ language: options.language });
const resolved = [];
for (const name of options.speakerNames) {
const match = items.find((s) => s.name.toLowerCase() === name.toLowerCase());
if (!match) {
const available = items.map((s) => s.name).join(", ");
throw new Error(`Speaker "${name}" not found. Available: ${available}`);
}
resolved.push(match.speakerInnerId);
}
return resolved;
}
//#endregion
//#region source/explainer/explainer.ts
async function createExplainer(client, options) {
const lang = options.lang ?? inferLanguage(options.query);
const speakers = await resolveSpeakers(client, {
speakerNames: options.speaker ? [options.speaker] : void 0,
speakerIds: options.speakerId ? [options.speakerId] : void 0,
language: lang,
count: 1
});
const size = options.imageSize;
const { aspectRatio } = options;
const { episodeId } = await client.createExplainerVideo({
query: options.query,
sources: buildSources(options.sourceUrl, options.sourceText),
style: options.style,
skipAudio: options.skipAudio,
imageConfig: {
size,
aspectRatio
},
template: {
type: "storybook",
mode: options.mode,
speakers,
language: lang,
style: options.style,
size,
aspectRatio
}
});
if (!options.wait) {
if (options.json) printJson({ episodeId });
else console.log(`\u2713 Explainer submitted: ${episodeId}`);
return;
}
const detail = await pollUntilDone(client, episodeId, {
timeout: options.timeout,
label: "Creating explainer",
json: options.json
});
if (options.json) printJson(detail);
else printDetail("Explainer created", [
["ID:", detail.id],
["Title:", detail.topicDetail.title.data],
["Status:", detail.processStatus]
]);
}
async function listExplainerVideos(client, options) {
const { items } = await client.listExplainerVideos({
page: options.page,
pageSize: options.pageSize
});
if (options.json) {
printJson(items);
return;
}
printTable([
"ID",
"Title",
"Status",
"Created"
], items.map((episode) => [
episode.id,
episode.title,
episode.processStatus,
new Date(episode.createdAt).toISOString().slice(0, 10)
]));
}
//#endregion
//#region source/explainer/_cli.ts
function collect$4(value, previous) {
return [...previous, value];
}
function register$7(program) {
const cmd = program.command("explainer").description("Explainer video generation");
cmd.command("create").description("Create an explainer video").option("--query <text>", "Topic text").option("--source-url <url>", "Reference URL (repeatable)", collect$4, []).option("--source-text <text>", "Reference text (repeatable)", collect$4, []).option("--mode <mode>", "Generation mode: info, story", "info").option("--lang <lang>", "Language: en, zh, ja (auto-detected if omitted)").option("--speaker <name>", "Speaker name").option("--speaker-id <id>", "Speaker inner ID").option("--skip-audio", "Skip audio generation", false).option("--image-size <size>", "Image size: 2K, 4K", "2K").option("--aspect-ratio <ratio>", "Aspect ratio: 16:9, 9:16, 1:1", "16:9").option("--style <style>", "Visual style").option("--no-wait", "Return immediately without polling").option("--timeout <seconds>", "Polling timeout", Number, 300).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await createExplainer(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("list").description("List explainer videos").option("--page <n>", "Page number", Number, 1).option("--page-size <n>", "Items per page", Number, 20).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await listExplainerVideos(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
}
//#endregion
//#region source/_shared/upload.ts
const audioExtensions = new Set([
".mp3",
".wav",
".flac",
".m4a",
".ogg",
".aac"
]);
const imageExtensions = new Set([
".jpg",
".jpeg",
".png",
".webp",
".gif"
]);
const maxSizeBytes = {
audio: 20 * 1024 * 1024,
image: 10 * 1024 * 1024
};
const categoryForType = {
audio: "episode",
image: "banana"
};
const mimeTypes = new Map([
[".mp3", "audio/mpeg"],
[".wav", "audio/wav"],
[".flac", "audio/flac"],
[".m4a", "audio/mp4"],
[".ogg", "audio/ogg"],
[".aac", "audio/aac"],
[".jpg", "image/jpeg"],
[".jpeg", "image/jpeg"],
[".png", "image/png"],
[".webp", "image/webp"],
[".gif", "image/gif"]
]);
function allowedExtensions(accept) {
return accept === "audio" ? audioExtensions : imageExtensions;
}
async function resolveFileOrUrl(client, input, options) {
const trimmed = input.trim();
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
const filePath = path.resolve(trimmed);
try {
await access(filePath);
} catch {
throw new Error(`File not found: ${trimmed}`);
}
const ext = path.extname(filePath).toLowerCase();
const allowed = allowedExtensions(options.accept);
if (!allowed.has(ext)) {
const expected = [...allowed].join(", ");
throw new Error(`Unsupported ${options.accept} format: ${ext} (expected: ${expected})`);
}
const fileStat = await stat(filePath);
const maxBytes = maxSizeBytes[options.accept];
if (fileStat.size > maxBytes) {
const sizeMb = (fileStat.size / (1024 * 1024)).toFixed(1);
const maxMb = maxBytes / (1024 * 1024);
throw new Error(`File too large: ${sizeMb} MB (max ${String(maxMb)} MB for ${options.accept})`);
}
const contentType = mimeTypes.get(ext);
const fileKey = path.basename(filePath);
const category = categoryForType[options.accept];
const { presignedUrl, fileUrl } = await client.createFileUpload({
fileKey,
contentType,
category
});
const buffer = await readFile(filePath);
const response = await fetch(presignedUrl, {
method: "PUT",
body: buffer,
headers: {
"Content-Type": contentType,
"Content-Length": String(buffer.length)
}
});
if (!response.ok) throw new Error(`Upload failed: ${String(response.status)} ${response.statusText}`);
const { pathname } = new URL(fileUrl);
return `https://storage.googleapis.com${pathname}`;
}
//#endregion
//#region source/image/image.ts
async function createImage(client, options) {
const allReferences = [...options.reference, ...options.referenceUrl];
if (allReferences.length > 5) throw new Error("Too many reference images (max 5)");
const referenceImageUrls = allReferences.length > 0 ? await Promise.all(allReferences.map(async (ref) => resolveFileOrUrl(client, ref, { accept: "image" }))) : void 0;
const { imageId } = await client.createAIImage({
prompt: options.prompt,
...options.model && { model: options.model },
...options.lang && { language: options.lang },
aspectRatio: options.aspectRatio,
imageSize: options.size,
...referenceImageUrls && { referenceImageUrls }
});
if (!options.wait) {
if (options.json) printJson({ imageId });
else console.log(`\u2713 Image submitted: ${imageId}`);
return;
}
const item = await pollImageUntilDone(client, imageId, {
timeout: options.timeout,
json: options.json
});
if (options.json) printJson(item);
else printDetail("Image created", [
["ID:", item.id],
["URL:", item.imageUrl],
["Status:", item.status]
]);
}
async function listImages(client, options) {
const { items } = await client.listAIImages({
page: options.page,
pageSize: options.pageSize
});
if (options.json) {
printJson(items);
return;
}
printTable([
"ID",
"Prompt",
"Status",
"Created"
], items.map((image) => [
image.id,
image.prompt.slice(0, 40),
image.status,
new Date(image.createdAt).toISOString().slice(0, 10)
]));
}
async function getImage(client, imageId, json) {
const item = await client.getAIImage(imageId);
if (json) {
printJson(item);
return;
}
printDetail("Image details", [
["ID:", item.id],
["Prompt:", item.prompt],
["URL:", item.imageUrl],
["Size:", item.imageSize],
["Ratio:", item.aspectRatio],
["Status:", item.status],
["Created:", new Date(item.createdAt).toISOString()]
]);
}
//#endregion
//#region source/image/_cli.ts
function collect$3(value, previous) {
return [...previous, value];
}
function register$6(program) {
const cmd = program.command("image").description("AI image generation");
cmd.command("create").description("Create an AI image").requiredOption("--prompt <text>", "Image description").option("--model <model>", "Model name").option("--lang <lang>", "Prompt language hint").option("--aspect-ratio <ratio>", "Aspect ratio", "1:1").option("--size <size>", "Image size: 1K, 2K, 4K", "2K").option("--reference <path-or-url>", "Reference image, local file or URL (repeatable, max 5)", collect$3, []).option("--no-wait", "Return immediately without polling").option("--timeout <seconds>", "Polling timeout", Number, 120).option("-j, --json", "Output JSON", false).addOption(new Option("--reference-url <url>", "").hideHelp().argParser((value, previous) => [...previous, value]).default([])).action(async (options) => {
try {
await createImage(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("list").description("List AI images").option("--page <n>", "Page number", Number, 1).option("--page-size <n>", "Items per page", Number, 20).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await listImages(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("get <id>").description("Get image details").option("-j, --json", "Output JSON", false).action(async (id, options) => {
try {
await getImage(await getClient(), id, options.json);
} catch (error) {
handleError(error, options.json);
}
});
}
//#endregion
//#region source/lyrics/lyrics.ts
function formatDateTime$1(timestamp) {
const d = new Date(timestamp);
return `${d.toLocaleDateString("sv-SE")} ${d.toLocaleTimeString("en-GB", { hour12: false })}`;
}
function formatDate$1(timestamp) {
return new Date(timestamp).toLocaleDateString("sv-SE");
}
function printLyricsDetail(task) {
if (task.status === "failed") {
const rows = [
["Task ID:", task.id],
["Status:", task.status],
["Error:", task.errorMessage],
["Created:", formatDateTime$1(task.createdAt)]
];
console.log(`\u2717 Lyrics task\n`);
for (const [key, value] of rows) if (value !== void 0) console.log(` ${key.padEnd(10)} ${String(value)}`);
return;
}
console.log(`\u2713 Lyrics generated (${task.variants.length} variants)\n`);
for (const [i, variant] of task.variants.entries()) {
console.log(` \u2500\u2500 Variant ${String(i + 1)} \u2500\u2500`);
console.log(` Title: ${variant.title}\n`);
for (const line of variant.text.split("\n")) console.log(` ${line}`);
console.log();
}
}
async function createGenerate$1(client, options) {
if (!options.prompt.trim()) throw new Error("Prompt is required");
const result = await client.createLyrics({ prompt: options.prompt });
if (!options.wait) {
if (options.json) printJson(result);
else console.log(`\u2713 Lyrics task submitted: ${result.taskId}`);
return;
}
const task = await pollLyricsTaskUntilDone(client, result.taskId, {
timeout: options.timeout,
json: options.json
});
if (options.json) printJson(task);
else printLyricsDetail(task);
}
async function listTasks$1(client, options) {
const response = await client.listLyricsTasks({
page: options.page,
pageSize: options.pageSize,
...options.status && { status: options.status }
});
if (options.json) {
printJson(response);
return;
}
printTable([
"ID",
"Status",
"Prompt",
"Variants",
"Created"
], response.items.map((task) => [
task.id,
task.status,
task.params.prompt.length > 30 ? task.params.prompt.slice(0, 30) + "…" : task.params.prompt,
String(task.variants.length),
formatDate$1(task.createdAt)
]));
}
async function getTask$1(client, taskId, json) {
const task = await client.getLyricsTask(taskId);
if (json) {
printJson(task);
return;
}
printLyricsDetail(task);
}
//#endregion
//#region source/lyrics/_cli.ts
function register$5(program) {
const cmd = program.command("lyrics").description("Lyrics generation");
cmd.command("generate").description("Generate lyrics from a text prompt").requiredOption("--prompt <text>", "Lyrics description (max 200 chars)").option("--no-wait", "Return immediately without polling").option("--timeout <seconds>", "Polling timeout", Number, 120).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await createGenerate$1(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("list").description("List lyrics tasks").option("--page <n>", "Page number", Number, 1).option("--page-size <n>", "Items per page", Number, 20).option("--status <status>", "Filter by status (pending, generating, success, failed)").option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await listTasks$1(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("get <taskId>").description("Get lyrics task details").option("-j, --json", "Output JSON", false).action(async (taskId, options) => {
try {
await getTask$1(await getClient(), taskId, options.json);
} catch (error) {
handleError(error, options.json);
}
});
}
//#endregion
//#region source/music/music.ts
function formatDuration(seconds) {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${String(m)}:${String(s).padStart(2, "0")}`;
}
function formatDate(timestamp) {
return new Date(timestamp).toLocaleDateString("sv-SE");
}
function formatDateTime(timestamp) {
const d = new Date(timestamp);
return `${d.toLocaleDateString("sv-SE")} ${d.toLocaleTimeString("en-GB", { hour12: false })}`;
}
function printMusicDetail(task) {
const rows = [
["Task ID:", task.id],
["Type:", task.taskType.toLowerCase()],
["Status:", task.status]
];
if (task.status === "failed") rows.push(["Error:", task.errorMessage]);
else {
const trackTitle = task.tracks[0]?.title ?? task.params.title;
if (trackTitle) rows.push(["Title:", trackTitle]);
rows.push(["Tracks:", task.tracks.length]);
for (const [i, track] of task.tracks.entries()) rows.push([`Track ${String(i + 1)}:`, `${track.audioUrl} (${formatDuration(track.duration)})`]);
}
rows.push(["Created:", formatDateTime(task.createdAt)]);
if (task.status === "failed") {
console.log(`\u2717 Music task\n`);
for (const [key, value] of rows) if (value !== void 0) console.log(` ${key.padEnd(10)} ${String(value)}`);
} else printDetail("Music task", rows);
}
async function createGenerate(client, options) {
if (!options.prompt.trim()) throw new Error("Prompt is required");
const result = await client.createMusicGenerate({
prompt: options.prompt,
...options.style && { style: options.style },
...options.title && { title: options.title },
...options.instrumental && { instrumental: true }
});
if (!options.wait) {
if (options.json) printJson(result);
else console.log(`\u2713 Music task submitted: ${result.taskId}`);
return;
}
const task = await pollMusicTaskUntilDone(client, result.taskId, {
timeout: options.timeout,
json: options.json
});
if (options.json) printJson(task);
else printMusicDetail(task);
}
async function createCover(client, options) {
const uploadUrl = await resolveFileOrUrl(client, options.audio, { accept: "audio" });
const result = await client.createMusicCover({
uploadUrl,
...options.prompt && { prompt: options.prompt },
...options.style && { style: options.style },
...options.title && { title: options.title },
...options.instrumental && { instrumental: true }
});
if (!options.wait) {
if (options.json) printJson(result);
else console.log(`\u2713 Music task submitted: ${result.taskId}`);
return;
}
const task = await pollMusicTaskUntilDone(client, result.taskId, {
timeout: options.timeout,
json: options.json
});
if (options.json) printJson(task);
else printMusicDetail(task);
}
async function createExtend(client, options) {
const parameters = {
uploadUrl: await resolveFileOrUrl(client, options.audio, { accept: "audio" }),
model: options.model,
continueAt: options.continueAt,
...options.prompt && { prompt: options.prompt },
...options.style && { style: options.style },
...options.title && { title: options.title },
...options.instrumental && { instrumental: true },
...options.negativeTags && { negativeTags: options.negativeTags },
...options.vocalGender && { vocalGender: options.vocalGender },
...options.styleWeight !== void 0 && { styleWeight: options.styleWeight },
...options.weirdness !== void 0 && { weirdnessConstraint: options.weirdness },
...options.audioWeight !== void 0 && { audioWeight: options.audioWeight }
};
const result = await client.createMusicExtend(parameters);
if (!options.wait) {
if (options.json) printJson(result);
else console.log(`\u2713 Music task submitted: ${result.taskId}`);
return;
}
const task = await pollMusicTaskUntilDone(client, result.taskId, {
timeout: options.timeout,
json: options.json
});
if (options.json) printJson(task);
else printMusicDetail(task);
}
async function listTasks(client, options) {
const { items } = await client.listMusicTasks({
page: options.page,
pageSize: options.pageSize,
...options.status && { status: options.status }
});
if (options.json) {
printJson({ items });
return;
}
printTable([
"ID",
"Type",
"Status",
"Title",
"Tracks",
"Created"
], items.map((task) => [
task.id,
task.taskType.toLowerCase(),
task.status,
task.tracks[0]?.title ?? task.params.title ?? "—",
String(task.tracks.length),
formatDate(task.createdAt)
]));
}
async function getTask(client, taskId, json) {
const task = await client.getMusicTask(taskId);
if (json) {
printJson(task);
return;
}
printMusicDetail(task);
}
//#endregion
//#region source/music/_cli.ts
function register$4(program) {
const cmd = program.command("music").description("Music generation");
cmd.command("generate").description("Generate music from a text prompt").requiredOption("--prompt <text>", "Music description").option("--style <text>", "Music style/mood").option("--title <text>", "Track title").option("--instrumental", "Instrumental only, no vocals", false).option("--no-wait", "Return immediately without polling").option("--timeout <seconds>", "Polling timeout", Number, 600).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await createGenerate(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("cover").description("Create a cover from reference audio").requiredOption("--audio <path-or-url>", "Reference audio file or URL").option("--prompt <text>", "Music description").option("--style <text>", "Music style/mood").option("--title <text>", "Track title").option("--instrumental", "Instrumental only, no vocals", false).option("--no-wait", "Return immediately without polling").option("--timeout <seconds>", "Polling timeout", Number, 600).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await createCover(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("extend").description("Extend music from reference audio").requiredOption("--audio <path-or-url>", "Reference audio file or URL").requiredOption("--model <version>", "Model version (V4, V4_5, V4_5PLUS, V4_5ALL, V5, V5_5)").requiredOption("--continue-at <seconds>", "Start extending from this time point", Number).option("--prompt <text>", "Lyrics or description").option("--style <text>", "Music style/mood").option("--title <text>", "Track title").option("--instrumental", "Instrumental only, no vocals", false).option("--negative-tags <text>", "Styles to exclude").option("--vocal-gender <gender>", "Vocal gender (m or f)").option("--style-weight <weight>", "Style guidance weight (0-1)", Number).option("--weirdness <weight>", "Creativity/weirdness constraint (0-1)", Number).option("--audio-weight <weight>", "Input audio influence weight (0-1)", Number).option("--no-wait", "Return immediately without polling").option("--timeout <seconds>", "Polling timeout", Number, 600).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await createExtend(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("list").description("List music tasks").option("--page <n>", "Page number", Number, 1).option("--page-size <n>", "Items per page", Number, 20).option("--status <status>", "Filter by status (pending, generating, uploading, success, failed)").option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await listTasks(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("get <taskId>").description("Get music task details").option("-j, --json", "Output JSON", false).action(async (taskId, options) => {
try {
await getTask(await getClient(), taskId, options.json);
} catch (error) {
handleError(error, options.json);
}
});
}
//#endregion
//#region source/podcast/podcast.ts
async function createPodcast(client, options) {
const lang = options.lang ?? inferLanguage(options.query);
const speakers = await resolveSpeakers(client, {
speakerNames: options.speaker,
speakerIds: options.speakerId,
language: lang
});
const { episodeId } = await client.createPodcast({
type: speakers.length <= 1 ? "podcast-solo" : "podcast-duo",
query: options.query,
sources: buildSources(options.sourceUrl, options.sourceText),
template: {
type: "podcast",
mode: options.mode,
speakers,
language: lang
}
});
if (!options.wait) {
if (options.json) printJson({ episodeId });
else console.log(`\u2713 Podcast submitted: ${episodeId}`);
return;
}
const detail = await pollUntilDone(client, episodeId, {
timeout: options.timeout,
label: "Creating podcast",
json: options.json
});
if (options.json) printJson(detail);
else printDetail("Podcast created", [
["ID:", detail.id],
["Title:", detail.topicDetail.title.data],
["Status:", detail.processStatus]
]);
}
async function listPodcasts(client, options) {
const { items } = await client.listPodcasts({
page: options.page,
pageSize: options.pageSize
});
if (options.json) {
printJson(items);
return;
}
printTable([
"ID",
"Title",
"Status",
"Created"
], items.map((episode) => [
episode.id,
episode.title,
episode.processStatus,
new Date(episode.createdAt).toISOString().slice(0, 10)
]));
}
//#endregion
//#region source/podcast/_cli.ts
function collect$2(value, previous) {
return [...previous, value];
}
function register$3(program) {
const cmd = program.command("podcast").description("Podcast generation");
cmd.command("create").description("Create a podcast episode").option("--query <text>", "Topic text").option("--source-url <url>", "Reference URL (repeatable)", collect$2, []).option("--source-text <text>", "Reference text (repeatable)", collect$2, []).option("--mode <mode>", "Generation mode: quick, deep, debate", "quick").option("--lang <lang>", "Language: en, zh, ja (auto-detected if omitted)").option("--speaker <name>", "Speaker name (repeatable)", collect$2, []).option("--speaker-id <id>", "Speaker inner ID (repeatable)", collect$2, []).option("--no-wait", "Return immediately without polling").option("--timeout <seconds>", "Polling timeout", Number, 300).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await createPodcast(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("list").description("List podcast episodes").option("--page <n>", "Page number", Number, 1).option("--page-size <n>", "Items per page", Number, 20).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await listPodcasts(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
}
//#endregion
//#region source/slides/slides.ts
async function createSlides(client, options) {
const lang = options.lang ?? inferLanguage(options.query);
const speakers = await resolveSpeakers(client, {
speakerNames: options.speaker ? [options.speaker] : void 0,
speakerIds: options.speakerId ? [options.speakerId] : void 0,
language: lang,
count: 1
});
const size = options.imageSize;
const { aspectRatio } = options;
const { episodeId } = await client.createSlides({
query: options.query,
sources: buildSources(options.sourceUrl, options.sourceText),
style: options.style,
skipAudio: options.skipAudio,
imageConfig: {
size,
aspectRatio
},
template: {
type: "storybook",
mode: "slides",
speakers,
language: lang,
style: options.style,
size,
aspectRatio
}
});
if (!options.wait) {
if (options.json) printJson({ episodeId });
else console.log(`\u2713 Slides submitted: ${episodeId}`);
return;
}
const detail = await pollUntilDone(client, episodeId, {
timeout: options.timeout,
label: "Creating slides",
json: options.json
});
if (options.json) printJson(detail);
else printDetail("Slides created", [
["ID:", detail.id],
["Title:", detail.topicDetail.title.data],
["Status:", detail.processStatus]
]);
}
async function listSlides(client, options) {
const { items } = await client.listSlides({
page: options.page,
pageSize: options.pageSize
});
if (options.json) {
printJson(items);
return;
}
printTable([
"ID",
"Title",
"Status",
"Created"
], items.map((episode) => [
episode.id,
episode.title,
episode.processStatus,
new Date(episode.createdAt).toISOString().slice(0, 10)
]));
}
//#endregion
//#region source/slides/_cli.ts
function collect$1(value, previous) {
return [...previous, value];
}
function register$2(program) {
const cmd = program.command("slides").description("Slides generation");
cmd.command("create").description("Create a slide deck").option("--query <text>", "Topic text").option("--source-url <url>", "Reference URL (repeatable)", collect$1, []).option("--source-text <text>", "Reference text (repeatable)", collect$1, []).option("--lang <lang>", "Language: en, zh, ja (auto-detected if omitted)").option("--speaker <name>", "Speaker name").option("--speaker-id <id>", "Speaker inner ID").option("--no-skip-audio", "Generate audio narration").option("--image-size <size>", "Image size: 2K, 4K", "2K").option("--aspect-ratio <ratio>", "Aspect ratio: 16:9, 9:16, 1:1", "16:9").option("--style <style>", "Visual style").option("--no-wait", "Return immediately without polling").option("--timeout <seconds>", "Polling timeout", Number, 300).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await createSlides(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("list").description("List slide decks").option("--page <n>", "Page number", Number, 1).option("--page-size <n>", "Items per page", Number, 20).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await listSlides(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
}
//#endregion
//#region source/speakers/speakers.ts
async function listSpeakers(client, options) {
const { items } = await client.listSpeakers({ language: options.lang });
if (options.json) {
printJson(items);
return;
}
printTable([
"Name",
"ID",
"Gender",
"Personality"
], items.map((s) => [
s.name,
s.speakerInnerId,
s.gender,
s.personality
]));
}
//#endregion
//#region source/speakers/_cli.ts
function register$1(program) {
program.command("speakers").description("Manage speakers").command("list").description("List available speakers").option("--lang <lang>", "Filter by language (en, zh, ja)").option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await listSpeakers(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
}
//#endregion
//#region source/tts/tts.ts
async function createTts(client, options) {
const lang = options.lang ?? inferLanguage(options.text);
const speakers = await resolveSpeakers(client, {
speakerNames: options.speaker ? [options.speaker] : void 0,
speakerIds: options.speakerId ? [options.speakerId] : void 0,
language: lang,
count: 1
});
const sources = options.text ? [{
type: "text",
content: options.text
}] : buildSources(options.sourceUrl, options.sourceText);
const { episodeId } = await client.createTTS({
sources,
template: {
type: "flowspeech",
mode: options.mode,
speakers,
language: lang
}
});
if (!options.wait) {
if (options.json) printJson({ episodeId });
else console.log(`\u2713 TTS submitted: ${episodeId}`);
return;
}
const detail = await pollUntilDone(client, episodeId, {
timeout: options.timeout,
label: "Creating TTS",
json: options.json
});
if (options.json) printJson(detail);
else printDetail("TTS created", [
["ID:", detail.id],
["Title:", detail.topicDetail.title.data],
["Status:", detail.processStatus]
]);
}
async function listTts(client, options) {
const { items } = await client.listTTS({
page: options.page,
pageSize: options.pageSize
});
if (options.json) {
printJson(items);
return;
}
printTable([
"ID",
"Title",
"Status",
"Created"
], items.map((episode) => [
episode.id,
episode.title,
episode.processStatus,
new Date(episode.createdAt).toISOString().slice(0, 10)
]));
}
//#endregion
//#region source/tts/_cli.ts
function register(program) {
const cmd = program.command("tts").description("Text-to-speech generation");
cmd.command("create").description("Create a TTS audio").option("--text <text>", "Text to convert to speech").option("--source-url <url>", "Reference URL (repeatable)", collect, []).option("--source-text <text>", "Reference text (repeatable)", collect, []).option("--mode <mode>", "Generation mode: smart, direct", "smart").option("--lang <lang>", "Language: en, zh, ja (auto-detected if omitted)").option("--speaker <name>", "Speaker name").option("--speaker-id <id>", "Speaker inner ID").option("--no-wait", "Return immediately without polling").option("--timeout <seconds>", "Polling timeout", Number, 300).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await createTts(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
cmd.command("list").description("List TTS episodes").option("--page <n>", "Page number", Number, 1).option("--page-size <n>", "Items per page", Number, 20).option("-j, --json", "Output JSON", false).action(async (options) => {
try {
await listTts(await getClient(), options);
} catch (error) {
handleError(error, options.json);
}
});
}
function collect(value, previous) {
return [...previous, value];
}
//#endregion
//#region source/cli.ts
const program = new Command();
program.name("listenhub").description("ListenHub CLI").version("0.1.0");
register$9(program);
register$3(program);
register(program);
register$7(program);
register$2(program);
register$6(program);
register$4(program);
register$5(program);
register$1(program);
register$8(program);
program.parse();
//#endregion
export {};
+44
-41
{
"name": "@marswave/listenhub-cli",
"version": "0.0.3",
"description": "Command-line interface for ListenHub",
"repository": "marswaveai/listenhub-cli",
"type": "module",
"bin": {
"listenhub": "distribution/source/cli.js"
},
"files": [
"distribution/source"
],
"license": "MIT",
"scripts": {
"prepublishOnly": "pnpm run build",
"clean": "del-cli distribution",
"dev": "pnpm run clean && tsc --watch",
"build": "pnpm run clean && tsc && chmod +x distribution/source/cli.js",
"pretest": "pnpm run build",
"test": "xo"
},
"dependencies": {
"@marswave/listenhub-sdk": "^0.0.4",
"commander": "^14.0.3",
"open": "^10.0.0",
"ora": "^8.0.0"
},
"devDependencies": {
"@sindresorhus/tsconfig": "^8.1.0",
"@types/node": "^25.5.0",
"del-cli": "^7.0.0",
"typescript": "^6.0.2",
"xo": "^2.0.2"
},
"engines": {
"node": ">=20"
},
"pnpm": {
"overrides": {
"typescript": "^6.0.2"
}
}
"name": "@marswave/listenhub-cli",
"version": "0.0.4",
"description": "Command-line interface for ListenHub",
"license": "MIT",
"repository": "marswaveai/listenhub-cli",
"bin": {
"listenhub": "dist/cli.mjs"
},
"files": [
"dist"
],
"type": "module",
"scripts": {
"dev": "vp pack --watch",
"build": "vp pack",
"lint": "vp lint",
"lint:fix": "vp lint --fix",
"fmt": "vp fmt",
"fmt:check": "vp fmt --check",
"check": "vp check",
"test": "vp test run",
"test:watch": "vp test",
"ready": "vp check && vp test run",
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"@marswave/listenhub-sdk": "^0.0.4",
"commander": "^14.0.3",
"open": "^10.0.0",
"ora": "^8.0.0"
},
"devDependencies": {
"@sindresorhus/tsconfig": "^8.1.0",
"@types/node": "^25.5.0",
"picomatch": "^4.0.4",
"typescript": "^5.9.3",
"vite": "^8.0.3",
"vite-plus": "^0.1.18",
"vitest": "^4.0.0"
},
"engines": {
"node": ">=20"
},
"packageManager": "pnpm@10.32.1"
}
+29
-29

@@ -43,5 +43,5 @@ # ListenHub CLI

| Command | Description |
|---------|-------------|
| `listenhub auth login` | Log in via browser OAuth |
| Command | Description |
| ----------------------- | ------------------------- |
| `listenhub auth login` | Log in via browser OAuth |
| `listenhub auth logout` | Log out and revoke tokens |

@@ -52,37 +52,37 @@ | `listenhub auth status` | Show current login status |

| Command | Description |
|---------|-------------|
| `listenhub music generate` | Generate music from a text prompt |
| `listenhub music cover` | Create a cover from reference audio |
| `listenhub music list` | List music tasks |
| `listenhub music get <id>` | Get music task details |
| Command | Description |
| -------------------------- | ----------------------------------- |
| `listenhub music generate` | Generate music from a text prompt |
| `listenhub music cover` | Create a cover from reference audio |
| `listenhub music list` | List music tasks |
| `listenhub music get <id>` | Get music task details |
### Content Creation
| Command | Description |
|---------|-------------|
| `listenhub podcast create` | Create a podcast episode |
| `listenhub podcast list` | List podcast episodes |
| `listenhub tts create` | Create text-to-speech audio |
| `listenhub tts list` | List TTS creations |
| `listenhub explainer create` | Create an explainer video |
| `listenhub explainer list` | List explainer videos |
| `listenhub slides create` | Create a slide deck |
| `listenhub slides list` | List slide decks |
| Command | Description |
| ---------------------------- | --------------------------- |
| `listenhub podcast create` | Create a podcast episode |
| `listenhub podcast list` | List podcast episodes |
| `listenhub tts create` | Create text-to-speech audio |
| `listenhub tts list` | List TTS creations |
| `listenhub explainer create` | Create an explainer video |
| `listenhub explainer list` | List explainer videos |
| `listenhub slides create` | Create a slide deck |
| `listenhub slides list` | List slide decks |
### Images
| Command | Description |
|---------|-------------|
| `listenhub image create` | Generate an AI image |
| `listenhub image list` | List AI images |
| `listenhub image get <id>` | Get image details |
| Command | Description |
| -------------------------- | -------------------- |
| `listenhub image create` | Generate an AI image |
| `listenhub image list` | List AI images |
| `listenhub image get <id>` | Get image details |
### Other
| Command | Description |
|---------|-------------|
| `listenhub speakers list` | List available speakers |
| `listenhub creation get <id>` | Get creation details |
| `listenhub creation delete <id...>` | Delete creations |
| Command | Description |
| ----------------------------------- | ----------------------- |
| `listenhub speakers list` | List available speakers |
| `listenhub creation get <id>` | Get creation details |
| `listenhub creation delete <id...>` | Delete creations |

@@ -89,0 +89,0 @@ Run `listenhub <command> --help` for full options.

@@ -43,36 +43,36 @@ # ListenHub CLI

| 命令 | 说明 |
|------|------|
| `listenhub auth login` | 浏览器 OAuth 登录 |
| `listenhub auth logout` | 登出并撤销 token |
| `listenhub auth status` | 查看登录状态 |
| 命令 | 说明 |
| ----------------------- | ----------------- |
| `listenhub auth login` | 浏览器 OAuth 登录 |
| `listenhub auth logout` | 登出并撤销 token |
| `listenhub auth status` | 查看登录状态 |
### 音乐
| 命令 | 说明 |
|------|------|
| 命令 | 说明 |
| -------------------------- | -------------------- |
| `listenhub music generate` | 根据文字描述生成音乐 |
| `listenhub music cover` | 用参考音频创建翻唱 |
| `listenhub music list` | 列出音乐任务 |
| `listenhub music get <id>` | 查看音乐任务详情 |
| `listenhub music cover` | 用参考音频创建翻唱 |
| `listenhub music list` | 列出音乐任务 |
| `listenhub music get <id>` | 查看音乐任务详情 |
### 内容创作
| 命令 | 说明 |
|------|------|
| `listenhub podcast create` | 创建播客 |
| `listenhub podcast list` | 列出播客 |
| `listenhub tts create` | 创建语音合成 |
| `listenhub tts list` | 列出语音合成 |
| 命令 | 说明 |
| ---------------------------- | ------------ |
| `listenhub podcast create` | 创建播客 |
| `listenhub podcast list` | 列出播客 |
| `listenhub tts create` | 创建语音合成 |
| `listenhub tts list` | 列出语音合成 |
| `listenhub explainer create` | 创建讲解视频 |
| `listenhub explainer list` | 列出讲解视频 |
| `listenhub slides create` | 创建幻灯片 |
| `listenhub slides list` | 列出幻灯片 |
| `listenhub explainer list` | 列出讲解视频 |
| `listenhub slides create` | 创建幻灯片 |
| `listenhub slides list` | 列出幻灯片 |
### 图片
| 命令 | 说明 |
|------|------|
| `listenhub image create` | AI 生图 |
| `listenhub image list` | 列出图片 |
| 命令 | 说明 |
| -------------------------- | ------------ |
| `listenhub image create` | AI 生图 |
| `listenhub image list` | 列出图片 |
| `listenhub image get <id>` | 查看图片详情 |

@@ -82,7 +82,7 @@

| 命令 | 说明 |
|------|------|
| `listenhub speakers list` | 列出可用声音 |
| `listenhub creation get <id>` | 查看作品详情 |
| `listenhub creation delete <id...>` | 删除作品 |
| 命令 | 说明 |
| ----------------------------------- | ------------ |
| `listenhub speakers list` | 列出可用声音 |
| `listenhub creation get <id>` | 查看作品详情 |
| `listenhub creation delete <id...>` | 删除作品 |

@@ -89,0 +89,0 @@ 每个命令都可以加 `--help` 查看完整选项。

import { ListenHubClient } from '@marswave/listenhub-sdk';
import { loadCredentials, saveCredentials } from './credentials.js';
import { CliAuthError } from './output.js';
const refreshBufferMs = 60_000;
// Single-flight: if a refresh is already in progress, reuse the same promise
let refreshPromise;
async function ensureFreshCredentials() {
const creds = await loadCredentials();
if (!creds) {
throw new CliAuthError('Not logged in. Run `listenhub auth login` first.');
}
if (creds.expiresAt - Date.now() >= refreshBufferMs) {
return; // Still fresh
}
const temporaryClient = new ListenHubClient({
accessToken: creds.accessToken,
});
const tokens = await temporaryClient.refresh({
refreshToken: creds.refreshToken,
});
await saveCredentials({
...creds,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: Date.now() + tokens.expiresIn * 1000,
});
}
export async function getClient() {
// Single-flight: concurrent callers share one refresh
refreshPromise ??= ensureFreshCredentials().finally(() => {
refreshPromise = undefined;
});
await refreshPromise;
const creds = await loadCredentials();
if (!creds) {
throw new CliAuthError('Not logged in. Run `listenhub auth login` first.');
}
return new ListenHubClient({ accessToken: creds.accessToken });
}
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
function getConfigDir() {
const xdg = process.env['XDG_CONFIG_HOME'];
return path.join(xdg ?? path.join(os.homedir(), '.config'), 'listenhub');
}
function getCredentialsPath() {
return path.join(getConfigDir(), 'credentials.json');
}
export async function loadCredentials() {
const filePath = getCredentialsPath();
try {
const raw = fs.readFileSync(filePath, 'utf8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- JSON structure matches StoredCredentials
return JSON.parse(raw);
}
catch {
return undefined;
}
}
export async function saveCredentials(creds) {
const dir = getConfigDir();
fs.mkdirSync(dir, { recursive: true });
const filePath = getCredentialsPath();
const temporaryPath = `${filePath}.tmp.${process.pid}`;
fs.writeFileSync(temporaryPath, JSON.stringify(creds, null, '\t'), {
mode: 0o600,
});
fs.renameSync(temporaryPath, filePath);
}
export async function deleteCredentials() {
const filePath = getCredentialsPath();
try {
fs.unlinkSync(filePath);
}
catch (error) {
// ENOENT is fine (already gone), anything else is a real problem
if (error instanceof Error &&
'code' in error &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- checking errno code on Error with 'code' property
error.code === 'ENOENT') {
return;
}
throw error;
}
}
const cjkRegex = /[\u4E00-\u9FFF\u3400-\u4DBF]/v;
const kanaRegex = /[\u3040-\u309F\u30A0-\u30FF]/v;
export function inferLanguage(text) {
if (!text)
return 'en';
if (kanaRegex.test(text))
return 'ja';
if (cjkRegex.test(text))
return 'zh';
return 'en';
}
import process from 'node:process';
import { ListenHubError } from '@marswave/listenhub-sdk';
export function printJson(data) {
console.log(JSON.stringify(data, null, 2));
}
export function printDetail(label, rows) {
console.log(`\u2713 ${label}\n`);
for (const [key, value] of rows) {
if (value !== undefined) {
console.log(` ${key.padEnd(10)} ${String(value)}`);
}
}
}
export function printTable(headers, rows) {
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)));
console.log(' ' + headers.map((h, i) => h.padEnd(widths[i])).join(' '));
for (const row of rows) {
console.log(' ' + row.map((c, i) => c.padEnd(widths[i])).join(' '));
}
}
export class CliTimeoutError extends Error {
constructor(message) {
super(message);
this.name = 'CliTimeoutError';
}
}
export class CliAuthError extends Error {
constructor(message) {
super(message);
this.name = 'CliAuthError';
}
}
export function handleError(error, json) {
if (json) {
const message = error instanceof ListenHubError
? { error: error.message, code: error.code, requestId: error.requestId }
: {
error: error instanceof Error ? error.message : String(error),
code: 'UNKNOWN',
};
console.error(JSON.stringify(message, null, 2));
}
else {
const message = error instanceof Error ? error.message : String(error);
console.error(`\u2717 Error: ${message}`);
}
if (error instanceof CliAuthError ||
(error instanceof ListenHubError &&
(error.status === 401 || error.status === 403))) {
process.exit(2); // eslint-disable-line unicorn/no-process-exit
}
if (error instanceof CliTimeoutError) {
process.exit(3); // eslint-disable-line unicorn/no-process-exit
}
process.exit(1); // eslint-disable-line unicorn/no-process-exit
}
import ora from 'ora';
import { CliTimeoutError } from './output.js';
const pollIntervalMs = 10_000;
const defaultTimeoutS = 300;
async function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export async function pollUntilDone(client, episodeId, options) {
const timeoutS = options.timeout ?? defaultTimeoutS;
const maxAttempts = Math.ceil(timeoutS / (pollIntervalMs / 1000));
const spinner = options.json
? undefined
: ora({
text: `${options.label ?? 'Creating'}... (1/${maxAttempts})`,
}).start();
for (let i = 0; i < maxAttempts; i++) {
if (i > 0) {
await sleep(pollIntervalMs); // eslint-disable-line no-await-in-loop
}
const detail = await client.getCreation(episodeId); // eslint-disable-line no-await-in-loop
if (detail.processStatus === 'success') {
spinner?.succeed(`${options.label ?? 'Created'} successfully`);
return detail;
}
if (detail.processStatus === 'fail') {
spinner?.fail('Creation failed');
throw new Error(`Creation failed (code: ${detail.failCode})`);
}
if (spinner) {
spinner.text = `${options.label ?? 'Creating'}... (${String(i + 2)}/${maxAttempts})`;
}
}
spinner?.fail('Timed out');
throw new CliTimeoutError(`Timed out after ${timeoutS}s`);
}
export async function pollImageUntilDone(client, imageId, options) {
const timeoutS = options.timeout ?? 120;
const maxAttempts = Math.ceil(timeoutS / (pollIntervalMs / 1000));
const spinner = options.json
? undefined
: ora({ text: `Creating image... (1/${maxAttempts})` }).start();
for (let i = 0; i < maxAttempts; i++) {
if (i > 0) {
await sleep(pollIntervalMs); // eslint-disable-line no-await-in-loop
}
const item = await client.getAIImage(imageId); // eslint-disable-line no-await-in-loop
if (item.status === 'success') {
spinner?.succeed('Image created successfully');
return item;
}
if (item.status === 'fail') {
spinner?.fail('Image creation failed');
throw new Error('Image creation failed');
}
if (spinner) {
spinner.text = `Creating image... (${String(i + 2)}/${maxAttempts})`;
}
}
spinner?.fail('Timed out');
throw new CliTimeoutError(`Timed out after ${timeoutS}s`);
}
export async function pollMusicTaskUntilDone(client, taskId, options) {
const timeoutS = options.timeout ?? 600;
const maxAttempts = Math.ceil(timeoutS / (pollIntervalMs / 1000));
const spinner = options.json
? undefined
: ora({ text: `Creating music... (1/${maxAttempts})` }).start();
for (let i = 0; i < maxAttempts; i++) {
if (i > 0) {
await sleep(pollIntervalMs); // eslint-disable-line no-await-in-loop
}
const task = await client.getMusicTask(taskId); // eslint-disable-line no-await-in-loop
if (task.status === 'success') {
spinner?.succeed('Music created successfully');
return task;
}
if (task.status === 'failed') {
spinner?.fail('Music creation failed');
throw new Error(`Music creation failed${task.errorMessage ? `: ${task.errorMessage}` : ''}`);
}
if (spinner) {
spinner.text = `Creating music... (${String(i + 2)}/${maxAttempts})`;
}
}
spinner?.fail('Timed out');
throw new CliTimeoutError(`Timed out after ${timeoutS}s`);
}
const lyricsIntervalMs = 5000;
export async function pollLyricsTaskUntilDone(client, taskId, options) {
const timeoutS = options.timeout ?? 120;
const maxAttempts = Math.ceil(timeoutS / (lyricsIntervalMs / 1000));
const spinner = options.json
? undefined
: ora({ text: `Creating lyrics... (1/${maxAttempts})` }).start();
for (let i = 0; i < maxAttempts; i++) {
if (i > 0) {
await sleep(lyricsIntervalMs); // eslint-disable-line no-await-in-loop
}
const task = await client.getLyricsTask(taskId); // eslint-disable-line no-await-in-loop
if (task.status === 'success') {
spinner?.succeed('Lyrics generated successfully');
return task;
}
if (task.status === 'failed') {
spinner?.fail('Lyrics generation failed');
throw new Error(`Lyrics generation failed${task.errorMessage ? `: ${task.errorMessage}` : ''}`);
}
if (spinner) {
spinner.text = `Creating lyrics... (${String(i + 2)}/${maxAttempts})`;
}
}
spinner?.fail('Timed out');
throw new CliTimeoutError(`Timed out after ${timeoutS}s`);
}
export function buildSources(urls, texts) {
const sources = [];
for (const uri of urls ?? []) {
sources.push({ type: 'url', uri });
}
for (const content of texts ?? []) {
sources.push({ type: 'text', content });
}
return sources;
}
// Default speaker innerIds per language (confirmed from skills shared/speaker-selection.md)
const defaultSpeakers = {
zh: ['CN-Man-Beijing-V2', 'gaoqing3-bfb5c88a'],
en: ['cozy-man-english', 'travel-girl-english'],
ja: ['tianzhongdunzi-5d612542', '1shenguhaoshivocals-c002bc47'],
};
export async function resolveSpeakers(client, options) {
// Direct IDs bypass resolution
if (options.speakerIds?.length) {
return options.speakerIds;
}
// No speaker specified -> use defaults
if (!options.speakerNames?.length) {
const defaults = defaultSpeakers[options.language] ?? defaultSpeakers.en;
const count = options.count ?? defaults.length;
return defaults.slice(0, count);
}
// Resolve names via API
const { items } = await client.listSpeakers({ language: options.language });
const resolved = [];
for (const name of options.speakerNames) {
const match = items.find((s) => s.name.toLowerCase() === name.toLowerCase());
if (!match) {
const available = items.map((s) => s.name).join(', ');
throw new Error(`Speaker "${name}" not found. Available: ${available}`);
}
resolved.push(match.speakerInnerId);
}
return resolved;
}
import { access, readFile, stat } from 'node:fs/promises';
import path from 'node:path';
const audioExtensions = new Set([
'.mp3',
'.wav',
'.flac',
'.m4a',
'.ogg',
'.aac',
]);
const imageExtensions = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
const maxSizeBytes = {
audio: 20 * 1024 * 1024,
image: 10 * 1024 * 1024,
};
const categoryForType = {
audio: 'episode',
image: 'banana',
};
const mimeTypes = new Map([
['.mp3', 'audio/mpeg'],
['.wav', 'audio/wav'],
['.flac', 'audio/flac'],
['.m4a', 'audio/mp4'],
['.ogg', 'audio/ogg'],
['.aac', 'audio/aac'],
['.jpg', 'image/jpeg'],
['.jpeg', 'image/jpeg'],
['.png', 'image/png'],
['.webp', 'image/webp'],
['.gif', 'image/gif'],
]);
function allowedExtensions(accept) {
return accept === 'audio' ? audioExtensions : imageExtensions;
}
export async function resolveFileOrUrl(client, input, options) {
const trimmed = input.trim();
// URL — pass through
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
return trimmed;
}
// Local file — resolve to absolute path
const filePath = path.resolve(trimmed);
// Existence check
try {
await access(filePath);
}
catch {
throw new Error(`File not found: ${trimmed}`);
}
// Extension check
const ext = path.extname(filePath).toLowerCase();
const allowed = allowedExtensions(options.accept);
if (!allowed.has(ext)) {
const expected = [...allowed].join(', ');
throw new Error(`Unsupported ${options.accept} format: ${ext} (expected: ${expected})`);
}
// Size check
const fileStat = await stat(filePath);
const maxBytes = maxSizeBytes[options.accept];
if (fileStat.size > maxBytes) {
const sizeMb = (fileStat.size / (1024 * 1024)).toFixed(1);
const maxMb = maxBytes / (1024 * 1024);
throw new Error(`File too large: ${sizeMb} MB (max ${String(maxMb)} MB for ${options.accept})`);
}
// Get presigned upload URL
const contentType = mimeTypes.get(ext);
const fileKey = path.basename(filePath);
const category = categoryForType[options.accept];
const { presignedUrl, fileUrl } = await client.createFileUpload({
fileKey,
contentType,
category,
});
// Upload to GCS
const buffer = await readFile(filePath);
const response = await fetch(presignedUrl, {
method: 'PUT',
body: buffer,
headers: {
'Content-Type': contentType,
'Content-Length': String(buffer.length),
},
});
if (!response.ok) {
throw new Error(`Upload failed: ${String(response.status)} ${response.statusText}`);
}
// Return a storage.googleapis.com URL so the server's resolveUploadUrl
// can correctly strip the bucket name prefix and re-sign for downstream use.
const { pathname } = new URL(fileUrl);
return `https://storage.googleapis.com${pathname}`;
}
import { handleError } from '../_shared/output.js';
import { runLogin, runLogout, runStatus } from './auth.js';
export function register(program) {
const auth = program.command('auth').description('Manage authentication');
auth
.command('login')
.description('Log in via browser OAuth')
.action(async () => {
try {
await runLogin();
}
catch (error) {
handleError(error, false);
}
});
auth
.command('logout')
.description('Log out and revoke tokens')
.action(async () => {
try {
await runLogout();
}
catch (error) {
handleError(error, false);
}
});
auth
.command('status')
.description('Show current login status')
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
await runStatus(options.json);
}
catch (error) {
handleError(error, options.json);
}
});
}
import process from 'node:process';
import { ListenHubClient } from '@marswave/listenhub-sdk';
import { deleteCredentials, loadCredentials, saveCredentials, } from '../_shared/credentials.js';
import { startCallbackServer } from './login-server.js';
export async function runLogin() {
const server = await startCallbackServer();
try {
const client = new ListenHubClient();
const { sessionId, authUrl } = await client.connectInit({
callbackPort: server.port,
});
console.error('Opening browser for login...');
// Dynamic import because `open` is ESM-only
const { default: open } = await import('open');
await open(authUrl);
const { code } = await server.waitForCode();
const tokens = await client.connectToken({ sessionId, code });
await saveCredentials({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: Date.now() + tokens.expiresIn * 1000,
});
// Fetch username to confirm
const authedClient = new ListenHubClient({
accessToken: tokens.accessToken,
});
const user = await authedClient.getCurrentUser();
console.log(`\u2713 Logged in as ${user.nickname || user.email || 'user'}`);
}
finally {
server.close();
}
}
export async function runLogout() {
const creds = await loadCredentials();
if (creds?.refreshToken) {
try {
const client = new ListenHubClient({ accessToken: creds.accessToken });
await client.revoke({ refreshToken: creds.refreshToken });
}
catch {
console.error('Warning: remote revoke failed, local credentials cleared');
}
}
await deleteCredentials();
console.log('\u2713 Logged out');
}
export async function runStatus(json) {
const creds = await loadCredentials();
if (!creds) {
if (json) {
console.log(JSON.stringify({ loggedIn: false }));
}
else {
console.log('Not logged in');
}
process.exit(1); // eslint-disable-line unicorn/no-process-exit
}
try {
const client = new ListenHubClient({ accessToken: creds.accessToken });
const user = await client.getCurrentUser();
const expiresAt = new Date(creds.expiresAt).toISOString();
if (json) {
console.log(JSON.stringify({
loggedIn: true,
user: user.nickname,
email: user.email,
expiresAt,
}, null, 2));
}
else {
console.log(`\u2713 Logged in as ${user.nickname || 'user'}`);
console.log(` Email: ${user.email}`);
console.log(` Expires at: ${expiresAt}`);
}
}
catch {
if (json) {
console.log(JSON.stringify({ loggedIn: false, error: 'Token expired or invalid' }));
}
else {
console.log('Not logged in (token expired or invalid)');
}
process.exit(1); // eslint-disable-line unicorn/no-process-exit
}
}
import http from 'node:http';
const loginTimeoutMs = 5 * 60 * 1000; // 5 minutes
export async function startCallbackServer() {
let resolveCode;
let rejectCode;
const codePromise = new Promise((resolve, reject) => {
resolveCode = resolve;
rejectCode = reject;
});
const timeout = setTimeout(() => {
rejectCode(new Error('Login timed out after 5 minutes. Please try again.'));
}, loginTimeoutMs);
const server = http.createServer((request, response) => {
const url = new URL(request.url, 'http://localhost');
const code = url.searchParams.get('code');
const error = url.searchParams.get('error');
if (error) {
const description = url.searchParams.get('error_description') ?? error;
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end(`Login failed: ${description}`);
clearTimeout(timeout);
rejectCode(new Error(`OAuth error: ${description}`));
return;
}
if (code) {
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end('<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>');
clearTimeout(timeout);
resolveCode({ code });
}
else {
response.writeHead(400, { 'Content-Type': 'text/plain' });
response.end('Missing code parameter');
}
});
await new Promise((resolve) => {
server.listen(0, '127.0.0.1', resolve);
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- server.listen on '127.0.0.1' always returns AddressInfo
const { port } = server.address();
return {
port,
waitForCode: async () => codePromise,
close() {
clearTimeout(timeout);
server.close();
},
};
}
#!/usr/bin/env node
import { Command } from 'commander';
import { register as registerAuth } from './auth/_cli.js';
import { register as registerCreation } from './creation/_cli.js';
import { register as registerExplainer } from './explainer/_cli.js';
import { register as registerImage } from './image/_cli.js';
import { register as registerLyrics } from './lyrics/_cli.js';
import { register as registerMusic } from './music/_cli.js';
import { register as registerPodcast } from './podcast/_cli.js';
import { register as registerSlides } from './slides/_cli.js';
import { register as registerSpeakers } from './speakers/_cli.js';
import { register as registerTts } from './tts/_cli.js';
const program = new Command();
program.name('listenhub').description('ListenHub CLI').version('0.1.0');
registerAuth(program);
registerPodcast(program);
registerTts(program);
registerExplainer(program);
registerSlides(program);
registerImage(program);
registerMusic(program);
registerLyrics(program);
registerSpeakers(program);
registerCreation(program);
program.parse();
import { getClient } from '../_shared/client.js';
import { handleError } from '../_shared/output.js';
import { deleteCreations, getCreation } from './creation.js';
export function register(program) {
const cmd = program.command('creation').description('Manage creations');
cmd
.command('get <id>')
.description('Get creation details')
.option('-j, --json', 'Output JSON', false)
.action(async (id, options) => {
try {
const client = await getClient();
await getCreation(client, id, options.json);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('delete <id...>')
.description('Delete one or more creations')
.option('-j, --json', 'Output JSON', false)
.action(async (ids, options) => {
try {
const client = await getClient();
await deleteCreations(client, ids, options.json);
}
catch (error) {
handleError(error, options.json);
}
});
}
import { printDetail, printJson } from '../_shared/output.js';
export async function getCreation(client, episodeId, json) {
const detail = await client.getCreation(episodeId);
if (json) {
printJson(detail);
return;
}
printDetail('Creation details', [
['ID:', detail.id],
['Type:', detail.generationType],
['Status:', detail.processStatus],
['Language:', detail.language],
['Created:', new Date(detail.createdAt).toISOString()],
]);
}
export async function deleteCreations(client, ids, json) {
await client.deleteCreations({ ids });
if (json) {
printJson({ deleted: ids });
}
else {
console.log(`\u2713 Deleted ${String(ids.length)} creation(s)`);
}
}
import { getClient } from '../_shared/client.js';
import { handleError } from '../_shared/output.js';
import { createExplainer, listExplainerVideos, } from './explainer.js';
function collect(value, previous) {
return [...previous, value];
}
export function register(program) {
const cmd = program
.command('explainer')
.description('Explainer video generation');
cmd
.command('create')
.description('Create an explainer video')
.option('--query <text>', 'Topic text')
.option('--source-url <url>', 'Reference URL (repeatable)', collect, [])
.option('--source-text <text>', 'Reference text (repeatable)', collect, [])
.option('--mode <mode>', 'Generation mode: info, story', 'info')
.option('--lang <lang>', 'Language: en, zh, ja (auto-detected if omitted)')
.option('--speaker <name>', 'Speaker name')
.option('--speaker-id <id>', 'Speaker inner ID')
.option('--skip-audio', 'Skip audio generation', false)
.option('--image-size <size>', 'Image size: 2K, 4K', '2K')
.option('--aspect-ratio <ratio>', 'Aspect ratio: 16:9, 9:16, 1:1', '16:9')
.option('--style <style>', 'Visual style')
.option('--no-wait', 'Return immediately without polling')
.option('--timeout <seconds>', 'Polling timeout', Number, 300)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await createExplainer(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('list')
.description('List explainer videos')
.option('--page <n>', 'Page number', Number, 1)
.option('--page-size <n>', 'Items per page', Number, 20)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await listExplainerVideos(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
}
import { inferLanguage } from '../_shared/language.js';
import { printDetail, printJson, printTable } from '../_shared/output.js';
import { pollUntilDone } from '../_shared/polling.js';
import { buildSources } from '../_shared/sources.js';
import { resolveSpeakers } from '../_shared/speaker-resolver.js';
export async function createExplainer(client, options) {
const lang = options.lang ?? inferLanguage(options.query);
const speakers = await resolveSpeakers(client, {
speakerNames: options.speaker ? [options.speaker] : undefined,
speakerIds: options.speakerId ? [options.speakerId] : undefined,
language: lang,
count: 1,
});
const size = options.imageSize;
const { aspectRatio } = options;
const { episodeId } = await client.createExplainerVideo({
query: options.query,
sources: buildSources(options.sourceUrl, options.sourceText),
style: options.style,
skipAudio: options.skipAudio,
imageConfig: { size, aspectRatio },
template: {
type: 'storybook',
mode: options.mode,
speakers,
language: lang,
style: options.style,
size,
aspectRatio,
},
});
if (!options.wait) {
if (options.json) {
printJson({ episodeId });
}
else {
console.log(`\u2713 Explainer submitted: ${episodeId}`);
}
return;
}
const detail = await pollUntilDone(client, episodeId, {
timeout: options.timeout,
label: 'Creating explainer',
json: options.json,
});
if (options.json) {
printJson(detail);
}
else {
printDetail('Explainer created', [
['ID:', detail.id],
['Title:', detail.topicDetail.title.data],
['Status:', detail.processStatus],
]);
}
}
export async function listExplainerVideos(client, options) {
const { items } = await client.listExplainerVideos({
page: options.page,
pageSize: options.pageSize,
});
if (options.json) {
printJson(items);
return;
}
const headers = ['ID', 'Title', 'Status', 'Created'];
const rows = items.map((episode) => [
episode.id,
episode.title,
episode.processStatus,
new Date(episode.createdAt).toISOString().slice(0, 10),
]);
printTable(headers, rows);
}
import { Option } from 'commander';
import { getClient } from '../_shared/client.js';
import { handleError } from '../_shared/output.js';
import { createImage, getImage, listImages, } from './image.js';
function collect(value, previous) {
return [...previous, value];
}
export function register(program) {
const cmd = program.command('image').description('AI image generation');
cmd
.command('create')
.description('Create an AI image')
.requiredOption('--prompt <text>', 'Image description')
.option('--model <model>', 'Model name')
.option('--lang <lang>', 'Prompt language hint')
.option('--aspect-ratio <ratio>', 'Aspect ratio', '1:1')
.option('--size <size>', 'Image size: 1K, 2K, 4K', '2K')
.option('--reference <path-or-url>', 'Reference image, local file or URL (repeatable, max 5)', collect, [])
.option('--no-wait', 'Return immediately without polling')
.option('--timeout <seconds>', 'Polling timeout', Number, 120)
.option('-j, --json', 'Output JSON', false)
.addOption(new Option('--reference-url <url>', '')
.hideHelp()
.argParser((value, previous) => [...previous, value])
.default([]))
.action(async (options) => {
try {
const client = await getClient();
await createImage(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('list')
.description('List AI images')
.option('--page <n>', 'Page number', Number, 1)
.option('--page-size <n>', 'Items per page', Number, 20)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await listImages(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('get <id>')
.description('Get image details')
.option('-j, --json', 'Output JSON', false)
.action(async (id, options) => {
try {
const client = await getClient();
await getImage(client, id, options.json);
}
catch (error) {
handleError(error, options.json);
}
});
}
import { printDetail, printJson, printTable } from '../_shared/output.js';
import { pollImageUntilDone } from '../_shared/polling.js';
import { resolveFileOrUrl } from '../_shared/upload.js';
export async function createImage(client, options) {
const allReferences = [...options.reference, ...options.referenceUrl];
if (allReferences.length > 5) {
throw new Error('Too many reference images (max 5)');
}
const referenceImageUrls = allReferences.length > 0
? await Promise.all(allReferences.map(async (ref) => resolveFileOrUrl(client, ref, { accept: 'image' })))
: undefined;
const { imageId } = await client.createAIImage({
prompt: options.prompt,
...(options.model && { model: options.model }),
...(options.lang && { language: options.lang }),
aspectRatio: options.aspectRatio,
imageSize: options.size,
...(referenceImageUrls && { referenceImageUrls }),
});
if (!options.wait) {
if (options.json) {
printJson({ imageId });
}
else {
console.log(`\u2713 Image submitted: ${imageId}`);
}
return;
}
const item = await pollImageUntilDone(client, imageId, {
timeout: options.timeout,
json: options.json,
});
if (options.json) {
printJson(item);
}
else {
printDetail('Image created', [
['ID:', item.id],
['URL:', item.imageUrl],
['Status:', item.status],
]);
}
}
export async function listImages(client, options) {
const { items } = await client.listAIImages({
page: options.page,
pageSize: options.pageSize,
});
if (options.json) {
printJson(items);
return;
}
const headers = ['ID', 'Prompt', 'Status', 'Created'];
const rows = items.map((image) => [
image.id,
image.prompt.slice(0, 40),
image.status,
new Date(image.createdAt).toISOString().slice(0, 10),
]);
printTable(headers, rows);
}
export async function getImage(client, imageId, json) {
const item = await client.getAIImage(imageId);
if (json) {
printJson(item);
return;
}
printDetail('Image details', [
['ID:', item.id],
['Prompt:', item.prompt],
['URL:', item.imageUrl],
['Size:', item.imageSize],
['Ratio:', item.aspectRatio],
['Status:', item.status],
['Created:', new Date(item.createdAt).toISOString()],
]);
}
import { getClient } from '../_shared/client.js';
import { handleError } from '../_shared/output.js';
import { createGenerate, getTask, listTasks, } from './lyrics.js';
export function register(program) {
const cmd = program.command('lyrics').description('Lyrics generation');
cmd
.command('generate')
.description('Generate lyrics from a text prompt')
.requiredOption('--prompt <text>', 'Lyrics description (max 200 chars)')
.option('--no-wait', 'Return immediately without polling')
.option('--timeout <seconds>', 'Polling timeout', Number, 120)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await createGenerate(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('list')
.description('List lyrics tasks')
.option('--page <n>', 'Page number', Number, 1)
.option('--page-size <n>', 'Items per page', Number, 20)
.option('--status <status>', 'Filter by status (pending, generating, success, failed)')
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await listTasks(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('get <taskId>')
.description('Get lyrics task details')
.option('-j, --json', 'Output JSON', false)
.action(async (taskId, options) => {
try {
const client = await getClient();
await getTask(client, taskId, options.json);
}
catch (error) {
handleError(error, options.json);
}
});
}
import { printJson, printTable } from '../_shared/output.js';
import { pollLyricsTaskUntilDone } from '../_shared/polling.js';
// --- Helpers ---
function formatDateTime(timestamp) {
const d = new Date(timestamp);
return `${d.toLocaleDateString('sv-SE')} ${d.toLocaleTimeString('en-GB', { hour12: false })}`;
}
function formatDate(timestamp) {
return new Date(timestamp).toLocaleDateString('sv-SE');
}
function printLyricsDetail(task) {
if (task.status === 'failed') {
const rows = [
['Task ID:', task.id],
['Status:', task.status],
['Error:', task.errorMessage],
['Created:', formatDateTime(task.createdAt)],
];
console.log(`\u2717 Lyrics task\n`);
for (const [key, value] of rows) {
if (value !== undefined) {
console.log(` ${key.padEnd(10)} ${String(value)}`);
}
}
return;
}
console.log(`\u2713 Lyrics generated (${task.variants.length} variants)\n`);
for (const [i, variant] of task.variants.entries()) {
console.log(` \u2500\u2500 Variant ${String(i + 1)} \u2500\u2500`);
console.log(` Title: ${variant.title}\n`);
for (const line of variant.text.split('\n')) {
console.log(` ${line}`);
}
console.log();
}
}
// --- Commands ---
export async function createGenerate(client, options) {
if (!options.prompt.trim()) {
throw new Error('Prompt is required');
}
const result = await client.createLyrics({
prompt: options.prompt,
});
if (!options.wait) {
if (options.json) {
printJson(result);
}
else {
console.log(`\u2713 Lyrics task submitted: ${result.taskId}`);
}
return;
}
const task = await pollLyricsTaskUntilDone(client, result.taskId, {
timeout: options.timeout,
json: options.json,
});
if (options.json) {
printJson(task);
}
else {
printLyricsDetail(task);
}
}
export async function listTasks(client, options) {
const response = await client.listLyricsTasks({
page: options.page,
pageSize: options.pageSize,
...(options.status && { status: options.status }),
});
if (options.json) {
printJson(response);
return;
}
const headers = ['ID', 'Status', 'Prompt', 'Variants', 'Created'];
const rows = response.items.map((task) => [
task.id,
task.status,
task.params.prompt.length > 30
? task.params.prompt.slice(0, 30) + '\u2026'
: task.params.prompt,
String(task.variants.length),
formatDate(task.createdAt),
]);
printTable(headers, rows);
}
export async function getTask(client, taskId, json) {
const task = await client.getLyricsTask(taskId);
if (json) {
printJson(task);
return;
}
printLyricsDetail(task);
}
import { getClient } from '../_shared/client.js';
import { handleError } from '../_shared/output.js';
import { createCover, createExtend, createGenerate, getTask, listTasks, } from './music.js';
export function register(program) {
const cmd = program.command('music').description('Music generation');
cmd
.command('generate')
.description('Generate music from a text prompt')
.requiredOption('--prompt <text>', 'Music description')
.option('--style <text>', 'Music style/mood')
.option('--title <text>', 'Track title')
.option('--instrumental', 'Instrumental only, no vocals', false)
.option('--no-wait', 'Return immediately without polling')
.option('--timeout <seconds>', 'Polling timeout', Number, 600)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await createGenerate(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('cover')
.description('Create a cover from reference audio')
.requiredOption('--audio <path-or-url>', 'Reference audio file or URL')
.option('--prompt <text>', 'Music description')
.option('--style <text>', 'Music style/mood')
.option('--title <text>', 'Track title')
.option('--instrumental', 'Instrumental only, no vocals', false)
.option('--no-wait', 'Return immediately without polling')
.option('--timeout <seconds>', 'Polling timeout', Number, 600)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await createCover(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('extend')
.description('Extend music from reference audio')
.requiredOption('--audio <path-or-url>', 'Reference audio file or URL')
.requiredOption('--model <version>', 'Model version (V4, V4_5, V4_5PLUS, V4_5ALL, V5, V5_5)')
.requiredOption('--continue-at <seconds>', 'Start extending from this time point', Number)
.option('--prompt <text>', 'Lyrics or description')
.option('--style <text>', 'Music style/mood')
.option('--title <text>', 'Track title')
.option('--instrumental', 'Instrumental only, no vocals', false)
.option('--negative-tags <text>', 'Styles to exclude')
.option('--vocal-gender <gender>', 'Vocal gender (m or f)')
.option('--style-weight <weight>', 'Style guidance weight (0-1)', Number)
.option('--weirdness <weight>', 'Creativity/weirdness constraint (0-1)', Number)
.option('--audio-weight <weight>', 'Input audio influence weight (0-1)', Number)
.option('--no-wait', 'Return immediately without polling')
.option('--timeout <seconds>', 'Polling timeout', Number, 600)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await createExtend(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('list')
.description('List music tasks')
.option('--page <n>', 'Page number', Number, 1)
.option('--page-size <n>', 'Items per page', Number, 20)
.option('--status <status>', 'Filter by status (pending, generating, uploading, success, failed)')
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await listTasks(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('get <taskId>')
.description('Get music task details')
.option('-j, --json', 'Output JSON', false)
.action(async (taskId, options) => {
try {
const client = await getClient();
await getTask(client, taskId, options.json);
}
catch (error) {
handleError(error, options.json);
}
});
}
import { printDetail, printJson, printTable } from '../_shared/output.js';
import { pollMusicTaskUntilDone } from '../_shared/polling.js';
import { resolveFileOrUrl } from '../_shared/upload.js';
// --- Helpers ---
function formatDuration(seconds) {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${String(m)}:${String(s).padStart(2, '0')}`;
}
function formatDate(timestamp) {
return new Date(timestamp).toLocaleDateString('sv-SE'); // YYYY-MM-DD
}
function formatDateTime(timestamp) {
const d = new Date(timestamp);
return `${d.toLocaleDateString('sv-SE')} ${d.toLocaleTimeString('en-GB', { hour12: false })}`;
}
function printMusicDetail(task) {
const rows = [
['Task ID:', task.id],
['Type:', task.taskType.toLowerCase()],
['Status:', task.status],
];
if (task.status === 'failed') {
rows.push(['Error:', task.errorMessage]);
}
else {
const trackTitle = task.tracks[0]?.title ?? task.params.title;
if (trackTitle) {
rows.push(['Title:', trackTitle]);
}
rows.push(['Tracks:', task.tracks.length]);
for (const [i, track] of task.tracks.entries()) {
rows.push([
`Track ${String(i + 1)}:`,
`${track.audioUrl} (${formatDuration(track.duration)})`,
]);
}
}
rows.push(['Created:', formatDateTime(task.createdAt)]);
if (task.status === 'failed') {
console.log(`\u2717 Music task\n`);
for (const [key, value] of rows) {
if (value !== undefined) {
console.log(` ${key.padEnd(10)} ${String(value)}`);
}
}
}
else {
printDetail('Music task', rows);
}
}
// --- Commands ---
export async function createGenerate(client, options) {
if (!options.prompt.trim()) {
throw new Error('Prompt is required');
}
const result = await client.createMusicGenerate({
prompt: options.prompt,
...(options.style && { style: options.style }),
...(options.title && { title: options.title }),
...(options.instrumental && { instrumental: true }),
});
if (!options.wait) {
if (options.json) {
printJson(result);
}
else {
console.log(`\u2713 Music task submitted: ${result.taskId}`);
}
return;
}
const task = await pollMusicTaskUntilDone(client, result.taskId, {
timeout: options.timeout,
json: options.json,
});
if (options.json) {
printJson(task);
}
else {
printMusicDetail(task);
}
}
export async function createCover(client, options) {
const uploadUrl = await resolveFileOrUrl(client, options.audio, {
accept: 'audio',
});
const result = await client.createMusicCover({
uploadUrl,
...(options.prompt && { prompt: options.prompt }),
...(options.style && { style: options.style }),
...(options.title && { title: options.title }),
...(options.instrumental && { instrumental: true }),
});
if (!options.wait) {
if (options.json) {
printJson(result);
}
else {
console.log(`\u2713 Music task submitted: ${result.taskId}`);
}
return;
}
const task = await pollMusicTaskUntilDone(client, result.taskId, {
timeout: options.timeout,
json: options.json,
});
if (options.json) {
printJson(task);
}
else {
printMusicDetail(task);
}
}
export async function createExtend(client, options) {
const uploadUrl = await resolveFileOrUrl(client, options.audio, {
accept: 'audio',
});
const parameters = {
uploadUrl,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- CLI string validated by Commander choices
model: options.model,
continueAt: options.continueAt,
...(options.prompt && { prompt: options.prompt }),
...(options.style && { style: options.style }),
...(options.title && { title: options.title }),
...(options.instrumental && { instrumental: true }),
...(options.negativeTags && { negativeTags: options.negativeTags }),
...(options.vocalGender && {
vocalGender:
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- CLI string validated by Commander choices
options.vocalGender,
}),
...(options.styleWeight !== undefined && {
styleWeight: options.styleWeight,
}),
...(options.weirdness !== undefined && {
weirdnessConstraint: options.weirdness,
}),
...(options.audioWeight !== undefined && {
audioWeight: options.audioWeight,
}),
};
const result = await client.createMusicExtend(parameters);
if (!options.wait) {
if (options.json) {
printJson(result);
}
else {
console.log(`\u2713 Music task submitted: ${result.taskId}`);
}
return;
}
const task = await pollMusicTaskUntilDone(client, result.taskId, {
timeout: options.timeout,
json: options.json,
});
if (options.json) {
printJson(task);
}
else {
printMusicDetail(task);
}
}
export async function listTasks(client, options) {
const { items } = await client.listMusicTasks({
page: options.page,
pageSize: options.pageSize,
...(options.status && { status: options.status }),
});
if (options.json) {
printJson({ items });
return;
}
const headers = ['ID', 'Type', 'Status', 'Title', 'Tracks', 'Created'];
const rows = items.map((task) => [
task.id,
task.taskType.toLowerCase(),
task.status,
task.tracks[0]?.title ?? task.params.title ?? '\u2014',
String(task.tracks.length),
formatDate(task.createdAt),
]);
printTable(headers, rows);
}
export async function getTask(client, taskId, json) {
const task = await client.getMusicTask(taskId);
if (json) {
printJson(task);
return;
}
printMusicDetail(task);
}
import { getClient } from '../_shared/client.js';
import { handleError } from '../_shared/output.js';
import { createPodcast, listPodcasts, } from './podcast.js';
function collect(value, previous) {
return [...previous, value];
}
export function register(program) {
const cmd = program.command('podcast').description('Podcast generation');
cmd
.command('create')
.description('Create a podcast episode')
.option('--query <text>', 'Topic text')
.option('--source-url <url>', 'Reference URL (repeatable)', collect, [])
.option('--source-text <text>', 'Reference text (repeatable)', collect, [])
.option('--mode <mode>', 'Generation mode: quick, deep, debate', 'quick')
.option('--lang <lang>', 'Language: en, zh, ja (auto-detected if omitted)')
.option('--speaker <name>', 'Speaker name (repeatable)', collect, [])
.option('--speaker-id <id>', 'Speaker inner ID (repeatable)', collect, [])
.option('--no-wait', 'Return immediately without polling')
.option('--timeout <seconds>', 'Polling timeout', Number, 300)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await createPodcast(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('list')
.description('List podcast episodes')
.option('--page <n>', 'Page number', Number, 1)
.option('--page-size <n>', 'Items per page', Number, 20)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await listPodcasts(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
}
import { inferLanguage } from '../_shared/language.js';
import { printDetail, printJson, printTable } from '../_shared/output.js';
import { pollUntilDone } from '../_shared/polling.js';
import { buildSources } from '../_shared/sources.js';
import { resolveSpeakers } from '../_shared/speaker-resolver.js';
export async function createPodcast(client, options) {
const lang = options.lang ?? inferLanguage(options.query);
const speakers = await resolveSpeakers(client, {
speakerNames: options.speaker,
speakerIds: options.speakerId,
language: lang,
});
const { episodeId } = await client.createPodcast({
type: speakers.length <= 1 ? 'podcast-solo' : 'podcast-duo',
query: options.query,
sources: buildSources(options.sourceUrl, options.sourceText),
template: {
type: 'podcast',
mode: options.mode,
speakers,
language: lang,
},
});
if (!options.wait) {
if (options.json) {
printJson({ episodeId });
}
else {
console.log(`\u2713 Podcast submitted: ${episodeId}`);
}
return;
}
const detail = await pollUntilDone(client, episodeId, {
timeout: options.timeout,
label: 'Creating podcast',
json: options.json,
});
if (options.json) {
printJson(detail);
}
else {
printDetail('Podcast created', [
['ID:', detail.id],
['Title:', detail.topicDetail.title.data],
['Status:', detail.processStatus],
]);
}
}
export async function listPodcasts(client, options) {
const { items } = await client.listPodcasts({
page: options.page,
pageSize: options.pageSize,
});
if (options.json) {
printJson(items);
return;
}
const headers = ['ID', 'Title', 'Status', 'Created'];
const rows = items.map((episode) => [
episode.id,
episode.title,
episode.processStatus,
new Date(episode.createdAt).toISOString().slice(0, 10),
]);
printTable(headers, rows);
}
import { getClient } from '../_shared/client.js';
import { handleError } from '../_shared/output.js';
import { createSlides, listSlides, } from './slides.js';
function collect(value, previous) {
return [...previous, value];
}
export function register(program) {
const cmd = program.command('slides').description('Slides generation');
cmd
.command('create')
.description('Create a slide deck')
.option('--query <text>', 'Topic text')
.option('--source-url <url>', 'Reference URL (repeatable)', collect, [])
.option('--source-text <text>', 'Reference text (repeatable)', collect, [])
.option('--lang <lang>', 'Language: en, zh, ja (auto-detected if omitted)')
.option('--speaker <name>', 'Speaker name')
.option('--speaker-id <id>', 'Speaker inner ID')
.option('--no-skip-audio', 'Generate audio narration')
.option('--image-size <size>', 'Image size: 2K, 4K', '2K')
.option('--aspect-ratio <ratio>', 'Aspect ratio: 16:9, 9:16, 1:1', '16:9')
.option('--style <style>', 'Visual style')
.option('--no-wait', 'Return immediately without polling')
.option('--timeout <seconds>', 'Polling timeout', Number, 300)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await createSlides(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('list')
.description('List slide decks')
.option('--page <n>', 'Page number', Number, 1)
.option('--page-size <n>', 'Items per page', Number, 20)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await listSlides(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
}
import { inferLanguage } from '../_shared/language.js';
import { printDetail, printJson, printTable } from '../_shared/output.js';
import { pollUntilDone } from '../_shared/polling.js';
import { buildSources } from '../_shared/sources.js';
import { resolveSpeakers } from '../_shared/speaker-resolver.js';
export async function createSlides(client, options) {
const lang = options.lang ?? inferLanguage(options.query);
const speakers = await resolveSpeakers(client, {
speakerNames: options.speaker ? [options.speaker] : undefined,
speakerIds: options.speakerId ? [options.speakerId] : undefined,
language: lang,
count: 1,
});
const size = options.imageSize;
const { aspectRatio } = options;
const { episodeId } = await client.createSlides({
query: options.query,
sources: buildSources(options.sourceUrl, options.sourceText),
style: options.style,
skipAudio: options.skipAudio,
imageConfig: { size, aspectRatio },
template: {
type: 'storybook',
mode: 'slides',
speakers,
language: lang,
style: options.style,
size,
aspectRatio,
},
});
if (!options.wait) {
if (options.json) {
printJson({ episodeId });
}
else {
console.log(`\u2713 Slides submitted: ${episodeId}`);
}
return;
}
const detail = await pollUntilDone(client, episodeId, {
timeout: options.timeout,
label: 'Creating slides',
json: options.json,
});
if (options.json) {
printJson(detail);
}
else {
printDetail('Slides created', [
['ID:', detail.id],
['Title:', detail.topicDetail.title.data],
['Status:', detail.processStatus],
]);
}
}
export async function listSlides(client, options) {
const { items } = await client.listSlides({
page: options.page,
pageSize: options.pageSize,
});
if (options.json) {
printJson(items);
return;
}
const headers = ['ID', 'Title', 'Status', 'Created'];
const rows = items.map((episode) => [
episode.id,
episode.title,
episode.processStatus,
new Date(episode.createdAt).toISOString().slice(0, 10),
]);
printTable(headers, rows);
}
import { getClient } from '../_shared/client.js';
import { handleError } from '../_shared/output.js';
import { listSpeakers } from './speakers.js';
export function register(program) {
const cmd = program.command('speakers').description('Manage speakers');
cmd
.command('list')
.description('List available speakers')
.option('--lang <lang>', 'Filter by language (en, zh, ja)')
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await listSpeakers(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
}
import { printJson, printTable } from '../_shared/output.js';
export async function listSpeakers(client, options) {
const { items } = await client.listSpeakers({
language: options.lang,
});
if (options.json) {
printJson(items);
return;
}
const headers = ['Name', 'ID', 'Gender', 'Personality'];
const rows = items.map((s) => [
s.name,
s.speakerInnerId,
s.gender,
s.personality,
]);
printTable(headers, rows);
}
import { getClient } from '../_shared/client.js';
import { handleError } from '../_shared/output.js';
import { createTts, listTts, } from './tts.js';
export function register(program) {
const cmd = program.command('tts').description('Text-to-speech generation');
cmd
.command('create')
.description('Create a TTS audio')
.option('--text <text>', 'Text to convert to speech')
.option('--source-url <url>', 'Reference URL (repeatable)', collect, [])
.option('--source-text <text>', 'Reference text (repeatable)', collect, [])
.option('--mode <mode>', 'Generation mode: smart, direct', 'smart')
.option('--lang <lang>', 'Language: en, zh, ja (auto-detected if omitted)')
.option('--speaker <name>', 'Speaker name')
.option('--speaker-id <id>', 'Speaker inner ID')
.option('--no-wait', 'Return immediately without polling')
.option('--timeout <seconds>', 'Polling timeout', Number, 300)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await createTts(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
cmd
.command('list')
.description('List TTS episodes')
.option('--page <n>', 'Page number', Number, 1)
.option('--page-size <n>', 'Items per page', Number, 20)
.option('-j, --json', 'Output JSON', false)
.action(async (options) => {
try {
const client = await getClient();
await listTts(client, options);
}
catch (error) {
handleError(error, options.json);
}
});
}
function collect(value, previous) {
return [...previous, value];
}
import { inferLanguage } from '../_shared/language.js';
import { printDetail, printJson, printTable } from '../_shared/output.js';
import { pollUntilDone } from '../_shared/polling.js';
import { buildSources } from '../_shared/sources.js';
import { resolveSpeakers } from '../_shared/speaker-resolver.js';
export async function createTts(client, options) {
const lang = options.lang ?? inferLanguage(options.text);
const speakers = await resolveSpeakers(client, {
speakerNames: options.speaker ? [options.speaker] : undefined,
speakerIds: options.speakerId ? [options.speakerId] : undefined,
language: lang,
count: 1,
});
const sources = options.text
? [{ type: 'text', content: options.text }]
: buildSources(options.sourceUrl, options.sourceText);
const { episodeId } = await client.createTTS({
sources,
template: {
type: 'flowspeech',
mode: options.mode,
speakers,
language: lang,
},
});
if (!options.wait) {
if (options.json) {
printJson({ episodeId });
}
else {
console.log(`\u2713 TTS submitted: ${episodeId}`);
}
return;
}
const detail = await pollUntilDone(client, episodeId, {
timeout: options.timeout,
label: 'Creating TTS',
json: options.json,
});
if (options.json) {
printJson(detail);
}
else {
printDetail('TTS created', [
['ID:', detail.id],
['Title:', detail.topicDetail.title.data],
['Status:', detail.processStatus],
]);
}
}
export async function listTts(client, options) {
const { items } = await client.listTTS({
page: options.page,
pageSize: options.pageSize,
});
if (options.json) {
printJson(items);
return;
}
const headers = ['ID', 'Title', 'Status', 'Created'];
const rows = items.map((episode) => [
episode.id,
episode.title,
episode.processStatus,
new Date(episode.createdAt).toISOString().slice(0, 10),
]);
printTable(headers, rows);
}