genai-electron 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/config/defaults.d.ts +130 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +146 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/paths.d.ts +22 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +49 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/reasoning-models.d.ts +3 -0
- package/dist/config/reasoning-models.d.ts.map +1 -0
- package/dist/config/reasoning-models.js +10 -0
- package/dist/config/reasoning-models.js.map +1 -0
- package/dist/download/Downloader.d.ts +16 -0
- package/dist/download/Downloader.d.ts.map +1 -0
- package/dist/download/Downloader.js +102 -0
- package/dist/download/Downloader.js.map +1 -0
- package/dist/download/checksum.d.ts +4 -0
- package/dist/download/checksum.d.ts.map +1 -0
- package/dist/download/checksum.js +16 -0
- package/dist/download/checksum.js.map +1 -0
- package/dist/download/huggingface.d.ts +7 -0
- package/dist/download/huggingface.d.ts.map +1 -0
- package/dist/download/huggingface.js +40 -0
- package/dist/download/huggingface.js.map +1 -0
- package/dist/errors/index.d.ts +37 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +71 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/managers/BinaryManager.d.ts +25 -0
- package/dist/managers/BinaryManager.d.ts.map +1 -0
- package/dist/managers/BinaryManager.js +436 -0
- package/dist/managers/BinaryManager.js.map +1 -0
- package/dist/managers/DiffusionServerManager.d.ts +56 -0
- package/dist/managers/DiffusionServerManager.d.ts.map +1 -0
- package/dist/managers/DiffusionServerManager.js +649 -0
- package/dist/managers/DiffusionServerManager.js.map +1 -0
- package/dist/managers/GenerationRegistry.d.ts +21 -0
- package/dist/managers/GenerationRegistry.d.ts.map +1 -0
- package/dist/managers/GenerationRegistry.js +67 -0
- package/dist/managers/GenerationRegistry.js.map +1 -0
- package/dist/managers/LlamaServerManager.d.ts +23 -0
- package/dist/managers/LlamaServerManager.d.ts.map +1 -0
- package/dist/managers/LlamaServerManager.js +226 -0
- package/dist/managers/LlamaServerManager.js.map +1 -0
- package/dist/managers/ModelManager.d.ts +28 -0
- package/dist/managers/ModelManager.d.ts.map +1 -0
- package/dist/managers/ModelManager.js +356 -0
- package/dist/managers/ModelManager.js.map +1 -0
- package/dist/managers/ResourceOrchestrator.d.ts +28 -0
- package/dist/managers/ResourceOrchestrator.d.ts.map +1 -0
- package/dist/managers/ResourceOrchestrator.js +204 -0
- package/dist/managers/ResourceOrchestrator.js.map +1 -0
- package/dist/managers/ServerManager.d.ts +35 -0
- package/dist/managers/ServerManager.d.ts.map +1 -0
- package/dist/managers/ServerManager.js +171 -0
- package/dist/managers/ServerManager.js.map +1 -0
- package/dist/managers/StorageManager.d.ts +20 -0
- package/dist/managers/StorageManager.d.ts.map +1 -0
- package/dist/managers/StorageManager.js +159 -0
- package/dist/managers/StorageManager.js.map +1 -0
- package/dist/process/ProcessManager.d.ts +19 -0
- package/dist/process/ProcessManager.d.ts.map +1 -0
- package/dist/process/ProcessManager.js +112 -0
- package/dist/process/ProcessManager.js.map +1 -0
- package/dist/process/health-check.d.ts +9 -0
- package/dist/process/health-check.d.ts.map +1 -0
- package/dist/process/health-check.js +91 -0
- package/dist/process/health-check.js.map +1 -0
- package/dist/process/llama-log-parser.d.ts +5 -0
- package/dist/process/llama-log-parser.d.ts.map +1 -0
- package/dist/process/llama-log-parser.js +69 -0
- package/dist/process/llama-log-parser.js.map +1 -0
- package/dist/process/log-manager.d.ts +18 -0
- package/dist/process/log-manager.d.ts.map +1 -0
- package/dist/process/log-manager.js +73 -0
- package/dist/process/log-manager.js.map +1 -0
- package/dist/system/SystemInfo.d.ts +26 -0
- package/dist/system/SystemInfo.d.ts.map +1 -0
- package/dist/system/SystemInfo.js +162 -0
- package/dist/system/SystemInfo.js.map +1 -0
- package/dist/system/cpu-detect.d.ts +7 -0
- package/dist/system/cpu-detect.d.ts.map +1 -0
- package/dist/system/cpu-detect.js +61 -0
- package/dist/system/cpu-detect.js.map +1 -0
- package/dist/system/gpu-detect.d.ts +4 -0
- package/dist/system/gpu-detect.d.ts.map +1 -0
- package/dist/system/gpu-detect.js +183 -0
- package/dist/system/gpu-detect.js.map +1 -0
- package/dist/system/memory-detect.d.ts +6 -0
- package/dist/system/memory-detect.d.ts.map +1 -0
- package/dist/system/memory-detect.js +75 -0
- package/dist/system/memory-detect.js.map +1 -0
- package/dist/types/images.d.ts +73 -0
- package/dist/types/images.d.ts.map +1 -0
- package/dist/types/images.js +2 -0
- package/dist/types/images.js.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/models.d.ts +57 -0
- package/dist/types/models.d.ts.map +1 -0
- package/dist/types/models.js +2 -0
- package/dist/types/models.js.map +1 -0
- package/dist/types/servers.d.ts +40 -0
- package/dist/types/servers.d.ts.map +1 -0
- package/dist/types/servers.js +2 -0
- package/dist/types/servers.js.map +1 -0
- package/dist/types/system.d.ts +37 -0
- package/dist/types/system.d.ts.map +1 -0
- package/dist/types/system.js +2 -0
- package/dist/types/system.js.map +1 -0
- package/dist/utils/electron-lifecycle.d.ts +9 -0
- package/dist/utils/electron-lifecycle.d.ts.map +1 -0
- package/dist/utils/electron-lifecycle.js +30 -0
- package/dist/utils/electron-lifecycle.js.map +1 -0
- package/dist/utils/error-helpers.d.ts +8 -0
- package/dist/utils/error-helpers.d.ts.map +1 -0
- package/dist/utils/error-helpers.js +96 -0
- package/dist/utils/error-helpers.js.map +1 -0
- package/dist/utils/file-utils.d.ts +11 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +113 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/generation-id.d.ts +2 -0
- package/dist/utils/generation-id.d.ts.map +1 -0
- package/dist/utils/generation-id.js +6 -0
- package/dist/utils/generation-id.js.map +1 -0
- package/dist/utils/gguf-parser.d.ts +9 -0
- package/dist/utils/gguf-parser.d.ts.map +1 -0
- package/dist/utils/gguf-parser.js +64 -0
- package/dist/utils/gguf-parser.js.map +1 -0
- package/dist/utils/model-metadata-helpers.d.ts +9 -0
- package/dist/utils/model-metadata-helpers.d.ts.map +1 -0
- package/dist/utils/model-metadata-helpers.js +92 -0
- package/dist/utils/model-metadata-helpers.js.map +1 -0
- package/dist/utils/platform-utils.d.ts +18 -0
- package/dist/utils/platform-utils.d.ts.map +1 -0
- package/dist/utils/platform-utils.js +61 -0
- package/dist/utils/platform-utils.js.map +1 -0
- package/dist/utils/zip-utils.d.ts +3 -0
- package/dist/utils/zip-utils.d.ts.map +1 -0
- package/dist/utils/zip-utils.js +72 -0
- package/dist/utils/zip-utils.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { ServerManager } from './ServerManager.js';
|
|
2
|
+
import { ModelManager } from './ModelManager.js';
|
|
3
|
+
import { SystemInfo } from '../system/SystemInfo.js';
|
|
4
|
+
import { ProcessManager } from '../process/ProcessManager.js';
|
|
5
|
+
import { ResourceOrchestrator } from './ResourceOrchestrator.js';
|
|
6
|
+
import { GenerationRegistry } from './GenerationRegistry.js';
|
|
7
|
+
import http from 'node:http';
|
|
8
|
+
import { promises as fs } from 'node:fs';
|
|
9
|
+
import { getTempPath } from '../config/paths.js';
|
|
10
|
+
import { BINARY_VERSIONS, DEFAULT_PORTS } from '../config/defaults.js';
|
|
11
|
+
import { deleteFile } from '../utils/file-utils.js';
|
|
12
|
+
import { ServerError, ModelNotFoundError, InsufficientResourcesError } from '../errors/index.js';
|
|
13
|
+
export class DiffusionServerManager extends ServerManager {
|
|
14
|
+
processManager;
|
|
15
|
+
modelManager;
|
|
16
|
+
systemInfo;
|
|
17
|
+
orchestrator;
|
|
18
|
+
registry;
|
|
19
|
+
binaryPath;
|
|
20
|
+
httpServer;
|
|
21
|
+
currentGeneration;
|
|
22
|
+
currentModelInfo;
|
|
23
|
+
modelLoadTime = 2000;
|
|
24
|
+
diffusionTimePerStepPerMegapixel = 1000;
|
|
25
|
+
vaeTimePerMegapixel = 8000;
|
|
26
|
+
generationStartTime;
|
|
27
|
+
loadStartTime;
|
|
28
|
+
loadEndTime;
|
|
29
|
+
diffusionStartTime;
|
|
30
|
+
diffusionEndTime;
|
|
31
|
+
vaeStartTime;
|
|
32
|
+
vaeEndTime;
|
|
33
|
+
syntheticProgressInterval;
|
|
34
|
+
currentStage;
|
|
35
|
+
totalEstimatedTime = 0;
|
|
36
|
+
loadProgress = { current: 0, total: 0 };
|
|
37
|
+
diffusionProgress = { current: 0, total: 0 };
|
|
38
|
+
constructor(modelManager = ModelManager.getInstance(), systemInfo = SystemInfo.getInstance(), llamaServer) {
|
|
39
|
+
super();
|
|
40
|
+
this.processManager = new ProcessManager();
|
|
41
|
+
this.modelManager = modelManager;
|
|
42
|
+
this.systemInfo = systemInfo;
|
|
43
|
+
this.registry = new GenerationRegistry({
|
|
44
|
+
maxResultAgeMs: parseInt(process.env.IMAGE_RESULT_TTL_MS || '300000', 10),
|
|
45
|
+
cleanupIntervalMs: parseInt(process.env.IMAGE_CLEANUP_INTERVAL_MS || '60000', 10),
|
|
46
|
+
});
|
|
47
|
+
if (llamaServer) {
|
|
48
|
+
this.orchestrator = new ResourceOrchestrator(systemInfo, llamaServer, this, modelManager);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async start(config) {
|
|
52
|
+
if (this._status === 'running') {
|
|
53
|
+
throw new ServerError('Server is already running', {
|
|
54
|
+
suggestion: 'Stop the server first with stop()',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
this.setStatus('starting');
|
|
58
|
+
this._config = config;
|
|
59
|
+
try {
|
|
60
|
+
const modelInfo = await this.modelManager.getModelInfo(config.modelId);
|
|
61
|
+
if (modelInfo.type !== 'diffusion') {
|
|
62
|
+
throw new ModelNotFoundError(`Model ${config.modelId} is not a diffusion model (type: ${modelInfo.type})`);
|
|
63
|
+
}
|
|
64
|
+
this.currentModelInfo = modelInfo;
|
|
65
|
+
const canRun = await this.systemInfo.canRunModel(modelInfo, { checkTotalMemory: true });
|
|
66
|
+
if (!canRun.possible) {
|
|
67
|
+
const memoryInfo = this.systemInfo.getMemoryInfo();
|
|
68
|
+
throw new InsufficientResourcesError(`System cannot run model: ${canRun.reason || 'Insufficient resources'}`, {
|
|
69
|
+
required: `Model size: ${Math.round(modelInfo.size / 1024 / 1024 / 1024)}GB`,
|
|
70
|
+
available: `Total RAM: ${Math.round(memoryInfo.total / 1024 / 1024 / 1024)}GB`,
|
|
71
|
+
suggestion: canRun.suggestion || canRun.reason || 'Try a smaller model',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
this.binaryPath = await this.ensureBinary(modelInfo.path, config.forceValidation);
|
|
75
|
+
const port = config.port || DEFAULT_PORTS.diffusion;
|
|
76
|
+
await this.checkPortAvailability(port);
|
|
77
|
+
await this.initializeLogManager('diffusion-server.log', `Starting diffusion server on port ${port}`);
|
|
78
|
+
await this.createHTTPServer(config);
|
|
79
|
+
this._port = port;
|
|
80
|
+
this._startedAt = new Date();
|
|
81
|
+
this.setStatus('running');
|
|
82
|
+
await this.logManager.write('Diffusion server is running', 'info');
|
|
83
|
+
this.systemInfo.clearCache();
|
|
84
|
+
this.emitEvent('started', this.getInfo());
|
|
85
|
+
return this.getInfo();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw await this.handleStartupError('diffusion-server', error, async () => {
|
|
89
|
+
if (this.httpServer) {
|
|
90
|
+
this.httpServer.close();
|
|
91
|
+
this.httpServer = undefined;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async stop() {
|
|
97
|
+
if (this._status === 'stopped') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.setStatus('stopping');
|
|
101
|
+
try {
|
|
102
|
+
if (this.logManager) {
|
|
103
|
+
await this.logManager.write('Stopping diffusion server...', 'info');
|
|
104
|
+
}
|
|
105
|
+
if (this.currentGeneration) {
|
|
106
|
+
this.currentGeneration.cancel();
|
|
107
|
+
this.currentGeneration = undefined;
|
|
108
|
+
}
|
|
109
|
+
if (this.httpServer) {
|
|
110
|
+
await new Promise((resolve) => {
|
|
111
|
+
this.httpServer.close(() => resolve());
|
|
112
|
+
});
|
|
113
|
+
this.httpServer = undefined;
|
|
114
|
+
}
|
|
115
|
+
this.registry.destroy();
|
|
116
|
+
this.setStatus('stopped');
|
|
117
|
+
this._port = 0;
|
|
118
|
+
if (this.logManager) {
|
|
119
|
+
await this.logManager.write('Diffusion server stopped', 'info');
|
|
120
|
+
}
|
|
121
|
+
this.systemInfo.clearCache();
|
|
122
|
+
this.emitEvent('stopped');
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
this.setStatus('stopped');
|
|
126
|
+
throw new ServerError(`Failed to stop server: ${error instanceof Error ? error.message : 'Unknown error'}`, { error: error instanceof Error ? error.message : String(error) });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async generateImage(config) {
|
|
130
|
+
if (this._status !== 'running') {
|
|
131
|
+
throw new ServerError('Server is not running', {
|
|
132
|
+
suggestion: 'Start the server first with start()',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (this.currentGeneration) {
|
|
136
|
+
throw new ServerError('Server is busy generating another image', {
|
|
137
|
+
suggestion: 'Wait for current generation to complete',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
console.log('[DiffusionServer] generateImage called');
|
|
141
|
+
console.log('[DiffusionServer] orchestrator exists:', !!this.orchestrator);
|
|
142
|
+
if (this.orchestrator) {
|
|
143
|
+
console.log('[DiffusionServer] Using orchestrator for automatic resource management');
|
|
144
|
+
return this.orchestrator.orchestrateImageGeneration(config);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
console.log('[DiffusionServer] No orchestrator - using direct execution');
|
|
148
|
+
return this.executeImageGeneration(config);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async isHealthy() {
|
|
152
|
+
return this._status === 'running' && this.httpServer !== undefined;
|
|
153
|
+
}
|
|
154
|
+
getInfo() {
|
|
155
|
+
const baseInfo = super.getInfo();
|
|
156
|
+
return {
|
|
157
|
+
...baseInfo,
|
|
158
|
+
busy: !!this.currentGeneration,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async ensureBinary(modelPath, forceValidation = false) {
|
|
162
|
+
return this.ensureBinaryHelper('diffusion', 'sd', BINARY_VERSIONS.diffusionCpp, modelPath, forceValidation);
|
|
163
|
+
}
|
|
164
|
+
async createHTTPServer(config) {
|
|
165
|
+
const port = config.port || DEFAULT_PORTS.diffusion;
|
|
166
|
+
this.httpServer = http.createServer(async (req, res) => {
|
|
167
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
168
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE');
|
|
169
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
170
|
+
if (req.method === 'OPTIONS') {
|
|
171
|
+
res.writeHead(200);
|
|
172
|
+
res.end();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
if (req.url === '/health' && req.method === 'GET') {
|
|
177
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
178
|
+
res.end(JSON.stringify({ status: 'ok', busy: !!this.currentGeneration }));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (req.url === '/v1/images/generations' && req.method === 'POST') {
|
|
182
|
+
await this.handleStartGeneration(req, res);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const getMatch = req.url?.match(/^\/v1\/images\/generations\/([^/]+)$/);
|
|
186
|
+
if (getMatch && getMatch[1] && req.method === 'GET') {
|
|
187
|
+
const generationId = getMatch[1];
|
|
188
|
+
await this.handleGetGeneration(generationId, res);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
192
|
+
res.end(JSON.stringify({ error: { message: 'Not found', code: 'NOT_FOUND' } }));
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
196
|
+
res.end(JSON.stringify({
|
|
197
|
+
error: {
|
|
198
|
+
message: error instanceof Error ? error.message : 'Internal server error',
|
|
199
|
+
code: 'INTERNAL_ERROR',
|
|
200
|
+
},
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
await new Promise((resolve, reject) => {
|
|
205
|
+
this.httpServer.listen(port, () => resolve());
|
|
206
|
+
this.httpServer.on('error', reject);
|
|
207
|
+
});
|
|
208
|
+
await this.logManager?.write(`HTTP server listening on port ${port}`, 'info');
|
|
209
|
+
}
|
|
210
|
+
async handleStartGeneration(req, res) {
|
|
211
|
+
const body = await this.parseRequestBody(req);
|
|
212
|
+
const imageConfig = JSON.parse(body);
|
|
213
|
+
if (!imageConfig.prompt) {
|
|
214
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
215
|
+
res.end(JSON.stringify({
|
|
216
|
+
error: { message: 'Missing required field: prompt', code: 'INVALID_REQUEST' },
|
|
217
|
+
}));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (imageConfig.count !== undefined) {
|
|
221
|
+
if (imageConfig.count < 1 || imageConfig.count > 5) {
|
|
222
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
223
|
+
res.end(JSON.stringify({
|
|
224
|
+
error: { message: 'count must be between 1 and 5', code: 'INVALID_REQUEST' },
|
|
225
|
+
}));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (this.currentGeneration) {
|
|
230
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
231
|
+
res.end(JSON.stringify({
|
|
232
|
+
error: {
|
|
233
|
+
message: 'Server is busy generating another image',
|
|
234
|
+
code: 'SERVER_BUSY',
|
|
235
|
+
suggestion: 'Wait for current generation to complete and try again',
|
|
236
|
+
},
|
|
237
|
+
}));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const id = this.registry.create(imageConfig);
|
|
241
|
+
this.runAsyncGeneration(id, imageConfig).catch((error) => {
|
|
242
|
+
this.registry.update(id, {
|
|
243
|
+
status: 'error',
|
|
244
|
+
error: {
|
|
245
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
246
|
+
code: this.mapErrorCode(error),
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
251
|
+
res.end(JSON.stringify({
|
|
252
|
+
id,
|
|
253
|
+
status: 'pending',
|
|
254
|
+
createdAt: Date.now(),
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
async handleGetGeneration(id, res) {
|
|
258
|
+
const state = this.registry.get(id);
|
|
259
|
+
if (!state) {
|
|
260
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
261
|
+
res.end(JSON.stringify({ error: { message: 'Generation not found', code: 'NOT_FOUND' } }));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const response = {
|
|
265
|
+
id: state.id,
|
|
266
|
+
status: state.status,
|
|
267
|
+
createdAt: state.createdAt,
|
|
268
|
+
updatedAt: state.updatedAt,
|
|
269
|
+
};
|
|
270
|
+
if (state.status === 'in_progress' && state.progress) {
|
|
271
|
+
response.progress = state.progress;
|
|
272
|
+
}
|
|
273
|
+
if (state.status === 'complete' && state.result) {
|
|
274
|
+
response.result = state.result;
|
|
275
|
+
}
|
|
276
|
+
if (state.status === 'error' && state.error) {
|
|
277
|
+
response.error = state.error;
|
|
278
|
+
}
|
|
279
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
280
|
+
res.end(JSON.stringify(response));
|
|
281
|
+
}
|
|
282
|
+
async runAsyncGeneration(id, config) {
|
|
283
|
+
const startTime = Date.now();
|
|
284
|
+
this.registry.update(id, { status: 'in_progress' });
|
|
285
|
+
const wrappedConfig = {
|
|
286
|
+
...config,
|
|
287
|
+
onProgress: (currentStep, totalSteps, stage, percentage) => {
|
|
288
|
+
this.registry.update(id, {
|
|
289
|
+
progress: {
|
|
290
|
+
currentStep,
|
|
291
|
+
totalSteps,
|
|
292
|
+
stage,
|
|
293
|
+
percentage,
|
|
294
|
+
currentImage: config.count && config.count > 1
|
|
295
|
+
? Math.floor((percentage || 0) / (100 / config.count)) + 1
|
|
296
|
+
: undefined,
|
|
297
|
+
totalImages: config.count && config.count > 1 ? config.count : undefined,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
config.onProgress?.(currentStep, totalSteps, stage, percentage);
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
const count = config.count || 1;
|
|
304
|
+
let results;
|
|
305
|
+
if (count > 1) {
|
|
306
|
+
results = await this.executeBatchGeneration(wrappedConfig);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
if (this.orchestrator) {
|
|
310
|
+
results = [await this.orchestrator.orchestrateImageGeneration(wrappedConfig)];
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
results = [await this.executeImageGeneration(wrappedConfig)];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const images = results.map((result) => ({
|
|
317
|
+
image: result.image.toString('base64'),
|
|
318
|
+
seed: result.seed,
|
|
319
|
+
width: result.width,
|
|
320
|
+
height: result.height,
|
|
321
|
+
}));
|
|
322
|
+
this.registry.update(id, {
|
|
323
|
+
status: 'complete',
|
|
324
|
+
result: {
|
|
325
|
+
images,
|
|
326
|
+
format: 'png',
|
|
327
|
+
timeTaken: Date.now() - startTime,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
mapErrorCode(error) {
|
|
332
|
+
if (error instanceof Error) {
|
|
333
|
+
const message = error.message.toLowerCase();
|
|
334
|
+
if (message.includes('server is busy'))
|
|
335
|
+
return 'SERVER_BUSY';
|
|
336
|
+
if (message.includes('not running'))
|
|
337
|
+
return 'SERVER_NOT_RUNNING';
|
|
338
|
+
if (message.includes('failed to spawn'))
|
|
339
|
+
return 'BACKEND_ERROR';
|
|
340
|
+
if (message.includes('exited with code'))
|
|
341
|
+
return 'BACKEND_ERROR';
|
|
342
|
+
if (message.includes('failed to read'))
|
|
343
|
+
return 'IO_ERROR';
|
|
344
|
+
}
|
|
345
|
+
return 'UNKNOWN_ERROR';
|
|
346
|
+
}
|
|
347
|
+
parseRequestBody(req) {
|
|
348
|
+
return new Promise((resolve, reject) => {
|
|
349
|
+
let body = '';
|
|
350
|
+
req.on('data', (chunk) => {
|
|
351
|
+
body += chunk.toString();
|
|
352
|
+
});
|
|
353
|
+
req.on('end', () => resolve(body));
|
|
354
|
+
req.on('error', reject);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async executeImageGeneration(config) {
|
|
358
|
+
const startTime = Date.now();
|
|
359
|
+
if (!this.currentModelInfo) {
|
|
360
|
+
throw new ServerError('Model information not available', {
|
|
361
|
+
suggestion: 'This is an internal error - model should have been loaded',
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const normalizedConfig = {
|
|
365
|
+
...config,
|
|
366
|
+
seed: config.seed === undefined || config.seed < 0 ? this.generateRandomSeed() : config.seed,
|
|
367
|
+
};
|
|
368
|
+
this.initializeProgressTracking(normalizedConfig);
|
|
369
|
+
const args = this.buildDiffusionArgs(normalizedConfig, this.currentModelInfo);
|
|
370
|
+
const outputPath = getTempPath(`sd-output-${Date.now()}.png`);
|
|
371
|
+
args.push('-o', outputPath);
|
|
372
|
+
await this.logManager?.write(`Generating image: ${this.binaryPath} ${args.join(' ')}`, 'info');
|
|
373
|
+
let cancelled = false;
|
|
374
|
+
let pid;
|
|
375
|
+
const generationPromise = new Promise((resolve, reject) => {
|
|
376
|
+
const spawnResult = this.processManager.spawn(this.binaryPath, args, {
|
|
377
|
+
onStdout: (data) => {
|
|
378
|
+
this.processStdoutForProgress(data, normalizedConfig);
|
|
379
|
+
this.logManager?.write(data, 'info').catch(() => void 0);
|
|
380
|
+
},
|
|
381
|
+
onStderr: (data) => {
|
|
382
|
+
this.logManager?.write(data, 'warn').catch(() => void 0);
|
|
383
|
+
},
|
|
384
|
+
onExit: async (code) => {
|
|
385
|
+
this.cleanupSyntheticProgress();
|
|
386
|
+
if (cancelled) {
|
|
387
|
+
reject(new Error('Image generation cancelled'));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (code !== 0) {
|
|
391
|
+
reject(new ServerError(`stable-diffusion.cpp exited with code ${code}`));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const imageBuffer = await fs.readFile(outputPath);
|
|
396
|
+
await deleteFile(outputPath).catch(() => void 0);
|
|
397
|
+
this.updateTimeEstimates(normalizedConfig);
|
|
398
|
+
resolve({
|
|
399
|
+
image: imageBuffer,
|
|
400
|
+
format: 'png',
|
|
401
|
+
timeTaken: Date.now() - startTime,
|
|
402
|
+
seed: normalizedConfig.seed,
|
|
403
|
+
width: normalizedConfig.width || 512,
|
|
404
|
+
height: normalizedConfig.height || 512,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
reject(new ServerError('Failed to read generated image', {
|
|
409
|
+
error: error instanceof Error ? error.message : String(error),
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
onError: (error) => {
|
|
414
|
+
this.cleanupSyntheticProgress();
|
|
415
|
+
reject(new ServerError('Failed to spawn stable-diffusion.cpp', {
|
|
416
|
+
error: error.message,
|
|
417
|
+
}));
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
pid = spawnResult.pid;
|
|
421
|
+
});
|
|
422
|
+
this.currentGeneration = {
|
|
423
|
+
promise: generationPromise,
|
|
424
|
+
cancel: () => {
|
|
425
|
+
cancelled = true;
|
|
426
|
+
this.cleanupSyntheticProgress();
|
|
427
|
+
if (pid !== undefined) {
|
|
428
|
+
this.processManager.kill(pid, 5000).catch(() => void 0);
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
try {
|
|
433
|
+
const result = await generationPromise;
|
|
434
|
+
this.currentGeneration = undefined;
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
this.currentGeneration = undefined;
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async executeBatchGeneration(config) {
|
|
443
|
+
const count = config.count || 1;
|
|
444
|
+
const images = [];
|
|
445
|
+
for (let i = 0; i < count; i++) {
|
|
446
|
+
const imageSeed = config.seed !== undefined && config.seed >= 0 ? config.seed + i : this.generateRandomSeed();
|
|
447
|
+
const wrappedConfig = {
|
|
448
|
+
...config,
|
|
449
|
+
seed: imageSeed,
|
|
450
|
+
onProgress: config.onProgress
|
|
451
|
+
? (currentStep, totalSteps, stage, percentage) => {
|
|
452
|
+
const completedImages = i;
|
|
453
|
+
const currentImageProgress = (percentage || 0) / 100;
|
|
454
|
+
const overallPercentage = ((completedImages + currentImageProgress) / count) * 100;
|
|
455
|
+
config.onProgress(currentStep, totalSteps, stage, overallPercentage);
|
|
456
|
+
}
|
|
457
|
+
: undefined,
|
|
458
|
+
};
|
|
459
|
+
const result = await this.executeImageGeneration(wrappedConfig);
|
|
460
|
+
images.push(result);
|
|
461
|
+
}
|
|
462
|
+
return images;
|
|
463
|
+
}
|
|
464
|
+
buildDiffusionArgs(config, modelInfo) {
|
|
465
|
+
const args = [];
|
|
466
|
+
args.push('-m', modelInfo.path);
|
|
467
|
+
if (config.prompt) {
|
|
468
|
+
args.push('-p', config.prompt);
|
|
469
|
+
}
|
|
470
|
+
if (config.negativePrompt) {
|
|
471
|
+
args.push('-n', config.negativePrompt);
|
|
472
|
+
}
|
|
473
|
+
if (config.width) {
|
|
474
|
+
args.push('-W', String(config.width));
|
|
475
|
+
}
|
|
476
|
+
if (config.height) {
|
|
477
|
+
args.push('-H', String(config.height));
|
|
478
|
+
}
|
|
479
|
+
if (config.steps) {
|
|
480
|
+
args.push('--steps', String(config.steps));
|
|
481
|
+
}
|
|
482
|
+
if (config.cfgScale) {
|
|
483
|
+
args.push('--cfg-scale', String(config.cfgScale));
|
|
484
|
+
}
|
|
485
|
+
if (config.seed !== undefined) {
|
|
486
|
+
args.push('-s', String(config.seed));
|
|
487
|
+
}
|
|
488
|
+
if (config.sampler) {
|
|
489
|
+
args.push('--sampling-method', config.sampler);
|
|
490
|
+
}
|
|
491
|
+
const serverConfig = this._config;
|
|
492
|
+
if (serverConfig.gpuLayers !== undefined && serverConfig.gpuLayers > 0) {
|
|
493
|
+
args.push('--n-gpu-layers', String(serverConfig.gpuLayers));
|
|
494
|
+
}
|
|
495
|
+
if (serverConfig.threads) {
|
|
496
|
+
args.push('-t', String(serverConfig.threads));
|
|
497
|
+
}
|
|
498
|
+
return args;
|
|
499
|
+
}
|
|
500
|
+
generateRandomSeed() {
|
|
501
|
+
return Math.floor(Math.random() * 2147483647);
|
|
502
|
+
}
|
|
503
|
+
initializeProgressTracking(config) {
|
|
504
|
+
const width = config.width || 512;
|
|
505
|
+
const height = config.height || 512;
|
|
506
|
+
const steps = config.steps || 20;
|
|
507
|
+
const megapixels = (width * height) / 1_000_000;
|
|
508
|
+
this.totalEstimatedTime =
|
|
509
|
+
this.modelLoadTime +
|
|
510
|
+
steps * megapixels * this.diffusionTimePerStepPerMegapixel +
|
|
511
|
+
megapixels * this.vaeTimePerMegapixel;
|
|
512
|
+
this.generationStartTime = Date.now();
|
|
513
|
+
this.currentStage = undefined;
|
|
514
|
+
this.loadStartTime = undefined;
|
|
515
|
+
this.loadEndTime = undefined;
|
|
516
|
+
this.diffusionStartTime = undefined;
|
|
517
|
+
this.diffusionEndTime = undefined;
|
|
518
|
+
this.vaeStartTime = undefined;
|
|
519
|
+
this.vaeEndTime = undefined;
|
|
520
|
+
this.loadProgress = { current: 0, total: 0 };
|
|
521
|
+
this.diffusionProgress = { current: 0, total: 0 };
|
|
522
|
+
}
|
|
523
|
+
processStdoutForProgress(data, config) {
|
|
524
|
+
if (data.includes('loading tensors from')) {
|
|
525
|
+
this.currentStage = 'loading';
|
|
526
|
+
this.loadStartTime = Date.now();
|
|
527
|
+
}
|
|
528
|
+
else if (data.includes('generating image:') || data.includes('sampling using')) {
|
|
529
|
+
if (this.currentStage === 'loading') {
|
|
530
|
+
this.loadEndTime = Date.now();
|
|
531
|
+
}
|
|
532
|
+
this.currentStage = 'diffusion';
|
|
533
|
+
this.diffusionStartTime = Date.now();
|
|
534
|
+
}
|
|
535
|
+
else if (data.includes('decoding 1 latents')) {
|
|
536
|
+
if (this.currentStage === 'diffusion') {
|
|
537
|
+
this.diffusionEndTime = Date.now();
|
|
538
|
+
}
|
|
539
|
+
this.currentStage = 'vae';
|
|
540
|
+
this.vaeStartTime = Date.now();
|
|
541
|
+
this.startSyntheticVaeProgress(config);
|
|
542
|
+
}
|
|
543
|
+
else if (data.includes('decode_first_stage completed')) {
|
|
544
|
+
this.vaeEndTime = Date.now();
|
|
545
|
+
this.cleanupSyntheticProgress();
|
|
546
|
+
if (config.onProgress) {
|
|
547
|
+
config.onProgress(0, 0, 'decoding', 100);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const progressMatch = data.match(/\|\s*(\d+)\/(\d+)\s*-/);
|
|
551
|
+
if (progressMatch && progressMatch[1] && progressMatch[2]) {
|
|
552
|
+
const current = parseInt(progressMatch[1], 10);
|
|
553
|
+
const total = parseInt(progressMatch[2], 10);
|
|
554
|
+
if (this.currentStage === 'loading') {
|
|
555
|
+
this.loadProgress = { current, total };
|
|
556
|
+
this.reportProgress(config);
|
|
557
|
+
}
|
|
558
|
+
else if (this.currentStage === 'diffusion') {
|
|
559
|
+
this.diffusionProgress = { current, total };
|
|
560
|
+
this.reportProgress(config);
|
|
561
|
+
}
|
|
562
|
+
else if (this.currentStage === undefined) {
|
|
563
|
+
this.currentStage = 'loading';
|
|
564
|
+
this.loadStartTime = Date.now();
|
|
565
|
+
this.loadProgress = { current, total };
|
|
566
|
+
this.reportProgress(config);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
reportProgress(config) {
|
|
571
|
+
if (!config.onProgress || !this.generationStartTime)
|
|
572
|
+
return;
|
|
573
|
+
const percentage = this.calculateOverallPercentage();
|
|
574
|
+
if (this.currentStage === 'loading' && this.loadProgress.total > 0) {
|
|
575
|
+
config.onProgress(this.loadProgress.current, this.loadProgress.total, 'loading', percentage);
|
|
576
|
+
}
|
|
577
|
+
else if (this.currentStage === 'diffusion' && this.diffusionProgress.total > 0) {
|
|
578
|
+
config.onProgress(this.diffusionProgress.current, this.diffusionProgress.total, 'diffusion', percentage);
|
|
579
|
+
}
|
|
580
|
+
else if (this.currentStage === 'vae') {
|
|
581
|
+
config.onProgress(0, 0, 'decoding', percentage);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
calculateOverallPercentage() {
|
|
585
|
+
if (!this.generationStartTime || this.totalEstimatedTime === 0)
|
|
586
|
+
return 0;
|
|
587
|
+
let elapsedTotal = 0;
|
|
588
|
+
if (this.currentStage === 'loading') {
|
|
589
|
+
const elapsedLoad = Date.now() - (this.loadStartTime || this.generationStartTime);
|
|
590
|
+
elapsedTotal = elapsedLoad;
|
|
591
|
+
}
|
|
592
|
+
else if (this.currentStage === 'diffusion') {
|
|
593
|
+
const actualLoadTime = this.loadEndTime
|
|
594
|
+
? this.loadEndTime - (this.loadStartTime || this.generationStartTime)
|
|
595
|
+
: this.modelLoadTime;
|
|
596
|
+
const elapsedDiffusion = Date.now() - (this.diffusionStartTime || Date.now());
|
|
597
|
+
elapsedTotal = actualLoadTime + elapsedDiffusion;
|
|
598
|
+
}
|
|
599
|
+
else if (this.currentStage === 'vae') {
|
|
600
|
+
const actualLoadTime = this.loadEndTime
|
|
601
|
+
? this.loadEndTime - (this.loadStartTime || this.generationStartTime)
|
|
602
|
+
: this.modelLoadTime;
|
|
603
|
+
const actualDiffusionTime = this.diffusionEndTime
|
|
604
|
+
? this.diffusionEndTime - (this.diffusionStartTime || Date.now())
|
|
605
|
+
: 0;
|
|
606
|
+
const elapsedVae = Date.now() - (this.vaeStartTime || Date.now());
|
|
607
|
+
elapsedTotal = actualLoadTime + actualDiffusionTime + elapsedVae;
|
|
608
|
+
}
|
|
609
|
+
return Math.min(100, Math.round((elapsedTotal / this.totalEstimatedTime) * 100));
|
|
610
|
+
}
|
|
611
|
+
startSyntheticVaeProgress(config) {
|
|
612
|
+
if (!config.onProgress)
|
|
613
|
+
return;
|
|
614
|
+
this.cleanupSyntheticProgress();
|
|
615
|
+
this.syntheticProgressInterval = setInterval(() => {
|
|
616
|
+
const percentage = this.calculateOverallPercentage();
|
|
617
|
+
config.onProgress(0, 0, 'decoding', percentage);
|
|
618
|
+
}, 100);
|
|
619
|
+
if (this.syntheticProgressInterval &&
|
|
620
|
+
typeof this.syntheticProgressInterval.unref === 'function') {
|
|
621
|
+
this.syntheticProgressInterval.unref();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
cleanupSyntheticProgress() {
|
|
625
|
+
if (this.syntheticProgressInterval) {
|
|
626
|
+
clearInterval(this.syntheticProgressInterval);
|
|
627
|
+
this.syntheticProgressInterval = undefined;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
updateTimeEstimates(config) {
|
|
631
|
+
const width = config.width || 512;
|
|
632
|
+
const height = config.height || 512;
|
|
633
|
+
const steps = config.steps || 20;
|
|
634
|
+
const megapixels = (width * height) / 1_000_000;
|
|
635
|
+
if (this.loadStartTime && this.loadEndTime) {
|
|
636
|
+
const actualLoadTime = this.loadEndTime - this.loadStartTime;
|
|
637
|
+
this.modelLoadTime = actualLoadTime;
|
|
638
|
+
}
|
|
639
|
+
if (this.diffusionStartTime && this.diffusionEndTime) {
|
|
640
|
+
const actualDiffusionTime = this.diffusionEndTime - this.diffusionStartTime;
|
|
641
|
+
this.diffusionTimePerStepPerMegapixel = actualDiffusionTime / (steps * megapixels);
|
|
642
|
+
}
|
|
643
|
+
if (this.vaeStartTime && this.vaeEndTime) {
|
|
644
|
+
const actualVaeTime = this.vaeEndTime - this.vaeStartTime;
|
|
645
|
+
this.vaeTimePerMegapixel = actualVaeTime / megapixels;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
//# sourceMappingURL=DiffusionServerManager.js.map
|