@nebutra/image-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-CYXB6GKX.js +132 -0
- package/dist/chunk-CYXB6GKX.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +42 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/examples/logo.ts +9 -0
- package/examples/poster.ts +9 -0
- package/examples/workflow.ts +10 -0
- package/package.json +59 -0
- package/src/cli.ts +32 -0
- package/src/index.test.ts +33 -0
- package/src/index.ts +177 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @nebutra/image-pipeline
|
|
2
|
+
|
|
3
|
+
Status: WIP — Not yet integrated into any production app.
|
|
4
|
+
|
|
5
|
+
`@nebutra/image-pipeline` is the BrandContext-first image capability surface.
|
|
6
|
+
It exposes logo, poster, hero image, icon, and workflow-shaped generation APIs.
|
|
7
|
+
The zero-config path writes deterministic SVG assets so examples and tests have
|
|
8
|
+
real files; model-backed generation is delegated to sidecar adapters.
|
|
9
|
+
|
|
10
|
+
It does not own prompt orchestration, Thread/Turn/Item state, model provider
|
|
11
|
+
routing, or user approval.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm image:doctor
|
|
17
|
+
pnpm image:debug
|
|
18
|
+
pnpm image:warmup
|
|
19
|
+
pnpm image:workflows
|
|
20
|
+
```
|
|
@@ -0,0 +1,132 @@
|
|
|
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 { CapabilityError } from "@nebutra/errors";
|
|
6
|
+
import {
|
|
7
|
+
assetId,
|
|
8
|
+
requireBrandContext,
|
|
9
|
+
summarizeBrandContext
|
|
10
|
+
} from "@nebutra/generation-context";
|
|
11
|
+
async function readImageDebug(root = process.cwd(), limit = 10) {
|
|
12
|
+
return readCapabilityDebug("image-pipeline", { root, limit });
|
|
13
|
+
}
|
|
14
|
+
function intentLabel(intent) {
|
|
15
|
+
switch (intent.type) {
|
|
16
|
+
case "logo":
|
|
17
|
+
return `${intent.company}: ${intent.oneLiner}`;
|
|
18
|
+
case "poster":
|
|
19
|
+
return `${intent.title}${intent.subtitle ? ` \u2014 ${intent.subtitle}` : ""}`;
|
|
20
|
+
case "icon":
|
|
21
|
+
return intent.subject;
|
|
22
|
+
case "hero":
|
|
23
|
+
return intent.headline;
|
|
24
|
+
case "style-transfer":
|
|
25
|
+
return `${intent.sourcePath}: ${intent.instruction}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function escapeXml(value) {
|
|
29
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
30
|
+
}
|
|
31
|
+
function svgFor(intent, brand) {
|
|
32
|
+
const primary = brand.palette[0]?.hex ?? "#111111";
|
|
33
|
+
const accent = brand.palette.find((color) => color.role === "accent")?.hex ?? "#F3C14B";
|
|
34
|
+
const label = intentLabel(intent);
|
|
35
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
|
|
36
|
+
<rect width="1200" height="800" fill="${escapeXml(primary)}"/>
|
|
37
|
+
<circle cx="960" cy="160" r="96" fill="${escapeXml(accent)}" opacity="0.9"/>
|
|
38
|
+
<text x="80" y="360" fill="#ffffff" font-family="${escapeXml(brand.typography.heading)}" font-size="78" font-weight="700">${escapeXml(brand.name)}</text>
|
|
39
|
+
<text x="84" y="440" fill="#ffffff" font-family="${escapeXml(brand.typography.body)}" font-size="34">${escapeXml(label)}</text>
|
|
40
|
+
<text x="84" y="520" fill="#ffffff" opacity="0.72" font-family="${escapeXml(brand.typography.body)}" font-size="24">${escapeXml(summarizeBrandContext(brand))}</text>
|
|
41
|
+
</svg>`;
|
|
42
|
+
}
|
|
43
|
+
var ImagePipeline = class {
|
|
44
|
+
#root;
|
|
45
|
+
#provider;
|
|
46
|
+
#comfyUrl;
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this.#root = options.root ?? process.cwd();
|
|
49
|
+
this.#provider = options.provider ?? "svg-local";
|
|
50
|
+
this.#comfyUrl = options.comfyUrl ?? process.env.COMFYUI_URL ?? "http://127.0.0.1:8188";
|
|
51
|
+
}
|
|
52
|
+
async generate(intent, brandInput) {
|
|
53
|
+
const brand = requireBrandContext(brandInput, "image-pipeline");
|
|
54
|
+
if (this.#provider !== "svg-local") {
|
|
55
|
+
throw new CapabilityError("image-pipeline", `${this.#provider} provider is not configured`, {
|
|
56
|
+
suggestion: "Use the zero-config svg-local provider or configure a sidecar adapter before selecting model-backed image generation.",
|
|
57
|
+
statusCode: 503
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const id = assetId("image", `${intent.type}_${intentLabel(intent)}`);
|
|
61
|
+
const path = join(this.#root, ".nebutra", "generated", "image-pipeline", `${id}.svg`);
|
|
62
|
+
await mkdir(dirname(path), { recursive: true });
|
|
63
|
+
await writeFile(path, svgFor(intent, brand), "utf8");
|
|
64
|
+
const asset = {
|
|
65
|
+
id,
|
|
66
|
+
tenantId: brand.tenantId,
|
|
67
|
+
kind: "image",
|
|
68
|
+
path,
|
|
69
|
+
brandId: brand.brandId,
|
|
70
|
+
provider: "svg-local",
|
|
71
|
+
model: "brand-context-svg-v1",
|
|
72
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
73
|
+
license: { status: "commercial-ok", source: "deterministic local renderer" },
|
|
74
|
+
width: 1200,
|
|
75
|
+
height: 800,
|
|
76
|
+
format: "svg",
|
|
77
|
+
metadata: { intent, brandSource: brand.sourcePath }
|
|
78
|
+
};
|
|
79
|
+
await appendCapabilityDebug(
|
|
80
|
+
"image-pipeline",
|
|
81
|
+
{ type: "generate", asset },
|
|
82
|
+
{ root: this.#root }
|
|
83
|
+
);
|
|
84
|
+
return asset;
|
|
85
|
+
}
|
|
86
|
+
async runWorkflow(workflowName, inputs, brand) {
|
|
87
|
+
requireBrandContext(brand, "image-pipeline");
|
|
88
|
+
return this.generate(
|
|
89
|
+
{
|
|
90
|
+
type: "poster",
|
|
91
|
+
title: workflowName,
|
|
92
|
+
subtitle: Object.keys(inputs).join(", ") || "workflow"
|
|
93
|
+
},
|
|
94
|
+
brand
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
async doctor() {
|
|
98
|
+
const comfyConfigured = Boolean(process.env.COMFYUI_URL);
|
|
99
|
+
return [
|
|
100
|
+
{ provider: "svg-local", ok: true },
|
|
101
|
+
comfyConfigured ? { provider: "comfy", ok: true } : {
|
|
102
|
+
provider: "comfy",
|
|
103
|
+
ok: false,
|
|
104
|
+
suggestion: `Set COMFYUI_URL to enable model-backed workflow execution. Default probe target is ${this.#comfyUrl}.`
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
provider: "remote",
|
|
108
|
+
ok: Boolean(process.env.IMAGE_REMOTE_API_KEY),
|
|
109
|
+
suggestion: "Set IMAGE_REMOTE_API_KEY to enable remote image fallback."
|
|
110
|
+
}
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
async workflows() {
|
|
114
|
+
return ["logo", "poster", "icon", "hero", "style-transfer"];
|
|
115
|
+
}
|
|
116
|
+
async warmup() {
|
|
117
|
+
if (this.#provider === "svg-local") {
|
|
118
|
+
return { provider: this.#provider, ok: true };
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
provider: this.#provider,
|
|
122
|
+
ok: false,
|
|
123
|
+
suggestion: "Start the image sidecar before warming model weights."
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export {
|
|
129
|
+
readImageDebug,
|
|
130
|
+
ImagePipeline
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=chunk-CYXB6GKX.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 { CapabilityError } from \"@nebutra/errors\";\nimport {\n assetId,\n type BrandContext,\n type GeneratedAsset,\n requireBrandContext,\n summarizeBrandContext,\n} from \"@nebutra/generation-context\";\n\nexport type ImageIntent =\n | { type: \"logo\"; company: string; oneLiner: string }\n | { type: \"poster\"; title: string; subtitle?: string }\n | { type: \"icon\"; subject: string }\n | { type: \"hero\"; headline: string }\n | { type: \"style-transfer\"; sourcePath: string; instruction: string };\n\nexport interface ImageAsset extends GeneratedAsset {\n readonly kind: \"image\";\n readonly width: number;\n readonly height: number;\n readonly format: \"svg\" | \"png\" | \"jpg\" | \"webp\";\n}\n\nexport interface ImageHealth {\n readonly provider: string;\n readonly ok: boolean;\n readonly suggestion?: string;\n}\n\nexport interface ImagePipelineOptions {\n readonly root?: string;\n readonly provider?: \"svg-local\" | \"comfy\" | \"remote\";\n readonly comfyUrl?: string;\n}\n\nexport async function readImageDebug(root = process.cwd(), limit = 10): Promise<unknown[]> {\n return readCapabilityDebug(\"image-pipeline\", { root, limit });\n}\n\nfunction intentLabel(intent: ImageIntent): string {\n switch (intent.type) {\n case \"logo\":\n return `${intent.company}: ${intent.oneLiner}`;\n case \"poster\":\n return `${intent.title}${intent.subtitle ? ` — ${intent.subtitle}` : \"\"}`;\n case \"icon\":\n return intent.subject;\n case \"hero\":\n return intent.headline;\n case \"style-transfer\":\n return `${intent.sourcePath}: ${intent.instruction}`;\n }\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\");\n}\n\nfunction svgFor(intent: ImageIntent, brand: BrandContext): string {\n const primary = brand.palette[0]?.hex ?? \"#111111\";\n const accent = brand.palette.find((color) => color.role === \"accent\")?.hex ?? \"#F3C14B\";\n const label = intentLabel(intent);\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"800\" viewBox=\"0 0 1200 800\">\n <rect width=\"1200\" height=\"800\" fill=\"${escapeXml(primary)}\"/>\n <circle cx=\"960\" cy=\"160\" r=\"96\" fill=\"${escapeXml(accent)}\" opacity=\"0.9\"/>\n <text x=\"80\" y=\"360\" fill=\"#ffffff\" font-family=\"${escapeXml(brand.typography.heading)}\" font-size=\"78\" font-weight=\"700\">${escapeXml(brand.name)}</text>\n <text x=\"84\" y=\"440\" fill=\"#ffffff\" font-family=\"${escapeXml(brand.typography.body)}\" font-size=\"34\">${escapeXml(label)}</text>\n <text x=\"84\" y=\"520\" fill=\"#ffffff\" opacity=\"0.72\" font-family=\"${escapeXml(brand.typography.body)}\" font-size=\"24\">${escapeXml(summarizeBrandContext(brand))}</text>\n</svg>`;\n}\n\nexport class ImagePipeline {\n readonly #root: string;\n readonly #provider: \"svg-local\" | \"comfy\" | \"remote\";\n readonly #comfyUrl: string;\n\n constructor(options: ImagePipelineOptions = {}) {\n this.#root = options.root ?? process.cwd();\n this.#provider = options.provider ?? \"svg-local\";\n this.#comfyUrl = options.comfyUrl ?? process.env.COMFYUI_URL ?? \"http://127.0.0.1:8188\";\n }\n\n async generate(intent: ImageIntent, brandInput: BrandContext | undefined): Promise<ImageAsset> {\n const brand = requireBrandContext(brandInput, \"image-pipeline\");\n if (this.#provider !== \"svg-local\") {\n throw new CapabilityError(\"image-pipeline\", `${this.#provider} provider is not configured`, {\n suggestion:\n \"Use the zero-config svg-local provider or configure a sidecar adapter before selecting model-backed image generation.\",\n statusCode: 503,\n });\n }\n\n const id = assetId(\"image\", `${intent.type}_${intentLabel(intent)}`);\n const path = join(this.#root, \".nebutra\", \"generated\", \"image-pipeline\", `${id}.svg`);\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, svgFor(intent, brand), \"utf8\");\n\n const asset: ImageAsset = {\n id,\n tenantId: brand.tenantId,\n kind: \"image\",\n path,\n brandId: brand.brandId,\n provider: \"svg-local\",\n model: \"brand-context-svg-v1\",\n createdAt: new Date().toISOString(),\n license: { status: \"commercial-ok\", source: \"deterministic local renderer\" },\n width: 1200,\n height: 800,\n format: \"svg\",\n metadata: { intent, brandSource: brand.sourcePath },\n };\n await appendCapabilityDebug(\n \"image-pipeline\",\n { type: \"generate\", asset },\n { root: this.#root },\n );\n return asset;\n }\n\n async runWorkflow(\n workflowName: string,\n inputs: Record<string, unknown>,\n brand: BrandContext | undefined,\n ): Promise<ImageAsset> {\n requireBrandContext(brand, \"image-pipeline\");\n return this.generate(\n {\n type: \"poster\",\n title: workflowName,\n subtitle: Object.keys(inputs).join(\", \") || \"workflow\",\n },\n brand,\n );\n }\n\n async doctor(): Promise<ImageHealth[]> {\n const comfyConfigured = Boolean(process.env.COMFYUI_URL);\n return [\n { provider: \"svg-local\", ok: true },\n comfyConfigured\n ? { provider: \"comfy\", ok: true }\n : {\n provider: \"comfy\",\n ok: false,\n suggestion: `Set COMFYUI_URL to enable model-backed workflow execution. Default probe target is ${this.#comfyUrl}.`,\n },\n {\n provider: \"remote\",\n ok: Boolean(process.env.IMAGE_REMOTE_API_KEY),\n suggestion: \"Set IMAGE_REMOTE_API_KEY to enable remote image fallback.\",\n },\n ];\n }\n\n async workflows(): Promise<readonly string[]> {\n return [\"logo\", \"poster\", \"icon\", \"hero\", \"style-transfer\"];\n }\n\n async warmup(): Promise<ImageHealth> {\n if (this.#provider === \"svg-local\") {\n return { provider: this.#provider, ok: true };\n }\n return {\n provider: this.#provider,\n ok: false,\n suggestion: \"Start the image sidecar before warming model weights.\",\n };\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ImagePipeline,
|
|
3
|
+
readImageDebug
|
|
4
|
+
} from "./chunk-CYXB6GKX.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
8
|
+
var command = process.argv[2] ?? "doctor";
|
|
9
|
+
var pipeline = new ImagePipeline();
|
|
10
|
+
if (command === "doctor") {
|
|
11
|
+
process.stdout.write(
|
|
12
|
+
`${JSON.stringify({ capability: "image-pipeline", results: await pipeline.doctor() }, null, 2)}
|
|
13
|
+
`
|
|
14
|
+
);
|
|
15
|
+
} else if (command === "debug") {
|
|
16
|
+
process.stdout.write(
|
|
17
|
+
`${JSON.stringify({ capability: "image-pipeline", entries: await readImageDebug() }, null, 2)}
|
|
18
|
+
`
|
|
19
|
+
);
|
|
20
|
+
} else if (command === "warmup") {
|
|
21
|
+
process.stdout.write(
|
|
22
|
+
`${JSON.stringify({ capability: "image-pipeline", result: await pipeline.warmup() }, null, 2)}
|
|
23
|
+
`
|
|
24
|
+
);
|
|
25
|
+
} else if (command === "workflows") {
|
|
26
|
+
process.stdout.write(
|
|
27
|
+
`${JSON.stringify({ capability: "image-pipeline", workflows: await pipeline.workflows() }, null, 2)}
|
|
28
|
+
`
|
|
29
|
+
);
|
|
30
|
+
} else if (command === "quickstart") {
|
|
31
|
+
const asset = await pipeline.generate(
|
|
32
|
+
{ type: "logo", company: "Loop", oneLiner: "AI debugging for indie devs" },
|
|
33
|
+
createDemoBrandContext()
|
|
34
|
+
);
|
|
35
|
+
process.stdout.write(`${JSON.stringify({ capability: "image-pipeline", asset }, null, 2)}
|
|
36
|
+
`);
|
|
37
|
+
} else {
|
|
38
|
+
process.stderr.write(`Unknown image-pipeline command: ${command}
|
|
39
|
+
`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
//# 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 { ImagePipeline, readImageDebug } from \"./index\";\n\nconst command = process.argv[2] ?? \"doctor\";\nconst pipeline = new ImagePipeline();\n\nif (command === \"doctor\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"image-pipeline\", results: await pipeline.doctor() }, null, 2)}\\n`,\n );\n} else if (command === \"debug\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"image-pipeline\", entries: await readImageDebug() }, null, 2)}\\n`,\n );\n} else if (command === \"warmup\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"image-pipeline\", result: await pipeline.warmup() }, null, 2)}\\n`,\n );\n} else if (command === \"workflows\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"image-pipeline\", workflows: await pipeline.workflows() }, null, 2)}\\n`,\n );\n} else if (command === \"quickstart\") {\n const asset = await pipeline.generate(\n { type: \"logo\", company: \"Loop\", oneLiner: \"AI debugging for indie devs\" },\n createDemoBrandContext(),\n );\n process.stdout.write(`${JSON.stringify({ capability: \"image-pipeline\", asset }, null, 2)}\\n`);\n} else {\n process.stderr.write(`Unknown image-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;AAEnC,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,UAAU;AAC/B,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,kBAAkB,QAAQ,MAAM,SAAS,OAAO,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EAC/F;AACF,WAAW,YAAY,aAAa;AAClC,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,kBAAkB,WAAW,MAAM,SAAS,UAAU,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EACrG;AACF,WAAW,YAAY,cAAc;AACnC,QAAM,QAAQ,MAAM,SAAS;AAAA,IAC3B,EAAE,MAAM,QAAQ,SAAS,QAAQ,UAAU,8BAA8B;AAAA,IACzE,uBAAuB;AAAA,EACzB;AACA,UAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,EAAE,YAAY,kBAAkB,MAAM,GAAG,MAAM,CAAC,CAAC;AAAA,CAAI;AAC9F,OAAO;AACL,UAAQ,OAAO,MAAM,mCAAmC,OAAO;AAAA,CAAI;AACnE,UAAQ,WAAW;AACrB;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { GeneratedAsset, BrandContext } from '@nebutra/generation-context';
|
|
2
|
+
|
|
3
|
+
type ImageIntent = {
|
|
4
|
+
type: "logo";
|
|
5
|
+
company: string;
|
|
6
|
+
oneLiner: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: "poster";
|
|
9
|
+
title: string;
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: "icon";
|
|
13
|
+
subject: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: "hero";
|
|
16
|
+
headline: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "style-transfer";
|
|
19
|
+
sourcePath: string;
|
|
20
|
+
instruction: string;
|
|
21
|
+
};
|
|
22
|
+
interface ImageAsset extends GeneratedAsset {
|
|
23
|
+
readonly kind: "image";
|
|
24
|
+
readonly width: number;
|
|
25
|
+
readonly height: number;
|
|
26
|
+
readonly format: "svg" | "png" | "jpg" | "webp";
|
|
27
|
+
}
|
|
28
|
+
interface ImageHealth {
|
|
29
|
+
readonly provider: string;
|
|
30
|
+
readonly ok: boolean;
|
|
31
|
+
readonly suggestion?: string;
|
|
32
|
+
}
|
|
33
|
+
interface ImagePipelineOptions {
|
|
34
|
+
readonly root?: string;
|
|
35
|
+
readonly provider?: "svg-local" | "comfy" | "remote";
|
|
36
|
+
readonly comfyUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
declare function readImageDebug(root?: string, limit?: number): Promise<unknown[]>;
|
|
39
|
+
declare class ImagePipeline {
|
|
40
|
+
#private;
|
|
41
|
+
constructor(options?: ImagePipelineOptions);
|
|
42
|
+
generate(intent: ImageIntent, brandInput: BrandContext | undefined): Promise<ImageAsset>;
|
|
43
|
+
runWorkflow(workflowName: string, inputs: Record<string, unknown>, brand: BrandContext | undefined): Promise<ImageAsset>;
|
|
44
|
+
doctor(): Promise<ImageHealth[]>;
|
|
45
|
+
workflows(): Promise<readonly string[]>;
|
|
46
|
+
warmup(): Promise<ImageHealth>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { type ImageAsset, type ImageHealth, type ImageIntent, ImagePipeline, type ImagePipelineOptions, readImageDebug };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/examples/logo.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
+
import { ImagePipeline } from "../src/index";
|
|
3
|
+
|
|
4
|
+
const asset = await new ImagePipeline().generate(
|
|
5
|
+
{ type: "logo", company: "Loop", oneLiner: "AI debugging for indie devs" },
|
|
6
|
+
createDemoBrandContext(),
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
process.stdout.write(`${asset.path}\n`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
+
import { ImagePipeline } from "../src/index";
|
|
3
|
+
|
|
4
|
+
const asset = await new ImagePipeline().generate(
|
|
5
|
+
{ type: "poster", title: "Ship with context", subtitle: "A founder operating system" },
|
|
6
|
+
createDemoBrandContext(),
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
process.stdout.write(`${asset.path}\n`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
+
import { ImagePipeline } from "../src/index";
|
|
3
|
+
|
|
4
|
+
const asset = await new ImagePipeline().runWorkflow(
|
|
5
|
+
"hero",
|
|
6
|
+
{ headline: "Debug the loop" },
|
|
7
|
+
createDemoBrandContext(),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
process.stdout.write(`${asset.path}\n`);
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebutra/image-pipeline",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "BrandContext-first image generation and workflow routing surface",
|
|
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/sandbox-runtime for local generation sidecars"
|
|
15
|
+
],
|
|
16
|
+
"gaps": [
|
|
17
|
+
"Comfy sidecar lifecycle is health-checked but not launched by this package",
|
|
18
|
+
"Remote image providers are adapter ports only",
|
|
19
|
+
"Workflow templates are deterministic SVG fallbacks until model nodes are installed"
|
|
20
|
+
],
|
|
21
|
+
"featureId": "image-pipeline",
|
|
22
|
+
"category": "ai",
|
|
23
|
+
"summary": "BrandContext-first image/logo/poster/icon generation surface"
|
|
24
|
+
},
|
|
25
|
+
"main": "./src/index.ts",
|
|
26
|
+
"types": "./src/index.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@nebutra/capability-kit": "0.2.0",
|
|
32
|
+
"@nebutra/errors": "0.1.0",
|
|
33
|
+
"@nebutra/generation-context": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.19.15",
|
|
37
|
+
"tsup": "^8.5.1",
|
|
38
|
+
"tsx": "^4.21.0",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vitest": "^4.1.4"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/ai/image-pipeline#readme",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/Nebutra/Nebutra-Sailor.git",
|
|
46
|
+
"directory": "packages/ai/image-pipeline"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/Nebutra/Nebutra-Sailor/issues"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"typecheck": "tsc --noEmit"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
+
import { ImagePipeline, readImageDebug } from "./index";
|
|
3
|
+
|
|
4
|
+
const command = process.argv[2] ?? "doctor";
|
|
5
|
+
const pipeline = new ImagePipeline();
|
|
6
|
+
|
|
7
|
+
if (command === "doctor") {
|
|
8
|
+
process.stdout.write(
|
|
9
|
+
`${JSON.stringify({ capability: "image-pipeline", results: await pipeline.doctor() }, null, 2)}\n`,
|
|
10
|
+
);
|
|
11
|
+
} else if (command === "debug") {
|
|
12
|
+
process.stdout.write(
|
|
13
|
+
`${JSON.stringify({ capability: "image-pipeline", entries: await readImageDebug() }, null, 2)}\n`,
|
|
14
|
+
);
|
|
15
|
+
} else if (command === "warmup") {
|
|
16
|
+
process.stdout.write(
|
|
17
|
+
`${JSON.stringify({ capability: "image-pipeline", result: await pipeline.warmup() }, null, 2)}\n`,
|
|
18
|
+
);
|
|
19
|
+
} else if (command === "workflows") {
|
|
20
|
+
process.stdout.write(
|
|
21
|
+
`${JSON.stringify({ capability: "image-pipeline", workflows: await pipeline.workflows() }, null, 2)}\n`,
|
|
22
|
+
);
|
|
23
|
+
} else if (command === "quickstart") {
|
|
24
|
+
const asset = await pipeline.generate(
|
|
25
|
+
{ type: "logo", company: "Loop", oneLiner: "AI debugging for indie devs" },
|
|
26
|
+
createDemoBrandContext(),
|
|
27
|
+
);
|
|
28
|
+
process.stdout.write(`${JSON.stringify({ capability: "image-pipeline", asset }, null, 2)}\n`);
|
|
29
|
+
} else {
|
|
30
|
+
process.stderr.write(`Unknown image-pipeline command: ${command}\n`);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { ImagePipeline, readImageDebug } 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("ImagePipeline", () => {
|
|
16
|
+
it("requires BrandContext", async () => {
|
|
17
|
+
const pipeline = new ImagePipeline();
|
|
18
|
+
await expect(
|
|
19
|
+
pipeline.generate({ type: "icon", subject: "debugger" }, undefined),
|
|
20
|
+
).rejects.toMatchObject({ capability: "image-pipeline", suggestion: expect.any(String) });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("writes a deterministic SVG asset with brand metadata", async () => {
|
|
24
|
+
root = await mkdtemp(join(tmpdir(), "image-pipeline-"));
|
|
25
|
+
const asset = await new ImagePipeline({ root }).generate(
|
|
26
|
+
{ type: "logo", company: "Loop", oneLiner: "AI debugging" },
|
|
27
|
+
createDemoBrandContext(),
|
|
28
|
+
);
|
|
29
|
+
expect(asset.format).toBe("svg");
|
|
30
|
+
expect(await readFile(asset.path, "utf8")).toContain("Loop");
|
|
31
|
+
expect(await readImageDebug(root)).toHaveLength(1);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
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 { CapabilityError } from "@nebutra/errors";
|
|
5
|
+
import {
|
|
6
|
+
assetId,
|
|
7
|
+
type BrandContext,
|
|
8
|
+
type GeneratedAsset,
|
|
9
|
+
requireBrandContext,
|
|
10
|
+
summarizeBrandContext,
|
|
11
|
+
} from "@nebutra/generation-context";
|
|
12
|
+
|
|
13
|
+
export type ImageIntent =
|
|
14
|
+
| { type: "logo"; company: string; oneLiner: string }
|
|
15
|
+
| { type: "poster"; title: string; subtitle?: string }
|
|
16
|
+
| { type: "icon"; subject: string }
|
|
17
|
+
| { type: "hero"; headline: string }
|
|
18
|
+
| { type: "style-transfer"; sourcePath: string; instruction: string };
|
|
19
|
+
|
|
20
|
+
export interface ImageAsset extends GeneratedAsset {
|
|
21
|
+
readonly kind: "image";
|
|
22
|
+
readonly width: number;
|
|
23
|
+
readonly height: number;
|
|
24
|
+
readonly format: "svg" | "png" | "jpg" | "webp";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ImageHealth {
|
|
28
|
+
readonly provider: string;
|
|
29
|
+
readonly ok: boolean;
|
|
30
|
+
readonly suggestion?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ImagePipelineOptions {
|
|
34
|
+
readonly root?: string;
|
|
35
|
+
readonly provider?: "svg-local" | "comfy" | "remote";
|
|
36
|
+
readonly comfyUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readImageDebug(root = process.cwd(), limit = 10): Promise<unknown[]> {
|
|
40
|
+
return readCapabilityDebug("image-pipeline", { root, limit });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function intentLabel(intent: ImageIntent): string {
|
|
44
|
+
switch (intent.type) {
|
|
45
|
+
case "logo":
|
|
46
|
+
return `${intent.company}: ${intent.oneLiner}`;
|
|
47
|
+
case "poster":
|
|
48
|
+
return `${intent.title}${intent.subtitle ? ` — ${intent.subtitle}` : ""}`;
|
|
49
|
+
case "icon":
|
|
50
|
+
return intent.subject;
|
|
51
|
+
case "hero":
|
|
52
|
+
return intent.headline;
|
|
53
|
+
case "style-transfer":
|
|
54
|
+
return `${intent.sourcePath}: ${intent.instruction}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function escapeXml(value: string): string {
|
|
59
|
+
return value
|
|
60
|
+
.replaceAll("&", "&")
|
|
61
|
+
.replaceAll("<", "<")
|
|
62
|
+
.replaceAll(">", ">")
|
|
63
|
+
.replaceAll('"', """);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function svgFor(intent: ImageIntent, brand: BrandContext): string {
|
|
67
|
+
const primary = brand.palette[0]?.hex ?? "#111111";
|
|
68
|
+
const accent = brand.palette.find((color) => color.role === "accent")?.hex ?? "#F3C14B";
|
|
69
|
+
const label = intentLabel(intent);
|
|
70
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
|
|
71
|
+
<rect width="1200" height="800" fill="${escapeXml(primary)}"/>
|
|
72
|
+
<circle cx="960" cy="160" r="96" fill="${escapeXml(accent)}" opacity="0.9"/>
|
|
73
|
+
<text x="80" y="360" fill="#ffffff" font-family="${escapeXml(brand.typography.heading)}" font-size="78" font-weight="700">${escapeXml(brand.name)}</text>
|
|
74
|
+
<text x="84" y="440" fill="#ffffff" font-family="${escapeXml(brand.typography.body)}" font-size="34">${escapeXml(label)}</text>
|
|
75
|
+
<text x="84" y="520" fill="#ffffff" opacity="0.72" font-family="${escapeXml(brand.typography.body)}" font-size="24">${escapeXml(summarizeBrandContext(brand))}</text>
|
|
76
|
+
</svg>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class ImagePipeline {
|
|
80
|
+
readonly #root: string;
|
|
81
|
+
readonly #provider: "svg-local" | "comfy" | "remote";
|
|
82
|
+
readonly #comfyUrl: string;
|
|
83
|
+
|
|
84
|
+
constructor(options: ImagePipelineOptions = {}) {
|
|
85
|
+
this.#root = options.root ?? process.cwd();
|
|
86
|
+
this.#provider = options.provider ?? "svg-local";
|
|
87
|
+
this.#comfyUrl = options.comfyUrl ?? process.env.COMFYUI_URL ?? "http://127.0.0.1:8188";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async generate(intent: ImageIntent, brandInput: BrandContext | undefined): Promise<ImageAsset> {
|
|
91
|
+
const brand = requireBrandContext(brandInput, "image-pipeline");
|
|
92
|
+
if (this.#provider !== "svg-local") {
|
|
93
|
+
throw new CapabilityError("image-pipeline", `${this.#provider} provider is not configured`, {
|
|
94
|
+
suggestion:
|
|
95
|
+
"Use the zero-config svg-local provider or configure a sidecar adapter before selecting model-backed image generation.",
|
|
96
|
+
statusCode: 503,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const id = assetId("image", `${intent.type}_${intentLabel(intent)}`);
|
|
101
|
+
const path = join(this.#root, ".nebutra", "generated", "image-pipeline", `${id}.svg`);
|
|
102
|
+
await mkdir(dirname(path), { recursive: true });
|
|
103
|
+
await writeFile(path, svgFor(intent, brand), "utf8");
|
|
104
|
+
|
|
105
|
+
const asset: ImageAsset = {
|
|
106
|
+
id,
|
|
107
|
+
tenantId: brand.tenantId,
|
|
108
|
+
kind: "image",
|
|
109
|
+
path,
|
|
110
|
+
brandId: brand.brandId,
|
|
111
|
+
provider: "svg-local",
|
|
112
|
+
model: "brand-context-svg-v1",
|
|
113
|
+
createdAt: new Date().toISOString(),
|
|
114
|
+
license: { status: "commercial-ok", source: "deterministic local renderer" },
|
|
115
|
+
width: 1200,
|
|
116
|
+
height: 800,
|
|
117
|
+
format: "svg",
|
|
118
|
+
metadata: { intent, brandSource: brand.sourcePath },
|
|
119
|
+
};
|
|
120
|
+
await appendCapabilityDebug(
|
|
121
|
+
"image-pipeline",
|
|
122
|
+
{ type: "generate", asset },
|
|
123
|
+
{ root: this.#root },
|
|
124
|
+
);
|
|
125
|
+
return asset;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async runWorkflow(
|
|
129
|
+
workflowName: string,
|
|
130
|
+
inputs: Record<string, unknown>,
|
|
131
|
+
brand: BrandContext | undefined,
|
|
132
|
+
): Promise<ImageAsset> {
|
|
133
|
+
requireBrandContext(brand, "image-pipeline");
|
|
134
|
+
return this.generate(
|
|
135
|
+
{
|
|
136
|
+
type: "poster",
|
|
137
|
+
title: workflowName,
|
|
138
|
+
subtitle: Object.keys(inputs).join(", ") || "workflow",
|
|
139
|
+
},
|
|
140
|
+
brand,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async doctor(): Promise<ImageHealth[]> {
|
|
145
|
+
const comfyConfigured = Boolean(process.env.COMFYUI_URL);
|
|
146
|
+
return [
|
|
147
|
+
{ provider: "svg-local", ok: true },
|
|
148
|
+
comfyConfigured
|
|
149
|
+
? { provider: "comfy", ok: true }
|
|
150
|
+
: {
|
|
151
|
+
provider: "comfy",
|
|
152
|
+
ok: false,
|
|
153
|
+
suggestion: `Set COMFYUI_URL to enable model-backed workflow execution. Default probe target is ${this.#comfyUrl}.`,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
provider: "remote",
|
|
157
|
+
ok: Boolean(process.env.IMAGE_REMOTE_API_KEY),
|
|
158
|
+
suggestion: "Set IMAGE_REMOTE_API_KEY to enable remote image fallback.",
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async workflows(): Promise<readonly string[]> {
|
|
164
|
+
return ["logo", "poster", "icon", "hero", "style-transfer"];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async warmup(): Promise<ImageHealth> {
|
|
168
|
+
if (this.#provider === "svg-local") {
|
|
169
|
+
return { provider: this.#provider, ok: true };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
provider: this.#provider,
|
|
173
|
+
ok: false,
|
|
174
|
+
suggestion: "Start the image sidecar before warming model weights.",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
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
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["src/index.ts", "src/cli.ts"],
|
|
5
|
+
format: ["esm"],
|
|
6
|
+
dts: true,
|
|
7
|
+
sourcemap: true,
|
|
8
|
+
clean: true,
|
|
9
|
+
target: "es2022",
|
|
10
|
+
external: ["@nebutra/capability-kit/debug", "@nebutra/errors", "@nebutra/generation-context"],
|
|
11
|
+
});
|