@doufunao123/asset-gateway 0.11.0 → 0.12.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.
Files changed (2) hide show
  1. package/dist/index.js +549 -60
  2. 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 Command9 } from "commander";
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.8.1";
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: { token: { type: "string", required: true, description: "API token (agk_ prefix for admin, or any API key)" } }
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 from a text prompt",
381
+ description: "Generate or edit an image (Gemini / GPT Image / Grok)",
376
382
  params: {
377
- "--prompt": { type: "string", required: true, description: "Image description prompt" },
378
- "--provider": { type: "string", required: false, description: "Provider to use" },
379
- "--transparent": { type: "bool", default: false, description: "Request transparent background" },
380
- "--model": { type: "string", required: false, description: "Model to use" },
381
- "--size": { type: "string", required: false, description: "Image size (e.g. 1024x1024)" },
382
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
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: "Generate a video from a text prompt",
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: "Video description prompt" },
389
- "--provider": { type: "string", required: false, description: "Provider to use" },
390
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
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: "Generate audio from a text prompt",
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, description: "Audio description prompt" },
397
- "--type": { type: "string", required: false, description: "Audio type: bgm or sfx" },
398
- "--duration": { type: "number", required: false, description: "Duration in seconds" },
399
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
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: "Generate a 3D model",
451
+ description: "3D model generation (Tripo)",
404
452
  params: {
405
- "--image": { type: "string", required: false, description: "Reference image URL" },
406
- "--prompt": { type: "string", required: false, description: "Model description prompt" },
407
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
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: "Generate text via LLM",
466
+ description: "LLM text via proxy",
412
467
  params: {
413
- "--prompt": { type: "string", required: true, description: "Text prompt" },
414
- "--model": { type: "string", required: false, description: "Model to use" },
415
- "--max-tokens": { type: "number", required: false, description: "Maximum tokens" },
416
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
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
- provider: {
422
- description: "Provider management",
476
+ process: {
477
+ description: "Image/video post-process on gateway (ImageMagick/ffmpeg/rembg)",
423
478
  subcommands: {
424
- list: { description: "List available providers" },
425
- health: {
426
- description: "Check provider health",
427
- params: { name: { type: "string", required: false, description: "Specific provider name" } }
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: "Job management",
573
+ description: "Async job history",
433
574
  subcommands: {
434
575
  list: {
435
576
  description: "List jobs",
436
577
  params: {
437
- "--status": { type: "string", required: false, description: "Filter by status" },
438
- "--limit": { type: "number", required: false, description: "Maximum number of jobs to return" }
578
+ "--status": { type: "string" },
579
+ "--limit": { type: "number" }
439
580
  }
440
581
  },
441
- status: {
442
- description: "Get job status",
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: "Self-describe available commands (JSON Schema)",
587
+ description: "Command introspection (this output)",
453
588
  params: {
454
- command: { type: "string", required: false, description: "Specific command to describe" }
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,10 +624,10 @@ 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
- const map = { image: "png", audio: "mp3", music: "mp3", tts: "mp3", video: "mp4", model3d: "glb", text: "txt" };
630
+ const map = { image: "png", audio: "mp3", music: "mp3", tts: "mp3", video: "mp4", model3d: "glb", text: "txt", sprite: "png" };
494
631
  return map[assetType] ?? "bin";
495
632
  }
496
633
  function stripDataUri(data) {
@@ -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 from a text prompt").requiredOption("--prompt <text>", "Music description prompt").option("--duration <seconds>", "Duration in seconds").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
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
- if (options.duration) {
602
- body.params = { duration_seconds: Number(options.duration) };
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("Text-to-speech synthesis via Qwen3-TTS").requiredOption("--prompt <text>", "Text to synthesize").option("--voice <name>", "Voice name or custom voice ID", "Cherry").option("--language <lang>", "Language hint: Auto, Chinese, English, Japanese, etc.", "Auto").option("--model <model>", "Qwen3-TTS model", "qwen3-tts-flash").option("--instructions <text>", "Natural language speaking instructions (for instruct models)").option("--provider <id>", "Provider to use").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
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,
@@ -683,6 +913,34 @@ function createGenerateCommand() {
683
913
  }
684
914
  })
685
915
  );
916
+ command.addCommand(
917
+ new Command3("sprite").description("Animate a sprite image using PixelEngine AI").requiredOption("--prompt <text>", "Animation prompt describing the desired motion").requiredOption("--input <path>", "Input sprite image (local path or URL)").option("--model <model>", "Model: pixel-engine-v1.1 or frame-engine-v1.1", "pixel-engine-v1.1").option("--output-frames <n>", "Number of animation frames (even integer)", "8").option("--output-format <fmt>", "Output format: spritesheet, webp, gif", "spritesheet").option("--colors <n>", "Pixel palette color count (2-256, pixel model only)").option("--negative-prompt <text>", "What to avoid in the generation").option("--seed <n>", "Seed for reproducibility").option("--matte-color <hex>", "Matte color for alpha flattening (6-char hex)").option("--enhance-prompt", "Enhance the prompt before generation").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
918
+ try {
919
+ const ctx = createContext(this);
920
+ const params = {
921
+ output_frames: Number(options.outputFrames),
922
+ output_format: options.outputFormat
923
+ };
924
+ if (options.colors) params.pixel_config = { colors: Number(options.colors) };
925
+ if (options.negativePrompt) params.negative_prompt = options.negativePrompt;
926
+ if (options.seed) params.seed = Number(options.seed);
927
+ if (options.matteColor) params.matte_color = options.matteColor;
928
+ const body = {
929
+ asset_type: "sprite",
930
+ prompt: options.prompt,
931
+ model: options.model,
932
+ input_file: options.input,
933
+ params
934
+ };
935
+ const data = await ctx.client.post("/api/generate", body);
936
+ const localPath = await saveOutput(data, "sprite", options.outputDir);
937
+ if (localPath) data.local_path = localPath;
938
+ printSuccess("generate.sprite", data, ctx);
939
+ } catch (error2) {
940
+ printError("generate.sprite", error2);
941
+ }
942
+ })
943
+ );
686
944
  return command;
687
945
  }
688
946
 
@@ -744,17 +1002,32 @@ function readInputAsBase64(input) {
744
1002
  return input;
745
1003
  }
746
1004
  function saveProcessOutput(data, outputDir) {
747
- const outputData = data.output_data;
748
- if (typeof outputData !== "string" || !outputData) return null;
749
1005
  mkdirSync3(outputDir, { recursive: true });
750
1006
  const timestamp = Date.now();
1007
+ const outputs = data.outputs;
1008
+ if (Array.isArray(outputs) && outputs.length > 0) {
1009
+ const localPaths = [];
1010
+ for (let i = 0; i < outputs.length; i++) {
1011
+ const item = outputs[i];
1012
+ const b64 = item.output_data;
1013
+ if (typeof b64 !== "string" || !b64) continue;
1014
+ const filePath2 = join3(outputDir, `frame_${timestamp}_${String(i).padStart(4, "0")}.png`);
1015
+ writeFileSync3(filePath2, Buffer.from(b64, "base64"));
1016
+ localPaths.push(filePath2);
1017
+ }
1018
+ delete data.outputs;
1019
+ data.local_paths = localPaths;
1020
+ return localPaths[0] ?? null;
1021
+ }
1022
+ const outputData = data.output_data;
1023
+ if (typeof outputData !== "string" || !outputData) return null;
751
1024
  const filePath = join3(outputDir, `processed_${timestamp}.png`);
752
1025
  writeFileSync3(filePath, Buffer.from(outputData, "base64"));
753
1026
  delete data.output_data;
754
1027
  return filePath;
755
1028
  }
756
1029
  function createProcessCommand() {
757
- const command = new Command5("process").description("Post-process images (crop, resize)");
1030
+ const command = new Command5("process").description("Post-process images and video (crop, resize, compose, extract-frames, remove-bg)");
758
1031
  command.addCommand(
759
1032
  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
1033
  try {
@@ -787,6 +1060,65 @@ function createProcessCommand() {
787
1060
  }
788
1061
  })
789
1062
  );
1063
+ command.addCommand(
1064
+ 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) {
1065
+ try {
1066
+ const ctx = createContext(this);
1067
+ const inputs = Array.isArray(options.input) ? options.input : [options.input];
1068
+ const data = await ctx.client.post("/api/process", {
1069
+ inputs: inputs.map(readInputAsBase64),
1070
+ operations: [{
1071
+ op: "compose",
1072
+ direction: options.direction,
1073
+ columns: options.columns ? Number(options.columns) : void 0,
1074
+ padding: Number(options.padding),
1075
+ frame_width: options.frameWidth ? Number(options.frameWidth) : void 0,
1076
+ frame_height: options.frameHeight ? Number(options.frameHeight) : void 0
1077
+ }]
1078
+ });
1079
+ const localPath = saveProcessOutput(data, options.outputDir);
1080
+ if (localPath) data.local_path = localPath;
1081
+ printSuccess("process.compose", data, ctx);
1082
+ } catch (error2) {
1083
+ printError("process.compose", error2);
1084
+ }
1085
+ })
1086
+ );
1087
+ command.addCommand(
1088
+ 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) {
1089
+ try {
1090
+ const ctx = createContext(this);
1091
+ const data = await ctx.client.post("/api/process", {
1092
+ input: readInputAsBase64(options.input),
1093
+ operations: [{ op: "extract_frames", count: Number(options.count) }]
1094
+ });
1095
+ const localPath = saveProcessOutput(data, options.outputDir);
1096
+ if (localPath) data.local_path = localPath;
1097
+ printSuccess("process.extract_frames", data, ctx);
1098
+ } catch (error2) {
1099
+ printError("process.extract_frames", error2);
1100
+ }
1101
+ })
1102
+ );
1103
+ command.addCommand(
1104
+ 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) {
1105
+ try {
1106
+ const ctx = createContext(this);
1107
+ const inputs = Array.isArray(options.input) ? options.input : [options.input];
1108
+ const op = { op: "remove_bg" };
1109
+ if (options.bgColor) op.bg_color = options.bgColor;
1110
+ const data = await ctx.client.post("/api/process", {
1111
+ inputs: inputs.map(readInputAsBase64),
1112
+ operations: [op]
1113
+ });
1114
+ const localPath = saveProcessOutput(data, options.outputDir);
1115
+ if (localPath) data.local_path = localPath;
1116
+ printSuccess("process.remove_bg", data, ctx);
1117
+ } catch (error2) {
1118
+ printError("process.remove_bg", error2);
1119
+ }
1120
+ })
1121
+ );
790
1122
  return command;
791
1123
  }
792
1124
 
@@ -822,6 +1154,46 @@ async function saveProcess3dOutput(data, operation, outputDir, format) {
822
1154
  writeFileSync4(filePath, buffer);
823
1155
  return filePath;
824
1156
  }
1157
+ function decodeBase64Payload(payload) {
1158
+ const marker = ";base64,";
1159
+ const index = payload.indexOf(marker);
1160
+ const raw = index >= 0 ? payload.slice(index + marker.length) : payload;
1161
+ return Buffer.from(raw, "base64");
1162
+ }
1163
+ async function saveRenderSpritesOutput(data, outputDir) {
1164
+ const frames = Array.isArray(data.frames) ? data.frames : null;
1165
+ if (!frames) {
1166
+ return;
1167
+ }
1168
+ mkdirSync4(outputDir, { recursive: true });
1169
+ const localPaths = [];
1170
+ for (let index = 0; index < frames.length; index += 1) {
1171
+ const frame = frames[index];
1172
+ if (!isRecord2(frame)) {
1173
+ continue;
1174
+ }
1175
+ const filename = typeof frame.filename === "string" ? frame.filename : `frame_${String(index).padStart(4, "0")}.png`;
1176
+ if (typeof frame.image_base64 !== "string") {
1177
+ continue;
1178
+ }
1179
+ const outputPath = join4(outputDir, filename);
1180
+ writeFileSync4(outputPath, decodeBase64Payload(frame.image_base64));
1181
+ delete frame.image_base64;
1182
+ frame.local_path = outputPath;
1183
+ localPaths.push(outputPath);
1184
+ }
1185
+ if (isRecord2(data.metadata)) {
1186
+ const metadataPath = join4(outputDir, "metadata.json");
1187
+ writeFileSync4(metadataPath, `${JSON.stringify(data.metadata, null, 2)}
1188
+ `);
1189
+ data.local_metadata_path = metadataPath;
1190
+ }
1191
+ data.output_dir = outputDir;
1192
+ data.local_paths = localPaths;
1193
+ }
1194
+ function isRecord2(value) {
1195
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1196
+ }
825
1197
  function createProcess3dCommand() {
826
1198
  const command = new Command6("process3d").description("3D model post-processing via Tripo pipeline");
827
1199
  command.addCommand(
@@ -917,6 +1289,25 @@ function createProcess3dCommand() {
917
1289
  }
918
1290
  })
919
1291
  );
1292
+ command.addCommand(
1293
+ 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) {
1294
+ try {
1295
+ const ctx = createContext(this);
1296
+ const data = await ctx.client.post("/api/process3d", {
1297
+ task_id: options.taskId,
1298
+ operation: "render_sprites",
1299
+ frame_count: Number(options.frameCount),
1300
+ resolution: Number(options.resolution),
1301
+ camera_angle: options.cameraAngle,
1302
+ directions: Number(options.directions)
1303
+ });
1304
+ await saveRenderSpritesOutput(data, options.outputDir);
1305
+ printSuccess("process3d.render_sprites", data, ctx);
1306
+ } catch (error2) {
1307
+ printError("process3d.render_sprites", error2);
1308
+ }
1309
+ })
1310
+ );
920
1311
  command.addCommand(
921
1312
  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
1313
  try {
@@ -1103,8 +1494,105 @@ function createUploadCommand() {
1103
1494
  return command;
1104
1495
  }
1105
1496
 
1497
+ // src/commands/voice.ts
1498
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1499
+ import { extname } from "path";
1500
+ import { Command as Command9 } from "commander";
1501
+ function inferAudioMime(filePath) {
1502
+ const extension = extname(filePath).toLowerCase();
1503
+ const map = {
1504
+ ".mp3": "audio/mpeg",
1505
+ ".wav": "audio/wav",
1506
+ ".pcm": "audio/pcm",
1507
+ ".opus": "audio/opus",
1508
+ ".ogg": "audio/ogg",
1509
+ ".m4a": "audio/mp4",
1510
+ ".aac": "audio/aac",
1511
+ ".flac": "audio/flac"
1512
+ };
1513
+ return map[extension] ?? "application/octet-stream";
1514
+ }
1515
+ function readAudioAsBase64(filePath) {
1516
+ if (!existsSync4(filePath)) {
1517
+ throw configError(`Audio file not found: ${filePath}`);
1518
+ }
1519
+ const bytes = readFileSync3(filePath);
1520
+ return {
1521
+ audio_base64: bytes.toString("base64"),
1522
+ audio_mime: inferAudioMime(filePath)
1523
+ };
1524
+ }
1525
+ function withVoiceType(path, type) {
1526
+ if (!type) {
1527
+ return path;
1528
+ }
1529
+ const params = new URLSearchParams({ type });
1530
+ return `${path}?${params.toString()}`;
1531
+ }
1532
+ function createVoiceCommand() {
1533
+ const command = new Command9("voice").description("Manage Qwen3-TTS custom voices");
1534
+ command.addCommand(
1535
+ 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) {
1536
+ try {
1537
+ const ctx = createContext(this);
1538
+ const audio = readAudioAsBase64(options.audio);
1539
+ const body = {
1540
+ ...audio,
1541
+ name: options.name
1542
+ };
1543
+ if (options.targetModel) body.target_model = options.targetModel;
1544
+ const data = await ctx.client.post("/api/voice/clone", body);
1545
+ printSuccess("voice.clone", data, ctx);
1546
+ } catch (error2) {
1547
+ printError("voice.clone", error2);
1548
+ }
1549
+ })
1550
+ );
1551
+ command.addCommand(
1552
+ 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) {
1553
+ try {
1554
+ const ctx = createContext(this);
1555
+ const body = {
1556
+ voice_prompt: options.prompt,
1557
+ preview_text: options.previewText,
1558
+ name: options.name
1559
+ };
1560
+ if (options.targetModel) body.target_model = options.targetModel;
1561
+ const data = await ctx.client.post("/api/voice/design", body);
1562
+ printSuccess("voice.design", data, ctx);
1563
+ } catch (error2) {
1564
+ printError("voice.design", error2);
1565
+ }
1566
+ })
1567
+ );
1568
+ command.addCommand(
1569
+ new Command9("list").description("List custom cloned or designed voices").option("--type <type>", "Voice type: vc or vd").action(async function(options) {
1570
+ try {
1571
+ const ctx = createContext(this);
1572
+ const data = await ctx.client.get(withVoiceType("/api/voice/list", options.type));
1573
+ printSuccess("voice.list", data, ctx);
1574
+ } catch (error2) {
1575
+ printError("voice.list", error2);
1576
+ }
1577
+ })
1578
+ );
1579
+ command.addCommand(
1580
+ 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) {
1581
+ try {
1582
+ const ctx = createContext(this);
1583
+ const path = withVoiceType(`/api/voice/${encodeURIComponent(voiceId)}`, options.type);
1584
+ const data = await ctx.client.delete(path);
1585
+ printSuccess("voice.delete", data, ctx);
1586
+ } catch (error2) {
1587
+ printError("voice.delete", error2);
1588
+ }
1589
+ })
1590
+ );
1591
+ return command;
1592
+ }
1593
+
1106
1594
  // src/index.ts
1107
- var program = new Command9().name("asset-gateway").description("Universal asset generation gateway CLI").version(CLI_VERSION).option(
1595
+ var program = new Command10().name("asset-gateway").description("Universal asset generation gateway CLI").version(CLI_VERSION).option(
1108
1596
  "--gateway-url <url>",
1109
1597
  `Gateway URL (default: $ASSET_GATEWAY_URL, auth config, or ${DEFAULT_GATEWAY_URL})`
1110
1598
  ).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 +1602,7 @@ program.addCommand(createProcessCommand());
1114
1602
  program.addCommand(createProcess3dCommand());
1115
1603
  program.addCommand(createProviderCommand());
1116
1604
  program.addCommand(createUploadCommand());
1605
+ program.addCommand(createVoiceCommand());
1117
1606
  program.addCommand(createJobCommand());
1118
1607
  program.addCommand(createDescribeCommand());
1119
1608
  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.0",
3
+ "version": "0.12.0",
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": {