@gthanks/moviespec 0.1.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 ADDED
@@ -0,0 +1,45 @@
1
+ # MovieSpec
2
+
3
+ MovieSpec 是一套面向 AI 图片/视频生成的「规范先行」工作方式:先把世界观、角色、视觉风格、镜头语言与分镜结构写成可机读/可复用的规范文档,再从这些规范稳定编译出 Prompt 与参数,并将每次生成的输入与输出写入 manifest 以便复现与排查一致性漂移。
4
+
5
+ ## 目录结构
6
+
7
+ - `bin/`:CLI 入口
8
+ - `schema/`:可机读 schema(类型、路径、依赖、编译规则)
9
+ - `templates/`:各类规范文档模板(Markdown + YAML front matter)
10
+
11
+ ## 安装
12
+
13
+ 如果你想在全局使用该命令,可以通过 NPM 安装:
14
+
15
+ ```bash
16
+ npm install -g @gthanks/moviespec@latest
17
+ ```
18
+
19
+ ## 用户项目最小目录结构(与工具仓库分离)
20
+
21
+ MovieSpec 工具仓库是本目录(`moviespec/`)。用户实际制作项目建议使用独立工作目录(任意路径),并满足最小结构:
22
+
23
+ - `config.yaml`
24
+ - `specs/`
25
+ - `storyboard/`
26
+ - `prompts/`(由 `compile-prompts` 生成)
27
+ - `output/`(由 `generate` 生成)
28
+
29
+ 你可以在用户项目目录下执行:
30
+
31
+ ```bash
32
+ moviespec init
33
+ ```
34
+
35
+ CLI 将按 `schema/moviespec.schema.yaml` 的默认路径读取与写入上述目录。
36
+
37
+ ## 最小命令集(v1)
38
+
39
+ - `moviespec init`
40
+ - `moviespec list types`
41
+ - `moviespec new <type> <name>`
42
+ - `moviespec compile-prompts --shots <file>`
43
+ - `moviespec validate` / `moviespec check`
44
+ - `moviespec generate image --shot <id>`(stub)
45
+ - `moviespec generate sequence --shots <file>`(stub)
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ import chalk from 'chalk';
9
+ import { Command } from 'commander';
10
+ import fg from 'fast-glob';
11
+ import YAML from 'yaml';
12
+
13
+ function toPosix(p) {
14
+ return p.split(path.sep).join(path.posix.sep);
15
+ }
16
+
17
+ async function pathExists(p) {
18
+ try {
19
+ await fs.access(p);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ async function ensureDir(p) {
27
+ await fs.mkdir(p, { recursive: true });
28
+ }
29
+
30
+ async function readYamlFile(filePath) {
31
+ const raw = await fs.readFile(filePath, 'utf8');
32
+ return YAML.parse(raw);
33
+ }
34
+
35
+ async function writeYamlFile(filePath, data) {
36
+ const raw = YAML.stringify(data);
37
+ await fs.writeFile(filePath, raw, 'utf8');
38
+ }
39
+
40
+ async function readJsonFile(filePath) {
41
+ const raw = await fs.readFile(filePath, 'utf8');
42
+ return JSON.parse(raw);
43
+ }
44
+
45
+ async function writeJsonFile(filePath, data) {
46
+ const raw = JSON.stringify(data, null, 2) + '\n';
47
+ await fs.writeFile(filePath, raw, 'utf8');
48
+ }
49
+
50
+ function stableStringify(value) {
51
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
52
+ if (Array.isArray(value)) return `[${value.map((v) => stableStringify(v)).join(',')}]`;
53
+ const keys = Object.keys(value).sort();
54
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
55
+ }
56
+
57
+ async function sha256Hex(str) {
58
+ const crypto = await import('node:crypto');
59
+ return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
60
+ }
61
+
62
+ async function loadMovieSpecSchema() {
63
+ const here = path.dirname(fileURLToPath(import.meta.url));
64
+ const schemaPath = path.resolve(here, '..', 'schema', 'moviespec.schema.yaml');
65
+ return readYamlFile(schemaPath);
66
+ }
67
+
68
+ function resolveProjectRoot(projectRoot) {
69
+ return path.resolve(projectRoot ?? process.cwd());
70
+ }
71
+
72
+ async function readProjectConfig(projectRoot, schema) {
73
+ const configPath = path.join(projectRoot, schema.project?.config ?? 'config.yaml');
74
+ if (!(await pathExists(configPath))) return null;
75
+ return readYamlFile(configPath);
76
+ }
77
+
78
+ async function listTypes(schema) {
79
+ return Object.keys(schema.types ?? {});
80
+ }
81
+
82
+ function defaultTypePath(schema, typeId) {
83
+ const type = schema.types?.[typeId];
84
+ if (!type) throw new Error(`Unknown type: ${typeId}`);
85
+ return type.defaultPath;
86
+ }
87
+
88
+ async function cmdInit(projectRoot) {
89
+ const schema = await loadMovieSpecSchema();
90
+ const root = resolveProjectRoot(projectRoot);
91
+
92
+ const dirs = [
93
+ schema.project?.specsDir ?? 'specs',
94
+ schema.project?.storyboardDir ?? 'storyboard',
95
+ schema.project?.promptsDir ?? 'prompts',
96
+ schema.project?.outputDir ?? 'output',
97
+ path.join(schema.project?.outputDir ?? 'output', schema.project?.manifestsDir ?? 'manifests'),
98
+ path.join(schema.project?.outputDir ?? 'output', schema.project?.shotsDir ?? 'shots')
99
+ ];
100
+
101
+ for (const d of dirs) {
102
+ await ensureDir(path.join(root, d));
103
+ }
104
+
105
+ const configPath = path.join(root, schema.project?.config ?? 'config.yaml');
106
+ if (!(await pathExists(configPath))) {
107
+ const defaultConfig = {
108
+ moviespec_version: schema.version,
109
+ media: {
110
+ resolution: '1920x1080',
111
+ aspect_ratio: '16:9',
112
+ fps: 24
113
+ },
114
+ lora: {
115
+ characters: {},
116
+ styles: {}
117
+ },
118
+ backend: {
119
+ id: 'stub'
120
+ }
121
+ };
122
+ await writeYamlFile(configPath, defaultConfig);
123
+ }
124
+
125
+ console.log(chalk.green(`Initialized MovieSpec project at: ${root}`));
126
+
127
+ try {
128
+ const prompts = await import('@inquirer/prompts');
129
+ const selectedAgents = await prompts.checkbox({
130
+ message: 'Select AI agents to configure for this MovieSpec project:',
131
+ choices: [
132
+ { name: 'Cursor', value: 'cursor' },
133
+ { name: 'Windsurf', value: 'windsurf' },
134
+ { name: 'Antigravity', value: 'antigravity' }
135
+ ]
136
+ });
137
+
138
+ if (selectedAgents.length > 0) {
139
+ const here = path.dirname(fileURLToPath(import.meta.url));
140
+ const templatesDir = path.resolve(here, '..', 'templates', 'agents');
141
+
142
+ const agentFiles = {
143
+ cursor: '.cursorrules',
144
+ windsurf: '.windsurfrules',
145
+ antigravity: 'AGENTS.md'
146
+ };
147
+
148
+ for (const agent of selectedAgents) {
149
+ const sourceFile = path.join(templatesDir, `${agent}.md`);
150
+ const targetFile = path.join(root, agentFiles[agent]);
151
+
152
+ if (await pathExists(sourceFile)) {
153
+ const content = await fs.readFile(sourceFile, 'utf8');
154
+ await fs.writeFile(targetFile, content, 'utf8');
155
+ console.log(chalk.blue(`Configured rules for ${agent}: ${agentFiles[agent]}`));
156
+ } else {
157
+ console.log(chalk.yellow(`Warning: Template for ${agent} not found at ${sourceFile}`));
158
+ }
159
+ }
160
+ }
161
+ } catch (err) {
162
+ // If the prompt is cancelled or in non-interactive environment, just ignore safely
163
+ if (err.name !== 'ExitPromptError') {
164
+ console.log(chalk.yellow(`Could not run interactive agent setup: ${err.message}`));
165
+ }
166
+ }
167
+ }
168
+
169
+ async function cmdListTypes() {
170
+ const schema = await loadMovieSpecSchema();
171
+ const types = await listTypes(schema);
172
+ for (const t of types) {
173
+ const desc = schema.types?.[t]?.description ?? '';
174
+ console.log(`${t}${desc ? `\t${desc}` : ''}`);
175
+ }
176
+ }
177
+
178
+ async function cmdNew(typeId, name, { projectRoot }) {
179
+ const schema = await loadMovieSpecSchema();
180
+ const root = resolveProjectRoot(projectRoot);
181
+
182
+ const templateRel = schema.types?.[typeId]?.template;
183
+ if (!templateRel) throw new Error(`Unknown type or missing template: ${typeId}`);
184
+ const here = path.dirname(fileURLToPath(import.meta.url));
185
+ const templateAbs = path.resolve(here, '..', templateRel);
186
+
187
+ const targetRel = defaultTypePath(schema, typeId);
188
+ const targetDir = path.join(root, targetRel);
189
+ await ensureDir(targetDir);
190
+
191
+ const filename = `${name}.md`;
192
+ const targetPath = path.join(targetDir, filename);
193
+ if (await pathExists(targetPath)) throw new Error(`File already exists: ${targetPath}`);
194
+
195
+ const template = await fs.readFile(templateAbs, 'utf8');
196
+ const rendered = template
197
+ .replaceAll('{{name}}', name)
198
+ .replaceAll('{{type}}', typeId);
199
+
200
+ await fs.writeFile(targetPath, rendered, 'utf8');
201
+ console.log(chalk.green(`Created ${typeId}: ${toPosix(path.relative(root, targetPath))}`));
202
+ }
203
+
204
+ async function readAllSpecInputs(projectRoot, schema) {
205
+ const root = resolveProjectRoot(projectRoot);
206
+ const specsRoot = path.join(root, schema.project?.specsDir ?? 'specs');
207
+
208
+ const inputs = {};
209
+ for (const typeId of await listTypes(schema)) {
210
+ const rel = defaultTypePath(schema, typeId);
211
+ const abs = path.join(root, rel);
212
+ const files = await fg(['**/*.md', '**/*.yaml', '**/*.yml', '**/*.json'], { cwd: abs, dot: false, onlyFiles: true, suppressErrors: true });
213
+ inputs[typeId] = {
214
+ baseDir: abs,
215
+ files: files.map((f) => path.join(abs, f))
216
+ };
217
+ }
218
+
219
+ const storyboardDir = path.join(root, schema.project?.storyboardDir ?? 'storyboard');
220
+ const storyboardFiles = await fg(['**/*.yaml', '**/*.yml', '**/*.json'], { cwd: storyboardDir, dot: false, onlyFiles: true, suppressErrors: true });
221
+
222
+ return {
223
+ root,
224
+ specsRoot,
225
+ storyboardDir,
226
+ storyboardFiles: storyboardFiles.map((f) => path.join(storyboardDir, f)),
227
+ inputs
228
+ };
229
+ }
230
+
231
+ async function readStoryboardShots(filePath) {
232
+ const ext = path.extname(filePath).toLowerCase();
233
+ if (ext === '.json') return readJsonFile(filePath);
234
+ return readYamlFile(filePath);
235
+ }
236
+
237
+ function normalizeShots(data) {
238
+ if (!data) return [];
239
+ if (Array.isArray(data)) return data;
240
+ if (Array.isArray(data.shots)) return data.shots;
241
+ return [];
242
+ }
243
+
244
+ function buildPromptParts({ shot, config }) {
245
+ const parts = [];
246
+ if (shot?.prompt?.positive) parts.push(String(shot.prompt.positive));
247
+ if (shot?.action) parts.push(String(shot.action));
248
+ if (shot?.scene) parts.push(String(shot.scene));
249
+ if (shot?.composition) parts.push(String(shot.composition));
250
+
251
+ if (config?.media?.resolution) parts.push(`resolution:${config.media.resolution}`);
252
+ if (config?.media?.aspect_ratio) parts.push(`aspect_ratio:${config.media.aspect_ratio}`);
253
+ if (typeof config?.media?.fps !== 'undefined') parts.push(`fps:${config.media.fps}`);
254
+
255
+ return parts.filter(Boolean);
256
+ }
257
+
258
+ async function cmdCompilePrompts({ shots, projectRoot }) {
259
+ const schema = await loadMovieSpecSchema();
260
+ const root = resolveProjectRoot(projectRoot);
261
+ const config = await readProjectConfig(root, schema);
262
+ if (!config) throw new Error(`Missing config file: ${path.join(root, schema.project?.config ?? 'config.yaml')}`);
263
+
264
+ const storyboardPath = path.resolve(root, shots);
265
+ const storyboardData = await readStoryboardShots(storyboardPath);
266
+ const shotList = normalizeShots(storyboardData);
267
+
268
+ const promptsDir = path.join(root, schema.project?.promptsDir ?? 'prompts');
269
+ await ensureDir(promptsDir);
270
+
271
+ const context = await readAllSpecInputs(root, schema);
272
+ const depFingerprints = {};
273
+ for (const [typeId, info] of Object.entries(context.inputs)) {
274
+ const hashes = [];
275
+ for (const f of info.files) {
276
+ const raw = await fs.readFile(f, 'utf8');
277
+ hashes.push(await sha256Hex(raw));
278
+ }
279
+ depFingerprints[typeId] = await sha256Hex(stableStringify(hashes.sort()));
280
+ }
281
+
282
+ const compiled = [];
283
+ for (const s of shotList) {
284
+ const shotId = s.id ?? s.shot_id;
285
+ if (!shotId) throw new Error('Each shot must have an id (id or shot_id)');
286
+
287
+ const mergedMedia = {
288
+ resolution: s.media?.resolution ?? config.media?.resolution,
289
+ aspect_ratio: s.media?.aspect_ratio ?? config.media?.aspect_ratio,
290
+ fps: typeof s.media?.fps !== 'undefined' ? s.media.fps : config.media?.fps
291
+ };
292
+
293
+ const promptParts = buildPromptParts({ shot: s, config: { ...config, media: mergedMedia } });
294
+ const positive = promptParts.join(', ');
295
+ const negative = (s.prompt?.negative ?? '').toString();
296
+
297
+ const out = {
298
+ shot_id: shotId,
299
+ prompt: {
300
+ positive,
301
+ negative
302
+ },
303
+ media: mergedMedia,
304
+ deps: depFingerprints
305
+ };
306
+
307
+ compiled.push(out);
308
+ await writeJsonFile(path.join(promptsDir, `${shotId}.prompt.json`), out);
309
+ }
310
+
311
+ console.log(chalk.green(`Compiled ${compiled.length} shot prompt(s) to ${toPosix(path.relative(root, promptsDir))}`));
312
+ }
313
+
314
+ async function cmdValidate({ projectRoot, shots }) {
315
+ const schema = await loadMovieSpecSchema();
316
+ const root = resolveProjectRoot(projectRoot);
317
+
318
+ const configPath = path.join(root, schema.project?.config ?? 'config.yaml');
319
+ if (!(await pathExists(configPath))) throw new Error(`Missing config file: ${configPath}`);
320
+
321
+ const config = await readProjectConfig(root, schema);
322
+ if (!config?.moviespec_version) throw new Error('config.yaml missing moviespec_version');
323
+
324
+ const context = await readAllSpecInputs(root, schema);
325
+
326
+ const currentDepFingerprints = {};
327
+ for (const [typeId, info] of Object.entries(context.inputs)) {
328
+ const hashes = [];
329
+ for (const f of info.files) {
330
+ const raw = await fs.readFile(f, 'utf8');
331
+ hashes.push(await sha256Hex(raw));
332
+ }
333
+ currentDepFingerprints[typeId] = await sha256Hex(stableStringify(hashes.sort()));
334
+ }
335
+
336
+ const storyboardPath = shots ? path.resolve(root, shots) : null;
337
+ if (storyboardPath) {
338
+ if (!(await pathExists(storyboardPath))) throw new Error(`Shots file not found: ${storyboardPath}`);
339
+
340
+ const data = await readStoryboardShots(storyboardPath);
341
+ const shotList = normalizeShots(data);
342
+
343
+ const characterDirRel = defaultTypePath(schema, 'characters');
344
+ const characterDirAbs = path.join(root, characterDirRel);
345
+ const characterFiles = await fg(['**/*.md', '**/*.yaml', '**/*.yml', '**/*.json'], { cwd: characterDirAbs, onlyFiles: true, suppressErrors: true });
346
+ const characterNames = new Set(characterFiles.map((f) => path.parse(f).name));
347
+
348
+ for (const s of shotList) {
349
+ const shotId = s.id ?? s.shot_id ?? '(unknown)';
350
+ const roles = Array.isArray(s.characters) ? s.characters : [];
351
+ for (const r of roles) {
352
+ if (!characterNames.has(String(r))) {
353
+ throw new Error(`Storyboard shot ${shotId} references missing character: ${r}`);
354
+ }
355
+ }
356
+ }
357
+
358
+ const promptsDir = path.join(root, schema.project?.promptsDir ?? 'prompts');
359
+ const dirty = [];
360
+ for (const s of shotList) {
361
+ const shotId = s.id ?? s.shot_id;
362
+ if (!shotId) continue;
363
+ const promptPath = path.join(promptsDir, `${shotId}.prompt.json`);
364
+ if (!(await pathExists(promptPath))) continue;
365
+ const compiled = await readJsonFile(promptPath);
366
+ const previousDeps = compiled?.deps ?? {};
367
+ if (stableStringify(previousDeps) !== stableStringify(currentDepFingerprints)) {
368
+ dirty.push(String(shotId));
369
+ }
370
+ }
371
+ if (dirty.length > 0) {
372
+ console.log(chalk.yellow(`Out-of-sync/Dirty shots detected: ${dirty.join(', ')}`));
373
+ console.log(chalk.yellow('Re-run compile-prompts (and re-generate if needed).'));
374
+ }
375
+ }
376
+
377
+ const requiredDirs = [
378
+ schema.project?.specsDir ?? 'specs',
379
+ schema.project?.storyboardDir ?? 'storyboard',
380
+ schema.project?.promptsDir ?? 'prompts',
381
+ schema.project?.outputDir ?? 'output'
382
+ ];
383
+
384
+ for (const d of requiredDirs) {
385
+ if (!(await pathExists(path.join(root, d)))) {
386
+ throw new Error(`Missing required directory: ${path.join(root, d)}`);
387
+ }
388
+ }
389
+
390
+ console.log(chalk.green('Validation passed (dry-run).'));
391
+ }
392
+
393
+ async function cmdGenerateImage({ projectRoot, shot }) {
394
+ const schema = await loadMovieSpecSchema();
395
+ const root = resolveProjectRoot(projectRoot);
396
+ const config = await readProjectConfig(root, schema);
397
+ if (!config) throw new Error('Missing config.yaml');
398
+
399
+ const promptsDir = path.join(root, schema.project?.promptsDir ?? 'prompts');
400
+ const promptPath = path.join(promptsDir, `${shot}.prompt.json`);
401
+ if (!(await pathExists(promptPath))) {
402
+ throw new Error(`Missing compiled prompt for shot ${shot}. Run compile-prompts first.`);
403
+ }
404
+
405
+ const compiled = await readJsonFile(promptPath);
406
+
407
+ const outputRoot = path.join(root, schema.project?.outputDir ?? 'output');
408
+ const shotsDir = path.join(outputRoot, schema.project?.shotsDir ?? 'shots');
409
+ await ensureDir(shotsDir);
410
+
411
+ const runId = new Date().toISOString().replace(/[:.]/g, '-');
412
+ const shotRunDir = path.join(shotsDir, String(shot), `run_${runId}`);
413
+ await ensureDir(shotRunDir);
414
+
415
+ const manifest = {
416
+ shot_id: shot,
417
+ backend: config.backend ?? { id: 'stub' },
418
+ tool: { id: 'moviespec', version: '0.1.0' },
419
+ created_at: new Date().toISOString(),
420
+ prompt: compiled.prompt,
421
+ media: compiled.media,
422
+ deps: compiled.deps,
423
+ lora: config.lora ?? { characters: {}, styles: {} },
424
+ outputs: {
425
+ placeholder: 'stub'
426
+ }
427
+ };
428
+
429
+ await writeJsonFile(path.join(shotRunDir, 'manifest.json'), manifest);
430
+ await fs.writeFile(path.join(shotRunDir, 'OUTPUT_STUB.txt'), 'stub output\n', 'utf8');
431
+
432
+ console.log(chalk.green(`Generated stub output for shot ${shot}: ${toPosix(path.relative(root, shotRunDir))}`));
433
+ }
434
+
435
+ async function cmdGenerateSequence({ projectRoot, shots }) {
436
+ const schema = await loadMovieSpecSchema();
437
+ const root = resolveProjectRoot(projectRoot);
438
+ const storyboardPath = path.resolve(root, shots);
439
+ if (!(await pathExists(storyboardPath))) throw new Error(`Shots file not found: ${storyboardPath}`);
440
+
441
+ const data = await readStoryboardShots(storyboardPath);
442
+ const shotList = normalizeShots(data);
443
+ let count = 0;
444
+ for (const s of shotList) {
445
+ const shotId = s.id ?? s.shot_id;
446
+ if (!shotId) continue;
447
+ await cmdGenerateImage({ projectRoot: root, shot: String(shotId) });
448
+ count += 1;
449
+ }
450
+ console.log(chalk.green(`Generated stub sequence for ${count} shot(s).`));
451
+ }
452
+
453
+ const program = new Command();
454
+ program.name('moviespec').description('MovieSpec CLI').version('0.1.0');
455
+
456
+ program
457
+ .command('init')
458
+ .option('-C, --project-root <path>', 'Project root (default: cwd)')
459
+ .action(async (opts) => {
460
+ await cmdInit(opts.projectRoot);
461
+ });
462
+
463
+ program
464
+ .command('list')
465
+ .description('List resources')
466
+ .command('types')
467
+ .action(async () => {
468
+ await cmdListTypes();
469
+ });
470
+
471
+ program
472
+ .command('new')
473
+ .argument('<type>', 'Type id')
474
+ .argument('<name>', 'Document name')
475
+ .option('-C, --project-root <path>', 'Project root (default: cwd)')
476
+ .action(async (type, name, opts) => {
477
+ await cmdNew(type, name, { projectRoot: opts.projectRoot });
478
+ });
479
+
480
+ program
481
+ .command('compile-prompts')
482
+ .requiredOption('--shots <file>', 'Storyboard shots file (yaml/json)')
483
+ .option('-C, --project-root <path>', 'Project root (default: cwd)')
484
+ .action(async (opts) => {
485
+ await cmdCompilePrompts({ shots: opts.shots, projectRoot: opts.projectRoot });
486
+ });
487
+
488
+ program
489
+ .command('validate')
490
+ .option('--shots <file>', 'Storyboard shots file (optional)')
491
+ .option('-C, --project-root <path>', 'Project root (default: cwd)')
492
+ .action(async (opts) => {
493
+ await cmdValidate({ projectRoot: opts.projectRoot, shots: opts.shots });
494
+ });
495
+
496
+ program
497
+ .command('check')
498
+ .option('--shots <file>', 'Storyboard shots file (optional)')
499
+ .option('-C, --project-root <path>', 'Project root (default: cwd)')
500
+ .action(async (opts) => {
501
+ await cmdValidate({ projectRoot: opts.projectRoot, shots: opts.shots });
502
+ });
503
+
504
+ const generate = program.command('generate').description('Generate assets');
505
+
506
+ generate
507
+ .command('image')
508
+ .requiredOption('--shot <id>', 'Shot id')
509
+ .option('-C, --project-root <path>', 'Project root (default: cwd)')
510
+ .action(async (opts) => {
511
+ await cmdGenerateImage({ projectRoot: opts.projectRoot, shot: opts.shot });
512
+ });
513
+
514
+ generate
515
+ .command('sequence')
516
+ .requiredOption('--shots <file>', 'Storyboard shots file (yaml/json)')
517
+ .option('-C, --project-root <path>', 'Project root (default: cwd)')
518
+ .action(async (opts) => {
519
+ await cmdGenerateSequence({ projectRoot: opts.projectRoot, shots: opts.shots });
520
+ });
521
+
522
+ program.parseAsync(process.argv).catch((err) => {
523
+ console.error(chalk.red(err?.message ?? String(err)));
524
+ process.exitCode = 1;
525
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@gthanks/moviespec",
3
+ "version": "0.1.0",
4
+ "description": "Specification-driven consistency framework for AI image and video generation",
5
+ "author": "gthanks",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "schema",
13
+ "templates"
14
+ ],
15
+ "type": "module",
16
+ "bin": {
17
+ "moviespec": "./bin/moviespec.js"
18
+ },
19
+ "engines": {
20
+ "node": ">=20.19.0"
21
+ },
22
+ "dependencies": {
23
+ "@inquirer/prompts": "^8.3.0",
24
+ "chalk": "^5.5.0",
25
+ "commander": "^14.0.0",
26
+ "fast-glob": "^3.3.3",
27
+ "yaml": "^2.8.2"
28
+ }
29
+ }
@@ -0,0 +1,55 @@
1
+ version: 0.1.0
2
+ id: manifest
3
+ format: json
4
+ required:
5
+ - shot_id
6
+ - created_at
7
+ - backend
8
+ - tool
9
+ - prompt
10
+ - media
11
+ - deps
12
+ properties:
13
+ shot_id:
14
+ type: string
15
+ created_at:
16
+ type: string
17
+ backend:
18
+ type: object
19
+ required: [id]
20
+ properties:
21
+ id: { type: string }
22
+ params: { type: object }
23
+ tool:
24
+ type: object
25
+ required: [id, version]
26
+ properties:
27
+ id: { type: string }
28
+ version: { type: string }
29
+ prompt:
30
+ type: object
31
+ required: [positive, negative]
32
+ properties:
33
+ positive: { type: string }
34
+ negative: { type: string }
35
+ media:
36
+ type: object
37
+ properties:
38
+ resolution: { type: string }
39
+ aspect_ratio: { type: string }
40
+ fps: { type: number }
41
+ deps:
42
+ type: object
43
+ lora:
44
+ type: object
45
+ properties:
46
+ characters: { type: object }
47
+ styles: { type: object }
48
+ sources:
49
+ type: object
50
+ properties:
51
+ compiled_prompt: { type: string }
52
+ storyboard: { type: string }
53
+ specs: { type: object }
54
+ outputs:
55
+ type: object
@@ -0,0 +1,56 @@
1
+ version: 0.1.0
2
+ project:
3
+ config: config.yaml
4
+ specsDir: specs
5
+ storyboardDir: storyboard
6
+ promptsDir: prompts
7
+ outputDir: output
8
+ manifestsDir: manifests
9
+ shotsDir: shots
10
+ types:
11
+ world_bible:
12
+ description: 世界观
13
+ defaultPath: specs/world_bible
14
+ template: templates/world_bible.md
15
+ dependsOn: []
16
+ characters:
17
+ description: 人物设定
18
+ defaultPath: specs/characters
19
+ template: templates/characters.md
20
+ dependsOn: []
21
+ visual_style:
22
+ description: 视觉风格
23
+ defaultPath: specs/visual_style
24
+ template: templates/visual_style.md
25
+ dependsOn: []
26
+ cinematography:
27
+ description: 镜头语言
28
+ defaultPath: specs/cinematography
29
+ template: templates/cinematography.md
30
+ dependsOn: []
31
+ shot_template:
32
+ description: 分镜模板
33
+ defaultPath: specs/shot_template
34
+ template: templates/shot_template.md
35
+ dependsOn:
36
+ - characters
37
+ - visual_style
38
+ - cinematography
39
+ prompt_guide:
40
+ description: AI Prompt 规范
41
+ defaultPath: specs/prompt_guide
42
+ template: templates/prompt_guide.md
43
+ dependsOn:
44
+ - world_bible
45
+ - visual_style
46
+ compile:
47
+ prompt:
48
+ priority:
49
+ - shot
50
+ - characters
51
+ - cinematography
52
+ - visual_style
53
+ - world_bible
54
+ - prompt_guide
55
+ maxTokens:
56
+ stub: null
@@ -0,0 +1,25 @@
1
+ version: 0.1.0
2
+ id: requirements
3
+ format: yaml
4
+ required:
5
+ - project
6
+ - logline
7
+ properties:
8
+ project: { type: string }
9
+ logline: { type: string }
10
+ style_keywords:
11
+ type: array
12
+ items: { type: string }
13
+ characters:
14
+ type: array
15
+ items:
16
+ type: object
17
+ required: [name]
18
+ properties:
19
+ name: { type: string }
20
+ description: { type: string }
21
+ constraints:
22
+ type: object
23
+ properties:
24
+ language: { type: string }
25
+ target: { type: string }
@@ -0,0 +1,37 @@
1
+ version: 0.1.0
2
+ id: storyboard
3
+ format: yaml_or_json
4
+ shots:
5
+ required:
6
+ - id
7
+ properties:
8
+ id: { type: string }
9
+ characters:
10
+ type: array
11
+ items: { type: string }
12
+ scene: { type: string }
13
+ scene_context:
14
+ type: object
15
+ properties:
16
+ camera: { type: string, description: "摄像机运动,如 Dolly, Pan, Tilt" }
17
+ lighting: { type: string, description: "环境光效" }
18
+ assets:
19
+ type: object
20
+ properties:
21
+ seed: { type: integer, description: "一致性种子" }
22
+ face_id: { type: string, description: "角色面部特征 ID" }
23
+ reference_image: { type: string, description: "参考图 URL 或路径" }
24
+ action: { type: string }
25
+ composition: { type: string }
26
+ duration_s: { type: number }
27
+ media:
28
+ type: object
29
+ properties:
30
+ resolution: { type: string }
31
+ aspect_ratio: { type: string }
32
+ fps: { type: number }
33
+ prompt:
34
+ type: object
35
+ properties:
36
+ positive: { type: string }
37
+ negative: { type: string }
@@ -0,0 +1,23 @@
1
+ version: 0.1.0
2
+ id: validator_contract
3
+ input:
4
+ required:
5
+ - manifest
6
+ properties:
7
+ manifest:
8
+ type: object
9
+ references:
10
+ type: object
11
+ description: Optional references such as spec snapshots or reference images.
12
+ rules:
13
+ type: array
14
+ items: { type: string }
15
+ description: Specific evaluation rules or VQA prompts for the multi-modal LLM to check.
16
+ output:
17
+ required:
18
+ - ok
19
+ - report
20
+ properties:
21
+ ok: { type: boolean }
22
+ score: { type: number }
23
+ report: { type: object }
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Assistant guidelines for working with MovieSpec projects
3
+ ---
4
+
5
+ # MovieSpec Guidelines
6
+
7
+ You are Antigravity, and you are operating within a MovieSpec project workspace. MovieSpec projects enforce a specific architecture to guarantee consistency across AI image and video generation tasks.
8
+
9
+ - **Schemas and Templates**: Always adhere to the project's YAML schemas. When generating markdown for characters or shots, include the required YAML frontmatter (like `seed`, `face_id`, `reference_image`).
10
+ - **Consistency**: Maintain continuity of `scene_context` across sequences of shots.
11
+ - **Workflow Tools**: You have access to `moviespec` CLI commands. If you modify a storyboard, run `moviespec compile-prompts --shots <your_file.yaml>`.
@@ -0,0 +1,7 @@
1
+ # MovieSpec Guidelines
2
+
3
+ You are assisting with a MovieSpec project. MovieSpec enforces consistency in AI image and video generation through predictable YAML schemas.
4
+
5
+ 1. **Schemas and Templates**: When the user wants to create a new character, shot, or world bible, DO NOT do it arbitrarily. Read the corresponding schema in `schema/` or use the templates in `templates/` to guide your outputs. Structure everything in valid YAML or Markdown with YAML frontmatter.
6
+ 2. **Shot Consistency**: When creating a `storyboard.schema.yaml` or a `.prompt.json` shot, always ensure `assets` (seed, face_id, reference_image) and `scene_context` (camera, lighting) are maintained and passed between related shots to ensure visual consistency.
7
+ 3. **Compilation**: After saving or modifying a storyboard, remind the user to run `moviespec compile-prompts --shots <file>` to regenerate prompt outputs.
@@ -0,0 +1,7 @@
1
+ # MovieSpec Guidelines
2
+
3
+ You are an AI agent operating in a MovieSpec project workspace. Your primary task is to generate and maintain consistent AI video generation documents.
4
+
5
+ 1. **Schemas and Templates**: When the user wants to create a new character, shot, or world bible, DO NOT do it arbitrarily. Read the corresponding schema in `schema/` or use the templates in `templates/` to guide your outputs. Structure everything in valid YAML or Markdown with YAML frontmatter.
6
+ 2. **Shot Consistency**: When creating a `storyboard.schema.yaml` or a `.prompt.json` shot, always ensure `assets` (seed, face_id, reference_image) and `scene_context` (camera, lighting) are maintained and passed between related shots to ensure visual consistency.
7
+ 3. **Compilation**: After saving or modifying a storyboard, automatically run `moviespec compile-prompts --shots <file>` for the user if necessary.
@@ -0,0 +1,19 @@
1
+ ---
2
+ type: characters
3
+ name: "{{name}}"
4
+ seed:
5
+ face_id: ""
6
+ reference_image: ""
7
+ ---
8
+
9
+ # 角色:{{name}}
10
+
11
+ ## 外观
12
+
13
+ ## 服饰与配色
14
+
15
+ ## 性格与动机
16
+
17
+ ## 关键标识
18
+
19
+ ## LoRA(可选)
@@ -0,0 +1,14 @@
1
+ ---
2
+ type: cinematography
3
+ name: "{{name}}"
4
+ ---
5
+
6
+ # 镜头语言:{{name}}
7
+
8
+ ## 景别
9
+
10
+ ## 镜头运动
11
+
12
+ ## 构图原则
13
+
14
+ ## 节奏与剪辑偏好
@@ -0,0 +1,12 @@
1
+ ---
2
+ type: prompt_guide
3
+ name: "{{name}}"
4
+ ---
5
+
6
+ # Prompt 规范:{{name}}
7
+
8
+ ## 组装优先级
9
+
10
+ ## 长度控制策略
11
+
12
+ ## 通用负面词(可选)
@@ -0,0 +1,18 @@
1
+ ---
2
+ type: shot_template
3
+ name: "{{name}}"
4
+ ---
5
+
6
+ # 分镜模板:{{name}}
7
+
8
+ ## 每镜字段建议
9
+
10
+ - id
11
+ - characters
12
+ - scene
13
+ - scene_context (camera/lighting)
14
+ - action
15
+ - composition
16
+ - assets (seed/face_id/reference_image)
17
+ - media (resolution/aspect_ratio/fps 可选覆盖)
18
+ - prompt (positive/negative)
@@ -0,0 +1,14 @@
1
+ ---
2
+ type: visual_style
3
+ name: "{{name}}"
4
+ ---
5
+
6
+ # 视觉风格:{{name}}
7
+
8
+ ## 参考风格关键词
9
+
10
+ ## 光影与色彩
11
+
12
+ ## 材质与细节
13
+
14
+ ## 禁用元素
@@ -0,0 +1,14 @@
1
+ ---
2
+ type: world_bible
3
+ name: "{{name}}"
4
+ ---
5
+
6
+ # 世界观:{{name}}
7
+
8
+ ## 核心设定
9
+
10
+ ## 时代与地域
11
+
12
+ ## 规则与禁忌
13
+
14
+ ## 关键道具/势力