@gjsify/image-generation 0.0.0-stage → 0.64.29

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 CHANGED
@@ -1,3 +1,99 @@
1
- # Temporary Holding Version
1
+ # @gjsify/image-generation
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Image generation in the WebML CG lifecycle shape: an `ImageGenerator` with `availability()`, `create({ monitor, signal })` and `destroy()`, a pure-TypeScript PNG decoder, and a local stable-diffusion.cpp engine for diffusion models on Node and GJS. The engine is a parameter, so an app can plug in a remote provider without gjsify shipping it.
4
+
5
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project. **Tier 3 (experimental, ADR 0003)**: the `ImageGenerator` and the engine's availability logic are unit-tested on Node and GJS; the sd.cpp engine is tested against a fake addon only. No real model has run through this port yet. Design: ADR 0137.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ gjsify install @gjsify/image-generation
11
+ ```
12
+
13
+ The root export needs nothing else. The sd.cpp engine needs its optional peers:
14
+
15
+ ```bash
16
+ gjsify install stable-diffusion-cpp-node-api @stable-diffusion-cpp-node-api/linux-x64 @stable-diffusion-cpp-node-api/linux-x64-vulkan
17
+ gjsify install @gjsify/napi # GJS only
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```typescript
23
+ import { ImageGenerator, decode } from '@gjsify/image-generation';
24
+ import { sdCppImageGeneration } from '@gjsify/image-generation/sdcpp';
25
+
26
+ const engine = sdCppImageGeneration({ store, model: 'my-model' });
27
+
28
+ if ((await ImageGenerator.availability({ engine })) !== 'unavailable') {
29
+ const generator = await ImageGenerator.create({
30
+ engine,
31
+ monitor: (m) => m.addEventListener('downloadprogress', (e) => console.log(e.loaded)),
32
+ });
33
+ const result = await generator.generate({ prompt: 'a cat in a spaceship' });
34
+ const image = await decode(result.blob); // { width, height, data: Uint8ClampedArray }
35
+ await generator.destroy();
36
+ }
37
+ ```
38
+
39
+ `create()` downloads the model when it is missing (through `@gjsify/model-store`, digest-checked) and loads it. `availability()` reads no model bytes: it checks for the prebuilt binaries, the model file, and whether the machine has the memory for it (`@gjsify/machine-fit`).
40
+
41
+ ## Exports
42
+
43
+ - **`.`** — `ImageGenerator`, `decode()`, and the types around them.
44
+ - **`./engine`** — the `ImageEngine` / `ImageEngineFactory` interfaces for writing an engine of your own, local or remote.
45
+ - **`./sdcpp`** — `sdCppImageGeneration(options)`, the engine factory that wraps stable-diffusion.cpp.
46
+
47
+ What `ImageGenerator` guarantees for every engine:
48
+
49
+ - One `generate()` runs at a time; later calls queue behind it.
50
+ - Requests are validated before they reach the engine (positive integers where given, valid init image, finite LoRA multipliers).
51
+ - An aborted `signal` rejects at once. A signal that is already aborted never reaches the engine.
52
+ - `destroy()` waits for the calls it has already accepted, then frees the engine. Later calls reject.
53
+
54
+ ## The sd.cpp engine
55
+
56
+ | Option | Default | |
57
+ |---|---|---|
58
+ | `store` | — | a `ModelStore<ImageModelEntry>` with the model manifest |
59
+ | `model` | — | an id from the store |
60
+ | `gpu` | `auto` | a `GpuSetting`, e.g. `parseGpuSetting(env.MYAPP_GPU)` from `@gjsify/machine-fit` |
61
+ | `gpuGuardDir` | shared by all gjsify GPU engines | where the GPU guard keeps its pin |
62
+ | `probe` | live machine probe | the machine `availability()` asks |
63
+
64
+ The model entry extends `ModelEntry` with sd.cpp-specific roles:
65
+
66
+ - `diffusionModel`: which file in `entry.files` is the diffusion checkpoint (required).
67
+ - `vae`: optional VAE file name.
68
+ - `llm`: optional text-encoder / LLM file name (e.g. Qwen for Z-Image).
69
+ - `backend`: optional per-module device assignment, e.g. `te=cpu`.
70
+ - `defaultWidth` / `defaultHeight`: the size the footprint was measured at.
71
+ - `footprintBytes`: measured working set at the default size.
72
+
73
+ `ImageRequest` carries `prompt`, optional `negativePrompt`, `width`, `height`, `steps`, `guidance`, `seed`, `sampler`, `scheduler`, `loras` and an `init` image for img2img.
74
+
75
+ ### Files the person already has
76
+
77
+ For a checkpoint the person downloaded themselves, pass `local` instead of `store` and `model`. Nothing is fetched or verified:
78
+
79
+ ```typescript
80
+ const engine = sdCppImageGeneration({
81
+ local: { modelId: 'my-checkpoint', diffusionModel: '/models/sdxl.safetensors', vae: '/models/vae.safetensors' },
82
+ });
83
+ ```
84
+
85
+ `availability()` is `available` when every named file exists and `unavailable` otherwise, never `downloadable`. `create()` throws and names the missing files. `LocalImageModel` takes `modelId` and `diffusionModel` (both required), `vae`, `llm`, `backend`, `defaultWidth` / `defaultHeight` (512 when absent) and `footprintBytes`. The memory check runs only when `footprintBytes` is given.
86
+
87
+ **The GPU guard.** A Vulkan driver can hang a process instead of failing. Before going to the GPU, the engine leaves a pin in `gpuGuardDir` and removes it when the work finishes. A pin left over from a crash keeps the next run on the CPU and reports it through `onGpuPinned`. `gpu: 'on'` removes the pin.
88
+
89
+ ### Bundling for GJS
90
+
91
+ `./sdcpp` imports `@gjsify/napi` statically. Evaluating the module loads no addon and no model. The import is static because a bundled dynamic `import('@gjsify/napi')` never settles on GJS: its modules with top-level await become lazy async initialisers, and the import hangs without an error.
92
+
93
+ On GJS the native addon is loaded through `@gjsify/napi`. Start the app with `gjsify run` so the napi typelib is found.
94
+
95
+ On macOS and Windows the sd.cpp engine runs on Node only, because the GJS route loads stable-diffusion-cpp-node-api's Linux prebuilt binaries.
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1 @@
1
+ var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{inflateSync as e}from"node:zlib";const t=new Uint8Array([137,80,78,71,13,10,26,10]);function makeCrcTable(){let e=new Uint32Array(256);for(let t=0;t<256;t++){let n=t;for(let e=0;e<8;e++)n=n&1?3988292384^n>>>1:n>>>1;e[t]=n}return e}const n=makeCrcTable();function crc32(e){let t=-1;for(let r=0;r<e.length;r++)t=n[(t^e[r])&255]^t>>>8;return~t}function readUint32(e,t){return e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3]}function typeOf(e,t){return String.fromCharCode(e[t],e[t+1],e[t+2],e[t+3])}function assertPngSignature(e){if(e.length<t.length)throw Error(`PNG decoder: not a PNG file`);for(let n=0;n<t.length;n++)if(e[n]!==t[n])throw Error(`PNG decoder: not a PNG file`)}function paeth(e,t,n){let r=e+t-n,i=Math.abs(r-e),a=Math.abs(r-t),o=Math.abs(r-n);return i<=a&&i<=o?e:a<=o?t:n}function parseIhdr(e){let t=readUint32(e,0),n=readUint32(e,4),r=e[8],i=e[9],a=e[10],o=e[11],s=e[12];if(a!==0)throw Error(`PNG decoder: unsupported compression method`);if(o!==0)throw Error(`PNG decoder: unsupported filter method`);if(s!==0)throw Error(`PNG decoder: interlaced PNGs are not supported`);if(r!==8)throw Error(`PNG decoder: unsupported bit depth ${r}`);let c=i===2?3:i===6?4:0;if(c===0)throw Error(`PNG decoder: unsupported color type ${i} (only 8-bit RGB/RGBA)`);return{width:t,height:n,bitDepth:r,colorType:i,channels:c}}function unfilter(e,t,n,r){let i=t*r,a=new Uint8ClampedArray(i*n);for(let t=0;t<n;t++){let n=e[t*(i+1)],o=t*(i+1)+1,s=(t-1)*i,c=t*i;for(let l=0;l<i;l++){let i=e[o+l],u=l>=r?a[c+l-r]:0,d=t>0?a[s+l]:0,f=t>0&&l>=r?a[s+l-r]:0,p=0;switch(n){case 0:p=i;break;case 1:p=i+u;break;case 2:p=i+d;break;case 3:p=i+Math.trunc((u+d)/2);break;case 4:p=i+paeth(u,d,f);break;default:throw Error(`PNG decoder: unknown filter ${n} at row ${t}`)}a[c+l]=p&255}}return a}function expandRgbToRgba(e){let t=e.length/3,n=new Uint8ClampedArray(t*4);for(let t=0,r=0;t<e.length;t+=3,r+=4)n[r]=e[t],n[r+1]=e[t+1],n[r+2]=e[t+2],n[r+3]=255;return n}async function decode(n){let r=new Uint8Array(await n.arrayBuffer());assertPngSignature(r);let i=t.length,a,o=[];for(;i<r.length;){if(i+12>r.length)throw Error(`PNG decoder: truncated chunk`);let e=readUint32(r,i),t=typeOf(r,i+4);if(i+12+e>r.length)throw Error(`PNG decoder: truncated chunk data`);let n=r.subarray(i+8,i+8+e);if(readUint32(r,i+8+e)!==crc32(r.subarray(i+4,i+8+e)))throw Error(`PNG decoder: CRC mismatch in ${t} chunk`);switch(t){case`IHDR`:if(a!==void 0)throw Error(`PNG decoder: duplicate IHDR`);a=parseIhdr(n);break;case`IDAT`:o.push(n);break;case`IEND`:i=r.length;break}i+=12+e}if(a===void 0)throw Error(`PNG decoder: missing IHDR`);if(o.length===0)throw Error(`PNG decoder: missing IDAT`);let s=o.reduce((e,t)=>e+t.length,0),c=new Uint8Array(s),l=0;for(let e of o)c.set(e,l),l+=e.length;let u=e(c),d=a.width*a.channels,f=a.height*(d+1);if(u.length!==f)throw Error(`PNG decoder: inflated ${u.length} bytes, expected ${f}`);let p=unfilter(u,a.width,a.height,a.channels);return{width:a.width,height:a.height,data:a.channels===3?expandRgbToRgba(p):p}}export{decode};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{deflateSync as e}from"node:zlib";import{Blob as t}from"@gjsify/buffer";function makeCrcTable(){let e=new Uint32Array(256);for(let t=0;t<256;t++){let n=t;for(let e=0;e<8;e++)n=n&1?3988292384^n>>>1:n>>>1;e[t]=n}return e}const n=makeCrcTable();function crc32(e){let t=-1;for(let r=0;r<e.length;r++)t=n[(t^e[r])&255]^t>>>8;return~t}function writeUint32(e){return new Uint8Array([e>>>24&255,e>>>16&255,e>>>8&255,e&255])}function typeBytes(e){return new TextEncoder().encode(e)}function makeChunk(e,t){let n=typeBytes(e),r=new Uint8Array(n.length+t.length);r.set(n),r.set(t,n.length);let i=new Uint8Array(4+r.length+4);return i.set(writeUint32(t.length)),i.set(r,4),i.set(writeUint32(crc32(r)),4+r.length),i}function encodeRaw(t,n,r,i){if(t.length!==n*r*i)throw Error(`Pixel buffer is ${t.length} bytes, expected ${n*r*i}.`);let a=n*i,o=new Uint8Array(r*(a+1));for(let e=0;e<r;e++)o[e*(a+1)]=0,o.set(t.subarray(e*a,(e+1)*a),e*(a+1)+1);return e(o)}function encodePng(e){let{width:n,height:r,rgb:i}=e,a=new Uint8Array([n>>>24&255,n>>>16&255,n>>>8&255,n&255,r>>>24&255,r>>>16&255,r>>>8&255,r&255,8,2,0,0,0]),o=encodeRaw(i,n,r,3),s=[new Uint8Array([137,80,78,71,13,10,26,10]),makeChunk(`IHDR`,a),makeChunk(`IDAT`,o),makeChunk(`IEND`,new Uint8Array)],c=s.reduce((e,t)=>e+t.length,0),l=new Uint8Array(c),u=0;for(let e of s)l.set(e,u),u+=e.length;return new t([l],{type:`image/png`})}function encodePngRgba(e){let{width:n,height:r,rgba:i}=e,a=new Uint8Array([n>>>24&255,n>>>16&255,n>>>8&255,n&255,r>>>24&255,r>>>16&255,r>>>8&255,r&255,8,6,0,0,0]),o=encodeRaw(i,n,r,4),s=[new Uint8Array([137,80,78,71,13,10,26,10]),makeChunk(`IHDR`,a),makeChunk(`IDAT`,o),makeChunk(`IEND`,new Uint8Array)],c=s.reduce((e,t)=>e+t.length,0),l=new Uint8Array(c),u=0;for(let e of s)l.set(e,u),u+=e.length;return new t([l],{type:`image/png`})}export{encodePng,encodePngRgba};
File without changes
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";function untilAborted(e){if(e===void 0)return{promise:new Promise(()=>{}),dispose(){}};let onAbort=()=>{};return{promise:new Promise((t,n)=>{onAbort=()=>n(e.reason),e.addEventListener(`abort`,onAbort,{once:!0})}),dispose:()=>e.removeEventListener(`abort`,onAbort)}}function validateRequest(e){if(!e.prompt||typeof e.prompt!=`string`)throw TypeError(`ImageRequest.prompt must be a non-empty string`);if(e.width!==void 0&&(!Number.isInteger(e.width)||e.width<=0))throw RangeError(`ImageRequest.width must be a positive integer`);if(e.height!==void 0&&(!Number.isInteger(e.height)||e.height<=0))throw RangeError(`ImageRequest.height must be a positive integer`);if(e.steps!==void 0&&(!Number.isInteger(e.steps)||e.steps<=0))throw RangeError(`ImageRequest.steps must be a positive integer`);if(e.guidance!==void 0&&(!Number.isFinite(e.guidance)||e.guidance<0))throw RangeError(`ImageRequest.guidance must be a non-negative finite number`);if(e.seed!==void 0&&!Number.isInteger(e.seed))throw RangeError(`ImageRequest.seed must be an integer`);if(e.init!==void 0){let{width:t,height:n,rgb:r,strength:i}=e.init;if(!Number.isInteger(t)||t<=0||!Number.isInteger(n)||n<=0)throw RangeError(`InitImage.width and height must be positive integers`);if(!(r instanceof Uint8Array)||r.length!==t*n*3)throw RangeError(`InitImage.rgb must be a Uint8Array of width * height * 3 bytes`);if(!Number.isFinite(i)||i<0||i>1)throw RangeError(`InitImage.strength must be between 0 and 1`)}if(e.loras!==void 0)for(let t of e.loras){if(typeof t.path!=`string`||t.path.length===0)throw TypeError(`LoraRef.path must be a non-empty string`);if(!Number.isFinite(t.multiplier))throw TypeError(`LoraRef.multiplier must be a finite number`)}}var e=class ImageGenerator{#e;#t=Promise.resolve();#n=!1;constructor(e){this.#e=e}static availability(e){return e.engine.availability()}static async create(e){e.signal?.throwIfAborted();let t=await e.engine.create({monitor:e.monitor,signal:e.signal});if(e.signal?.aborted&&(await t.destroy().catch(()=>void 0),e.signal.throwIfAborted()),!t.modelId||typeof t.modelId!=`string`)throw await t.destroy().catch(()=>void 0),Error(`engine ${t.info.name} reports no modelId`);return new ImageGenerator(t)}get engine(){return this.#e.info}get modelId(){return this.#e.modelId}get backend(){return this.#e.backend}async generate(e,t={}){return validateRequest(e),this.#r(t,n=>this.#e.generate(e,{signal:n,onProgress:t.onProgress}))}async destroy(){this.#n||(this.#n=!0,await this.#t.catch(()=>void 0),await this.#e.destroy())}#r(e,t){if(this.#n)return Promise.reject(Error(`ImageGenerator was destroyed`));let{signal:n}=e;if(n?.aborted)return Promise.reject(n.reason);let r=this.#t.catch(()=>void 0).then(async()=>{n?.throwIfAborted();let e=await t(n);if(!e.blob||!(e.blob instanceof Blob))throw TypeError(`engine ${this.#e.info.name} returned no Blob`);if(e.blob.size===0)throw Error(`engine ${this.#e.info.name} returned an empty PNG`);if(!Number.isInteger(e.width)||!Number.isInteger(e.height)||e.width<=0||e.height<=0)throw Error(`engine ${this.#e.info.name} returned invalid size ${e.width}x${e.height}`);return e});this.#t=r;let i=untilAborted(n);return Promise.race([r,i.promise]).finally(i.dispose)}};export{e as ImageGenerator};
@@ -0,0 +1 @@
1
+ import{decode as e}from"./decode.js";import{ImageGenerator as t}from"./generator.js";export{t as ImageGenerator,e as decode};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{existsSync as e}from"node:fs";import{dirname as t,join as n,resolve as r}from"node:path";import i from"node:process";import{hasVulkan as a}from"@gjsify/machine-fit";const o=`GJSIFY_SDCPP_ADDON`,s=`node_stable_diffusion.node`,c=`@stable-diffusion-cpp-node-api`;function ancestors(e){let n=[];for(let i=r(e);;i=t(i))if(n.push(i),t(i)===i)return n}function currentTarget(e=i){return`${e.platform}-${e.arch}`}function addonPath(e,t){return n(e,`node_modules`,c,t,s)}function findAddon(e=i.env,t){return findAddonForTarget(currentTarget(),t,e)}var AddonMissingError=class extends Error{};const l=new Set([`linux-x64`,`linux-arm64`]),u=new Set([`linux-x64`,`linux-arm64`,`darwin-arm64`,`darwin-x64`,`win32-x64`]);function hasPrebuiltBins(e=currentTarget(),t=runningOnGjs()){return t?l.has(e):u.has(e)}function runningOnGjs(e=globalThis){return e.imports!==void 0&&e.process?.versions?.gjs!==void 0}function backendCandidates(e,t,n){if(!l.has(e)||t===`off`||e!==`linux-x64`)return[e];let r=`${e}-vulkan`;return t===`on`||a(n)?[r,e]:[e]}function addonDir(e=currentTarget(),n){return t(findAddonForTarget(e,n))}function findAddonForTarget(t,r,a=i.env){let c=a[o]?.trim();if(c){let t=c.endsWith(`.node`)?c:n(c,s);if(e(t))return t;throw new AddonMissingError(`${o}=${c} has no ${s}`)}let l=[...r?ancestors(r):[],...ancestors(i.cwd())];for(let n of l){let r=addonPath(n,t);if(e(r))return r}throw new AddonMissingError(`No stable-diffusion.cpp addon for ${t}: install stable-diffusion-cpp-node-api or set ${o}.`)}export{o as ADDON_ENV,s as ADDON_FILE,AddonMissingError,c as SDCPP_PACKAGE,addonDir,addonPath,backendCandidates,currentTarget,findAddon,hasPrebuiltBins,runningOnGjs};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{encodePng as e}from"./encode.js";import{currentTarget as t,hasPrebuiltBins as n,runningOnGjs as r}from"./sdcpp-bin.js";import{existsSync as i}from"node:fs";import{checkFit as a,createGpuGuard as o,defaultGuardDir as s,onBattery as c,readMachineMemory as l,systemProbe as u}from"@gjsify/machine-fit";const d={name:`sd.cpp`,locality:`local`};var GenerationAborted=class extends Error{constructor(){super(`Aborted`),this.name=`AbortError`}};function toNativeOptions(e,t,n){let r={prompt:e.prompt,width:t,height:n,seed:e.seed??Math.floor(Math.random()*2**32),sampleParams:{sampleSteps:e.steps??20,guidance:{txtCfg:e.guidance??7.5}}};return e.negativePrompt&&(r.negativePrompt=e.negativePrompt),e.sampler&&(r.sampleParams.sampleMethod=e.sampler),e.scheduler&&(r.sampleParams.scheduler=e.scheduler),e.loras?.length&&(r.loras=e.loras.map(e=>({path:e.path,multiplier:e.multiplier}))),e.init&&(r.initImage={width:e.init.width,height:e.init.height,channel:3,data:e.init.rgb},r.strength=e.init.strength),r}function contextOptions(e){let t={diffusionModelPath:e.diffusionModel};return e.vae&&(t.vaePath=e.vae),e.llm&&(t.llmPath=e.llm),e.backend&&(t.backend=e.backend),t}function toNativeContext(e,t){return contextOptions({diffusionModel:t[e.diffusionModel],vae:e.vae?t[e.vae]:void 0,llm:e.llm?t[e.llm]:void 0,backend:e.backend})}function storeSource(e){let{store:t}=e,n=t.require(e.model);return assertModelFiles(n),{modelId:n.modelId,backend:n.backend,footprintBytes:n.footprintBytes,width:n.defaultWidth,height:n.defaultHeight,availability:async()=>t.availability(n.id),async prepare(e){let r=await t.install(n.id,{monitor:e.monitor,signal:e.signal});return{diffusionModel:r.files[n.diffusionModel],vae:n.vae?r.files[n.vae]:void 0,llm:n.llm?r.files[n.llm]:void 0,backend:n.backend}}}}function missingFiles(e){return[e.diffusionModel,e.vae,e.llm].filter(e=>e!==void 0&&e!==``&&!i(e))}function localSource(e){if(!e.modelId)throw Error(`sd.cpp local model needs a modelId`);if(!e.diffusionModel)throw Error(`sd.cpp local model '${e.modelId}' needs a diffusionModel path`);return{modelId:e.modelId,backend:e.backend,footprintBytes:e.footprintBytes,width:e.defaultWidth??512,height:e.defaultHeight??512,availability:async()=>missingFiles(e).length===0?`available`:`unavailable`,async prepare(){let t=missingFiles(e);if(t.length>0)throw Error(`sd.cpp model '${e.modelId}' has missing files: ${t.join(`, `)}`);return{diffusionModel:e.diffusionModel,vae:e.vae,llm:e.llm,backend:e.backend}}}}function resolveSize(e,t){return{width:e.width??t.width,height:e.height??t.height}}let f=!1;function defaultOnGpuPinned(e){console.error(`The GPU is switched off for image generation: process ${e.pid} died while using it (${e.at}). Turn the GPU setting on to try it again.`)}function assertModelFiles(e){let t=new Set(e.files.map(e=>e.file));if(!t.has(e.diffusionModel))throw Error(`sd.cpp model '${e.id}' names '${e.diffusionModel}' as diffusionModel, but it is not in files`);if(e.vae&&!t.has(e.vae))throw Error(`sd.cpp model '${e.id}' names '${e.vae}' as vae, but it is not in files`);if(e.llm&&!t.has(e.llm))throw Error(`sd.cpp model '${e.id}' names '${e.llm}' as llm, but it is not in files`)}function isCpuOnly(e){return e===void 0?!1:e.split(`,`).every(e=>e.trim().toLowerCase()===`cpu`)}function createSdCppEngine(i,p){let m=`local`in p?localSource(p.local):storeSource(p),h=p.gpu??`auto`,g,gpuGuard=()=>g??=o({dir:p.gpuGuardDir??s(`gjsify`)}),gpuBarred=()=>{if(isCpuOnly(m.backend)||h===`off`)return!0;if(h===`on`)return gpuGuard().clear(),!1;let e=gpuGuard().pin();return e===void 0?!1:(f||(f=!0,(p.onGpuPinned??defaultOnGpuPinned)(e)),!0)};return{info:d,async availability(){let e=p.probe??u();if(c(e)||!n(t(e),r()))return`unavailable`;if(m.footprintBytes!==void 0){let t=l(e);if(a(m.footprintBytes,t).placement===`none`)return`unavailable`}return m.availability()},async create(t={}){let n=await m.prepare(t);t.signal?.throwIfAborted();let r,a,o=!1,open=async()=>{r??=await i();let e=gpuBarred(),t=contextOptions(n);e&&!t.backend&&(t.backend=`cpu`),a??=await r.StableDiffusionContext.create(t),o=!e};if(await open(),!a)throw Error(`sd.cpp context did not open`);let generate=async t=>{let{width:n,height:r}=resolveSize(t,m),i=toNativeOptions(t,n,r),o=(await a.generateImage(i))[0];if(!o)throw Error(`The engine returned no image.`);if(o.channel!==3)throw Error(`sd.cpp returned ${o.channel} channels, expected 3`);let s=new Uint8Array(o.data);return{blob:e({width:o.width,height:o.height,rgb:s}),width:o.width,height:o.height,seed:i.seed,modelId:m.modelId}};return{info:d,modelId:m.modelId,get backend(){return o?`gpu`:`cpu`},async generate(e,t={}){let{signal:n,onProgress:i}=t;if(n?.aborted)throw new GenerationAborted;let onAbort=()=>a.abort();n?.addEventListener(`abort`,onAbort,{once:!0});let work=async()=>(r.setProgressCallback(i?({step:e,steps:t,time:n})=>i({step:e,steps:t,seconds:n}):null),await generate(e));try{return o?await gpuGuard().during(work):await work()}catch(e){throw n?.aborted?new GenerationAborted:e}finally{n?.removeEventListener(`abort`,onAbort),r.setProgressCallback(null)}},async destroy(){a?.close(),a=void 0}}}}}export{GenerationAborted,d as SDCPP_ENGINE,createSdCppEngine,toNativeContext,toNativeOptions};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{AddonMissingError as e,addonDir as t,addonPath as n,backendCandidates as r,currentTarget as i,findAddon as a,hasPrebuiltBins as o,runningOnGjs as s}from"./sdcpp-bin.js";import{SDCPP_ENGINE as c,createSdCppEngine as l,toNativeContext as u,toNativeOptions as d}from"./sdcpp-engine.js";import{dirname as f}from"node:path";import{fileURLToPath as p}from"node:url";import{loadAddon as m}from"@gjsify/napi";async function loadSdAddon(){return m(a(process.env,f(p(import.meta.url))))}function sdCppImageGeneration(e){return l(loadSdAddon,e)}export{e as AddonMissingError,c as SDCPP_ENGINE,t as addonDir,n as addonPath,r as backendCandidates,i as currentTarget,a as findAddon,o as hasPrebuiltBins,s as runningOnGjs,sdCppImageGeneration,u as toNativeContext,d as toNativeOptions};
@@ -0,0 +1,10 @@
1
+ import type { DecodedImage } from './engine.js';
2
+ export interface PngHeader {
3
+ readonly width: number;
4
+ readonly height: number;
5
+ readonly bitDepth: number;
6
+ readonly colorType: number;
7
+ readonly channels: number;
8
+ }
9
+ /** Decode a PNG blob to the structural shape of `ImageData`. */
10
+ export declare function decode(blob: Blob): Promise<DecodedImage>;
@@ -0,0 +1,16 @@
1
+ export interface RgbImage {
2
+ readonly width: number;
3
+ readonly height: number;
4
+ /** Tightly packed RGB, `width * height * 3` bytes. */
5
+ readonly rgb: Uint8Array;
6
+ }
7
+ export interface RgbaImage {
8
+ readonly width: number;
9
+ readonly height: number;
10
+ /** Tightly packed RGBA, `width * height * 4` bytes. */
11
+ readonly rgba: Uint8Array;
12
+ }
13
+ /** Encode an RGB image as a PNG `Blob`. */
14
+ export declare function encodePng(image: RgbImage): Blob;
15
+ /** Encode an RGBA image as a PNG `Blob`. */
16
+ export declare function encodePngRgba(image: RgbaImage): Blob;
@@ -0,0 +1,83 @@
1
+ import type { CreateMonitorCallback } from '@gjsify/model-store';
2
+ /** The WebML CG availability states. */
3
+ export type Availability = 'unavailable' | 'downloadable' | 'downloading' | 'available';
4
+ /** Which engine runs, so a surface can say where the image is drawn (ADR 0132 § 3). */
5
+ export interface EngineInfo {
6
+ readonly name: string;
7
+ readonly locality: 'local' | 'remote';
8
+ }
9
+ export interface LoraRef {
10
+ /** Verified file path for this LoRA. */
11
+ readonly path: string;
12
+ /** Strength, typically 0..1. */
13
+ readonly multiplier: number;
14
+ }
15
+ /** A starting image for img2img; `strength` 0..1 says how far to move from it. */
16
+ export interface InitImage {
17
+ readonly width: number;
18
+ readonly height: number;
19
+ /** Tightly packed RGB, `width * height * 3` bytes. */
20
+ readonly rgb: Uint8Array;
21
+ readonly strength: number;
22
+ }
23
+ /** What to draw. Defaults and upper bounds are the engine's. */
24
+ export interface ImageRequest {
25
+ readonly prompt: string;
26
+ readonly negativePrompt?: string;
27
+ readonly width?: number;
28
+ readonly height?: number;
29
+ readonly steps?: number;
30
+ /** Classifier-free guidance scale. */
31
+ readonly guidance?: number;
32
+ readonly seed?: number;
33
+ readonly sampler?: string;
34
+ readonly scheduler?: string;
35
+ readonly loras?: readonly LoraRef[];
36
+ readonly init?: InitImage;
37
+ }
38
+ export interface GenerateProgress {
39
+ readonly step: number;
40
+ readonly steps: number;
41
+ /** Seconds spent so far on the current step. */
42
+ readonly seconds: number;
43
+ }
44
+ export interface GenerateOptions {
45
+ readonly signal?: AbortSignal;
46
+ readonly onProgress?: (progress: GenerateProgress) => void;
47
+ }
48
+ /** The structural shape of `ImageData` from `@gjsify/canvas2d-core`, without the dependency. */
49
+ export interface DecodedImage {
50
+ readonly width: number;
51
+ readonly height: number;
52
+ /** RGBA, `width * height * 4` bytes. */
53
+ readonly data: Uint8ClampedArray;
54
+ }
55
+ /** One generated image: the engine's PNG bytes plus the metadata needed to record it. */
56
+ export interface ImageResult {
57
+ readonly blob: Blob;
58
+ readonly width: number;
59
+ readonly height: number;
60
+ /** The seed the engine used, so a liked image can be reproduced. */
61
+ readonly seed: number;
62
+ readonly modelId: string;
63
+ }
64
+ export interface EngineCreateOptions {
65
+ readonly monitor?: CreateMonitorCallback;
66
+ readonly signal?: AbortSignal;
67
+ }
68
+ /** A loaded model. Every method may be called again after the previous call settled. */
69
+ export interface ImageEngine {
70
+ readonly info: EngineInfo;
71
+ /** What produced the image, recorded by a gallery next to it. */
72
+ readonly modelId: string;
73
+ /** Where the image is drawn right now, when the engine knows; it can change mid-run. */
74
+ readonly backend?: 'gpu' | 'cpu';
75
+ generate(request: ImageRequest, options?: GenerateOptions): Promise<ImageResult>;
76
+ destroy(): Promise<void>;
77
+ }
78
+ /** How `ImageGenerator` asks an engine whether it can run, and starts it. */
79
+ export interface ImageEngineFactory {
80
+ readonly info: EngineInfo;
81
+ availability(): Promise<Availability>;
82
+ create(options?: EngineCreateOptions): Promise<ImageEngine>;
83
+ }
@@ -0,0 +1,20 @@
1
+ import type { Availability, EngineCreateOptions, EngineInfo, GenerateOptions, ImageEngineFactory, ImageRequest, ImageResult } from './engine.js';
2
+ export interface ImageGeneratorAvailabilityOptions {
3
+ readonly engine: ImageEngineFactory;
4
+ }
5
+ export interface ImageGeneratorCreateOptions extends EngineCreateOptions {
6
+ readonly engine: ImageEngineFactory;
7
+ }
8
+ export declare class ImageGenerator {
9
+ #private;
10
+ private constructor();
11
+ static availability(options: ImageGeneratorAvailabilityOptions): Promise<Availability>;
12
+ /** Downloads the model when the engine needs one and it is missing, then loads it. */
13
+ static create(options: ImageGeneratorCreateOptions): Promise<ImageGenerator>;
14
+ get engine(): EngineInfo;
15
+ get modelId(): string;
16
+ get backend(): 'gpu' | 'cpu' | undefined;
17
+ generate(request: ImageRequest, options?: GenerateOptions): Promise<ImageResult>;
18
+ /** Waits for a running call, then frees the model. Later calls reject. */
19
+ destroy(): Promise<void>;
20
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,3 @@
1
+ export { ImageGenerator, type ImageGeneratorAvailabilityOptions, type ImageGeneratorCreateOptions, } from './generator.js';
2
+ export { decode } from './decode.js';
3
+ export type { Availability, DecodedImage, EngineCreateOptions, EngineInfo, GenerateOptions, GenerateProgress, ImageEngine, ImageEngineFactory, ImageRequest, ImageResult, InitImage, LoraRef, } from './engine.js';
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,40 @@
1
+ import { type GpuSetting, type MachineProbe } from '@gjsify/machine-fit';
2
+ export declare const ADDON_ENV = "GJSIFY_SDCPP_ADDON";
3
+ export declare const ADDON_FILE = "node_stable_diffusion.node";
4
+ export declare const SDCPP_PACKAGE = "@stable-diffusion-cpp-node-api";
5
+ /** This machine, as stable-diffusion-cpp-node-api names its binary directories. */
6
+ export declare function currentTarget(probe?: Pick<MachineProbe, 'platform' | 'arch'>): string;
7
+ /** The addon file inside the package's per-target directory. */
8
+ export declare function addonPath(root: string, target: string): string;
9
+ /**
10
+ * Where the addon is: `GJSIFY_SDCPP_ADDON` (a `.node` file or its directory)
11
+ * first, then the `node_modules` above the running bundle and above the working
12
+ * directory — a bundle cannot resolve a package at run time, but it sits inside
13
+ * the checkout that installed it.
14
+ */
15
+ export declare function findAddon(env?: Record<string, string | undefined>, here?: string): string;
16
+ export declare class AddonMissingError extends Error {
17
+ }
18
+ /** Is there an addon build for this machine? A table lookup, so it is free to ask. */
19
+ export declare function hasPrebuiltBins(target?: string, onGjs?: boolean): boolean;
20
+ /** What {@link runningOnGjs} looks at; injected so the Node-with-node-gi case is testable. */
21
+ export interface RuntimeHost {
22
+ imports?: unknown;
23
+ process?: {
24
+ versions?: {
25
+ gjs?: string;
26
+ };
27
+ };
28
+ }
29
+ /**
30
+ * Is this GJS? Not "is there an `imports`": node-gi defines `globalThis.imports`
31
+ * on Node too, and asking only that sent a Node app down the `@gjsify/napi` route.
32
+ */
33
+ export declare function runningOnGjs(host?: RuntimeHost): boolean;
34
+ /**
35
+ * The GJS builds to try, best first; the last is always the CPU build. Only
36
+ * Linux x64 has a Vulkan variant.
37
+ */
38
+ export declare function backendCandidates(target: string, gpu: GpuSetting, probe: MachineProbe): string[];
39
+ /** The directory holding the addon and its backends, for `@gjsify/napi`. */
40
+ export declare function addonDir(target?: string, here?: string): string;
@@ -0,0 +1,130 @@
1
+ import type { ModelEntry, ModelStore } from '@gjsify/model-store';
2
+ import { type GpuPin, type GpuSetting, type MachineProbe } from '@gjsify/machine-fit';
3
+ import type { EngineInfo, ImageEngineFactory, ImageRequest } from './engine.js';
4
+ export declare const SDCPP_ENGINE: EngineInfo;
5
+ /** A fetchable image model: one or more verified files plus sd.cpp's role mapping. */
6
+ export interface ImageModelEntry extends ModelEntry {
7
+ /** What a gallery records next to the image (`ImageGenerator.modelId`). */
8
+ readonly modelId: string;
9
+ /** Which file in {@link ModelEntry.files} is the diffusion checkpoint. */
10
+ readonly diffusionModel: string;
11
+ /** Optional VAE file name. */
12
+ readonly vae?: string;
13
+ /** Optional text-encoder / LLM file name (e.g. Qwen for Z-Image). */
14
+ readonly llm?: string;
15
+ /** Per-module device assignment, e.g. `te=cpu` — see sd.cpp's docs/backend.md. */
16
+ readonly backend?: string;
17
+ /** Default width the footprint was measured at. */
18
+ readonly defaultWidth: number;
19
+ /** Default height the footprint was measured at. */
20
+ readonly defaultHeight: number;
21
+ /** Working set at defaultWidth/defaultHeight, measured, not derived from file size. */
22
+ readonly footprintBytes: number;
23
+ }
24
+ /** Fields every sd.cpp engine takes, whatever supplies the model files. */
25
+ export interface SdCppCommonOptions {
26
+ /** The person's GPU switch, e.g. `parseGpuSetting(env.MYAPP_GPU)`. `on` also lifts a pin. */
27
+ readonly gpu?: GpuSetting;
28
+ /**
29
+ * Where the GPU guard keeps its pin. Default: the one shared by every
30
+ * gjsify GPU engine (ADR 0132 § 6).
31
+ */
32
+ readonly gpuGuardDir?: string;
33
+ /** Told once per process when a pin keeps this engine on the CPU. Default: a line on stderr. */
34
+ readonly onGpuPinned?: (pin: GpuPin) => void;
35
+ /** The machine `availability()` asks. */
36
+ readonly probe?: MachineProbe;
37
+ }
38
+ /** The model comes from a {@link ModelStore}: fetched, verified and placed by the store. */
39
+ export interface SdCppEngineOptions<E extends ImageModelEntry = ImageModelEntry> extends SdCppCommonOptions {
40
+ /** A store that contains the model manifest. */
41
+ readonly store: ModelStore<E>;
42
+ /** Model id from the store; `create()` requires one. */
43
+ readonly model: string;
44
+ }
45
+ /**
46
+ * A model that already sits on disk, chosen by the person (a checkpoint they downloaded
47
+ * themselves). Nothing is fetched or verified: the files either exist or the engine is
48
+ * `unavailable`.
49
+ */
50
+ export interface LocalImageModel {
51
+ /** What a gallery records next to the image (`ImageGenerator.modelId`). */
52
+ readonly modelId: string;
53
+ /** Path of the diffusion checkpoint. */
54
+ readonly diffusionModel: string;
55
+ /** Path of the VAE, if the model needs a separate one. */
56
+ readonly vae?: string;
57
+ /** Path of the text encoder / LLM, if the model family needs one. */
58
+ readonly llm?: string;
59
+ /** Per-module device assignment, e.g. `te=cpu` — see sd.cpp's docs/backend.md. */
60
+ readonly backend?: string;
61
+ /** Size used when a request names none. Default 512x512. */
62
+ readonly defaultWidth?: number;
63
+ readonly defaultHeight?: number;
64
+ /**
65
+ * Working set at the default size. Optional: without it `availability()` cannot say the
66
+ * machine is too small, and the model simply tries to load.
67
+ */
68
+ readonly footprintBytes?: number;
69
+ }
70
+ export interface SdCppLocalOptions extends SdCppCommonOptions {
71
+ readonly local: LocalImageModel;
72
+ }
73
+ export type SdCppOptions<E extends ImageModelEntry = ImageModelEntry> = SdCppEngineOptions<E> | SdCppLocalOptions;
74
+ export interface NativeGenerateOptions {
75
+ prompt: string;
76
+ negativePrompt?: string;
77
+ width: number;
78
+ height: number;
79
+ seed: number;
80
+ sampleParams: {
81
+ sampleSteps: number;
82
+ guidance: {
83
+ txtCfg: number;
84
+ };
85
+ sampleMethod?: string;
86
+ scheduler?: string;
87
+ };
88
+ loras?: Array<{
89
+ path: string;
90
+ multiplier: number;
91
+ }>;
92
+ initImage?: {
93
+ width: number;
94
+ height: number;
95
+ channel: number;
96
+ data: Uint8Array;
97
+ };
98
+ strength?: number;
99
+ }
100
+ export interface NativeImage {
101
+ width: number;
102
+ height: number;
103
+ channel: number;
104
+ data: ArrayLike<number>;
105
+ }
106
+ export interface NativeContext {
107
+ generateImage(options: NativeGenerateOptions): Promise<NativeImage[]>;
108
+ abort(): void;
109
+ close(): void;
110
+ }
111
+ /** The slice of the addon this engine uses; also what tests fake. */
112
+ export interface SdAddon {
113
+ StableDiffusionContext: {
114
+ create(options: Record<string, unknown>): Promise<NativeContext>;
115
+ };
116
+ setProgressCallback(callback: ((data: {
117
+ step: number;
118
+ steps: number;
119
+ time: number;
120
+ }) => void) | null): void;
121
+ setLogCallback?(callback: ((level: number, message: string) => void) | null): void;
122
+ }
123
+ export declare class GenerationAborted extends Error {
124
+ constructor();
125
+ }
126
+ /** Request → the addon's option object. Pure, so the mapping is testable without a GPU. */
127
+ export declare function toNativeOptions(request: ImageRequest, width: number, height: number): NativeGenerateOptions;
128
+ export declare function toNativeContext(model: ImageModelEntry, files: Record<string, string>): Record<string, unknown>;
129
+ /** {@link sdCppImageGeneration} with the addon loader passed in. */
130
+ export declare function createSdCppEngine<E extends ImageModelEntry = ImageModelEntry>(loadAddon: () => Promise<SdAddon>, options: SdCppOptions<E>): ImageEngineFactory;
@@ -0,0 +1,10 @@
1
+ import type { ImageEngineFactory } from './engine.js';
2
+ import { type ImageModelEntry, type SdCppOptions } from './sdcpp-engine.js';
3
+ export { SDCPP_ENGINE, type ImageModelEntry, type LocalImageModel, type NativeGenerateOptions, type NativeImage, type SdAddon, type SdCppCommonOptions, type SdCppEngineOptions, type SdCppLocalOptions, type SdCppOptions, toNativeContext, toNativeOptions, } from './sdcpp-engine.js';
4
+ export { addonDir, addonPath, AddonMissingError, backendCandidates, currentTarget, findAddon, hasPrebuiltBins, runningOnGjs, } from './sdcpp-bin.js';
5
+ /**
6
+ * The sd.cpp image-generation engine factory, for `ImageGenerator.availability()` and
7
+ * `ImageGenerator.create()`. Pass `{ store, model }` for a fetched model or `{ local }` for
8
+ * files the person already has.
9
+ */
10
+ export declare function sdCppImageGeneration<E extends ImageModelEntry = ImageModelEntry>(options: SdCppOptions<E>): ImageEngineFactory;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,100 @@
1
1
  {
2
- "name": "@gjsify/image-generation",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@gjsify/image-generation",
3
+ "version": "0.64.29",
4
+ "description": "Image generation in the WebML CG lifecycle shape: an ImageGenerator with availability(), create({ monitor, signal }) and destroy(), and a local stable-diffusion.cpp engine for diffusion models on Node and GJS.",
5
+ "module": "lib/esm/index.js",
6
+ "types": "lib/types/index.d.ts",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/types/index.d.ts",
12
+ "default": "./lib/esm/index.js"
13
+ },
14
+ "./engine": {
15
+ "types": "./lib/types/engine.d.ts",
16
+ "default": "./lib/esm/engine.js"
17
+ },
18
+ "./sdcpp": {
19
+ "types": "./lib/types/sdcpp.d.ts",
20
+ "default": "./lib/esm/sdcpp.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "lib",
25
+ "!lib/types/**/*.spec.d.ts"
26
+ ],
27
+ "scripts": {
28
+ "clear": "gjsify clear lib tmp tsconfig.tsbuildinfo test.gjs.mjs test.node.mjs",
29
+ "check": "gjsify tsc --noEmit",
30
+ "build": "gjsify run build:gjsify && gjsify run build:types",
31
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
32
+ "build:types": "gjsify tsc",
33
+ "build:test": "gjsify run build:test:gjs && gjsify run build:test:node",
34
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs --external @gjsify/napi",
35
+ "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs --external @gjsify/napi",
36
+ "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:node && gjsify run test:gjs",
37
+ "test:gjs": "gjsify run test.gjs.mjs",
38
+ "test:node": "node test.node.mjs"
39
+ },
40
+ "keywords": [
41
+ "image-generation",
42
+ "diffusion",
43
+ "ai",
44
+ "stable-diffusion",
45
+ "sd.cpp",
46
+ "webml"
47
+ ],
48
+ "dependencies": {
49
+ "@gjsify/buffer": "^0.64.29",
50
+ "@gjsify/fs": "^0.64.29",
51
+ "@gjsify/machine-fit": "^0.64.29",
52
+ "@gjsify/model-store": "^0.64.29",
53
+ "@gjsify/path": "^0.64.29",
54
+ "@gjsify/process": "^0.64.29",
55
+ "@gjsify/zlib": "^0.64.29"
56
+ },
57
+ "peerDependencies": {
58
+ "@gjsify/napi": "^0.64.19"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@gjsify/napi": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "devDependencies": {
66
+ "@gjsify/cli": "^0.64.29",
67
+ "@gjsify/unit": "^0.64.29",
68
+ "@types/node": "^25.9.2",
69
+ "typescript": "^6.0.3"
70
+ },
71
+ "license": "MIT",
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "git+https://github.com/gjsify/gjsify.git",
75
+ "directory": "packages/gjs/image-generation"
76
+ },
77
+ "bugs": {
78
+ "url": "https://github.com/gjsify/gjsify/issues"
79
+ },
80
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/gjs/image-generation#readme",
81
+ "gjsify": {
82
+ "tier": 3,
83
+ "runtimes": {
84
+ "gjs": "polyfill",
85
+ "node": "none",
86
+ "browser": "none",
87
+ "nativescript": "none"
88
+ },
89
+ "os": {
90
+ "linux": "supported",
91
+ "darwin": "partial",
92
+ "win32": "partial"
93
+ },
94
+ "osNotes": {
95
+ "darwin": "The sd.cpp engine runs on Node only: the GJS route loads stable-diffusion.cpp's Linux prebuilt binaries.",
96
+ "win32": "The sd.cpp engine runs on Node only: the GJS route loads stable-diffusion.cpp's Linux prebuilt binaries."
97
+ },
98
+ "headless": true
99
+ }
100
+ }