faceless-cli 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client.mjs +96 -103
- package/src/config.mjs +29 -29
- package/src/generated/operations.json +1 -1
- package/src/index.mjs +521 -633
- package/src/mcp/stdio.mjs +76 -85
- package/src/oauth.mjs +181 -0
- package/src/output.mjs +83 -86
package/src/index.mjs
CHANGED
|
@@ -8,773 +8,661 @@ import { print, printError } from "./output.mjs";
|
|
|
8
8
|
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
9
9
|
let spec;
|
|
10
10
|
try {
|
|
11
|
-
|
|
12
|
-
fs.readFileSync(new URL("./generated/operations.json", import.meta.url), "utf8")
|
|
13
|
-
);
|
|
11
|
+
spec = JSON.parse(fs.readFileSync(new URL("./generated/operations.json", import.meta.url), "utf8"));
|
|
14
12
|
} catch {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
18
|
-
process.exit(1);
|
|
13
|
+
process.stderr.write('cli/src/generated/operations.json is missing. Run "npm run generate:agents" in the repo root to generate it.\n');
|
|
14
|
+
process.exit(1);
|
|
19
15
|
}
|
|
20
16
|
const operations = spec.operations;
|
|
21
17
|
|
|
22
18
|
function getOp(operationId) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
const op = operations.find((o) => o.operationId === operationId);
|
|
20
|
+
if (!op) {
|
|
21
|
+
throw new CliError("internal_error", `Unknown operation: ${operationId}`);
|
|
22
|
+
}
|
|
23
|
+
return op;
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
function toInt(value) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
const n = parseInt(value, 10);
|
|
28
|
+
if (Number.isNaN(n)) throw new CliError("invalid_input", `Not a number: ${value}`);
|
|
29
|
+
return n;
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
function splitList(value) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
return value
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((v) => v.trim())
|
|
36
|
+
.filter(Boolean);
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
function compact(obj) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
42
|
+
if (value !== undefined && value !== null) out[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
const MODELS = ["storyboard", "motion_lite", "motion_pro"];
|
|
52
48
|
const PLATFORMS = ["youtube", "tiktok", "instagram", "x", "facebook", "linkedin", "threads"];
|
|
53
|
-
const OPTION_KINDS = [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const value = pathParams[name];
|
|
75
|
-
if (value === undefined) {
|
|
76
|
-
throw new CliError("invalid_input", `Missing required parameter: ${name}`);
|
|
77
|
-
}
|
|
78
|
-
return encodeURIComponent(String(value));
|
|
79
|
-
});
|
|
80
|
-
return request({
|
|
81
|
-
method: op.method,
|
|
82
|
-
path,
|
|
83
|
-
query: compact(query),
|
|
84
|
-
body: body === undefined ? undefined : compact(body),
|
|
85
|
-
apiKey: resolveApiKey(globals),
|
|
86
|
-
baseUrl: resolveBaseUrl(globals),
|
|
87
|
-
idempotencyKey,
|
|
88
|
-
});
|
|
49
|
+
const OPTION_KINDS = ["sources", "styles", "niches", "languages", "durations", "models", "captionThemes", "music", "backgrounds", "subreddits"];
|
|
50
|
+
|
|
51
|
+
async function execute(operationId, { pathParams = {}, query = {}, body, idempotencyKey } = {}, cmd) {
|
|
52
|
+
const globals = cmd.optsWithGlobals();
|
|
53
|
+
const op = getOp(operationId);
|
|
54
|
+
const path = op.path.replace(/\{(\w+)\}/g, (_, name) => {
|
|
55
|
+
const value = pathParams[name];
|
|
56
|
+
if (value === undefined) {
|
|
57
|
+
throw new CliError("invalid_input", `Missing required parameter: ${name}`);
|
|
58
|
+
}
|
|
59
|
+
return encodeURIComponent(String(value));
|
|
60
|
+
});
|
|
61
|
+
return request({
|
|
62
|
+
method: op.method,
|
|
63
|
+
path,
|
|
64
|
+
query: compact(query),
|
|
65
|
+
body: body === undefined ? undefined : compact(body),
|
|
66
|
+
apiKey: resolveApiKey(globals),
|
|
67
|
+
baseUrl: resolveBaseUrl(globals),
|
|
68
|
+
idempotencyKey,
|
|
69
|
+
});
|
|
89
70
|
}
|
|
90
71
|
|
|
91
72
|
async function waitForTerminal({ operationId, label, id, timeoutSec, cmd }) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
await sleep(5000);
|
|
107
|
-
}
|
|
73
|
+
const terminal = getOp(operationId).terminalStates || [];
|
|
74
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
75
|
+
for (;;) {
|
|
76
|
+
const result = await execute(operationId, { pathParams: { id } }, cmd);
|
|
77
|
+
const data = (result && result.data) || {};
|
|
78
|
+
if (terminal.includes(data.status)) {
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
if (Date.now() >= deadline) {
|
|
82
|
+
throw new CliError("timeout", `Timed out after ${timeoutSec}s waiting for ${label} ${id} (last status: ${data.status || "unknown"})`);
|
|
83
|
+
}
|
|
84
|
+
await sleep(5000);
|
|
85
|
+
}
|
|
108
86
|
}
|
|
109
87
|
|
|
110
88
|
// Polls GET /videos/{id}/status until completed or failed.
|
|
111
89
|
function waitForVideo(id, timeoutSec, cmd) {
|
|
112
|
-
|
|
90
|
+
return waitForTerminal({ operationId: "getVideoStatus", label: "video", id, timeoutSec, cmd });
|
|
113
91
|
}
|
|
114
92
|
|
|
115
93
|
// Polls GET /renders/{id} until done or error.
|
|
116
94
|
function waitForRender(id, timeoutSec, cmd) {
|
|
117
|
-
|
|
95
|
+
return waitForTerminal({ operationId: "getRender", label: "render", id, timeoutSec, cmd });
|
|
118
96
|
}
|
|
119
97
|
|
|
120
98
|
function noteCost(operationId) {
|
|
121
|
-
|
|
122
|
-
|
|
99
|
+
const cost = getOp(operationId).creditCost;
|
|
100
|
+
if (cost && cost !== "none") process.stderr.write(`This costs ${cost}.\n`);
|
|
123
101
|
}
|
|
124
102
|
|
|
125
103
|
// Async create commands share one shape: state the cost, fire the request,
|
|
126
104
|
// then optionally poll the created video's status until it is terminal.
|
|
127
105
|
async function createAndMaybeWait(operationId, body, opts, cmd) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
106
|
+
const globals = cmd.optsWithGlobals();
|
|
107
|
+
noteCost(operationId);
|
|
108
|
+
const created = await execute(operationId, { body, idempotencyKey: opts.idempotencyKey }, cmd);
|
|
109
|
+
const id = created?.data?.id;
|
|
110
|
+
if (!opts.wait || !id) {
|
|
111
|
+
print(created, { json: globals.json });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
print(await waitForVideo(id, opts.timeout, cmd), { json: globals.json });
|
|
137
115
|
}
|
|
138
116
|
|
|
139
117
|
function withWaitFlags(command, what) {
|
|
140
|
-
|
|
141
|
-
.option("--wait", `poll until the ${what} reaches a terminal state`)
|
|
142
|
-
.option("--timeout <sec>", "max seconds to wait with --wait", toInt, 600);
|
|
118
|
+
return command.option("--wait", `poll until the ${what} reaches a terminal state`).option("--timeout <sec>", "max seconds to wait with --wait", toInt, 600);
|
|
143
119
|
}
|
|
144
120
|
|
|
145
121
|
// Series settings shared by "series create" and "series update". Field names
|
|
146
122
|
// mirror the createSeries request schema.
|
|
147
123
|
function withSeriesFlags(command) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"background gameplay/footage id (see: faceless options --kind backgrounds)"
|
|
173
|
-
)
|
|
174
|
-
.option("--random-background-video", "pick a random background video per episode")
|
|
175
|
-
.option("--layout <layout>", "video layout variant")
|
|
176
|
-
.addOption(
|
|
177
|
-
new Option("--broll-model <model>", "generation model for visuals").choices(MODELS)
|
|
178
|
-
)
|
|
179
|
-
.option("--show-emojis", "overlay emojis on captions")
|
|
180
|
-
.option("--enable-background-music", "mix background music under the narration")
|
|
181
|
-
.option(
|
|
182
|
-
"--background-music-mood <mood>",
|
|
183
|
-
"background music mood (see: faceless options --kind music)"
|
|
184
|
-
)
|
|
185
|
-
.option("--hashtags <text>", "hashtags appended to post captions")
|
|
186
|
-
.option("--tone <tone>", "writing tone for generated scripts")
|
|
187
|
-
.addOption(
|
|
188
|
-
new Option("--youtube-privacy <p>", "privacy for auto-posted YouTube videos").choices([
|
|
189
|
-
"public",
|
|
190
|
-
"unlisted",
|
|
191
|
-
"private",
|
|
192
|
-
])
|
|
193
|
-
);
|
|
124
|
+
return command
|
|
125
|
+
.option("--niche <niche>", "content niche, e.g. scary stories (see: faceless options --kind niches)")
|
|
126
|
+
.option("--custom-prompt <text>", "custom topic prompt used instead of (or alongside) a niche")
|
|
127
|
+
.option("--voice <id>", "TTS voice id for narration (see: faceless voices)")
|
|
128
|
+
.option("--style <style>", "visual style for generated imagery (see: faceless options --kind styles)")
|
|
129
|
+
.option("--language <language>", "video language, e.g. English")
|
|
130
|
+
.addOption(new Option("--duration <sec>", "target episode length in seconds").choices(["30", "60", "90"]))
|
|
131
|
+
.option("--destination <platform>", "primary auto-post destination, e.g. youtube or tiktok")
|
|
132
|
+
.option("--destination-accounts <id[,id...]>", "connected account ids to auto-post to (see: faceless accounts)")
|
|
133
|
+
.option("--auto-post-time <HH:mm>", "daily auto-post time in the series timezone")
|
|
134
|
+
.option("--posting-days <days>", "comma-separated days of the week to post, e.g. Monday,Wednesday (omit for every day)")
|
|
135
|
+
.option("--timezone <tz>", "IANA timezone for scheduling, e.g. America/New_York")
|
|
136
|
+
.option("--caption-style <name>", "caption theme name (see: faceless options --kind captionThemes)")
|
|
137
|
+
.option("--subreddit <name>", 'subreddit to pull posts from when source is "Reddit post"')
|
|
138
|
+
.option("--background-video <id>", "background gameplay/footage id (see: faceless options --kind backgrounds)")
|
|
139
|
+
.option("--random-background-video", "pick a random background video per episode")
|
|
140
|
+
.option("--layout <layout>", "video layout variant")
|
|
141
|
+
.addOption(new Option("--broll-model <model>", "generation model for visuals").choices(MODELS))
|
|
142
|
+
.option("--show-emojis", "overlay emojis on captions")
|
|
143
|
+
.option("--enable-background-music", "mix background music under the narration")
|
|
144
|
+
.option("--background-music-mood <mood>", "background music mood (see: faceless options --kind music)")
|
|
145
|
+
.option("--hashtags <text>", "hashtags appended to post captions")
|
|
146
|
+
.option("--tone <tone>", "writing tone for generated scripts")
|
|
147
|
+
.addOption(new Option("--youtube-privacy <p>", "privacy for auto-posted YouTube videos").choices(["public", "unlisted", "private"]));
|
|
194
148
|
}
|
|
195
149
|
|
|
196
150
|
function seriesBody(opts) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
151
|
+
return compact({
|
|
152
|
+
name: opts.name,
|
|
153
|
+
source: opts.source,
|
|
154
|
+
niche: opts.niche,
|
|
155
|
+
customPrompt: opts.customPrompt,
|
|
156
|
+
voice: opts.voice,
|
|
157
|
+
style: opts.style,
|
|
158
|
+
language: opts.language,
|
|
159
|
+
duration: opts.duration,
|
|
160
|
+
destination: opts.destination,
|
|
161
|
+
destinationAccounts: opts.destinationAccounts ? splitList(opts.destinationAccounts) : undefined,
|
|
162
|
+
autoPostTime: opts.autoPostTime,
|
|
163
|
+
postingDays: opts.postingDays ? splitList(opts.postingDays) : undefined,
|
|
164
|
+
timezone: opts.timezone,
|
|
165
|
+
captionStyle: opts.captionStyle,
|
|
166
|
+
subreddit: opts.subreddit,
|
|
167
|
+
backgroundVideo: opts.backgroundVideo,
|
|
168
|
+
useRandomBackgroundVideo: opts.randomBackgroundVideo,
|
|
169
|
+
layout: opts.layout,
|
|
170
|
+
brollModel: opts.brollModel,
|
|
171
|
+
showEmojis: opts.showEmojis,
|
|
172
|
+
enableBackgroundMusic: opts.enableBackgroundMusic,
|
|
173
|
+
backgroundMusicMood: opts.backgroundMusicMood,
|
|
174
|
+
hashtags: opts.hashtags,
|
|
175
|
+
tone: opts.tone,
|
|
176
|
+
youtubePrivacyStatus: opts.youtubePrivacy,
|
|
177
|
+
paused: opts.paused,
|
|
178
|
+
});
|
|
225
179
|
}
|
|
226
180
|
|
|
227
181
|
const program = new Command();
|
|
228
182
|
|
|
229
183
|
program
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
.option("--api-key <key>", "API key (overrides env and config file)")
|
|
237
|
-
.option("--api-url <url>", "API base URL (default https://faceless.so/api/v1)");
|
|
184
|
+
.name("faceless")
|
|
185
|
+
.description("Faceless.so CLI: create AI faceless videos, run automated series and publish to YouTube, TikTok, Instagram and more.")
|
|
186
|
+
.version(pkg.version)
|
|
187
|
+
.option("--json", "output raw JSON")
|
|
188
|
+
.option("--api-key <key>", "API key (overrides env and config file)")
|
|
189
|
+
.option("--api-url <url>", "API base URL (default https://faceless.so/api/v1)");
|
|
238
190
|
|
|
239
191
|
program
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
192
|
+
.command("login")
|
|
193
|
+
.description("Log in via OAuth in your browser (default), or save an API key with --api-key / --manual")
|
|
194
|
+
.option("--manual", "prompt for an API key instead of the browser OAuth flow")
|
|
195
|
+
.action(async (opts, cmd) => {
|
|
196
|
+
const globals = cmd.optsWithGlobals();
|
|
197
|
+
const baseUrl = resolveBaseUrl(globals);
|
|
198
|
+
|
|
199
|
+
// The key path, kept for CI and anyone who prefers long-lived keys.
|
|
200
|
+
let key = globals.apiKey;
|
|
201
|
+
if (!key && opts.manual) {
|
|
202
|
+
const rl = readline.createInterface({
|
|
203
|
+
input: process.stdin,
|
|
204
|
+
output: process.stderr,
|
|
205
|
+
});
|
|
206
|
+
key = (await rl.question("API key (fl_live_...): ")).trim();
|
|
207
|
+
rl.close();
|
|
208
|
+
if (!key) throw new CliError("invalid_input", "No API key provided");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let verifyKey = key;
|
|
212
|
+
if (key) {
|
|
213
|
+
// Exclusive on purpose: exactly one stored credential, so precedence never surprises.
|
|
214
|
+
saveConfig({ apiKey: key, oauth: undefined });
|
|
215
|
+
} else {
|
|
216
|
+
// Default: the browser OAuth flow. This is the path an agent can complete - no key to paste.
|
|
217
|
+
const { loginWithOAuth } = await import("./oauth.mjs");
|
|
218
|
+
const tokens = await loginWithOAuth({ baseUrl, log: (line) => process.stderr.write(line) });
|
|
219
|
+
saveConfig({ oauth: tokens, apiKey: undefined });
|
|
220
|
+
verifyKey = tokens.accessToken;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const result = await request({ method: "get", path: "/me", apiKey: verifyKey, baseUrl });
|
|
224
|
+
if (key) saveConfig({ apiKey: key, oauth: undefined }); // keep only after /me verified
|
|
225
|
+
if (globals.json || !process.stdout.isTTY) {
|
|
226
|
+
print(result, { json: true });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const data = (result && result.data) || {};
|
|
230
|
+
const scopes = data.auth?.scopes || [];
|
|
231
|
+
process.stdout.write(`Logged in as ${data.team?.name || "unknown team"}\n`);
|
|
232
|
+
process.stdout.write(`Scopes: ${scopes.length ? scopes.join(", ") : "(none)"}\n`);
|
|
233
|
+
process.stdout.write(`Saved ${key ? "key" : "OAuth session"} to ${CONFIG_PATH}\n`);
|
|
234
|
+
});
|
|
271
235
|
|
|
272
236
|
program
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
237
|
+
.command("logout")
|
|
238
|
+
.description("Forget the stored credential (revokes the OAuth session server-side when possible)")
|
|
239
|
+
.action(async (_opts, cmd) => {
|
|
240
|
+
const globals = cmd.optsWithGlobals();
|
|
241
|
+
const config = loadConfig();
|
|
242
|
+
if (config.oauth?.refreshToken) {
|
|
243
|
+
// Best-effort revocation: RFC 7009 always answers 200, and a network failure must not stop
|
|
244
|
+
// the local forget - the refresh token is useless without the config file anyway.
|
|
245
|
+
const site = config.oauth.site || resolveBaseUrl(globals).replace(/\/api\/v1$/, "");
|
|
246
|
+
await fetch(`${site}/api/oauth2/revoke`, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: { "Content-Type": "application/json" },
|
|
249
|
+
body: JSON.stringify({ token: config.oauth.refreshToken }),
|
|
250
|
+
}).catch(() => {});
|
|
251
|
+
}
|
|
252
|
+
saveConfig({ apiKey: undefined, oauth: undefined });
|
|
253
|
+
process.stdout.write("Logged out.\n");
|
|
254
|
+
});
|
|
285
255
|
|
|
286
256
|
program
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
257
|
+
.command("whoami")
|
|
258
|
+
.description(getOp("getMe").summary)
|
|
259
|
+
.action(async (_opts, cmd) => {
|
|
260
|
+
const result = await execute("getMe", {}, cmd);
|
|
261
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
262
|
+
});
|
|
293
263
|
|
|
294
264
|
program
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
265
|
+
.command("credits")
|
|
266
|
+
.description(getOp("getCredits").summary)
|
|
267
|
+
.option("--page <n>", "page number", toInt)
|
|
268
|
+
.option("--limit <n>", "items per page (max 100)", toInt)
|
|
269
|
+
.action(async (opts, cmd) => {
|
|
270
|
+
const result = await execute("getCredits", { query: { page: opts.page, limit: opts.limit } }, cmd);
|
|
271
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
272
|
+
});
|
|
303
273
|
|
|
304
274
|
const videos = program.command("videos").description("Faceless video projects");
|
|
305
275
|
|
|
306
276
|
withWaitFlags(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
.option("--enable-background-music", "mix background music under the narration")
|
|
322
|
-
.option("--master-style <text>", "extra style directive applied to every generated scene")
|
|
323
|
-
.option("--global-negative-prompt <text>", "things the image model should avoid in every scene")
|
|
324
|
-
.option("--idempotency-key <k>", "Idempotency-Key header value"),
|
|
325
|
-
"video"
|
|
277
|
+
videos
|
|
278
|
+
.command("create")
|
|
279
|
+
.description(getOp("createVideo").summary)
|
|
280
|
+
.requiredOption("--script <text>", "the full narration script the video is generated from")
|
|
281
|
+
.requiredOption("--voice-id <id>", "TTS voice id for the narration (see: faceless voices)")
|
|
282
|
+
.addOption(new Option("--model <model>", "generation model: storyboard (20 credits), motion_lite (50) or motion_pro (100)").choices(MODELS))
|
|
283
|
+
.option("--style <style>", "visual style for generated imagery (see: faceless options --kind styles)")
|
|
284
|
+
.option("--language <language>", "script language, e.g. English")
|
|
285
|
+
.option("--name <name>", "project name; defaults to the start of the script")
|
|
286
|
+
.option("--enable-background-music", "mix background music under the narration")
|
|
287
|
+
.option("--master-style <text>", "extra style directive applied to every generated scene")
|
|
288
|
+
.option("--global-negative-prompt <text>", "things the image model should avoid in every scene")
|
|
289
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value"),
|
|
290
|
+
"video"
|
|
326
291
|
).action(async (opts, cmd) => {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
292
|
+
await createAndMaybeWait(
|
|
293
|
+
"createVideo",
|
|
294
|
+
{
|
|
295
|
+
script: opts.script,
|
|
296
|
+
voiceId: opts.voiceId,
|
|
297
|
+
model: opts.model,
|
|
298
|
+
style: opts.style,
|
|
299
|
+
language: opts.language,
|
|
300
|
+
name: opts.name,
|
|
301
|
+
enableBackgroundMusic: opts.enableBackgroundMusic,
|
|
302
|
+
masterStyle: opts.masterStyle,
|
|
303
|
+
globalNegativePrompt: opts.globalNegativePrompt,
|
|
304
|
+
},
|
|
305
|
+
opts,
|
|
306
|
+
cmd
|
|
307
|
+
);
|
|
343
308
|
});
|
|
344
309
|
|
|
345
310
|
withWaitFlags(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
311
|
+
videos
|
|
312
|
+
.command("captions")
|
|
313
|
+
.description(getOp("createCaptionVideo").summary)
|
|
314
|
+
.option("--video-url <url>", "public URL of the video file to caption")
|
|
315
|
+
.option("--audio-url <url>", "public URL of an audio file to turn into a captioned video")
|
|
316
|
+
.option("--name <name>", "project name")
|
|
317
|
+
.option("--language <language>", "spoken language of the file, e.g. English")
|
|
318
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value"),
|
|
319
|
+
"video"
|
|
355
320
|
).action(async (opts, cmd) => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
321
|
+
await createAndMaybeWait(
|
|
322
|
+
"createCaptionVideo",
|
|
323
|
+
{
|
|
324
|
+
videoUrl: opts.videoUrl,
|
|
325
|
+
audioUrl: opts.audioUrl,
|
|
326
|
+
name: opts.name,
|
|
327
|
+
language: opts.language,
|
|
328
|
+
},
|
|
329
|
+
opts,
|
|
330
|
+
cmd
|
|
331
|
+
);
|
|
367
332
|
});
|
|
368
333
|
|
|
369
334
|
videos
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
cmd
|
|
380
|
-
);
|
|
381
|
-
print(result, { json: cmd.optsWithGlobals().json });
|
|
382
|
-
});
|
|
335
|
+
.command("list")
|
|
336
|
+
.description(getOp("listVideos").summary)
|
|
337
|
+
.option("--archived", "only archived videos")
|
|
338
|
+
.option("--page <n>", "page number", toInt)
|
|
339
|
+
.option("--limit <n>", "items per page (max 100)", toInt)
|
|
340
|
+
.action(async (opts, cmd) => {
|
|
341
|
+
const result = await execute("listVideos", { query: { archived: opts.archived, page: opts.page, limit: opts.limit } }, cmd);
|
|
342
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
343
|
+
});
|
|
383
344
|
|
|
384
345
|
videos
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
346
|
+
.command("get <id>")
|
|
347
|
+
.description(getOp("getVideo").summary)
|
|
348
|
+
.action(async (id, _opts, cmd) => {
|
|
349
|
+
const result = await execute("getVideo", { pathParams: { id } }, cmd);
|
|
350
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
351
|
+
});
|
|
391
352
|
|
|
392
353
|
videos
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
facebookPost:
|
|
425
|
-
opts.facebookDescription !== undefined
|
|
426
|
-
? { description: opts.facebookDescription }
|
|
427
|
-
: undefined,
|
|
428
|
-
linkedinPost:
|
|
429
|
-
opts.linkedinDescription !== undefined
|
|
430
|
-
? { description: opts.linkedinDescription }
|
|
431
|
-
: undefined,
|
|
432
|
-
threadsPost: opts.threadsText !== undefined ? { text: opts.threadsText } : undefined,
|
|
433
|
-
});
|
|
434
|
-
const result = await execute("updateVideo", { pathParams: { id }, body }, cmd);
|
|
435
|
-
print(result, { json: cmd.optsWithGlobals().json });
|
|
436
|
-
});
|
|
354
|
+
.command("update <id>")
|
|
355
|
+
.description(getOp("updateVideo").summary)
|
|
356
|
+
.option("--name <name>", "project name")
|
|
357
|
+
.option("--youtube-title <text>", "YouTube title (max 100 chars)")
|
|
358
|
+
.option("--youtube-description <text>", "YouTube description")
|
|
359
|
+
.addOption(new Option("--youtube-privacy <p>", "YouTube privacy, defaults to public").choices(["public", "unlisted", "private"]))
|
|
360
|
+
.option("--tiktok-title <text>", "TikTok caption")
|
|
361
|
+
.option("--instagram-caption <text>", "Instagram caption")
|
|
362
|
+
.option("--x-text <text>", "X post text (max 280 chars)")
|
|
363
|
+
.option("--facebook-description <text>", "Facebook description")
|
|
364
|
+
.option("--linkedin-description <text>", "LinkedIn description")
|
|
365
|
+
.option("--threads-text <text>", "Threads post text")
|
|
366
|
+
.action(async (id, opts, cmd) => {
|
|
367
|
+
const youtubePost = compact({
|
|
368
|
+
title: opts.youtubeTitle,
|
|
369
|
+
description: opts.youtubeDescription,
|
|
370
|
+
privacyStatus: opts.youtubePrivacy,
|
|
371
|
+
});
|
|
372
|
+
const body = compact({
|
|
373
|
+
name: opts.name,
|
|
374
|
+
youtubePost: Object.keys(youtubePost).length ? youtubePost : undefined,
|
|
375
|
+
tiktokPost: opts.tiktokTitle !== undefined ? { title: opts.tiktokTitle } : undefined,
|
|
376
|
+
instagramPost: opts.instagramCaption !== undefined ? { caption: opts.instagramCaption } : undefined,
|
|
377
|
+
xPost: opts.xText !== undefined ? { text: opts.xText } : undefined,
|
|
378
|
+
facebookPost: opts.facebookDescription !== undefined ? { description: opts.facebookDescription } : undefined,
|
|
379
|
+
linkedinPost: opts.linkedinDescription !== undefined ? { description: opts.linkedinDescription } : undefined,
|
|
380
|
+
threadsPost: opts.threadsText !== undefined ? { text: opts.threadsText } : undefined,
|
|
381
|
+
});
|
|
382
|
+
const result = await execute("updateVideo", { pathParams: { id }, body }, cmd);
|
|
383
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
384
|
+
});
|
|
437
385
|
|
|
438
386
|
videos
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
withWaitFlags(
|
|
452
|
-
videos.command("status <id>").description(getOp("getVideoStatus").summary),
|
|
453
|
-
"video"
|
|
454
|
-
).action(async (id, opts, cmd) => {
|
|
455
|
-
const globals = cmd.optsWithGlobals();
|
|
456
|
-
const result = opts.wait
|
|
457
|
-
? await waitForVideo(id, opts.timeout, cmd)
|
|
458
|
-
: await execute("getVideoStatus", { pathParams: { id } }, cmd);
|
|
459
|
-
print(result, { json: globals.json });
|
|
387
|
+
.command("select-thumbnail <id>")
|
|
388
|
+
.description(getOp("selectVideoThumbnail").summary)
|
|
389
|
+
.requiredOption("--variant <variantId>", "variant id from the thumbnails.variants array on `faceless videos get <id>`")
|
|
390
|
+
.action(async (id, opts, cmd) => {
|
|
391
|
+
const result = await execute("selectVideoThumbnail", { pathParams: { id }, body: { variantId: opts.variant } }, cmd);
|
|
392
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
withWaitFlags(videos.command("status <id>").description(getOp("getVideoStatus").summary), "video").action(async (id, opts, cmd) => {
|
|
396
|
+
const globals = cmd.optsWithGlobals();
|
|
397
|
+
const result = opts.wait ? await waitForVideo(id, opts.timeout, cmd) : await execute("getVideoStatus", { pathParams: { id } }, cmd);
|
|
398
|
+
print(result, { json: globals.json });
|
|
460
399
|
});
|
|
461
400
|
|
|
462
401
|
withWaitFlags(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
402
|
+
videos
|
|
403
|
+
.command("render <id>")
|
|
404
|
+
.description(getOp("renderVideo").summary)
|
|
405
|
+
.addOption(new Option("--codec <codec>", "output codec; h264 for MP4").choices(["h264", "vp8"]))
|
|
406
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value"),
|
|
407
|
+
"render"
|
|
469
408
|
).action(async (id, opts, cmd) => {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
print(started, { json: globals.json });
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
print(await waitForRender(renderId, opts.timeout, cmd), { json: globals.json });
|
|
409
|
+
const globals = cmd.optsWithGlobals();
|
|
410
|
+
const started = await execute("renderVideo", { pathParams: { id }, body: { codec: opts.codec }, idempotencyKey: opts.idempotencyKey }, cmd);
|
|
411
|
+
const renderId = started?.data?.renderId;
|
|
412
|
+
if (!opts.wait || !renderId) {
|
|
413
|
+
print(started, { json: globals.json });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
print(await waitForRender(renderId, opts.timeout, cmd), { json: globals.json });
|
|
482
417
|
});
|
|
483
418
|
|
|
484
419
|
videos
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
420
|
+
.command("delete <id>")
|
|
421
|
+
.description(getOp("deleteVideo").summary)
|
|
422
|
+
.action(async (id, _opts, cmd) => {
|
|
423
|
+
const result = await execute("deleteVideo", { pathParams: { id } }, cmd);
|
|
424
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
425
|
+
});
|
|
491
426
|
|
|
492
427
|
const renders = program.command("renders").description("Render progress");
|
|
493
428
|
|
|
494
|
-
withWaitFlags(renders.command("get <id>").description(getOp("getRender").summary), "render").action(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
: await execute("getRender", { pathParams: { id } }, cmd);
|
|
500
|
-
print(result, { json: globals.json });
|
|
501
|
-
}
|
|
502
|
-
);
|
|
429
|
+
withWaitFlags(renders.command("get <id>").description(getOp("getRender").summary), "render").action(async (id, opts, cmd) => {
|
|
430
|
+
const globals = cmd.optsWithGlobals();
|
|
431
|
+
const result = opts.wait ? await waitForRender(id, opts.timeout, cmd) : await execute("getRender", { pathParams: { id } }, cmd);
|
|
432
|
+
print(result, { json: globals.json });
|
|
433
|
+
});
|
|
503
434
|
|
|
504
435
|
const series = program.command("series").description("Automated video series");
|
|
505
436
|
|
|
506
437
|
withSeriesFlags(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
"--source <source>",
|
|
513
|
-
'content source, e.g. "Facts & stories" (see: faceless options --kind sources)'
|
|
514
|
-
)
|
|
438
|
+
series
|
|
439
|
+
.command("create")
|
|
440
|
+
.description(getOp("createSeries").summary)
|
|
441
|
+
.requiredOption("--name <name>", "series name shown in the dashboard")
|
|
442
|
+
.requiredOption("--source <source>", 'content source, e.g. "Facts & stories" (see: faceless options --kind sources)')
|
|
515
443
|
)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
cmd
|
|
522
|
-
);
|
|
523
|
-
print(result, { json: cmd.optsWithGlobals().json });
|
|
524
|
-
});
|
|
444
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value")
|
|
445
|
+
.action(async (opts, cmd) => {
|
|
446
|
+
const result = await execute("createSeries", { body: seriesBody(opts), idempotencyKey: opts.idempotencyKey }, cmd);
|
|
447
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
448
|
+
});
|
|
525
449
|
|
|
526
450
|
series
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
451
|
+
.command("list")
|
|
452
|
+
.description(getOp("listSeries").summary)
|
|
453
|
+
.action(async (_opts, cmd) => {
|
|
454
|
+
const result = await execute("listSeries", {}, cmd);
|
|
455
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
456
|
+
});
|
|
533
457
|
|
|
534
458
|
series
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
459
|
+
.command("get <id>")
|
|
460
|
+
.description(getOp("getSeries").summary)
|
|
461
|
+
.action(async (id, _opts, cmd) => {
|
|
462
|
+
const result = await execute("getSeries", { pathParams: { id } }, cmd);
|
|
463
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
464
|
+
});
|
|
541
465
|
|
|
542
466
|
withSeriesFlags(
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
467
|
+
series
|
|
468
|
+
.command("update <id>")
|
|
469
|
+
.description(getOp("updateSeries").summary)
|
|
470
|
+
.option("--name <name>", "series name")
|
|
471
|
+
.option("--source <source>", "content source (see: faceless options --kind sources)")
|
|
472
|
+
.option("--paused", "pause automatic episode generation")
|
|
473
|
+
.option("--no-paused", "resume automatic episode generation")
|
|
550
474
|
).action(async (id, opts, cmd) => {
|
|
551
|
-
|
|
552
|
-
|
|
475
|
+
const result = await execute("updateSeries", { pathParams: { id }, body: seriesBody(opts) }, cmd);
|
|
476
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
553
477
|
});
|
|
554
478
|
|
|
555
479
|
series
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
480
|
+
.command("delete <id>")
|
|
481
|
+
.description(getOp("deleteSeries").summary)
|
|
482
|
+
.action(async (id, _opts, cmd) => {
|
|
483
|
+
const result = await execute("deleteSeries", { pathParams: { id } }, cmd);
|
|
484
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
485
|
+
});
|
|
562
486
|
|
|
563
487
|
withWaitFlags(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
.description(getOp("generateSeriesEpisode").summary)
|
|
567
|
-
.option("--idempotency-key <k>", "Idempotency-Key header value"),
|
|
568
|
-
"episode's video"
|
|
488
|
+
series.command("generate <id>").description(getOp("generateSeriesEpisode").summary).option("--idempotency-key <k>", "Idempotency-Key header value"),
|
|
489
|
+
"episode's video"
|
|
569
490
|
).action(async (id, opts, cmd) => {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
print(started, { json: globals.json });
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
print(await waitForVideo(projectId, opts.timeout, cmd), { json: globals.json });
|
|
491
|
+
const globals = cmd.optsWithGlobals();
|
|
492
|
+
noteCost("generateSeriesEpisode");
|
|
493
|
+
const started = await execute("generateSeriesEpisode", { pathParams: { id }, idempotencyKey: opts.idempotencyKey }, cmd);
|
|
494
|
+
const projectId = started?.data?.projectId;
|
|
495
|
+
if (!opts.wait || !projectId) {
|
|
496
|
+
print(started, { json: globals.json });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
print(await waitForVideo(projectId, opts.timeout, cmd), { json: globals.json });
|
|
583
500
|
});
|
|
584
501
|
|
|
585
502
|
series
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
cmd
|
|
595
|
-
);
|
|
596
|
-
print(result, { json: cmd.optsWithGlobals().json });
|
|
597
|
-
});
|
|
503
|
+
.command("episodes <id>")
|
|
504
|
+
.description(getOp("listSeriesEpisodes").summary)
|
|
505
|
+
.option("--page <n>", "page number", toInt)
|
|
506
|
+
.option("--limit <n>", "items per page (max 100)", toInt)
|
|
507
|
+
.action(async (id, opts, cmd) => {
|
|
508
|
+
const result = await execute("listSeriesEpisodes", { pathParams: { id }, query: { page: opts.page, limit: opts.limit } }, cmd);
|
|
509
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
510
|
+
});
|
|
598
511
|
|
|
599
512
|
const posts = program.command("posts").description("Publishing and scheduling");
|
|
600
513
|
|
|
601
514
|
posts
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
description: opts.description,
|
|
630
|
-
privacyStatus: opts.privacy,
|
|
631
|
-
authId: opts.authId,
|
|
632
|
-
},
|
|
633
|
-
idempotencyKey: opts.idempotencyKey,
|
|
634
|
-
},
|
|
635
|
-
cmd
|
|
636
|
-
);
|
|
637
|
-
print(result, { json: cmd.optsWithGlobals().json });
|
|
638
|
-
});
|
|
515
|
+
.command("publish")
|
|
516
|
+
.description(getOp("publishPost").summary)
|
|
517
|
+
.requiredOption("--video-id <id>", "the video (project) to publish")
|
|
518
|
+
.addOption(new Option("--platform <platform>", "destination platform; must be connected").choices(PLATFORMS).makeOptionMandatory())
|
|
519
|
+
.option("--title <text>", "post title or caption; falls back to the video's stored post metadata")
|
|
520
|
+
.option("--description <text>", "longer description (YouTube, Facebook, LinkedIn)")
|
|
521
|
+
.addOption(new Option("--privacy <p>", "YouTube only; defaults to public").choices(["public", "unlisted", "private"]))
|
|
522
|
+
.option("--auth-id <id>", "specific connected account id (see: faceless accounts)")
|
|
523
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value")
|
|
524
|
+
.action(async (opts, cmd) => {
|
|
525
|
+
const result = await execute(
|
|
526
|
+
"publishPost",
|
|
527
|
+
{
|
|
528
|
+
body: {
|
|
529
|
+
videoId: opts.videoId,
|
|
530
|
+
platform: opts.platform,
|
|
531
|
+
title: opts.title,
|
|
532
|
+
description: opts.description,
|
|
533
|
+
privacyStatus: opts.privacy,
|
|
534
|
+
authId: opts.authId,
|
|
535
|
+
},
|
|
536
|
+
idempotencyKey: opts.idempotencyKey,
|
|
537
|
+
},
|
|
538
|
+
cmd
|
|
539
|
+
);
|
|
540
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
541
|
+
});
|
|
639
542
|
|
|
640
543
|
posts
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
544
|
+
.command("schedule")
|
|
545
|
+
.description(getOp("schedulePost").summary)
|
|
546
|
+
.requiredOption("--video-id <id>", "the video (project) to schedule")
|
|
547
|
+
.requiredOption(
|
|
548
|
+
"--platforms <platform[,platform...]>",
|
|
549
|
+
"platforms to post to; each needs its post metadata set on the video first (faceless videos update)"
|
|
550
|
+
)
|
|
551
|
+
.requiredOption("--scheduled-time <ISO>", "when to post (ISO 8601, future)")
|
|
552
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value")
|
|
553
|
+
.action(async (opts, cmd) => {
|
|
554
|
+
const result = await execute(
|
|
555
|
+
"schedulePost",
|
|
556
|
+
{
|
|
557
|
+
body: {
|
|
558
|
+
videoId: opts.videoId,
|
|
559
|
+
platforms: splitList(opts.platforms),
|
|
560
|
+
scheduledTime: opts.scheduledTime,
|
|
561
|
+
},
|
|
562
|
+
idempotencyKey: opts.idempotencyKey,
|
|
563
|
+
},
|
|
564
|
+
cmd
|
|
565
|
+
);
|
|
566
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
567
|
+
});
|
|
665
568
|
|
|
666
569
|
posts
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
570
|
+
.command("cancel <videoId>")
|
|
571
|
+
.description(getOp("cancelPost").summary)
|
|
572
|
+
.action(async (videoId, _opts, cmd) => {
|
|
573
|
+
const result = await execute("cancelPost", { pathParams: { id: videoId } }, cmd);
|
|
574
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
575
|
+
});
|
|
673
576
|
|
|
674
577
|
program
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
578
|
+
.command("calendar")
|
|
579
|
+
.description(getOp("getCalendar").summary)
|
|
580
|
+
.requiredOption("--start-date <date>", "range start (ISO 8601, inclusive)")
|
|
581
|
+
.requiredOption("--end-date <date>", "range end (ISO 8601, inclusive)")
|
|
582
|
+
.addOption(new Option("--platform <platform>", "filter to one platform").choices(PLATFORMS))
|
|
583
|
+
.option("--status <status>", "filter by post status, e.g. scheduled, posted, failed")
|
|
584
|
+
.action(async (opts, cmd) => {
|
|
585
|
+
const result = await execute(
|
|
586
|
+
"getCalendar",
|
|
587
|
+
{
|
|
588
|
+
query: {
|
|
589
|
+
startDate: opts.startDate,
|
|
590
|
+
endDate: opts.endDate,
|
|
591
|
+
platform: opts.platform,
|
|
592
|
+
status: opts.status,
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
cmd
|
|
596
|
+
);
|
|
597
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
598
|
+
});
|
|
696
599
|
|
|
697
600
|
program
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
601
|
+
.command("accounts")
|
|
602
|
+
.description(getOp("listAccounts").summary)
|
|
603
|
+
.action(async (_opts, cmd) => {
|
|
604
|
+
const result = await execute("listAccounts", {}, cmd);
|
|
605
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
606
|
+
});
|
|
704
607
|
|
|
705
608
|
program
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
609
|
+
.command("voices")
|
|
610
|
+
.description(getOp("listVoices").summary)
|
|
611
|
+
.action(async (_opts, cmd) => {
|
|
612
|
+
const result = await execute("listVoices", {}, cmd);
|
|
613
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
614
|
+
});
|
|
712
615
|
|
|
713
616
|
program
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
.action(async (opts, cmd) => {
|
|
722
|
-
const result = await execute("listOptions", { query: { kind: opts.kind } }, cmd);
|
|
723
|
-
print(result, { json: cmd.optsWithGlobals().json });
|
|
724
|
-
});
|
|
617
|
+
.command("options")
|
|
618
|
+
.description(getOp("listOptions").summary)
|
|
619
|
+
.addOption(new Option("--kind <kind>", "which catalog to return; omit to list available kinds").choices(OPTION_KINDS))
|
|
620
|
+
.action(async (opts, cmd) => {
|
|
621
|
+
const result = await execute("listOptions", { query: { kind: opts.kind } }, cmd);
|
|
622
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
623
|
+
});
|
|
725
624
|
|
|
726
625
|
const assets = program.command("assets").description("Media asset library");
|
|
727
626
|
|
|
728
627
|
assets
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
628
|
+
.command("create")
|
|
629
|
+
.description(getOp("createAsset").summary)
|
|
630
|
+
.requiredOption("--url <url>", "public URL of the media file")
|
|
631
|
+
.option("--name <name>", "display name for the asset")
|
|
632
|
+
.option("--file-type <mime>", "MIME type hint, e.g. video/mp4")
|
|
633
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value")
|
|
634
|
+
.action(async (opts, cmd) => {
|
|
635
|
+
const result = await execute(
|
|
636
|
+
"createAsset",
|
|
637
|
+
{
|
|
638
|
+
body: { url: opts.url, name: opts.name, fileType: opts.fileType },
|
|
639
|
+
idempotencyKey: opts.idempotencyKey,
|
|
640
|
+
},
|
|
641
|
+
cmd
|
|
642
|
+
);
|
|
643
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
644
|
+
});
|
|
746
645
|
|
|
747
646
|
program
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
)
|
|
758
|
-
.option("--auth-id <id>", "filter to one connected account (see: faceless accounts)")
|
|
759
|
-
.option("--range <days>", "trailing window in days (default 30, max 365)", toInt)
|
|
760
|
-
.action(async (opts, cmd) => {
|
|
761
|
-
const result = await execute(
|
|
762
|
-
"getAnalytics",
|
|
763
|
-
{ query: { platform: opts.platform, authId: opts.authId, range: opts.range } },
|
|
764
|
-
cmd
|
|
765
|
-
);
|
|
766
|
-
print(result, { json: cmd.optsWithGlobals().json });
|
|
767
|
-
});
|
|
647
|
+
.command("analytics")
|
|
648
|
+
.description(getOp("getAnalytics").summary)
|
|
649
|
+
.addOption(new Option("--platform <platform>", "filter to one platform").choices(["youtube", "tiktok", "instagram", "facebook"]))
|
|
650
|
+
.option("--auth-id <id>", "filter to one connected account (see: faceless accounts)")
|
|
651
|
+
.option("--range <days>", "trailing window in days (default 30, max 365)", toInt)
|
|
652
|
+
.action(async (opts, cmd) => {
|
|
653
|
+
const result = await execute("getAnalytics", { query: { platform: opts.platform, authId: opts.authId, range: opts.range } }, cmd);
|
|
654
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
655
|
+
});
|
|
768
656
|
|
|
769
657
|
program
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
658
|
+
.command("mcp")
|
|
659
|
+
.description("Start the local Faceless MCP server over stdio")
|
|
660
|
+
.action(async () => {
|
|
661
|
+
const { runMcpServer } = await import("./mcp/stdio.mjs");
|
|
662
|
+
await runMcpServer();
|
|
663
|
+
});
|
|
776
664
|
|
|
777
665
|
program.parseAsync(process.argv).catch((err) => {
|
|
778
|
-
|
|
779
|
-
|
|
666
|
+
printError(err);
|
|
667
|
+
process.exit(exitCodeFor(err));
|
|
780
668
|
});
|