genai-electron 0.9.0 → 0.11.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.
@@ -6,11 +6,12 @@ import { ResourceOrchestrator } from './ResourceOrchestrator.js';
6
6
  import { GenerationRegistry } from './GenerationRegistry.js';
7
7
  import http from 'node:http';
8
8
  import { promises as fs } from 'node:fs';
9
- import { getTempPath, PATHS } from '../config/paths.js';
10
- import { BINARY_VERSIONS, DEFAULT_PORTS, DIFFUSION_VRAM_THRESHOLDS, DIFFUSION_COMPONENT_FLAGS, DIFFUSION_COMPONENT_ORDER, } from '../config/defaults.js';
9
+ import { getTempPath } from '../config/paths.js';
10
+ import { BINARY_VERSIONS, DEFAULT_PORTS, DIFFUSION_VRAM_THRESHOLDS, DIFFUSION_COMPONENT_FLAGS, DIFFUSION_COMPONENT_ORDER, DIFFUSION_CALIBRATION_DEFAULTS, } from '../config/defaults.js';
11
11
  import { deleteFile } from '../utils/file-utils.js';
12
+ import { debugLog } from '../utils/debug-log.js';
12
13
  import { findFreePort } from '../process/port-utils.js';
13
- import { ServerError, ModelNotFoundError, InsufficientResourcesError } from '../errors/index.js';
14
+ import { GenaiElectronError, ServerError, ModelNotFoundError, InsufficientResourcesError, } from '../errors/index.js';
14
15
  export class DiffusionServerManager extends ServerManager {
15
16
  static VALID_CONFIG_FIELDS = new Set([
16
17
  'modelId',
@@ -34,6 +35,8 @@ export class DiffusionServerManager extends ServerManager {
34
35
  currentGeneration;
35
36
  activeGeneration;
36
37
  currentModelInfo;
38
+ lastResolvedOptimizations;
39
+ calibrating = false;
37
40
  modelLoadTime = 2000;
38
41
  diffusionTimePerStepPerMegapixel = 1000;
39
42
  vaeTimePerMegapixel = 8000;
@@ -71,6 +74,11 @@ export class DiffusionServerManager extends ServerManager {
71
74
  suggestion: 'Stop the server first with stop()',
72
75
  });
73
76
  }
77
+ if (this.calibrating) {
78
+ throw new ServerError('Cannot start server while offload calibration is in progress', {
79
+ suggestion: 'Wait for calibrate() to finish, or abort it via its AbortSignal',
80
+ });
81
+ }
74
82
  this.validateConfigFields(config, DiffusionServerManager.VALID_CONFIG_FIELDS, 'DiffusionServerManager');
75
83
  this.setStatus('starting');
76
84
  this._config = config;
@@ -173,6 +181,389 @@ export class DiffusionServerManager extends ServerManager {
173
181
  }
174
182
  await this.logManager?.write(`Generation ${id} cancelled`, 'info');
175
183
  }
184
+ async calibrate(config) {
185
+ if (this._status !== 'stopped') {
186
+ throw new ServerError('Cannot calibrate while the server is running', {
187
+ suggestion: 'Stop the server with stop() before calibrating',
188
+ });
189
+ }
190
+ if (this.calibrating) {
191
+ throw new ServerError('Calibration is already in progress', {
192
+ suggestion: 'Wait for the current calibrate() call to finish',
193
+ });
194
+ }
195
+ const defaults = DIFFUSION_CALIBRATION_DEFAULTS;
196
+ const sizes = config.sizes && config.sizes.length > 0 ? config.sizes : [...defaults.sizes];
197
+ for (const size of sizes) {
198
+ if (!Number.isInteger(size.width) ||
199
+ !Number.isInteger(size.height) ||
200
+ size.width <= 0 ||
201
+ size.height <= 0 ||
202
+ size.width % 64 !== 0 ||
203
+ size.height % 64 !== 0) {
204
+ throw new ServerError(`Invalid calibration size ${size.width}x${size.height}: dimensions must be positive multiples of 64`, { suggestion: 'Use sd.cpp-compatible dimensions, e.g. 512x512, 768x768, 512x1024' });
205
+ }
206
+ }
207
+ const samples = Math.max(1, Math.floor(config.samples ?? defaults.samples));
208
+ const steps = config.steps ?? defaults.steps;
209
+ const seed = config.seed ?? defaults.seed;
210
+ const sampler = config.sampler ?? defaults.sampler;
211
+ const prompt = config.prompt ?? defaults.prompt;
212
+ const runs = [];
213
+ if (config.signal?.aborted) {
214
+ throw this.calibrationAbortError(runs);
215
+ }
216
+ this.calibrating = true;
217
+ const savedConfig = this._config;
218
+ const savedModelInfo = this.currentModelInfo;
219
+ const savedBinaryPath = this.binaryPath;
220
+ const abortListener = () => {
221
+ this.currentGeneration?.cancel();
222
+ };
223
+ config.signal?.addEventListener('abort', abortListener);
224
+ let emitFn;
225
+ let comboCountForProgress = 0;
226
+ let lastOverallPercent = 0;
227
+ let succeeded = false;
228
+ try {
229
+ const modelInfo = await this.modelManager.getModelInfo(config.modelId);
230
+ if (modelInfo.type !== 'diffusion') {
231
+ throw new ModelNotFoundError(`Model ${config.modelId} is not a diffusion model (type: ${modelInfo.type})`);
232
+ }
233
+ const requestedCombos = (config.combos && config.combos.length > 0 ? config.combos : defaults.combos).map((combo) => ({ ...combo }));
234
+ const skippedCombos = [];
235
+ let combos = requestedCombos;
236
+ if (defaults.sd35LargePattern.test(modelInfo.id) ||
237
+ defaults.sd35LargePattern.test(modelInfo.name)) {
238
+ combos = requestedCombos.filter((combo) => {
239
+ if (combo.clipOnCpu === true) {
240
+ skippedCombos.push({
241
+ combo,
242
+ reason: 'SD3.5-Large produces garbled output with --clip-on-cpu (leejet/stable-diffusion.cpp#1578)',
243
+ });
244
+ return false;
245
+ }
246
+ return true;
247
+ });
248
+ }
249
+ if (combos.length === 0) {
250
+ throw new ServerError('No offload combos left to benchmark after SD3.5-Large filtering', {
251
+ skippedCombos,
252
+ suggestion: 'Provide combos without clipOnCpu: true for this model',
253
+ });
254
+ }
255
+ comboCountForProgress = combos.length;
256
+ const totalUnits = combos.length * (1 + samples * sizes.length);
257
+ let completedUnits = 0;
258
+ const emit = (p) => {
259
+ try {
260
+ config.onProgress?.(p);
261
+ }
262
+ catch (error) {
263
+ debugLog('[Calibrate] onProgress callback threw:', error);
264
+ }
265
+ try {
266
+ this.emit('calibration-progress', p);
267
+ }
268
+ catch (error) {
269
+ debugLog('[Calibrate] calibration-progress listener threw:', error);
270
+ }
271
+ };
272
+ emitFn = emit;
273
+ const overallPercent = (generationFraction = 0) => {
274
+ const pct = Math.min(100, ((completedUnits + generationFraction) / totalUnits) * 100);
275
+ lastOverallPercent = Math.max(lastOverallPercent, pct);
276
+ return lastOverallPercent;
277
+ };
278
+ emit({
279
+ phase: 'preparing',
280
+ comboIndex: 0,
281
+ comboCount: combos.length,
282
+ sizeIndex: 0,
283
+ sizeCount: sizes.length,
284
+ overallPercent: 0,
285
+ });
286
+ const canRun = await this.systemInfo.canRunModel(modelInfo, { checkTotalMemory: true });
287
+ if (!canRun.possible) {
288
+ const memoryInfo = this.systemInfo.getMemoryInfo();
289
+ throw new InsufficientResourcesError(`System cannot run model: ${canRun.reason || 'Insufficient resources'}`, {
290
+ required: `Model size: ${Math.round(modelInfo.size / 1024 / 1024 / 1024)}GB`,
291
+ available: `Total RAM: ${Math.round(memoryInfo.total / 1024 / 1024 / 1024)}GB`,
292
+ suggestion: canRun.suggestion || canRun.reason || 'Try a smaller model',
293
+ });
294
+ }
295
+ if (!this.logManager) {
296
+ await this.initializeLogManager('diffusion-server.log', 'Offload calibration starting');
297
+ }
298
+ void this.logManager
299
+ ?.write(`Calibration: model=${config.modelId}, sizes=${sizes
300
+ .map((s) => `${s.width}x${s.height}`)
301
+ .join(',')}, combos=${combos.length}${skippedCombos.length > 0 ? ` (${skippedCombos.length} skipped: SD3.5-Large)` : ''}, steps=${steps}, samples=${samples}`, 'info')
302
+ .catch(() => void 0);
303
+ this.currentModelInfo = modelInfo;
304
+ this.binaryPath = await this.ensureBinary(modelInfo);
305
+ if (config.signal?.aborted) {
306
+ throw this.calibrationAbortError(runs);
307
+ }
308
+ const syntheticConfig = { modelId: config.modelId };
309
+ if (config.threads !== undefined) {
310
+ syntheticConfig.threads = config.threads;
311
+ }
312
+ if (config.batchSize !== undefined) {
313
+ syntheticConfig.batchSize = config.batchSize;
314
+ }
315
+ this._config = syntheticConfig;
316
+ await this.orchestrator?.waitForReload();
317
+ await this.orchestrator?.offloadLLM();
318
+ for (let comboIndex = 0; comboIndex < combos.length; comboIndex++) {
319
+ const combo = combos[comboIndex];
320
+ const baseProgress = {
321
+ comboIndex,
322
+ comboCount: combos.length,
323
+ combo,
324
+ sizeCount: sizes.length,
325
+ };
326
+ let warmupFailure;
327
+ if (config.signal?.aborted) {
328
+ throw this.calibrationAbortError(runs);
329
+ }
330
+ emit({
331
+ phase: 'warmup',
332
+ ...baseProgress,
333
+ sizeIndex: 0,
334
+ size: sizes[0],
335
+ overallPercent: overallPercent(),
336
+ });
337
+ try {
338
+ await this.runCalibrationGeneration({
339
+ prompt,
340
+ size: sizes[0],
341
+ steps,
342
+ cfgScale: config.cfgScale,
343
+ seed,
344
+ sampler,
345
+ combo,
346
+ onGenerationProgress: (pct) => emit({
347
+ phase: 'warmup',
348
+ ...baseProgress,
349
+ sizeIndex: 0,
350
+ size: sizes[0],
351
+ generationPercent: pct,
352
+ overallPercent: overallPercent(pct / 100),
353
+ }),
354
+ });
355
+ }
356
+ catch (error) {
357
+ if (config.signal?.aborted) {
358
+ throw this.calibrationAbortError(runs);
359
+ }
360
+ warmupFailure = this.classifyCalibrationFailure(error);
361
+ }
362
+ completedUnits++;
363
+ for (let sizeIndex = 0; sizeIndex < sizes.length; sizeIndex++) {
364
+ const size = sizes[sizeIndex];
365
+ const samplesMs = [];
366
+ const snapshots = [];
367
+ let resolved;
368
+ let failure;
369
+ if (sizeIndex === 0 && warmupFailure) {
370
+ failure = warmupFailure;
371
+ completedUnits += samples;
372
+ }
373
+ else {
374
+ for (let sample = 1; sample <= samples; sample++) {
375
+ if (config.signal?.aborted) {
376
+ throw this.calibrationAbortError(runs);
377
+ }
378
+ emit({
379
+ phase: 'sampling',
380
+ ...baseProgress,
381
+ sizeIndex,
382
+ size,
383
+ sample,
384
+ sampleCount: samples,
385
+ overallPercent: overallPercent(),
386
+ });
387
+ try {
388
+ const result = await this.runCalibrationGeneration({
389
+ prompt,
390
+ size,
391
+ steps,
392
+ cfgScale: config.cfgScale,
393
+ seed,
394
+ sampler,
395
+ combo,
396
+ onGenerationProgress: (pct) => emit({
397
+ phase: 'sampling',
398
+ ...baseProgress,
399
+ sizeIndex,
400
+ size,
401
+ sample,
402
+ sampleCount: samples,
403
+ generationPercent: pct,
404
+ overallPercent: overallPercent(pct / 100),
405
+ }),
406
+ });
407
+ samplesMs.push(result.timeTaken);
408
+ snapshots.push(this.snapshotStageMs());
409
+ resolved = this.lastResolvedOptimizations
410
+ ? { ...this.lastResolvedOptimizations }
411
+ : undefined;
412
+ completedUnits++;
413
+ }
414
+ catch (error) {
415
+ if (config.signal?.aborted) {
416
+ throw this.calibrationAbortError(runs);
417
+ }
418
+ failure = this.classifyCalibrationFailure(error);
419
+ completedUnits += samples - sample + 1;
420
+ break;
421
+ }
422
+ }
423
+ }
424
+ const run = { size, combo, status: failure ? failure.status : 'ok' };
425
+ if (resolved) {
426
+ run.resolved = resolved;
427
+ }
428
+ if (samplesMs.length > 0) {
429
+ run.samplesMs = samplesMs;
430
+ }
431
+ if (failure) {
432
+ run.error = failure.message;
433
+ }
434
+ else {
435
+ const median = medianOf(samplesMs);
436
+ run.timeTakenMs = median;
437
+ let bestIdx = 0;
438
+ for (let i = 1; i < samplesMs.length; i++) {
439
+ if (Math.abs(samplesMs[i] - median) < Math.abs(samplesMs[bestIdx] - median)) {
440
+ bestIdx = i;
441
+ }
442
+ }
443
+ const stage = snapshots[bestIdx];
444
+ if (stage &&
445
+ (stage.loadMs !== undefined ||
446
+ stage.diffusionMs !== undefined ||
447
+ stage.decodeMs !== undefined)) {
448
+ run.stageMs = stage;
449
+ }
450
+ }
451
+ runs.push(run);
452
+ void this.logManager
453
+ ?.write(`Calibration run: ${combo.label ?? JSON.stringify(combo)} @ ${size.width}x${size.height} → ${run.status}${run.timeTakenMs !== undefined ? ` (${Math.round(run.timeTakenMs)} ms)` : ''}${run.error ? ` — ${run.error.split('\n')[0]}` : ''}`, run.status === 'ok' ? 'info' : 'warn')
454
+ .catch(() => void 0);
455
+ }
456
+ }
457
+ const recommended = pickRecommended(runs, defaults.tieTolerancePct);
458
+ const machine = {};
459
+ try {
460
+ const gpu = await this.systemInfo.getGPUInfo();
461
+ machine.gpuType = gpu.type;
462
+ machine.gpuName = gpu.name;
463
+ machine.vramBytes = gpu.vram;
464
+ machine.vramAvailableBytes = gpu.vramAvailable;
465
+ }
466
+ catch {
467
+ }
468
+ const report = {
469
+ machine,
470
+ modelId: config.modelId,
471
+ steps,
472
+ sampler,
473
+ samples,
474
+ runs,
475
+ recommended,
476
+ };
477
+ if (skippedCombos.length > 0) {
478
+ report.skippedCombos = skippedCombos;
479
+ }
480
+ succeeded = true;
481
+ return report;
482
+ }
483
+ finally {
484
+ config.signal?.removeEventListener('abort', abortListener);
485
+ this._config = savedConfig;
486
+ this.currentModelInfo = savedModelInfo;
487
+ this.binaryPath = savedBinaryPath;
488
+ this.calibrating = false;
489
+ if (this.orchestrator) {
490
+ emitFn?.({
491
+ phase: 'restoring-llm',
492
+ comboIndex: Math.max(0, comboCountForProgress - 1),
493
+ comboCount: comboCountForProgress,
494
+ sizeIndex: Math.max(0, sizes.length - 1),
495
+ sizeCount: sizes.length,
496
+ overallPercent: succeeded ? 100 : lastOverallPercent,
497
+ });
498
+ await this.orchestrator.reloadLLM();
499
+ }
500
+ if (succeeded) {
501
+ emitFn?.({
502
+ phase: 'done',
503
+ comboIndex: Math.max(0, comboCountForProgress - 1),
504
+ comboCount: comboCountForProgress,
505
+ sizeIndex: Math.max(0, sizes.length - 1),
506
+ sizeCount: sizes.length,
507
+ overallPercent: 100,
508
+ });
509
+ }
510
+ }
511
+ }
512
+ isCalibrating() {
513
+ return this.calibrating;
514
+ }
515
+ calibrationAbortError(runs) {
516
+ return new ServerError('Calibration aborted', {
517
+ code: 'CALIBRATION_ABORTED',
518
+ runs: [...runs],
519
+ suggestion: 'Partial results are available in error.details.runs',
520
+ });
521
+ }
522
+ async runCalibrationGeneration(params) {
523
+ const genConfig = {
524
+ prompt: params.prompt,
525
+ width: params.size.width,
526
+ height: params.size.height,
527
+ steps: params.steps,
528
+ seed: params.seed,
529
+ sampler: params.sampler,
530
+ onProgress: (_currentStep, _totalSteps, _stage, percentage) => {
531
+ if (percentage !== undefined) {
532
+ params.onGenerationProgress(percentage);
533
+ }
534
+ },
535
+ };
536
+ if (params.cfgScale !== undefined) {
537
+ genConfig.cfgScale = params.cfgScale;
538
+ }
539
+ return this.executeImageGeneration(genConfig, params.combo);
540
+ }
541
+ snapshotStageMs() {
542
+ const stage = {};
543
+ if (this.loadStartTime && this.loadEndTime) {
544
+ stage.loadMs = this.loadEndTime - this.loadStartTime;
545
+ }
546
+ if (this.diffusionStartTime && this.diffusionEndTime) {
547
+ stage.diffusionMs = this.diffusionEndTime - this.diffusionStartTime;
548
+ }
549
+ if (this.vaeStartTime && this.vaeEndTime) {
550
+ stage.decodeMs = this.vaeEndTime - this.vaeStartTime;
551
+ }
552
+ return stage;
553
+ }
554
+ classifyCalibrationFailure(error) {
555
+ const message = error instanceof Error ? error.message : String(error);
556
+ let stderr = '';
557
+ if (error instanceof GenaiElectronError && error.details && typeof error.details === 'object') {
558
+ const detailStderr = error.details.stderr;
559
+ if (typeof detailStderr === 'string') {
560
+ stderr = detailStderr;
561
+ }
562
+ }
563
+ const text = `${message}\n${stderr}`;
564
+ const isOom = DIFFUSION_CALIBRATION_DEFAULTS.oomPatterns.some((pattern) => pattern.test(text));
565
+ return { status: isOom ? 'oom' : 'error', message };
566
+ }
176
567
  async generateImage(config) {
177
568
  if (this._status !== 'running') {
178
569
  throw new ServerError('Server is not running', {
@@ -447,7 +838,7 @@ export class DiffusionServerManager extends ServerManager {
447
838
  req.on('error', reject);
448
839
  });
449
840
  }
450
- async executeImageGeneration(config) {
841
+ async executeImageGeneration(config, flagOverrides) {
451
842
  const startTime = Date.now();
452
843
  if (!this.currentModelInfo) {
453
844
  throw new ServerError('Model information not available', {
@@ -459,7 +850,7 @@ export class DiffusionServerManager extends ServerManager {
459
850
  seed: config.seed === undefined || config.seed < 0 ? this.generateRandomSeed() : config.seed,
460
851
  };
461
852
  this.initializeProgressTracking(normalizedConfig);
462
- const optimizations = await this.computeDiffusionOptimizations();
853
+ const optimizations = await this.computeDiffusionOptimizations(flagOverrides);
463
854
  const args = this.buildDiffusionArgs(normalizedConfig, this.currentModelInfo, optimizations);
464
855
  const outputPath = getTempPath(`sd-output-${Date.now()}.png`);
465
856
  args.push('-o', outputPath);
@@ -574,7 +965,7 @@ export class DiffusionServerManager extends ServerManager {
574
965
  }
575
966
  return images;
576
967
  }
577
- async computeDiffusionOptimizations() {
968
+ async computeDiffusionOptimizations(flagOverrides) {
578
969
  const serverConfig = this._config;
579
970
  const modelSize = this.currentModelInfo?.size ?? 0;
580
971
  const modelFootprint = modelSize * DIFFUSION_VRAM_THRESHOLDS.modelOverheadMultiplier;
@@ -590,15 +981,10 @@ export class DiffusionServerManager extends ServerManager {
590
981
  }
591
982
  else {
592
983
  const headroom = gpu.vram - modelFootprint;
593
- const isCuda = await this.isInstalledVariantCuda();
594
- autoClipOnCpu = isCuda
595
- ? false
596
- : headroom < DIFFUSION_VRAM_THRESHOLDS.clipOnCpuHeadroomBytes;
597
- autoVaeOnCpu = isCuda ? false : headroom < DIFFUSION_VRAM_THRESHOLDS.vaeOnCpuHeadroomBytes;
598
- autoOffloadToCpu = isCuda ? false : modelFootprint > gpu.vram * 0.85;
599
- if (!isCuda &&
600
- gpu.vramAvailable !== undefined &&
601
- gpu.vramAvailable - modelFootprint < 2 * 1024 ** 3) {
984
+ autoClipOnCpu = headroom < DIFFUSION_VRAM_THRESHOLDS.clipOnCpuHeadroomBytes;
985
+ autoVaeOnCpu = headroom < DIFFUSION_VRAM_THRESHOLDS.vaeOnCpuHeadroomBytes;
986
+ autoOffloadToCpu = modelFootprint > gpu.vram * 0.85;
987
+ if (gpu.vramAvailable !== undefined && gpu.vramAvailable - modelFootprint < 2 * 1024 ** 3) {
602
988
  autoClipOnCpu = true;
603
989
  }
604
990
  }
@@ -610,25 +996,17 @@ export class DiffusionServerManager extends ServerManager {
610
996
  }
611
997
  const hasLLMComponent = !!this.currentModelInfo?.components?.llm;
612
998
  const autoDiffusionFlashAttention = hasLLMComponent;
613
- const clipOnCpu = serverConfig.clipOnCpu ?? autoClipOnCpu;
614
- const vaeOnCpu = serverConfig.vaeOnCpu ?? autoVaeOnCpu;
615
- const offloadToCpu = serverConfig.offloadToCpu ?? autoOffloadToCpu;
616
- const diffusionFlashAttention = serverConfig.diffusionFlashAttention ?? autoDiffusionFlashAttention;
999
+ const clipOnCpu = flagOverrides?.clipOnCpu ?? serverConfig.clipOnCpu ?? autoClipOnCpu;
1000
+ const vaeOnCpu = flagOverrides?.vaeOnCpu ?? serverConfig.vaeOnCpu ?? autoVaeOnCpu;
1001
+ const offloadToCpu = flagOverrides?.offloadToCpu ?? serverConfig.offloadToCpu ?? autoOffloadToCpu;
1002
+ const diffusionFlashAttention = flagOverrides?.diffusionFlashAttention ??
1003
+ serverConfig.diffusionFlashAttention ??
1004
+ autoDiffusionFlashAttention;
617
1005
  const batchSize = serverConfig.batchSize;
1006
+ this.lastResolvedOptimizations = { clipOnCpu, vaeOnCpu, offloadToCpu, diffusionFlashAttention };
618
1007
  await this.logManager?.write(`VRAM optimizations: clipOnCpu=${clipOnCpu}, vaeOnCpu=${vaeOnCpu}, offloadToCpu=${offloadToCpu}, diffusionFa=${diffusionFlashAttention}${batchSize !== undefined ? `, batchSize=${batchSize}` : ''} (auto: clip=${autoClipOnCpu}, vae=${autoVaeOnCpu}, offload=${autoOffloadToCpu}, fa=${autoDiffusionFlashAttention})`, 'info');
619
1008
  return { clipOnCpu, vaeOnCpu, offloadToCpu, diffusionFlashAttention, batchSize };
620
1009
  }
621
- async isInstalledVariantCuda() {
622
- try {
623
- const variantCachePath = `${PATHS.binaries.diffusion}/.variant.json`;
624
- const content = await fs.readFile(variantCachePath, 'utf-8');
625
- const cache = JSON.parse(content);
626
- return cache.variant === 'cuda';
627
- }
628
- catch {
629
- return false;
630
- }
631
- }
632
1010
  buildDiffusionArgs(config, modelInfo, optimizations) {
633
1011
  const args = [];
634
1012
  if (modelInfo.components) {
@@ -736,7 +1114,7 @@ export class DiffusionServerManager extends ServerManager {
736
1114
  this.totalEstimatedTime = loadTime + diffusionTime + vaeTime;
737
1115
  }
738
1116
  processStdoutForProgress(data, config) {
739
- if (data.includes('loading tensors from')) {
1117
+ if (data.includes('loading tensors from') || data.includes('loading model from')) {
740
1118
  this.currentStage = 'loading';
741
1119
  this.loadStartTime = Date.now();
742
1120
  this.reportProgress(config);
@@ -768,7 +1146,7 @@ export class DiffusionServerManager extends ServerManager {
768
1146
  config.onProgress(0, 0, 'decoding', 100);
769
1147
  }
770
1148
  }
771
- const progressMatch = data.match(/\|\s*(\d+)\/(\d+)\s*-/);
1149
+ const progressMatch = data.match(/\|\s*(\d+)\/(\d+)\s*-\s*[\d.]+\s*(?:it\/s|s\/it)/);
772
1150
  if (progressMatch && progressMatch[1] && progressMatch[2]) {
773
1151
  const current = parseInt(progressMatch[1], 10);
774
1152
  const total = parseInt(progressMatch[2], 10);
@@ -787,6 +1165,21 @@ export class DiffusionServerManager extends ServerManager {
787
1165
  this.reportProgress(config);
788
1166
  }
789
1167
  }
1168
+ const byteMatch = data.match(/\|\s*(\d+)\/(\d+)\s*-\s*[\d.]+\s*(?:B|KB|MB|GB)\/s/);
1169
+ if (byteMatch && byteMatch[1] && byteMatch[2]) {
1170
+ const current = parseInt(byteMatch[1], 10);
1171
+ const total = parseInt(byteMatch[2], 10);
1172
+ if (this.currentStage === 'loading') {
1173
+ this.loadProgress = { current, total };
1174
+ this.reportProgress(config);
1175
+ }
1176
+ else if (this.currentStage === undefined) {
1177
+ this.currentStage = 'loading';
1178
+ this.loadStartTime = Date.now();
1179
+ this.loadProgress = { current, total };
1180
+ this.reportProgress(config);
1181
+ }
1182
+ }
790
1183
  }
791
1184
  reportProgress(config) {
792
1185
  if (!config.onProgress || !this.generationStartTime)
@@ -899,4 +1292,47 @@ export class DiffusionServerManager extends ServerManager {
899
1292
  }
900
1293
  }
901
1294
  }
1295
+ function medianOf(values) {
1296
+ const sorted = [...values].sort((a, b) => a - b);
1297
+ const mid = Math.floor(sorted.length / 2);
1298
+ return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1299
+ }
1300
+ function countForcedFlags(combo) {
1301
+ let count = 0;
1302
+ if (combo.clipOnCpu !== undefined)
1303
+ count++;
1304
+ if (combo.vaeOnCpu !== undefined)
1305
+ count++;
1306
+ if (combo.offloadToCpu !== undefined)
1307
+ count++;
1308
+ if (combo.diffusionFlashAttention !== undefined)
1309
+ count++;
1310
+ return count;
1311
+ }
1312
+ export function pickRecommended(runs, tolerancePct) {
1313
+ const bySize = new Map();
1314
+ for (const run of runs) {
1315
+ if (run.status !== 'ok' || run.timeTakenMs === undefined) {
1316
+ continue;
1317
+ }
1318
+ const key = `${run.size.width}x${run.size.height}`;
1319
+ const list = bySize.get(key);
1320
+ if (list) {
1321
+ list.push(run);
1322
+ }
1323
+ else {
1324
+ bySize.set(key, [run]);
1325
+ }
1326
+ }
1327
+ const recommended = {};
1328
+ for (const [key, okRuns] of bySize) {
1329
+ const fastest = okRuns.reduce((a, b) => (b.timeTakenMs < a.timeTakenMs ? b : a));
1330
+ const threshold = fastest.timeTakenMs * (1 + tolerancePct / 100);
1331
+ const winner = okRuns
1332
+ .filter((run) => run.timeTakenMs <= threshold)
1333
+ .sort((a, b) => countForcedFlags(a.combo) - countForcedFlags(b.combo) || a.timeTakenMs - b.timeTakenMs)[0];
1334
+ recommended[key] = winner.combo;
1335
+ }
1336
+ return recommended;
1337
+ }
902
1338
  //# sourceMappingURL=DiffusionServerManager.js.map