parley-live 0.2.0 → 0.4.0
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/README.md +18 -1
- package/dist/index.js +489 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,10 @@ parley ugc-ad --product product.jpg --name "Cold Brew Kit" --actor jordan --mark
|
|
|
16
16
|
|
|
17
17
|
| Command | What it does |
|
|
18
18
|
|---|---|
|
|
19
|
-
| `parley doctor` | Node, server, session, credits, live engines, the `@last` outputs, and the fix for anything missing. Run it first; `--json` for agents. |
|
|
19
|
+
| `parley doctor [--require <version>]` | Node, server, session, credits, live engines, the `@last` outputs, whether a newer release exists, and the fix for anything missing. `--require` fails "ready" when this CLI is older than what your skills expect. Run it first; `--json` for agents. |
|
|
20
|
+
| `parley voice "<text>" \| --file script.txt` | Narration as a hosted WAV. `--voice warm\|energetic\|documentary\|deep\|bright\|storyteller`, `--quality natural\|fish\|studio`, `--no-fallback`, `--out`. Natural voice: 1 credit per 1,000 characters; studio: 1 per 300. Becomes `@last` for `--narration`. `parley voices` lists personas and prices. |
|
|
21
|
+
| `parley studio …` | The film pipeline without JSON: `character add\|cast\|list`, `location add\|cast\|list`, `shot add\|list`, `render <shotId> [--wait]`, `assemble`, `reels`. Every subcommand takes `--project proj_…`. |
|
|
22
|
+
| `parley deploy <dir> --name <slug>` | Ship a built static site (index.html at the root) to a live URL. Reuse `--project proj_…` to redeploy the same site at the same URL. Free. |
|
|
20
23
|
| `parley login [--no-browser]` | Device-code sign-in. Prints a link, you sign in once in the browser, the CLI receives a session token. `--no-browser` prints the link instead of opening it (cloud sandboxes, SSH). |
|
|
21
24
|
| `parley account` | Credit balance. |
|
|
22
25
|
| `parley engines` | Which video engines this server can run right now, with credit rates. |
|
|
@@ -50,6 +53,20 @@ The CLI remembers the last image and the last clip it made (`~/.parley/last.json
|
|
|
50
53
|
parley image "matte black espresso machine on marble, morning light" --ratio 9:16 --out hero.png
|
|
51
54
|
parley video "steam rises, a hand lifts the cup" --image @last --move dolly-in --ratio 9:16
|
|
52
55
|
parley image "same machine, top-down on oak" --ref @last --ratio 1:1
|
|
56
|
+
parley voice --file script.txt --voice documentary --out narration.wav
|
|
57
|
+
parley youtube --beat https://…/01.png --beat https://…/02.png --narration @last --captions
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### A film from the terminal
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
parley studio character add --project proj_letter-7f3k --name Maya --description "Maya, 32, athletic, oval face, dark hair in a low knot, oat technical top"
|
|
64
|
+
parley studio character cast char_…
|
|
65
|
+
parley studio shot add --project proj_letter-7f3k --order 0 --framing "Maya at a kitchen table at dawn, medium shot" \
|
|
66
|
+
--directing "She unfolds the letter and reads. Clip ends with her eyes on the page." \
|
|
67
|
+
--characters char_… --duration 12 --camera "arri-alexa-35, dolly-in"
|
|
68
|
+
parley studio render shot_… --wait
|
|
69
|
+
parley studio assemble --project proj_letter-7f3k --title "The Letter" --synopsis "A woman reads the letter she avoided."
|
|
53
70
|
```
|
|
54
71
|
|
|
55
72
|
## For agents
|
package/dist/index.js
CHANGED
|
@@ -14,10 +14,20 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Run `parley help <command>` for the flags of one command.
|
|
16
16
|
*/
|
|
17
|
-
import { readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
17
|
+
import { readFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { api, authWebUrl, backendUrl, clearCredentials, CliError, downloadTo, fileToDataUri, isUrl, loadCredentials, loadLast, openInBrowser, rememberLast, resolveRef, saveCredentials, sleep, } from './client.js';
|
|
20
|
-
const VERSION = '0.
|
|
20
|
+
const VERSION = '0.4.0';
|
|
21
|
+
/** Semver-ish compare: 1 if a > b, -1 if a < b, 0 if equal (numeric parts only). */
|
|
22
|
+
function compareVersions(a, b) {
|
|
23
|
+
const pa = a.replace(/^v/, '').split('.').map((x) => parseInt(x, 10) || 0);
|
|
24
|
+
const pb = b.replace(/^v/, '').split('.').map((x) => parseInt(x, 10) || 0);
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
if ((pa[i] || 0) !== (pb[i] || 0))
|
|
27
|
+
return (pa[i] || 0) > (pb[i] || 0) ? 1 : -1;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
21
31
|
function parseArgs(argv) {
|
|
22
32
|
const out = { command: '', positionals: [], flags: {} };
|
|
23
33
|
const rest = [...argv];
|
|
@@ -362,12 +372,16 @@ async function cmdYoutube(p) {
|
|
|
362
372
|
const tier = str(p.flags.tier, 'standard');
|
|
363
373
|
if (!YT_TIERS.includes(tier))
|
|
364
374
|
throw new CliError(`--tier must be one of ${YT_TIERS.join(', ')}`);
|
|
365
|
-
const
|
|
375
|
+
const narrationFlag = str(p.flags.narration);
|
|
376
|
+
const narration = narrationFlag ? resolveRef(narrationFlag, 'audio') : undefined;
|
|
366
377
|
const music = str(p.flags.music);
|
|
367
378
|
const project = str(p.flags.project);
|
|
368
379
|
const silent = p.flags.silent === true;
|
|
369
380
|
if (!narration && !silent) {
|
|
370
|
-
throw new CliError('Pass --narration <https audio URL> (the voiceover the video is cut to), or --silent if the user explicitly wants no voiceover.');
|
|
381
|
+
throw new CliError('Pass --narration <https audio URL | @last> (the voiceover the video is cut to; make one with `parley voice`), or --silent if the user explicitly wants no voiceover.');
|
|
382
|
+
}
|
|
383
|
+
if (narration && !isUrl(narration)) {
|
|
384
|
+
throw new CliError(`--narration must be a public https URL (got "${narration}"). \`parley voice\` prints one, and @last reuses it.`);
|
|
371
385
|
}
|
|
372
386
|
const body = {
|
|
373
387
|
beats: beats.map((imageUrl) => ({ imageUrl })),
|
|
@@ -406,6 +420,420 @@ async function cmdYoutube(p) {
|
|
|
406
420
|
const bytes = await downloadTo(videoUrl, out);
|
|
407
421
|
emit({ ok: true, file: out, bytes, jobId: created.jobId, videoUrl, estimatedCredits: created.estimatedCredits ?? null, result: job.result }, `Saved ${out} (${Math.round(bytes / 1024)} KB)\nHosted: ${videoUrl}`);
|
|
408
422
|
}
|
|
423
|
+
const VOICES = ['warm', 'energetic', 'documentary', 'deep', 'bright', 'storyteller'];
|
|
424
|
+
const VOICE_TIERS = ['natural', 'fish', 'studio'];
|
|
425
|
+
/**
|
|
426
|
+
* parley voice "<text>" | --file script.txt [--voice warm|…] [--quality natural|fish|studio]
|
|
427
|
+
* [--no-fallback] [--out narration.wav]
|
|
428
|
+
* Narration as a hosted WAV. Natural (Kokoro) costs 1 credit per 1,000
|
|
429
|
+
* characters, studio (ElevenLabs) 1 per 300; the server never ships the
|
|
430
|
+
* synthetic fallback voice silently (voiceWarning), and --no-fallback refuses
|
|
431
|
+
* it outright. The result is @last for --narration.
|
|
432
|
+
*/
|
|
433
|
+
const VOICE_MAX_CHARS = 5000;
|
|
434
|
+
const VOICE_SPLIT_TARGET = 4500;
|
|
435
|
+
/**
|
|
436
|
+
* Split a long narration into sections of at most `max` characters, cutting
|
|
437
|
+
* at paragraph breaks first, then sentence ends, then (last resort) spaces,
|
|
438
|
+
* so a section never ends mid-word and rarely mid-sentence.
|
|
439
|
+
*/
|
|
440
|
+
export function splitNarration(text, max = VOICE_SPLIT_TARGET) {
|
|
441
|
+
const clean = text.replace(/\r\n/g, '\n').trim();
|
|
442
|
+
if (clean.length <= max)
|
|
443
|
+
return [clean];
|
|
444
|
+
const sections = [];
|
|
445
|
+
let current = '';
|
|
446
|
+
const push = () => { if (current.trim())
|
|
447
|
+
sections.push(current.trim()); current = ''; };
|
|
448
|
+
const units = clean.split(/\n\s*\n/).flatMap((para) => {
|
|
449
|
+
if (para.length <= max)
|
|
450
|
+
return [para];
|
|
451
|
+
const sentences = para.match(/[^.!?]+[.!?]+["')\]]*\s*|[^.!?]+$/g) || [para];
|
|
452
|
+
return sentences.flatMap((s) => {
|
|
453
|
+
if (s.length <= max)
|
|
454
|
+
return [s];
|
|
455
|
+
const words = s.split(/\s+/);
|
|
456
|
+
const chunks = [];
|
|
457
|
+
let w = '';
|
|
458
|
+
for (const word of words) {
|
|
459
|
+
if ((w + ' ' + word).trim().length > max) {
|
|
460
|
+
chunks.push(w.trim());
|
|
461
|
+
w = word;
|
|
462
|
+
}
|
|
463
|
+
else
|
|
464
|
+
w = (w + ' ' + word).trim();
|
|
465
|
+
}
|
|
466
|
+
if (w)
|
|
467
|
+
chunks.push(w);
|
|
468
|
+
return chunks;
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
for (const unit of units) {
|
|
472
|
+
const sep = current && /\n\s*\n/.test(clean.slice(clean.indexOf(unit) - 3, clean.indexOf(unit))) ? '\n\n' : ' ';
|
|
473
|
+
if ((current + sep + unit).trim().length > max)
|
|
474
|
+
push();
|
|
475
|
+
current = current ? `${current}${sep}${unit}` : unit;
|
|
476
|
+
}
|
|
477
|
+
push();
|
|
478
|
+
return sections;
|
|
479
|
+
}
|
|
480
|
+
async function cmdVoice(p) {
|
|
481
|
+
const file = str(p.flags.file);
|
|
482
|
+
let text = p.positionals.join(' ').trim();
|
|
483
|
+
if (file) {
|
|
484
|
+
if (!existsSync(file))
|
|
485
|
+
throw new CliError(`Script file not found: ${file}`);
|
|
486
|
+
text = readFileSync(file, 'utf8').trim();
|
|
487
|
+
}
|
|
488
|
+
if (!text)
|
|
489
|
+
throw new CliError(`Usage: parley voice "<text>" | --file script.txt [--voice ${VOICES.join('|')}] [--quality ${VOICE_TIERS.join('|')}] [--split] [--no-fallback] [--out narration.wav]`);
|
|
490
|
+
const voice = str(p.flags.voice);
|
|
491
|
+
if (voice && !VOICES.includes(voice))
|
|
492
|
+
throw new CliError(`--voice must be one of ${VOICES.join(', ')}`);
|
|
493
|
+
const quality = str(p.flags.quality, 'natural');
|
|
494
|
+
if (!VOICE_TIERS.includes(quality))
|
|
495
|
+
throw new CliError(`--quality must be one of ${VOICE_TIERS.join(', ')}`);
|
|
496
|
+
const split = p.flags.split === true || text.length > VOICE_MAX_CHARS;
|
|
497
|
+
if (text.length > VOICE_MAX_CHARS && p.flags.split !== true) {
|
|
498
|
+
note(`The text is ${text.length} characters (over one request's ${VOICE_MAX_CHARS}); splitting into sections and joining them into one narration.`);
|
|
499
|
+
}
|
|
500
|
+
const sections = split ? splitNarration(text) : [text];
|
|
501
|
+
const common = { quality, ...(voice ? { voice } : {}), ...(p.flags['no-fallback'] === true ? { allowFallback: false } : {}) };
|
|
502
|
+
const parts = [];
|
|
503
|
+
for (let i = 0; i < sections.length; i++) {
|
|
504
|
+
note(sections.length > 1
|
|
505
|
+
? `Narrating section ${i + 1}/${sections.length} (${sections[i].length} characters) with the ${quality} voice${voice ? ` (${voice})` : ''}…`
|
|
506
|
+
: `Narrating ${sections[i].length} characters with the ${quality} voice${voice ? ` (${voice})` : ''}…`);
|
|
507
|
+
parts.push(await api('/api/voice/generate', { body: { text: sections[i], ...common } }));
|
|
508
|
+
}
|
|
509
|
+
let finalUrl = parts[0].audioUrl;
|
|
510
|
+
let durationSeconds = parts[0].durationSeconds;
|
|
511
|
+
if (parts.length > 1) {
|
|
512
|
+
note(`Joining ${parts.length} sections into one narration…`);
|
|
513
|
+
const j = await api('/api/voice/join', { body: { urls: parts.map((x) => x.audioUrl) } });
|
|
514
|
+
finalUrl = j.audioUrl;
|
|
515
|
+
durationSeconds = j.durationSeconds;
|
|
516
|
+
}
|
|
517
|
+
const out = outPathFor(p, `parley-voice-${Date.now()}.wav`);
|
|
518
|
+
const bytes = await downloadTo(finalUrl, out);
|
|
519
|
+
rememberLast('audio', { file: out, url: finalUrl });
|
|
520
|
+
const warnings = parts.map((x) => x.voiceWarning).filter((w) => !!w);
|
|
521
|
+
if (warnings.length)
|
|
522
|
+
console.error(`warning: ${warnings[0]}${warnings.length > 1 ? ` (${warnings.length} sections)` : ''}`);
|
|
523
|
+
const creditsCharged = parts.reduce((s, x) => s + (x.creditsCharged || 0), 0);
|
|
524
|
+
const engines = [...new Set(parts.map((x) => x.engine))].join('+');
|
|
525
|
+
emit({ ok: true, file: out, bytes, audioUrl: finalUrl, durationSeconds, engine: engines, voice: parts[0].voice, creditsCharged, sections: parts.length, sectionUrls: parts.map((x) => x.audioUrl), voiceWarning: warnings[0] ?? null }, `Saved ${out} (${durationSeconds ?? '?'}s, ${engines}, ${creditsCharged} credits${parts.length > 1 ? `, ${parts.length} sections joined` : ''})\nHosted: ${finalUrl}`);
|
|
526
|
+
}
|
|
527
|
+
async function cmdVoices() {
|
|
528
|
+
const v = await api('/api/voice/voices', { auth: false });
|
|
529
|
+
if (JSON_MODE) {
|
|
530
|
+
console.log(JSON.stringify({ ok: true, ...v }));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
for (const x of v.voices)
|
|
534
|
+
console.log(`${x.id.padEnd(13)} ${x.label}`);
|
|
535
|
+
console.log('');
|
|
536
|
+
for (const [tier, t] of Object.entries(v.tiers))
|
|
537
|
+
console.log(`${tier.padEnd(13)} ${t.creditsPer} (${t.engine})`);
|
|
538
|
+
console.log(`\nup to ${v.maxChars} characters per request`);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* parley studio <character|shot|render|assemble|reels> …
|
|
542
|
+
* First-class Studio commands over the same API `parley api` reaches, so a
|
|
543
|
+
* person at a terminal gets the film pipeline without writing JSON:
|
|
544
|
+
* studio character add --project P --name N --description D [--tags a,b] [--wardrobe W] [--voice V]
|
|
545
|
+
* studio character cast <characterId> [--count 1] [--direction "…"] [--force]
|
|
546
|
+
* studio character list --project P
|
|
547
|
+
* studio shot add --project P --order N --framing "…" --directing "…" [--characters id,id] [--location id]
|
|
548
|
+
* [--duration 12] [--camera "arri-alexa-35, dolly-in"] [--line "characterId:text"]…
|
|
549
|
+
* studio shot list --project P
|
|
550
|
+
* studio render <shotId> [--force] [--wait]
|
|
551
|
+
* studio assemble --project P --title T [--synopsis S] [--title-text X] [--music path] [--narration url]
|
|
552
|
+
* studio reels --project P
|
|
553
|
+
*/
|
|
554
|
+
async function cmdStudio(p) {
|
|
555
|
+
const [group, action, target] = p.positionals;
|
|
556
|
+
const project = str(p.flags.project);
|
|
557
|
+
const needProject = () => {
|
|
558
|
+
if (!project || !/^proj_[A-Za-z0-9_-]{6,}$/.test(project))
|
|
559
|
+
throw new CliError('--project must be a Studio project id like proj_letter-7f3k (proj_ plus at least six letters, digits, _ or -).');
|
|
560
|
+
return project;
|
|
561
|
+
};
|
|
562
|
+
const out = (obj, human) => { if (JSON_MODE)
|
|
563
|
+
console.log(JSON.stringify(obj));
|
|
564
|
+
else
|
|
565
|
+
console.log(human()); };
|
|
566
|
+
if (group === 'character' && action === 'add') {
|
|
567
|
+
const P = needProject();
|
|
568
|
+
const name = str(p.flags.name);
|
|
569
|
+
const description = str(p.flags.description);
|
|
570
|
+
if (!name || !description)
|
|
571
|
+
throw new CliError('studio character add needs --name and --description (the identity lock: age, build, face, hair, skin, wardrobe).');
|
|
572
|
+
const body = { name, description };
|
|
573
|
+
const tags = list(p.flags.tags);
|
|
574
|
+
if (tags.length)
|
|
575
|
+
body.personalityTags = tags;
|
|
576
|
+
const wardrobe = str(p.flags.wardrobe);
|
|
577
|
+
if (wardrobe)
|
|
578
|
+
body.wardrobe = wardrobe;
|
|
579
|
+
const voice = str(p.flags.voice);
|
|
580
|
+
if (voice)
|
|
581
|
+
body.voiceId = voice;
|
|
582
|
+
const c = await api(`/api/studio/projects/${P}/characters`, { body });
|
|
583
|
+
return out({ ok: true, character: c }, () => `Character ${c.name} created: ${c.id}\nNext: parley studio character cast ${c.id}`);
|
|
584
|
+
}
|
|
585
|
+
if (group === 'character' && action === 'cast') {
|
|
586
|
+
if (!target)
|
|
587
|
+
throw new CliError('Usage: parley studio character cast <characterId> [--count 1] [--direction "…"] [--force]');
|
|
588
|
+
const body = { count: num(p.flags.count, 1) };
|
|
589
|
+
const direction = str(p.flags.direction);
|
|
590
|
+
if (direction)
|
|
591
|
+
body.additionalDirection = direction;
|
|
592
|
+
if (p.flags.force === true)
|
|
593
|
+
body.force = true;
|
|
594
|
+
note('Casting (renders the reference image)…');
|
|
595
|
+
const r = await api(`/api/studio/characters/${target}/cast`, { body });
|
|
596
|
+
const refs = r.character?.reference_image_paths || r.reference_image_paths || [];
|
|
597
|
+
return out({ ok: true, ...r }, () => `${r.skipped_reason ? r.skipped_reason + '\n' : ''}Reference images: ${refs.length ? refs.join(', ') : '(see JSON)'}`);
|
|
598
|
+
}
|
|
599
|
+
if (group === 'character' && action === 'list') {
|
|
600
|
+
const P = needProject();
|
|
601
|
+
const r = await api(`/api/studio/projects/${P}/characters`);
|
|
602
|
+
const items = Array.isArray(r) ? r : r.characters || [];
|
|
603
|
+
return out({ ok: true, characters: items }, () => items.map((c) => `${c.id} ${c.name} refs=${(c.reference_image_paths || []).length}`).join('\n') || '(no characters yet)');
|
|
604
|
+
}
|
|
605
|
+
if (group === 'location' && action === 'add') {
|
|
606
|
+
const P = needProject();
|
|
607
|
+
const name = str(p.flags.name);
|
|
608
|
+
const description = str(p.flags.description);
|
|
609
|
+
if (!name || !description)
|
|
610
|
+
throw new CliError('studio location add needs --name and --description (the place, time of day, light, the props that identify it).');
|
|
611
|
+
const l = await api(`/api/studio/projects/${P}/locations`, { body: { name, description } });
|
|
612
|
+
return out({ ok: true, location: l }, () => `Location ${l.name} created: ${l.id}\nNext: parley studio location cast ${l.id}`);
|
|
613
|
+
}
|
|
614
|
+
if (group === 'location' && action === 'cast') {
|
|
615
|
+
if (!target)
|
|
616
|
+
throw new CliError('Usage: parley studio location cast <locationId> [--count 1] [--force]');
|
|
617
|
+
const body = { count: num(p.flags.count, 1) };
|
|
618
|
+
if (p.flags.force === true)
|
|
619
|
+
body.force = true;
|
|
620
|
+
note('Casting the location (renders its reference image)…');
|
|
621
|
+
const r = await api(`/api/studio/locations/${target}/cast`, { body });
|
|
622
|
+
const refs = r.location?.reference_image_paths || r.reference_image_paths || [];
|
|
623
|
+
return out({ ok: true, ...r }, () => `${r.skipped_reason ? r.skipped_reason + '\n' : ''}Reference images: ${refs.length ? refs.join(', ') : '(see JSON)'}`);
|
|
624
|
+
}
|
|
625
|
+
if (group === 'location' && action === 'list') {
|
|
626
|
+
const P = needProject();
|
|
627
|
+
const r = await api(`/api/studio/projects/${P}/locations`);
|
|
628
|
+
const items = Array.isArray(r) ? r : r.locations || [];
|
|
629
|
+
return out({ ok: true, locations: items }, () => items.map((l) => `${l.id} ${l.name} refs=${(l.reference_image_paths || []).length}`).join('\n') || '(no locations yet)');
|
|
630
|
+
}
|
|
631
|
+
if (group === 'shot' && action === 'add') {
|
|
632
|
+
const P = needProject();
|
|
633
|
+
const framing = str(p.flags.framing);
|
|
634
|
+
const directing = str(p.flags.directing);
|
|
635
|
+
if (!framing || !directing)
|
|
636
|
+
throw new CliError('studio shot add needs --framing (the still) and --directing (what happens, with the ending stated).');
|
|
637
|
+
const body = { orderIndex: num(p.flags.order, 0), framingPrompt: framing, directingPrompt: directing };
|
|
638
|
+
const chars = list(p.flags.characters);
|
|
639
|
+
if (chars.length)
|
|
640
|
+
body.characterIds = chars;
|
|
641
|
+
const location = str(p.flags.location);
|
|
642
|
+
if (location)
|
|
643
|
+
body.locationId = location;
|
|
644
|
+
const duration = str(p.flags.duration);
|
|
645
|
+
if (duration)
|
|
646
|
+
body.durationSeconds = Number(duration);
|
|
647
|
+
const camera = str(p.flags.camera);
|
|
648
|
+
if (camera)
|
|
649
|
+
body.cameraEmulation = camera;
|
|
650
|
+
const lines = list(p.flags.line).map((l) => { const i = l.indexOf(':'); if (i < 1)
|
|
651
|
+
throw new CliError('--line must be "characterId:text"'); return { characterId: l.slice(0, i).trim(), text: l.slice(i + 1).trim() }; });
|
|
652
|
+
if (lines.length)
|
|
653
|
+
body.dialogLines = lines;
|
|
654
|
+
const s = await api(`/api/studio/projects/${P}/shots`, { body });
|
|
655
|
+
return out({ ok: true, shot: s }, () => `Shot #${(s.order_index ?? 0) + 1} created: ${s.id}\nNext: parley studio render ${s.id} --wait`);
|
|
656
|
+
}
|
|
657
|
+
if (group === 'shot' && action === 'list') {
|
|
658
|
+
const P = needProject();
|
|
659
|
+
const r = await api(`/api/studio/projects/${P}/shots`);
|
|
660
|
+
const items = Array.isArray(r) ? r : r.shots || [];
|
|
661
|
+
return out({ ok: true, shots: items }, () => items.map((s) => `#${(s.order_index ?? 0) + 1} ${s.id} ${String(s.status).padEnd(9)} ${s.duration_seconds ?? '?'}s ${s.camera_emulation || ''}${s.video_path ? ' clip ready' : ''}`).join('\n') || '(no shots yet)');
|
|
662
|
+
}
|
|
663
|
+
if (group === 'render') {
|
|
664
|
+
const shotId = action;
|
|
665
|
+
if (!shotId)
|
|
666
|
+
throw new CliError('Usage: parley studio render <shotId> [--force] [--wait]');
|
|
667
|
+
const body = {};
|
|
668
|
+
if (p.flags.force === true)
|
|
669
|
+
body.force = true;
|
|
670
|
+
note('Starting the render (a single multi-minute call)…');
|
|
671
|
+
const r = await api(`/api/studio/shots/${shotId}/render`, { body });
|
|
672
|
+
if (r.skipped_reason)
|
|
673
|
+
return out({ ok: true, ...r }, () => r.skipped_reason);
|
|
674
|
+
if (p.flags.wait !== true)
|
|
675
|
+
return out({ ok: true, ...r }, () => `Render started for ${shotId} (status ${r.status ?? 'running'}). Poll with: parley studio shot list --project ${r.project_id || '<project>'}`);
|
|
676
|
+
const P = r.project_id || project;
|
|
677
|
+
if (!P)
|
|
678
|
+
throw new CliError('Cannot wait without a project id; pass --project.');
|
|
679
|
+
const deadline = Date.now() + 45 * 60 * 1000;
|
|
680
|
+
const pollMs = Number(process.env.PARLEY_POLL_MS) || 30000;
|
|
681
|
+
let last = '';
|
|
682
|
+
while (Date.now() < deadline) {
|
|
683
|
+
await sleep(pollMs);
|
|
684
|
+
const shots = await api(`/api/studio/projects/${P}/shots`).then((x) => (Array.isArray(x) ? x : x.shots || []));
|
|
685
|
+
const s = shots.find((x) => x.id === shotId);
|
|
686
|
+
if (!s)
|
|
687
|
+
throw new CliError(`Shot ${shotId} disappeared from project ${P}.`);
|
|
688
|
+
if (s.status !== last) {
|
|
689
|
+
note(` ${s.status}`);
|
|
690
|
+
last = s.status;
|
|
691
|
+
}
|
|
692
|
+
if (s.status === 'ready' && s.video_path)
|
|
693
|
+
return out({ ok: true, shot: s }, () => `Shot ${shotId} rendered: ${s.video_path}`);
|
|
694
|
+
if (s.status === 'failed')
|
|
695
|
+
throw new CliError(`Shot ${shotId} failed to render${s.error ? `: ${s.error}` : ''}.`);
|
|
696
|
+
}
|
|
697
|
+
throw new CliError('Timed out waiting for the render (45 minutes).');
|
|
698
|
+
}
|
|
699
|
+
if (group === 'assemble') {
|
|
700
|
+
const P = needProject();
|
|
701
|
+
const title = str(p.flags.title);
|
|
702
|
+
if (!title)
|
|
703
|
+
throw new CliError('studio assemble needs --title (and usually --synopsis).');
|
|
704
|
+
const body = { title };
|
|
705
|
+
const synopsis = str(p.flags.synopsis);
|
|
706
|
+
if (synopsis)
|
|
707
|
+
body.synopsis = synopsis;
|
|
708
|
+
const titleText = str(p.flags['title-text']);
|
|
709
|
+
if (titleText)
|
|
710
|
+
body.titleText = titleText;
|
|
711
|
+
const music = str(p.flags.music);
|
|
712
|
+
if (music)
|
|
713
|
+
body.musicPath = music;
|
|
714
|
+
const narration = str(p.flags.narration);
|
|
715
|
+
if (narration)
|
|
716
|
+
body.narrationUrl = resolveRef(narration, 'audio');
|
|
717
|
+
note('Assembling the reel (score generated and ducked automatically)…');
|
|
718
|
+
const reel = await api(`/api/studio/projects/${P}/assemble`, { body });
|
|
719
|
+
if (reel.public_url)
|
|
720
|
+
rememberLast('video', { file: reel.video_path || reel.public_url, url: reel.public_url });
|
|
721
|
+
return out({ ok: true, reel }, () => `Reel ${reel.id || ''}: ${reel.public_url || reel.video_path || '(see JSON)'}`);
|
|
722
|
+
}
|
|
723
|
+
if (group === 'reels') {
|
|
724
|
+
const P = needProject();
|
|
725
|
+
const r = await api(`/api/studio/projects/${P}/reels`);
|
|
726
|
+
const items = r.reels || (Array.isArray(r) ? r : []);
|
|
727
|
+
return out({ ok: true, reels: items }, () => items.map((x) => `${x.id} ${x.public_url || x.video_path}`).join('\n') || '(no reels yet)');
|
|
728
|
+
}
|
|
729
|
+
throw new CliError('Usage: parley studio <character add|cast|list | location add|cast|list | shot add|list | render <shotId> | assemble | reels> … (see `parley help studio`)');
|
|
730
|
+
}
|
|
731
|
+
const DEPLOY_MAX_FILES = 600;
|
|
732
|
+
const DEPLOY_MAX_TOTAL_BYTES = 40 * 1024 * 1024;
|
|
733
|
+
const DEPLOY_SKIP_DIRS = new Set(['node_modules', '.git', '.svn', '.hg', '.parley', '.cache', '.next', '.nuxt', '.turbo', 'coverage']);
|
|
734
|
+
const TEXT_EXT = new Set(['html', 'htm', 'css', 'js', 'mjs', 'cjs', 'map', 'json', 'txt', 'md', 'svg', 'xml', 'webmanifest', 'txt', 'csv', 'ics', 'rss', 'atom', 'yml', 'yaml', 'toml', 'wasm.txt']);
|
|
735
|
+
const MIME = {
|
|
736
|
+
html: 'text/html', htm: 'text/html', css: 'text/css', js: 'text/javascript', mjs: 'text/javascript', cjs: 'text/javascript', map: 'application/json',
|
|
737
|
+
json: 'application/json', txt: 'text/plain', md: 'text/markdown', svg: 'image/svg+xml', xml: 'application/xml', webmanifest: 'application/manifest+json',
|
|
738
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif', ico: 'image/x-icon',
|
|
739
|
+
mp4: 'video/mp4', webm: 'video/webm', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg',
|
|
740
|
+
woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf', otf: 'font/otf', pdf: 'application/pdf', wasm: 'application/wasm', glb: 'model/gltf-binary',
|
|
741
|
+
};
|
|
742
|
+
function walkDir(root, rel = '') {
|
|
743
|
+
const out = [];
|
|
744
|
+
for (const entry of readdirSync(path.join(root, rel), { withFileTypes: true })) {
|
|
745
|
+
if (entry.name.startsWith('.DS_Store'))
|
|
746
|
+
continue;
|
|
747
|
+
const r = rel ? `${rel}/${entry.name}` : entry.name;
|
|
748
|
+
if (entry.isDirectory()) {
|
|
749
|
+
if (!DEPLOY_SKIP_DIRS.has(entry.name))
|
|
750
|
+
out.push(...walkDir(root, r));
|
|
751
|
+
}
|
|
752
|
+
else if (entry.isFile())
|
|
753
|
+
out.push(r);
|
|
754
|
+
}
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* parley deploy <dir> --name <project> [--project proj_…] [--summary "…"]
|
|
759
|
+
* Ships a built static site (the folder with index.html at its root) to
|
|
760
|
+
* Parley's Cloudflare Pages deploy: every file is written into a fresh
|
|
761
|
+
* workspace (text as text, binaries base64), then deployed. Prints the live
|
|
762
|
+
* URL. Static only: the server's deploy is a direct upload with no build
|
|
763
|
+
* step, so run your build first and point this at its output.
|
|
764
|
+
*/
|
|
765
|
+
async function cmdDeploy(p) {
|
|
766
|
+
const dir = p.positionals[0];
|
|
767
|
+
const name = str(p.flags.name);
|
|
768
|
+
if (!dir || !name)
|
|
769
|
+
throw new CliError('Usage: parley deploy <built-site-dir> --name <project-name> [--project proj_…] [--summary "…"]\n The dir must contain index.html at its root (run your build first).');
|
|
770
|
+
const root = path.resolve(dir);
|
|
771
|
+
if (!existsSync(root) || !statSync(root).isDirectory())
|
|
772
|
+
throw new CliError(`Not a directory: ${dir}`);
|
|
773
|
+
if (!existsSync(path.join(root, 'index.html')))
|
|
774
|
+
throw new CliError(`No index.html at the root of ${dir}. Point deploy at the BUILT output (dist/, build/, out/, _site/), not the source.`);
|
|
775
|
+
if (!/[a-z0-9]/i.test(name))
|
|
776
|
+
throw new CliError('--name must contain at least one letter or digit.');
|
|
777
|
+
const files = walkDir(root);
|
|
778
|
+
if (files.length === 0)
|
|
779
|
+
throw new CliError('The directory is empty.');
|
|
780
|
+
if (files.length > DEPLOY_MAX_FILES)
|
|
781
|
+
throw new CliError(`${files.length} files is over the ${DEPLOY_MAX_FILES}-file limit; deploy the built output only.`);
|
|
782
|
+
let total = 0;
|
|
783
|
+
for (const f of files)
|
|
784
|
+
total += statSync(path.join(root, f)).size;
|
|
785
|
+
if (total > DEPLOY_MAX_TOTAL_BYTES)
|
|
786
|
+
throw new CliError(`${Math.round(total / 1048576)} MB is over the ${DEPLOY_MAX_TOTAL_BYTES / 1048576} MB limit. Move large media to Parley-hosted URLs (parley image / video print them) and reference them instead of shipping them.`);
|
|
787
|
+
const project = str(p.flags.project) || `proj_site-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
788
|
+
if (!/^proj_[A-Za-z0-9_-]{6,}$/.test(project))
|
|
789
|
+
throw new CliError('--project must look like proj_ followed by at least six letters, digits, _ or -.');
|
|
790
|
+
note(`Uploading ${files.length} files (${Math.round(total / 1024)} KB) into workspace ${project}…`);
|
|
791
|
+
let n = 0;
|
|
792
|
+
for (const f of files) {
|
|
793
|
+
const abs = path.join(root, f);
|
|
794
|
+
const ext = path.extname(f).slice(1).toLowerCase();
|
|
795
|
+
const mimeType = MIME[ext] || (TEXT_EXT.has(ext) ? 'text/plain' : 'application/octet-stream');
|
|
796
|
+
const isText = TEXT_EXT.has(ext) || mimeType.startsWith('text/');
|
|
797
|
+
const buf = readFileSync(abs);
|
|
798
|
+
await api(`/api/workspace/${project}/files`, {
|
|
799
|
+
body: { path: f.replace(/\\/g, '/'), content: isText ? buf.toString('utf8') : buf.toString('base64'), mimeType, isBase64: !isText, allowFullRewrite: true },
|
|
800
|
+
});
|
|
801
|
+
n++;
|
|
802
|
+
if (n % 25 === 0)
|
|
803
|
+
note(` ${n}/${files.length}`);
|
|
804
|
+
}
|
|
805
|
+
note('Deploying…');
|
|
806
|
+
const summary = str(p.flags.summary);
|
|
807
|
+
const d = await api('/api/deploy-project', { body: { workspaceId: project, projectName: name, ...(summary ? { summary } : {}) } });
|
|
808
|
+
let url = d.url;
|
|
809
|
+
let deploymentId = d.deploymentId;
|
|
810
|
+
if (!url && d.jobId) {
|
|
811
|
+
// Asynchronous variant: poll the job until it reports a URL.
|
|
812
|
+
const deadline = Date.now() + 20 * 60 * 1000;
|
|
813
|
+
const pollMs = Number(process.env.PARLEY_POLL_MS) || 4000;
|
|
814
|
+
let last = '';
|
|
815
|
+
while (Date.now() < deadline) {
|
|
816
|
+
await sleep(pollMs);
|
|
817
|
+
const j = await api(`/api/deploy-project/jobs/${d.jobId}`);
|
|
818
|
+
const stage = j.progress?.stage || j.stage || j.status;
|
|
819
|
+
if (stage !== last) {
|
|
820
|
+
note(` ${stage}${j.progress?.message ? ` – ${j.progress.message}` : ''}`);
|
|
821
|
+
last = stage;
|
|
822
|
+
}
|
|
823
|
+
if (j.status === 'failed' || j.ok === false)
|
|
824
|
+
throw new CliError(j.error || j.message || 'Deploy failed.');
|
|
825
|
+
url = j.url || j.result?.url;
|
|
826
|
+
deploymentId = j.deploymentId || j.result?.deploymentId;
|
|
827
|
+
if (url && (j.status === 'succeeded' || j.status === 'done' || j.status === 'complete' || j.stage === 'done'))
|
|
828
|
+
break;
|
|
829
|
+
if (url && !j.status)
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (!url)
|
|
834
|
+
throw new CliError(`Deploy did not return a URL: ${JSON.stringify(d).slice(0, 200)}`);
|
|
835
|
+
emit({ ok: true, url, projectName: name, workspaceId: project, deploymentId: deploymentId ?? null, files: files.length, bytes: total }, `Live: ${url}\nWorkspace ${project} (${files.length} files). Redeploy the same site with --project ${project}.`);
|
|
836
|
+
}
|
|
409
837
|
/**
|
|
410
838
|
* parley api <GET|POST|PATCH|PUT|DELETE> <path> [--data '<json>' | --data @file.json]
|
|
411
839
|
* Signed raw call to any Parley endpoint (the Studio's projects, characters,
|
|
@@ -442,13 +870,45 @@ async function cmdApi(p) {
|
|
|
442
870
|
* the last outputs available as @last. Exit 0 always; `ready` says whether a
|
|
443
871
|
* generation would succeed right now.
|
|
444
872
|
*/
|
|
445
|
-
async function cmdDoctor() {
|
|
873
|
+
async function cmdDoctor(p) {
|
|
446
874
|
const report = { cli: VERSION, node: process.version, backendUrl: backendUrl() };
|
|
447
875
|
const fixes = [];
|
|
448
876
|
const major = Number(process.version.replace(/^v/, '').split('.')[0]);
|
|
449
877
|
report.nodeOk = major >= 20;
|
|
450
878
|
if (!report.nodeOk)
|
|
451
879
|
fixes.push(`Node ${process.version} is too old: install Node 20 or newer.`);
|
|
880
|
+
// --require <version>: the skills declare the CLI they were written for.
|
|
881
|
+
// A CLI older than that is not "ready", whatever else is fine, so an agent
|
|
882
|
+
// never runs a skill against a CLI that lacks the commands the skill uses.
|
|
883
|
+
const required = str(p.flags.require);
|
|
884
|
+
if (required) {
|
|
885
|
+
report.required = required;
|
|
886
|
+
report.cliOk = compareVersions(VERSION, required) >= 0;
|
|
887
|
+
if (!report.cliOk)
|
|
888
|
+
fixes.push(`parley ${VERSION} is older than the ${required} these skills expect: run \`npm install -g parley-live@latest\` (or use \`npx -y parley-live@latest\`).`);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
report.cliOk = true;
|
|
892
|
+
}
|
|
893
|
+
// Best-effort "is there a newer release" check against the registry; never
|
|
894
|
+
// blocks, never fails the report (a sandbox without npm access just skips it).
|
|
895
|
+
// PARLEY_SKIP_UPDATE_CHECK=1 turns it off (tests, air-gapped CI).
|
|
896
|
+
if (process.env.PARLEY_SKIP_UPDATE_CHECK !== '1')
|
|
897
|
+
try {
|
|
898
|
+
const ctrl = new AbortController();
|
|
899
|
+
const t = setTimeout(() => ctrl.abort(), 3000);
|
|
900
|
+
const r = await fetch('https://registry.npmjs.org/parley-live/latest', { signal: ctrl.signal, headers: { Accept: 'application/json' } });
|
|
901
|
+
clearTimeout(t);
|
|
902
|
+
if (r.ok) {
|
|
903
|
+
const latest = (await r.json()).version;
|
|
904
|
+
if (latest) {
|
|
905
|
+
report.latest = latest;
|
|
906
|
+
if (compareVersions(latest, VERSION) > 0)
|
|
907
|
+
report.update = `parley-live ${latest} is available (you have ${VERSION}): npm install -g parley-live@latest`;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
catch { /* offline or blocked: fine */ }
|
|
452
912
|
try {
|
|
453
913
|
const e = await api('/video/engines', { auth: false });
|
|
454
914
|
report.reachable = true;
|
|
@@ -481,35 +941,39 @@ async function cmdDoctor() {
|
|
|
481
941
|
}
|
|
482
942
|
}
|
|
483
943
|
const last = loadLast();
|
|
484
|
-
report.last = { image: last.image?.url ?? last.image?.file ?? null, video: last.video?.url ?? last.video?.file ?? null };
|
|
485
|
-
report.ready = report.nodeOk === true && report.reachable === true && report.signedIn === true && (typeof report.credits !== 'number' || report.credits > 0);
|
|
944
|
+
report.last = { image: last.image?.url ?? last.image?.file ?? null, video: last.video?.url ?? last.video?.file ?? null, audio: last.audio?.url ?? last.audio?.file ?? null };
|
|
945
|
+
report.ready = report.nodeOk === true && report.cliOk === true && report.reachable === true && report.signedIn === true && (typeof report.credits !== 'number' || report.credits > 0);
|
|
486
946
|
report.fixes = fixes;
|
|
487
947
|
if (JSON_MODE) {
|
|
488
948
|
console.log(JSON.stringify({ ok: true, ...report }));
|
|
489
949
|
return;
|
|
490
950
|
}
|
|
491
|
-
console.log(`parley ${VERSION} on Node ${process.version}${report.nodeOk ? '' : ' (too old)'}`);
|
|
951
|
+
console.log(`parley ${VERSION} on Node ${process.version}${report.nodeOk ? '' : ' (too old)'}${report.update ? ` (update: ${report.latest})` : ''}${required ? ` (skills expect ${required}: ${report.cliOk ? 'ok' : 'TOO OLD'})` : ''}`);
|
|
492
952
|
console.log(`server ${report.backendUrl} ${report.reachable ? 'reachable' : 'UNREACHABLE'}`);
|
|
493
953
|
console.log(`session ${report.signedIn ? `signed in as ${report.email || 'user'}, ${report.credits} credits` : 'not signed in'}`);
|
|
494
954
|
if (report.engines) {
|
|
495
955
|
const live = Object.entries(report.engines).filter(([, v]) => v.ready).map(([k]) => k);
|
|
496
956
|
console.log(`engines ${live.join(', ')} (${report.cameraMoves} camera moves)`);
|
|
497
957
|
}
|
|
498
|
-
if (report.last && (report.last.image || report.last.video))
|
|
499
|
-
console.log(`last image=${report.last.image || '-'} video=${report.last.video || '-'}`);
|
|
958
|
+
if (report.last && (report.last.image || report.last.video || report.last.audio))
|
|
959
|
+
console.log(`last image=${report.last.image || '-'} video=${report.last.video || '-'} audio=${report.last.audio || '-'}`);
|
|
500
960
|
console.log(`ready ${report.ready ? 'yes' : 'no'}`);
|
|
501
961
|
for (const f of fixes)
|
|
502
962
|
console.log(` fix: ${f}`);
|
|
503
963
|
}
|
|
504
964
|
function help(topic) {
|
|
505
965
|
const lines = {
|
|
506
|
-
doctor: 'parley doctor [--json]\n Node, server, session, credits, live engines
|
|
966
|
+
doctor: 'parley doctor [--require <version>] [--json]\n Node, server, session, credits, live engines, the @last outputs and whether a newer release exists, with the fix for anything missing. --require fails "ready" when this CLI is older than what your skills expect. Run this first.',
|
|
967
|
+
voice: `parley voice "<text>" | --file script.txt [--voice ${VOICES.join('|')}] [--quality ${VOICE_TIERS.join('|')}] [--split] [--no-fallback] [--out narration.wav]\n Narration as a hosted WAV (natural voice 1 credit per 1,000 characters; studio 1 per 300). Text over 5,000 characters is split at paragraph and sentence breaks and joined back into one narration. Becomes @last for --narration.`,
|
|
968
|
+
voices: 'parley voices\n The narration personas, the tiers and their prices.',
|
|
969
|
+
studio: 'parley studio character add|cast|list · location add|cast|list · shot add|list · render <shotId> [--wait] · assemble · reels\n The film pipeline without JSON: cast characters and locations (identity locks), write shots ("lens look, move" camera), render, assemble a reel. Every subcommand takes --project proj_… and --json.',
|
|
507
970
|
login: 'parley login [--no-browser] [--backend URL]\n Sign in once in your browser. --no-browser prints the link instead of opening it (cloud sandboxes, SSH). Stores a session token under ~/.parley (or $PARLEY_HOME).',
|
|
508
971
|
image: `parley image "<prompt>" [--ratio ${Object.keys(RATIO_TO_SIZE).join('|')}] [--model gpt-image-2|gemini-3-pro-image-preview] [--quality low|medium|high] [--image-size 1K|2K|4K] [--ref <file|url|@last>]... [--out file]\n Generate an image. --ratio sizes it for the destination; --ref passes reference images (identity / style locks).`,
|
|
509
972
|
video: `parley video "<motion prompt>" --image <file|url|@last> [--look ${LOOKS.join('|')}] [--move <slug>]... [--duration 5] [--ratio 16:9|9:16|1:1] [--out clip.mp4]\n Animate a still (@last = the image you just made) or --look photoreal for text-to-video. --move adds named camera moves; see \`parley camera-moves\`.`,
|
|
510
973
|
thumbnail: 'parley thumbnail "<video title>" [--variants 1-3] [--overlay "2-4 WORDS"] [--accent WORD] [--person true|false] [--out dir]',
|
|
511
974
|
'ugc-ad': `parley ugc-ad --product <image> --name "Product" [--actor maya|jordan|sofia|marcus | --face <image>] [--angle ${ANGLES.join('|')}] [--marketplace ${MARKETPLACES.join('|')}] [--points "a;b;c"] [--seconds 3-16] [--url https://...] [--out ad.mp4]`,
|
|
512
975
|
youtube: `parley youtube --beat <https still>... [--narration <https audio>] [--music <https audio>] [--tier ${YT_TIERS.join('|')}] [--captions] [--silent] [--project <workspace id>] [--out video.mp4]\n Animate ordered beat stills and cut them to the narration into one finished video.`,
|
|
976
|
+
deploy: 'parley deploy <built-site-dir> --name <project-name> [--project proj_…] [--summary "…"]\n Ship a built static site (index.html at the root) to a live URL on Parley\'s hosting. Reuse --project to redeploy the same site.',
|
|
513
977
|
api: 'parley api <GET|POST|PATCH|PUT|DELETE> </path> [--data \'{...}\' | --data @file.json]\n Signed raw call to any Parley endpoint (Studio projects, characters, shots, renders, reels).',
|
|
514
978
|
engines: 'parley engines\n Which video engines this server can run right now and their credit rates.',
|
|
515
979
|
'camera-moves': 'parley camera-moves\n The named camera-move presets accepted by `parley video --move`.',
|
|
@@ -535,7 +999,17 @@ async function main(argv) {
|
|
|
535
999
|
break;
|
|
536
1000
|
case 'doctor':
|
|
537
1001
|
case 'status':
|
|
538
|
-
await cmdDoctor();
|
|
1002
|
+
await cmdDoctor(p);
|
|
1003
|
+
break;
|
|
1004
|
+
case 'voice':
|
|
1005
|
+
case 'narrate':
|
|
1006
|
+
await cmdVoice(p);
|
|
1007
|
+
break;
|
|
1008
|
+
case 'voices':
|
|
1009
|
+
await cmdVoices();
|
|
1010
|
+
break;
|
|
1011
|
+
case 'studio':
|
|
1012
|
+
await cmdStudio(p);
|
|
539
1013
|
break;
|
|
540
1014
|
case 'logout':
|
|
541
1015
|
await cmdLogout();
|
|
@@ -574,6 +1048,9 @@ async function main(argv) {
|
|
|
574
1048
|
case 'api':
|
|
575
1049
|
await cmdApi(p);
|
|
576
1050
|
break;
|
|
1051
|
+
case 'deploy':
|
|
1052
|
+
await cmdDeploy(p);
|
|
1053
|
+
break;
|
|
577
1054
|
case 'help':
|
|
578
1055
|
case '--help':
|
|
579
1056
|
case '-h':
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parley-live",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Generate images, video clips, YouTube thumbnails
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Generate images, video clips with named camera moves, narration, YouTube thumbnails, product ads, complete videos and Studio films, and deploy static sites, from the terminal with Parley. Built for coding agents: every command has a --json mode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|