@wzrdtech/core 0.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/package.json +33 -0
- package/src/index.ts +3 -0
- package/src/manifest.ts +55 -0
- package/src/planner.ts +78 -0
- package/src/schema.ts +228 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wzrdtech/core",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Core Zap recipe schema, planning, and registry utilities.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/gratitude5dee/Zap.git",
|
|
14
|
+
"directory": "packages/core"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/gratitude5dee/Zap/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://zap.wzrd.tech",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./manifest": "./src/manifest.ts",
|
|
23
|
+
"./planner": "./src/planner.ts",
|
|
24
|
+
"./schema": "./src/schema.ts"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"yaml": "^2.8.1",
|
|
28
|
+
"zod": "4.4.3"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": "24.x"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/index.ts
ADDED
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export type SkillManifestEntry = {
|
|
6
|
+
fileCount: number;
|
|
7
|
+
hash: string;
|
|
8
|
+
path: string;
|
|
9
|
+
skill: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type SkillManifest = {
|
|
13
|
+
generatedAt: string;
|
|
14
|
+
skills: SkillManifestEntry[];
|
|
15
|
+
version: 1;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function generateSkillManifest(skillsDir: string): Promise<SkillManifest> {
|
|
19
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
20
|
+
const skills: SkillManifestEntry[] = [];
|
|
21
|
+
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (!entry.isDirectory()) continue;
|
|
24
|
+
const root = path.join(skillsDir, entry.name);
|
|
25
|
+
const files = await listFiles(root);
|
|
26
|
+
const hash = createHash("sha256");
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const relative = path.relative(root, file);
|
|
29
|
+
hash.update(relative);
|
|
30
|
+
hash.update(await fs.readFile(file));
|
|
31
|
+
}
|
|
32
|
+
skills.push({
|
|
33
|
+
fileCount: files.length,
|
|
34
|
+
hash: hash.digest("hex"),
|
|
35
|
+
path: path.relative(process.cwd(), root),
|
|
36
|
+
skill: entry.name,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
generatedAt: new Date().toISOString(),
|
|
42
|
+
skills: skills.sort((left, right) => left.skill.localeCompare(right.skill)),
|
|
43
|
+
version: 1,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function listFiles(root: string): Promise<string[]> {
|
|
48
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
49
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
50
|
+
const fullPath = path.join(root, entry.name);
|
|
51
|
+
if (entry.isDirectory()) return listFiles(fullPath);
|
|
52
|
+
return [fullPath];
|
|
53
|
+
}));
|
|
54
|
+
return files.flat().sort();
|
|
55
|
+
}
|
package/src/planner.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ZapSpec, ZapStep } from "./schema.ts";
|
|
2
|
+
|
|
3
|
+
export type PlannedZapStep = ZapStep & {
|
|
4
|
+
originalId: string;
|
|
5
|
+
repeatIndex?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ZapPlan = {
|
|
9
|
+
budgetCapUsd: number;
|
|
10
|
+
estimateUsd: number;
|
|
11
|
+
extendCount: number;
|
|
12
|
+
steps: PlannedZapStep[];
|
|
13
|
+
zap: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const modelRates: Record<string, { perSecond?: number; perRequest?: number }> = {
|
|
17
|
+
"fal-ai/flux/dev": { perRequest: 0.03 },
|
|
18
|
+
"fal-ai/kling-video/v2.1/pro/image-to-video": { perSecond: 0.28 },
|
|
19
|
+
"fal-ai/veo3.1": { perSecond: 0.45 },
|
|
20
|
+
"gemini-omni-flash-preview": { perSecond: 0.1 },
|
|
21
|
+
"happyhorse-1.1-i2v": { perSecond: 0.28 },
|
|
22
|
+
"seedance-2-0-260128": { perSecond: 0.07 },
|
|
23
|
+
"seedance-2-0-260128-upscale": { perSecond: 0.056 },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function planZapRun(zap: ZapSpec, extendCount: number): ZapPlan {
|
|
27
|
+
const steps = expandRepeatSteps(zap, extendCount);
|
|
28
|
+
const estimateUsd = steps.reduce((sum, step) => sum + quoteStep(step), 0);
|
|
29
|
+
return {
|
|
30
|
+
budgetCapUsd: zap.budget.cap_usd,
|
|
31
|
+
estimateUsd,
|
|
32
|
+
extendCount,
|
|
33
|
+
steps,
|
|
34
|
+
zap: zap.zap,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function assertWithinBudget(plan: ZapPlan) {
|
|
39
|
+
if (plan.estimateUsd > plan.budgetCapUsd) {
|
|
40
|
+
throw new Error(`Run quote $${plan.estimateUsd.toFixed(2)} exceeds recipe cap $${plan.budgetCapUsd}.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validateRequiredInputs(zap: ZapSpec, inputs: Record<string, unknown>) {
|
|
45
|
+
for (const [name, spec] of Object.entries(zap.inputs)) {
|
|
46
|
+
if (spec.required && inputs[name] === undefined) {
|
|
47
|
+
throw new Error(`Missing required input ${name}.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isLocalStep(step: ZapStep) {
|
|
53
|
+
return step.kind === "stitch" || step.kind === "keyframes";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function quoteStep(step: ZapStep) {
|
|
57
|
+
if (isLocalStep(step)) return 0;
|
|
58
|
+
const model = step.model ?? "local";
|
|
59
|
+
const rate = modelRates[model];
|
|
60
|
+
if (!rate) return 0;
|
|
61
|
+
if (rate.perRequest !== undefined) return rate.perRequest;
|
|
62
|
+
return (rate.perSecond ?? 0) * (step.duration_s ?? 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function expandRepeatSteps(zap: ZapSpec, extendCount: number): PlannedZapStep[] {
|
|
66
|
+
return zap.steps.flatMap((step) => {
|
|
67
|
+
if (step.kind !== "video.extend") return [{ ...step, originalId: step.id }];
|
|
68
|
+
const max = step.repeat?.max ?? 64;
|
|
69
|
+
const min = step.repeat?.min ?? 0;
|
|
70
|
+
const count = Math.max(min, Math.min(extendCount, max));
|
|
71
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
72
|
+
...step,
|
|
73
|
+
id: `${step.id}_${index + 1}`,
|
|
74
|
+
originalId: step.id,
|
|
75
|
+
repeatIndex: index + 1,
|
|
76
|
+
}));
|
|
77
|
+
});
|
|
78
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { parseDocument } from "yaml";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export class ZapSchemaError extends Error {
|
|
5
|
+
readonly code = "SCHEMA_INVALID";
|
|
6
|
+
readonly retryable = false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const zapInputSchema = z.object({
|
|
10
|
+
hint: z.string().optional(),
|
|
11
|
+
label: z.string().optional(),
|
|
12
|
+
options: z.array(z.string()).optional(),
|
|
13
|
+
required: z.boolean().default(false),
|
|
14
|
+
type: z.enum(["string", "textarea", "image", "video", "select", "number"]),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const zapStepKindSchema = z.enum([
|
|
18
|
+
"image.gen",
|
|
19
|
+
"image.edit",
|
|
20
|
+
"video.gen",
|
|
21
|
+
"video.extend",
|
|
22
|
+
"video.edit",
|
|
23
|
+
"video.upscale",
|
|
24
|
+
"audio.tts",
|
|
25
|
+
"audio.music",
|
|
26
|
+
"audio.sfx",
|
|
27
|
+
"keyframes",
|
|
28
|
+
"stitch",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export const zapProviderSchema = z.enum(["gmi", "fal", "prodia", "runware"]);
|
|
32
|
+
export type ZapProvider = z.infer<typeof zapProviderSchema>;
|
|
33
|
+
|
|
34
|
+
export const zapStitchSchema = z.object({
|
|
35
|
+
engine: z.enum(["auto", "local", "hyperframes"]).default("auto"),
|
|
36
|
+
fps: z.number().int().min(1).max(120).optional(),
|
|
37
|
+
format: z.enum(["mp4", "webm"]).default("mp4"),
|
|
38
|
+
quality: z.enum(["draft", "standard", "high"]).default("standard"),
|
|
39
|
+
}).default({ engine: "auto", format: "mp4", quality: "standard" });
|
|
40
|
+
|
|
41
|
+
export const zapStepSchema = z.object({
|
|
42
|
+
audio: z.record(z.string(), z.unknown()).optional(),
|
|
43
|
+
candidates: z.number().int().min(1).max(16).optional(),
|
|
44
|
+
duration_s: z.number().positive().optional(),
|
|
45
|
+
extend: z.object({ mode: z.enum(["chain", "anchored"]).default("chain") }).optional(),
|
|
46
|
+
first_frame: z.record(z.string(), z.unknown()).optional(),
|
|
47
|
+
id: z.string().min(1),
|
|
48
|
+
inputs: z.array(z.string()).optional(),
|
|
49
|
+
judge: z.record(z.string(), z.unknown()).optional(),
|
|
50
|
+
keyframes: z.record(z.string(), z.unknown()).optional(),
|
|
51
|
+
kind: zapStepKindSchema,
|
|
52
|
+
model: z.string().optional(),
|
|
53
|
+
prompt: z.string().optional(),
|
|
54
|
+
provider: zapProviderSchema.optional(),
|
|
55
|
+
reference_images: z.array(z.string()).optional(),
|
|
56
|
+
repeat: z.object({
|
|
57
|
+
default: z.number().int().min(0).optional(),
|
|
58
|
+
max: z.number().int().min(0).max(64).optional(),
|
|
59
|
+
min: z.number().int().min(0).optional(),
|
|
60
|
+
}).optional(),
|
|
61
|
+
retry: z.object({
|
|
62
|
+
backoff_s: z.number().min(0).max(300).default(0),
|
|
63
|
+
fallback_model: z.string().optional(),
|
|
64
|
+
fallback_provider: zapProviderSchema.optional(),
|
|
65
|
+
max: z.number().int().min(0).max(8).default(0),
|
|
66
|
+
}).optional(),
|
|
67
|
+
rlhf: z.union([z.literal("optional"), z.boolean()]).optional(),
|
|
68
|
+
shared: z.boolean().optional(),
|
|
69
|
+
stitch: zapStitchSchema.optional(),
|
|
70
|
+
tier: z.enum(["draft", "final"]).optional(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const zapPublishSchema = z.object({
|
|
74
|
+
embed: z.object({
|
|
75
|
+
allowOrigins: z.array(z.string()).default(["*"]),
|
|
76
|
+
enabled: z.boolean().default(true),
|
|
77
|
+
height: z.number().int().min(240).max(2160).default(720),
|
|
78
|
+
theme: z.enum(["auto", "dark", "light"]).default("auto"),
|
|
79
|
+
width: z.number().int().min(240).max(3840).default(1280),
|
|
80
|
+
}).optional(),
|
|
81
|
+
slug: z.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/).optional(),
|
|
82
|
+
visibility: z.enum(["public", "unlisted", "private"]).default("public"),
|
|
83
|
+
}).partial().optional();
|
|
84
|
+
|
|
85
|
+
export const zapSpecSchema = z.object({
|
|
86
|
+
budget: z.object({
|
|
87
|
+
cap_usd: z.number().positive(),
|
|
88
|
+
estimate_usd: z.number().nonnegative(),
|
|
89
|
+
}),
|
|
90
|
+
defaults: z.object({
|
|
91
|
+
aspect: z.string().optional(),
|
|
92
|
+
models: z.record(z.string(), z.string()).default(() => ({})),
|
|
93
|
+
provider: zapProviderSchema.default("gmi"),
|
|
94
|
+
}).default(() => ({ models: {}, provider: "gmi" as const })),
|
|
95
|
+
description: z.string(),
|
|
96
|
+
inputs: z.record(z.string(), zapInputSchema).default({}),
|
|
97
|
+
output: z.string().default("Zap.mp4"),
|
|
98
|
+
publish: zapPublishSchema,
|
|
99
|
+
steps: z.array(zapStepSchema).min(1),
|
|
100
|
+
version: z.literal(2),
|
|
101
|
+
zap: z.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export type ZapInput = z.infer<typeof zapInputSchema>;
|
|
105
|
+
export type ZapStep = z.infer<typeof zapStepSchema>;
|
|
106
|
+
export type ZapStepKind = z.infer<typeof zapStepKindSchema>;
|
|
107
|
+
export type ZapSpec = z.infer<typeof zapSpecSchema>;
|
|
108
|
+
export type PublicZapSpec = ZapSpec & { title: string };
|
|
109
|
+
|
|
110
|
+
export function parseZapMarkdown(markdown: string): ZapSpec {
|
|
111
|
+
const frontmatter = extractFrontmatter(markdown);
|
|
112
|
+
const parsed = parseDocument(frontmatter).toJS();
|
|
113
|
+
const spec = zapSpecSchema.parse(parsed);
|
|
114
|
+
validateSpec(spec);
|
|
115
|
+
return spec;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function validateZapPromptTemplates(spec: ZapSpec, promptContents: Record<string, string>) {
|
|
119
|
+
for (const step of spec.steps) {
|
|
120
|
+
const promptRef = step.prompt;
|
|
121
|
+
if (!promptRef || !isPromptFile(promptRef)) continue;
|
|
122
|
+
const content = promptContents[promptRef];
|
|
123
|
+
if (content === undefined) {
|
|
124
|
+
throw new ZapSchemaError(`Step ${step.id} references missing prompt file ${promptRef}.`);
|
|
125
|
+
}
|
|
126
|
+
validateTemplateVariables(spec, step.id, content);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function publicZapSpec(spec: ZapSpec): PublicZapSpec {
|
|
131
|
+
return { ...spec, title: titleize(spec.zap) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractFrontmatter(markdown: string) {
|
|
135
|
+
const match = markdown.match(/^---\n([\s\S]*?)\n---/);
|
|
136
|
+
if (!match) {
|
|
137
|
+
throw new Error("Zap recipe is missing YAML frontmatter.");
|
|
138
|
+
}
|
|
139
|
+
return match[1];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validateSpec(spec: ZapSpec) {
|
|
143
|
+
validateDuplicateStepIds(spec);
|
|
144
|
+
validateStepRefs(spec);
|
|
145
|
+
validateVideoDurations(spec);
|
|
146
|
+
validateInlineVariables(spec);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateInlineVariables(spec: ZapSpec) {
|
|
150
|
+
for (const step of spec.steps) {
|
|
151
|
+
const promptRef = step.prompt ?? "";
|
|
152
|
+
if (!isPromptFile(promptRef)) validateTemplateVariables(spec, step.id, promptRef);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateDuplicateStepIds(spec: ZapSpec) {
|
|
157
|
+
const seen = new Set<string>();
|
|
158
|
+
for (const step of spec.steps) {
|
|
159
|
+
if (seen.has(step.id)) throw new Error(`Duplicate step id ${step.id}.`);
|
|
160
|
+
seen.add(step.id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateStepRefs(spec: ZapSpec) {
|
|
165
|
+
const declaredInputs = new Set(Object.keys(spec.inputs));
|
|
166
|
+
const priorSteps = new Set<string>();
|
|
167
|
+
for (const step of spec.steps) {
|
|
168
|
+
for (const ref of [...(step.inputs ?? []), ...(step.reference_images ?? [])]) {
|
|
169
|
+
validateRef({ declaredInputs, priorSteps, ref, stepId: step.id });
|
|
170
|
+
}
|
|
171
|
+
priorSteps.add(step.id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateRef({
|
|
176
|
+
declaredInputs,
|
|
177
|
+
priorSteps,
|
|
178
|
+
ref,
|
|
179
|
+
stepId,
|
|
180
|
+
}: {
|
|
181
|
+
declaredInputs: Set<string>;
|
|
182
|
+
priorSteps: Set<string>;
|
|
183
|
+
ref: string;
|
|
184
|
+
stepId: string;
|
|
185
|
+
}) {
|
|
186
|
+
if (ref.startsWith("user.")) {
|
|
187
|
+
const inputName = ref.slice("user.".length);
|
|
188
|
+
if (declaredInputs.has(inputName)) return;
|
|
189
|
+
throw new ZapSchemaError(`Step ${stepId} references undeclared input ${ref}.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (ref.endsWith(".*")) {
|
|
193
|
+
const prefix = ref.slice(0, -2);
|
|
194
|
+
if (priorSteps.has(prefix)) return;
|
|
195
|
+
throw new ZapSchemaError(`Step ${stepId} references unknown repeated step ${ref}.`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (priorSteps.has(ref) || declaredInputs.has(ref)) return;
|
|
199
|
+
throw new ZapSchemaError(`Step ${stepId} references unknown input or step ${ref}.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function validateVideoDurations(spec: ZapSpec) {
|
|
203
|
+
for (const step of spec.steps) {
|
|
204
|
+
if (step.kind.startsWith("video.") && step.duration_s === undefined) {
|
|
205
|
+
throw new ZapSchemaError(`Video step ${step.id} is missing duration_s.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateTemplateVariables(spec: ZapSpec, stepId: string, template: string) {
|
|
211
|
+
const declared = new Set(Object.keys(spec.inputs));
|
|
212
|
+
for (const variable of template.matchAll(/\{([A-Z0-9_]+)\}/g)) {
|
|
213
|
+
if (!declared.has(variable[1])) {
|
|
214
|
+
throw new ZapSchemaError(`Step ${stepId} references undeclared input {${variable[1]}}.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isPromptFile(prompt: string) {
|
|
220
|
+
return prompt.endsWith(".md") || prompt.startsWith("prompts/");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function titleize(slug: string) {
|
|
224
|
+
return slug
|
|
225
|
+
.split("-")
|
|
226
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
227
|
+
.join(" ");
|
|
228
|
+
}
|