@nebutra/video-pipeline 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/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-test.log +14 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +676 -0
- package/README.md +20 -0
- package/dist/chunk-YOKTCRLB.js +143 -0
- package/dist/chunk-YOKTCRLB.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/examples/cost.ts +10 -0
- package/examples/plan.ts +9 -0
- package/examples/preview.ts +10 -0
- package/package.json +62 -0
- package/src/cli.ts +31 -0
- package/src/index.test.ts +34 -0
- package/src/index.ts +180 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @nebutra/video-pipeline
|
|
2
|
+
|
|
3
|
+
Status: WIP — Not yet integrated into any production app.
|
|
4
|
+
|
|
5
|
+
`@nebutra/video-pipeline` is the storyboard-first video capability surface.
|
|
6
|
+
Every render call requires BrandContext and a storyboard. The zero-config path
|
|
7
|
+
writes deterministic JSON preview/composition manifests; model-backed clip
|
|
8
|
+
generation and ffmpeg composition are sidecar ports.
|
|
9
|
+
|
|
10
|
+
It does not own prompt orchestration, Thread/Turn/Item state, model provider
|
|
11
|
+
routing, or approval lifecycle.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm video:doctor
|
|
17
|
+
pnpm video:debug
|
|
18
|
+
pnpm video:cost
|
|
19
|
+
pnpm video:preview
|
|
20
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
|
|
5
|
+
import {
|
|
6
|
+
assetId,
|
|
7
|
+
requireBrandContext,
|
|
8
|
+
summarizeBrandContext
|
|
9
|
+
} from "@nebutra/generation-context";
|
|
10
|
+
import {
|
|
11
|
+
storyboardTotalDuration
|
|
12
|
+
} from "@nebutra/reel/storyboard";
|
|
13
|
+
async function readVideoDebug(root = process.cwd(), limit = 10) {
|
|
14
|
+
return readCapabilityDebug("video-pipeline", { root, limit });
|
|
15
|
+
}
|
|
16
|
+
function sceneCount(durationS) {
|
|
17
|
+
return Math.max(1, Math.min(8, Math.ceil(durationS / 8)));
|
|
18
|
+
}
|
|
19
|
+
var VideoPipeline = class {
|
|
20
|
+
#root;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.#root = options.root ?? process.cwd();
|
|
23
|
+
}
|
|
24
|
+
async plan(intent, brandInput) {
|
|
25
|
+
const brand = requireBrandContext(brandInput, "video-pipeline");
|
|
26
|
+
const count = sceneCount(intent.durationS);
|
|
27
|
+
const baseDuration = Math.max(1, Math.floor(intent.durationS / count));
|
|
28
|
+
const scenes = Array.from({ length: count }, (_, index) => {
|
|
29
|
+
const sceneNumber = index + 1;
|
|
30
|
+
const durationS = index === count - 1 ? intent.durationS - baseDuration * index : baseDuration;
|
|
31
|
+
const prompt = `${brand.name} ${intent.type} scene ${sceneNumber}: ${summarizeBrandContext(brand)}`;
|
|
32
|
+
return {
|
|
33
|
+
id: `scene_${sceneNumber}`,
|
|
34
|
+
sceneIndex: sceneNumber,
|
|
35
|
+
prompt,
|
|
36
|
+
description: prompt,
|
|
37
|
+
duration: durationS,
|
|
38
|
+
durationS,
|
|
39
|
+
status: "draft",
|
|
40
|
+
outputEnabled: true,
|
|
41
|
+
selectedImageIndex: -1,
|
|
42
|
+
outputHistory: [],
|
|
43
|
+
outputHistoryCursor: -1,
|
|
44
|
+
referenceImages: brand.referenceImages.map((reference) => reference.path),
|
|
45
|
+
transition: index === 0 ? "cut" : "fade",
|
|
46
|
+
musicCue: brand.toneKeywords.join(", "),
|
|
47
|
+
voiceCue: sceneNumber === 1 ? "introduce the product promise" : "advance the story"
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
id: assetId("storyboard", `${intent.type}_${brand.brandId}`),
|
|
52
|
+
tenantId: brand.tenantId,
|
|
53
|
+
brandId: brand.brandId,
|
|
54
|
+
intent,
|
|
55
|
+
scenes,
|
|
56
|
+
totalDurationS: storyboardTotalDuration(scenes)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async render(storyboard, brandInput) {
|
|
60
|
+
const brand = requireBrandContext(brandInput, "video-pipeline");
|
|
61
|
+
const id = assetId("video", storyboard.id);
|
|
62
|
+
const path = join(this.#root, ".nebutra", "generated", "video-pipeline", `${id}.video.json`);
|
|
63
|
+
await mkdir(dirname(path), { recursive: true });
|
|
64
|
+
await writeFile(
|
|
65
|
+
path,
|
|
66
|
+
`${JSON.stringify({ storyboard, brand: summarizeBrandContext(brand), renderer: "manifest-local" }, null, 2)}
|
|
67
|
+
`,
|
|
68
|
+
"utf8"
|
|
69
|
+
);
|
|
70
|
+
const asset = {
|
|
71
|
+
id,
|
|
72
|
+
tenantId: brand.tenantId,
|
|
73
|
+
kind: "video",
|
|
74
|
+
path,
|
|
75
|
+
brandId: brand.brandId,
|
|
76
|
+
provider: "manifest-local",
|
|
77
|
+
model: "storyboard-manifest-v1",
|
|
78
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
79
|
+
license: { status: "commercial-ok", source: "deterministic local manifest" },
|
|
80
|
+
durationS: storyboard.totalDurationS,
|
|
81
|
+
format: "manifest-json",
|
|
82
|
+
storyboardId: storyboard.id,
|
|
83
|
+
metadata: { scenes: storyboard.scenes.length }
|
|
84
|
+
};
|
|
85
|
+
await appendCapabilityDebug("video-pipeline", { type: "render", asset }, { root: this.#root });
|
|
86
|
+
return asset;
|
|
87
|
+
}
|
|
88
|
+
async create(intent, brand) {
|
|
89
|
+
return this.render(await this.plan(intent, brand), brand);
|
|
90
|
+
}
|
|
91
|
+
async compose(output) {
|
|
92
|
+
const first = output.videoTrack[0];
|
|
93
|
+
if (!first) {
|
|
94
|
+
throw new Error("compose requires at least one video track");
|
|
95
|
+
}
|
|
96
|
+
const id = assetId("composition", first.id);
|
|
97
|
+
const path = join(this.#root, ".nebutra", "generated", "video-pipeline", `${id}.compose.json`);
|
|
98
|
+
await mkdir(dirname(path), { recursive: true });
|
|
99
|
+
await writeFile(path, `${JSON.stringify(output, null, 2)}
|
|
100
|
+
`, "utf8");
|
|
101
|
+
return { ...first, id, path, storyboardId: first.storyboardId };
|
|
102
|
+
}
|
|
103
|
+
async cost(storyboard) {
|
|
104
|
+
return {
|
|
105
|
+
estimatedUsd: Number((storyboard.totalDurationS * 3e-3).toFixed(4)),
|
|
106
|
+
durationS: storyboard.totalDurationS
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async preview(storyboard) {
|
|
110
|
+
const path = join(
|
|
111
|
+
this.#root,
|
|
112
|
+
".nebutra",
|
|
113
|
+
"generated",
|
|
114
|
+
"video-pipeline",
|
|
115
|
+
`${storyboard.id}.preview.json`
|
|
116
|
+
);
|
|
117
|
+
await mkdir(dirname(path), { recursive: true });
|
|
118
|
+
await writeFile(path, `${JSON.stringify(storyboard, null, 2)}
|
|
119
|
+
`, "utf8");
|
|
120
|
+
return { path };
|
|
121
|
+
}
|
|
122
|
+
async doctor() {
|
|
123
|
+
return [
|
|
124
|
+
{ provider: "manifest-local", ok: true },
|
|
125
|
+
{
|
|
126
|
+
provider: "local-video-model",
|
|
127
|
+
ok: Boolean(process.env.VIDEO_LOCAL_MODEL_PATH),
|
|
128
|
+
suggestion: "Set VIDEO_LOCAL_MODEL_PATH to enable local clip rendering."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
provider: "remote-video",
|
|
132
|
+
ok: Boolean(process.env.VIDEO_REMOTE_API_KEY),
|
|
133
|
+
suggestion: "Set VIDEO_REMOTE_API_KEY to enable remote video fallback."
|
|
134
|
+
}
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
readVideoDebug,
|
|
141
|
+
VideoPipeline
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=chunk-YOKTCRLB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { appendCapabilityDebug, readCapabilityDebug } from \"@nebutra/capability-kit/debug\";\nimport {\n assetId,\n type BrandContext,\n type GeneratedAsset,\n requireBrandContext,\n summarizeBrandContext,\n} from \"@nebutra/generation-context\";\nimport {\n type StoryboardPlan,\n type StoryboardScene,\n storyboardTotalDuration,\n} from \"@nebutra/reel/storyboard\";\n\nexport type VideoIntent =\n | { type: \"brand-film\"; durationS: number; theme: string }\n | { type: \"turntable\"; durationS: number; subject: string }\n | { type: \"clip\"; durationS: number; prompt: string };\n\nexport type Storyboard = StoryboardPlan<VideoIntent>;\n\nexport interface VideoAsset extends GeneratedAsset {\n readonly kind: \"video\";\n readonly durationS: number;\n readonly format: \"manifest-json\" | \"mp4\";\n readonly storyboardId: string;\n}\n\nexport interface SyncedVideoOutput {\n readonly videoTrack: readonly VideoAsset[];\n readonly audioTrack?: readonly GeneratedAsset[];\n readonly voiceTrack?: readonly GeneratedAsset[];\n readonly captionTrack?: readonly { startS: number; endS: number; text: string }[];\n}\n\nexport interface VideoHealth {\n readonly provider: string;\n readonly ok: boolean;\n readonly suggestion?: string;\n}\n\nexport interface VideoPipelineOptions {\n readonly root?: string;\n}\n\nexport async function readVideoDebug(root = process.cwd(), limit = 10): Promise<unknown[]> {\n return readCapabilityDebug(\"video-pipeline\", { root, limit });\n}\n\nfunction sceneCount(durationS: number): number {\n return Math.max(1, Math.min(8, Math.ceil(durationS / 8)));\n}\n\nexport class VideoPipeline {\n readonly #root: string;\n\n constructor(options: VideoPipelineOptions = {}) {\n this.#root = options.root ?? process.cwd();\n }\n\n async plan(intent: VideoIntent, brandInput: BrandContext | undefined): Promise<Storyboard> {\n const brand = requireBrandContext(brandInput, \"video-pipeline\");\n const count = sceneCount(intent.durationS);\n const baseDuration = Math.max(1, Math.floor(intent.durationS / count));\n const scenes = Array.from({ length: count }, (_, index): StoryboardScene => {\n const sceneNumber = index + 1;\n const durationS =\n index === count - 1 ? intent.durationS - baseDuration * index : baseDuration;\n const prompt = `${brand.name} ${intent.type} scene ${sceneNumber}: ${summarizeBrandContext(brand)}`;\n return {\n id: `scene_${sceneNumber}`,\n sceneIndex: sceneNumber,\n prompt,\n description: prompt,\n duration: durationS,\n durationS,\n status: \"draft\",\n outputEnabled: true,\n selectedImageIndex: -1,\n outputHistory: [],\n outputHistoryCursor: -1,\n referenceImages: brand.referenceImages.map((reference) => reference.path),\n transition: index === 0 ? \"cut\" : \"fade\",\n musicCue: brand.toneKeywords.join(\", \"),\n voiceCue: sceneNumber === 1 ? \"introduce the product promise\" : \"advance the story\",\n };\n });\n return {\n id: assetId(\"storyboard\", `${intent.type}_${brand.brandId}`),\n tenantId: brand.tenantId,\n brandId: brand.brandId,\n intent,\n scenes,\n totalDurationS: storyboardTotalDuration(scenes),\n };\n }\n\n async render(storyboard: Storyboard, brandInput: BrandContext | undefined): Promise<VideoAsset> {\n const brand = requireBrandContext(brandInput, \"video-pipeline\");\n const id = assetId(\"video\", storyboard.id);\n const path = join(this.#root, \".nebutra\", \"generated\", \"video-pipeline\", `${id}.video.json`);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(\n path,\n `${JSON.stringify({ storyboard, brand: summarizeBrandContext(brand), renderer: \"manifest-local\" }, null, 2)}\\n`,\n \"utf8\",\n );\n const asset: VideoAsset = {\n id,\n tenantId: brand.tenantId,\n kind: \"video\",\n path,\n brandId: brand.brandId,\n provider: \"manifest-local\",\n model: \"storyboard-manifest-v1\",\n createdAt: new Date().toISOString(),\n license: { status: \"commercial-ok\", source: \"deterministic local manifest\" },\n durationS: storyboard.totalDurationS,\n format: \"manifest-json\",\n storyboardId: storyboard.id,\n metadata: { scenes: storyboard.scenes.length },\n };\n await appendCapabilityDebug(\"video-pipeline\", { type: \"render\", asset }, { root: this.#root });\n return asset;\n }\n\n async create(intent: VideoIntent, brand: BrandContext | undefined): Promise<VideoAsset> {\n return this.render(await this.plan(intent, brand), brand);\n }\n\n async compose(output: SyncedVideoOutput): Promise<VideoAsset> {\n const first = output.videoTrack[0];\n if (!first) {\n throw new Error(\"compose requires at least one video track\");\n }\n const id = assetId(\"composition\", first.id);\n const path = join(this.#root, \".nebutra\", \"generated\", \"video-pipeline\", `${id}.compose.json`);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(output, null, 2)}\\n`, \"utf8\");\n return { ...first, id, path, storyboardId: first.storyboardId };\n }\n\n async cost(storyboard: Storyboard): Promise<{ estimatedUsd: number; durationS: number }> {\n return {\n estimatedUsd: Number((storyboard.totalDurationS * 0.003).toFixed(4)),\n durationS: storyboard.totalDurationS,\n };\n }\n\n async preview(storyboard: Storyboard): Promise<{ path: string }> {\n const path = join(\n this.#root,\n \".nebutra\",\n \"generated\",\n \"video-pipeline\",\n `${storyboard.id}.preview.json`,\n );\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(storyboard, null, 2)}\\n`, \"utf8\");\n return { path };\n }\n\n async doctor(): Promise<VideoHealth[]> {\n return [\n { provider: \"manifest-local\", ok: true },\n {\n provider: \"local-video-model\",\n ok: Boolean(process.env.VIDEO_LOCAL_MODEL_PATH),\n suggestion: \"Set VIDEO_LOCAL_MODEL_PATH to enable local clip rendering.\",\n },\n {\n provider: \"remote-video\",\n ok: Boolean(process.env.VIDEO_REMOTE_API_KEY),\n suggestion: \"Set VIDEO_REMOTE_API_KEY to enable remote video fallback.\",\n },\n ];\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VideoPipeline,
|
|
3
|
+
readVideoDebug
|
|
4
|
+
} from "./chunk-YOKTCRLB.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
8
|
+
var command = process.argv[2] ?? "doctor";
|
|
9
|
+
var pipeline = new VideoPipeline();
|
|
10
|
+
var brand = createDemoBrandContext();
|
|
11
|
+
var storyboard = await pipeline.plan(
|
|
12
|
+
{ type: "brand-film", durationS: 16, theme: "indie developer journey" },
|
|
13
|
+
brand
|
|
14
|
+
);
|
|
15
|
+
if (command === "doctor") {
|
|
16
|
+
process.stdout.write(
|
|
17
|
+
`${JSON.stringify({ capability: "video-pipeline", results: await pipeline.doctor() }, null, 2)}
|
|
18
|
+
`
|
|
19
|
+
);
|
|
20
|
+
} else if (command === "debug") {
|
|
21
|
+
process.stdout.write(
|
|
22
|
+
`${JSON.stringify({ capability: "video-pipeline", entries: await readVideoDebug() }, null, 2)}
|
|
23
|
+
`
|
|
24
|
+
);
|
|
25
|
+
} else if (command === "cost") {
|
|
26
|
+
process.stdout.write(
|
|
27
|
+
`${JSON.stringify({ capability: "video-pipeline", cost: await pipeline.cost(storyboard) }, null, 2)}
|
|
28
|
+
`
|
|
29
|
+
);
|
|
30
|
+
} else if (command === "preview") {
|
|
31
|
+
process.stdout.write(
|
|
32
|
+
`${JSON.stringify({ capability: "video-pipeline", preview: await pipeline.preview(storyboard) }, null, 2)}
|
|
33
|
+
`
|
|
34
|
+
);
|
|
35
|
+
} else {
|
|
36
|
+
process.stderr.write(`Unknown video-pipeline command: ${command}
|
|
37
|
+
`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { createDemoBrandContext } from \"@nebutra/generation-context\";\nimport { readVideoDebug, VideoPipeline } from \"./index\";\n\nconst command = process.argv[2] ?? \"doctor\";\nconst pipeline = new VideoPipeline();\nconst brand = createDemoBrandContext();\nconst storyboard = await pipeline.plan(\n { type: \"brand-film\", durationS: 16, theme: \"indie developer journey\" },\n brand,\n);\n\nif (command === \"doctor\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"video-pipeline\", results: await pipeline.doctor() }, null, 2)}\\n`,\n );\n} else if (command === \"debug\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"video-pipeline\", entries: await readVideoDebug() }, null, 2)}\\n`,\n );\n} else if (command === \"cost\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"video-pipeline\", cost: await pipeline.cost(storyboard) }, null, 2)}\\n`,\n );\n} else if (command === \"preview\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"video-pipeline\", preview: await pipeline.preview(storyboard) }, null, 2)}\\n`,\n );\n} else {\n process.stderr.write(`Unknown video-pipeline command: ${command}\\n`);\n process.exitCode = 1;\n}\n"],"mappings":";;;;;;AAAA,SAAS,8BAA8B;AAGvC,IAAM,UAAU,QAAQ,KAAK,CAAC,KAAK;AACnC,IAAM,WAAW,IAAI,cAAc;AACnC,IAAM,QAAQ,uBAAuB;AACrC,IAAM,aAAa,MAAM,SAAS;AAAA,EAChC,EAAE,MAAM,cAAc,WAAW,IAAI,OAAO,0BAA0B;AAAA,EACtE;AACF;AAEA,IAAI,YAAY,UAAU;AACxB,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,kBAAkB,SAAS,MAAM,SAAS,OAAO,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EAChG;AACF,WAAW,YAAY,SAAS;AAC9B,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,kBAAkB,SAAS,MAAM,eAAe,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EAC/F;AACF,WAAW,YAAY,QAAQ;AAC7B,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,kBAAkB,MAAM,MAAM,SAAS,KAAK,UAAU,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EACrG;AACF,WAAW,YAAY,WAAW;AAChC,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,kBAAkB,SAAS,MAAM,SAAS,QAAQ,UAAU,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EAC3G;AACF,OAAO;AACL,UAAQ,OAAO,MAAM,mCAAmC,OAAO;AAAA,CAAI;AACnE,UAAQ,WAAW;AACrB;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { GeneratedAsset, BrandContext } from '@nebutra/generation-context';
|
|
2
|
+
import { StoryboardPlan } from '@nebutra/reel/storyboard';
|
|
3
|
+
|
|
4
|
+
type VideoIntent = {
|
|
5
|
+
type: "brand-film";
|
|
6
|
+
durationS: number;
|
|
7
|
+
theme: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "turntable";
|
|
10
|
+
durationS: number;
|
|
11
|
+
subject: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: "clip";
|
|
14
|
+
durationS: number;
|
|
15
|
+
prompt: string;
|
|
16
|
+
};
|
|
17
|
+
type Storyboard = StoryboardPlan<VideoIntent>;
|
|
18
|
+
interface VideoAsset extends GeneratedAsset {
|
|
19
|
+
readonly kind: "video";
|
|
20
|
+
readonly durationS: number;
|
|
21
|
+
readonly format: "manifest-json" | "mp4";
|
|
22
|
+
readonly storyboardId: string;
|
|
23
|
+
}
|
|
24
|
+
interface SyncedVideoOutput {
|
|
25
|
+
readonly videoTrack: readonly VideoAsset[];
|
|
26
|
+
readonly audioTrack?: readonly GeneratedAsset[];
|
|
27
|
+
readonly voiceTrack?: readonly GeneratedAsset[];
|
|
28
|
+
readonly captionTrack?: readonly {
|
|
29
|
+
startS: number;
|
|
30
|
+
endS: number;
|
|
31
|
+
text: string;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
interface VideoHealth {
|
|
35
|
+
readonly provider: string;
|
|
36
|
+
readonly ok: boolean;
|
|
37
|
+
readonly suggestion?: string;
|
|
38
|
+
}
|
|
39
|
+
interface VideoPipelineOptions {
|
|
40
|
+
readonly root?: string;
|
|
41
|
+
}
|
|
42
|
+
declare function readVideoDebug(root?: string, limit?: number): Promise<unknown[]>;
|
|
43
|
+
declare class VideoPipeline {
|
|
44
|
+
#private;
|
|
45
|
+
constructor(options?: VideoPipelineOptions);
|
|
46
|
+
plan(intent: VideoIntent, brandInput: BrandContext | undefined): Promise<Storyboard>;
|
|
47
|
+
render(storyboard: Storyboard, brandInput: BrandContext | undefined): Promise<VideoAsset>;
|
|
48
|
+
create(intent: VideoIntent, brand: BrandContext | undefined): Promise<VideoAsset>;
|
|
49
|
+
compose(output: SyncedVideoOutput): Promise<VideoAsset>;
|
|
50
|
+
cost(storyboard: Storyboard): Promise<{
|
|
51
|
+
estimatedUsd: number;
|
|
52
|
+
durationS: number;
|
|
53
|
+
}>;
|
|
54
|
+
preview(storyboard: Storyboard): Promise<{
|
|
55
|
+
path: string;
|
|
56
|
+
}>;
|
|
57
|
+
doctor(): Promise<VideoHealth[]>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { type Storyboard, type SyncedVideoOutput, type VideoAsset, type VideoHealth, type VideoIntent, VideoPipeline, type VideoPipelineOptions, readVideoDebug };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/examples/cost.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
+
import { VideoPipeline } from "../src/index";
|
|
3
|
+
|
|
4
|
+
const pipeline = new VideoPipeline();
|
|
5
|
+
const storyboard = await pipeline.plan(
|
|
6
|
+
{ type: "brand-film", durationS: 60, theme: "debugging launch" },
|
|
7
|
+
createDemoBrandContext(),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
process.stdout.write(`${JSON.stringify(await pipeline.cost(storyboard), null, 2)}\n`);
|
package/examples/plan.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
+
import { VideoPipeline } from "../src/index";
|
|
3
|
+
|
|
4
|
+
const storyboard = await new VideoPipeline().plan(
|
|
5
|
+
{ type: "brand-film", durationS: 24, theme: "indie developer journey" },
|
|
6
|
+
createDemoBrandContext(),
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
process.stdout.write(`${JSON.stringify(storyboard.scenes, null, 2)}\n`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
+
import { VideoPipeline } from "../src/index";
|
|
3
|
+
|
|
4
|
+
const pipeline = new VideoPipeline();
|
|
5
|
+
const storyboard = await pipeline.plan(
|
|
6
|
+
{ type: "turntable", durationS: 8, subject: "product mockup" },
|
|
7
|
+
createDemoBrandContext(),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
process.stdout.write(`${JSON.stringify(await pipeline.preview(storyboard), null, 2)}\n`);
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebutra/video-pipeline",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Storyboard-first video planning, rendering manifests, and cost previews",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"nebutra": {
|
|
9
|
+
"status": "wip",
|
|
10
|
+
"productionReady": false,
|
|
11
|
+
"surface": "generation-capability",
|
|
12
|
+
"requires": [
|
|
13
|
+
"@nebutra/generation-context for BrandContext",
|
|
14
|
+
"@nebutra/image-pipeline for visual asset inputs",
|
|
15
|
+
"@nebutra/sandbox-runtime for video model and ffmpeg sidecars"
|
|
16
|
+
],
|
|
17
|
+
"gaps": [
|
|
18
|
+
"Model-backed clip rendering is adapter-gated",
|
|
19
|
+
"FFmpeg composition is represented as a deterministic manifest until the sandbox sidecar is wired",
|
|
20
|
+
"Remote provider quotas are health-checked through env only"
|
|
21
|
+
],
|
|
22
|
+
"featureId": "video-pipeline",
|
|
23
|
+
"category": "ai",
|
|
24
|
+
"summary": "Storyboard-first brand video planning, cost, preview, and composition surface"
|
|
25
|
+
},
|
|
26
|
+
"main": "./src/index.ts",
|
|
27
|
+
"types": "./src/index.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@nebutra/capability-kit": "0.2.0",
|
|
33
|
+
"@nebutra/errors": "0.1.0",
|
|
34
|
+
"@nebutra/generation-context": "0.1.0",
|
|
35
|
+
"@nebutra/image-pipeline": "0.1.0",
|
|
36
|
+
"@nebutra/reel": "1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.19.15",
|
|
40
|
+
"tsup": "^8.5.1",
|
|
41
|
+
"tsx": "^4.21.0",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"vitest": "^4.1.4"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/ai/video-pipeline#readme",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/Nebutra/Nebutra-Sailor.git",
|
|
49
|
+
"directory": "packages/ai/video-pipeline"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/Nebutra/Nebutra-Sailor/issues"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"typecheck": "tsc --noEmit"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
+
import { readVideoDebug, VideoPipeline } from "./index";
|
|
3
|
+
|
|
4
|
+
const command = process.argv[2] ?? "doctor";
|
|
5
|
+
const pipeline = new VideoPipeline();
|
|
6
|
+
const brand = createDemoBrandContext();
|
|
7
|
+
const storyboard = await pipeline.plan(
|
|
8
|
+
{ type: "brand-film", durationS: 16, theme: "indie developer journey" },
|
|
9
|
+
brand,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
if (command === "doctor") {
|
|
13
|
+
process.stdout.write(
|
|
14
|
+
`${JSON.stringify({ capability: "video-pipeline", results: await pipeline.doctor() }, null, 2)}\n`,
|
|
15
|
+
);
|
|
16
|
+
} else if (command === "debug") {
|
|
17
|
+
process.stdout.write(
|
|
18
|
+
`${JSON.stringify({ capability: "video-pipeline", entries: await readVideoDebug() }, null, 2)}\n`,
|
|
19
|
+
);
|
|
20
|
+
} else if (command === "cost") {
|
|
21
|
+
process.stdout.write(
|
|
22
|
+
`${JSON.stringify({ capability: "video-pipeline", cost: await pipeline.cost(storyboard) }, null, 2)}\n`,
|
|
23
|
+
);
|
|
24
|
+
} else if (command === "preview") {
|
|
25
|
+
process.stdout.write(
|
|
26
|
+
`${JSON.stringify({ capability: "video-pipeline", preview: await pipeline.preview(storyboard) }, null, 2)}\n`,
|
|
27
|
+
);
|
|
28
|
+
} else {
|
|
29
|
+
process.stderr.write(`Unknown video-pipeline command: ${command}\n`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { VideoPipeline } from "./index";
|
|
7
|
+
|
|
8
|
+
let root: string | undefined;
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
if (root) await rm(root, { recursive: true, force: true });
|
|
12
|
+
root = undefined;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("VideoPipeline", () => {
|
|
16
|
+
it("requires storyboard before render", async () => {
|
|
17
|
+
root = await mkdtemp(join(tmpdir(), "video-pipeline-"));
|
|
18
|
+
const pipeline = new VideoPipeline({ root });
|
|
19
|
+
const brand = createDemoBrandContext();
|
|
20
|
+
const storyboard = await pipeline.plan(
|
|
21
|
+
{ type: "brand-film", durationS: 20, theme: "launch" },
|
|
22
|
+
brand,
|
|
23
|
+
);
|
|
24
|
+
expect(storyboard.scenes.length).toBeGreaterThan(1);
|
|
25
|
+
const asset = await pipeline.render(storyboard, brand);
|
|
26
|
+
expect(await readFile(asset.path, "utf8")).toContain(storyboard.id);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("requires BrandContext", async () => {
|
|
30
|
+
await expect(
|
|
31
|
+
new VideoPipeline().plan({ type: "clip", durationS: 4, prompt: "ship" }, undefined),
|
|
32
|
+
).rejects.toMatchObject({ capability: "video-pipeline" });
|
|
33
|
+
});
|
|
34
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
|
|
4
|
+
import {
|
|
5
|
+
assetId,
|
|
6
|
+
type BrandContext,
|
|
7
|
+
type GeneratedAsset,
|
|
8
|
+
requireBrandContext,
|
|
9
|
+
summarizeBrandContext,
|
|
10
|
+
} from "@nebutra/generation-context";
|
|
11
|
+
import {
|
|
12
|
+
type StoryboardPlan,
|
|
13
|
+
type StoryboardScene,
|
|
14
|
+
storyboardTotalDuration,
|
|
15
|
+
} from "@nebutra/reel/storyboard";
|
|
16
|
+
|
|
17
|
+
export type VideoIntent =
|
|
18
|
+
| { type: "brand-film"; durationS: number; theme: string }
|
|
19
|
+
| { type: "turntable"; durationS: number; subject: string }
|
|
20
|
+
| { type: "clip"; durationS: number; prompt: string };
|
|
21
|
+
|
|
22
|
+
export type Storyboard = StoryboardPlan<VideoIntent>;
|
|
23
|
+
|
|
24
|
+
export interface VideoAsset extends GeneratedAsset {
|
|
25
|
+
readonly kind: "video";
|
|
26
|
+
readonly durationS: number;
|
|
27
|
+
readonly format: "manifest-json" | "mp4";
|
|
28
|
+
readonly storyboardId: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SyncedVideoOutput {
|
|
32
|
+
readonly videoTrack: readonly VideoAsset[];
|
|
33
|
+
readonly audioTrack?: readonly GeneratedAsset[];
|
|
34
|
+
readonly voiceTrack?: readonly GeneratedAsset[];
|
|
35
|
+
readonly captionTrack?: readonly { startS: number; endS: number; text: string }[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface VideoHealth {
|
|
39
|
+
readonly provider: string;
|
|
40
|
+
readonly ok: boolean;
|
|
41
|
+
readonly suggestion?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface VideoPipelineOptions {
|
|
45
|
+
readonly root?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readVideoDebug(root = process.cwd(), limit = 10): Promise<unknown[]> {
|
|
49
|
+
return readCapabilityDebug("video-pipeline", { root, limit });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sceneCount(durationS: number): number {
|
|
53
|
+
return Math.max(1, Math.min(8, Math.ceil(durationS / 8)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class VideoPipeline {
|
|
57
|
+
readonly #root: string;
|
|
58
|
+
|
|
59
|
+
constructor(options: VideoPipelineOptions = {}) {
|
|
60
|
+
this.#root = options.root ?? process.cwd();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async plan(intent: VideoIntent, brandInput: BrandContext | undefined): Promise<Storyboard> {
|
|
64
|
+
const brand = requireBrandContext(brandInput, "video-pipeline");
|
|
65
|
+
const count = sceneCount(intent.durationS);
|
|
66
|
+
const baseDuration = Math.max(1, Math.floor(intent.durationS / count));
|
|
67
|
+
const scenes = Array.from({ length: count }, (_, index): StoryboardScene => {
|
|
68
|
+
const sceneNumber = index + 1;
|
|
69
|
+
const durationS =
|
|
70
|
+
index === count - 1 ? intent.durationS - baseDuration * index : baseDuration;
|
|
71
|
+
const prompt = `${brand.name} ${intent.type} scene ${sceneNumber}: ${summarizeBrandContext(brand)}`;
|
|
72
|
+
return {
|
|
73
|
+
id: `scene_${sceneNumber}`,
|
|
74
|
+
sceneIndex: sceneNumber,
|
|
75
|
+
prompt,
|
|
76
|
+
description: prompt,
|
|
77
|
+
duration: durationS,
|
|
78
|
+
durationS,
|
|
79
|
+
status: "draft",
|
|
80
|
+
outputEnabled: true,
|
|
81
|
+
selectedImageIndex: -1,
|
|
82
|
+
outputHistory: [],
|
|
83
|
+
outputHistoryCursor: -1,
|
|
84
|
+
referenceImages: brand.referenceImages.map((reference) => reference.path),
|
|
85
|
+
transition: index === 0 ? "cut" : "fade",
|
|
86
|
+
musicCue: brand.toneKeywords.join(", "),
|
|
87
|
+
voiceCue: sceneNumber === 1 ? "introduce the product promise" : "advance the story",
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
id: assetId("storyboard", `${intent.type}_${brand.brandId}`),
|
|
92
|
+
tenantId: brand.tenantId,
|
|
93
|
+
brandId: brand.brandId,
|
|
94
|
+
intent,
|
|
95
|
+
scenes,
|
|
96
|
+
totalDurationS: storyboardTotalDuration(scenes),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async render(storyboard: Storyboard, brandInput: BrandContext | undefined): Promise<VideoAsset> {
|
|
101
|
+
const brand = requireBrandContext(brandInput, "video-pipeline");
|
|
102
|
+
const id = assetId("video", storyboard.id);
|
|
103
|
+
const path = join(this.#root, ".nebutra", "generated", "video-pipeline", `${id}.video.json`);
|
|
104
|
+
await mkdir(dirname(path), { recursive: true });
|
|
105
|
+
await writeFile(
|
|
106
|
+
path,
|
|
107
|
+
`${JSON.stringify({ storyboard, brand: summarizeBrandContext(brand), renderer: "manifest-local" }, null, 2)}\n`,
|
|
108
|
+
"utf8",
|
|
109
|
+
);
|
|
110
|
+
const asset: VideoAsset = {
|
|
111
|
+
id,
|
|
112
|
+
tenantId: brand.tenantId,
|
|
113
|
+
kind: "video",
|
|
114
|
+
path,
|
|
115
|
+
brandId: brand.brandId,
|
|
116
|
+
provider: "manifest-local",
|
|
117
|
+
model: "storyboard-manifest-v1",
|
|
118
|
+
createdAt: new Date().toISOString(),
|
|
119
|
+
license: { status: "commercial-ok", source: "deterministic local manifest" },
|
|
120
|
+
durationS: storyboard.totalDurationS,
|
|
121
|
+
format: "manifest-json",
|
|
122
|
+
storyboardId: storyboard.id,
|
|
123
|
+
metadata: { scenes: storyboard.scenes.length },
|
|
124
|
+
};
|
|
125
|
+
await appendCapabilityDebug("video-pipeline", { type: "render", asset }, { root: this.#root });
|
|
126
|
+
return asset;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async create(intent: VideoIntent, brand: BrandContext | undefined): Promise<VideoAsset> {
|
|
130
|
+
return this.render(await this.plan(intent, brand), brand);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async compose(output: SyncedVideoOutput): Promise<VideoAsset> {
|
|
134
|
+
const first = output.videoTrack[0];
|
|
135
|
+
if (!first) {
|
|
136
|
+
throw new Error("compose requires at least one video track");
|
|
137
|
+
}
|
|
138
|
+
const id = assetId("composition", first.id);
|
|
139
|
+
const path = join(this.#root, ".nebutra", "generated", "video-pipeline", `${id}.compose.json`);
|
|
140
|
+
await mkdir(dirname(path), { recursive: true });
|
|
141
|
+
await writeFile(path, `${JSON.stringify(output, null, 2)}\n`, "utf8");
|
|
142
|
+
return { ...first, id, path, storyboardId: first.storyboardId };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async cost(storyboard: Storyboard): Promise<{ estimatedUsd: number; durationS: number }> {
|
|
146
|
+
return {
|
|
147
|
+
estimatedUsd: Number((storyboard.totalDurationS * 0.003).toFixed(4)),
|
|
148
|
+
durationS: storyboard.totalDurationS,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async preview(storyboard: Storyboard): Promise<{ path: string }> {
|
|
153
|
+
const path = join(
|
|
154
|
+
this.#root,
|
|
155
|
+
".nebutra",
|
|
156
|
+
"generated",
|
|
157
|
+
"video-pipeline",
|
|
158
|
+
`${storyboard.id}.preview.json`,
|
|
159
|
+
);
|
|
160
|
+
await mkdir(dirname(path), { recursive: true });
|
|
161
|
+
await writeFile(path, `${JSON.stringify(storyboard, null, 2)}\n`, "utf8");
|
|
162
|
+
return { path };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async doctor(): Promise<VideoHealth[]> {
|
|
166
|
+
return [
|
|
167
|
+
{ provider: "manifest-local", ok: true },
|
|
168
|
+
{
|
|
169
|
+
provider: "local-video-model",
|
|
170
|
+
ok: Boolean(process.env.VIDEO_LOCAL_MODEL_PATH),
|
|
171
|
+
suggestion: "Set VIDEO_LOCAL_MODEL_PATH to enable local clip rendering.",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
provider: "remote-video",
|
|
175
|
+
ok: Boolean(process.env.VIDEO_REMOTE_API_KEY),
|
|
176
|
+
suggestion: "Set VIDEO_REMOTE_API_KEY to enable remote video fallback.",
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"target": "esnext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"incremental": false
|
|
9
|
+
},
|
|
10
|
+
"include": ["src", "examples"],
|
|
11
|
+
"exclude": ["node_modules", "dist"]
|
|
12
|
+
}
|