@doufunao123/asset-gateway 0.11.0 → 0.11.1
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/dist/index.js +520 -59
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command10 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/auth.ts
|
|
7
7
|
import { existsSync as existsSync2, unlinkSync } from "fs";
|
|
@@ -70,7 +70,7 @@ function normalizeError(error2) {
|
|
|
70
70
|
|
|
71
71
|
// src/meta.ts
|
|
72
72
|
var CLI_NAME = "asset-gateway";
|
|
73
|
-
var CLI_VERSION = "0.
|
|
73
|
+
var CLI_VERSION = "0.11.1";
|
|
74
74
|
var CLI_DESCRIPTION = "Universal asset generation gateway CLI";
|
|
75
75
|
var DEFAULT_GATEWAY_URL = "https://upload.xiaomao.chat";
|
|
76
76
|
|
|
@@ -174,6 +174,8 @@ var GatewayClient = class {
|
|
|
174
174
|
this.baseUrl = baseUrl;
|
|
175
175
|
this.token = token;
|
|
176
176
|
}
|
|
177
|
+
baseUrl;
|
|
178
|
+
token;
|
|
177
179
|
async get(path) {
|
|
178
180
|
return this.request("GET", path);
|
|
179
181
|
}
|
|
@@ -356,13 +358,17 @@ function mask(token) {
|
|
|
356
358
|
|
|
357
359
|
// src/commands/describe.ts
|
|
358
360
|
import { Command as Command2 } from "commander";
|
|
361
|
+
|
|
362
|
+
// src/describe-schemas.ts
|
|
359
363
|
var SCHEMAS = {
|
|
360
364
|
auth: {
|
|
361
365
|
description: "Credential management",
|
|
362
366
|
subcommands: {
|
|
363
367
|
set: {
|
|
364
368
|
description: "Save token and gateway URL locally",
|
|
365
|
-
params: {
|
|
369
|
+
params: {
|
|
370
|
+
token: { type: "string", required: true, description: "API token or admin token" }
|
|
371
|
+
}
|
|
366
372
|
},
|
|
367
373
|
status: { description: "Show current authentication status" },
|
|
368
374
|
clear: { description: "Remove saved credentials" }
|
|
@@ -372,91 +378,222 @@ var SCHEMAS = {
|
|
|
372
378
|
description: "Generate assets via the gateway",
|
|
373
379
|
subcommands: {
|
|
374
380
|
image: {
|
|
375
|
-
description: "Generate an image
|
|
381
|
+
description: "Generate or edit an image (Gemini / GPT Image / Grok)",
|
|
376
382
|
params: {
|
|
377
|
-
"--prompt": { type: "string", required: true, description: "Image
|
|
378
|
-
"--provider": { type: "string", required: false
|
|
379
|
-
"--transparent": { type: "bool",
|
|
380
|
-
"--model": { type: "string"
|
|
381
|
-
"--size": { type: "string",
|
|
382
|
-
"--
|
|
383
|
+
"--prompt": { type: "string", required: true, description: "Image prompt" },
|
|
384
|
+
"--provider": { type: "string", required: false },
|
|
385
|
+
"--transparent": { type: "bool", description: "Transparent background" },
|
|
386
|
+
"--model": { type: "string" },
|
|
387
|
+
"--size": { type: "string", description: 'e.g. "1024x1024"' },
|
|
388
|
+
"--input": { type: "string", description: "Image URL for editing" },
|
|
389
|
+
"--ref": { type: "string[]", description: "Reference image URLs (repeatable)" },
|
|
390
|
+
"--edit-mode": { type: "string", description: "edit | inpaint | restyle | expand" },
|
|
391
|
+
"--session": { type: "string", description: "Multi-turn session id" },
|
|
392
|
+
"--output-dir": { type: "string", default: "." }
|
|
383
393
|
}
|
|
384
394
|
},
|
|
385
395
|
video: {
|
|
386
|
-
description: "
|
|
396
|
+
description: "Text or image-to-video (Grok)",
|
|
397
|
+
params: {
|
|
398
|
+
"--prompt": { type: "string", required: true },
|
|
399
|
+
"--provider": { type: "string" },
|
|
400
|
+
"--input": { type: "string", description: "Image URL for I2V" },
|
|
401
|
+
"--output-dir": { type: "string", default: "." }
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
batch: {
|
|
405
|
+
description: "Batch image (etc.) generation; optional sprite compose",
|
|
387
406
|
params: {
|
|
388
|
-
"--prompt": { type: "string", required: true, description: "
|
|
389
|
-
"--
|
|
390
|
-
"--
|
|
407
|
+
"--prompt": { type: "string[]", required: true, description: "One prompt per frame" },
|
|
408
|
+
"--asset-type": { type: "string", default: "image" },
|
|
409
|
+
"--transparent": { type: "bool" },
|
|
410
|
+
"--size": { type: "string" },
|
|
411
|
+
"--ref": { type: "string[]" },
|
|
412
|
+
"--compose": { type: "string", description: "horizontal | vertical | grid" },
|
|
413
|
+
"--columns": { type: "number" },
|
|
414
|
+
"--frame-size": { type: "string", description: "e.g. 64x64" },
|
|
415
|
+
"--output-dir": { type: "string", default: "." }
|
|
391
416
|
}
|
|
392
417
|
},
|
|
393
418
|
audio: {
|
|
394
|
-
description: "
|
|
419
|
+
description: "BGM/SFX via ElevenLabs sound-generation",
|
|
420
|
+
params: {
|
|
421
|
+
"--prompt": { type: "string", required: true },
|
|
422
|
+
"--type": { type: "string", description: "bgm | sfx" },
|
|
423
|
+
"--duration": { type: "number", description: "Seconds" },
|
|
424
|
+
"--output-dir": { type: "string", default: "." }
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
music: {
|
|
428
|
+
description: "Music via ElevenLabs POST /v1/music (model music_v1)",
|
|
429
|
+
params: {
|
|
430
|
+
"--prompt": { type: "string", required: true },
|
|
431
|
+
"--duration": { type: "number", description: "Seconds; mapped to music_length_ms (3s\u2013600s)" },
|
|
432
|
+
"--force-instrumental": { type: "bool", description: "Optional; passed to provider" },
|
|
433
|
+
"--output-format": { type: "string", description: "Optional; e.g. mp3_44100_128" },
|
|
434
|
+
"--output-dir": { type: "string", default: "." }
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
tts: {
|
|
438
|
+
description: "TTS: default Qwen3-TTS (voice/language/instructions). ElevenLabs: --provider elevenlabs --voice-id <id>",
|
|
395
439
|
params: {
|
|
396
|
-
"--prompt": { type: "string", required: true
|
|
397
|
-
"--
|
|
398
|
-
"--
|
|
399
|
-
"--
|
|
440
|
+
"--prompt": { type: "string", required: true },
|
|
441
|
+
"--voice": { type: "string", description: "Qwen voice name or custom id", default: "Cherry" },
|
|
442
|
+
"--voice-id": { type: "string", description: "ElevenLabs voice id (with --provider elevenlabs)" },
|
|
443
|
+
"--language": { type: "string", default: "Auto" },
|
|
444
|
+
"--model": { type: "string", default: "qwen3-tts-flash" },
|
|
445
|
+
"--instructions": { type: "string", description: "Instruct-model style control" },
|
|
446
|
+
"--provider": { type: "string", description: "qwen_tts | elevenlabs" },
|
|
447
|
+
"--output-dir": { type: "string", default: "." }
|
|
400
448
|
}
|
|
401
449
|
},
|
|
402
450
|
model: {
|
|
403
|
-
description: "
|
|
451
|
+
description: "3D model generation (Tripo)",
|
|
404
452
|
params: {
|
|
405
|
-
"--image": { type: "string",
|
|
406
|
-
"--prompt": { type: "string"
|
|
407
|
-
"--
|
|
453
|
+
"--image": { type: "string", description: "Reference image URL" },
|
|
454
|
+
"--prompt": { type: "string" },
|
|
455
|
+
"--model-version": { type: "string" },
|
|
456
|
+
"--face-limit": { type: "number" },
|
|
457
|
+
"--pbr": { type: "bool" },
|
|
458
|
+
"--texture-quality": { type: "string" },
|
|
459
|
+
"--auto-size": { type: "bool" },
|
|
460
|
+
"--negative-prompt": { type: "string" },
|
|
461
|
+
"--multiview": { type: "string", description: "Comma-separated 4 view URLs" },
|
|
462
|
+
"--output-dir": { type: "string", default: "." }
|
|
408
463
|
}
|
|
409
464
|
},
|
|
410
465
|
text: {
|
|
411
|
-
description: "
|
|
466
|
+
description: "LLM text via proxy",
|
|
412
467
|
params: {
|
|
413
|
-
"--prompt": { type: "string", required: true
|
|
414
|
-
"--model": { type: "string"
|
|
415
|
-
"--max-tokens": { type: "number"
|
|
416
|
-
"--output-dir": { type: "string", default: "."
|
|
468
|
+
"--prompt": { type: "string", required: true },
|
|
469
|
+
"--model": { type: "string" },
|
|
470
|
+
"--max-tokens": { type: "number" },
|
|
471
|
+
"--output-dir": { type: "string", default: "." }
|
|
417
472
|
}
|
|
418
473
|
}
|
|
419
474
|
}
|
|
420
475
|
},
|
|
421
|
-
|
|
422
|
-
description: "
|
|
476
|
+
process: {
|
|
477
|
+
description: "Image/video post-process on gateway (ImageMagick/ffmpeg/rembg)",
|
|
423
478
|
subcommands: {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
479
|
+
crop: {
|
|
480
|
+
description: "Smart crop (trim / power_of2)",
|
|
481
|
+
params: {
|
|
482
|
+
"--input": { type: "string", required: true, description: "File path or URL" },
|
|
483
|
+
"--mode": { type: "string", default: "tightest" },
|
|
484
|
+
"--output-dir": { type: "string", default: "." }
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
resize: {
|
|
488
|
+
description: "Resize to exact width/height",
|
|
489
|
+
params: {
|
|
490
|
+
"--input": { type: "string", required: true },
|
|
491
|
+
"--width": { type: "number", required: true },
|
|
492
|
+
"--height": { type: "number", required: true },
|
|
493
|
+
"--output-dir": { type: "string", default: "." }
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
compose: {
|
|
497
|
+
description: "Sprite sheet from multiple images",
|
|
498
|
+
params: {
|
|
499
|
+
"--input": { type: "string[]", required: true },
|
|
500
|
+
"--direction": { type: "string", default: "horizontal" },
|
|
501
|
+
"--columns": { type: "number" },
|
|
502
|
+
"--padding": { type: "string" },
|
|
503
|
+
"--frame-width": { type: "number" },
|
|
504
|
+
"--frame-height": { type: "number" },
|
|
505
|
+
"--output-dir": { type: "string", default: "." }
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
"extract-frames": {
|
|
509
|
+
description: "Sample frames from video (ffmpeg)",
|
|
510
|
+
params: {
|
|
511
|
+
"--input": { type: "string", required: true },
|
|
512
|
+
"--count": { type: "string", default: "8" },
|
|
513
|
+
"--output-dir": { type: "string", default: "." }
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
"remove-bg": {
|
|
517
|
+
description: "Remove background (rembg / fallback)",
|
|
518
|
+
params: {
|
|
519
|
+
"--input": { type: "string[]", required: true },
|
|
520
|
+
"--bg-color": { type: "string" },
|
|
521
|
+
"--output-dir": { type: "string", default: "." }
|
|
522
|
+
}
|
|
428
523
|
}
|
|
429
524
|
}
|
|
430
525
|
},
|
|
526
|
+
process3d: {
|
|
527
|
+
description: "Tripo 3D follow-up operations (chain on tripo_task_id)",
|
|
528
|
+
subcommands: {
|
|
529
|
+
convert: { description: "Export format (FBX/GLTF/\u2026)", params: { "--task-id": { required: true }, "--format": { required: true } } },
|
|
530
|
+
texture: { description: "Re-texture", params: { "--task-id": { required: true } } },
|
|
531
|
+
rig: { description: "Auto-rig", params: { "--task-id": { required: true } } },
|
|
532
|
+
animate: { description: "Retarget animation", params: { "--task-id": { required: true } } },
|
|
533
|
+
"render-sprites": { description: "Blender render to 2D frames", params: { "--task-id": { required: true } } },
|
|
534
|
+
reduce: { description: "Low-poly", params: { "--task-id": { required: true } } },
|
|
535
|
+
stylize: { description: "Style transfer", params: { "--task-id": { required: true } } },
|
|
536
|
+
segment: { description: "Mesh segmentation", params: { "--task-id": { required: true } } },
|
|
537
|
+
prerigcheck: { description: "Rig eligibility", params: { "--task-id": { required: true } } },
|
|
538
|
+
refine: { description: "Refine quality", params: { "--task-id": { required: true } } },
|
|
539
|
+
import: { description: "Import external model", params: { "--file-url": { type: "string" }, "--file-path": { type: "string" } } }
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
voice: {
|
|
543
|
+
description: "Qwen3-TTS custom voices (clone / design / list / delete)",
|
|
544
|
+
subcommands: {
|
|
545
|
+
clone: {
|
|
546
|
+
description: "Clone from audio sample",
|
|
547
|
+
params: { "--audio": { required: true }, "--name": { required: true } }
|
|
548
|
+
},
|
|
549
|
+
design: {
|
|
550
|
+
description: "Design voice from text",
|
|
551
|
+
params: { "--prompt": { required: true }, "--name": { required: true } }
|
|
552
|
+
},
|
|
553
|
+
list: { description: "List custom voices", params: { "--type": { type: "string" } } },
|
|
554
|
+
delete: { description: "Delete by voice id", params: { "<voice-id>": { required: true } } }
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
upload: {
|
|
558
|
+
description: "Upload and list gateway assets",
|
|
559
|
+
subcommands: {
|
|
560
|
+
file: { description: "Upload file \u2192 URL", params: { "<path>": { required: true } } },
|
|
561
|
+
list: { description: "List uploads" },
|
|
562
|
+
delete: { description: "Delete by filename (admin)", params: { "<filename>": { required: true } } }
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
provider: {
|
|
566
|
+
description: "Provider discovery and health",
|
|
567
|
+
subcommands: {
|
|
568
|
+
list: { description: "List providers" },
|
|
569
|
+
health: { description: "Health check", params: { name: { type: "string", required: false } } }
|
|
570
|
+
}
|
|
571
|
+
},
|
|
431
572
|
job: {
|
|
432
|
-
description: "
|
|
573
|
+
description: "Async job history",
|
|
433
574
|
subcommands: {
|
|
434
575
|
list: {
|
|
435
576
|
description: "List jobs",
|
|
436
577
|
params: {
|
|
437
|
-
"--status": { type: "string"
|
|
438
|
-
"--limit": { type: "number"
|
|
578
|
+
"--status": { type: "string" },
|
|
579
|
+
"--limit": { type: "number" }
|
|
439
580
|
}
|
|
440
581
|
},
|
|
441
|
-
status: {
|
|
442
|
-
|
|
443
|
-
params: { id: { type: "string", required: true, description: "Job ID" } }
|
|
444
|
-
},
|
|
445
|
-
cancel: {
|
|
446
|
-
description: "Cancel a job",
|
|
447
|
-
params: { id: { type: "string", required: true, description: "Job ID" } }
|
|
448
|
-
}
|
|
582
|
+
status: { description: "Job detail", params: { id: { type: "string", required: true } } },
|
|
583
|
+
cancel: { description: "Cancel pending/running", params: { id: { type: "string", required: true } } }
|
|
449
584
|
}
|
|
450
585
|
},
|
|
451
586
|
describe: {
|
|
452
|
-
description: "
|
|
587
|
+
description: "Command introspection (this output)",
|
|
453
588
|
params: {
|
|
454
|
-
command: { type: "string", required: false, description: "
|
|
589
|
+
command: { type: "string", required: false, description: "Top-level group: generate, process, \u2026" }
|
|
455
590
|
}
|
|
456
591
|
}
|
|
457
592
|
};
|
|
593
|
+
|
|
594
|
+
// src/commands/describe.ts
|
|
458
595
|
function createDescribeCommand() {
|
|
459
|
-
return new Command2("describe").description("Self-describe available commands (JSON Schema)").argument("[command]", "Specific command to describe").action(function(commandArg) {
|
|
596
|
+
return new Command2("describe").description("Self-describe available commands (JSON Schema)").argument("[command]", "Specific command group to describe (e.g. generate, process)").action(function(commandArg) {
|
|
460
597
|
const globals = this.optsWithGlobals();
|
|
461
598
|
if (!commandArg) {
|
|
462
599
|
output(
|
|
@@ -487,7 +624,7 @@ function createDescribeCommand() {
|
|
|
487
624
|
|
|
488
625
|
// src/commands/generate.ts
|
|
489
626
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
490
|
-
import { join as join2 } from "path";
|
|
627
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
491
628
|
import { Command as Command3 } from "commander";
|
|
492
629
|
function inferExtension(assetType) {
|
|
493
630
|
const map = { image: "png", audio: "mp3", music: "mp3", tts: "mp3", video: "mp4", model3d: "glb", text: "txt" };
|
|
@@ -524,6 +661,37 @@ async function saveOutput(result, assetType, outputDir) {
|
|
|
524
661
|
}
|
|
525
662
|
return null;
|
|
526
663
|
}
|
|
664
|
+
async function saveNamedOutput(result, assetType, filePath) {
|
|
665
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
666
|
+
if (result.output_data) {
|
|
667
|
+
const raw = String(result.output_data);
|
|
668
|
+
if (assetType === "text") {
|
|
669
|
+
writeFileSync2(filePath, raw, "utf8");
|
|
670
|
+
} else {
|
|
671
|
+
writeFileSync2(filePath, Buffer.from(stripDataUri(raw), "base64"));
|
|
672
|
+
}
|
|
673
|
+
return filePath;
|
|
674
|
+
}
|
|
675
|
+
if (result.output_url) {
|
|
676
|
+
const response = await fetch(String(result.output_url));
|
|
677
|
+
if (!response.ok) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
681
|
+
writeFileSync2(filePath, buffer);
|
|
682
|
+
return filePath;
|
|
683
|
+
}
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
function parseFrameSize(raw) {
|
|
687
|
+
const [width, height] = raw.split("x");
|
|
688
|
+
const frameWidth = Number(width);
|
|
689
|
+
const frameHeight = Number(height);
|
|
690
|
+
if (!Number.isFinite(frameWidth) || !Number.isFinite(frameHeight)) {
|
|
691
|
+
throw new Error("frame size must be formatted as WIDTHxHEIGHT");
|
|
692
|
+
}
|
|
693
|
+
return { frame_width: frameWidth, frame_height: frameHeight };
|
|
694
|
+
}
|
|
527
695
|
function createGenerateCommand() {
|
|
528
696
|
const command = new Command3("generate").description("Generate assets via the gateway");
|
|
529
697
|
command.addCommand(
|
|
@@ -552,7 +720,7 @@ function createGenerateCommand() {
|
|
|
552
720
|
})
|
|
553
721
|
);
|
|
554
722
|
command.addCommand(
|
|
555
|
-
new Command3("video").description("Generate a video from a text prompt").requiredOption("--prompt <text>", "Video description prompt").option("--provider <id>", "Provider to use").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
723
|
+
new Command3("video").description("Generate a video from a text prompt (or image-to-video with --input)").requiredOption("--prompt <text>", "Video description prompt").option("--provider <id>", "Provider to use").option("--input <url>", "Reference image URL for image-to-video (Grok)").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
556
724
|
try {
|
|
557
725
|
const ctx = createContext(this);
|
|
558
726
|
const body = {
|
|
@@ -560,6 +728,7 @@ function createGenerateCommand() {
|
|
|
560
728
|
prompt: options.prompt
|
|
561
729
|
};
|
|
562
730
|
if (options.provider) body.provider = options.provider;
|
|
731
|
+
if (options.input) body.input_file = options.input;
|
|
563
732
|
const data = await ctx.client.post("/api/generate", body);
|
|
564
733
|
const localPath = await saveOutput(data, "video", options.outputDir);
|
|
565
734
|
if (localPath) data.local_path = localPath;
|
|
@@ -569,6 +738,56 @@ function createGenerateCommand() {
|
|
|
569
738
|
}
|
|
570
739
|
})
|
|
571
740
|
);
|
|
741
|
+
command.addCommand(
|
|
742
|
+
new Command3("batch").description("Batch generate multiple assets with shared parameters").requiredOption("--prompt <texts...>", "Multiple prompts (one per frame)").option("--asset-type <type>", "Asset type", "image").option("--transparent", "Request transparent background").option("--size <size>", "Image size").option("--ref <urls...>", "Reference image URLs").option("--compose <direction>", "Auto-compose: horizontal, vertical, grid").option("--columns <n>", "Grid columns for compose").option("--frame-size <size>", "Frame size for compose (e.g. 64x64)").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
743
|
+
try {
|
|
744
|
+
const ctx = createContext(this);
|
|
745
|
+
const shared = {};
|
|
746
|
+
if (options.transparent) shared.transparent = true;
|
|
747
|
+
if (options.size) shared.size = options.size;
|
|
748
|
+
if (options.ref && options.ref.length > 0) shared.reference_images = options.ref;
|
|
749
|
+
const body = {
|
|
750
|
+
asset_type: options.assetType,
|
|
751
|
+
prompts: options.prompt,
|
|
752
|
+
shared
|
|
753
|
+
};
|
|
754
|
+
if (options.compose) {
|
|
755
|
+
const compose = { direction: options.compose };
|
|
756
|
+
if (options.columns) compose.columns = Number(options.columns);
|
|
757
|
+
if (options.frameSize) Object.assign(compose, parseFrameSize(options.frameSize));
|
|
758
|
+
body.compose = compose;
|
|
759
|
+
}
|
|
760
|
+
const data = await ctx.client.post("/api/generate/batch", body);
|
|
761
|
+
const assetType = String(options.assetType);
|
|
762
|
+
mkdirSync2(options.outputDir, { recursive: true });
|
|
763
|
+
if (Array.isArray(data.frames)) {
|
|
764
|
+
for (const frame of data.frames) {
|
|
765
|
+
if (typeof frame !== "object" || frame === null) continue;
|
|
766
|
+
const record = frame;
|
|
767
|
+
const index = typeof record.index === "number" ? record.index : 0;
|
|
768
|
+
const localPath = await saveNamedOutput(
|
|
769
|
+
record,
|
|
770
|
+
assetType,
|
|
771
|
+
join2(options.outputDir, `frame_${String(index).padStart(3, "0")}.${inferExtension(assetType)}`)
|
|
772
|
+
);
|
|
773
|
+
if (localPath) record.local_path = localPath;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (typeof data.spritesheet === "object" && data.spritesheet !== null) {
|
|
777
|
+
const record = data.spritesheet;
|
|
778
|
+
const localPath = await saveNamedOutput(
|
|
779
|
+
record,
|
|
780
|
+
"image",
|
|
781
|
+
join2(options.outputDir, "spritesheet.png")
|
|
782
|
+
);
|
|
783
|
+
if (localPath) record.local_path = localPath;
|
|
784
|
+
}
|
|
785
|
+
printSuccess("generate.batch", data, ctx);
|
|
786
|
+
} catch (error2) {
|
|
787
|
+
printError("generate.batch", error2);
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
);
|
|
572
791
|
command.addCommand(
|
|
573
792
|
new Command3("audio").description("Generate audio from a text prompt").requiredOption("--prompt <text>", "Audio description prompt").option("--type <type>", "Audio type: bgm or sfx").option("--duration <seconds>", "Duration in seconds").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
574
793
|
try {
|
|
@@ -591,16 +810,21 @@ function createGenerateCommand() {
|
|
|
591
810
|
})
|
|
592
811
|
);
|
|
593
812
|
command.addCommand(
|
|
594
|
-
new Command3("music").description("Generate music
|
|
813
|
+
new Command3("music").description("Generate music (ElevenLabs /v1/music)").requiredOption("--prompt <text>", "Music description prompt").option("--duration <seconds>", "Duration in seconds (maps to music_length_ms)").option("--force-instrumental", "Force instrumental output (ElevenLabs)").option(
|
|
814
|
+
"--output-format <fmt>",
|
|
815
|
+
"ElevenLabs output_format query, e.g. mp3_44100_128"
|
|
816
|
+
).option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
595
817
|
try {
|
|
596
818
|
const ctx = createContext(this);
|
|
597
819
|
const body = {
|
|
598
820
|
asset_type: "music",
|
|
599
821
|
prompt: options.prompt
|
|
600
822
|
};
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
823
|
+
const params = {};
|
|
824
|
+
if (options.duration) params.duration_seconds = Number(options.duration);
|
|
825
|
+
if (options.forceInstrumental) params.force_instrumental = true;
|
|
826
|
+
if (options.outputFormat) params.output_format = options.outputFormat;
|
|
827
|
+
if (Object.keys(params).length > 0) body.params = params;
|
|
604
828
|
const data = await ctx.client.post("/api/generate", body);
|
|
605
829
|
const localPath = await saveOutput(data, "music", options.outputDir);
|
|
606
830
|
if (localPath) data.local_path = localPath;
|
|
@@ -611,7 +835,12 @@ function createGenerateCommand() {
|
|
|
611
835
|
})
|
|
612
836
|
);
|
|
613
837
|
command.addCommand(
|
|
614
|
-
new Command3("tts").description(
|
|
838
|
+
new Command3("tts").description(
|
|
839
|
+
"Text-to-speech: default Qwen3-TTS; use --provider elevenlabs --voice-id for ElevenLabs"
|
|
840
|
+
).requiredOption("--prompt <text>", "Text to synthesize").option("--voice <name>", "Qwen voice name or custom voice id", "Cherry").option(
|
|
841
|
+
"--voice-id <id>",
|
|
842
|
+
"ElevenLabs voice_id (use with --provider elevenlabs; routes to TTS API)"
|
|
843
|
+
).option("--language <lang>", "Language hint: Auto, Chinese, English, Japanese, etc.", "Auto").option("--model <model>", "Model id (Qwen TTS or ElevenLabs model_id)", "qwen3-tts-flash").option("--instructions <text>", "Natural language speaking instructions (for instruct models)").option("--provider <id>", "qwen_tts | elevenlabs").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
615
844
|
try {
|
|
616
845
|
const ctx = createContext(this);
|
|
617
846
|
const params = {
|
|
@@ -619,6 +848,7 @@ function createGenerateCommand() {
|
|
|
619
848
|
language_type: options.language
|
|
620
849
|
};
|
|
621
850
|
if (options.instructions) params.instructions = options.instructions;
|
|
851
|
+
if (options.voiceId) params.voice_id = options.voiceId;
|
|
622
852
|
const body = {
|
|
623
853
|
asset_type: "tts",
|
|
624
854
|
prompt: options.prompt,
|
|
@@ -744,17 +974,32 @@ function readInputAsBase64(input) {
|
|
|
744
974
|
return input;
|
|
745
975
|
}
|
|
746
976
|
function saveProcessOutput(data, outputDir) {
|
|
747
|
-
const outputData = data.output_data;
|
|
748
|
-
if (typeof outputData !== "string" || !outputData) return null;
|
|
749
977
|
mkdirSync3(outputDir, { recursive: true });
|
|
750
978
|
const timestamp = Date.now();
|
|
979
|
+
const outputs = data.outputs;
|
|
980
|
+
if (Array.isArray(outputs) && outputs.length > 0) {
|
|
981
|
+
const localPaths = [];
|
|
982
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
983
|
+
const item = outputs[i];
|
|
984
|
+
const b64 = item.output_data;
|
|
985
|
+
if (typeof b64 !== "string" || !b64) continue;
|
|
986
|
+
const filePath2 = join3(outputDir, `frame_${timestamp}_${String(i).padStart(4, "0")}.png`);
|
|
987
|
+
writeFileSync3(filePath2, Buffer.from(b64, "base64"));
|
|
988
|
+
localPaths.push(filePath2);
|
|
989
|
+
}
|
|
990
|
+
delete data.outputs;
|
|
991
|
+
data.local_paths = localPaths;
|
|
992
|
+
return localPaths[0] ?? null;
|
|
993
|
+
}
|
|
994
|
+
const outputData = data.output_data;
|
|
995
|
+
if (typeof outputData !== "string" || !outputData) return null;
|
|
751
996
|
const filePath = join3(outputDir, `processed_${timestamp}.png`);
|
|
752
997
|
writeFileSync3(filePath, Buffer.from(outputData, "base64"));
|
|
753
998
|
delete data.output_data;
|
|
754
999
|
return filePath;
|
|
755
1000
|
}
|
|
756
1001
|
function createProcessCommand() {
|
|
757
|
-
const command = new Command5("process").description("Post-process images (crop, resize)");
|
|
1002
|
+
const command = new Command5("process").description("Post-process images and video (crop, resize, compose, extract-frames, remove-bg)");
|
|
758
1003
|
command.addCommand(
|
|
759
1004
|
new Command5("crop").description("Smart crop an image (trim transparent borders)").requiredOption("--input <path>", "Input image (file path or URL)").option("--mode <mode>", "Crop mode: tightest or power_of2", "tightest").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
760
1005
|
try {
|
|
@@ -787,6 +1032,65 @@ function createProcessCommand() {
|
|
|
787
1032
|
}
|
|
788
1033
|
})
|
|
789
1034
|
);
|
|
1035
|
+
command.addCommand(
|
|
1036
|
+
new Command5("compose").description("Compose multiple images into a sprite sheet").requiredOption("--input <paths...>", "Input images (files or URLs)").option("--direction <dir>", "Layout: horizontal, vertical, grid", "horizontal").option("--columns <n>", "Columns for grid layout").option("--padding <n>", "Padding between frames in px", "0").option("--frame-width <n>", "Normalize each frame to this width").option("--frame-height <n>", "Normalize each frame to this height").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
1037
|
+
try {
|
|
1038
|
+
const ctx = createContext(this);
|
|
1039
|
+
const inputs = Array.isArray(options.input) ? options.input : [options.input];
|
|
1040
|
+
const data = await ctx.client.post("/api/process", {
|
|
1041
|
+
inputs: inputs.map(readInputAsBase64),
|
|
1042
|
+
operations: [{
|
|
1043
|
+
op: "compose",
|
|
1044
|
+
direction: options.direction,
|
|
1045
|
+
columns: options.columns ? Number(options.columns) : void 0,
|
|
1046
|
+
padding: Number(options.padding),
|
|
1047
|
+
frame_width: options.frameWidth ? Number(options.frameWidth) : void 0,
|
|
1048
|
+
frame_height: options.frameHeight ? Number(options.frameHeight) : void 0
|
|
1049
|
+
}]
|
|
1050
|
+
});
|
|
1051
|
+
const localPath = saveProcessOutput(data, options.outputDir);
|
|
1052
|
+
if (localPath) data.local_path = localPath;
|
|
1053
|
+
printSuccess("process.compose", data, ctx);
|
|
1054
|
+
} catch (error2) {
|
|
1055
|
+
printError("process.compose", error2);
|
|
1056
|
+
}
|
|
1057
|
+
})
|
|
1058
|
+
);
|
|
1059
|
+
command.addCommand(
|
|
1060
|
+
new Command5("extract-frames").description("Extract evenly-spaced frames from a video using ffmpeg").requiredOption("--input <path>", "Input video (file path or URL)").option("--count <n>", "Number of frames to extract", "8").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
1061
|
+
try {
|
|
1062
|
+
const ctx = createContext(this);
|
|
1063
|
+
const data = await ctx.client.post("/api/process", {
|
|
1064
|
+
input: readInputAsBase64(options.input),
|
|
1065
|
+
operations: [{ op: "extract_frames", count: Number(options.count) }]
|
|
1066
|
+
});
|
|
1067
|
+
const localPath = saveProcessOutput(data, options.outputDir);
|
|
1068
|
+
if (localPath) data.local_path = localPath;
|
|
1069
|
+
printSuccess("process.extract_frames", data, ctx);
|
|
1070
|
+
} catch (error2) {
|
|
1071
|
+
printError("process.extract_frames", error2);
|
|
1072
|
+
}
|
|
1073
|
+
})
|
|
1074
|
+
);
|
|
1075
|
+
command.addCommand(
|
|
1076
|
+
new Command5("remove-bg").description("Remove background from image(s) using rembg or ImageMagick fallback").requiredOption("--input <paths...>", "Input images (files or URLs)").option("--bg-color <color>", "Background color hint for fallback (e.g. white, black)").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
1077
|
+
try {
|
|
1078
|
+
const ctx = createContext(this);
|
|
1079
|
+
const inputs = Array.isArray(options.input) ? options.input : [options.input];
|
|
1080
|
+
const op = { op: "remove_bg" };
|
|
1081
|
+
if (options.bgColor) op.bg_color = options.bgColor;
|
|
1082
|
+
const data = await ctx.client.post("/api/process", {
|
|
1083
|
+
inputs: inputs.map(readInputAsBase64),
|
|
1084
|
+
operations: [op]
|
|
1085
|
+
});
|
|
1086
|
+
const localPath = saveProcessOutput(data, options.outputDir);
|
|
1087
|
+
if (localPath) data.local_path = localPath;
|
|
1088
|
+
printSuccess("process.remove_bg", data, ctx);
|
|
1089
|
+
} catch (error2) {
|
|
1090
|
+
printError("process.remove_bg", error2);
|
|
1091
|
+
}
|
|
1092
|
+
})
|
|
1093
|
+
);
|
|
790
1094
|
return command;
|
|
791
1095
|
}
|
|
792
1096
|
|
|
@@ -822,6 +1126,46 @@ async function saveProcess3dOutput(data, operation, outputDir, format) {
|
|
|
822
1126
|
writeFileSync4(filePath, buffer);
|
|
823
1127
|
return filePath;
|
|
824
1128
|
}
|
|
1129
|
+
function decodeBase64Payload(payload) {
|
|
1130
|
+
const marker = ";base64,";
|
|
1131
|
+
const index = payload.indexOf(marker);
|
|
1132
|
+
const raw = index >= 0 ? payload.slice(index + marker.length) : payload;
|
|
1133
|
+
return Buffer.from(raw, "base64");
|
|
1134
|
+
}
|
|
1135
|
+
async function saveRenderSpritesOutput(data, outputDir) {
|
|
1136
|
+
const frames = Array.isArray(data.frames) ? data.frames : null;
|
|
1137
|
+
if (!frames) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
mkdirSync4(outputDir, { recursive: true });
|
|
1141
|
+
const localPaths = [];
|
|
1142
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
1143
|
+
const frame = frames[index];
|
|
1144
|
+
if (!isRecord2(frame)) {
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
const filename = typeof frame.filename === "string" ? frame.filename : `frame_${String(index).padStart(4, "0")}.png`;
|
|
1148
|
+
if (typeof frame.image_base64 !== "string") {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
const outputPath = join4(outputDir, filename);
|
|
1152
|
+
writeFileSync4(outputPath, decodeBase64Payload(frame.image_base64));
|
|
1153
|
+
delete frame.image_base64;
|
|
1154
|
+
frame.local_path = outputPath;
|
|
1155
|
+
localPaths.push(outputPath);
|
|
1156
|
+
}
|
|
1157
|
+
if (isRecord2(data.metadata)) {
|
|
1158
|
+
const metadataPath = join4(outputDir, "metadata.json");
|
|
1159
|
+
writeFileSync4(metadataPath, `${JSON.stringify(data.metadata, null, 2)}
|
|
1160
|
+
`);
|
|
1161
|
+
data.local_metadata_path = metadataPath;
|
|
1162
|
+
}
|
|
1163
|
+
data.output_dir = outputDir;
|
|
1164
|
+
data.local_paths = localPaths;
|
|
1165
|
+
}
|
|
1166
|
+
function isRecord2(value) {
|
|
1167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1168
|
+
}
|
|
825
1169
|
function createProcess3dCommand() {
|
|
826
1170
|
const command = new Command6("process3d").description("3D model post-processing via Tripo pipeline");
|
|
827
1171
|
command.addCommand(
|
|
@@ -917,6 +1261,25 @@ function createProcess3dCommand() {
|
|
|
917
1261
|
}
|
|
918
1262
|
})
|
|
919
1263
|
);
|
|
1264
|
+
command.addCommand(
|
|
1265
|
+
new Command6("render-sprites").description("Render animated 3D model to 2D sprite frames via Blender").requiredOption("--task-id <id>", "Tripo task ID").option("--frame-count <n>", "Number of frames", "8").option("--resolution <n>", "Frame resolution in pixels", "64").option("--camera-angle <angle>", "Camera: front, side, iso, 3/4, top", "front").option("--directions <n>", "Rotation directions: 1, 4, 8", "1").option("--output-dir <dir>", "Output directory", ".").action(async function(options) {
|
|
1266
|
+
try {
|
|
1267
|
+
const ctx = createContext(this);
|
|
1268
|
+
const data = await ctx.client.post("/api/process3d", {
|
|
1269
|
+
task_id: options.taskId,
|
|
1270
|
+
operation: "render_sprites",
|
|
1271
|
+
frame_count: Number(options.frameCount),
|
|
1272
|
+
resolution: Number(options.resolution),
|
|
1273
|
+
camera_angle: options.cameraAngle,
|
|
1274
|
+
directions: Number(options.directions)
|
|
1275
|
+
});
|
|
1276
|
+
await saveRenderSpritesOutput(data, options.outputDir);
|
|
1277
|
+
printSuccess("process3d.render_sprites", data, ctx);
|
|
1278
|
+
} catch (error2) {
|
|
1279
|
+
printError("process3d.render_sprites", error2);
|
|
1280
|
+
}
|
|
1281
|
+
})
|
|
1282
|
+
);
|
|
920
1283
|
command.addCommand(
|
|
921
1284
|
new Command6("reduce").description("Reduce polygon count (high-poly to low-poly)").requiredOption("--task-id <id>", "Tripo task ID").option("--face-limit <n>", "Target face count").option("--quad", "Use quad topology").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
|
|
922
1285
|
try {
|
|
@@ -1103,8 +1466,105 @@ function createUploadCommand() {
|
|
|
1103
1466
|
return command;
|
|
1104
1467
|
}
|
|
1105
1468
|
|
|
1469
|
+
// src/commands/voice.ts
|
|
1470
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1471
|
+
import { extname } from "path";
|
|
1472
|
+
import { Command as Command9 } from "commander";
|
|
1473
|
+
function inferAudioMime(filePath) {
|
|
1474
|
+
const extension = extname(filePath).toLowerCase();
|
|
1475
|
+
const map = {
|
|
1476
|
+
".mp3": "audio/mpeg",
|
|
1477
|
+
".wav": "audio/wav",
|
|
1478
|
+
".pcm": "audio/pcm",
|
|
1479
|
+
".opus": "audio/opus",
|
|
1480
|
+
".ogg": "audio/ogg",
|
|
1481
|
+
".m4a": "audio/mp4",
|
|
1482
|
+
".aac": "audio/aac",
|
|
1483
|
+
".flac": "audio/flac"
|
|
1484
|
+
};
|
|
1485
|
+
return map[extension] ?? "application/octet-stream";
|
|
1486
|
+
}
|
|
1487
|
+
function readAudioAsBase64(filePath) {
|
|
1488
|
+
if (!existsSync4(filePath)) {
|
|
1489
|
+
throw configError(`Audio file not found: ${filePath}`);
|
|
1490
|
+
}
|
|
1491
|
+
const bytes = readFileSync3(filePath);
|
|
1492
|
+
return {
|
|
1493
|
+
audio_base64: bytes.toString("base64"),
|
|
1494
|
+
audio_mime: inferAudioMime(filePath)
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function withVoiceType(path, type) {
|
|
1498
|
+
if (!type) {
|
|
1499
|
+
return path;
|
|
1500
|
+
}
|
|
1501
|
+
const params = new URLSearchParams({ type });
|
|
1502
|
+
return `${path}?${params.toString()}`;
|
|
1503
|
+
}
|
|
1504
|
+
function createVoiceCommand() {
|
|
1505
|
+
const command = new Command9("voice").description("Manage Qwen3-TTS custom voices");
|
|
1506
|
+
command.addCommand(
|
|
1507
|
+
new Command9("clone").description("Clone a voice from an audio sample").requiredOption("--audio <path>", "Reference audio file path").requiredOption("--name <name>", "Name for the cloned voice").option("--target-model <model>", "Voice cloning model, e.g. qwen3-tts-vc-2026-01-22").action(async function(options) {
|
|
1508
|
+
try {
|
|
1509
|
+
const ctx = createContext(this);
|
|
1510
|
+
const audio = readAudioAsBase64(options.audio);
|
|
1511
|
+
const body = {
|
|
1512
|
+
...audio,
|
|
1513
|
+
name: options.name
|
|
1514
|
+
};
|
|
1515
|
+
if (options.targetModel) body.target_model = options.targetModel;
|
|
1516
|
+
const data = await ctx.client.post("/api/voice/clone", body);
|
|
1517
|
+
printSuccess("voice.clone", data, ctx);
|
|
1518
|
+
} catch (error2) {
|
|
1519
|
+
printError("voice.clone", error2);
|
|
1520
|
+
}
|
|
1521
|
+
})
|
|
1522
|
+
);
|
|
1523
|
+
command.addCommand(
|
|
1524
|
+
new Command9("design").description("Create a synthetic voice from a text description").requiredOption("--prompt <text>", "Voice description prompt").requiredOption("--preview-text <text>", "Preview text for the generated sample").requiredOption("--name <name>", "Name for the designed voice").option("--target-model <model>", "Voice design model, e.g. qwen3-tts-vd-2026-01-26").action(async function(options) {
|
|
1525
|
+
try {
|
|
1526
|
+
const ctx = createContext(this);
|
|
1527
|
+
const body = {
|
|
1528
|
+
voice_prompt: options.prompt,
|
|
1529
|
+
preview_text: options.previewText,
|
|
1530
|
+
name: options.name
|
|
1531
|
+
};
|
|
1532
|
+
if (options.targetModel) body.target_model = options.targetModel;
|
|
1533
|
+
const data = await ctx.client.post("/api/voice/design", body);
|
|
1534
|
+
printSuccess("voice.design", data, ctx);
|
|
1535
|
+
} catch (error2) {
|
|
1536
|
+
printError("voice.design", error2);
|
|
1537
|
+
}
|
|
1538
|
+
})
|
|
1539
|
+
);
|
|
1540
|
+
command.addCommand(
|
|
1541
|
+
new Command9("list").description("List custom cloned or designed voices").option("--type <type>", "Voice type: vc or vd").action(async function(options) {
|
|
1542
|
+
try {
|
|
1543
|
+
const ctx = createContext(this);
|
|
1544
|
+
const data = await ctx.client.get(withVoiceType("/api/voice/list", options.type));
|
|
1545
|
+
printSuccess("voice.list", data, ctx);
|
|
1546
|
+
} catch (error2) {
|
|
1547
|
+
printError("voice.list", error2);
|
|
1548
|
+
}
|
|
1549
|
+
})
|
|
1550
|
+
);
|
|
1551
|
+
command.addCommand(
|
|
1552
|
+
new Command9("delete").description("Delete a custom cloned or designed voice").argument("<voice-id>", "Voice ID to delete").option("--type <type>", "Voice type: vc or vd").action(async function(voiceId, options) {
|
|
1553
|
+
try {
|
|
1554
|
+
const ctx = createContext(this);
|
|
1555
|
+
const path = withVoiceType(`/api/voice/${encodeURIComponent(voiceId)}`, options.type);
|
|
1556
|
+
const data = await ctx.client.delete(path);
|
|
1557
|
+
printSuccess("voice.delete", data, ctx);
|
|
1558
|
+
} catch (error2) {
|
|
1559
|
+
printError("voice.delete", error2);
|
|
1560
|
+
}
|
|
1561
|
+
})
|
|
1562
|
+
);
|
|
1563
|
+
return command;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1106
1566
|
// src/index.ts
|
|
1107
|
-
var program = new
|
|
1567
|
+
var program = new Command10().name("asset-gateway").description("Universal asset generation gateway CLI").version(CLI_VERSION).option(
|
|
1108
1568
|
"--gateway-url <url>",
|
|
1109
1569
|
`Gateway URL (default: $ASSET_GATEWAY_URL, auth config, or ${DEFAULT_GATEWAY_URL})`
|
|
1110
1570
|
).option("--token <token>", "API token for authentication").option("--human", "Human-readable output instead of JSON").option("--fields <fields>", "Comma-separated list of output fields");
|
|
@@ -1114,6 +1574,7 @@ program.addCommand(createProcessCommand());
|
|
|
1114
1574
|
program.addCommand(createProcess3dCommand());
|
|
1115
1575
|
program.addCommand(createProviderCommand());
|
|
1116
1576
|
program.addCommand(createUploadCommand());
|
|
1577
|
+
program.addCommand(createVoiceCommand());
|
|
1117
1578
|
program.addCommand(createJobCommand());
|
|
1118
1579
|
program.addCommand(createDescribeCommand());
|
|
1119
1580
|
await program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doufunao123/asset-gateway",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Universal asset generation gateway CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
21
21
|
"dev": "tsup src/index.ts --format esm --watch",
|
|
22
22
|
"lint": "tsc --noEmit",
|
|
23
|
+
"test:process3d": "bash ../scripts/e2e-process3d-chain.sh",
|
|
23
24
|
"prepublishOnly": "npm run build"
|
|
24
25
|
},
|
|
25
26
|
"engines": {
|