dist-mcp / services / drawThingsGrpcService.js
dist-mcp / services / drawThingsGrpcService.js
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DrawThingsGrpcService = void 0;
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const grpc = __importStar(require("@grpc/grpc-js"));
const protoLoader = __importStar(require("@grpc/proto-loader"));
const core_bundle_mjs_1 = require("../core-bundle.mjs");
const crypto_1 = __importDefault(require("crypto"));
const child_process_1 = require("child_process");
const os_1 = __importDefault(require("os"));
const customConfigsLoader_js_1 = require("./customConfigsLoader.js");
// Simple file logger (aligned with index.ts logs directory) – CJS-safe
// Local project-root resolver (no top-level await, CJS-safe)
const __svcDir = (() => {
try {
const p = process.argv && process.argv[1] ? process.argv[1] : process.cwd();
return path_1.default.dirname(p);
}
catch {
return process.cwd();
}
})();
function __resolveProjectRootFrom(startDir) {
// Prefer directory containing manifest.json or package.json
try {
let dir = startDir;
for (let i = 0; i < 50; i++) {
try {
const hasManifest = fs_1.default.existsSync(path_1.default.join(dir, "manifest.json"));
const hasPkg = fs_1.default.existsSync(path_1.default.join(dir, "package.json"));
if (hasManifest || hasPkg)
return dir;
}
catch { }
const parent = path_1.default.dirname(dir);
if (parent === dir)
break;
dir = parent;
}
}
catch { }
// Else, fallback to dist/src markers
{
let dir = startDir;
for (let i = 0; i < 20; i++) {
if (path_1.default.basename(dir) === "dist")
return path_1.default.dirname(dir);
const parent = path_1.default.dirname(dir);
if (parent === dir)
break;
dir = parent;
}
}
{
let dir = startDir;
for (let i = 0; i < 5; i++) {
if (path_1.default.basename(dir) === "src")
return path_1.default.dirname(dir);
const parent = path_1.default.dirname(dir);
if (parent === dir)
break;
dir = parent;
}
}
return startDir;
}
const __projectRoot = __resolveProjectRootFrom(__svcDir);
const __logsDir = path_1.default.join(__projectRoot, "logs");
try {
if (!fs_1.default.existsSync(__logsDir))
fs_1.default.mkdirSync(__logsDir, { recursive: true });
}
catch { }
function localTs() {
try {
return new Date().toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZoneName: "short",
});
}
catch {
return new Date().toString();
}
}
function logFile(msg) {
const line = `${localTs()} - ${msg}\n`;
try {
fs_1.default.appendFileSync(path_1.default.join(__logsDir, "generate-image-plugin.log"), line);
}
catch { }
}
function logErr(msg) {
const line = `${localTs()} - ERROR: ${msg}\n`;
try {
fs_1.default.appendFileSync(path_1.default.join(__logsDir, "error.log"), line);
}
catch { }
try {
console.error(`[draw-things-chat] ${msg}`);
}
catch { }
}
function getTensorHelpersMode() {
const raw = String(process.env.DT_TENSOR_HELPERS_MODE || "auto")
.trim()
.toLowerCase();
if (raw === "ts" || raw === "node" || raw === "js")
return "ts";
if (raw === "bin" || raw === "binary" || raw === "native")
return "bin";
return "auto";
}
function buildTensorHelperCandidates(projectRoot, helperName, envCmd) {
const jsPath = path_1.default.resolve(projectRoot, "dist", "helpers", `${helperName}.js`);
const binDistPath = path_1.default.resolve(projectRoot, "dist", "helpers", helperName);
const mode = getTensorHelpersMode();
const ordered = mode === "bin"
? [envCmd, binDistPath, jsPath]
: [envCmd, jsPath, binDistPath];
return ordered.filter(Boolean);
}
function logToolSelection(context, tool) {
if (!tool) {
logFile(`[tool] ${context} not found (no decoder/encoder available)`);
return;
}
logFile(`[tool] ${context} origin=${tool.origin} impl=${tool.impl} path=${tool.display}`);
}
function resolveExternalToolCandidates(projectRoot, helperName, envCmd) {
const candidates = buildTensorHelperCandidates(projectRoot, helperName, envCmd);
const tools = [];
for (const c of candidates) {
try {
const abs = path_1.default.isAbsolute(c) ? c : path_1.default.resolve(projectRoot, c);
if (!fs_1.default.existsSync(abs))
continue;
const origin = c === envCmd ? "env" : "auto";
if (abs.endsWith(".js")) {
tools.push({
cmd: process.execPath,
argsPrefix: [abs],
display: abs,
impl: "ts",
origin,
});
continue;
}
try {
const st = fs_1.default.statSync(abs);
if ((st.mode & 0o111) === 0) {
try {
fs_1.default.chmodSync(abs, 0o755);
}
catch { }
}
}
catch { }
tools.push({
cmd: abs,
argsPrefix: [],
display: abs,
impl: "mac-bin",
origin,
});
}
catch { }
}
return tools;
}
function logToolCandidates(context, tools) {
if (!tools.length) {
logFile(`[tool] ${context} not found (no decoder/encoder available)`);
return;
}
logFile(`[tool] ${context} candidates=${tools.length} primary=${tools[0].display}`);
}
/**
* Normalize numFrames for the Draw Things backend.
* Valid values: 1 (image), or multiples of 32 + 1 (e.g. 33, 65, 97, 129, ..., 641).
* Accepts ×32 (auto-corrects to ×32+1) and rounds invalid values to nearest valid.
*/
function normalizeNumFrames(n) {
if (n <= 1)
return { value: 1 };
if ((n - 1) % 32 === 0)
return { value: n }; // ×32+1 → 1:1
if (n % 32 === 0)
return { value: n + 1 }; // ×32 → +1
const rounded = Math.round(n / 32) * 32 + 1; // nearest ×32+1
const clamped = Math.max(33, Math.min(641, rounded));
return {
value: clamped,
hint: `numFrames adjusted to ${clamped} (must be multiple of 32 + 1).`,
};
}
/**
* DrawThingsGrpcService
* A transport-swappable alternative to DrawThingsService (HTTP).
*
* Configuration (ENV):
* - DRAWTHINGS_GRPC_TARGET (e.g. "127.0.0.1:50051")
* - DRAWTHINGS_GRPC_PROTO (absolute path to .proto)
* - DRAWTHINGS_GRPC_PACKAGE (e.g. "drawthings")
* - DRAWTHINGS_GRPC_SERVICE (e.g. "StableDiffusion")
* - DRAWTHINGS_GRPC_METHOD_TXT2IMG (default: "Txt2Img")
* - DRAWTHINGS_GRPC_METHOD_IMG2IMG (default: "Img2Img")
*/
class DrawThingsGrpcService {
constructor(target) {
this.name = "drawthings";
this.client = null;
this.tlsMode = "auto";
this.currentSecurity = "unknown";
this.baseUrl =
target ||
process.env.DRAWTHINGS_GRPC_TARGET ||
`127.0.0.1:${process.env.DRAWTHINGS_GRPC_PORT || 7859}`;
this.serviceName =
process.env.DRAWTHINGS_GRPC_SERVICE || "ImageGenerationService";
// grpc-js maps RPC names to lowerCamelCase
this.methodGenerate = "generateImage";
this.grpcCompression = (process.env.DRAWTHINGS_GRPC_COMPRESSION || "identity").toLowerCase();
this.loadClient();
}
setBaseUrl(url) {
this.baseUrl = url;
this.loadClient();
}
loadClient() {
try {
const projectRoot = __projectRoot;
const resolveProtoPath = () => {
const fromEnv = process.env.DRAWTHINGS_GRPC_PROTO;
if (fromEnv && fs_1.default.existsSync(fromEnv))
return fromEnv;
const candidates = [
path_1.default.resolve(projectRoot, "dist", "interfaces", "imageService.proto"),
path_1.default.resolve(projectRoot, "src", "interfaces", "imageService.proto"),
path_1.default.resolve(projectRoot, "dist", "src", "interfaces", "imageService.proto"),
path_1.default.resolve(__dirname, "..", "interfaces", "imageService.proto"),
];
for (const p of candidates)
if (fs_1.default.existsSync(p))
return p;
return candidates[0];
};
const protoPath = resolveProtoPath();
const opts = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
};
const packageDef = protoLoader.loadSync(protoPath, opts);
const grpcObj = grpc.loadPackageDefinition(packageDef) || {};
const SvcCtor = grpcObj[this.serviceName];
if (!SvcCtor) {
console.error(`[DrawThingsGrpcService] Service not found: ${this.serviceName} (proto: ${protoPath})`);
logErr(`gRPC service not found: ${this.serviceName} (proto: ${protoPath})`);
try {
const available = Object.keys(grpcObj || {});
console.error(`[DrawThingsGrpcService] Available services: ${available.join(", ")}`);
logFile(`[gRPC] Available services: ${available.join(", ")}`);
}
catch { }
this.client = null;
return;
}
const channelOptions = {
"grpc.max_receive_message_length": 64 * 1024 * 1024,
"grpc.max_send_message_length": 64 * 1024 * 1024,
"grpc.ssl_target_name_override": "localhost",
"grpc.default_authority": "localhost",
};
const loadRootCAs = () => {
const tlsDirs = [
path_1.default.resolve(projectRoot, "src", "interfaces", "tls"),
path_1.default.resolve(projectRoot, "dist", "interfaces", "tls"),
path_1.default.resolve(__dirname, "..", "interfaces", "tls"),
];
const baseDir = tlsDirs.find((d) => {
try {
return fs_1.default.existsSync(d);
}
catch {
return false;
}
});
if (!baseDir)
return undefined;
const serverPath = path_1.default.join(baseDir, "server_crt.crt");
const rootPath = path_1.default.join(baseDir, "root_ca.crt");
if (!fs_1.default.existsSync(serverPath) || !fs_1.default.existsSync(rootPath))
return undefined;
const serverTxt = fs_1.default
.readFileSync(serverPath, "utf8")
.replace(/\r\n/g, "\n")
.trim();
const rootTxt = fs_1.default
.readFileSync(rootPath, "utf8")
.replace(/\r\n/g, "\n")
.trim();
if (!/-----BEGIN CERTIFICATE-----/.test(serverTxt) ||
!/-----BEGIN CERTIFICATE-----/.test(rootTxt))
return undefined;
const bundle = `${serverTxt}\n${rootTxt}\n`;
try {
const out = path_1.default.join(baseDir, "server.pem");
const prev = fs_1.default.existsSync(out)
? fs_1.default.readFileSync(out, "utf8").replace(/\r\n/g, "\n").trim()
: "";
if (prev !== bundle.trim()) {
fs_1.default.writeFileSync(out, bundle, "utf8");
console.log(`[TLS] Generated bundle: ${out}`);
}
else {
console.log(`[TLS] Using existing bundle: ${out}`);
}
}
catch { }
return Buffer.from(bundle, "utf8");
};
const creds = grpc.credentials.createSsl(loadRootCAs());
this.client = new SvcCtor(this.baseUrl, creds, channelOptions);
this.currentSecurity = "tls";
try {
globalThis.__DT_GRPC_TLS_MODE__ = this.tlsMode || "auto";
globalThis.__DT_GRPC_TLS_SELECTED__ = this.currentSecurity;
}
catch { }
try {
const proto = Object.getPrototypeOf(this.client) || {};
const methods = Object.getOwnPropertyNames(proto).filter((k) => typeof this.client[k] === "function" && k !== "constructor");
console.log(`[DrawThingsGrpcService] client methods: ${methods.join(", ")}`);
logFile(`[gRPC] client methods: ${methods.join(", ")}`);
}
catch { }
}
catch (e) {
console.error(`[DrawThingsGrpcService] loadClient error: ${e instanceof Error ? e.message : String(e)}`);
logErr(`loadClient error: ${e instanceof Error ? e.message : String(e)}`);
this.client = null;
}
}
async checkApiConnection() {
if (!this.client)
return false;
// Try Echo first (surfaces sharedSecret requirement)
try {
const echo = this.client["echo"]?.bind(this.client);
if (typeof echo === "function") {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const req = { name: "generate-image-plugin" };
if (sharedSecret)
req.sharedSecret = sharedSecret;
await new Promise((resolve, reject) => {
echo(req, (err, _resp) => {
if (err)
return reject(err);
resolve();
});
});
return true;
}
}
catch (e) {
try {
const msg = e && e.message ? String(e.message) : String(e);
const code = e?.code;
console.error(`[gRPC check] echo failed (mode=${this.currentSecurity}) code=${code ?? "?"} msg=${msg}`);
logErr(`gRPC echo failed (mode=${this.currentSecurity}) code=${code ?? "?"} msg=${msg}`);
}
catch { }
// If auto mode, attempt opposite security on first failure
if (this.tlsMode === "auto") {
// Toggle mode by rebuilding client quickly with TLS and reasonable authority candidates
try {
const __dirname = __svcDir;
const projectRoot = __resolveProjectRootFrom(__dirname);
const protoPath = (() => {
const fromEnv = process.env.DRAWTHINGS_GRPC_PROTO;
if (fromEnv && fs_1.default.existsSync(fromEnv))
return fromEnv;
const c1 = path_1.default.resolve(projectRoot, "src", "interfaces", "imageService.proto");
if (fs_1.default.existsSync(c1))
return c1;
const c3 = path_1.default.resolve(projectRoot, "dist", "interfaces", "imageService.proto");
if (fs_1.default.existsSync(c3))
return c3;
// no dist/src fallback in Rollup build
return c1;
})();
const packageDef = protoLoader.loadSync(protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const grpcObj = grpc.loadPackageDefinition(packageDef) || {};
const SvcCtor = grpcObj[this.serviceName];
if (SvcCtor) {
const root = (() => {
const pieces = [];
const tlsDirCandidates = [
path_1.default.resolve(projectRoot, "src", "interfaces", "tls"),
path_1.default.resolve(projectRoot, "dist", "interfaces", "tls"),
];
const names = [
"ca.crt",
"ca.pem",
"server.crt",
"server.pem",
"rootCA.crt",
"rootCA.pem",
];
for (const d of tlsDirCandidates) {
for (const n of names) {
try {
const p = path_1.default.join(d, n);
if (fs_1.default.existsSync(p))
pieces.push(fs_1.default.readFileSync(p));
}
catch { }
}
}
return pieces.length ? Buffer.concat(pieces) : undefined;
})();
const hostname = String(this.baseUrl || "").split(":")[0] || "localhost";
const candidateSet = new Set([
hostname,
"localhost",
"server",
"drawthings",
"draw-things",
]);
try {
const { X509Certificate } = await import("crypto");
const protoDirs = [
path_1.default.resolve(projectRoot, "src", "interfaces", "tls"),
path_1.default.resolve(projectRoot, "dist", "interfaces", "tls"),
];
const certFiles = [
"server_crt.crt",
"server_cert.crt",
"server.crt",
"server.pem",
"root_ca.crt",
"rootCA.crt",
"ca.crt",
"ca.pem",
];
for (const d of protoDirs) {
for (const f of certFiles) {
const p = path_1.default.join(d, f);
try {
if (!fs_1.default.existsSync(p))
continue;
const txt = fs_1.default.readFileSync(p, "utf8");
const blocks = txt
.split(/-----END CERTIFICATE-----/g)
.map((b) => b.includes("BEGIN CERTIFICATE")
? b + "-----END CERTIFICATE-----\n"
: "")
.filter((b) => b.trim().length > 0);
for (const pem of blocks) {
try {
// @ts-ignore
const x = new X509Certificate(pem);
const subj = x.subject || "";
const san = x.subjectAltName || "";
const m = subj.match(/CN=([^,\n]+)/);
if (m && m[1])
candidateSet.add(m[1].trim());
if (san) {
san
.split(/[,]/)
.map((s) => s.trim())
.filter((s) => /^DNS:/i.test(s))
.map((s) => s.replace(/^DNS:\s*/i, ""))
.forEach((dname) => candidateSet.add(dname));
}
}
catch { }
}
}
catch { }
}
}
}
catch { }
const authCandidates = Array.from(candidateSet);
// Try TLS with different authorities
for (const auth of authCandidates) {
const channelOptions = {
"grpc.max_receive_message_length": 64 * 1024 * 1024,
"grpc.max_send_message_length": 64 * 1024 * 1024,
"grpc.ssl_target_name_override": auth,
"grpc.default_authority": auth,
};
const creds = grpc.credentials.createSsl(root);
this.client = new SvcCtor(this.baseUrl, creds, channelOptions);
this.currentSecurity = "tls";
try {
globalThis.__DT_GRPC_TLS_SELECTED__ =
this.currentSecurity;
}
catch { }
try {
const echo2 = this.client["echo"]?.bind(this.client);
if (typeof echo2 === "function") {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const req = { name: "generate-image-plugin" };
if (sharedSecret)
req.sharedSecret = sharedSecret;
await new Promise((resolve2, reject2) => {
echo2(req, (err2) => err2 ? reject2(err2) : resolve2());
});
return true;
}
}
catch (e2) {
try {
const msg = e2 && e2.message
? String(e2.message)
: String(e2);
const code = e2?.code;
console.error(`[gRPC check] TLS echo failed (authority=${auth}) code=${code ?? "?"} msg=${msg}`);
}
catch { }
}
}
// As a last check, retry insecure (in case server was plaintext)
{
const channelOptions = {
"grpc.max_receive_message_length": 64 * 1024 * 1024,
"grpc.max_send_message_length": 64 * 1024 * 1024,
};
const creds = grpc.credentials.createInsecure();
this.client = new SvcCtor(this.baseUrl, creds, channelOptions);
this.currentSecurity = "insecure";
try {
globalThis.__DT_GRPC_TLS_SELECTED__ =
this.currentSecurity;
}
catch (e3) {
try {
const msg = e3 && e3.message
? String(e3.message)
: String(e3);
const code = e3?.code;
console.error(`[gRPC check] insecure echo retry failed code=${code ?? "?"} msg=${msg}`);
}
catch { }
}
try {
const echo3 = this.client["echo"]?.bind(this.client);
if (typeof echo3 === "function") {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const req = { name: "generate-image-plugin" };
if (sharedSecret)
req.sharedSecret = sharedSecret;
await new Promise((resolve3, reject3) => {
echo3(req, (err3) => err3 ? reject3(err3) : resolve3());
});
return true;
}
}
catch { }
}
}
}
catch { }
}
}
const deadline = new Date(Date.now() + 8000);
return new Promise((resolve) => {
try {
this.client.waitForReady(deadline, (err) => {
resolve(!err);
});
}
catch {
resolve(false);
}
});
}
ensureClient() {
if (!this.client)
throw new Error("gRPC client not configured. Set DRAWTHINGS_GRPC_* envs.");
}
toBackendError(e) {
let status = undefined;
let msg = "unknown error";
if (e && typeof e === "object") {
if (typeof e.code === "number")
status = e.code; // grpc status code
if (typeof e.details === "string")
msg = e.details;
else if (typeof e.message === "string")
msg = e.message;
}
return { isError: true, status, errorMessage: msg };
}
async generateImage(inputParams = {}, onProgress) {
try {
const __tmpDirs = [];
const __registerTmp = (d) => {
try {
__tmpDirs.push(d);
}
catch { }
};
const __cleanupTmp = () => {
for (const d of __tmpDirs) {
try {
fs_1.default.rmSync(d, { recursive: true, force: true });
}
catch { }
}
};
const isVideoTxt2Vid = inputParams?._dt_video_mode === "txt2vid";
const baseDefaults = isVideoTxt2Vid ? core_bundle_mjs_1.defaultParamsText2Video : core_bundle_mjs_1.defaultParams;
const baseOverlayMode = isVideoTxt2Vid ? "txt2vid" : "txt2img";
const audit = (0, core_bundle_mjs_1.buildAuditLogger)({ backend: this.name, mode: baseOverlayMode });
// NOTE: Service-level audit is kept minimal; full audit in core (tools.ts)
this.ensureClient();
// We'll build the FlatBuffer config AFTER canonical mapping so steps/size/variants land correctly
let configBytes = undefined;
// Capture effective fields from the final FlatBuffer view
let payloadWidth;
let payloadHeight;
let payloadSteps;
let payloadSeed;
let payloadSeedMode;
let payloadModel;
let payloadNumFrames = 1;
let payloadFps = 24;
let requiredModelFile;
let requiredLoraFiles;
// Capture overlay source for audit logging
let overlaySource = "default";
let overlayPreset;
let seedSource = "default";
let seedModeSource = "default";
let defaultsUsed;
let overlayLookupMode;
let strengthUsed;
let stepsUsed;
let samplerUsed;
let guidanceScaleUsed;
let shiftUsed;
let resolutionDependentShiftUsed;
let compressionArtifactsUsed;
let compressionArtifactsQualityUsed;
// validate + canonical mapping
let params = {};
try {
const r = (0, core_bundle_mjs_1.validateImageGenerationParams)(inputParams);
if (r.valid)
params = inputParams;
}
catch { }
if (params.random_string &&
(!params.prompt || Object.keys(params).length === 1)) {
params.prompt = params.random_string;
delete params.random_string;
}
if (!params.prompt)
params.prompt = inputParams.prompt || baseDefaults.prompt;
const promptOriginTxt = typeof inputParams?.prompt === "string" &&
String(inputParams.prompt).trim()
? "user"
: `default:drawthings:${baseOverlayMode}`;
let usedImageFormat = undefined;
let usedQuality = undefined;
try {
const fmt = inputParams.imageFormat;
// Shorthand should not override explicit dimensions (core passes requested_effective via width/height).
if (params.width == null && params.height == null) {
if (fmt === "square") {
params.width = 1024;
params.height = 1024;
usedImageFormat = "square";
}
else if (fmt === "landscape") {
params.width = 1024;
params.height = 768;
usedImageFormat = "landscape";
}
else if (fmt === "portrait") {
params.width = 768;
params.height = 1024;
usedImageFormat = "portrait";
}
else if (fmt === "16:9") {
params.width = 1024;
params.height = 576;
usedImageFormat = "16:9";
}
}
const qual = inputParams.quality;
if (qual === "low") {
params.steps = 4;
usedQuality = "low";
}
else if (qual === "medium") {
params.steps = 8;
usedQuality = "medium";
}
else if (qual === "high") {
params.steps = 12;
usedQuality = "high";
}
else {
usedQuality = "auto";
}
const variants = inputParams.variants;
if (typeof variants === "number") {
const v = Math.max(1, Math.min(4, Math.round(variants)));
params.batch_size = v;
params.batch_count = 1;
}
// Video: normalize numFrames to valid ×32+1 value
if (isVideoTxt2Vid) {
const rawNumFrames = inputParams.numFrames;
if (typeof rawNumFrames === "number" && rawNumFrames > 1) {
const normalized = normalizeNumFrames(rawNumFrames);
params.num_frames = normalized.value;
if (normalized.hint)
console.info(`[txt2vid] ${normalized.hint}`);
}
}
}
catch { }
// Build FlatBuffer configuration from effective params (defaults overlaid with mapped values)
try {
const allowedKeys = new Set(Object.keys(baseDefaults));
const filtered = {};
for (const [k, v] of Object.entries(params)) {
if (allowedKeys.has(k))
filtered[k] = v;
}
// Apply effective overlay (Custom Configs → Model Overlay → Defaults)
const modelId = params.model;
const { source, presetName, params: overlayParams, } = (0, customConfigsLoader_js_1.getEffectiveOverlay)(modelId, baseOverlayMode);
// Size must be controlled by tool/core (or defaults), not by overlays.
// Batch must be controlled by tool/core via variants parameter.
// Upscaler must be controlled by tool/core via _dt_needs_upscaler decision.
const overlayParamsNoSize = (() => {
if (!overlayParams)
return overlayParams;
const o = { ...overlayParams };
delete o.width;
delete o.height;
delete o.batch_count;
delete o.batch_size;
delete o.batchCount;
delete o.batchSize;
delete o.upscaler;
delete o.upscaler_scale;
delete o.upscalerScale;
delete o.fps;
return o;
})();
// Capture overlay info for result metadata
overlaySource = source;
overlayPreset = presetName;
// Tool-level preset 'auto' means: do not override engine model.
// LM Studio UI may send "auto" explicitly; never forward that literal to Draw Things.
if (modelId === "auto") {
delete filtered.model;
}
if (overlayParams) {
const msg = `Applying ${source} overlay for '${modelId ?? 'auto'}' (${baseOverlayMode}, gRPC)${presetName ? ` preset=${presetName}` : ""}`;
logFile(msg);
console.info(msg);
// Remove 'model' from filtered so overlay model filename wins
delete filtered.model;
}
// Log overlay source to audit
audit.setOutput({
overlay_source: source,
...(presetName && { overlay_preset: presetName }),
});
// Seed policy:
// - Tool interface does not accept seed/seed_mode (core uses strict minimal schema).
// - Custom Configs MAY set seed (including -1) and seed_mode and should be effective.
const effectiveSeed = source === "custom" &&
typeof overlayParamsNoSize?.seed === "number" &&
Number.isFinite(overlayParamsNoSize?.seed)
? overlayParamsNoSize.seed
: baseDefaults.seed;
const effectiveSeedMode = source === "custom" &&
typeof overlayParamsNoSize?.seed_mode === "string" &&
String(overlayParamsNoSize.seed_mode).trim()
? String(overlayParamsNoSize.seed_mode)
: baseDefaults.seed_mode;
seedSource =
source === "custom" &&
typeof overlayParamsNoSize?.seed === "number" &&
Number.isFinite(overlayParamsNoSize?.seed)
? "custom"
: "default";
seedModeSource =
source === "custom" &&
typeof overlayParamsNoSize?.seed_mode === "string" &&
String(overlayParamsNoSize.seed_mode).trim()
? "custom"
: "default";
const effective = {
...baseDefaults,
...(overlayParamsNoSize || {}),
...filtered,
seed: effectiveSeed,
seed_mode: effectiveSeedMode,
};
defaultsUsed = isVideoTxt2Vid ? "defaultParamsDrawThingsText2Video" : "defaultParamsDrawThingsTxt2Img";
overlayLookupMode = baseOverlayMode;
strengthUsed =
typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined;
stepsUsed =
typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined;
samplerUsed =
typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined;
guidanceScaleUsed =
typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined;
shiftUsed =
typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined;
resolutionDependentShiftUsed =
typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined;
compressionArtifactsUsed =
typeof effective.compressionArtifacts === "string" ? effective.compressionArtifacts
: typeof effective.compression_artifacts === "string" ? effective.compression_artifacts
: null;
compressionArtifactsQualityUsed =
typeof effective.compressionArtifactsQuality === "number"
? effective.compressionArtifactsQuality
: typeof effective.compression_artifacts_quality === "number"
? effective.compression_artifacts_quality
: null;
// Always log which defaults were used as the base, even when overlay_source=default.
// Also log key params like strength to disambiguate edit vs img2img behavior.
audit.setOutput({
overlay_lookup_mode: baseOverlayMode,
defaults_used: isVideoTxt2Vid ? "defaultParamsDrawThingsText2Video" : "defaultParamsDrawThingsTxt2Img",
strength_used: typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined,
steps_used: typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined,
sampler_used: typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined,
guidance_scale_used: typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined,
shift_used: typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined,
resolution_dependent_shift_used: typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined,
num_frames_used: typeof effective.num_frames === "number" ? effective.num_frames : null,
compression_artifacts_used: typeof effective.compressionArtifacts === "string" ? effective.compressionArtifacts
: typeof effective.compression_artifacts === "string" ? effective.compression_artifacts
: null,
compression_artifacts_quality_used: typeof effective.compressionArtifactsQuality === "number"
? effective.compressionArtifactsQuality
: typeof effective.compression_artifacts_quality === "number"
? effective.compression_artifacts_quality
: null,
});
// Asset preflight inputs
try {
if (typeof effective.model === "string" && effective.model.trim()) {
requiredModelFile = String(effective.model).trim();
}
// Extract LoRAs for audit: user > overlay > defaults
const userSpecifiedLoras = Object.prototype.hasOwnProperty.call(filtered, "loras");
const overlaySelected = !!overlayParams;
if (userSpecifiedLoras) {
const ls = Array.isArray(filtered.loras) ? filtered.loras : [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else if (overlaySelected) {
const ls = Array.isArray(overlayParams?.loras)
? overlayParams.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else {
// Fallback: extract LoRAs from effective params (includes defaults)
const ls = Array.isArray(effective?.loras)
? effective.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
}
catch { }
const { buildDtGenerationConfiguration } = await import("./drawThingsConfigMapper.js");
const cfg = buildDtGenerationConfiguration(effective);
configBytes = cfg.bytes && cfg.bytes.length ? cfg.bytes : undefined;
try {
payloadWidth = cfg?.view?.width;
payloadHeight = cfg?.view?.height;
payloadSteps = cfg?.view?.steps;
payloadSeed = cfg?.view?.seed;
payloadSeedMode = cfg?.view?.seed_mode;
payloadModel = String(effective.model || "");
payloadNumFrames = typeof effective.num_frames === "number" ? effective.num_frames : 1;
payloadFps = (typeof filtered.fps === "number" ? filtered.fps : undefined)
?? (0, core_bundle_mjs_1.getDefaultFpsForModel)(payloadModel)
?? (typeof effective.fps === "number" ? effective.fps : 24);
}
catch { }
try {
const view = cfg.view || {};
audit.setOutput({
flatbuf_num_frames: typeof view.num_frames === "number" ? view.num_frames : null,
});
console.info(`[grpc-config] fields=${Object.keys(view).join(",")}`);
}
catch { }
}
catch { }
// Preflight Echo to detect sharedSecret policy
try {
const echo = this.client["echo"]?.bind(this.client);
if (typeof echo === "function") {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const req = { name: "generate-image-plugin" };
if (sharedSecret)
req.sharedSecret = sharedSecret;
await new Promise((resolve, reject) => {
echo(req, (err, resp) => {
if (err)
return reject(err);
if (resp?.sharedSecretMissing && !sharedSecret)
return reject(new Error("gRPC requires sharedSecret. Set DRAWTHINGS_SHARED_SECRET."));
resolve();
});
});
}
}
catch (e) {
throw e;
}
// Validate requested model/LoRA presence on server to avoid silent fallback rendering.
try {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const chk = await (0, core_bundle_mjs_1.checkDrawThingsGrpcAssets)({
client: this.client,
sharedSecret: sharedSecret || undefined,
modelFile: requiredModelFile,
loraFiles: requiredLoraFiles,
});
if (!chk.ok) {
const overlayCtx = overlaySource && overlaySource !== "default"
? `overlay=${overlaySource}${overlayPreset ? ` preset=${overlayPreset}` : ""}`
: "";
logErr(`[asset-check] ${chk.details.split("\n")[0]}${overlayCtx ? ` (${overlayCtx})` : ""}`);
return {
isError: true,
status: 400,
errorMessage: overlayCtx
? `${overlayCtx}\n\n${chk.details}`
: chk.details,
};
}
}
catch (e) {
// If we cannot validate (RPC failure), do not block generation.
try {
logErr(`[asset-check] warning: ${e instanceof Error ? e.message : String(e)}`);
}
catch { }
}
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const request = {
prompt: String(params.prompt || baseDefaults.prompt),
negativePrompt: String(params.negative_prompt || ""),
scaleFactor: 1,
keywords: [],
user: "generate-image-plugin",
device: "LAPTOP",
chunked: true,
...(sharedSecret ? { sharedSecret } : {}),
};
if (configBytes)
request.configuration = configBytes;
// Optional: include FlatBuffer configuration from env (Base64)
const cfgB64 = process.env.DRAWTHINGS_CONFIG_B64;
if (cfgB64 && typeof cfgB64 === "string" && cfgB64.trim()) {
try {
request.configuration = Buffer.from(cfgB64.trim(), "base64");
}
catch { }
}
const call = this.client[this.methodGenerate]?.bind(this.client);
if (typeof call !== "function")
throw new Error(`gRPC method not found: ${this.methodGenerate}`);
const startTime = Date.now();
const imagesBuffers = [];
const audioBuffers = [];
let chunkAccum = null;
let previewBuf = null;
let lastSeenStep = 0;
const handleData = (resp) => {
try {
// progress logging + callback
if (resp?.currentSignpost?.sampling?.step) {
const currentStep = resp.currentSignpost.sampling.step;
lastSeenStep = currentStep;
console.info(`gRPC progress: sampling step=${currentStep}`);
logFile(`t2i progress: sampling step=${currentStep}`);
// Invoke progress callback if provided
if (onProgress) {
try {
onProgress(currentStep, payloadSteps, `Sampling step ${currentStep}${payloadSteps ? `/${payloadSteps}` : ""}`);
}
catch { }
}
}
else if (resp?.currentSignpost != null && onProgress) {
const sp = resp.currentSignpost;
let label;
if (lastSeenStep === 0) {
if (sp.textEncoded != null)
label = "Loading...";
else if (sp.imageEncoded != null)
label = "Processing...";
}
else {
label = "Finishing...";
}
if (label) {
try {
onProgress(-1, payloadSteps, label);
}
catch { }
}
}
if (resp?.generatedImages &&
Array.isArray(resp.generatedImages)) {
const list = resp.generatedImages.map((b) => Buffer.isBuffer(b) ? b : Buffer.from(b));
const cs = resp?.chunkState;
const isMore = cs === 1 || cs === "MORE_CHUNKS";
const isLast = cs === 0 || cs === "LAST_CHUNK";
if (isMore) {
if (list.length > 0) {
chunkAccum = chunkAccum
? Buffer.concat([chunkAccum, list[0]])
: Buffer.from(list[0]);
}
}
else if (isLast && list.length > 0) {
if (chunkAccum) {
const full = Buffer.concat([chunkAccum, list[0]]);
imagesBuffers.push(full);
chunkAccum = null;
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
if (resp?.previewImage) {
const b = resp.previewImage;
previewBuf = Buffer.isBuffer(b) ? b : Buffer.from(b);
}
if (resp?.generatedAudio && Array.isArray(resp.generatedAudio)) {
for (const a of resp.generatedAudio) {
const ab = Buffer.isBuffer(a) ? a : Buffer.from(a);
if (ab.length > 0)
audioBuffers.push(ab);
}
}
}
catch { }
};
await new Promise((resolve, reject) => {
try {
// Optional response compression negotiation (off by default; enable via env)
const accept = process.env.DRAWTHINGS_GRPC_ACCEPT_ENCODING;
const md = new grpc.Metadata();
if (accept && accept.trim()) {
md.add("grpc-accept-encoding", accept.trim());
}
const stream = md.get("grpc-accept-encoding").length > 0
? call(request, md)
: call(request);
stream.on("data", handleData);
stream.on("error", (err) => reject(err));
stream.on("end", () => resolve());
}
catch (e) {
reject(e);
}
});
const endTime = Date.now();
let primaryBuf;
if (imagesBuffers.length > 0)
primaryBuf = imagesBuffers[0];
else if (previewBuf)
primaryBuf = previewBuf;
if (!primaryBuf)
throw new Error("gRPC stream yielded no image data");
// Decode tensor bytes to PNG via external CLI (always attempt)
const decoderTools = resolveExternalToolCandidates(__projectRoot, "GRPCBin2PNG", process.env.DT_DECODER_CMD);
logToolCandidates("t2i decode", decoderTools);
const decodedBuffers = [];
if (decoderTools.length) {
// derive width/height hints for decoder from params/defaults
const finalW = Number(params.width || baseDefaults.width || 1024);
const finalH = Number(params.height || baseDefaults.height || 1024);
const decoderCtx = JSON.stringify({
width: finalW,
height: finalH,
channels: 3,
dtype: "f32",
});
for (let i = 0; i < imagesBuffers.length; i++) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-dt-"));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, `img-${i + 1}.bin`);
const outPath = path_1.default.join(tmpDir, `img-${i + 1}.png`);
fs_1.default.writeFileSync(inPath, imagesBuffers[i]);
const args = ["--in", inPath, "--out", outPath];
let decoded = null;
for (const tool of decoderTools) {
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, DT_DECODER_CTX: decoderCtx },
});
if (proc.error) {
logErr(`[decoder] exception via ${tool.display}: ${proc.error.message}`);
continue;
}
if (proc.status !== 0) {
logErr(`[decoder] exit=${proc.status} via ${tool.display} stderr=${proc.stderr?.toString?.() || ""}`);
continue;
}
if (fs_1.default.existsSync(outPath) && fs_1.default.statSync(outPath).size > 0) {
decoded = fs_1.default.readFileSync(outPath);
break;
}
logErr(`[decoder] no output via ${tool.display}; outPath missing/empty`);
}
if (decoded)
decodedBuffers.push(decoded);
else
decodedBuffers.push(imagesBuffers[i]);
}
catch {
// fall back: keep raw tensor bytes
decodedBuffers.push(imagesBuffers[i]);
logErr(`[decoder] exception decoding t2i img-${i + 1}.bin; kept raw`);
}
}
if (decodedBuffers.length > 0) {
logFile(`[decoder] t2i: decoded ${decodedBuffers.length} frame(s) → PNG via ${decoderTools[0].display}`);
}
}
let finalBuffers = decoderTools.length ? decodedBuffers : imagesBuffers;
// Fallback to preview when no generated images were provided
if ((!finalBuffers || finalBuffers.length === 0) && previewBuf) {
if (decoderTools.length) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-dt-"));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, `preview.bin`);
const outPath = path_1.default.join(tmpDir, `preview.png`);
fs_1.default.writeFileSync(inPath, previewBuf);
const finalW = Number(params.width || baseDefaults.width || 1024);
const finalH = Number(params.height || baseDefaults.height || 1024);
const decoderCtx = JSON.stringify({
width: finalW,
height: finalH,
channels: 3,
dtype: "f32",
});
const args = ["--in", inPath, "--out", outPath];
let decodedPrev = null;
for (const tool of decoderTools) {
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, DT_DECODER_CTX: decoderCtx },
});
if (proc.error)
continue;
if (proc.status !== 0)
continue;
if (fs_1.default.existsSync(outPath) && fs_1.default.statSync(outPath).size > 0) {
decodedPrev = fs_1.default.readFileSync(outPath);
logFile(`[decoder] decoded preview.bin → PNG (${decodedPrev.length} bytes) via ${tool.display}`);
break;
}
}
finalBuffers = decodedPrev ? [decodedPrev] : [previewBuf];
}
catch {
finalBuffers = [previewBuf];
}
}
else {
finalBuffers = [previewBuf];
}
}
const pngCandidate = finalBuffers[0];
// Service-level audit disabled (consolidated audit is logged in index.ts)
// Effective dims from payload (FlatBuffer view) when available
const effW = Number(payloadWidth ??
params.width ??
baseDefaults.width ??
1024);
const effH = Number(payloadHeight ??
params.height ??
baseDefaults.height ??
1024);
// Service-level audit disabled (consolidated audit is logged in index.ts)
const dataUrl = `data:image/png;base64,${pngCandidate.toString("base64")}`;
logFile(`t2i completed: variants=${finalBuffers.length}, chunks=${imagesBuffers.length}, bytes=${pngCandidate.length}`);
const result = {
isError: false,
imageBuffer: pngCandidate,
imageData: dataUrl,
images: finalBuffers.length
? finalBuffers.map((b) => `data:image/png;base64,${b.toString("base64")}`)
: undefined,
metadata: {
alt: `Image generated from prompt: ${request.prompt}`,
inference_time_ms: endTime - startTime,
model: payloadModel,
width: effW,
height: effH,
requested_dimensions: {
width: effW,
height: effH,
},
steps: Number.isFinite(Number(payloadSteps))
? Number(payloadSteps)
: undefined,
seed: typeof payloadSeed === "number" && Number.isFinite(payloadSeed)
? payloadSeed
: undefined,
seed_mode: typeof payloadSeedMode === "string" && payloadSeedMode.trim()
? payloadSeedMode
: undefined,
seed_source: seedSource,
seed_mode_source: seedModeSource,
prompt_used: String(request.prompt || ""),
prompt_origin: promptOriginTxt,
transport: "grpc",
stream_chunks: imagesBuffers.length,
num_frames: payloadNumFrames,
fps: payloadFps,
// Overlay source tracking for audit
overlay_source: overlaySource,
...(overlayPreset && { overlay_preset: overlayPreset }),
...(defaultsUsed && { defaults_used: defaultsUsed }),
...(overlayLookupMode && { overlay_lookup_mode: overlayLookupMode }),
...(typeof strengthUsed === "number" && { strength_used: strengthUsed }),
...(typeof stepsUsed === "number" && { steps_used: stepsUsed }),
...(samplerUsed && { sampler_used: samplerUsed }),
...(typeof guidanceScaleUsed === "number" && {
guidance_scale_used: guidanceScaleUsed,
}),
...(typeof shiftUsed === "number" && { shift_used: shiftUsed }),
...(typeof resolutionDependentShiftUsed === "boolean" && {
resolution_dependent_shift_used: resolutionDependentShiftUsed,
}),
...(compressionArtifactsUsed != null && {
compression_artifacts_used: compressionArtifactsUsed,
}),
...(compressionArtifactsQualityUsed != null && {
compression_artifacts_quality_used: compressionArtifactsQualityUsed,
}),
...(requiredLoraFiles &&
requiredLoraFiles.length > 0 && { loras_used: requiredLoraFiles }),
},
audioBuffers: audioBuffers.length > 0 ? audioBuffers : undefined,
};
__cleanupTmp();
return result;
}
catch (e) {
try {
// attempt temp cleanup on error as well
// (safe even if empty)
}
catch { }
return this.toBackendError(e);
}
}
async generateImageImg2Img(inputParams = {}, sourceBuffer, onProgress) {
try {
const __tmpDirs = [];
const __registerTmp = (d) => {
try {
__tmpDirs.push(d);
}
catch { }
};
const __cleanupTmp = () => {
for (const d of __tmpDirs) {
try {
fs_1.default.rmSync(d, { recursive: true, force: true });
}
catch { }
}
};
const isVideoImg2Vid = inputParams?._dt_video_mode === "img2vid";
const audit = (0, core_bundle_mjs_1.buildAuditLogger)({ backend: this.name, mode: isVideoImg2Vid ? "img2vid" : "img2img" });
// NOTE: Service-level audit is kept minimal; full audit in core (tools.ts)
this.ensureClient();
const i2iProfile = isVideoImg2Vid ? undefined : inputParams?._dt_i2i_profile;
const baseDefaults = isVideoImg2Vid
? core_bundle_mjs_1.defaultParamsImage2Video
: i2iProfile === "zoom" ? core_bundle_mjs_1.defaultParamsZoom
: i2iProfile === "edit" ? core_bundle_mjs_1.defaultParamsEdit : core_bundle_mjs_1.defaultParamsImg2Img;
const baseLimits = isVideoImg2Vid
? core_bundle_mjs_1.drawthingsLimits
: i2iProfile === "edit" ? core_bundle_mjs_1.drawthingsEditLimits : core_bundle_mjs_1.drawthingsLimits;
let params = {};
try {
const r = (0, core_bundle_mjs_1.validateImageGenerationParams)(inputParams);
if (r.valid)
params = inputParams;
}
catch { }
if (params.random_string &&
(!params.prompt || Object.keys(params).length === 1)) {
params.prompt = params.random_string;
delete params.random_string;
}
if (!params.prompt)
params.prompt = inputParams.prompt || baseDefaults.prompt;
const promptOriginI2I = typeof inputParams?.prompt === "string" &&
String(inputParams.prompt).trim()
? "user"
: `default:drawthings:${isVideoImg2Vid ? "img2vid" : "img2img"}`;
let usedImageFormat = undefined;
let usedQuality = undefined;
try {
const fmt = inputParams.imageFormat;
if (fmt === "square") {
params.width = 1024;
params.height = 1024;
usedImageFormat = "square";
}
else if (fmt === "landscape") {
params.width = 1024;
params.height = 768;
usedImageFormat = "landscape";
}
else if (fmt === "portrait") {
params.width = 768;
params.height = 1024;
usedImageFormat = "portrait";
}
else if (fmt === "16:9") {
params.width = 1024;
params.height = 576;
usedImageFormat = "16:9";
}
const qual = inputParams.quality;
if (qual === "low") {
params.steps = 4;
usedQuality = "low";
}
else if (qual === "medium") {
params.steps = 8;
usedQuality = "medium";
}
else if (qual === "high") {
params.steps = 12;
usedQuality = "high";
}
else {
usedQuality = "auto";
}
const variants = inputParams.variants;
if (typeof variants === "number") {
const v = Math.max(1, Math.min(4, Math.round(variants)));
params.batch_size = v;
params.batch_count = 1;
}
// Video: normalize numFrames to valid ×32+1 value
if (isVideoImg2Vid) {
const rawNumFrames = inputParams.numFrames;
if (typeof rawNumFrames === "number" && rawNumFrames > 1) {
const normalized = normalizeNumFrames(rawNumFrames);
params.num_frames = normalized.value;
if (normalized.hint)
console.info(`[img2vid] ${normalized.hint}`);
}
}
}
catch { }
// Ensure PNG, capture source size
let initPng = sourceBuffer;
let srcW;
let srcH;
try {
const meta = await (0, core_bundle_mjs_1.getSize)(sourceBuffer);
srcW = meta.width;
srcH = meta.height;
initPng = await (0, core_bundle_mjs_1.toPng)(sourceBuffer);
}
catch { }
// User-requested OUT size (if explicitly provided)
const userReqW = (() => {
const v = inputParams?.width;
return typeof v === "number" && Number.isFinite(v)
? Math.round(v)
: undefined;
})();
const userReqH = (() => {
const v = inputParams?.height;
return typeof v === "number" && Number.isFinite(v)
? Math.round(v)
: undefined;
})();
// If source dimensions are known, record them as ORIGINAL size.
// Do NOT override generation size (width/height/target_*) to source size.
// Generation size should follow the requested/effective size.
if (srcW && srcH) {
params.original_width = srcW;
params.original_height = srcH;
const hasW = typeof params.width === "number" &&
Number.isFinite(params.width);
const hasH = typeof params.height === "number" &&
Number.isFinite(params.height);
if (!hasW)
params.width = userReqW ?? srcW;
if (!hasH)
params.height = userReqH ?? srcH;
const hasTW = typeof params.target_width === "number" &&
Number.isFinite(params.target_width);
const hasTH = typeof params.target_height === "number" &&
Number.isFinite(params.target_height);
if (!hasTW)
params.target_width = params.width;
if (!hasTH)
params.target_height = params.height;
}
// Build full configuration from effective params (HTTP-equivalent)
let configBytes = undefined;
let payloadWidth;
let payloadHeight;
let payloadSteps;
let payloadSeed;
let payloadSeedMode;
let payloadModel;
let payloadNumFrames = 1;
let payloadFps = 24;
let requiredModelFile;
let requiredLoraFiles;
// Capture overlay source for audit logging
let overlaySource = "default";
let overlayPreset;
// Debug metadata (propagated to core audit via result.metadata)
let defaultsUsed;
let overlayLookupMode;
let i2iProfileUsed;
let strengthUsed;
let stepsUsed;
let samplerUsed;
let guidanceScaleUsed;
let shiftUsed;
let resolutionDependentShiftUsed;
let compressionArtifactsUsed;
let compressionArtifactsQualityUsed;
try {
const allowedKeys = new Set(Object.keys(baseDefaults));
const filtered = {};
for (const [k, v] of Object.entries(params)) {
if (allowedKeys.has(k))
filtered[k] = v;
}
// Apply effective overlay (Custom Configs → Model Overlay → Defaults)
// Use i2iProfile to determine correct mode for overlay lookup
const modelId = params.model;
const overlayMode = isVideoImg2Vid ? "img2vid" : i2iProfile === "edit" ? "edit" : "img2img";
const { source, presetName, params: overlayParams, } = (0, customConfigsLoader_js_1.getEffectiveOverlay)(modelId, overlayMode);
// Size must be controlled by tool/core (or defaults), not by overlays.
// Batch must be controlled by tool/core via variants parameter.
// Upscaler must be controlled by tool/core via _dt_needs_upscaler decision.
const overlayParamsNoSize = (() => {
if (!overlayParams)
return overlayParams;
const o = { ...overlayParams };
delete o.width;
delete o.height;
delete o.batch_count;
delete o.batch_size;
delete o.batchCount;
delete o.batchSize;
delete o.upscaler;
delete o.upscaler_scale;
delete o.upscalerScale;
delete o.fps;
return o;
})();
// Capture overlay info for result metadata
overlaySource = source;
overlayPreset = presetName;
// Tool-level preset 'auto' means: do not override engine model.
if (modelId === "auto") {
delete filtered.model;
}
if (overlayParams) {
const msg = `Applying ${source} overlay for '${modelId ?? 'auto'}' (${overlayMode}, gRPC)${presetName ? ` preset=${presetName}` : ""}`;
logFile(msg);
console.info(msg);
// Remove 'model' from filtered so overlay model filename wins
delete filtered.model;
}
// Log overlay source to audit
audit.setOutput({
overlay_source: source,
...(presetName && { overlay_preset: presetName }),
});
const effectiveSeed = source === "custom" &&
typeof overlayParamsNoSize?.seed === "number" &&
Number.isFinite(overlayParamsNoSize?.seed)
? overlayParamsNoSize.seed
: baseDefaults.seed;
const effective = {
...baseDefaults,
...(overlayParamsNoSize || {}),
...filtered,
seed: effectiveSeed,
};
defaultsUsed = isVideoImg2Vid
? "defaultParamsDrawThingsImage2Video"
: i2iProfile === "zoom"
? "defaultParamsZoom"
: i2iProfile === "edit"
? "defaultParamsDrawThingsEdit"
: "defaultParamsDrawThingsImg2Img";
overlayLookupMode = overlayMode;
i2iProfileUsed = isVideoImg2Vid ? undefined : i2iProfile;
strengthUsed =
typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined;
stepsUsed =
typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined;
samplerUsed =
typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined;
guidanceScaleUsed =
typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined;
shiftUsed =
typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined;
resolutionDependentShiftUsed =
typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined;
compressionArtifactsUsed =
typeof effective.compressionArtifacts === "string" ? effective.compressionArtifacts
: typeof effective.compression_artifacts === "string" ? effective.compression_artifacts
: null;
compressionArtifactsQualityUsed =
typeof effective.compressionArtifactsQuality === "number"
? effective.compressionArtifactsQuality
: typeof effective.compression_artifacts_quality === "number"
? effective.compression_artifacts_quality
: null;
{
const defaultsUsed = isVideoImg2Vid
? "defaultParamsDrawThingsImage2Video"
: i2iProfile === "zoom"
? "defaultParamsZoom"
: i2iProfile === "edit"
? "defaultParamsDrawThingsEdit"
: "defaultParamsDrawThingsImg2Img";
audit.setOutput({
overlay_lookup_mode: overlayMode,
defaults_used: defaultsUsed,
i2i_profile: i2iProfile,
strength_used: typeof effective.strength === "number" &&
Number.isFinite(effective.strength)
? effective.strength
: undefined,
steps_used: typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined,
sampler_used: typeof effective.sampler === "string" &&
String(effective.sampler).trim()
? String(effective.sampler)
: undefined,
guidance_scale_used: typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined,
shift_used: typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined,
resolution_dependent_shift_used: typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined,
num_frames_used: typeof effective.num_frames === "number" ? effective.num_frames : null,
compression_artifacts_used: typeof effective.compressionArtifacts === "string" ? effective.compressionArtifacts
: typeof effective.compression_artifacts === "string" ? effective.compression_artifacts
: null,
compression_artifacts_quality_used: typeof effective.compressionArtifactsQuality === "number"
? effective.compressionArtifactsQuality
: typeof effective.compression_artifacts_quality === "number"
? effective.compression_artifacts_quality
: null,
});
}
// Asset preflight inputs
try {
if (typeof effective.model === "string" && effective.model.trim()) {
requiredModelFile = String(effective.model).trim();
}
const userSpecifiedLoras = Object.prototype.hasOwnProperty.call(filtered, "loras");
const overlaySelected = !!overlayParams;
if (userSpecifiedLoras) {
const ls = Array.isArray(filtered.loras) ? filtered.loras : [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else if (overlaySelected) {
const ls = Array.isArray(overlayParams?.loras)
? overlayParams.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else {
// Fallback: extract LoRAs from effective params (includes defaults)
const ls = Array.isArray(effective?.loras)
? effective.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
}
catch { }
// Upscaler is controlled deterministically by core.
// No heuristics/fallbacks here to avoid legacy drift.
{
const needs = inputParams?._dt_needs_upscaler;
if (typeof needs !== "boolean") {
throw new Error(`Invariant failed: _dt_needs_upscaler must be provided by core for ${i2iProfile}`);
}
if (needs && baseLimits.upscaleMethod !== "zoom-pass") {
effective.upscaler = baseLimits.upscaler;
effective.upscaler_scale = baseLimits.upscalerScaleFactor;
console.info(`DT gRPC ${i2iProfile} upscaler enabled (core)`);
}
else {
effective.upscaler = null;
effective.upscaler_scale = 0;
console.info(`DT gRPC ${i2iProfile} upscaler disabled (core)`);
}
}
const { buildDtGenerationConfiguration } = await import("./drawThingsConfigMapper.js");
const cfg = buildDtGenerationConfiguration(effective);
configBytes = cfg.bytes && cfg.bytes.length ? cfg.bytes : undefined;
try {
const view = cfg?.view || {};
payloadWidth = view.width;
payloadHeight = view.height;
payloadSteps = view.steps;
payloadSeed = view.seed;
payloadSeedMode = view.seed_mode;
payloadModel = String(effective.model || "");
payloadNumFrames = typeof effective.num_frames === "number" ? effective.num_frames : 1;
payloadFps = (typeof filtered.fps === "number" ? filtered.fps : undefined)
?? (0, core_bundle_mjs_1.getDefaultFpsForModel)(payloadModel)
?? (typeof effective.fps === "number" ? effective.fps : 24);
}
catch { }
try {
const view = cfg.view || {};
audit.setOutput({
flatbuf_num_frames: typeof view.num_frames === "number" ? view.num_frames : null,
});
console.info(`[grpc-config] fields=${Object.keys(view).join(",")}`);
}
catch { }
}
catch { }
// Validate requested model/LoRA presence on server to avoid silent fallback rendering.
try {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const chk = await (0, core_bundle_mjs_1.checkDrawThingsGrpcAssets)({
client: this.client,
sharedSecret: sharedSecret || undefined,
modelFile: requiredModelFile,
loraFiles: requiredLoraFiles,
});
if (!chk.ok) {
logErr(`[asset-check] ${chk.details.split("\n")[0]}`);
__cleanupTmp();
return {
isError: true,
status: 400,
errorMessage: overlaySource && overlaySource !== "default"
? `overlay=${overlaySource}${overlayPreset ? ` preset=${overlayPreset}` : ""}\n\n${chk.details}`
: chk.details,
};
}
}
catch (e) {
try {
logErr(`[asset-check] warning: ${e instanceof Error ? e.message : String(e)}`);
}
catch { }
}
// Validate requested model/LoRA presence on server to avoid silent fallback rendering.
try {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const chk = await (0, core_bundle_mjs_1.checkDrawThingsGrpcAssets)({
client: this.client,
sharedSecret: sharedSecret || undefined,
modelFile: requiredModelFile,
loraFiles: requiredLoraFiles,
});
if (!chk.ok) {
logErr(`[asset-check] ${chk.details.split("\n")[0]}`);
return {
isError: true,
status: 400,
errorMessage: overlaySource && overlaySource !== "default"
? `overlay=${overlaySource}${overlayPreset ? ` preset=${overlayPreset}` : ""}\n\n${chk.details}`
: chk.details,
};
}
}
catch (e) {
try {
logErr(`[asset-check] warning: ${e instanceof Error ? e.message : String(e)}`);
}
catch { }
}
const sharedSecret2 = process.env.DRAWTHINGS_SHARED_SECRET;
const request = {
prompt: String(params.prompt || core_bundle_mjs_1.defaultParamsImg2Img.prompt),
negativePrompt: String(params.negative_prompt || ""),
scaleFactor: 1,
keywords: [],
user: "generate-image-plugin",
device: "LAPTOP",
chunked: true,
...(sharedSecret2 ? { sharedSecret: sharedSecret2 } : {}),
};
if (configBytes)
request.configuration = configBytes;
// Convert PNG → NNC tensor blob via PNG2GRPCBin (always attempt)
const projectRoot2 = __resolveProjectRootFrom(__svcDir);
const encoderTools = resolveExternalToolCandidates(projectRoot2, "PNG2GRPCBin", process.env.DT_ENCODER_CMD);
logToolCandidates("i2i encode", encoderTools);
if (encoderTools.length) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-enc-"));
__registerTmp(tmpDir);
const pngPath = path_1.default.join(tmpDir, "input.png");
const imgBinPath = path_1.default.join(tmpDir, "image.bin");
fs_1.default.writeFileSync(pngPath, initPng);
const args = ["--in", pngPath, "--out", imgBinPath];
let lastStdout = "";
let lastStderr = "";
let blob = null;
let toolUsed = null;
for (const tool of encoderTools) {
try {
try {
if (fs_1.default.existsSync(imgBinPath))
fs_1.default.unlinkSync(imgBinPath);
}
catch { }
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
});
if (proc.error) {
lastStderr = proc.error.message;
logErr(`[i2i-encode] exception via ${tool.display}: ${lastStderr}`);
continue;
}
lastStdout = (proc.stdout || Buffer.alloc(0))
.toString("utf8")
.trim();
lastStderr = (proc.stderr || Buffer.alloc(0))
.toString("utf8")
.trim();
if (typeof proc.status === "number" && proc.status !== 0) {
logErr(`[i2i-encode] exit=${proc.status} via ${tool.display} stderr=${lastStderr}`);
continue;
}
if (fs_1.default.existsSync(imgBinPath) &&
fs_1.default.statSync(imgBinPath).size > 0) {
blob = fs_1.default.readFileSync(imgBinPath);
toolUsed = tool;
break;
}
logErr(`[i2i-encode] no output via ${tool.display}; image.bin missing/empty stdout=${lastStdout}`);
}
catch (e) {
logErr(`[i2i-encode] exception via ${tool.display}: ${e instanceof Error ? e.message : String(e)}`);
continue;
}
}
if (blob && toolUsed) {
// Prefer stdout hex; fallback to local SHA-256
let imgHashBuf = null;
if (/^[0-9a-fA-F]{64}$/.test(lastStdout)) {
imgHashBuf = Buffer.from(lastStdout, "hex");
}
else {
const h = crypto_1.default.createHash("sha256").update(blob).digest();
imgHashBuf = h;
}
const hashHex = imgHashBuf.toString("hex");
logFile(`[encoder] encoded input.png → image.bin (${blob.length} bytes) via ${toolUsed.display}`);
// CAS preflight: FilesExist + optional UploadFile (single chunk)
try {
const filesExist = this.client["filesExist"]?.bind(this.client);
if (typeof filesExist === "function") {
await new Promise((resolve) => {
filesExist({
files: [],
filesWithHash: [hashHex],
...(sharedSecret2 ? { sharedSecret: sharedSecret2 } : {}),
}, (err, resp) => {
if (err) {
logErr(`FilesExist error: ${err?.message || String(err)}`);
return resolve();
}
try {
const existArr = Array.isArray(resp?.existences)
? resp.existences
: [];
const exists = existArr[0] === true;
logFile(`[CAS] FilesExist hash=${hashHex} exists=${exists}`);
if (!exists) {
try {
const upload = this.client["uploadFile"]?.call(this.client);
if (upload && typeof upload.write === "function") {
const fname = `image-${hashHex}.bin`;
upload.write({
initRequest: {
filename: fname,
sha256: imgHashBuf,
totalSize: blob.length,
},
...(sharedSecret2
? { sharedSecret: sharedSecret2 }
: {}),
});
upload.write({
chunk: {
content: blob,
filename: fname,
offset: 0,
},
...(sharedSecret2
? { sharedSecret: sharedSecret2 }
: {}),
});
upload.end();
upload.on("end", () => logFile(`[CAS] UploadFile completed hash=${hashHex} size=${blob.length}`));
upload.on("error", (ue) => logErr(`[CAS] UploadFile error: ${ue?.message || String(ue)}`));
}
}
catch (ue) {
logErr(`[CAS] UploadFile exception: ${ue?.message || String(ue)}`);
}
}
}
catch { }
resolve();
});
});
}
}
catch { }
request.image = imgHashBuf;
request.contents = [blob];
logFile(`[i2i-encode] built image.bin (${blob.length} bytes) hash=${imgHashBuf.toString("hex")}`);
// Mask omitted intentionally
// Cleanup
try {
fs_1.default.unlinkSync(pngPath);
}
catch { }
try {
fs_1.default.unlinkSync(imgBinPath);
}
catch { }
// no mask cleanup
}
else {
console.error(`[i2i-encode] encoder did not produce image.bin; stdout=${lastStdout}`);
logErr(`[i2i-encode] encoder did not produce image.bin; stdout=${lastStdout} stderr=${lastStderr}`);
}
}
catch (e) {
console.error(`[i2i-encode] encoder failed: ${e instanceof Error ? e.message : String(e)}`);
logErr(`[i2i-encode] encoder failed: ${e instanceof Error ? e.message : String(e)}`);
}
}
// Fallback when encoder not available or failed: inline PNG (legacy behavior)
if (!request.image ||
!request.contents ||
!Array.isArray(request.contents) ||
request.contents.length === 0) {
request.contents = [Buffer.from(initPng)];
}
const call = this.client[this.methodGenerate]?.bind(this.client);
if (typeof call !== "function")
throw new Error(`gRPC method not found: ${this.methodGenerate}`);
const startTime = Date.now();
const imagesBuffers = [];
const audioBuffers = [];
let chunkAccum = null;
let previewBuf = null;
let lastSeenStep = 0;
const handleData = (resp) => {
try {
if (resp?.currentSignpost?.sampling?.step) {
const currentStep = resp.currentSignpost.sampling.step;
lastSeenStep = currentStep;
console.info(`gRPC progress: sampling step=${currentStep}`);
logFile(`i2i progress: sampling step=${currentStep}`);
// Invoke progress callback if provided
if (onProgress) {
try {
onProgress(currentStep, payloadSteps, `Sampling step ${currentStep}${payloadSteps ? `/${payloadSteps}` : ""}`);
}
catch { }
}
}
else if (resp?.currentSignpost != null && onProgress) {
const sp = resp.currentSignpost;
let label;
if (lastSeenStep === 0) {
if (sp.textEncoded != null)
label = "Loading...";
else if (sp.imageEncoded != null)
label = "Processing...";
}
else {
label = "Finishing...";
}
if (label) {
try {
onProgress(-1, payloadSteps, label);
}
catch { }
}
}
if (resp?.generatedImages &&
Array.isArray(resp.generatedImages)) {
const list = resp.generatedImages.map((b) => Buffer.isBuffer(b) ? b : Buffer.from(b));
const cs = resp?.chunkState;
const isMore = cs === 1 || cs === "MORE_CHUNKS";
const isLast = cs === 0 || cs === "LAST_CHUNK";
if (isMore) {
if (list.length > 0) {
chunkAccum = chunkAccum
? Buffer.concat([chunkAccum, list[0]])
: Buffer.from(list[0]);
}
}
else if (isLast && list.length > 0) {
if (chunkAccum) {
const full = Buffer.concat([chunkAccum, list[0]]);
imagesBuffers.push(full);
chunkAccum = null;
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
if (resp?.previewImage) {
const b = resp.previewImage;
previewBuf = Buffer.isBuffer(b) ? b : Buffer.from(b);
}
if (resp?.generatedAudio && Array.isArray(resp.generatedAudio)) {
for (const a of resp.generatedAudio) {
const ab = Buffer.isBuffer(a) ? a : Buffer.from(a);
if (ab.length > 0)
audioBuffers.push(ab);
}
}
}
catch { }
};
await new Promise((resolve, reject) => {
try {
const stream = call(request);
stream.on("data", handleData);
stream.on("error", (err) => reject(err));
stream.on("end", () => resolve());
}
catch (e) {
reject(e);
}
});
const endTime = Date.now();
// Always attempt to decode tensor bytes to PNG via external CLI (same as txt2img)
const decoderTools2 = resolveExternalToolCandidates(__projectRoot, "GRPCBin2PNG", process.env.DT_DECODER_CMD);
logToolCandidates("i2i decode", decoderTools2);
if (decoderTools2.length && imagesBuffers.length > 0) {
const finalW = Number(params.width || core_bundle_mjs_1.defaultParamsImg2Img.width || 1024);
const finalH = Number(params.height || core_bundle_mjs_1.defaultParamsImg2Img.height || 1024);
const decoderCtx = JSON.stringify({
width: finalW,
height: finalH,
channels: 3,
dtype: "f32",
});
const decoded = [];
for (let i = 0; i < imagesBuffers.length; i++) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-dt-"));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, `img2img-${i + 1}.bin`);
const outPath = path_1.default.join(tmpDir, `img2img-${i + 1}.png`);
fs_1.default.writeFileSync(inPath, imagesBuffers[i]);
const args = ["--in", inPath, "--out", outPath];
let decodedOne = null;
for (const tool of decoderTools2) {
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, DT_DECODER_CTX: decoderCtx },
});
if (proc.error) {
logErr(`[decoder] exception via ${tool.display}: ${proc.error.message}`);
continue;
}
if (proc.status !== 0) {
logErr(`[decoder] exit=${proc.status} via ${tool.display} stderr=${proc.stderr?.toString?.() || ""}`);
continue;
}
if (fs_1.default.existsSync(outPath) && fs_1.default.statSync(outPath).size > 0) {
decodedOne = fs_1.default.readFileSync(outPath);
break;
}
}
if (decodedOne) {
decoded.push(decodedOne);
try {
fs_1.default.unlinkSync(inPath);
}
catch { }
}
else {
decoded.push(imagesBuffers[i]);
}
}
catch {
decoded.push(imagesBuffers[i]);
}
}
if (decoded.length > 0) {
imagesBuffers.length = 0;
imagesBuffers.push(...decoded);
logFile(`[decoder] i2i: decoded ${decoded.length} frame(s) → PNG via ${decoderTools2[0].display}`);
}
}
let primaryBuf;
if (imagesBuffers.length > 0)
primaryBuf = imagesBuffers[0];
else if (previewBuf) {
// Try decoding preview as well when needed
if (decoderTools2.length && previewBuf) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-dt-"));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, `preview-i2i.bin`);
const outPath = path_1.default.join(tmpDir, `preview-i2i.png`);
fs_1.default.writeFileSync(inPath, previewBuf);
const finalW = Number(params.width || core_bundle_mjs_1.defaultParamsImg2Img.width || 1024);
const finalH = Number(params.height || core_bundle_mjs_1.defaultParamsImg2Img.height || 1024);
const decoderCtx = JSON.stringify({
width: finalW,
height: finalH,
channels: 3,
dtype: "f32",
});
const args = ["--in", inPath, "--out", outPath];
let decodedPrev = null;
for (const tool of decoderTools2) {
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, DT_DECODER_CTX: decoderCtx },
});
if (proc.error)
continue;
if (proc.status !== 0)
continue;
if (fs_1.default.existsSync(outPath) && fs_1.default.statSync(outPath).size > 0) {
decodedPrev = fs_1.default.readFileSync(outPath);
logFile(`[decoder] decoded preview-i2i.bin → PNG (${decodedPrev.length} bytes) via ${tool.display}`);
break;
}
}
primaryBuf = decodedPrev ? decodedPrev : previewBuf;
try {
fs_1.default.unlinkSync(inPath);
}
catch { }
}
catch {
primaryBuf = previewBuf;
}
}
else {
primaryBuf = previewBuf;
}
}
if (!primaryBuf)
throw new Error("gRPC stream yielded no image data");
// Service-level audit disabled (consolidated audit is logged in index.ts)
// Service-level audit disabled (consolidated audit is logged in index.ts)
const dataUrl = `data:image/png;base64,${primaryBuf.toString("base64")}`;
logFile(`i2i completed: images=${imagesBuffers.length}, bytes=${primaryBuf.length}`);
// Effective dims from payload (FlatBuffer view) when available; else fall back
const effW = Number(payloadWidth ??
params.width ??
core_bundle_mjs_1.defaultParamsImg2Img.width ??
1024);
const effH = Number(payloadHeight ??
params.height ??
core_bundle_mjs_1.defaultParamsImg2Img.height ??
1024);
const result = {
isError: false,
imageBuffer: primaryBuf,
imageData: dataUrl,
images: imagesBuffers.length
? imagesBuffers.map((b) => `data:image/png;base64,${b.toString("base64")}`)
: undefined,
metadata: {
alt: `Image-to-image from prompt: ${request.prompt}`,
inference_time_ms: endTime - startTime,
model: payloadModel,
width: effW,
height: effH,
requested_dimensions: {
width: effW,
height: effH,
},
steps: Number.isFinite(Number(payloadSteps))
? Number(payloadSteps)
: undefined,
seed: typeof payloadSeed === "number" && Number.isFinite(payloadSeed)
? payloadSeed
: undefined,
seed_mode: typeof payloadSeedMode === "string" && payloadSeedMode.trim()
? payloadSeedMode
: undefined,
prompt_used: String(request.prompt || ""),
prompt_origin: promptOriginI2I,
transport: "grpc",
stream_chunks: imagesBuffers.length,
num_frames: payloadNumFrames,
fps: payloadFps,
// Overlay source tracking for audit
overlay_source: overlaySource,
...(overlayPreset && { overlay_preset: overlayPreset }),
...(defaultsUsed && { defaults_used: defaultsUsed }),
...(overlayLookupMode && { overlay_lookup_mode: overlayLookupMode }),
...(i2iProfileUsed && { i2i_profile: i2iProfileUsed }),
...(typeof strengthUsed === "number" && { strength_used: strengthUsed }),
...(typeof stepsUsed === "number" && { steps_used: stepsUsed }),
...(samplerUsed && { sampler_used: samplerUsed }),
...(typeof guidanceScaleUsed === "number" && {
guidance_scale_used: guidanceScaleUsed,
}),
...(typeof shiftUsed === "number" && { shift_used: shiftUsed }),
...(typeof resolutionDependentShiftUsed === "boolean" && {
resolution_dependent_shift_used: resolutionDependentShiftUsed,
}),
...(compressionArtifactsUsed != null && {
compression_artifacts_used: compressionArtifactsUsed,
}),
...(compressionArtifactsQualityUsed != null && {
compression_artifacts_quality_used: compressionArtifactsQualityUsed,
}),
...(requiredLoraFiles &&
requiredLoraFiles.length > 0 && { loras_used: requiredLoraFiles }),
},
audioBuffers: audioBuffers.length > 0 ? audioBuffers : undefined,
};
__cleanupTmp();
return result;
}
catch (e) {
try {
// attempt temp cleanup on error as well
}
catch { }
return this.toBackendError(e);
}
}
/**
* Edit mode generation with multiple reference images.
* Canvas (first buffer) gets priority; remaining are moodboard.
*/
async generateImageEdit(inputParams = {}, referenceBuffers, onProgress) {
const rawProfile = inputParams?._dt_i2i_profile;
const i2iProfileUsed = rawProfile === "img2img" ? "img2img" : "edit";
// Multi-reference calls can be used for BOTH user modes:
// - mode='edit' => use edit defaults/overlays
// - mode='image2image' (with moodboard via gRPC) => use img2img defaults/overlays
const overlayMode = i2iProfileUsed;
// For MVP: delegate to generateImageImg2Img with edit profile
// Multi-reference will use contents[] array
if (!referenceBuffers || referenceBuffers.length === 0) {
return {
isError: true,
errorMessage: "Edit mode requires at least one reference image",
};
}
// Refactor-proofing: multi-reference is a shared entry point (edit + image2image).
// If core ever forgets to pass the profile, we default to edit but emit a loud warning.
if (referenceBuffers.length > 1 && rawProfile !== "img2img" && rawProfile !== "edit") {
try {
logFile("[multi-ref] WARNING: _dt_i2i_profile missing; defaulting to edit profile. This indicates a core/service interface regression.");
}
catch { }
}
// For single-reference edit, use existing img2img path with edit profile
if (referenceBuffers.length === 1) {
const editParams = { ...inputParams, _dt_i2i_profile: "edit" };
return this.generateImageImg2Img(editParams, referenceBuffers[0], onProgress);
}
// Multi-reference edit: encode all buffers and send via contents[]
try {
const __tmpDirs = [];
const __registerTmp = (d) => {
try {
__tmpDirs.push(d);
}
catch { }
};
const __cleanupTmp = () => {
for (const d of __tmpDirs) {
try {
fs_1.default.rmSync(d, { recursive: true, force: true });
}
catch { }
}
};
const audit = (0, core_bundle_mjs_1.buildAuditLogger)({
backend: this.name,
mode: overlayMode === "img2img" ? "img2img" : "edit",
});
// NOTE: Service-level audit is kept minimal; full audit in core (tools.ts)
this.ensureClient();
// Overlay tracking for result metadata
let overlaySource = "default";
let overlayPreset;
// Debug metadata (propagated to core audit via result.metadata)
let defaultsUsed;
let overlayLookupMode;
let strengthUsed;
let stepsUsed;
let samplerUsed;
let guidanceScaleUsed;
let shiftUsed;
let resolutionDependentShiftUsed;
// LoRA tracking
let requiredLoraFiles;
const baseDefaults = overlayMode === "edit" ? core_bundle_mjs_1.defaultParamsEdit : core_bundle_mjs_1.defaultParamsImg2Img;
const baseLimits = overlayMode === "edit" ? core_bundle_mjs_1.drawthingsEditLimits : core_bundle_mjs_1.drawthingsLimits;
let params = {};
try {
const r = (0, core_bundle_mjs_1.validateImageGenerationParams)(inputParams);
if (r.valid)
params = inputParams;
}
catch { }
if (!params.prompt)
params.prompt = inputParams.prompt || baseDefaults.prompt;
// Map variants parameter to batch_size for multi-variant generation
try {
const variants = inputParams.variants;
if (typeof variants === "number") {
const v = Math.max(1, Math.min(4, Math.round(variants)));
params.batch_size = v;
params.batch_count = 1;
}
}
catch { }
// Encode all reference images to NNC tensors
const resolveEncoderCmd = () => {
const envCmd = process.env.DT_ENCODER_CMD;
const projectRoot2 = __resolveProjectRootFrom(__svcDir);
const candidates = buildTensorHelperCandidates(projectRoot2, "PNG2GRPCBin", envCmd);
for (const c of candidates) {
try {
const abs = path_1.default.isAbsolute(c) ? c : path_1.default.resolve(projectRoot2, c);
if (!fs_1.default.existsSync(abs))
continue;
const origin = c === envCmd ? "env" : "auto";
if (abs.endsWith(".js")) {
return {
cmd: process.execPath,
argsPrefix: [abs],
display: abs,
impl: "ts",
origin,
};
}
try {
const st = fs_1.default.statSync(abs);
if ((st.mode & 0o111) === 0) {
try {
fs_1.default.chmodSync(abs, 0o755);
}
catch { }
}
}
catch { }
return {
cmd: abs,
argsPrefix: [],
display: abs,
impl: "mac-bin",
origin,
};
}
catch { }
}
return null;
};
const encoderCmd = resolveEncoderCmd();
logToolSelection("edit encode", encoderCmd);
if (!encoderCmd) {
return {
isError: true,
errorMessage: "PNG2GRPCBin encoder not found for multi-reference edit",
};
}
const contentsTensors = [];
let primaryHash = null;
const moodboardHashes = []; // SHA256 hashes for shuffle hints
let canvasW;
let canvasH;
for (let i = 0; i < referenceBuffers.length; i++) {
const refBuf = referenceBuffers[i];
const pngBuf = await (0, core_bundle_mjs_1.toPng)(refBuf);
if (i === 0) {
try {
const meta = await (0, core_bundle_mjs_1.getSize)(pngBuf);
canvasW = meta.width;
canvasH = meta.height;
}
catch { }
}
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `generate-image-plugin-edit-${i}-`));
__registerTmp(tmpDir);
const pngPath = path_1.default.join(tmpDir, "input.png");
const imgBinPath = path_1.default.join(tmpDir, "image.bin");
fs_1.default.writeFileSync(pngPath, pngBuf);
const args = ["--in", pngPath, "--out", imgBinPath];
const proc = (0, child_process_1.spawnSync)(encoderCmd.cmd, [...encoderCmd.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
});
if (proc.error) {
__cleanupTmp();
return {
isError: true,
errorMessage: `Encoder failed for reference ${i + 1}: ${proc.error.message}`,
};
}
if (typeof proc.status === "number" && proc.status !== 0) {
const stdout = proc.stdout ? proc.stdout.toString("utf8") : "";
const stderr = proc.stderr ? proc.stderr.toString("utf8") : "";
logErr(`[edit] encoder exit=${proc.status} ref=${i + 1} cmd=${encoderCmd.cmd} ${[...encoderCmd.argsPrefix, ...args].join(" ")}`);
if (stdout.trim())
logFile(`[edit] encoder stdout ref=${i + 1}: ${stdout.trim()}`);
if (stderr.trim())
logErr(`[edit] encoder stderr ref=${i + 1}: ${stderr.trim()}`);
__cleanupTmp();
return {
isError: true,
errorMessage: `Encoder failed for reference ${i + 1} (exit ${proc.status})${stderr.trim() ? `: ${stderr.trim()}` : ""}`,
};
}
if (!fs_1.default.existsSync(imgBinPath) || fs_1.default.statSync(imgBinPath).size === 0) {
const stdout = proc.stdout ? proc.stdout.toString("utf8") : "";
const stderr = proc.stderr ? proc.stderr.toString("utf8") : "";
if (stdout.trim())
logFile(`[edit] encoder stdout ref=${i + 1}: ${stdout.trim()}`);
if (stderr.trim())
logErr(`[edit] encoder stderr ref=${i + 1}: ${stderr.trim()}`);
__cleanupTmp();
return {
isError: true,
errorMessage: `Encoder produced no output for reference ${i + 1}`,
};
}
const blob = fs_1.default.readFileSync(imgBinPath);
contentsTensors.push(blob);
// Compute SHA256 hash for each tensor
const tensorHash = crypto_1.default.createHash("sha256").update(blob).digest();
// First image is the canvas (primary image)
if (i === 0) {
primaryHash = tensorHash;
}
else {
// Moodboard images: collect hashes for shuffle hints
moodboardHashes.push(tensorHash);
}
// Cleanup temp files
try {
fs_1.default.unlinkSync(pngPath);
}
catch { }
try {
fs_1.default.unlinkSync(imgBinPath);
}
catch { }
}
logFile(`[${overlayMode === "img2img" ? "image2image" : "edit"}] encoded ${contentsTensors.length} reference images for multi-ref`);
// Match img2img behavior: internal processing size follows the actual canvas tensor size.
// This keeps edit and img2img identical except for moodboard hints.
if (canvasW && canvasH) {
params.original_width = canvasW;
params.original_height = canvasH;
const hasW = typeof params.width === "number" &&
Number.isFinite(params.width);
const hasH = typeof params.height === "number" &&
Number.isFinite(params.height);
if (!hasW)
params.width = canvasW;
if (!hasH)
params.height = canvasH;
const hasTW = typeof params.target_width === "number" &&
Number.isFinite(params.target_width);
const hasTH = typeof params.target_height === "number" &&
Number.isFinite(params.target_height);
if (!hasTW)
params.target_width = params.width;
if (!hasTH)
params.target_height = params.height;
}
// Build FlatBuffer config
let configBytes = undefined;
let payloadWidth;
let payloadHeight;
let payloadSteps;
let payloadSeed;
let payloadSeedMode;
let payloadModel;
let payloadNumFrames = 1;
let payloadFps = 24;
try {
const allowedKeys = new Set(Object.keys(baseDefaults));
const filtered = {};
for (const [k, v] of Object.entries(params)) {
if (allowedKeys.has(k))
filtered[k] = v;
}
const modelId = params.model;
const { source, presetName, params: overlayParams, } = (0, customConfigsLoader_js_1.getEffectiveOverlay)(modelId, overlayMode);
// Size must be controlled by tool/core (or defaults), not by overlays.
// Batch must be controlled by tool/core via variants parameter.
// Upscaler must be controlled by tool/core via _dt_needs_upscaler decision.
const overlayParamsNoSize = (() => {
if (!overlayParams)
return overlayParams;
const o = { ...overlayParams };
delete o.width;
delete o.height;
delete o.batch_count;
delete o.batch_size;
delete o.batchCount;
delete o.batchSize;
delete o.upscaler;
delete o.upscaler_scale;
delete o.upscalerScale;
delete o.fps;
return o;
})();
// Capture overlay info for result metadata
overlaySource = source;
overlayPreset = presetName;
if (modelId === "auto") {
delete filtered.model;
}
if (overlayParams) {
logFile(`Applying ${source} overlay for '${modelId ?? "auto"}' (${overlayMode === "img2img" ? "image2image" : "edit"}, gRPC)${presetName ? ` preset=${presetName}` : ""}`);
delete filtered.model;
}
// Log overlay source to audit
audit.setOutput({
overlay_source: source,
...(presetName && { overlay_preset: presetName }),
});
const effective = {
...baseDefaults,
...(overlayParamsNoSize || {}),
...filtered,
seed: baseDefaults.seed,
};
defaultsUsed =
overlayMode === "edit"
? "defaultParamsDrawThingsEdit"
: "defaultParamsDrawThingsImg2Img";
overlayLookupMode = overlayMode;
strengthUsed =
typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined;
stepsUsed =
typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined;
samplerUsed =
typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined;
guidanceScaleUsed =
typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined;
shiftUsed =
typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined;
resolutionDependentShiftUsed =
typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined;
audit.setOutput({
overlay_lookup_mode: overlayMode,
defaults_used: overlayMode === "edit"
? "defaultParamsDrawThingsEdit"
: "defaultParamsDrawThingsImg2Img",
strength_used: typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined,
steps_used: typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined,
sampler_used: typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined,
guidance_scale_used: typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined,
shift_used: typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined,
resolution_dependent_shift_used: typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined,
});
// Extract LoRAs for audit: user > overlay > defaults
try {
const userSpecifiedLoras = Object.prototype.hasOwnProperty.call(filtered, "loras");
const overlaySelected = !!overlayParams;
if (userSpecifiedLoras) {
const ls = Array.isArray(filtered.loras) ? filtered.loras : [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else if (overlaySelected) {
const ls = Array.isArray(overlayParams?.loras)
? overlayParams.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else {
// Fallback: extract LoRAs from effective params (includes defaults)
const ls = Array.isArray(effective?.loras)
? effective.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
}
catch { }
const { buildDtGenerationConfiguration } = await import("./drawThingsConfigMapper.js");
const cfg = buildDtGenerationConfiguration(effective);
configBytes = cfg.bytes && cfg.bytes.length ? cfg.bytes : undefined;
try {
const view = cfg?.view || {};
payloadWidth = view.width;
payloadHeight = view.height;
payloadSteps = view.steps;
payloadSeed = view.seed;
payloadSeedMode = view.seed_mode;
payloadModel = String(effective.model || "");
payloadNumFrames = typeof effective.num_frames === "number" ? effective.num_frames : 1;
payloadFps = (typeof filtered.fps === "number" ? filtered.fps : undefined)
?? (0, core_bundle_mjs_1.getDefaultFpsForModel)(payloadModel)
?? (typeof effective.fps === "number" ? effective.fps : 24);
}
catch { }
}
catch { }
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
// Build shuffle hints for moodboard images
// CRITICAL: Moodboard images must be referenced via hints with hintType="shuffle"
// See: moodboard_grpc_research.md for details
// Weight distribution: 1/n per image (matches Draw Things client behavior)
const moodboardWeight = moodboardHashes.length > 0 ? 1.0 / moodboardHashes.length : 1.0;
const shuffleHints = moodboardHashes.map((hash) => ({
tensor: hash, // SHA256 hash (32 bytes) referencing tensor in contents[]
weight: moodboardWeight,
}));
const request = {
prompt: String(params.prompt || baseDefaults.prompt),
negativePrompt: String(params.negative_prompt || ""),
scaleFactor: 1,
keywords: [],
user: "generate-image-plugin",
device: "LAPTOP",
chunked: true,
...(sharedSecret ? { sharedSecret } : {}),
image: primaryHash, // Hash of first (canvas) image
contents: contentsTensors, // All reference tensors (Content-Addressable Storage)
// Moodboard images as shuffle hints
...(shuffleHints.length > 0
? {
hints: [
{
hintType: "shuffle", // MUST be "shuffle" for moodboard/reference images
tensors: shuffleHints,
},
],
}
: {}),
};
if (configBytes)
request.configuration = configBytes;
logFile(`[edit] request built: canvas=1, moodboard=${shuffleHints.length}, total_contents=${contentsTensors.length}`);
const call = this.client[this.methodGenerate]?.bind(this.client);
if (typeof call !== "function")
throw new Error(`gRPC method not found: ${this.methodGenerate}`);
const startTime = Date.now();
const imagesBuffers = [];
const audioBuffers = [];
let chunkAccum = null;
let previewBuf = null;
let lastSeenStep = 0;
const handleData = (resp) => {
try {
if (resp?.currentSignpost?.sampling?.step) {
const currentStep = resp.currentSignpost.sampling.step;
lastSeenStep = currentStep;
console.info(`gRPC progress: sampling step=${currentStep}`);
logFile(`edit progress: sampling step=${currentStep}`);
// Invoke progress callback if provided
if (onProgress) {
try {
onProgress(currentStep, payloadSteps, `Sampling step ${currentStep}${payloadSteps ? `/${payloadSteps}` : ""}`);
}
catch { }
}
}
else if (resp?.currentSignpost != null && onProgress) {
const sp = resp.currentSignpost;
let label;
if (lastSeenStep === 0) {
if (sp.textEncoded != null)
label = "Loading...";
else if (sp.imageEncoded != null)
label = "Processing...";
}
else {
label = "Finishing...";
}
if (label) {
try {
onProgress(-1, payloadSteps, label);
}
catch { }
}
}
if (resp?.generatedImages &&
Array.isArray(resp.generatedImages)) {
const list = resp.generatedImages.map((b) => Buffer.isBuffer(b) ? b : Buffer.from(b));
const cs = resp?.chunkState;
const isMore = cs === 1 || cs === "MORE_CHUNKS";
const isLast = cs === 0 || cs === "LAST_CHUNK";
if (isMore) {
if (list.length > 0) {
chunkAccum = chunkAccum
? Buffer.concat([chunkAccum, list[0]])
: Buffer.from(list[0]);
}
}
else if (isLast && list.length > 0) {
if (chunkAccum) {
const full = Buffer.concat([chunkAccum, list[0]]);
imagesBuffers.push(full);
chunkAccum = null;
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
if (resp?.previewImage) {
const b = resp.previewImage;
previewBuf = Buffer.isBuffer(b) ? b : Buffer.from(b);
}
if (resp?.generatedAudio && Array.isArray(resp.generatedAudio)) {
for (const a of resp.generatedAudio) {
const ab = Buffer.isBuffer(a) ? a : Buffer.from(a);
if (ab.length > 0)
audioBuffers.push(ab);
}
}
}
catch { }
};
await new Promise((resolve, reject) => {
try {
const stream = call(request);
stream.on("data", handleData);
stream.on("error", (err) => reject(err));
stream.on("end", () => resolve());
}
catch (e) {
reject(e);
}
});
const endTime = Date.now();
// Decode tensor to PNG
const resolveDecoderCmd = () => {
const envCmd = process.env.DT_DECODER_CMD;
const projectRoot2 = __projectRoot;
const candidates = buildTensorHelperCandidates(projectRoot2, "GRPCBin2PNG", envCmd);
for (const c of candidates) {
try {
const abs = path_1.default.isAbsolute(c) ? c : path_1.default.resolve(projectRoot2, c);
if (!fs_1.default.existsSync(abs))
continue;
const origin = c === envCmd ? "env" : "auto";
if (abs.endsWith(".js")) {
return {
cmd: process.execPath,
argsPrefix: [abs],
display: abs,
impl: "ts",
origin,
};
}
try {
const st = fs_1.default.statSync(abs);
if ((st.mode & 0o111) === 0) {
try {
fs_1.default.chmodSync(abs, 0o755);
}
catch { }
}
}
catch { }
return {
cmd: abs,
argsPrefix: [],
display: abs,
impl: "mac-bin",
origin,
};
}
catch { }
}
return null;
};
const decoderCmd = resolveDecoderCmd();
logToolSelection("edit decode", decoderCmd);
let primaryBuf = null;
if (decoderCmd && imagesBuffers.length > 0) {
const pngBuffers = [];
for (let i = 0; i < imagesBuffers.length; i++) {
const rawBuf = imagesBuffers[i];
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `generate-image-plugin-dec-${i}-`));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, "input.bin");
const outPath = path_1.default.join(tmpDir, "output.png");
fs_1.default.writeFileSync(inPath, rawBuf);
const proc = (0, child_process_1.spawnSync)(decoderCmd.cmd, [...decoderCmd.argsPrefix, "--in", inPath, "--out", outPath], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
});
if (!proc.error &&
fs_1.default.existsSync(outPath) &&
fs_1.default.statSync(outPath).size > 0) {
pngBuffers.push(fs_1.default.readFileSync(outPath));
}
}
catch { }
}
if (pngBuffers.length > 0) {
primaryBuf = pngBuffers[0];
imagesBuffers.length = 0;
for (const b of pngBuffers)
imagesBuffers.push(b);
}
}
if (!primaryBuf && previewBuf) {
primaryBuf = previewBuf;
}
if (!primaryBuf)
throw new Error("gRPC edit stream yielded no image data");
const dataUrl = `data:image/png;base64,${primaryBuf.toString("base64")}`;
logFile(`edit completed: refs=${referenceBuffers.length}, images=${imagesBuffers.length}, bytes=${primaryBuf.length}`);
const effW = Number(payloadWidth ?? params.width ?? 1024);
const effH = Number(payloadHeight ?? params.height ?? 1024);
const result = {
isError: false,
imageBuffer: primaryBuf,
imageData: dataUrl,
images: imagesBuffers.length
? imagesBuffers.map((b) => `data:image/png;base64,${b.toString("base64")}`)
: undefined,
metadata: {
alt: overlayMode === "img2img"
? `Image-to-image from prompt: ${request.prompt}`
: `Edit from prompt: ${request.prompt}`,
inference_time_ms: endTime - startTime,
model: payloadModel,
width: effW,
height: effH,
requested_dimensions: {
width: effW,
height: effH,
},
steps: Number.isFinite(Number(payloadSteps))
? Number(payloadSteps)
: undefined,
seed: typeof payloadSeed === "number" && Number.isFinite(payloadSeed)
? payloadSeed
: undefined,
seed_mode: typeof payloadSeedMode === "string" && payloadSeedMode.trim()
? payloadSeedMode
: undefined,
prompt_used: String(request.prompt || ""),
reference_count: referenceBuffers.length,
transport: "grpc",
stream_chunks: imagesBuffers.length,
num_frames: payloadNumFrames,
fps: payloadFps,
// Overlay source tracking for audit
overlay_source: overlaySource,
...(overlayPreset && { overlay_preset: overlayPreset }),
...(defaultsUsed && { defaults_used: defaultsUsed }),
...(overlayLookupMode && { overlay_lookup_mode: overlayLookupMode }),
i2i_profile: i2iProfileUsed,
...(typeof strengthUsed === "number" && { strength_used: strengthUsed }),
...(typeof stepsUsed === "number" && { steps_used: stepsUsed }),
...(samplerUsed && { sampler_used: samplerUsed }),
...(typeof guidanceScaleUsed === "number" && {
guidance_scale_used: guidanceScaleUsed,
}),
...(typeof shiftUsed === "number" && { shift_used: shiftUsed }),
...(typeof resolutionDependentShiftUsed === "boolean" && {
resolution_dependent_shift_used: resolutionDependentShiftUsed,
}),
...(requiredLoraFiles &&
requiredLoraFiles.length > 0 && { loras_used: requiredLoraFiles }),
},
audioBuffers: audioBuffers.length > 0 ? audioBuffers : undefined,
};
__cleanupTmp();
return result;
}
catch (e) {
return this.toBackendError(e);
}
}
}
exports.DrawThingsGrpcService = DrawThingsGrpcService;
exports.default = DrawThingsGrpcService;
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DrawThingsGrpcService = void 0;
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const grpc = __importStar(require("@grpc/grpc-js"));
const protoLoader = __importStar(require("@grpc/proto-loader"));
const core_bundle_mjs_1 = require("../core-bundle.mjs");
const crypto_1 = __importDefault(require("crypto"));
const child_process_1 = require("child_process");
const os_1 = __importDefault(require("os"));
const customConfigsLoader_js_1 = require("./customConfigsLoader.js");
// Simple file logger (aligned with index.ts logs directory) – CJS-safe
// Local project-root resolver (no top-level await, CJS-safe)
const __svcDir = (() => {
try {
const p = process.argv && process.argv[1] ? process.argv[1] : process.cwd();
return path_1.default.dirname(p);
}
catch {
return process.cwd();
}
})();
function __resolveProjectRootFrom(startDir) {
// Prefer directory containing manifest.json or package.json
try {
let dir = startDir;
for (let i = 0; i < 50; i++) {
try {
const hasManifest = fs_1.default.existsSync(path_1.default.join(dir, "manifest.json"));
const hasPkg = fs_1.default.existsSync(path_1.default.join(dir, "package.json"));
if (hasManifest || hasPkg)
return dir;
}
catch { }
const parent = path_1.default.dirname(dir);
if (parent === dir)
break;
dir = parent;
}
}
catch { }
// Else, fallback to dist/src markers
{
let dir = startDir;
for (let i = 0; i < 20; i++) {
if (path_1.default.basename(dir) === "dist")
return path_1.default.dirname(dir);
const parent = path_1.default.dirname(dir);
if (parent === dir)
break;
dir = parent;
}
}
{
let dir = startDir;
for (let i = 0; i < 5; i++) {
if (path_1.default.basename(dir) === "src")
return path_1.default.dirname(dir);
const parent = path_1.default.dirname(dir);
if (parent === dir)
break;
dir = parent;
}
}
return startDir;
}
const __projectRoot = __resolveProjectRootFrom(__svcDir);
const __logsDir = path_1.default.join(__projectRoot, "logs");
try {
if (!fs_1.default.existsSync(__logsDir))
fs_1.default.mkdirSync(__logsDir, { recursive: true });
}
catch { }
function localTs() {
try {
return new Date().toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZoneName: "short",
});
}
catch {
return new Date().toString();
}
}
function logFile(msg) {
const line = `${localTs()} - ${msg}\n`;
try {
fs_1.default.appendFileSync(path_1.default.join(__logsDir, "generate-image-plugin.log"), line);
}
catch { }
}
function logErr(msg) {
const line = `${localTs()} - ERROR: ${msg}\n`;
try {
fs_1.default.appendFileSync(path_1.default.join(__logsDir, "error.log"), line);
}
catch { }
try {
console.error(`[draw-things-chat] ${msg}`);
}
catch { }
}
function getTensorHelpersMode() {
const raw = String(process.env.DT_TENSOR_HELPERS_MODE || "auto")
.trim()
.toLowerCase();
if (raw === "ts" || raw === "node" || raw === "js")
return "ts";
if (raw === "bin" || raw === "binary" || raw === "native")
return "bin";
return "auto";
}
function buildTensorHelperCandidates(projectRoot, helperName, envCmd) {
const jsPath = path_1.default.resolve(projectRoot, "dist", "helpers", `${helperName}.js`);
const binDistPath = path_1.default.resolve(projectRoot, "dist", "helpers", helperName);
const mode = getTensorHelpersMode();
const ordered = mode === "bin"
? [envCmd, binDistPath, jsPath]
: [envCmd, jsPath, binDistPath];
return ordered.filter(Boolean);
}
function logToolSelection(context, tool) {
if (!tool) {
logFile(`[tool] ${context} not found (no decoder/encoder available)`);
return;
}
logFile(`[tool] ${context} origin=${tool.origin} impl=${tool.impl} path=${tool.display}`);
}
function resolveExternalToolCandidates(projectRoot, helperName, envCmd) {
const candidates = buildTensorHelperCandidates(projectRoot, helperName, envCmd);
const tools = [];
for (const c of candidates) {
try {
const abs = path_1.default.isAbsolute(c) ? c : path_1.default.resolve(projectRoot, c);
if (!fs_1.default.existsSync(abs))
continue;
const origin = c === envCmd ? "env" : "auto";
if (abs.endsWith(".js")) {
tools.push({
cmd: process.execPath,
argsPrefix: [abs],
display: abs,
impl: "ts",
origin,
});
continue;
}
try {
const st = fs_1.default.statSync(abs);
if ((st.mode & 0o111) === 0) {
try {
fs_1.default.chmodSync(abs, 0o755);
}
catch { }
}
}
catch { }
tools.push({
cmd: abs,
argsPrefix: [],
display: abs,
impl: "mac-bin",
origin,
});
}
catch { }
}
return tools;
}
function logToolCandidates(context, tools) {
if (!tools.length) {
logFile(`[tool] ${context} not found (no decoder/encoder available)`);
return;
}
logFile(`[tool] ${context} candidates=${tools.length} primary=${tools[0].display}`);
}
/**
* Normalize numFrames for the Draw Things backend.
* Valid values: 1 (image), or multiples of 32 + 1 (e.g. 33, 65, 97, 129, ..., 641).
* Accepts ×32 (auto-corrects to ×32+1) and rounds invalid values to nearest valid.
*/
function normalizeNumFrames(n) {
if (n <= 1)
return { value: 1 };
if ((n - 1) % 32 === 0)
return { value: n }; // ×32+1 → 1:1
if (n % 32 === 0)
return { value: n + 1 }; // ×32 → +1
const rounded = Math.round(n / 32) * 32 + 1; // nearest ×32+1
const clamped = Math.max(33, Math.min(641, rounded));
return {
value: clamped,
hint: `numFrames adjusted to ${clamped} (must be multiple of 32 + 1).`,
};
}
/**
* DrawThingsGrpcService
* A transport-swappable alternative to DrawThingsService (HTTP).
*
* Configuration (ENV):
* - DRAWTHINGS_GRPC_TARGET (e.g. "127.0.0.1:50051")
* - DRAWTHINGS_GRPC_PROTO (absolute path to .proto)
* - DRAWTHINGS_GRPC_PACKAGE (e.g. "drawthings")
* - DRAWTHINGS_GRPC_SERVICE (e.g. "StableDiffusion")
* - DRAWTHINGS_GRPC_METHOD_TXT2IMG (default: "Txt2Img")
* - DRAWTHINGS_GRPC_METHOD_IMG2IMG (default: "Img2Img")
*/
class DrawThingsGrpcService {
constructor(target) {
this.name = "drawthings";
this.client = null;
this.tlsMode = "auto";
this.currentSecurity = "unknown";
this.baseUrl =
target ||
process.env.DRAWTHINGS_GRPC_TARGET ||
`127.0.0.1:${process.env.DRAWTHINGS_GRPC_PORT || 7859}`;
this.serviceName =
process.env.DRAWTHINGS_GRPC_SERVICE || "ImageGenerationService";
// grpc-js maps RPC names to lowerCamelCase
this.methodGenerate = "generateImage";
this.grpcCompression = (process.env.DRAWTHINGS_GRPC_COMPRESSION || "identity").toLowerCase();
this.loadClient();
}
setBaseUrl(url) {
this.baseUrl = url;
this.loadClient();
}
loadClient() {
try {
const projectRoot = __projectRoot;
const resolveProtoPath = () => {
const fromEnv = process.env.DRAWTHINGS_GRPC_PROTO;
if (fromEnv && fs_1.default.existsSync(fromEnv))
return fromEnv;
const candidates = [
path_1.default.resolve(projectRoot, "dist", "interfaces", "imageService.proto"),
path_1.default.resolve(projectRoot, "src", "interfaces", "imageService.proto"),
path_1.default.resolve(projectRoot, "dist", "src", "interfaces", "imageService.proto"),
path_1.default.resolve(__dirname, "..", "interfaces", "imageService.proto"),
];
for (const p of candidates)
if (fs_1.default.existsSync(p))
return p;
return candidates[0];
};
const protoPath = resolveProtoPath();
const opts = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
};
const packageDef = protoLoader.loadSync(protoPath, opts);
const grpcObj = grpc.loadPackageDefinition(packageDef) || {};
const SvcCtor = grpcObj[this.serviceName];
if (!SvcCtor) {
console.error(`[DrawThingsGrpcService] Service not found: ${this.serviceName} (proto: ${protoPath})`);
logErr(`gRPC service not found: ${this.serviceName} (proto: ${protoPath})`);
try {
const available = Object.keys(grpcObj || {});
console.error(`[DrawThingsGrpcService] Available services: ${available.join(", ")}`);
logFile(`[gRPC] Available services: ${available.join(", ")}`);
}
catch { }
this.client = null;
return;
}
const channelOptions = {
"grpc.max_receive_message_length": 64 * 1024 * 1024,
"grpc.max_send_message_length": 64 * 1024 * 1024,
"grpc.ssl_target_name_override": "localhost",
"grpc.default_authority": "localhost",
};
const loadRootCAs = () => {
const tlsDirs = [
path_1.default.resolve(projectRoot, "src", "interfaces", "tls"),
path_1.default.resolve(projectRoot, "dist", "interfaces", "tls"),
path_1.default.resolve(__dirname, "..", "interfaces", "tls"),
];
const baseDir = tlsDirs.find((d) => {
try {
return fs_1.default.existsSync(d);
}
catch {
return false;
}
});
if (!baseDir)
return undefined;
const serverPath = path_1.default.join(baseDir, "server_crt.crt");
const rootPath = path_1.default.join(baseDir, "root_ca.crt");
if (!fs_1.default.existsSync(serverPath) || !fs_1.default.existsSync(rootPath))
return undefined;
const serverTxt = fs_1.default
.readFileSync(serverPath, "utf8")
.replace(/\r\n/g, "\n")
.trim();
const rootTxt = fs_1.default
.readFileSync(rootPath, "utf8")
.replace(/\r\n/g, "\n")
.trim();
if (!/-----BEGIN CERTIFICATE-----/.test(serverTxt) ||
!/-----BEGIN CERTIFICATE-----/.test(rootTxt))
return undefined;
const bundle = `${serverTxt}\n${rootTxt}\n`;
try {
const out = path_1.default.join(baseDir, "server.pem");
const prev = fs_1.default.existsSync(out)
? fs_1.default.readFileSync(out, "utf8").replace(/\r\n/g, "\n").trim()
: "";
if (prev !== bundle.trim()) {
fs_1.default.writeFileSync(out, bundle, "utf8");
console.log(`[TLS] Generated bundle: ${out}`);
}
else {
console.log(`[TLS] Using existing bundle: ${out}`);
}
}
catch { }
return Buffer.from(bundle, "utf8");
};
const creds = grpc.credentials.createSsl(loadRootCAs());
this.client = new SvcCtor(this.baseUrl, creds, channelOptions);
this.currentSecurity = "tls";
try {
globalThis.__DT_GRPC_TLS_MODE__ = this.tlsMode || "auto";
globalThis.__DT_GRPC_TLS_SELECTED__ = this.currentSecurity;
}
catch { }
try {
const proto = Object.getPrototypeOf(this.client) || {};
const methods = Object.getOwnPropertyNames(proto).filter((k) => typeof this.client[k] === "function" && k !== "constructor");
console.log(`[DrawThingsGrpcService] client methods: ${methods.join(", ")}`);
logFile(`[gRPC] client methods: ${methods.join(", ")}`);
}
catch { }
}
catch (e) {
console.error(`[DrawThingsGrpcService] loadClient error: ${e instanceof Error ? e.message : String(e)}`);
logErr(`loadClient error: ${e instanceof Error ? e.message : String(e)}`);
this.client = null;
}
}
async checkApiConnection() {
if (!this.client)
return false;
// Try Echo first (surfaces sharedSecret requirement)
try {
const echo = this.client["echo"]?.bind(this.client);
if (typeof echo === "function") {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const req = { name: "generate-image-plugin" };
if (sharedSecret)
req.sharedSecret = sharedSecret;
await new Promise((resolve, reject) => {
echo(req, (err, _resp) => {
if (err)
return reject(err);
resolve();
});
});
return true;
}
}
catch (e) {
try {
const msg = e && e.message ? String(e.message) : String(e);
const code = e?.code;
console.error(`[gRPC check] echo failed (mode=${this.currentSecurity}) code=${code ?? "?"} msg=${msg}`);
logErr(`gRPC echo failed (mode=${this.currentSecurity}) code=${code ?? "?"} msg=${msg}`);
}
catch { }
// If auto mode, attempt opposite security on first failure
if (this.tlsMode === "auto") {
// Toggle mode by rebuilding client quickly with TLS and reasonable authority candidates
try {
const __dirname = __svcDir;
const projectRoot = __resolveProjectRootFrom(__dirname);
const protoPath = (() => {
const fromEnv = process.env.DRAWTHINGS_GRPC_PROTO;
if (fromEnv && fs_1.default.existsSync(fromEnv))
return fromEnv;
const c1 = path_1.default.resolve(projectRoot, "src", "interfaces", "imageService.proto");
if (fs_1.default.existsSync(c1))
return c1;
const c3 = path_1.default.resolve(projectRoot, "dist", "interfaces", "imageService.proto");
if (fs_1.default.existsSync(c3))
return c3;
// no dist/src fallback in Rollup build
return c1;
})();
const packageDef = protoLoader.loadSync(protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const grpcObj = grpc.loadPackageDefinition(packageDef) || {};
const SvcCtor = grpcObj[this.serviceName];
if (SvcCtor) {
const root = (() => {
const pieces = [];
const tlsDirCandidates = [
path_1.default.resolve(projectRoot, "src", "interfaces", "tls"),
path_1.default.resolve(projectRoot, "dist", "interfaces", "tls"),
];
const names = [
"ca.crt",
"ca.pem",
"server.crt",
"server.pem",
"rootCA.crt",
"rootCA.pem",
];
for (const d of tlsDirCandidates) {
for (const n of names) {
try {
const p = path_1.default.join(d, n);
if (fs_1.default.existsSync(p))
pieces.push(fs_1.default.readFileSync(p));
}
catch { }
}
}
return pieces.length ? Buffer.concat(pieces) : undefined;
})();
const hostname = String(this.baseUrl || "").split(":")[0] || "localhost";
const candidateSet = new Set([
hostname,
"localhost",
"server",
"drawthings",
"draw-things",
]);
try {
const { X509Certificate } = await import("crypto");
const protoDirs = [
path_1.default.resolve(projectRoot, "src", "interfaces", "tls"),
path_1.default.resolve(projectRoot, "dist", "interfaces", "tls"),
];
const certFiles = [
"server_crt.crt",
"server_cert.crt",
"server.crt",
"server.pem",
"root_ca.crt",
"rootCA.crt",
"ca.crt",
"ca.pem",
];
for (const d of protoDirs) {
for (const f of certFiles) {
const p = path_1.default.join(d, f);
try {
if (!fs_1.default.existsSync(p))
continue;
const txt = fs_1.default.readFileSync(p, "utf8");
const blocks = txt
.split(/-----END CERTIFICATE-----/g)
.map((b) => b.includes("BEGIN CERTIFICATE")
? b + "-----END CERTIFICATE-----\n"
: "")
.filter((b) => b.trim().length > 0);
for (const pem of blocks) {
try {
// @ts-ignore
const x = new X509Certificate(pem);
const subj = x.subject || "";
const san = x.subjectAltName || "";
const m = subj.match(/CN=([^,\n]+)/);
if (m && m[1])
candidateSet.add(m[1].trim());
if (san) {
san
.split(/[,]/)
.map((s) => s.trim())
.filter((s) => /^DNS:/i.test(s))
.map((s) => s.replace(/^DNS:\s*/i, ""))
.forEach((dname) => candidateSet.add(dname));
}
}
catch { }
}
}
catch { }
}
}
}
catch { }
const authCandidates = Array.from(candidateSet);
// Try TLS with different authorities
for (const auth of authCandidates) {
const channelOptions = {
"grpc.max_receive_message_length": 64 * 1024 * 1024,
"grpc.max_send_message_length": 64 * 1024 * 1024,
"grpc.ssl_target_name_override": auth,
"grpc.default_authority": auth,
};
const creds = grpc.credentials.createSsl(root);
this.client = new SvcCtor(this.baseUrl, creds, channelOptions);
this.currentSecurity = "tls";
try {
globalThis.__DT_GRPC_TLS_SELECTED__ =
this.currentSecurity;
}
catch { }
try {
const echo2 = this.client["echo"]?.bind(this.client);
if (typeof echo2 === "function") {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const req = { name: "generate-image-plugin" };
if (sharedSecret)
req.sharedSecret = sharedSecret;
await new Promise((resolve2, reject2) => {
echo2(req, (err2) => err2 ? reject2(err2) : resolve2());
});
return true;
}
}
catch (e2) {
try {
const msg = e2 && e2.message
? String(e2.message)
: String(e2);
const code = e2?.code;
console.error(`[gRPC check] TLS echo failed (authority=${auth}) code=${code ?? "?"} msg=${msg}`);
}
catch { }
}
}
// As a last check, retry insecure (in case server was plaintext)
{
const channelOptions = {
"grpc.max_receive_message_length": 64 * 1024 * 1024,
"grpc.max_send_message_length": 64 * 1024 * 1024,
};
const creds = grpc.credentials.createInsecure();
this.client = new SvcCtor(this.baseUrl, creds, channelOptions);
this.currentSecurity = "insecure";
try {
globalThis.__DT_GRPC_TLS_SELECTED__ =
this.currentSecurity;
}
catch (e3) {
try {
const msg = e3 && e3.message
? String(e3.message)
: String(e3);
const code = e3?.code;
console.error(`[gRPC check] insecure echo retry failed code=${code ?? "?"} msg=${msg}`);
}
catch { }
}
try {
const echo3 = this.client["echo"]?.bind(this.client);
if (typeof echo3 === "function") {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const req = { name: "generate-image-plugin" };
if (sharedSecret)
req.sharedSecret = sharedSecret;
await new Promise((resolve3, reject3) => {
echo3(req, (err3) => err3 ? reject3(err3) : resolve3());
});
return true;
}
}
catch { }
}
}
}
catch { }
}
}
const deadline = new Date(Date.now() + 8000);
return new Promise((resolve) => {
try {
this.client.waitForReady(deadline, (err) => {
resolve(!err);
});
}
catch {
resolve(false);
}
});
}
ensureClient() {
if (!this.client)
throw new Error("gRPC client not configured. Set DRAWTHINGS_GRPC_* envs.");
}
toBackendError(e) {
let status = undefined;
let msg = "unknown error";
if (e && typeof e === "object") {
if (typeof e.code === "number")
status = e.code; // grpc status code
if (typeof e.details === "string")
msg = e.details;
else if (typeof e.message === "string")
msg = e.message;
}
return { isError: true, status, errorMessage: msg };
}
async generateImage(inputParams = {}, onProgress) {
try {
const __tmpDirs = [];
const __registerTmp = (d) => {
try {
__tmpDirs.push(d);
}
catch { }
};
const __cleanupTmp = () => {
for (const d of __tmpDirs) {
try {
fs_1.default.rmSync(d, { recursive: true, force: true });
}
catch { }
}
};
const isVideoTxt2Vid = inputParams?._dt_video_mode === "txt2vid";
const baseDefaults = isVideoTxt2Vid ? core_bundle_mjs_1.defaultParamsText2Video : core_bundle_mjs_1.defaultParams;
const baseOverlayMode = isVideoTxt2Vid ? "txt2vid" : "txt2img";
const audit = (0, core_bundle_mjs_1.buildAuditLogger)({ backend: this.name, mode: baseOverlayMode });
// NOTE: Service-level audit is kept minimal; full audit in core (tools.ts)
this.ensureClient();
// We'll build the FlatBuffer config AFTER canonical mapping so steps/size/variants land correctly
let configBytes = undefined;
// Capture effective fields from the final FlatBuffer view
let payloadWidth;
let payloadHeight;
let payloadSteps;
let payloadSeed;
let payloadSeedMode;
let payloadModel;
let payloadNumFrames = 1;
let payloadFps = 24;
let requiredModelFile;
let requiredLoraFiles;
// Capture overlay source for audit logging
let overlaySource = "default";
let overlayPreset;
let seedSource = "default";
let seedModeSource = "default";
let defaultsUsed;
let overlayLookupMode;
let strengthUsed;
let stepsUsed;
let samplerUsed;
let guidanceScaleUsed;
let shiftUsed;
let resolutionDependentShiftUsed;
let compressionArtifactsUsed;
let compressionArtifactsQualityUsed;
// validate + canonical mapping
let params = {};
try {
const r = (0, core_bundle_mjs_1.validateImageGenerationParams)(inputParams);
if (r.valid)
params = inputParams;
}
catch { }
if (params.random_string &&
(!params.prompt || Object.keys(params).length === 1)) {
params.prompt = params.random_string;
delete params.random_string;
}
if (!params.prompt)
params.prompt = inputParams.prompt || baseDefaults.prompt;
const promptOriginTxt = typeof inputParams?.prompt === "string" &&
String(inputParams.prompt).trim()
? "user"
: `default:drawthings:${baseOverlayMode}`;
let usedImageFormat = undefined;
let usedQuality = undefined;
try {
const fmt = inputParams.imageFormat;
// Shorthand should not override explicit dimensions (core passes requested_effective via width/height).
if (params.width == null && params.height == null) {
if (fmt === "square") {
params.width = 1024;
params.height = 1024;
usedImageFormat = "square";
}
else if (fmt === "landscape") {
params.width = 1024;
params.height = 768;
usedImageFormat = "landscape";
}
else if (fmt === "portrait") {
params.width = 768;
params.height = 1024;
usedImageFormat = "portrait";
}
else if (fmt === "16:9") {
params.width = 1024;
params.height = 576;
usedImageFormat = "16:9";
}
}
const qual = inputParams.quality;
if (qual === "low") {
params.steps = 4;
usedQuality = "low";
}
else if (qual === "medium") {
params.steps = 8;
usedQuality = "medium";
}
else if (qual === "high") {
params.steps = 12;
usedQuality = "high";
}
else {
usedQuality = "auto";
}
const variants = inputParams.variants;
if (typeof variants === "number") {
const v = Math.max(1, Math.min(4, Math.round(variants)));
params.batch_size = v;
params.batch_count = 1;
}
// Video: normalize numFrames to valid ×32+1 value
if (isVideoTxt2Vid) {
const rawNumFrames = inputParams.numFrames;
if (typeof rawNumFrames === "number" && rawNumFrames > 1) {
const normalized = normalizeNumFrames(rawNumFrames);
params.num_frames = normalized.value;
if (normalized.hint)
console.info(`[txt2vid] ${normalized.hint}`);
}
}
}
catch { }
// Build FlatBuffer configuration from effective params (defaults overlaid with mapped values)
try {
const allowedKeys = new Set(Object.keys(baseDefaults));
const filtered = {};
for (const [k, v] of Object.entries(params)) {
if (allowedKeys.has(k))
filtered[k] = v;
}
// Apply effective overlay (Custom Configs → Model Overlay → Defaults)
const modelId = params.model;
const { source, presetName, params: overlayParams, } = (0, customConfigsLoader_js_1.getEffectiveOverlay)(modelId, baseOverlayMode);
// Size must be controlled by tool/core (or defaults), not by overlays.
// Batch must be controlled by tool/core via variants parameter.
// Upscaler must be controlled by tool/core via _dt_needs_upscaler decision.
const overlayParamsNoSize = (() => {
if (!overlayParams)
return overlayParams;
const o = { ...overlayParams };
delete o.width;
delete o.height;
delete o.batch_count;
delete o.batch_size;
delete o.batchCount;
delete o.batchSize;
delete o.upscaler;
delete o.upscaler_scale;
delete o.upscalerScale;
delete o.fps;
return o;
})();
// Capture overlay info for result metadata
overlaySource = source;
overlayPreset = presetName;
// Tool-level preset 'auto' means: do not override engine model.
// LM Studio UI may send "auto" explicitly; never forward that literal to Draw Things.
if (modelId === "auto") {
delete filtered.model;
}
if (overlayParams) {
const msg = `Applying ${source} overlay for '${modelId ?? 'auto'}' (${baseOverlayMode}, gRPC)${presetName ? ` preset=${presetName}` : ""}`;
logFile(msg);
console.info(msg);
// Remove 'model' from filtered so overlay model filename wins
delete filtered.model;
}
// Log overlay source to audit
audit.setOutput({
overlay_source: source,
...(presetName && { overlay_preset: presetName }),
});
// Seed policy:
// - Tool interface does not accept seed/seed_mode (core uses strict minimal schema).
// - Custom Configs MAY set seed (including -1) and seed_mode and should be effective.
const effectiveSeed = source === "custom" &&
typeof overlayParamsNoSize?.seed === "number" &&
Number.isFinite(overlayParamsNoSize?.seed)
? overlayParamsNoSize.seed
: baseDefaults.seed;
const effectiveSeedMode = source === "custom" &&
typeof overlayParamsNoSize?.seed_mode === "string" &&
String(overlayParamsNoSize.seed_mode).trim()
? String(overlayParamsNoSize.seed_mode)
: baseDefaults.seed_mode;
seedSource =
source === "custom" &&
typeof overlayParamsNoSize?.seed === "number" &&
Number.isFinite(overlayParamsNoSize?.seed)
? "custom"
: "default";
seedModeSource =
source === "custom" &&
typeof overlayParamsNoSize?.seed_mode === "string" &&
String(overlayParamsNoSize.seed_mode).trim()
? "custom"
: "default";
const effective = {
...baseDefaults,
...(overlayParamsNoSize || {}),
...filtered,
seed: effectiveSeed,
seed_mode: effectiveSeedMode,
};
defaultsUsed = isVideoTxt2Vid ? "defaultParamsDrawThingsText2Video" : "defaultParamsDrawThingsTxt2Img";
overlayLookupMode = baseOverlayMode;
strengthUsed =
typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined;
stepsUsed =
typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined;
samplerUsed =
typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined;
guidanceScaleUsed =
typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined;
shiftUsed =
typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined;
resolutionDependentShiftUsed =
typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined;
compressionArtifactsUsed =
typeof effective.compressionArtifacts === "string" ? effective.compressionArtifacts
: typeof effective.compression_artifacts === "string" ? effective.compression_artifacts
: null;
compressionArtifactsQualityUsed =
typeof effective.compressionArtifactsQuality === "number"
? effective.compressionArtifactsQuality
: typeof effective.compression_artifacts_quality === "number"
? effective.compression_artifacts_quality
: null;
// Always log which defaults were used as the base, even when overlay_source=default.
// Also log key params like strength to disambiguate edit vs img2img behavior.
audit.setOutput({
overlay_lookup_mode: baseOverlayMode,
defaults_used: isVideoTxt2Vid ? "defaultParamsDrawThingsText2Video" : "defaultParamsDrawThingsTxt2Img",
strength_used: typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined,
steps_used: typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined,
sampler_used: typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined,
guidance_scale_used: typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined,
shift_used: typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined,
resolution_dependent_shift_used: typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined,
num_frames_used: typeof effective.num_frames === "number" ? effective.num_frames : null,
compression_artifacts_used: typeof effective.compressionArtifacts === "string" ? effective.compressionArtifacts
: typeof effective.compression_artifacts === "string" ? effective.compression_artifacts
: null,
compression_artifacts_quality_used: typeof effective.compressionArtifactsQuality === "number"
? effective.compressionArtifactsQuality
: typeof effective.compression_artifacts_quality === "number"
? effective.compression_artifacts_quality
: null,
});
// Asset preflight inputs
try {
if (typeof effective.model === "string" && effective.model.trim()) {
requiredModelFile = String(effective.model).trim();
}
// Extract LoRAs for audit: user > overlay > defaults
const userSpecifiedLoras = Object.prototype.hasOwnProperty.call(filtered, "loras");
const overlaySelected = !!overlayParams;
if (userSpecifiedLoras) {
const ls = Array.isArray(filtered.loras) ? filtered.loras : [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else if (overlaySelected) {
const ls = Array.isArray(overlayParams?.loras)
? overlayParams.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else {
// Fallback: extract LoRAs from effective params (includes defaults)
const ls = Array.isArray(effective?.loras)
? effective.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
}
catch { }
const { buildDtGenerationConfiguration } = await import("./drawThingsConfigMapper.js");
const cfg = buildDtGenerationConfiguration(effective);
configBytes = cfg.bytes && cfg.bytes.length ? cfg.bytes : undefined;
try {
payloadWidth = cfg?.view?.width;
payloadHeight = cfg?.view?.height;
payloadSteps = cfg?.view?.steps;
payloadSeed = cfg?.view?.seed;
payloadSeedMode = cfg?.view?.seed_mode;
payloadModel = String(effective.model || "");
payloadNumFrames = typeof effective.num_frames === "number" ? effective.num_frames : 1;
payloadFps = (typeof filtered.fps === "number" ? filtered.fps : undefined)
?? (0, core_bundle_mjs_1.getDefaultFpsForModel)(payloadModel)
?? (typeof effective.fps === "number" ? effective.fps : 24);
}
catch { }
try {
const view = cfg.view || {};
audit.setOutput({
flatbuf_num_frames: typeof view.num_frames === "number" ? view.num_frames : null,
});
console.info(`[grpc-config] fields=${Object.keys(view).join(",")}`);
}
catch { }
}
catch { }
// Preflight Echo to detect sharedSecret policy
try {
const echo = this.client["echo"]?.bind(this.client);
if (typeof echo === "function") {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const req = { name: "generate-image-plugin" };
if (sharedSecret)
req.sharedSecret = sharedSecret;
await new Promise((resolve, reject) => {
echo(req, (err, resp) => {
if (err)
return reject(err);
if (resp?.sharedSecretMissing && !sharedSecret)
return reject(new Error("gRPC requires sharedSecret. Set DRAWTHINGS_SHARED_SECRET."));
resolve();
});
});
}
}
catch (e) {
throw e;
}
// Validate requested model/LoRA presence on server to avoid silent fallback rendering.
try {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const chk = await (0, core_bundle_mjs_1.checkDrawThingsGrpcAssets)({
client: this.client,
sharedSecret: sharedSecret || undefined,
modelFile: requiredModelFile,
loraFiles: requiredLoraFiles,
});
if (!chk.ok) {
const overlayCtx = overlaySource && overlaySource !== "default"
? `overlay=${overlaySource}${overlayPreset ? ` preset=${overlayPreset}` : ""}`
: "";
logErr(`[asset-check] ${chk.details.split("\n")[0]}${overlayCtx ? ` (${overlayCtx})` : ""}`);
return {
isError: true,
status: 400,
errorMessage: overlayCtx
? `${overlayCtx}\n\n${chk.details}`
: chk.details,
};
}
}
catch (e) {
// If we cannot validate (RPC failure), do not block generation.
try {
logErr(`[asset-check] warning: ${e instanceof Error ? e.message : String(e)}`);
}
catch { }
}
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const request = {
prompt: String(params.prompt || baseDefaults.prompt),
negativePrompt: String(params.negative_prompt || ""),
scaleFactor: 1,
keywords: [],
user: "generate-image-plugin",
device: "LAPTOP",
chunked: true,
...(sharedSecret ? { sharedSecret } : {}),
};
if (configBytes)
request.configuration = configBytes;
// Optional: include FlatBuffer configuration from env (Base64)
const cfgB64 = process.env.DRAWTHINGS_CONFIG_B64;
if (cfgB64 && typeof cfgB64 === "string" && cfgB64.trim()) {
try {
request.configuration = Buffer.from(cfgB64.trim(), "base64");
}
catch { }
}
const call = this.client[this.methodGenerate]?.bind(this.client);
if (typeof call !== "function")
throw new Error(`gRPC method not found: ${this.methodGenerate}`);
const startTime = Date.now();
const imagesBuffers = [];
const audioBuffers = [];
let chunkAccum = null;
let previewBuf = null;
let lastSeenStep = 0;
const handleData = (resp) => {
try {
// progress logging + callback
if (resp?.currentSignpost?.sampling?.step) {
const currentStep = resp.currentSignpost.sampling.step;
lastSeenStep = currentStep;
console.info(`gRPC progress: sampling step=${currentStep}`);
logFile(`t2i progress: sampling step=${currentStep}`);
// Invoke progress callback if provided
if (onProgress) {
try {
onProgress(currentStep, payloadSteps, `Sampling step ${currentStep}${payloadSteps ? `/${payloadSteps}` : ""}`);
}
catch { }
}
}
else if (resp?.currentSignpost != null && onProgress) {
const sp = resp.currentSignpost;
let label;
if (lastSeenStep === 0) {
if (sp.textEncoded != null)
label = "Loading...";
else if (sp.imageEncoded != null)
label = "Processing...";
}
else {
label = "Finishing...";
}
if (label) {
try {
onProgress(-1, payloadSteps, label);
}
catch { }
}
}
if (resp?.generatedImages &&
Array.isArray(resp.generatedImages)) {
const list = resp.generatedImages.map((b) => Buffer.isBuffer(b) ? b : Buffer.from(b));
const cs = resp?.chunkState;
const isMore = cs === 1 || cs === "MORE_CHUNKS";
const isLast = cs === 0 || cs === "LAST_CHUNK";
if (isMore) {
if (list.length > 0) {
chunkAccum = chunkAccum
? Buffer.concat([chunkAccum, list[0]])
: Buffer.from(list[0]);
}
}
else if (isLast && list.length > 0) {
if (chunkAccum) {
const full = Buffer.concat([chunkAccum, list[0]]);
imagesBuffers.push(full);
chunkAccum = null;
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
if (resp?.previewImage) {
const b = resp.previewImage;
previewBuf = Buffer.isBuffer(b) ? b : Buffer.from(b);
}
if (resp?.generatedAudio && Array.isArray(resp.generatedAudio)) {
for (const a of resp.generatedAudio) {
const ab = Buffer.isBuffer(a) ? a : Buffer.from(a);
if (ab.length > 0)
audioBuffers.push(ab);
}
}
}
catch { }
};
await new Promise((resolve, reject) => {
try {
// Optional response compression negotiation (off by default; enable via env)
const accept = process.env.DRAWTHINGS_GRPC_ACCEPT_ENCODING;
const md = new grpc.Metadata();
if (accept && accept.trim()) {
md.add("grpc-accept-encoding", accept.trim());
}
const stream = md.get("grpc-accept-encoding").length > 0
? call(request, md)
: call(request);
stream.on("data", handleData);
stream.on("error", (err) => reject(err));
stream.on("end", () => resolve());
}
catch (e) {
reject(e);
}
});
const endTime = Date.now();
let primaryBuf;
if (imagesBuffers.length > 0)
primaryBuf = imagesBuffers[0];
else if (previewBuf)
primaryBuf = previewBuf;
if (!primaryBuf)
throw new Error("gRPC stream yielded no image data");
// Decode tensor bytes to PNG via external CLI (always attempt)
const decoderTools = resolveExternalToolCandidates(__projectRoot, "GRPCBin2PNG", process.env.DT_DECODER_CMD);
logToolCandidates("t2i decode", decoderTools);
const decodedBuffers = [];
if (decoderTools.length) {
// derive width/height hints for decoder from params/defaults
const finalW = Number(params.width || baseDefaults.width || 1024);
const finalH = Number(params.height || baseDefaults.height || 1024);
const decoderCtx = JSON.stringify({
width: finalW,
height: finalH,
channels: 3,
dtype: "f32",
});
for (let i = 0; i < imagesBuffers.length; i++) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-dt-"));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, `img-${i + 1}.bin`);
const outPath = path_1.default.join(tmpDir, `img-${i + 1}.png`);
fs_1.default.writeFileSync(inPath, imagesBuffers[i]);
const args = ["--in", inPath, "--out", outPath];
let decoded = null;
for (const tool of decoderTools) {
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, DT_DECODER_CTX: decoderCtx },
});
if (proc.error) {
logErr(`[decoder] exception via ${tool.display}: ${proc.error.message}`);
continue;
}
if (proc.status !== 0) {
logErr(`[decoder] exit=${proc.status} via ${tool.display} stderr=${proc.stderr?.toString?.() || ""}`);
continue;
}
if (fs_1.default.existsSync(outPath) && fs_1.default.statSync(outPath).size > 0) {
decoded = fs_1.default.readFileSync(outPath);
break;
}
logErr(`[decoder] no output via ${tool.display}; outPath missing/empty`);
}
if (decoded)
decodedBuffers.push(decoded);
else
decodedBuffers.push(imagesBuffers[i]);
}
catch {
// fall back: keep raw tensor bytes
decodedBuffers.push(imagesBuffers[i]);
logErr(`[decoder] exception decoding t2i img-${i + 1}.bin; kept raw`);
}
}
if (decodedBuffers.length > 0) {
logFile(`[decoder] t2i: decoded ${decodedBuffers.length} frame(s) → PNG via ${decoderTools[0].display}`);
}
}
let finalBuffers = decoderTools.length ? decodedBuffers : imagesBuffers;
// Fallback to preview when no generated images were provided
if ((!finalBuffers || finalBuffers.length === 0) && previewBuf) {
if (decoderTools.length) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-dt-"));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, `preview.bin`);
const outPath = path_1.default.join(tmpDir, `preview.png`);
fs_1.default.writeFileSync(inPath, previewBuf);
const finalW = Number(params.width || baseDefaults.width || 1024);
const finalH = Number(params.height || baseDefaults.height || 1024);
const decoderCtx = JSON.stringify({
width: finalW,
height: finalH,
channels: 3,
dtype: "f32",
});
const args = ["--in", inPath, "--out", outPath];
let decodedPrev = null;
for (const tool of decoderTools) {
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, DT_DECODER_CTX: decoderCtx },
});
if (proc.error)
continue;
if (proc.status !== 0)
continue;
if (fs_1.default.existsSync(outPath) && fs_1.default.statSync(outPath).size > 0) {
decodedPrev = fs_1.default.readFileSync(outPath);
logFile(`[decoder] decoded preview.bin → PNG (${decodedPrev.length} bytes) via ${tool.display}`);
break;
}
}
finalBuffers = decodedPrev ? [decodedPrev] : [previewBuf];
}
catch {
finalBuffers = [previewBuf];
}
}
else {
finalBuffers = [previewBuf];
}
}
const pngCandidate = finalBuffers[0];
// Service-level audit disabled (consolidated audit is logged in index.ts)
// Effective dims from payload (FlatBuffer view) when available
const effW = Number(payloadWidth ??
params.width ??
baseDefaults.width ??
1024);
const effH = Number(payloadHeight ??
params.height ??
baseDefaults.height ??
1024);
// Service-level audit disabled (consolidated audit is logged in index.ts)
const dataUrl = `data:image/png;base64,${pngCandidate.toString("base64")}`;
logFile(`t2i completed: variants=${finalBuffers.length}, chunks=${imagesBuffers.length}, bytes=${pngCandidate.length}`);
const result = {
isError: false,
imageBuffer: pngCandidate,
imageData: dataUrl,
images: finalBuffers.length
? finalBuffers.map((b) => `data:image/png;base64,${b.toString("base64")}`)
: undefined,
metadata: {
alt: `Image generated from prompt: ${request.prompt}`,
inference_time_ms: endTime - startTime,
model: payloadModel,
width: effW,
height: effH,
requested_dimensions: {
width: effW,
height: effH,
},
steps: Number.isFinite(Number(payloadSteps))
? Number(payloadSteps)
: undefined,
seed: typeof payloadSeed === "number" && Number.isFinite(payloadSeed)
? payloadSeed
: undefined,
seed_mode: typeof payloadSeedMode === "string" && payloadSeedMode.trim()
? payloadSeedMode
: undefined,
seed_source: seedSource,
seed_mode_source: seedModeSource,
prompt_used: String(request.prompt || ""),
prompt_origin: promptOriginTxt,
transport: "grpc",
stream_chunks: imagesBuffers.length,
num_frames: payloadNumFrames,
fps: payloadFps,
// Overlay source tracking for audit
overlay_source: overlaySource,
...(overlayPreset && { overlay_preset: overlayPreset }),
...(defaultsUsed && { defaults_used: defaultsUsed }),
...(overlayLookupMode && { overlay_lookup_mode: overlayLookupMode }),
...(typeof strengthUsed === "number" && { strength_used: strengthUsed }),
...(typeof stepsUsed === "number" && { steps_used: stepsUsed }),
...(samplerUsed && { sampler_used: samplerUsed }),
...(typeof guidanceScaleUsed === "number" && {
guidance_scale_used: guidanceScaleUsed,
}),
...(typeof shiftUsed === "number" && { shift_used: shiftUsed }),
...(typeof resolutionDependentShiftUsed === "boolean" && {
resolution_dependent_shift_used: resolutionDependentShiftUsed,
}),
...(compressionArtifactsUsed != null && {
compression_artifacts_used: compressionArtifactsUsed,
}),
...(compressionArtifactsQualityUsed != null && {
compression_artifacts_quality_used: compressionArtifactsQualityUsed,
}),
...(requiredLoraFiles &&
requiredLoraFiles.length > 0 && { loras_used: requiredLoraFiles }),
},
audioBuffers: audioBuffers.length > 0 ? audioBuffers : undefined,
};
__cleanupTmp();
return result;
}
catch (e) {
try {
// attempt temp cleanup on error as well
// (safe even if empty)
}
catch { }
return this.toBackendError(e);
}
}
async generateImageImg2Img(inputParams = {}, sourceBuffer, onProgress) {
try {
const __tmpDirs = [];
const __registerTmp = (d) => {
try {
__tmpDirs.push(d);
}
catch { }
};
const __cleanupTmp = () => {
for (const d of __tmpDirs) {
try {
fs_1.default.rmSync(d, { recursive: true, force: true });
}
catch { }
}
};
const isVideoImg2Vid = inputParams?._dt_video_mode === "img2vid";
const audit = (0, core_bundle_mjs_1.buildAuditLogger)({ backend: this.name, mode: isVideoImg2Vid ? "img2vid" : "img2img" });
// NOTE: Service-level audit is kept minimal; full audit in core (tools.ts)
this.ensureClient();
const i2iProfile = isVideoImg2Vid ? undefined : inputParams?._dt_i2i_profile;
const baseDefaults = isVideoImg2Vid
? core_bundle_mjs_1.defaultParamsImage2Video
: i2iProfile === "zoom" ? core_bundle_mjs_1.defaultParamsZoom
: i2iProfile === "edit" ? core_bundle_mjs_1.defaultParamsEdit : core_bundle_mjs_1.defaultParamsImg2Img;
const baseLimits = isVideoImg2Vid
? core_bundle_mjs_1.drawthingsLimits
: i2iProfile === "edit" ? core_bundle_mjs_1.drawthingsEditLimits : core_bundle_mjs_1.drawthingsLimits;
let params = {};
try {
const r = (0, core_bundle_mjs_1.validateImageGenerationParams)(inputParams);
if (r.valid)
params = inputParams;
}
catch { }
if (params.random_string &&
(!params.prompt || Object.keys(params).length === 1)) {
params.prompt = params.random_string;
delete params.random_string;
}
if (!params.prompt)
params.prompt = inputParams.prompt || baseDefaults.prompt;
const promptOriginI2I = typeof inputParams?.prompt === "string" &&
String(inputParams.prompt).trim()
? "user"
: `default:drawthings:${isVideoImg2Vid ? "img2vid" : "img2img"}`;
let usedImageFormat = undefined;
let usedQuality = undefined;
try {
const fmt = inputParams.imageFormat;
if (fmt === "square") {
params.width = 1024;
params.height = 1024;
usedImageFormat = "square";
}
else if (fmt === "landscape") {
params.width = 1024;
params.height = 768;
usedImageFormat = "landscape";
}
else if (fmt === "portrait") {
params.width = 768;
params.height = 1024;
usedImageFormat = "portrait";
}
else if (fmt === "16:9") {
params.width = 1024;
params.height = 576;
usedImageFormat = "16:9";
}
const qual = inputParams.quality;
if (qual === "low") {
params.steps = 4;
usedQuality = "low";
}
else if (qual === "medium") {
params.steps = 8;
usedQuality = "medium";
}
else if (qual === "high") {
params.steps = 12;
usedQuality = "high";
}
else {
usedQuality = "auto";
}
const variants = inputParams.variants;
if (typeof variants === "number") {
const v = Math.max(1, Math.min(4, Math.round(variants)));
params.batch_size = v;
params.batch_count = 1;
}
// Video: normalize numFrames to valid ×32+1 value
if (isVideoImg2Vid) {
const rawNumFrames = inputParams.numFrames;
if (typeof rawNumFrames === "number" && rawNumFrames > 1) {
const normalized = normalizeNumFrames(rawNumFrames);
params.num_frames = normalized.value;
if (normalized.hint)
console.info(`[img2vid] ${normalized.hint}`);
}
}
}
catch { }
// Ensure PNG, capture source size
let initPng = sourceBuffer;
let srcW;
let srcH;
try {
const meta = await (0, core_bundle_mjs_1.getSize)(sourceBuffer);
srcW = meta.width;
srcH = meta.height;
initPng = await (0, core_bundle_mjs_1.toPng)(sourceBuffer);
}
catch { }
// User-requested OUT size (if explicitly provided)
const userReqW = (() => {
const v = inputParams?.width;
return typeof v === "number" && Number.isFinite(v)
? Math.round(v)
: undefined;
})();
const userReqH = (() => {
const v = inputParams?.height;
return typeof v === "number" && Number.isFinite(v)
? Math.round(v)
: undefined;
})();
// If source dimensions are known, record them as ORIGINAL size.
// Do NOT override generation size (width/height/target_*) to source size.
// Generation size should follow the requested/effective size.
if (srcW && srcH) {
params.original_width = srcW;
params.original_height = srcH;
const hasW = typeof params.width === "number" &&
Number.isFinite(params.width);
const hasH = typeof params.height === "number" &&
Number.isFinite(params.height);
if (!hasW)
params.width = userReqW ?? srcW;
if (!hasH)
params.height = userReqH ?? srcH;
const hasTW = typeof params.target_width === "number" &&
Number.isFinite(params.target_width);
const hasTH = typeof params.target_height === "number" &&
Number.isFinite(params.target_height);
if (!hasTW)
params.target_width = params.width;
if (!hasTH)
params.target_height = params.height;
}
// Build full configuration from effective params (HTTP-equivalent)
let configBytes = undefined;
let payloadWidth;
let payloadHeight;
let payloadSteps;
let payloadSeed;
let payloadSeedMode;
let payloadModel;
let payloadNumFrames = 1;
let payloadFps = 24;
let requiredModelFile;
let requiredLoraFiles;
// Capture overlay source for audit logging
let overlaySource = "default";
let overlayPreset;
// Debug metadata (propagated to core audit via result.metadata)
let defaultsUsed;
let overlayLookupMode;
let i2iProfileUsed;
let strengthUsed;
let stepsUsed;
let samplerUsed;
let guidanceScaleUsed;
let shiftUsed;
let resolutionDependentShiftUsed;
let compressionArtifactsUsed;
let compressionArtifactsQualityUsed;
try {
const allowedKeys = new Set(Object.keys(baseDefaults));
const filtered = {};
for (const [k, v] of Object.entries(params)) {
if (allowedKeys.has(k))
filtered[k] = v;
}
// Apply effective overlay (Custom Configs → Model Overlay → Defaults)
// Use i2iProfile to determine correct mode for overlay lookup
const modelId = params.model;
const overlayMode = isVideoImg2Vid ? "img2vid" : i2iProfile === "edit" ? "edit" : "img2img";
const { source, presetName, params: overlayParams, } = (0, customConfigsLoader_js_1.getEffectiveOverlay)(modelId, overlayMode);
// Size must be controlled by tool/core (or defaults), not by overlays.
// Batch must be controlled by tool/core via variants parameter.
// Upscaler must be controlled by tool/core via _dt_needs_upscaler decision.
const overlayParamsNoSize = (() => {
if (!overlayParams)
return overlayParams;
const o = { ...overlayParams };
delete o.width;
delete o.height;
delete o.batch_count;
delete o.batch_size;
delete o.batchCount;
delete o.batchSize;
delete o.upscaler;
delete o.upscaler_scale;
delete o.upscalerScale;
delete o.fps;
return o;
})();
// Capture overlay info for result metadata
overlaySource = source;
overlayPreset = presetName;
// Tool-level preset 'auto' means: do not override engine model.
if (modelId === "auto") {
delete filtered.model;
}
if (overlayParams) {
const msg = `Applying ${source} overlay for '${modelId ?? 'auto'}' (${overlayMode}, gRPC)${presetName ? ` preset=${presetName}` : ""}`;
logFile(msg);
console.info(msg);
// Remove 'model' from filtered so overlay model filename wins
delete filtered.model;
}
// Log overlay source to audit
audit.setOutput({
overlay_source: source,
...(presetName && { overlay_preset: presetName }),
});
const effectiveSeed = source === "custom" &&
typeof overlayParamsNoSize?.seed === "number" &&
Number.isFinite(overlayParamsNoSize?.seed)
? overlayParamsNoSize.seed
: baseDefaults.seed;
const effective = {
...baseDefaults,
...(overlayParamsNoSize || {}),
...filtered,
seed: effectiveSeed,
};
defaultsUsed = isVideoImg2Vid
? "defaultParamsDrawThingsImage2Video"
: i2iProfile === "zoom"
? "defaultParamsZoom"
: i2iProfile === "edit"
? "defaultParamsDrawThingsEdit"
: "defaultParamsDrawThingsImg2Img";
overlayLookupMode = overlayMode;
i2iProfileUsed = isVideoImg2Vid ? undefined : i2iProfile;
strengthUsed =
typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined;
stepsUsed =
typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined;
samplerUsed =
typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined;
guidanceScaleUsed =
typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined;
shiftUsed =
typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined;
resolutionDependentShiftUsed =
typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined;
compressionArtifactsUsed =
typeof effective.compressionArtifacts === "string" ? effective.compressionArtifacts
: typeof effective.compression_artifacts === "string" ? effective.compression_artifacts
: null;
compressionArtifactsQualityUsed =
typeof effective.compressionArtifactsQuality === "number"
? effective.compressionArtifactsQuality
: typeof effective.compression_artifacts_quality === "number"
? effective.compression_artifacts_quality
: null;
{
const defaultsUsed = isVideoImg2Vid
? "defaultParamsDrawThingsImage2Video"
: i2iProfile === "zoom"
? "defaultParamsZoom"
: i2iProfile === "edit"
? "defaultParamsDrawThingsEdit"
: "defaultParamsDrawThingsImg2Img";
audit.setOutput({
overlay_lookup_mode: overlayMode,
defaults_used: defaultsUsed,
i2i_profile: i2iProfile,
strength_used: typeof effective.strength === "number" &&
Number.isFinite(effective.strength)
? effective.strength
: undefined,
steps_used: typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined,
sampler_used: typeof effective.sampler === "string" &&
String(effective.sampler).trim()
? String(effective.sampler)
: undefined,
guidance_scale_used: typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined,
shift_used: typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined,
resolution_dependent_shift_used: typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined,
num_frames_used: typeof effective.num_frames === "number" ? effective.num_frames : null,
compression_artifacts_used: typeof effective.compressionArtifacts === "string" ? effective.compressionArtifacts
: typeof effective.compression_artifacts === "string" ? effective.compression_artifacts
: null,
compression_artifacts_quality_used: typeof effective.compressionArtifactsQuality === "number"
? effective.compressionArtifactsQuality
: typeof effective.compression_artifacts_quality === "number"
? effective.compression_artifacts_quality
: null,
});
}
// Asset preflight inputs
try {
if (typeof effective.model === "string" && effective.model.trim()) {
requiredModelFile = String(effective.model).trim();
}
const userSpecifiedLoras = Object.prototype.hasOwnProperty.call(filtered, "loras");
const overlaySelected = !!overlayParams;
if (userSpecifiedLoras) {
const ls = Array.isArray(filtered.loras) ? filtered.loras : [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else if (overlaySelected) {
const ls = Array.isArray(overlayParams?.loras)
? overlayParams.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else {
// Fallback: extract LoRAs from effective params (includes defaults)
const ls = Array.isArray(effective?.loras)
? effective.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
}
catch { }
// Upscaler is controlled deterministically by core.
// No heuristics/fallbacks here to avoid legacy drift.
{
const needs = inputParams?._dt_needs_upscaler;
if (typeof needs !== "boolean") {
throw new Error(`Invariant failed: _dt_needs_upscaler must be provided by core for ${i2iProfile}`);
}
if (needs && baseLimits.upscaleMethod !== "zoom-pass") {
effective.upscaler = baseLimits.upscaler;
effective.upscaler_scale = baseLimits.upscalerScaleFactor;
console.info(`DT gRPC ${i2iProfile} upscaler enabled (core)`);
}
else {
effective.upscaler = null;
effective.upscaler_scale = 0;
console.info(`DT gRPC ${i2iProfile} upscaler disabled (core)`);
}
}
const { buildDtGenerationConfiguration } = await import("./drawThingsConfigMapper.js");
const cfg = buildDtGenerationConfiguration(effective);
configBytes = cfg.bytes && cfg.bytes.length ? cfg.bytes : undefined;
try {
const view = cfg?.view || {};
payloadWidth = view.width;
payloadHeight = view.height;
payloadSteps = view.steps;
payloadSeed = view.seed;
payloadSeedMode = view.seed_mode;
payloadModel = String(effective.model || "");
payloadNumFrames = typeof effective.num_frames === "number" ? effective.num_frames : 1;
payloadFps = (typeof filtered.fps === "number" ? filtered.fps : undefined)
?? (0, core_bundle_mjs_1.getDefaultFpsForModel)(payloadModel)
?? (typeof effective.fps === "number" ? effective.fps : 24);
}
catch { }
try {
const view = cfg.view || {};
audit.setOutput({
flatbuf_num_frames: typeof view.num_frames === "number" ? view.num_frames : null,
});
console.info(`[grpc-config] fields=${Object.keys(view).join(",")}`);
}
catch { }
}
catch { }
// Validate requested model/LoRA presence on server to avoid silent fallback rendering.
try {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const chk = await (0, core_bundle_mjs_1.checkDrawThingsGrpcAssets)({
client: this.client,
sharedSecret: sharedSecret || undefined,
modelFile: requiredModelFile,
loraFiles: requiredLoraFiles,
});
if (!chk.ok) {
logErr(`[asset-check] ${chk.details.split("\n")[0]}`);
__cleanupTmp();
return {
isError: true,
status: 400,
errorMessage: overlaySource && overlaySource !== "default"
? `overlay=${overlaySource}${overlayPreset ? ` preset=${overlayPreset}` : ""}\n\n${chk.details}`
: chk.details,
};
}
}
catch (e) {
try {
logErr(`[asset-check] warning: ${e instanceof Error ? e.message : String(e)}`);
}
catch { }
}
// Validate requested model/LoRA presence on server to avoid silent fallback rendering.
try {
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
const chk = await (0, core_bundle_mjs_1.checkDrawThingsGrpcAssets)({
client: this.client,
sharedSecret: sharedSecret || undefined,
modelFile: requiredModelFile,
loraFiles: requiredLoraFiles,
});
if (!chk.ok) {
logErr(`[asset-check] ${chk.details.split("\n")[0]}`);
return {
isError: true,
status: 400,
errorMessage: overlaySource && overlaySource !== "default"
? `overlay=${overlaySource}${overlayPreset ? ` preset=${overlayPreset}` : ""}\n\n${chk.details}`
: chk.details,
};
}
}
catch (e) {
try {
logErr(`[asset-check] warning: ${e instanceof Error ? e.message : String(e)}`);
}
catch { }
}
const sharedSecret2 = process.env.DRAWTHINGS_SHARED_SECRET;
const request = {
prompt: String(params.prompt || core_bundle_mjs_1.defaultParamsImg2Img.prompt),
negativePrompt: String(params.negative_prompt || ""),
scaleFactor: 1,
keywords: [],
user: "generate-image-plugin",
device: "LAPTOP",
chunked: true,
...(sharedSecret2 ? { sharedSecret: sharedSecret2 } : {}),
};
if (configBytes)
request.configuration = configBytes;
// Convert PNG → NNC tensor blob via PNG2GRPCBin (always attempt)
const projectRoot2 = __resolveProjectRootFrom(__svcDir);
const encoderTools = resolveExternalToolCandidates(projectRoot2, "PNG2GRPCBin", process.env.DT_ENCODER_CMD);
logToolCandidates("i2i encode", encoderTools);
if (encoderTools.length) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-enc-"));
__registerTmp(tmpDir);
const pngPath = path_1.default.join(tmpDir, "input.png");
const imgBinPath = path_1.default.join(tmpDir, "image.bin");
fs_1.default.writeFileSync(pngPath, initPng);
const args = ["--in", pngPath, "--out", imgBinPath];
let lastStdout = "";
let lastStderr = "";
let blob = null;
let toolUsed = null;
for (const tool of encoderTools) {
try {
try {
if (fs_1.default.existsSync(imgBinPath))
fs_1.default.unlinkSync(imgBinPath);
}
catch { }
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
});
if (proc.error) {
lastStderr = proc.error.message;
logErr(`[i2i-encode] exception via ${tool.display}: ${lastStderr}`);
continue;
}
lastStdout = (proc.stdout || Buffer.alloc(0))
.toString("utf8")
.trim();
lastStderr = (proc.stderr || Buffer.alloc(0))
.toString("utf8")
.trim();
if (typeof proc.status === "number" && proc.status !== 0) {
logErr(`[i2i-encode] exit=${proc.status} via ${tool.display} stderr=${lastStderr}`);
continue;
}
if (fs_1.default.existsSync(imgBinPath) &&
fs_1.default.statSync(imgBinPath).size > 0) {
blob = fs_1.default.readFileSync(imgBinPath);
toolUsed = tool;
break;
}
logErr(`[i2i-encode] no output via ${tool.display}; image.bin missing/empty stdout=${lastStdout}`);
}
catch (e) {
logErr(`[i2i-encode] exception via ${tool.display}: ${e instanceof Error ? e.message : String(e)}`);
continue;
}
}
if (blob && toolUsed) {
// Prefer stdout hex; fallback to local SHA-256
let imgHashBuf = null;
if (/^[0-9a-fA-F]{64}$/.test(lastStdout)) {
imgHashBuf = Buffer.from(lastStdout, "hex");
}
else {
const h = crypto_1.default.createHash("sha256").update(blob).digest();
imgHashBuf = h;
}
const hashHex = imgHashBuf.toString("hex");
logFile(`[encoder] encoded input.png → image.bin (${blob.length} bytes) via ${toolUsed.display}`);
// CAS preflight: FilesExist + optional UploadFile (single chunk)
try {
const filesExist = this.client["filesExist"]?.bind(this.client);
if (typeof filesExist === "function") {
await new Promise((resolve) => {
filesExist({
files: [],
filesWithHash: [hashHex],
...(sharedSecret2 ? { sharedSecret: sharedSecret2 } : {}),
}, (err, resp) => {
if (err) {
logErr(`FilesExist error: ${err?.message || String(err)}`);
return resolve();
}
try {
const existArr = Array.isArray(resp?.existences)
? resp.existences
: [];
const exists = existArr[0] === true;
logFile(`[CAS] FilesExist hash=${hashHex} exists=${exists}`);
if (!exists) {
try {
const upload = this.client["uploadFile"]?.call(this.client);
if (upload && typeof upload.write === "function") {
const fname = `image-${hashHex}.bin`;
upload.write({
initRequest: {
filename: fname,
sha256: imgHashBuf,
totalSize: blob.length,
},
...(sharedSecret2
? { sharedSecret: sharedSecret2 }
: {}),
});
upload.write({
chunk: {
content: blob,
filename: fname,
offset: 0,
},
...(sharedSecret2
? { sharedSecret: sharedSecret2 }
: {}),
});
upload.end();
upload.on("end", () => logFile(`[CAS] UploadFile completed hash=${hashHex} size=${blob.length}`));
upload.on("error", (ue) => logErr(`[CAS] UploadFile error: ${ue?.message || String(ue)}`));
}
}
catch (ue) {
logErr(`[CAS] UploadFile exception: ${ue?.message || String(ue)}`);
}
}
}
catch { }
resolve();
});
});
}
}
catch { }
request.image = imgHashBuf;
request.contents = [blob];
logFile(`[i2i-encode] built image.bin (${blob.length} bytes) hash=${imgHashBuf.toString("hex")}`);
// Mask omitted intentionally
// Cleanup
try {
fs_1.default.unlinkSync(pngPath);
}
catch { }
try {
fs_1.default.unlinkSync(imgBinPath);
}
catch { }
// no mask cleanup
}
else {
console.error(`[i2i-encode] encoder did not produce image.bin; stdout=${lastStdout}`);
logErr(`[i2i-encode] encoder did not produce image.bin; stdout=${lastStdout} stderr=${lastStderr}`);
}
}
catch (e) {
console.error(`[i2i-encode] encoder failed: ${e instanceof Error ? e.message : String(e)}`);
logErr(`[i2i-encode] encoder failed: ${e instanceof Error ? e.message : String(e)}`);
}
}
// Fallback when encoder not available or failed: inline PNG (legacy behavior)
if (!request.image ||
!request.contents ||
!Array.isArray(request.contents) ||
request.contents.length === 0) {
request.contents = [Buffer.from(initPng)];
}
const call = this.client[this.methodGenerate]?.bind(this.client);
if (typeof call !== "function")
throw new Error(`gRPC method not found: ${this.methodGenerate}`);
const startTime = Date.now();
const imagesBuffers = [];
const audioBuffers = [];
let chunkAccum = null;
let previewBuf = null;
let lastSeenStep = 0;
const handleData = (resp) => {
try {
if (resp?.currentSignpost?.sampling?.step) {
const currentStep = resp.currentSignpost.sampling.step;
lastSeenStep = currentStep;
console.info(`gRPC progress: sampling step=${currentStep}`);
logFile(`i2i progress: sampling step=${currentStep}`);
// Invoke progress callback if provided
if (onProgress) {
try {
onProgress(currentStep, payloadSteps, `Sampling step ${currentStep}${payloadSteps ? `/${payloadSteps}` : ""}`);
}
catch { }
}
}
else if (resp?.currentSignpost != null && onProgress) {
const sp = resp.currentSignpost;
let label;
if (lastSeenStep === 0) {
if (sp.textEncoded != null)
label = "Loading...";
else if (sp.imageEncoded != null)
label = "Processing...";
}
else {
label = "Finishing...";
}
if (label) {
try {
onProgress(-1, payloadSteps, label);
}
catch { }
}
}
if (resp?.generatedImages &&
Array.isArray(resp.generatedImages)) {
const list = resp.generatedImages.map((b) => Buffer.isBuffer(b) ? b : Buffer.from(b));
const cs = resp?.chunkState;
const isMore = cs === 1 || cs === "MORE_CHUNKS";
const isLast = cs === 0 || cs === "LAST_CHUNK";
if (isMore) {
if (list.length > 0) {
chunkAccum = chunkAccum
? Buffer.concat([chunkAccum, list[0]])
: Buffer.from(list[0]);
}
}
else if (isLast && list.length > 0) {
if (chunkAccum) {
const full = Buffer.concat([chunkAccum, list[0]]);
imagesBuffers.push(full);
chunkAccum = null;
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
if (resp?.previewImage) {
const b = resp.previewImage;
previewBuf = Buffer.isBuffer(b) ? b : Buffer.from(b);
}
if (resp?.generatedAudio && Array.isArray(resp.generatedAudio)) {
for (const a of resp.generatedAudio) {
const ab = Buffer.isBuffer(a) ? a : Buffer.from(a);
if (ab.length > 0)
audioBuffers.push(ab);
}
}
}
catch { }
};
await new Promise((resolve, reject) => {
try {
const stream = call(request);
stream.on("data", handleData);
stream.on("error", (err) => reject(err));
stream.on("end", () => resolve());
}
catch (e) {
reject(e);
}
});
const endTime = Date.now();
// Always attempt to decode tensor bytes to PNG via external CLI (same as txt2img)
const decoderTools2 = resolveExternalToolCandidates(__projectRoot, "GRPCBin2PNG", process.env.DT_DECODER_CMD);
logToolCandidates("i2i decode", decoderTools2);
if (decoderTools2.length && imagesBuffers.length > 0) {
const finalW = Number(params.width || core_bundle_mjs_1.defaultParamsImg2Img.width || 1024);
const finalH = Number(params.height || core_bundle_mjs_1.defaultParamsImg2Img.height || 1024);
const decoderCtx = JSON.stringify({
width: finalW,
height: finalH,
channels: 3,
dtype: "f32",
});
const decoded = [];
for (let i = 0; i < imagesBuffers.length; i++) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-dt-"));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, `img2img-${i + 1}.bin`);
const outPath = path_1.default.join(tmpDir, `img2img-${i + 1}.png`);
fs_1.default.writeFileSync(inPath, imagesBuffers[i]);
const args = ["--in", inPath, "--out", outPath];
let decodedOne = null;
for (const tool of decoderTools2) {
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, DT_DECODER_CTX: decoderCtx },
});
if (proc.error) {
logErr(`[decoder] exception via ${tool.display}: ${proc.error.message}`);
continue;
}
if (proc.status !== 0) {
logErr(`[decoder] exit=${proc.status} via ${tool.display} stderr=${proc.stderr?.toString?.() || ""}`);
continue;
}
if (fs_1.default.existsSync(outPath) && fs_1.default.statSync(outPath).size > 0) {
decodedOne = fs_1.default.readFileSync(outPath);
break;
}
}
if (decodedOne) {
decoded.push(decodedOne);
try {
fs_1.default.unlinkSync(inPath);
}
catch { }
}
else {
decoded.push(imagesBuffers[i]);
}
}
catch {
decoded.push(imagesBuffers[i]);
}
}
if (decoded.length > 0) {
imagesBuffers.length = 0;
imagesBuffers.push(...decoded);
logFile(`[decoder] i2i: decoded ${decoded.length} frame(s) → PNG via ${decoderTools2[0].display}`);
}
}
let primaryBuf;
if (imagesBuffers.length > 0)
primaryBuf = imagesBuffers[0];
else if (previewBuf) {
// Try decoding preview as well when needed
if (decoderTools2.length && previewBuf) {
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "generate-image-plugin-dt-"));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, `preview-i2i.bin`);
const outPath = path_1.default.join(tmpDir, `preview-i2i.png`);
fs_1.default.writeFileSync(inPath, previewBuf);
const finalW = Number(params.width || core_bundle_mjs_1.defaultParamsImg2Img.width || 1024);
const finalH = Number(params.height || core_bundle_mjs_1.defaultParamsImg2Img.height || 1024);
const decoderCtx = JSON.stringify({
width: finalW,
height: finalH,
channels: 3,
dtype: "f32",
});
const args = ["--in", inPath, "--out", outPath];
let decodedPrev = null;
for (const tool of decoderTools2) {
const proc = (0, child_process_1.spawnSync)(tool.cmd, [...tool.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, DT_DECODER_CTX: decoderCtx },
});
if (proc.error)
continue;
if (proc.status !== 0)
continue;
if (fs_1.default.existsSync(outPath) && fs_1.default.statSync(outPath).size > 0) {
decodedPrev = fs_1.default.readFileSync(outPath);
logFile(`[decoder] decoded preview-i2i.bin → PNG (${decodedPrev.length} bytes) via ${tool.display}`);
break;
}
}
primaryBuf = decodedPrev ? decodedPrev : previewBuf;
try {
fs_1.default.unlinkSync(inPath);
}
catch { }
}
catch {
primaryBuf = previewBuf;
}
}
else {
primaryBuf = previewBuf;
}
}
if (!primaryBuf)
throw new Error("gRPC stream yielded no image data");
// Service-level audit disabled (consolidated audit is logged in index.ts)
// Service-level audit disabled (consolidated audit is logged in index.ts)
const dataUrl = `data:image/png;base64,${primaryBuf.toString("base64")}`;
logFile(`i2i completed: images=${imagesBuffers.length}, bytes=${primaryBuf.length}`);
// Effective dims from payload (FlatBuffer view) when available; else fall back
const effW = Number(payloadWidth ??
params.width ??
core_bundle_mjs_1.defaultParamsImg2Img.width ??
1024);
const effH = Number(payloadHeight ??
params.height ??
core_bundle_mjs_1.defaultParamsImg2Img.height ??
1024);
const result = {
isError: false,
imageBuffer: primaryBuf,
imageData: dataUrl,
images: imagesBuffers.length
? imagesBuffers.map((b) => `data:image/png;base64,${b.toString("base64")}`)
: undefined,
metadata: {
alt: `Image-to-image from prompt: ${request.prompt}`,
inference_time_ms: endTime - startTime,
model: payloadModel,
width: effW,
height: effH,
requested_dimensions: {
width: effW,
height: effH,
},
steps: Number.isFinite(Number(payloadSteps))
? Number(payloadSteps)
: undefined,
seed: typeof payloadSeed === "number" && Number.isFinite(payloadSeed)
? payloadSeed
: undefined,
seed_mode: typeof payloadSeedMode === "string" && payloadSeedMode.trim()
? payloadSeedMode
: undefined,
prompt_used: String(request.prompt || ""),
prompt_origin: promptOriginI2I,
transport: "grpc",
stream_chunks: imagesBuffers.length,
num_frames: payloadNumFrames,
fps: payloadFps,
// Overlay source tracking for audit
overlay_source: overlaySource,
...(overlayPreset && { overlay_preset: overlayPreset }),
...(defaultsUsed && { defaults_used: defaultsUsed }),
...(overlayLookupMode && { overlay_lookup_mode: overlayLookupMode }),
...(i2iProfileUsed && { i2i_profile: i2iProfileUsed }),
...(typeof strengthUsed === "number" && { strength_used: strengthUsed }),
...(typeof stepsUsed === "number" && { steps_used: stepsUsed }),
...(samplerUsed && { sampler_used: samplerUsed }),
...(typeof guidanceScaleUsed === "number" && {
guidance_scale_used: guidanceScaleUsed,
}),
...(typeof shiftUsed === "number" && { shift_used: shiftUsed }),
...(typeof resolutionDependentShiftUsed === "boolean" && {
resolution_dependent_shift_used: resolutionDependentShiftUsed,
}),
...(compressionArtifactsUsed != null && {
compression_artifacts_used: compressionArtifactsUsed,
}),
...(compressionArtifactsQualityUsed != null && {
compression_artifacts_quality_used: compressionArtifactsQualityUsed,
}),
...(requiredLoraFiles &&
requiredLoraFiles.length > 0 && { loras_used: requiredLoraFiles }),
},
audioBuffers: audioBuffers.length > 0 ? audioBuffers : undefined,
};
__cleanupTmp();
return result;
}
catch (e) {
try {
// attempt temp cleanup on error as well
}
catch { }
return this.toBackendError(e);
}
}
/**
* Edit mode generation with multiple reference images.
* Canvas (first buffer) gets priority; remaining are moodboard.
*/
async generateImageEdit(inputParams = {}, referenceBuffers, onProgress) {
const rawProfile = inputParams?._dt_i2i_profile;
const i2iProfileUsed = rawProfile === "img2img" ? "img2img" : "edit";
// Multi-reference calls can be used for BOTH user modes:
// - mode='edit' => use edit defaults/overlays
// - mode='image2image' (with moodboard via gRPC) => use img2img defaults/overlays
const overlayMode = i2iProfileUsed;
// For MVP: delegate to generateImageImg2Img with edit profile
// Multi-reference will use contents[] array
if (!referenceBuffers || referenceBuffers.length === 0) {
return {
isError: true,
errorMessage: "Edit mode requires at least one reference image",
};
}
// Refactor-proofing: multi-reference is a shared entry point (edit + image2image).
// If core ever forgets to pass the profile, we default to edit but emit a loud warning.
if (referenceBuffers.length > 1 && rawProfile !== "img2img" && rawProfile !== "edit") {
try {
logFile("[multi-ref] WARNING: _dt_i2i_profile missing; defaulting to edit profile. This indicates a core/service interface regression.");
}
catch { }
}
// For single-reference edit, use existing img2img path with edit profile
if (referenceBuffers.length === 1) {
const editParams = { ...inputParams, _dt_i2i_profile: "edit" };
return this.generateImageImg2Img(editParams, referenceBuffers[0], onProgress);
}
// Multi-reference edit: encode all buffers and send via contents[]
try {
const __tmpDirs = [];
const __registerTmp = (d) => {
try {
__tmpDirs.push(d);
}
catch { }
};
const __cleanupTmp = () => {
for (const d of __tmpDirs) {
try {
fs_1.default.rmSync(d, { recursive: true, force: true });
}
catch { }
}
};
const audit = (0, core_bundle_mjs_1.buildAuditLogger)({
backend: this.name,
mode: overlayMode === "img2img" ? "img2img" : "edit",
});
// NOTE: Service-level audit is kept minimal; full audit in core (tools.ts)
this.ensureClient();
// Overlay tracking for result metadata
let overlaySource = "default";
let overlayPreset;
// Debug metadata (propagated to core audit via result.metadata)
let defaultsUsed;
let overlayLookupMode;
let strengthUsed;
let stepsUsed;
let samplerUsed;
let guidanceScaleUsed;
let shiftUsed;
let resolutionDependentShiftUsed;
// LoRA tracking
let requiredLoraFiles;
const baseDefaults = overlayMode === "edit" ? core_bundle_mjs_1.defaultParamsEdit : core_bundle_mjs_1.defaultParamsImg2Img;
const baseLimits = overlayMode === "edit" ? core_bundle_mjs_1.drawthingsEditLimits : core_bundle_mjs_1.drawthingsLimits;
let params = {};
try {
const r = (0, core_bundle_mjs_1.validateImageGenerationParams)(inputParams);
if (r.valid)
params = inputParams;
}
catch { }
if (!params.prompt)
params.prompt = inputParams.prompt || baseDefaults.prompt;
// Map variants parameter to batch_size for multi-variant generation
try {
const variants = inputParams.variants;
if (typeof variants === "number") {
const v = Math.max(1, Math.min(4, Math.round(variants)));
params.batch_size = v;
params.batch_count = 1;
}
}
catch { }
// Encode all reference images to NNC tensors
const resolveEncoderCmd = () => {
const envCmd = process.env.DT_ENCODER_CMD;
const projectRoot2 = __resolveProjectRootFrom(__svcDir);
const candidates = buildTensorHelperCandidates(projectRoot2, "PNG2GRPCBin", envCmd);
for (const c of candidates) {
try {
const abs = path_1.default.isAbsolute(c) ? c : path_1.default.resolve(projectRoot2, c);
if (!fs_1.default.existsSync(abs))
continue;
const origin = c === envCmd ? "env" : "auto";
if (abs.endsWith(".js")) {
return {
cmd: process.execPath,
argsPrefix: [abs],
display: abs,
impl: "ts",
origin,
};
}
try {
const st = fs_1.default.statSync(abs);
if ((st.mode & 0o111) === 0) {
try {
fs_1.default.chmodSync(abs, 0o755);
}
catch { }
}
}
catch { }
return {
cmd: abs,
argsPrefix: [],
display: abs,
impl: "mac-bin",
origin,
};
}
catch { }
}
return null;
};
const encoderCmd = resolveEncoderCmd();
logToolSelection("edit encode", encoderCmd);
if (!encoderCmd) {
return {
isError: true,
errorMessage: "PNG2GRPCBin encoder not found for multi-reference edit",
};
}
const contentsTensors = [];
let primaryHash = null;
const moodboardHashes = []; // SHA256 hashes for shuffle hints
let canvasW;
let canvasH;
for (let i = 0; i < referenceBuffers.length; i++) {
const refBuf = referenceBuffers[i];
const pngBuf = await (0, core_bundle_mjs_1.toPng)(refBuf);
if (i === 0) {
try {
const meta = await (0, core_bundle_mjs_1.getSize)(pngBuf);
canvasW = meta.width;
canvasH = meta.height;
}
catch { }
}
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `generate-image-plugin-edit-${i}-`));
__registerTmp(tmpDir);
const pngPath = path_1.default.join(tmpDir, "input.png");
const imgBinPath = path_1.default.join(tmpDir, "image.bin");
fs_1.default.writeFileSync(pngPath, pngBuf);
const args = ["--in", pngPath, "--out", imgBinPath];
const proc = (0, child_process_1.spawnSync)(encoderCmd.cmd, [...encoderCmd.argsPrefix, ...args], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
});
if (proc.error) {
__cleanupTmp();
return {
isError: true,
errorMessage: `Encoder failed for reference ${i + 1}: ${proc.error.message}`,
};
}
if (typeof proc.status === "number" && proc.status !== 0) {
const stdout = proc.stdout ? proc.stdout.toString("utf8") : "";
const stderr = proc.stderr ? proc.stderr.toString("utf8") : "";
logErr(`[edit] encoder exit=${proc.status} ref=${i + 1} cmd=${encoderCmd.cmd} ${[...encoderCmd.argsPrefix, ...args].join(" ")}`);
if (stdout.trim())
logFile(`[edit] encoder stdout ref=${i + 1}: ${stdout.trim()}`);
if (stderr.trim())
logErr(`[edit] encoder stderr ref=${i + 1}: ${stderr.trim()}`);
__cleanupTmp();
return {
isError: true,
errorMessage: `Encoder failed for reference ${i + 1} (exit ${proc.status})${stderr.trim() ? `: ${stderr.trim()}` : ""}`,
};
}
if (!fs_1.default.existsSync(imgBinPath) || fs_1.default.statSync(imgBinPath).size === 0) {
const stdout = proc.stdout ? proc.stdout.toString("utf8") : "";
const stderr = proc.stderr ? proc.stderr.toString("utf8") : "";
if (stdout.trim())
logFile(`[edit] encoder stdout ref=${i + 1}: ${stdout.trim()}`);
if (stderr.trim())
logErr(`[edit] encoder stderr ref=${i + 1}: ${stderr.trim()}`);
__cleanupTmp();
return {
isError: true,
errorMessage: `Encoder produced no output for reference ${i + 1}`,
};
}
const blob = fs_1.default.readFileSync(imgBinPath);
contentsTensors.push(blob);
// Compute SHA256 hash for each tensor
const tensorHash = crypto_1.default.createHash("sha256").update(blob).digest();
// First image is the canvas (primary image)
if (i === 0) {
primaryHash = tensorHash;
}
else {
// Moodboard images: collect hashes for shuffle hints
moodboardHashes.push(tensorHash);
}
// Cleanup temp files
try {
fs_1.default.unlinkSync(pngPath);
}
catch { }
try {
fs_1.default.unlinkSync(imgBinPath);
}
catch { }
}
logFile(`[${overlayMode === "img2img" ? "image2image" : "edit"}] encoded ${contentsTensors.length} reference images for multi-ref`);
// Match img2img behavior: internal processing size follows the actual canvas tensor size.
// This keeps edit and img2img identical except for moodboard hints.
if (canvasW && canvasH) {
params.original_width = canvasW;
params.original_height = canvasH;
const hasW = typeof params.width === "number" &&
Number.isFinite(params.width);
const hasH = typeof params.height === "number" &&
Number.isFinite(params.height);
if (!hasW)
params.width = canvasW;
if (!hasH)
params.height = canvasH;
const hasTW = typeof params.target_width === "number" &&
Number.isFinite(params.target_width);
const hasTH = typeof params.target_height === "number" &&
Number.isFinite(params.target_height);
if (!hasTW)
params.target_width = params.width;
if (!hasTH)
params.target_height = params.height;
}
// Build FlatBuffer config
let configBytes = undefined;
let payloadWidth;
let payloadHeight;
let payloadSteps;
let payloadSeed;
let payloadSeedMode;
let payloadModel;
let payloadNumFrames = 1;
let payloadFps = 24;
try {
const allowedKeys = new Set(Object.keys(baseDefaults));
const filtered = {};
for (const [k, v] of Object.entries(params)) {
if (allowedKeys.has(k))
filtered[k] = v;
}
const modelId = params.model;
const { source, presetName, params: overlayParams, } = (0, customConfigsLoader_js_1.getEffectiveOverlay)(modelId, overlayMode);
// Size must be controlled by tool/core (or defaults), not by overlays.
// Batch must be controlled by tool/core via variants parameter.
// Upscaler must be controlled by tool/core via _dt_needs_upscaler decision.
const overlayParamsNoSize = (() => {
if (!overlayParams)
return overlayParams;
const o = { ...overlayParams };
delete o.width;
delete o.height;
delete o.batch_count;
delete o.batch_size;
delete o.batchCount;
delete o.batchSize;
delete o.upscaler;
delete o.upscaler_scale;
delete o.upscalerScale;
delete o.fps;
return o;
})();
// Capture overlay info for result metadata
overlaySource = source;
overlayPreset = presetName;
if (modelId === "auto") {
delete filtered.model;
}
if (overlayParams) {
logFile(`Applying ${source} overlay for '${modelId ?? "auto"}' (${overlayMode === "img2img" ? "image2image" : "edit"}, gRPC)${presetName ? ` preset=${presetName}` : ""}`);
delete filtered.model;
}
// Log overlay source to audit
audit.setOutput({
overlay_source: source,
...(presetName && { overlay_preset: presetName }),
});
const effective = {
...baseDefaults,
...(overlayParamsNoSize || {}),
...filtered,
seed: baseDefaults.seed,
};
defaultsUsed =
overlayMode === "edit"
? "defaultParamsDrawThingsEdit"
: "defaultParamsDrawThingsImg2Img";
overlayLookupMode = overlayMode;
strengthUsed =
typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined;
stepsUsed =
typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined;
samplerUsed =
typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined;
guidanceScaleUsed =
typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined;
shiftUsed =
typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined;
resolutionDependentShiftUsed =
typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined;
audit.setOutput({
overlay_lookup_mode: overlayMode,
defaults_used: overlayMode === "edit"
? "defaultParamsDrawThingsEdit"
: "defaultParamsDrawThingsImg2Img",
strength_used: typeof effective.strength === "number" && Number.isFinite(effective.strength)
? effective.strength
: undefined,
steps_used: typeof effective.steps === "number" && Number.isFinite(effective.steps)
? effective.steps
: undefined,
sampler_used: typeof effective.sampler === "string" && String(effective.sampler).trim()
? String(effective.sampler)
: undefined,
guidance_scale_used: typeof effective.guidance_scale === "number" &&
Number.isFinite(effective.guidance_scale)
? effective.guidance_scale
: undefined,
shift_used: typeof effective.shift === "number" && Number.isFinite(effective.shift)
? effective.shift
: undefined,
resolution_dependent_shift_used: typeof effective.resolution_dependent_shift === "boolean"
? effective.resolution_dependent_shift
: undefined,
});
// Extract LoRAs for audit: user > overlay > defaults
try {
const userSpecifiedLoras = Object.prototype.hasOwnProperty.call(filtered, "loras");
const overlaySelected = !!overlayParams;
if (userSpecifiedLoras) {
const ls = Array.isArray(filtered.loras) ? filtered.loras : [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else if (overlaySelected) {
const ls = Array.isArray(overlayParams?.loras)
? overlayParams.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
else {
// Fallback: extract LoRAs from effective params (includes defaults)
const ls = Array.isArray(effective?.loras)
? effective.loras
: [];
requiredLoraFiles = ls
.map((x) => x?.file)
.filter((x) => typeof x === "string" && x.trim())
.map((x) => String(x).trim());
}
}
catch { }
const { buildDtGenerationConfiguration } = await import("./drawThingsConfigMapper.js");
const cfg = buildDtGenerationConfiguration(effective);
configBytes = cfg.bytes && cfg.bytes.length ? cfg.bytes : undefined;
try {
const view = cfg?.view || {};
payloadWidth = view.width;
payloadHeight = view.height;
payloadSteps = view.steps;
payloadSeed = view.seed;
payloadSeedMode = view.seed_mode;
payloadModel = String(effective.model || "");
payloadNumFrames = typeof effective.num_frames === "number" ? effective.num_frames : 1;
payloadFps = (typeof filtered.fps === "number" ? filtered.fps : undefined)
?? (0, core_bundle_mjs_1.getDefaultFpsForModel)(payloadModel)
?? (typeof effective.fps === "number" ? effective.fps : 24);
}
catch { }
}
catch { }
const sharedSecret = process.env.DRAWTHINGS_SHARED_SECRET;
// Build shuffle hints for moodboard images
// CRITICAL: Moodboard images must be referenced via hints with hintType="shuffle"
// See: moodboard_grpc_research.md for details
// Weight distribution: 1/n per image (matches Draw Things client behavior)
const moodboardWeight = moodboardHashes.length > 0 ? 1.0 / moodboardHashes.length : 1.0;
const shuffleHints = moodboardHashes.map((hash) => ({
tensor: hash, // SHA256 hash (32 bytes) referencing tensor in contents[]
weight: moodboardWeight,
}));
const request = {
prompt: String(params.prompt || baseDefaults.prompt),
negativePrompt: String(params.negative_prompt || ""),
scaleFactor: 1,
keywords: [],
user: "generate-image-plugin",
device: "LAPTOP",
chunked: true,
...(sharedSecret ? { sharedSecret } : {}),
image: primaryHash, // Hash of first (canvas) image
contents: contentsTensors, // All reference tensors (Content-Addressable Storage)
// Moodboard images as shuffle hints
...(shuffleHints.length > 0
? {
hints: [
{
hintType: "shuffle", // MUST be "shuffle" for moodboard/reference images
tensors: shuffleHints,
},
],
}
: {}),
};
if (configBytes)
request.configuration = configBytes;
logFile(`[edit] request built: canvas=1, moodboard=${shuffleHints.length}, total_contents=${contentsTensors.length}`);
const call = this.client[this.methodGenerate]?.bind(this.client);
if (typeof call !== "function")
throw new Error(`gRPC method not found: ${this.methodGenerate}`);
const startTime = Date.now();
const imagesBuffers = [];
const audioBuffers = [];
let chunkAccum = null;
let previewBuf = null;
let lastSeenStep = 0;
const handleData = (resp) => {
try {
if (resp?.currentSignpost?.sampling?.step) {
const currentStep = resp.currentSignpost.sampling.step;
lastSeenStep = currentStep;
console.info(`gRPC progress: sampling step=${currentStep}`);
logFile(`edit progress: sampling step=${currentStep}`);
// Invoke progress callback if provided
if (onProgress) {
try {
onProgress(currentStep, payloadSteps, `Sampling step ${currentStep}${payloadSteps ? `/${payloadSteps}` : ""}`);
}
catch { }
}
}
else if (resp?.currentSignpost != null && onProgress) {
const sp = resp.currentSignpost;
let label;
if (lastSeenStep === 0) {
if (sp.textEncoded != null)
label = "Loading...";
else if (sp.imageEncoded != null)
label = "Processing...";
}
else {
label = "Finishing...";
}
if (label) {
try {
onProgress(-1, payloadSteps, label);
}
catch { }
}
}
if (resp?.generatedImages &&
Array.isArray(resp.generatedImages)) {
const list = resp.generatedImages.map((b) => Buffer.isBuffer(b) ? b : Buffer.from(b));
const cs = resp?.chunkState;
const isMore = cs === 1 || cs === "MORE_CHUNKS";
const isLast = cs === 0 || cs === "LAST_CHUNK";
if (isMore) {
if (list.length > 0) {
chunkAccum = chunkAccum
? Buffer.concat([chunkAccum, list[0]])
: Buffer.from(list[0]);
}
}
else if (isLast && list.length > 0) {
if (chunkAccum) {
const full = Buffer.concat([chunkAccum, list[0]]);
imagesBuffers.push(full);
chunkAccum = null;
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
else {
for (const buf of list)
imagesBuffers.push(buf);
}
}
if (resp?.previewImage) {
const b = resp.previewImage;
previewBuf = Buffer.isBuffer(b) ? b : Buffer.from(b);
}
if (resp?.generatedAudio && Array.isArray(resp.generatedAudio)) {
for (const a of resp.generatedAudio) {
const ab = Buffer.isBuffer(a) ? a : Buffer.from(a);
if (ab.length > 0)
audioBuffers.push(ab);
}
}
}
catch { }
};
await new Promise((resolve, reject) => {
try {
const stream = call(request);
stream.on("data", handleData);
stream.on("error", (err) => reject(err));
stream.on("end", () => resolve());
}
catch (e) {
reject(e);
}
});
const endTime = Date.now();
// Decode tensor to PNG
const resolveDecoderCmd = () => {
const envCmd = process.env.DT_DECODER_CMD;
const projectRoot2 = __projectRoot;
const candidates = buildTensorHelperCandidates(projectRoot2, "GRPCBin2PNG", envCmd);
for (const c of candidates) {
try {
const abs = path_1.default.isAbsolute(c) ? c : path_1.default.resolve(projectRoot2, c);
if (!fs_1.default.existsSync(abs))
continue;
const origin = c === envCmd ? "env" : "auto";
if (abs.endsWith(".js")) {
return {
cmd: process.execPath,
argsPrefix: [abs],
display: abs,
impl: "ts",
origin,
};
}
try {
const st = fs_1.default.statSync(abs);
if ((st.mode & 0o111) === 0) {
try {
fs_1.default.chmodSync(abs, 0o755);
}
catch { }
}
}
catch { }
return {
cmd: abs,
argsPrefix: [],
display: abs,
impl: "mac-bin",
origin,
};
}
catch { }
}
return null;
};
const decoderCmd = resolveDecoderCmd();
logToolSelection("edit decode", decoderCmd);
let primaryBuf = null;
if (decoderCmd && imagesBuffers.length > 0) {
const pngBuffers = [];
for (let i = 0; i < imagesBuffers.length; i++) {
const rawBuf = imagesBuffers[i];
try {
const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `generate-image-plugin-dec-${i}-`));
__registerTmp(tmpDir);
const inPath = path_1.default.join(tmpDir, "input.bin");
const outPath = path_1.default.join(tmpDir, "output.png");
fs_1.default.writeFileSync(inPath, rawBuf);
const proc = (0, child_process_1.spawnSync)(decoderCmd.cmd, [...decoderCmd.argsPrefix, "--in", inPath, "--out", outPath], {
encoding: "buffer",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
});
if (!proc.error &&
fs_1.default.existsSync(outPath) &&
fs_1.default.statSync(outPath).size > 0) {
pngBuffers.push(fs_1.default.readFileSync(outPath));
}
}
catch { }
}
if (pngBuffers.length > 0) {
primaryBuf = pngBuffers[0];
imagesBuffers.length = 0;
for (const b of pngBuffers)
imagesBuffers.push(b);
}
}
if (!primaryBuf && previewBuf) {
primaryBuf = previewBuf;
}
if (!primaryBuf)
throw new Error("gRPC edit stream yielded no image data");
const dataUrl = `data:image/png;base64,${primaryBuf.toString("base64")}`;
logFile(`edit completed: refs=${referenceBuffers.length}, images=${imagesBuffers.length}, bytes=${primaryBuf.length}`);
const effW = Number(payloadWidth ?? params.width ?? 1024);
const effH = Number(payloadHeight ?? params.height ?? 1024);
const result = {
isError: false,
imageBuffer: primaryBuf,
imageData: dataUrl,
images: imagesBuffers.length
? imagesBuffers.map((b) => `data:image/png;base64,${b.toString("base64")}`)
: undefined,
metadata: {
alt: overlayMode === "img2img"
? `Image-to-image from prompt: ${request.prompt}`
: `Edit from prompt: ${request.prompt}`,
inference_time_ms: endTime - startTime,
model: payloadModel,
width: effW,
height: effH,
requested_dimensions: {
width: effW,
height: effH,
},
steps: Number.isFinite(Number(payloadSteps))
? Number(payloadSteps)
: undefined,
seed: typeof payloadSeed === "number" && Number.isFinite(payloadSeed)
? payloadSeed
: undefined,
seed_mode: typeof payloadSeedMode === "string" && payloadSeedMode.trim()
? payloadSeedMode
: undefined,
prompt_used: String(request.prompt || ""),
reference_count: referenceBuffers.length,
transport: "grpc",
stream_chunks: imagesBuffers.length,
num_frames: payloadNumFrames,
fps: payloadFps,
// Overlay source tracking for audit
overlay_source: overlaySource,
...(overlayPreset && { overlay_preset: overlayPreset }),
...(defaultsUsed && { defaults_used: defaultsUsed }),
...(overlayLookupMode && { overlay_lookup_mode: overlayLookupMode }),
i2i_profile: i2iProfileUsed,
...(typeof strengthUsed === "number" && { strength_used: strengthUsed }),
...(typeof stepsUsed === "number" && { steps_used: stepsUsed }),
...(samplerUsed && { sampler_used: samplerUsed }),
...(typeof guidanceScaleUsed === "number" && {
guidance_scale_used: guidanceScaleUsed,
}),
...(typeof shiftUsed === "number" && { shift_used: shiftUsed }),
...(typeof resolutionDependentShiftUsed === "boolean" && {
resolution_dependent_shift_used: resolutionDependentShiftUsed,
}),
...(requiredLoraFiles &&
requiredLoraFiles.length > 0 && { loras_used: requiredLoraFiles }),
},
audioBuffers: audioBuffers.length > 0 ? audioBuffers : undefined,
};
__cleanupTmp();
return result;
}
catch (e) {
return this.toBackendError(e);
}
}
}
exports.DrawThingsGrpcService = DrawThingsGrpcService;
exports.default = DrawThingsGrpcService;