opencomic-ai-bin 1.0.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/index.mts ADDED
@@ -0,0 +1,922 @@
1
+ import fs from 'fs';
2
+ import fsp from 'fs/promises';
3
+ import p from 'path';
4
+ import crypto from 'crypto';
5
+ import {spawn} from 'child_process';
6
+
7
+ const ___dirname = typeof __dirname !== 'undefined' ? __dirname : import.meta.dirname;
8
+
9
+ export type Formats =
10
+ | 'bmp'
11
+ | 'dib'
12
+ | 'exr'
13
+ | 'hdr'
14
+ | 'jpe'
15
+ | 'jpeg'
16
+ | 'jpg'
17
+ | 'pbm'
18
+ | 'pgm'
19
+ | 'pic'
20
+ | 'png'
21
+ | 'pnm'
22
+ | 'ppm'
23
+ | 'pxm'
24
+ | 'ras'
25
+ | 'sr'
26
+ | 'tif'
27
+ | 'tiff'
28
+ | 'webp';
29
+
30
+ export type ModelType = 'upscale' | 'descreen' | 'artifact-removal';
31
+ export type Upscaler = 'realcugan' | 'waifu2x' | 'upscayl';
32
+ export type Speed = 'Very Fast' | 'Fast' | 'Moderate' | 'Slow' | 'Very Slow';
33
+
34
+ export interface UpscalerObject {
35
+ name: string;
36
+ binary: string;
37
+ platforms: Partial<Record<NodeJS.Platform, Partial<Record<NodeJS.Architecture, string>>>>;
38
+ }
39
+
40
+ const upscalers: Record<Upscaler, UpscalerObject> = {
41
+ realcugan: {
42
+ name: 'RealCUGAN NCNN Vulkan',
43
+ binary: 'realcugan-ncnn-vulkan',
44
+ platforms: {
45
+ darwin: {
46
+ x64: 'mac/x64/realcugan/realcugan-ncnn-vulkan.app',
47
+ arm64: 'mac/arm64/realcugan/realcugan-ncnn-vulkan.app',
48
+ },
49
+ win32: {
50
+ x64: 'win/x64/realcugan/realcugan-ncnn-vulkan.exe',
51
+ },
52
+ linux: {
53
+ x64: 'linux/x64/realcugan/realcugan-ncnn-vulkan',
54
+ arm64: 'linux/arm64/realcugan/realcugan-ncnn-vulkan',
55
+ },
56
+ },
57
+ },
58
+ waifu2x: {
59
+ name: 'Waifu2x NCNN Vulkan',
60
+ binary: 'waifu2x-ncnn-vulkan',
61
+ platforms: {
62
+ darwin: {
63
+ x64: 'mac/x64/waifu2x/waifu2x-ncnn-vulkan.app',
64
+ arm64: 'mac/arm64/waifu2x/waifu2x-ncnn-vulkan.app',
65
+ },
66
+ win32: {
67
+ x64: 'win/x64/waifu2x/waifu2x-ncnn-vulkan.exe',
68
+ },
69
+ linux: {
70
+ x64: 'linux/x64/waifu2x/waifu2x-ncnn-vulkan',
71
+ arm64: 'linux/arm64/waifu2x/waifu2x-ncnn-vulkan',
72
+ },
73
+ },
74
+ },
75
+ upscayl: {
76
+ name: 'Upscayl',
77
+ binary: 'upscayl-bin',
78
+ platforms: {
79
+ darwin: {
80
+ x64: 'mac/x64/upscayl/upscayl-bin.app',
81
+ arm64: 'mac/arm64/upscayl/upscayl-bin.app',
82
+ },
83
+ win32: {
84
+ x64: 'win/x64/upscayl/upscayl-bin.exe',
85
+ },
86
+ linux: {
87
+ x64: 'linux/x64/upscayl/upscayl-bin',
88
+ },
89
+ },
90
+ },
91
+ }
92
+
93
+ export interface ModelObject {
94
+ key?: Model,
95
+ name: string;
96
+ upscaler: Upscaler;
97
+ type?: ModelType;
98
+ scales: number[];
99
+ noise: number[] | undefined;
100
+ latency: number;
101
+ speed?: Speed;
102
+ folder: string;
103
+ path?: string;
104
+ files: string[];
105
+ supportCurrentPlatform?: boolean;
106
+ }
107
+
108
+ let models: Record<ModelType, Record<string, ModelObject>> = {
109
+ upscale: {
110
+ /*'realcugan-nose': {
111
+ name: 'RealCUGAN NoSE',
112
+ upscaper: 'realcugan',
113
+ scales: [2],
114
+ noise: [0, 3],
115
+ latency: 0,
116
+ folder: './realcugan/models-nose',
117
+ files: [],
118
+ },
119
+ 'realcugan-pro': {
120
+ name: 'RealCUGAN Pro',
121
+ upscaper: 'realcugan',
122
+ scales: [1, 2, 3],
123
+ noise: [0, 3],
124
+ latency: 0,
125
+ folder: './realcugan/models-pro',
126
+ files: [],
127
+ },*/
128
+ 'realcugan': {
129
+ name: 'RealCUGAN',
130
+ upscaler: 'realcugan',
131
+ scales: [/*1, */2, 3, 4],
132
+ noise: [0, 3],
133
+ latency: 1.32,
134
+ folder: './realcugan/models-se',
135
+ files: [
136
+ 'up2x-conservative.bin',
137
+ 'up2x-conservative.param',
138
+ 'up2x-denoise1x.bin',
139
+ 'up2x-denoise1x.param',
140
+ 'up2x-denoise2x.bin',
141
+ 'up2x-denoise2x.param',
142
+ 'up2x-denoise3x.bin',
143
+ 'up2x-denoise3x.param',
144
+ 'up2x-no-denoise.bin',
145
+ 'up2x-no-denoise.param',
146
+ 'up3x-conservative.bin',
147
+ 'up3x-conservative.param',
148
+ 'up3x-denoise3x.bin',
149
+ 'up3x-denoise3x.param',
150
+ 'up3x-no-denoise.bin',
151
+ 'up3x-no-denoise.param',
152
+ 'up4x-conservative.bin',
153
+ 'up4x-conservative.param',
154
+ 'up4x-denoise3x.bin',
155
+ 'up4x-denoise3x.param',
156
+ 'up4x-no-denoise.bin',
157
+ 'up4x-no-denoise.param',
158
+ ],
159
+ },
160
+ 'realesr-animevideov3': {
161
+ name: 'RealESR AnimeVideo v3',
162
+ upscaler: 'upscayl',
163
+ scales: [2, 3, 4],
164
+ noise: undefined,
165
+ latency: 1.53,
166
+ folder: './models',
167
+ files: [
168
+ 'realesr-animevideov3-x2.bin',
169
+ 'realesr-animevideov3-x2.param',
170
+ 'realesr-animevideov3-x3.bin',
171
+ 'realesr-animevideov3-x3.param',
172
+ 'realesr-animevideov3-x4.bin',
173
+ 'realesr-animevideov3-x4.param',
174
+ ],
175
+ },
176
+ 'realesrgan-x4plus': {
177
+ name: 'RealESRGAN x4 Plus',
178
+ upscaler: 'upscayl',
179
+ scales: [2, 3, 4],
180
+ noise: undefined,
181
+ latency: 7.5,
182
+ folder: './models',
183
+ files: [
184
+ 'realesrgan-x4plus.bin',
185
+ 'realesrgan-x4plus.param',
186
+ ],
187
+ },
188
+ 'realesrgan-x4plus-anime': {
189
+ name: 'RealESRGAN x4 Plus Anime',
190
+ upscaler: 'upscayl',
191
+ scales: [2, 3, 4],
192
+ noise: undefined,
193
+ latency: 3.26,
194
+ folder: './models',
195
+ files: [
196
+ 'realesrgan-x4plus-anime.bin',
197
+ 'realesrgan-x4plus-anime.param',
198
+ ],
199
+ },
200
+ 'realesrnet-x4plus': {
201
+ name: 'RealESRNet x4 Plus',
202
+ upscaler: 'upscayl',
203
+ scales: [2, 3, 4],
204
+ noise: undefined,
205
+ latency: 7.17,
206
+ folder: './models',
207
+ files: [
208
+ 'realesrnet-x4plus.bin',
209
+ 'realesrnet-x4plus.param',
210
+ ],
211
+ },
212
+ 'waifu2x-models-cunet': {
213
+ name: 'Waifu2x CUnet',
214
+ upscaler: 'waifu2x',
215
+ scales: [1, 2, 4, 8, 16, 32],
216
+ noise: [0, 1, 2, 3],
217
+ latency: 2.92,
218
+ folder: './waifu2x/models-cunet',
219
+ files: [
220
+ 'noise0_model.bin',
221
+ 'noise0_model.param',
222
+ 'noise0_scale2.0x_model.bin',
223
+ 'noise0_scale2.0x_model.param',
224
+ 'noise1_model.bin',
225
+ 'noise1_model.param',
226
+ 'noise1_scale2.0x_model.bin',
227
+ 'noise1_scale2.0x_model.param',
228
+ 'noise2_model.bin',
229
+ 'noise2_model.param',
230
+ 'noise2_scale2.0x_model.bin',
231
+ 'noise2_scale2.0x_model.param',
232
+ 'noise3_model.bin',
233
+ 'noise3_model.param',
234
+ 'noise3_scale2.0x_model.bin',
235
+ 'noise3_scale2.0x_model.param',
236
+ 'scale2.0x_model.bin',
237
+ 'scale2.0x_model.param',
238
+ ],
239
+ },
240
+ 'waifu2x-models-upconv': {
241
+ name: 'Waifu2x UpConv',
242
+ upscaler: 'waifu2x',
243
+ scales: [1, 2, 4, 8, 16, 32],
244
+ noise: [0, 1, 2, 3],
245
+ latency: 0.8,
246
+ folder: './waifu2x/models-upconv_7_anime_style_art_rgb',
247
+ files: [
248
+ 'noise0_scale2.0x_model.bin',
249
+ 'noise0_scale2.0x_model.param',
250
+ 'noise1_scale2.0x_model.bin',
251
+ 'noise1_scale2.0x_model.param',
252
+ 'noise2_scale2.0x_model.bin',
253
+ 'noise2_scale2.0x_model.param',
254
+ 'noise3_scale2.0x_model.bin',
255
+ 'noise3_scale2.0x_model.param',
256
+ 'scale2.0x_model.bin',
257
+ 'scale2.0x_model.param',
258
+ ],
259
+ },
260
+ '4x-WTP-ColorDS': {
261
+ name: 'WTP ColorDS',
262
+ upscaler: 'upscayl',
263
+ scales: [2, 3, 4],
264
+ noise: undefined,
265
+ latency: 7.62,
266
+ folder: './models',
267
+ files: [
268
+ '4x-WTP-ColorDS.bin',
269
+ '4x-WTP-ColorDS.param',
270
+ ],
271
+ },
272
+ 'remacri-4x': {
273
+ name: 'Remacri',
274
+ upscaler: 'upscayl',
275
+ scales: [2, 3, 4],
276
+ noise: undefined,
277
+ latency: 7.82,
278
+ folder: './models',
279
+ files: [
280
+ 'remacri-4x.bin',
281
+ 'remacri-4x.param',
282
+ ],
283
+ },
284
+ 'ultramix-balanced-4x': {
285
+ name: 'Ultramix Balanced',
286
+ upscaler: 'upscayl',
287
+ scales: [2, 3, 4],
288
+ noise: undefined,
289
+ latency: 10,
290
+ folder: './models',
291
+ files: [
292
+ 'ultramix-balanced-4x.bin',
293
+ 'ultramix-balanced-4x.param',
294
+ ],
295
+ },
296
+ 'ultrasharp-4x': {
297
+ name: 'Ultrasharp',
298
+ upscaler: 'upscayl',
299
+ scales: [2, 3, 4],
300
+ noise: undefined,
301
+ latency: 7.46,
302
+ folder: './models',
303
+ files: [
304
+ 'ultrasharp-4x.bin',
305
+ 'ultrasharp-4x.param',
306
+ ],
307
+ },
308
+ /*'2x-AnimeSharpV4_RCAN_fp16_op17': {
309
+ name: 'AnimeSharpV4 RCAN',
310
+ upscaler: 'upscayl',
311
+ scales: [2],
312
+ noise: undefined,
313
+ latency: 0,
314
+ folder: './models',
315
+ files: [
316
+ '2x-AnimeSharpV4_RCAN_fp16_op17.bin',
317
+ '2x-AnimeSharpV4_RCAN_fp16_op17.param',
318
+ ],
319
+ },*/
320
+ '4xHFA2k': {
321
+ name: 'HFA2k',
322
+ upscaler: 'upscayl',
323
+ scales: [2, 3, 4],
324
+ noise: undefined,
325
+ latency: 7.39,
326
+ folder: './models',
327
+ files: [
328
+ '4xHFA2k.bin',
329
+ '4xHFA2k.param',
330
+ ],
331
+ },
332
+ '4xLSDIRCompactC3': {
333
+ name: 'LSDIR Compact C3',
334
+ upscaler: 'upscayl',
335
+ scales: [2, 3, 4],
336
+ noise: undefined,
337
+ latency: 1.37,
338
+ folder: './models',
339
+ files: [
340
+ '4xLSDIRCompactC3.bin',
341
+ '4xLSDIRCompactC3.param',
342
+ ],
343
+ },
344
+ '4xLSDIRplusC': {
345
+ name: 'LSDIR Plus C',
346
+ upscaler: 'upscayl',
347
+ scales: [2, 3, 4],
348
+ noise: undefined,
349
+ latency: 8.3,
350
+ folder: './models',
351
+ files: [
352
+ '4xLSDIRplusC.bin',
353
+ '4xLSDIRplusC.param',
354
+ ],
355
+ },
356
+ '4x_NMKD-Siax_200k': {
357
+ name: 'NMKD Siax',
358
+ upscaler: 'upscayl',
359
+ scales: [2, 3, 4],
360
+ noise: undefined,
361
+ latency: 7.24,
362
+ folder: './models',
363
+ files: [
364
+ '4x_NMKD-Siax_200k.bin',
365
+ '4x_NMKD-Siax_200k.param',
366
+ ],
367
+ },
368
+ '4xNomos8kSC': {
369
+ name: 'Nomos 8k SC',
370
+ upscaler: 'upscayl',
371
+ scales: [2, 3, 4],
372
+ noise: undefined,
373
+ latency: 7.11,
374
+ folder: './models',
375
+ files: [
376
+ '4xNomos8kSC.bin',
377
+ '4xNomos8kSC.param',
378
+ ],
379
+ },
380
+ 'RealESRGAN_General_WDN_x4_v3': {
381
+ name: 'RealESRGAN General WDN v3',
382
+ upscaler: 'upscayl',
383
+ scales: [2, 3, 4],
384
+ noise: undefined,
385
+ latency: 1.47,
386
+ folder: './models',
387
+ files: [
388
+ 'RealESRGAN_General_WDN_x4_v3.bin',
389
+ 'RealESRGAN_General_WDN_x4_v3.param',
390
+ ],
391
+ },
392
+ 'RealESRGAN_General_x4_v3': {
393
+ name: 'RealESRGAN General v3',
394
+ upscaler: 'upscayl',
395
+ scales: [2, 3, 4],
396
+ noise: undefined,
397
+ latency: 1.45,
398
+ folder: './models',
399
+ files: [
400
+ 'RealESRGAN_General_x4_v3.bin',
401
+ 'RealESRGAN_General_x4_v3.param',
402
+ ],
403
+ },
404
+ 'uniscale_restore_x4': {
405
+ name: 'Uniscale Restore x4',
406
+ upscaler: 'upscayl',
407
+ scales: [2, 3, 4],
408
+ noise: undefined,
409
+ latency: 7.01,
410
+ folder: './models',
411
+ files: [
412
+ 'uniscale_restore_x4.bin',
413
+ 'uniscale_restore_x4.param',
414
+ ],
415
+ },
416
+ 'unknown-2.0.1': {
417
+ name: 'Unknown 2.0.1',
418
+ upscaler: 'upscayl',
419
+ scales: [2, 3, 4],
420
+ noise: undefined,
421
+ latency: 7.33,
422
+ folder: './models',
423
+ files: [
424
+ 'unknown-2.0.1.bin',
425
+ 'unknown-2.0.1.param',
426
+ ],
427
+ },
428
+ },
429
+ descreen: {
430
+ '1x_halftone_patch_060000_G': {
431
+ name: 'Halftone Patch 060000 G',
432
+ upscaler: 'upscayl',
433
+ scales: [1],
434
+ noise: undefined,
435
+ latency: 6.71,
436
+ folder: './models',
437
+ files: [
438
+ '1x_halftone_patch_060000_G.bin',
439
+ '1x_halftone_patch_060000_G.param',
440
+ ],
441
+ },
442
+ '1x_wtp_descreenton_compact': {
443
+ name: 'WTP DescreenTon Compact',
444
+ upscaler: 'upscayl',
445
+ scales: [1],
446
+ noise: undefined,
447
+ latency: 0.5,
448
+ folder: './models',
449
+ files: [
450
+ '1x_wtp_descreenton_compact.bin',
451
+ '1x_wtp_descreenton_compact.param',
452
+ ],
453
+ },
454
+ },
455
+ 'artifact-removal': {
456
+ '1x_NMKD-Jaywreck3-Lite_320k': {
457
+ name: 'NMKD Jaywreck3 Lite',
458
+ upscaler: 'upscayl',
459
+ scales: [1],
460
+ noise: undefined,
461
+ latency: 3.66,
462
+ folder: './models',
463
+ files: [
464
+ '1x_NMKD-Jaywreck3-Lite_320k.bin',
465
+ '1x_NMKD-Jaywreck3-Lite_320k.param',
466
+ ],
467
+ },
468
+ '1x_NMKD-Jaywreck3-Soft-Lite_320k': {
469
+ name: 'NMKD Jaywreck3 Soft Lite',
470
+ upscaler: 'upscayl',
471
+ scales: [1],
472
+ noise: undefined,
473
+ latency: 3.66,
474
+ folder: './models',
475
+ files: [
476
+ '1x_NMKD-Jaywreck3-Soft-Lite_320k.bin',
477
+ '1x_NMKD-Jaywreck3-Soft-Lite_320k.param',
478
+ ],
479
+ },
480
+ '1x-SaiyaJin-DeJpeg': {
481
+ name: 'SaiyaJin DeJpeg',
482
+ upscaler: 'upscayl',
483
+ scales: [1],
484
+ noise: undefined,
485
+ latency: 8.55,
486
+ folder: './models',
487
+ files: [
488
+ '1x-SaiyaJin-DeJpeg.bin',
489
+ '1x-SaiyaJin-DeJpeg.param',
490
+ ],
491
+ },
492
+ },
493
+ };
494
+
495
+ const modelSpeed = (latency: number): Speed => {
496
+
497
+ if(latency <= 1)
498
+ return 'Very Fast';
499
+ else if(latency <= 4)
500
+ return 'Fast';
501
+ else if(latency <= 7)
502
+ return 'Moderate';
503
+ else if(latency <= 10)
504
+ return 'Slow';
505
+ else
506
+ return 'Very Slow'; // Not used yet
507
+
508
+ }
509
+
510
+ const parseModels = (models: Record<string, ModelObject>, type: ModelType): Record<string, ModelObject> => {
511
+
512
+ const parsedModels: Record<string, ModelObject> = {};
513
+
514
+ for(const [key, model] of Object.entries(models))
515
+ {
516
+ parsedModels[key] = {
517
+ key,
518
+ type,
519
+ ...model,
520
+ speed: modelSpeed(model.latency),
521
+ supportCurrentPlatform: upscalers[model.upscaler].platforms[process.platform]?.[process.arch] ? true : false,
522
+ };
523
+ }
524
+
525
+ return parsedModels;
526
+ }
527
+
528
+ models = {
529
+ upscale: parseModels(models.upscale, 'upscale'),
530
+ descreen: parseModels(models.descreen, 'descreen'),
531
+ 'artifact-removal': parseModels(models['artifact-removal'], 'artifact-removal'),
532
+ };
533
+
534
+ export type Model = keyof typeof models.upscale & keyof typeof models.descreen & keyof typeof models['artifact-removal'];
535
+
536
+ export interface OpenComicAIOptions {
537
+ model?: Model;
538
+ noise?: 0 | 1 | 2 | 3;
539
+ scale?: number;
540
+ // format?: 'jpg' | 'png' | 'webp';
541
+ tileSize?: number;
542
+ gpuId?: string;
543
+ threads?: number;
544
+ tta?: boolean;
545
+ }
546
+
547
+ export interface Downloading {
548
+ start?: () => void;
549
+ progress?: (progress: number) => void;
550
+ end?: () => void;
551
+ }
552
+
553
+ const DEFAULT_MODEL: Model = 'realcugan';
554
+ const DOWNLOADING_URL = 'https://raw.githubusercontent.com/ollm/opencomic-ai-models/db60a923bfab0afccee4e478b2ca6666ec75fdb4/models/';
555
+
556
+ const modelsList: Model[] = [...Object.keys(models.upscale) as Model[], ...Object.keys(models.descreen) as Model[], ...Object.keys(models['artifact-removal']) as Model[]];
557
+ const modelsTypeList: Record<ModelType, Model[]> = {
558
+ upscale: Object.keys(models.upscale) as Model[],
559
+ descreen: Object.keys(models.descreen) as Model[],
560
+ 'artifact-removal': Object.keys(models['artifact-removal']) as Model[],
561
+ };
562
+
563
+ export default class OpenComicAI {
564
+
565
+ public static models = models;
566
+ public static modelsList = modelsList;
567
+ public static modelsTypeList = modelsTypeList;
568
+ public static modelsPath: string | undefined = undefined;
569
+
570
+ private static resolve = (path: string): string => {
571
+
572
+ if(!p.isAbsolute(path))
573
+ {
574
+ if(typeof module !== 'undefined')
575
+ path = p.resolve(module?.parent?.path ?? '', path);
576
+ else
577
+ path = p.resolve(import.meta?.dirname ?? '', path);
578
+ }
579
+
580
+ return p.normalize(path);
581
+
582
+ }
583
+
584
+ public static setModelsPath = (path: string): void => {
585
+
586
+ path = OpenComicAI.resolve(path);
587
+
588
+ if(!fs.existsSync(path))
589
+ throw new Error(`Models path does not exist: ${path}`);
590
+
591
+ OpenComicAI.modelsPath = path;
592
+
593
+ }
594
+
595
+ public static model = (model: Model = DEFAULT_MODEL): ModelObject => {
596
+
597
+ if(!modelsList.includes(model as Model))
598
+ throw new Error(`Model not found: ${model}`);
599
+
600
+ const _model = model as Model;
601
+ const modelInfo = models.upscale[_model] || models.descreen[_model] || models['artifact-removal'][_model];
602
+ const modelType = modelInfo.type as string;
603
+
604
+ return {
605
+ ...modelInfo,
606
+ path: OpenComicAI.modelsPath ? p.join(OpenComicAI.modelsPath, modelType, modelInfo.folder) : p.join(modelType, modelInfo.folder),
607
+ };
608
+ }
609
+
610
+ public static binary = (model: Model): string => {
611
+
612
+ if(!modelsList.includes(model as Model))
613
+ throw new Error(`Model not found: ${model}`);
614
+
615
+ const base = p.join(___dirname, '..');
616
+
617
+ const upscaler = OpenComicAI.model(model as Model).upscaler;
618
+ const result = upscalers[upscaler].platforms[process.platform]?.[process.arch] ?? upscalers[upscaler].platforms[process.platform]?.x64 ?? upscalers[upscaler].platforms.linux?.x64 ?? '';
619
+
620
+ return p.join(base, result);
621
+
622
+ }
623
+
624
+ private static download = async (fileUrl: string, destPath: string, downloading?: Downloading | false): Promise<void> => {
625
+
626
+ const response = await fetch(fileUrl);
627
+
628
+ if(response.ok)
629
+ {
630
+ const contentLength = response.headers.get('content-length');
631
+ const len = contentLength ? parseInt(contentLength, 10) : 0;
632
+
633
+ if(!response.body)
634
+ {
635
+ console.error('Response body is null', fileUrl);
636
+ return;
637
+ }
638
+
639
+ const reader = response.body.getReader();
640
+ const fileStream = fs.createWriteStream(destPath);
641
+
642
+ let downloaded = 0;
643
+
644
+ while(true)
645
+ {
646
+ const {done, value} = await reader.read();
647
+
648
+ if(done)
649
+ break;
650
+
651
+ fileStream.write(value);
652
+ downloaded += value.byteLength;
653
+
654
+ if(downloading && downloading?.progress)
655
+ downloading?.progress(downloaded / len);
656
+ }
657
+
658
+ if(downloading && downloading?.progress)
659
+ downloading?.progress(1);
660
+
661
+ let resolve;
662
+
663
+ const promise = new Promise((_resolve) => {
664
+
665
+ resolve = _resolve;
666
+
667
+ });
668
+
669
+ fileStream.end(resolve);
670
+
671
+ await promise;
672
+ }
673
+ else
674
+ {
675
+ throw new Error(`Failed to download file: ${fileUrl}, status: ${response.status}`);
676
+ }
677
+
678
+ }
679
+
680
+ private static getModels = async (steps: OpenComicAIOptions[], downloading?: Downloading | false): Promise<void> => {
681
+
682
+ const toGetModels: Map<string, string> = new Map();
683
+
684
+ for(const step of steps)
685
+ {
686
+ const modelInfo = OpenComicAI.model(step.model as Model || DEFAULT_MODEL);
687
+
688
+ for(const file of modelInfo.files)
689
+ {
690
+ const filePath = p.join(modelInfo.path as string, file);
691
+
692
+ if(!fs.existsSync(filePath))
693
+ {
694
+ const base = new URL(`${modelInfo.type}/`, DOWNLOADING_URL);
695
+ const folder = new URL(`${modelInfo.folder}/`, base);
696
+ const fileUrl = new URL(file, folder).href;
697
+
698
+ toGetModels.set(fileUrl, filePath);
699
+ }
700
+ }
701
+ }
702
+
703
+ if(toGetModels.size > 0)
704
+ {
705
+ if(downloading && downloading.start)
706
+ downloading.start();
707
+
708
+ const entries = toGetModels.entries();
709
+ const binNum = [...toGetModels.values()].reduce((accumulator, destPath) => {
710
+
711
+ return accumulator + (destPath.endsWith('.bin') ? 1 : 0);
712
+
713
+ }, 0);
714
+
715
+ let index = -1;
716
+
717
+ for(const [fileUrl, destPath] of entries)
718
+ {
719
+ const folder = p.dirname(destPath);
720
+
721
+ if(!fs.existsSync(folder))
722
+ await fsp.mkdir(folder, {recursive: true});
723
+
724
+ let _downloading = {};
725
+
726
+ if(destPath.endsWith('.bin'))
727
+ {
728
+ index++;
729
+
730
+ _downloading = {
731
+ progress: (progress: number) => {
732
+
733
+ if(downloading && downloading.progress)
734
+ downloading.progress(((index + progress) / binNum));
735
+
736
+ }
737
+ };
738
+ }
739
+
740
+ await OpenComicAI.download(fileUrl, destPath, _downloading);
741
+ }
742
+
743
+ if(downloading && downloading.end)
744
+ downloading.end();
745
+ }
746
+
747
+ }
748
+
749
+ public static pipeline = async (source: string, dest: string, steps: OpenComicAIOptions[], progress?: ((progress?: number) => void) | false, downloading?: Downloading | false): Promise<string> => {
750
+
751
+ if(!OpenComicAI.modelsPath)
752
+ throw new Error('Models path is not set, use OpenComicAI.setModelsPath to set it before calling pipe.');
753
+
754
+ await OpenComicAI.getModels(steps, downloading);
755
+
756
+ const parsed = p.parse(dest);
757
+ let prevIntermediateDest: string = '';
758
+
759
+ for(let i = 0, len = steps.length; i < len; i++)
760
+ {
761
+ const step = steps[i];
762
+ const intermediateDest = i < len - 1 ? p.join(p.dirname(dest), `${crypto.randomUUID()}${parsed.ext}`) : dest;
763
+
764
+ const _progress = (p: number | undefined) => {
765
+
766
+ if(!progress)
767
+ return;
768
+
769
+ const overallProgress = (i + (p ?? 0)) / len;
770
+ progress(overallProgress);
771
+
772
+ }
773
+
774
+ if(progress)
775
+ progress(i / len);
776
+
777
+ await OpenComicAI.image(source, intermediateDest, step, _progress);
778
+
779
+ if(prevIntermediateDest && fs.existsSync(prevIntermediateDest))
780
+ await fsp.unlink(prevIntermediateDest);
781
+
782
+ source = intermediateDest;
783
+ prevIntermediateDest = OpenComicAI.resolve(intermediateDest);
784
+ }
785
+
786
+ return source;
787
+
788
+ }
789
+
790
+ private static closest = (array: number[], target: number): number => {
791
+
792
+ return array.reduce((prev, curr) => {
793
+ return Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev;
794
+ });
795
+
796
+ }
797
+
798
+ private static image = async (source: string, dest: string, options?: OpenComicAIOptions, progress?: ((progress?: number) => void) | false): Promise<string> => {
799
+
800
+ options = {...options};
801
+
802
+ source = OpenComicAI.resolve(source);
803
+ dest = OpenComicAI.resolve(dest);
804
+
805
+ const {dir, name} = p.parse(dest);
806
+
807
+ if(!options.model)
808
+ options.model = DEFAULT_MODEL;
809
+
810
+ if(!modelsList.includes(options.model as Model))
811
+ throw new Error(`Model not found: ${options.model}`);
812
+
813
+ const folder = p.dirname(dest);
814
+
815
+ if(!fs.existsSync(folder))
816
+ await fsp.mkdir(folder, {recursive: true});
817
+
818
+ const binary = OpenComicAI.binary(options.model);
819
+ const modelInfo = OpenComicAI.model(options.model);
820
+
821
+ const model = options.model;
822
+ // const format = options.format ?? p.extname(source).slice(1);
823
+ const threads: number | boolean = options.threads ? +options.threads : false;
824
+ let noise: number | boolean = options.noise ? +options.noise : false;
825
+ let scale: number | boolean = options.scale ? +options.scale : false;
826
+ const tileSize: string | boolean = options.tileSize?.toString() ?? false;
827
+ const gpuId: string | boolean = options.gpuId ?? false;
828
+ const tta: boolean = !!options.tta;
829
+
830
+ if(noise !== false && !modelInfo?.noise?.includes(noise))
831
+ noise = modelInfo?.noise ? OpenComicAI.closest(modelInfo.noise, noise) : false;
832
+
833
+ if(scale && !modelInfo.scales.includes(scale))
834
+ scale = OpenComicAI.closest(modelInfo.scales, scale);
835
+
836
+ const args: string[] = [
837
+ '-i', source,
838
+ '-o', dest,
839
+ '-m', modelInfo?.path as string,
840
+ // ...(format ? ['-f', format] : []),
841
+ ...(threads ? ['-j', `${threads}:${threads}:${threads}`] : []),
842
+ ...(noise !== false ? ['-n', noise.toString()] : []),
843
+ ...(scale ? ['-s', scale.toString()] : []),
844
+ ...(tileSize ? ['-t', tileSize] : []),
845
+ ...(gpuId ? ['-g', gpuId] : []),
846
+ ...(tta ? ['-x'] : []),
847
+ ];
848
+
849
+ switch(modelInfo.upscaler)
850
+ {
851
+ case 'waifu2x':
852
+
853
+ // No additional args for waifu2x
854
+
855
+ break;
856
+
857
+ case 'realcugan':
858
+
859
+ // No additional args for realcugan
860
+
861
+ break;
862
+
863
+ case 'upscayl':
864
+
865
+ args.push('-n', model);
866
+
867
+ break;
868
+ }
869
+
870
+ let result = '';
871
+
872
+ // console.log(`Executing: ${binary} ${args.join(' ')}`);
873
+
874
+ return new Promise<string>((resolve, reject) => {
875
+
876
+ const proc = spawn(binary, args);
877
+
878
+ proc.stderr.on('data', (data) => {
879
+
880
+ data = data.toString();
881
+ result += data;
882
+
883
+ if(!progress)
884
+ return;
885
+
886
+ const match = data.match(/([\d\.\,]+)%/);
887
+
888
+ if(match)
889
+ {
890
+ const percent = +(match[1].replace(',', '.'));
891
+ const _progress = Math.min(Math.max(percent / 100, 0), 1);
892
+ progress(_progress);
893
+ }
894
+
895
+ });
896
+
897
+ proc.on('error', (error) => {
898
+
899
+ reject(error);
900
+
901
+ });
902
+
903
+ proc.on('close', (code) => {
904
+
905
+ if(code === 0)
906
+ {
907
+ resolve(dest);
908
+ return;
909
+ }
910
+
911
+ const lines = result.split('\n').filter(line => line.trim() !== '');
912
+ const lastLine = lines[lines.length - 1] || '';
913
+ const lastLines = lines.slice(-20).join('\n');
914
+
915
+ console.error(lastLines);
916
+ reject(new Error(`Process exited with code ${code}: ${lastLine}`));
917
+
918
+ });
919
+ });
920
+
921
+ }
922
+ }