@scenerok/cli 2.2.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 +173 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +94 -0
- package/dist/commands/cache.d.ts +3 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +65 -0
- package/dist/commands/lint-video.d.ts +21 -0
- package/dist/commands/lint-video.d.ts.map +1 -0
- package/dist/commands/lint-video.js +122 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +173 -0
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.d.ts.map +1 -0
- package/dist/commands/render.js +242 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +208 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +254 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/lib/api.d.ts +268 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +160 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +53 -0
- package/dist/lib/format-api-error.d.ts +2 -0
- package/dist/lib/format-api-error.d.ts.map +1 -0
- package/dist/lib/format-api-error.js +24 -0
- package/dist/lib/language.d.ts +3 -0
- package/dist/lib/language.d.ts.map +1 -0
- package/dist/lib/language.js +9 -0
- package/dist/lib/logger.d.ts +8 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +51 -0
- package/dist/lib/project.d.ts +29 -0
- package/dist/lib/project.d.ts.map +1 -0
- package/dist/lib/project.js +156 -0
- package/dist/lib/sdk-scene-modules.d.ts +7 -0
- package/dist/lib/sdk-scene-modules.d.ts.map +1 -0
- package/dist/lib/sdk-scene-modules.js +35 -0
- package/dist/utils/skillsManifest.d.ts +42 -0
- package/dist/utils/skillsManifest.d.ts.map +1 -0
- package/dist/utils/skillsManifest.js +122 -0
- package/examples/system/3-tips-fitness.vid +45 -0
- package/examples/system/ecom-product-launch.vid +31 -0
- package/examples/system/glitch-title.vid +17 -0
- package/examples/system/meme-remix.vid +15 -0
- package/examples/system/minimal-text-reel.vid +11 -0
- package/examples/system/product-launch.vid +18 -0
- package/examples/system/rokmilk-chocolate-promo.vid +25 -0
- package/examples/system/testimonial-cut.vid +16 -0
- package/examples/system/ugc-testimonial-voiceover.vid +33 -0
- package/examples/system/voice-visual-promo.vid +21 -0
- package/package.json +51 -0
- package/skills/remote/ai-models.md +65 -0
- package/skills/remote/animations-catalog.md +89 -0
- package/skills/remote/animations-picker.md +77 -0
- package/skills/remote/brand-setup.md +69 -0
- package/skills/remote/catalog-index.json +1810 -0
- package/skills/remote/environments.md +73 -0
- package/skills/remote/intent-router.md +81 -0
- package/skills/remote/media-stdlib.md +66 -0
- package/skills/remote/music.md +69 -0
- package/skills/remote/sdk-guide.md +201 -0
- package/skills/remote/shaders-filters.md +97 -0
- package/skills/remote/text-styles.md +108 -0
- package/skills/remote/transitions-catalog.md +55 -0
- package/skills/remote/tts.md +82 -0
- package/skills/remote/vidscript-guide.md +574 -0
- package/skills/remote/vidscript-sample.md +28 -0
- package/skills/remote/vidscript-strict.md +251 -0
- package/skills/remote/workflow.md +72 -0
- package/skills/shared/SKILL.md +157 -0
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# SceneRok CLI
|
|
2
|
+
|
|
3
|
+
Create videos from your terminal and agent workflows.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @scenerok/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use with `npx`:
|
|
12
|
+
```bash
|
|
13
|
+
npx @scenerok/cli auth login
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Authenticate
|
|
20
|
+
scenerok auth login
|
|
21
|
+
|
|
22
|
+
# Validate a VidScript
|
|
23
|
+
scenerok validate video.vidscript
|
|
24
|
+
|
|
25
|
+
# Render a video
|
|
26
|
+
scenerok render video.vidscript --resolution 1080x1920 --watch
|
|
27
|
+
|
|
28
|
+
# Upload a local project and assets
|
|
29
|
+
scenerok project upload video.vidscript --assets ./assets
|
|
30
|
+
|
|
31
|
+
# Upload, render, watch, and download
|
|
32
|
+
scenerok project upload video.vidscript --assets ./assets --render --watch --download ./renders
|
|
33
|
+
|
|
34
|
+
# Pull generated asset cache
|
|
35
|
+
scenerok cache pull
|
|
36
|
+
|
|
37
|
+
# Check render status
|
|
38
|
+
scenerok status 42
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
### `auth`
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
scenerok auth login # Log in via browser
|
|
47
|
+
scenerok auth status # Check auth status
|
|
48
|
+
scenerok auth logout # Remove credentials
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `skills`
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
scenerok skills install <platform> [--sync] # Install router skill (+ optional full sync)
|
|
55
|
+
scenerok skills sync # Download remote skill bundle
|
|
56
|
+
scenerok skills check [--json] # Compare local vs remote manifest
|
|
57
|
+
scenerok skills update # Refresh outdated skills
|
|
58
|
+
scenerok skills list # List remote skills + local status
|
|
59
|
+
scenerok skills platforms # List supported agent platforms
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Supported platforms: `opencode`, `claude`, `codex`, `cursor`, `aider`
|
|
63
|
+
|
|
64
|
+
Remote skills are served from https://scenerok.com/.well-known/agent-skills/ and updated on every deploy — no CLI release needed for skill content changes.
|
|
65
|
+
|
|
66
|
+
### `validate`
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
scenerok validate <file.vidscript>
|
|
70
|
+
scenerok validate <file.vidscript> --generate-assets --report-json
|
|
71
|
+
scenerok validate <file.vidscript> --render --cost medium
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Validates VidScript syntax and returns scene count, duration, and warnings.
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
- `--generate-assets` - Real compile: plugins execute, assets generate + cache, durations and text geometry are measured (billed; requires auth)
|
|
78
|
+
- `--render` - Full pipeline: validate with generated assets → render → validate the output MP4 (ffprobe + LLM frame review, billed). Exit code 0 only when every stage passes — built for agent write→lint→render→lint loops
|
|
79
|
+
- `--cost <tier>` / `--frames <n>` - Frame-grab budget for the MP4 review (see `lint-video`)
|
|
80
|
+
- `--budget <usd>` - Total USD cap for the whole `--render` pipeline (plugin generation + render + MP4 review); the pipeline aborts before any billed call if the free dry-run estimate exceeds it, and the MP4 review draws only from what remains
|
|
81
|
+
- `--render-budget <usd>` - USD cap on plugin generation + render spend alone
|
|
82
|
+
- `--validation-budget <usd>` - USD cap on the MP4 LLM review; frame count is clamped to fit
|
|
83
|
+
- `--resolution <resolution>` - Output resolution for `--render`
|
|
84
|
+
- `--report-json` - Print reports as JSON (for agents)
|
|
85
|
+
|
|
86
|
+
### `lint-video`
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
scenerok lint-video <render-id> [--cost low|medium|high] [--frames <n>] [--budget <usd>] [--json]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Validates a completed render's actual MP4:
|
|
93
|
+
1. **ffprobe checks** (free): readable container, video stream present, duration/aspect/audio vs the VidScript
|
|
94
|
+
2. **Frame review** (billed by token usage): frames are grabbed at timestamps where the script says content appears (text midpoints, clip midpoints, opening, ending) and a vision LLM compares each frame with what the script expects — missing/illegible text, blank frames, artifacts
|
|
95
|
+
|
|
96
|
+
`--cost` sets the frame budget (low=4, medium=8, high=14); `--frames` sets it exactly (max 20); `--budget <usd>` is a hard USD cap that clamps the frame count to the most that fit. More frames cost more and cover more. All LLM cost is billed to your token balance and itemized in the output.
|
|
97
|
+
|
|
98
|
+
### `render`
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
scenerok render <file.vidscript> [options]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Options:
|
|
105
|
+
- `-r, --resolution <resolution>` - Output resolution (e.g. 1080x1920)
|
|
106
|
+
- `-p, --project-id <id>` - Attach the render to an uploaded project
|
|
107
|
+
- `-w, --watch` - Watch render status until complete
|
|
108
|
+
- `--download [dir]` - Download the completed MP4
|
|
109
|
+
- `--validate [cost]` - After rendering, validate the output MP4 (implies watching; billed frame review, cost tier low/medium/high)
|
|
110
|
+
- `--validation-budget <usd>` - Hard USD cap on the MP4 review when using `--validate`
|
|
111
|
+
|
|
112
|
+
### `status`
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
scenerok status <render-id>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Check the status of a render job.
|
|
119
|
+
|
|
120
|
+
Use `scenerok status <render-id> --download ./renders` to download a completed render.
|
|
121
|
+
|
|
122
|
+
### `project`
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
scenerok project upload <file.vidscript> --assets ./assets
|
|
126
|
+
scenerok project upload <file.vidscript> --assets ./assets --render --watch --download ./renders
|
|
127
|
+
scenerok project list
|
|
128
|
+
scenerok project render <project-id> --watch --download ./renders
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`project upload` creates a private SceneRok project on the main server, uploads referenced local assets, rewrites local `input` paths to server asset URLs, and returns a project id. Project renders are visible on the website.
|
|
132
|
+
|
|
133
|
+
### `cache`
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
scenerok cache pull
|
|
137
|
+
scenerok cache pull --plugin @scenerok/xai --output ~/.scenerok/cache/assets
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Downloads cached generated assets and writes a `manifest.json` to the output folder.
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
Config is stored in `~/.scenerok/config.json`:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"apiToken": "rf_...",
|
|
149
|
+
"baseUrl": "https://scenerok.com"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## VidScript
|
|
154
|
+
|
|
155
|
+
VidScript is a declarative language for describing video compositions.
|
|
156
|
+
|
|
157
|
+
```vidscript
|
|
158
|
+
import motion from "@scenerok/basic-animations"
|
|
159
|
+
|
|
160
|
+
input hero = "./assets/hero.mp4"
|
|
161
|
+
|
|
162
|
+
[0s .. 5s] = hero
|
|
163
|
+
[0.4s .. 3s] = text "Hello World", font: "Inter Bold", size: 72, x: "50%", y: "50%", align: center, animate: [motion.fadeIn(0.6s), motion.slideY(40, 0, 0.8s)]
|
|
164
|
+
|
|
165
|
+
output to "video.mp4", resolution: "1080x1920", fps: 30
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Animation helpers are package functions: `motion.fadeIn`, `motion.fadeOut`, `motion.slideX`, `motion.slideY`, `motion.popIn`, `motion.riseIn`, `motion.swingIn`, `motion.glitchIn`, `motion.float`, `motion.typewriter`.
|
|
169
|
+
|
|
170
|
+
## Links
|
|
171
|
+
|
|
172
|
+
- Website: https://scenerok.com
|
|
173
|
+
- Docs: https://scenerok.com/agents/get-started.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,eAAO,MAAM,WAAW,SA2GrB,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { readConfig, writeConfig, isAuthenticated } from '../lib/config.js';
|
|
5
|
+
import { initiateDeviceAuth, pollDeviceAuth } from '../lib/api.js';
|
|
6
|
+
import { getBaseUrl } from '../lib/config.js';
|
|
7
|
+
import { formatApiErrorMessage } from '../lib/format-api-error.js';
|
|
8
|
+
import { getLogFilePath, isVerbose, logError } from '../lib/logger.js';
|
|
9
|
+
export const authCommand = new Command('auth')
|
|
10
|
+
.description('Authenticate with SceneRok')
|
|
11
|
+
.addCommand(new Command('login')
|
|
12
|
+
.description('Log in to SceneRok via browser')
|
|
13
|
+
.option('-n, --name <name>', 'Device name', 'CLI')
|
|
14
|
+
.option('-u, --base-url <url>', 'SceneRok API base URL (overrides ~/.scenerok/config.json and SCENEROK_BASE_URL)')
|
|
15
|
+
.option('-v, --verbose', 'Print debug output and log API calls to ~/.scenerok/logs/cli.log')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
if (options.baseUrl) {
|
|
18
|
+
const config = readConfig();
|
|
19
|
+
config.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
20
|
+
writeConfig(config);
|
|
21
|
+
}
|
|
22
|
+
console.log(chalk.cyan('\n🔐 SceneRok Authentication\n'));
|
|
23
|
+
console.log(chalk.dim(`API: ${getBaseUrl()}`));
|
|
24
|
+
if (options.verbose || isVerbose()) {
|
|
25
|
+
console.log(chalk.dim(`Log file: ${getLogFilePath()}\n`));
|
|
26
|
+
}
|
|
27
|
+
const spinner = ora('Initiating device auth...').start();
|
|
28
|
+
try {
|
|
29
|
+
const auth = await initiateDeviceAuth(options.name);
|
|
30
|
+
spinner.succeed('Device auth initiated');
|
|
31
|
+
console.log(chalk.yellow('\n1. Open this URL in your browser:'));
|
|
32
|
+
console.log(chalk.underline(auth.verification_uri));
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.yellow('2. Enter this code if prompted:'));
|
|
35
|
+
console.log(chalk.bold(` ${auth.user_code}`));
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(chalk.yellow('3. Or run this command:'));
|
|
38
|
+
console.log(` open "${auth.verification_uri}"`);
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(chalk.dim(`Waiting for authentication... (expires in ${auth.expires_in}s)\n`));
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
const expiresIn = auth.expires_in * 1000;
|
|
43
|
+
while (Date.now() - startTime < expiresIn) {
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, auth.interval * 1000));
|
|
45
|
+
const result = await pollDeviceAuth(auth.device_code);
|
|
46
|
+
if (result.status === 'complete' && result.access_token) {
|
|
47
|
+
const config = readConfig();
|
|
48
|
+
config.apiToken = result.access_token;
|
|
49
|
+
writeConfig(config);
|
|
50
|
+
console.log(chalk.green('\n✅ Successfully authenticated with SceneRok!\n'));
|
|
51
|
+
console.log(chalk.dim('Your API token has been saved to ~/.scenerok/config.json'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (result.error === 'expired_token') {
|
|
55
|
+
console.log(chalk.red('\n❌ Authentication expired. Please try again.\n'));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
console.log(chalk.red('\n❌ Authentication timed out. Please try again.\n'));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
spinner.fail('Authentication failed');
|
|
64
|
+
logError('auth login failed', {
|
|
65
|
+
baseUrl: getBaseUrl(),
|
|
66
|
+
error: formatApiErrorMessage(error),
|
|
67
|
+
});
|
|
68
|
+
console.error(chalk.red(formatApiErrorMessage(error)));
|
|
69
|
+
console.error(chalk.dim(`\nLog: ${getLogFilePath()}`));
|
|
70
|
+
console.error(chalk.dim('Retry with: scenerok auth login --verbose (or SCENEROK_VERBOSE=1 scenerok auth login)'));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}))
|
|
74
|
+
.addCommand(new Command('status')
|
|
75
|
+
.description('Check authentication status')
|
|
76
|
+
.action(() => {
|
|
77
|
+
console.log(chalk.dim(`API: ${getBaseUrl()}`));
|
|
78
|
+
console.log(chalk.dim(`Log: ${getLogFilePath()}`));
|
|
79
|
+
if (isAuthenticated()) {
|
|
80
|
+
console.log(chalk.green('✅ Authenticated with SceneRok'));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(chalk.yellow('⚠️ Not authenticated'));
|
|
84
|
+
console.log(chalk.dim('Run: scenerok auth login'));
|
|
85
|
+
}
|
|
86
|
+
}))
|
|
87
|
+
.addCommand(new Command('logout')
|
|
88
|
+
.description('Log out and remove stored credentials')
|
|
89
|
+
.action(() => {
|
|
90
|
+
const config = readConfig();
|
|
91
|
+
delete config.apiToken;
|
|
92
|
+
writeConfig(config);
|
|
93
|
+
console.log(chalk.green('✅ Logged out successfully'));
|
|
94
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/commands/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBpC,eAAO,MAAM,YAAY,SA6CtB,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { basename, extname, join, resolve } from 'node:path';
|
|
6
|
+
import fetch from 'node-fetch';
|
|
7
|
+
import { getAssetCache } from '../lib/api.js';
|
|
8
|
+
import { getCacheDir, isAuthenticated } from '../lib/config.js';
|
|
9
|
+
function safeName(value) {
|
|
10
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120);
|
|
11
|
+
}
|
|
12
|
+
function filenameFor(url, cacheKey) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL(url);
|
|
15
|
+
const fromUrl = basename(parsed.pathname);
|
|
16
|
+
if (fromUrl && extname(fromUrl))
|
|
17
|
+
return safeName(fromUrl);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// fall through
|
|
21
|
+
}
|
|
22
|
+
return `${safeName(cacheKey)}.bin`;
|
|
23
|
+
}
|
|
24
|
+
export const cacheCommand = new Command('cache')
|
|
25
|
+
.description('Manage local SceneRok asset cache')
|
|
26
|
+
.addCommand(new Command('pull')
|
|
27
|
+
.description('Download cached generated assets to a local folder')
|
|
28
|
+
.option('-o, --output <dir>', 'Output directory', join(getCacheDir(), 'assets'))
|
|
29
|
+
.option('--plugin <name>', 'Filter by plugin name')
|
|
30
|
+
.option('--limit <count>', 'Maximum assets to pull', '100')
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
if (!isAuthenticated()) {
|
|
33
|
+
console.log(chalk.yellow('Not authenticated'));
|
|
34
|
+
console.log(chalk.dim('Run: scenerok auth login'));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const outputDir = resolve(options.output);
|
|
38
|
+
mkdirSync(outputDir, { recursive: true });
|
|
39
|
+
const spinner = ora('Fetching asset cache manifest...').start();
|
|
40
|
+
try {
|
|
41
|
+
const manifest = await getAssetCache({
|
|
42
|
+
plugin: options.plugin,
|
|
43
|
+
limit: Number.parseInt(options.limit, 10) || 100,
|
|
44
|
+
});
|
|
45
|
+
spinner.text = `Downloading ${manifest.entries.length} cached asset${manifest.entries.length === 1 ? '' : 's'}...`;
|
|
46
|
+
const downloaded = [];
|
|
47
|
+
for (const entry of manifest.entries) {
|
|
48
|
+
const response = await fetch(entry.r2Url);
|
|
49
|
+
if (!response.ok)
|
|
50
|
+
continue;
|
|
51
|
+
const filename = filenameFor(entry.r2Url, entry.cacheKey);
|
|
52
|
+
const filePath = join(outputDir, filename);
|
|
53
|
+
writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
|
|
54
|
+
downloaded.push({ ...entry, filePath });
|
|
55
|
+
}
|
|
56
|
+
writeFileSync(join(outputDir, 'manifest.json'), JSON.stringify(downloaded, null, 2));
|
|
57
|
+
spinner.succeed(`Downloaded ${downloaded.length} cached asset${downloaded.length === 1 ? '' : 's'}`);
|
|
58
|
+
console.log(chalk.dim(`Manifest: ${join(outputDir, 'manifest.json')}`));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
spinner.fail('Asset cache pull failed');
|
|
62
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { type VideoLintResult } from '../lib/api.js';
|
|
3
|
+
export declare const LINT_COST_CHOICES: string[];
|
|
4
|
+
export declare function parseLintOptions(opts: {
|
|
5
|
+
cost?: string;
|
|
6
|
+
frames?: string;
|
|
7
|
+
budget?: string;
|
|
8
|
+
}): {
|
|
9
|
+
cost?: string;
|
|
10
|
+
frames?: number;
|
|
11
|
+
budgetUsd?: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function printVideoLintResult(result: VideoLintResult, jsonMode?: boolean): void;
|
|
14
|
+
export declare function runVideoLint(renderId: number, options: {
|
|
15
|
+
cost?: string;
|
|
16
|
+
frames?: number;
|
|
17
|
+
budgetUsd?: number;
|
|
18
|
+
json?: boolean;
|
|
19
|
+
}): Promise<boolean>;
|
|
20
|
+
export declare const lintVideoCommand: Command;
|
|
21
|
+
//# sourceMappingURL=lint-video.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint-video.d.ts","sourceRoot":"","sources":["../../src/commands/lint-video.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGrE,eAAO,MAAM,iBAAiB,UAA4B,CAAC;AAE3D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAsBA;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,EAAE,QAAQ,UAAQ,GAAG,IAAI,CA8DpF;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9E,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED,eAAO,MAAM,gBAAgB,SAqBzB,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { validateRender } from '../lib/api.js';
|
|
5
|
+
import { isAuthenticated } from '../lib/config.js';
|
|
6
|
+
export const LINT_COST_CHOICES = ['low', 'medium', 'high'];
|
|
7
|
+
export function parseLintOptions(opts) {
|
|
8
|
+
if (opts.cost && !LINT_COST_CHOICES.includes(opts.cost)) {
|
|
9
|
+
console.log(chalk.red(`❌ Invalid --cost "${opts.cost}" — use ${LINT_COST_CHOICES.join(', ')}`));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
let frames;
|
|
13
|
+
if (opts.frames !== undefined) {
|
|
14
|
+
frames = Number.parseInt(opts.frames, 10);
|
|
15
|
+
if (Number.isNaN(frames) || frames < 1) {
|
|
16
|
+
console.log(chalk.red('❌ --frames must be a positive integer'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
let budgetUsd;
|
|
21
|
+
if (opts.budget !== undefined) {
|
|
22
|
+
budgetUsd = Number.parseFloat(opts.budget);
|
|
23
|
+
if (Number.isNaN(budgetUsd) || budgetUsd <= 0) {
|
|
24
|
+
console.log(chalk.red('❌ --budget must be a positive USD amount'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { cost: opts.cost, frames, budgetUsd };
|
|
29
|
+
}
|
|
30
|
+
export function printVideoLintResult(result, jsonMode = false) {
|
|
31
|
+
if (jsonMode) {
|
|
32
|
+
console.log(JSON.stringify(result, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(result.valid
|
|
36
|
+
? chalk.green('\n✅ Rendered video passed validation\n')
|
|
37
|
+
: chalk.red('\n❌ Rendered video failed validation\n'));
|
|
38
|
+
if (result.probe) {
|
|
39
|
+
const p = result.probe;
|
|
40
|
+
console.log(chalk.cyan(' Probe:'));
|
|
41
|
+
console.log(` ${p.width}x${p.height} @ ${p.fps?.toFixed(1) ?? '?'}fps, ` +
|
|
42
|
+
`${p.durationSeconds?.toFixed(2) ?? '?'}s, ${p.videoCodec ?? 'unknown codec'}, ` +
|
|
43
|
+
`audio: ${p.hasAudio ? 'yes' : 'no'}`);
|
|
44
|
+
}
|
|
45
|
+
for (const error of result.errors ?? []) {
|
|
46
|
+
console.log(chalk.red(` ✗ ${error}`));
|
|
47
|
+
}
|
|
48
|
+
for (const warning of result.warnings ?? []) {
|
|
49
|
+
console.log(chalk.yellow(` ⚠ ${warning}`));
|
|
50
|
+
}
|
|
51
|
+
const review = result.llmReview;
|
|
52
|
+
if (review) {
|
|
53
|
+
console.log(chalk.cyan(`\n Frame review (${result.framesAnalyzed} frames, score ${review.overallScore}/100):`));
|
|
54
|
+
for (const frame of review.frames) {
|
|
55
|
+
const mark = frame.matches ? chalk.green('✓') : chalk.red('✗');
|
|
56
|
+
console.log(` ${mark} ${frame.timestamp.toFixed(1)}s — ${frame.observed}`);
|
|
57
|
+
for (const issue of frame.issues) {
|
|
58
|
+
console.log(chalk.red(` ${issue}`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (review.summary) {
|
|
62
|
+
console.log(`\n ${review.summary}`);
|
|
63
|
+
}
|
|
64
|
+
if (review.suggestions.length > 0) {
|
|
65
|
+
console.log(chalk.cyan('\n Suggestions:'));
|
|
66
|
+
for (const suggestion of review.suggestions) {
|
|
67
|
+
console.log(` • ${suggestion}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (result.note) {
|
|
72
|
+
console.log(chalk.dim(`\n ${result.note}`));
|
|
73
|
+
}
|
|
74
|
+
if (result.billing) {
|
|
75
|
+
if (result.billing.budgetUsd !== undefined) {
|
|
76
|
+
console.log(chalk.dim(`\n Budget: $${result.billing.budgetUsd.toFixed(4)} cap`));
|
|
77
|
+
}
|
|
78
|
+
console.log(chalk.dim(`${result.billing.budgetUsd !== undefined ? '' : '\n'} LLM cost: $${result.billing.costUsd.toFixed(4)} ` +
|
|
79
|
+
`(${result.billing.totalTokens} LLM tokens, $${(result.billing.tokenUnitsCharged / 1000).toFixed(3)} wallet)`));
|
|
80
|
+
}
|
|
81
|
+
console.log('');
|
|
82
|
+
}
|
|
83
|
+
export async function runVideoLint(renderId, options) {
|
|
84
|
+
const spinner = options.json ? null : ora('Validating rendered video (probe + frame review)...').start();
|
|
85
|
+
try {
|
|
86
|
+
const result = await validateRender(renderId, {
|
|
87
|
+
cost: options.cost,
|
|
88
|
+
frames: options.frames,
|
|
89
|
+
budgetUsd: options.budgetUsd,
|
|
90
|
+
});
|
|
91
|
+
spinner?.stop();
|
|
92
|
+
printVideoLintResult(result, options.json);
|
|
93
|
+
return result.valid;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
spinner?.fail('Video validation failed');
|
|
97
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export const lintVideoCommand = new Command('lint-video')
|
|
102
|
+
.description('Validate a completed render\'s MP4: ffprobe checks plus LLM review of frame grabs compared against the VidScript (billed by usage)')
|
|
103
|
+
.argument('<renderId>', 'Completed render job ID')
|
|
104
|
+
.option('--cost <tier>', `Frame-grab budget: ${LINT_COST_CHOICES.join(', ')} (more frames = higher LLM cost, better coverage)`, 'medium')
|
|
105
|
+
.option('--frames <n>', 'Exact number of frame grabs (overrides --cost, max 20)')
|
|
106
|
+
.option('--budget <usd>', 'Hard USD cap: clamp frame grabs to the most that fit this budget')
|
|
107
|
+
.option('--json', 'Print the full report as JSON (for agents)')
|
|
108
|
+
.action(async (renderIdArg, opts) => {
|
|
109
|
+
if (!isAuthenticated()) {
|
|
110
|
+
console.log(chalk.yellow('⚠️ Not authenticated'));
|
|
111
|
+
console.log(chalk.dim('Run: scenerok auth login'));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const renderId = Number.parseInt(renderIdArg, 10);
|
|
115
|
+
if (Number.isNaN(renderId)) {
|
|
116
|
+
console.log(chalk.red('❌ Invalid render ID'));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const { cost, frames, budgetUsd } = parseLintOptions(opts);
|
|
120
|
+
const passed = await runVideoLint(renderId, { cost, frames, budgetUsd, json: opts.json });
|
|
121
|
+
process.exit(passed ? 0 : 1);
|
|
122
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuCpC,eAAO,MAAM,cAAc,SA+JxB,CAAC"}
|