Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b5ef1a6
feat: add diffusion SDK plugin integration
donriddo Mar 12, 2026
2c954a8
feat(diffusion): consolidate SDK plugin, fix sampling_method schema, …
donriddo Mar 12, 2026
55ce991
fix(diffusion): register generationStream in bare-client handler map
donriddo Mar 13, 2026
c1efe8e
feat(diffusion): add diffusion naming handler for update-models codegen
donriddo Mar 13, 2026
7a9adb7
feat(diffusion): sync registry models and use FLUX constant in tests
donriddo Mar 13, 2026
9033b98
fix(diffusion): prevent statsPromise hang and fix lint issues
donriddo Mar 15, 2026
e354da0
revert: remove non-matching patterns from generation client
donriddo Mar 15, 2026
8eebd62
chore: remove unrelated model history file
donriddo Mar 15, 2026
e312510
fix: configure FLUX companion models and GPU device for diffusion tests
donriddo Mar 15, 2026
9fdafc1
fix(examples): configure FLUX companion models consistently across al…
donriddo Mar 16, 2026
b05d038
fix(tests): use llm addon elephant.jpg for img2img test fixture
donriddo Mar 16, 2026
5b5aa5a
fix(examples): use path.resolve for img2img default image path
donriddo Mar 16, 2026
bf6e3cf
fix(tests): migrate generation executor to ResourceManager pattern
donriddo Mar 16, 2026
46d059f
feat(api): expose progressStream in generation() client helper
donriddo Mar 16, 2026
dd48917
refactor(api): use background fan-out loop for generation() streams
donriddo Mar 16, 2026
ed52421
chore: regenerate bun.lock and models registry after rebase
donriddo Mar 19, 2026
c9c4153
fix[api]: align SDK diffusion schemas with addon contract
donriddo Mar 19, 2026
2da4310
fix[api]: align SDK rng config with addon contract
donriddo Mar 19, 2026
68bb011
mod[api]: rename public API from generation() to diffusion()
donriddo Mar 19, 2026
e4f0ee8
fix: resolve pre-existing lint errors in diffusion client and load-model
donriddo Mar 19, 2026
8087dae
mod[api]: remove img2img functionality until addon support lands
donriddo Mar 22, 2026
a81c52a
feat[api]: wire up profiler and device defaults for diffusion addon
donriddo Mar 22, 2026
d6d6f8f
fix[api]: align diffusion client API with actual streaming behavior
donriddo Mar 24, 2026
a58a04c
chore: clean up internal comments from public-facing API
donriddo Mar 24, 2026
48038c0
fix[api]: add positive constraint to width/height, describe config fi…
donriddo Mar 25, 2026
77bd1ca
fix: add missing validator to download test custom expectation
donriddo Apr 1, 2026
91de5e8
fix: add mobile diffusion support, move executor to shared, bump test…
donriddo Apr 1, 2026
4667852
fix: use exported SDK model constant in diffusion-simple example
donriddo Apr 1, 2026
7370b60
fix[api]: address PR review comments for diffusion SDK integration
donriddo Apr 1, 2026
ebfb45b
Merge remote-tracking branch 'upstream/main' into feat/diffusion-sdk-…
donriddo Apr 1, 2026
4d730a4
fix: add eslint-disable for optional MCP SDK import in example
donriddo Apr 1, 2026
16a52d7
fix: bump diffusion-cpp to 0.1.1 for absolute path fix
donriddo Apr 3, 2026
37e5a24
fix: bump diffusion-cpp to 0.1.1 for absolute path fix
donriddo Apr 3, 2026
b61e727
Merge remote-tracking branch 'upstream/main' into feat/diffusion-sdk-…
donriddo Apr 3, 2026
c10fe12
Merge branch 'feat/diffusion-sdk-plugin-v2' of github.com:donriddo/qv…
donriddo Apr 3, 2026
7dd1d20
Merge branch 'main' into feat/diffusion-sdk-plugin-v2
donriddo Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
532 changes: 260 additions & 272 deletions packages/sdk/bun.lock

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions packages/sdk/client/api/diffusion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
diffusionStreamResponseSchema,
type DiffusionStreamRequest,
type DiffusionClientParams,
type DiffusionStats,
} from "@/schemas";
import { stream as streamRpc } from "@/client/rpc/rpc-client";

export interface DiffusionProgressTick {
step: number;
totalSteps: number;
elapsedMs: number;
}

interface DiffusionResult {
progressStream: AsyncGenerator<DiffusionProgressTick>;
outputs: Promise<Buffer[]>;
stats: Promise<DiffusionStats | undefined>;
}

/**
* Generate images using a loaded diffusion model.
*
* @example
* ```typescript
* // Basic usage
* const { outputs, stats } = diffusion({ modelId, prompt: "a cat" });
* const buffers = await outputs;
* fs.writeFileSync("output.png", buffers[0]);
*
* // With progress tracking
* const { progressStream, outputs } = diffusion({ modelId, prompt: "a cat" });
* for await (const { step, totalSteps } of progressStream) {
* console.log(`${step}/${totalSteps}`);
* }
* const buffers = await outputs;
* ```
*/
export function diffusion(params: DiffusionClientParams): DiffusionResult {
const request: DiffusionStreamRequest = {
type: "diffusionStream",
...params,
};

let statsResolver: (value: DiffusionStats | undefined) => void = () => {};
let statsRejecter: (error: unknown) => void = () => {};
const statsPromise = new Promise<DiffusionStats | undefined>(
(resolve, reject) => {
statsResolver = resolve;
statsRejecter = reject;
},
);
statsPromise.catch(() => {});

const progressQueue: DiffusionProgressTick[] = [];
const collectedBuffers: Buffer[] = [];
let progressDone = false;
let progressResolve: (() => void) | null = null;
let streamError: Error | null = null;

let outputsResolver: (value: Buffer[]) => void = () => {};
let outputsRejecter: (error: unknown) => void = () => {};
const outputsPromise = new Promise<Buffer[]>((resolve, reject) => {
outputsResolver = resolve;
outputsRejecter = reject;
});
outputsPromise.catch(() => {});

const processResponses = async () => {
try {
for await (const response of streamRpc(request)) {
if (
response &&
typeof response === "object" &&
"type" in response &&
response.type === "diffusionStream"
) {
const parsed = diffusionStreamResponseSchema.parse(response);

if (parsed.step != null && parsed.totalSteps != null && parsed.elapsedMs != null) {
progressQueue.push({ step: parsed.step, totalSteps: parsed.totalSteps, elapsedMs: parsed.elapsedMs });
if (progressResolve) {
progressResolve();
progressResolve = null;
}
}

if (parsed.data) {
collectedBuffers.push(Buffer.from(parsed.data, "base64"));
}

if (parsed.done) {
statsResolver(parsed.stats);
outputsResolver(collectedBuffers);
}
}
}
} catch (error) {
streamError = error instanceof Error ? error : new Error(String(error));
statsRejecter(streamError);
outputsRejecter(streamError);
}

progressDone = true;
if (progressResolve) {
progressResolve();
progressResolve = null;
}
};

void processResponses();

const progressStream = (async function* (): AsyncGenerator<DiffusionProgressTick> {
while (true) {
if (progressQueue.length > 0) {
yield progressQueue.shift()!;
} else if (progressDone) {
if (streamError) throw streamError as Error;
return;
} else {
await new Promise<void>((resolve) => { progressResolve = resolve; });
}
}
})();

return {
progressStream,
outputs: outputsPromise,
stats: statsPromise,
};
}
1 change: 1 addition & 0 deletions packages/sdk/client/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export { textToSpeech } from "./text-to-speech";
export { getModelInfo } from "./get-model-info";
export { ocr } from "./ocr";
export { invokePlugin, invokePluginStream } from "./invoke-plugin";
export { diffusion, type DiffusionProgressTick } from "./diffusion";
export {
modelRegistryList,
modelRegistrySearch,
Expand Down
54 changes: 54 additions & 0 deletions packages/sdk/examples/diffusion-flux2-klein.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { loadModel, unloadModel, diffusion, FLUX_2_KLEIN_4B_Q4_0, FLUX_2_KLEIN_4B_VAE, QWEN3_4B_Q4_K_M } from "@qvac/sdk";
import fs from "fs";
import path from "path";

// FLUX.2 [klein] uses a split-layout: separate diffusion model + LLM text encoder + VAE
const diffusionModelSrc = process.argv[2] || FLUX_2_KLEIN_4B_Q4_0;
const llmModelSrc = process.argv[3] || QWEN3_4B_Q4_K_M;
const vaeModelSrc = process.argv[4] || FLUX_2_KLEIN_4B_VAE;
const prompt = process.argv[5] || "a futuristic city at sunset, photorealistic";
const outputDir = process.argv[6] || ".";

console.log("Loading FLUX.2 [klein] split-layout model...");

const modelId = await loadModel({
modelSrc: diffusionModelSrc,
modelType: "diffusion",
modelConfig: {
device: "gpu",
threads: 4,
llmModelSrc,
vaeModelSrc,
},
onProgress: (p) => console.log(`Loading: ${p.percentage.toFixed(1)}%`),
});
console.log(`Model loaded: ${modelId}`);

console.log(`\nGenerating: "${prompt}"`);

const { progressStream, outputs, stats } = diffusion({
modelId,
prompt,
width: 512,
height: 512,
steps: 20,
guidance: 3.5,
seed: -1,
});

for await (const { step, totalSteps } of progressStream) {
process.stdout.write(`\rStep ${step}/${totalSteps}`);
}
console.log();

const buffers = await outputs;
for (let i = 0; i < buffers.length; i++) {
const outputPath = path.join(outputDir, `flux2_${i}.png`);
fs.writeFileSync(outputPath, buffers[i]!);
console.log(`Saved: ${outputPath}`);
}

console.log("\nStats:", await stats);
await unloadModel({ modelId, clearStorage: false });
console.log("Done.");
process.exit(0);
22 changes: 22 additions & 0 deletions packages/sdk/examples/diffusion-simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { loadModel, unloadModel, diffusion, SD_V2_1_1B_Q8_0 } from "@qvac/sdk";
import fs from "fs";

// Minimal diffusion example — single GGUF model, no companion files needed.
// Works with SD 1.x / 2.x all-in-one models.
const modelSrc = process.argv[2] || SD_V2_1_1B_Q8_0;
const prompt = process.argv[3] || "a photo of a cat sitting on a windowsill";

const modelId = await loadModel({
modelSrc,
modelType: "diffusion",
modelConfig: { prediction: "v" },
});

const { outputs } = diffusion({ modelId, prompt });
const buffers = await outputs;

fs.writeFileSync("output.png", buffers[0]!);
console.log("Saved: output.png");

await unloadModel({ modelId, clearStorage: false });
process.exit(0);
49 changes: 49 additions & 0 deletions packages/sdk/examples/diffusion-txt2img.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { loadModel, unloadModel, diffusion, FLUX_2_KLEIN_4B_Q4_0, FLUX_2_KLEIN_4B_VAE, QWEN3_4B_Q4_K_M } from "@qvac/sdk";
import fs from "fs";
import path from "path";

const modelSrc = process.argv[2] || FLUX_2_KLEIN_4B_Q4_0;

const prompt =
process.argv[3] ||
"a photo of a cat sitting on a windowsill, golden hour lighting";
const outputDir = process.argv[4] || ".";

console.log(`Loading diffusion model...`);
// FLUX.2 models require companion LLM + VAE models
const modelId = await loadModel({
modelSrc,
modelType: "diffusion",
modelConfig: { device: "gpu", threads: 4, llmModelSrc: QWEN3_4B_Q4_K_M, vaeModelSrc: FLUX_2_KLEIN_4B_VAE },
onProgress: (p) => console.log(`Loading: ${p.percentage.toFixed(1)}%`),
});
console.log(`Model loaded: ${modelId}`);

console.log(`\nGenerating: "${prompt}"`);

const { progressStream, outputs, stats } = diffusion({
modelId,
prompt,
width: 512,
height: 512,
steps: 20,
cfg_scale: 7.0,
seed: -1,
});

for await (const { step, totalSteps } of progressStream) {
process.stdout.write(`\rStep ${step}/${totalSteps}`);
}
console.log();

const buffers = await outputs;
for (let i = 0; i < buffers.length; i++) {
const outputPath = path.join(outputDir, `output_${i}.png`);
fs.writeFileSync(outputPath, buffers[i]!);
console.log(`Saved: ${outputPath}`);
}

console.log("\nStats:", await stats);
await unloadModel({ modelId, clearStorage: false });
console.log("Done.");
process.exit(0);
2 changes: 2 additions & 0 deletions packages/sdk/examples/mcp-websearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {

// MCP SDK is a user-installed optional dependency
// Install with: bun add @modelcontextprotocol/sdk
// eslint-disable-next-line import/no-unresolved
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
// eslint-disable-next-line import/no-unresolved
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

// ============================================================
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export {
ocr,
invokePlugin,
invokePluginStream,
diffusion,
type DiffusionProgressTick,
modelRegistryList,
modelRegistrySearch,
modelRegistryGetModel,
Expand Down Expand Up @@ -71,6 +73,9 @@ export {
type OCRClientParams,
type OCRTextBlock,
type OCROptions,
type DiffusionClientParams,
type DiffusionStreamResponse,
type DiffusionStats,
definePlugin,
defineHandler,
type QvacPlugin,
Expand All @@ -84,6 +89,7 @@ export {
PLUGIN_NMT,
PLUGIN_TTS,
PLUGIN_OCR,
PLUGIN_DIFFUSION,
SDK_DEFAULT_PLUGINS,
type BuiltinPlugin,
type ProfilerMode,
Expand Down
17 changes: 17 additions & 0 deletions packages/sdk/models/history/7481157e3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
commit=7481157e314d68f268a2fcac10af36e576339635
timestamp=2026-03-19T14:12:56.864Z
previous_count=312
new_count=322

[added]
FLUX_2_KLEIN_4B_VAE
SD_V2_1_1B_Q4_0
SD_V2_1_1B_Q8_0
SDXL_BASE_1_0_3B_Q4_0
SDXL_BASE_1_0_3B_Q8_0
FLUX_2_KLEIN_4B_Q4_0
FLUX_2_KLEIN_4B_Q4_K_M
FLUX_2_KLEIN_4B_Q6_K
FLUX_2_KLEIN_4B_Q8_0
QWEN3_4B_Q4_K_M

17 changes: 17 additions & 0 deletions packages/sdk/models/history/d514e414.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
commit=d514e414b345258b6fcac5887261002a8cb5beca
timestamp=2026-03-16T05:58:16.168Z
previous_count=310
new_count=320

[added]
FLUX_2_KLEIN_4B_VAE
SD_V2_1_1B_Q4_0
SD_V2_1_1B_Q8_0
SDXL_BASE_1_0_3B_Q4_0
SDXL_BASE_1_0_3B_Q8_0
FLUX_2_KLEIN_4B_Q4_0
FLUX_2_KLEIN_4B_Q4_K_M
FLUX_2_KLEIN_4B_Q6_K
FLUX_2_KLEIN_4B_Q8_0
QWEN3_4B_Q4_K_M

Loading
Loading