@lingjingai/lj-awb-cli-pre 0.3.15 → 0.3.17

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.
@@ -46,6 +46,21 @@ const REQUEST_SOURCE_CLI = 'LINGJING_AWB_CLI';
46
46
  const DEFAULT_TASK_RECORD_FILE_ENV = process.env.LINGJING_AWB_TASK_RECORD_FILE || process.env.AWB_TASK_RECORD_FILE;
47
47
  const execFileAsync = promisify(execFile);
48
48
  const COMMON_IMAGE_FORMATS = new Set(['jpg', 'jpeg', 'jfif', 'png', 'webp']);
49
+ const REMOTE_VOICE_DOWNLOAD_TIMEOUT_MS = 30_000;
50
+ const REMOTE_VOICE_DOWNLOAD_MAX_BYTES = 100 * 1024 * 1024;
51
+ const REMOTE_VOICE_MIME_TYPES = new Set([
52
+ 'audio/mpeg',
53
+ 'audio/wav',
54
+ 'audio/x-wav',
55
+ 'audio/mp4',
56
+ 'audio/aac',
57
+ 'audio/ogg',
58
+ 'video/mp4',
59
+ ]);
60
+ const REMOTE_VOICE_EXTENSIONS = new Set(['.mp3', '.wav', '.m4a', '.aac', '.ogg', '.mp4']);
61
+ const ASSET_PLATFORM_CODES = ['JIMENG', 'BYTEPLUS'];
62
+ const ASSET_PLATFORM_CODE_SET = new Set(ASSET_PLATFORM_CODES);
63
+ const ASSET_PLATFORM_HINT = ASSET_PLATFORM_CODES.join('|');
49
64
 
50
65
  export function normalizeUserInfo(payload) {
51
66
  const data = payload && typeof payload === 'object' ? payload : {};
@@ -371,7 +386,6 @@ export async function doctor(kwargs = {}) {
371
386
  },
372
387
  origins: {
373
388
  apiOrigin: API_ORIGIN,
374
- assetEditOrigin: awbApi.ASSET_EDIT_ORIGIN,
375
389
  },
376
390
  auth: summarizeAuth(auth, authContext.accessKeyInfo),
377
391
  paths: {
@@ -683,6 +697,7 @@ function normalizeModelRows(payload, kind, options = {}) {
683
697
  const row = compactRecord({
684
698
  modelGroupCode,
685
699
  displayName: item?.modelName ?? item?.name ?? item?.label ?? null,
700
+ modelDesc: item?.modelDesc ?? null,
686
701
  provider: item?.provider ?? item?.vendor ?? item?.supplier ?? item?.componyName ?? null,
687
702
  enabled: item?.enabled ?? item?.available ?? item?.status ?? null,
688
703
  modelStatus: item?.modelStatus ?? null,
@@ -720,6 +735,44 @@ export async function listModels(kind, kwargs = {}) {
720
735
  return { usage, models };
721
736
  }
722
737
 
738
+ function requireAssetPlatform(value, options = {}) {
739
+ const flag = options.flag || '--platform';
740
+ const platform = trimToNull(value);
741
+ if (!platform) {
742
+ throw argumentError(`缺少参数:${flag}`, `素材加白平台必须显式传入 ${ASSET_PLATFORM_HINT},不要依赖默认平台。`);
743
+ }
744
+ if (!ASSET_PLATFORM_CODE_SET.has(platform)) {
745
+ throw argumentError(`不支持的平台:${platform}`, `只接受 ${ASSET_PLATFORM_HINT};请使用 model asset-review-models 查看模型支持的平台。`);
746
+ }
747
+ return platform;
748
+ }
749
+
750
+ function optionalAssetPlatform(value) {
751
+ if (value == null || value === '') return null;
752
+ return requireAssetPlatform(value);
753
+ }
754
+
755
+ function normalizeAssetReviewModelRecord(item = {}) {
756
+ return compactRecord({
757
+ modelGroupCode: item?.modelGroupCode ?? item?.groupCode ?? null,
758
+ platform: item?.platform ?? null,
759
+ });
760
+ }
761
+
762
+ export async function assetReviewModels(kwargs = {}) {
763
+ const modelGroupCode = trimToNull(kwargs.modelGroupCode);
764
+ const platform = optionalAssetPlatform(kwargs.platform);
765
+ const payload = await awbApi.fetchAssetReviewModels();
766
+ const models = normalizeRows(payload)
767
+ .map((item) => normalizeAssetReviewModelRecord(item))
768
+ .filter((item) => !modelGroupCode || item.modelGroupCode === modelGroupCode)
769
+ .filter((item) => !platform || item.platform === platform);
770
+ return {
771
+ models,
772
+ ...(toBool(kwargs.includeRaw) ? { raw: payload } : {}),
773
+ };
774
+ }
775
+
723
776
  async function findModelGroup(modelGroupCode, options = {}) {
724
777
  const includeRaw = Boolean(options.includeRaw);
725
778
  const [imageResult, videoResult] = await Promise.allSettled([
@@ -898,9 +951,9 @@ function resourceSourceKindForValidation(detail = {}) {
898
951
  }
899
952
 
900
953
  function sourceAllowedByRule(rule = {}, sourceKind) {
901
- const sources = Array.isArray(rule.sources) ? rule.sources : [];
902
- if (!sources.length) return true;
903
- return sources.includes(sourceKind);
954
+ const shapes = Array.isArray(rule.valueShapes) ? rule.valueShapes : [];
955
+ if (!shapes.length) return true;
956
+ return shapes.includes(sourceKind);
904
957
  }
905
958
 
906
959
  function resourceFormatForValidation(detail = {}) {
@@ -1033,7 +1086,7 @@ function assertResourceShapeAgainstModel(resourceDetails = [], resourceRules = [
1033
1086
  if (!sourceAllowedByRule(rule, sourceKind)) {
1034
1087
  throw argumentError(
1035
1088
  `模型不支持 ${resourceText(resource)} 的来源:${sourceKind}`,
1036
- `该资源允许来源:${(rule.sources || []).join(', ') || '未限制'}。`,
1089
+ `该资源允许的 value 形状:${(rule.valueShapes || []).join(', ') || '未限制'}。`,
1037
1090
  );
1038
1091
  }
1039
1092
  const format = resourceFormatForValidation(detail);
@@ -1759,7 +1812,7 @@ function createSpecResourceRequirements(inputModes) {
1759
1812
  intent: item.intent,
1760
1813
  resource: item.requiredResources,
1761
1814
  syntax: item.resourceSyntax,
1762
- sources: item.sourceKinds,
1815
+ valueShapes: item.sourceKinds,
1763
1816
  ...item.resourceLimits,
1764
1817
  promptBinding: item.promptBinding,
1765
1818
  constraints: item.resourceConstraints,
@@ -1898,7 +1951,7 @@ function createSpecValidationRules(taskKind) {
1898
1951
  ];
1899
1952
  if (taskKind === 'video') {
1900
1953
  rules.splice(4, 0, 'reference_key 仅用于视频 reference 资源的可选 prompt 占位符绑定;prompt 中出现 <<<key>>> 时,必须有同名 reference 资源。');
1901
- rules.push('subject reference 必须使用 asset:<externalId>,externalId 应来自 subject publish + subject wait。');
1954
+ rules.push('subject reference 必须使用 asset:<externalId>,externalId 应来自 create subject + create subject-wait。');
1902
1955
  } else {
1903
1956
  rules.push('图片任务 resources 只接受 image reference,且不接受 reference_key 或 <<<key>>>;请用自然 prompt 描述图一、图二、参考图、主体等映射。');
1904
1957
  }
@@ -1907,8 +1960,8 @@ function createSpecValidationRules(taskKind) {
1907
1960
 
1908
1961
  function createSpecPreflight(taskKind) {
1909
1962
  const modelListCommand = taskKind === 'image' ? 'model image-models' : 'model video-models';
1910
- const feeCommand = taskKind === 'image' ? 'image fee' : 'video fee';
1911
- const createCommand = taskKind === 'image' ? 'image create' : 'video create';
1963
+ const feeCommand = taskKind === 'image' ? 'create image-fee' : 'create video-fee';
1964
+ const createCommand = taskKind === 'image' ? 'create image' : 'create video';
1912
1965
  return [
1913
1966
  'doctor --verify',
1914
1967
  `${modelListCommand} --model <keyword>`,
@@ -1927,26 +1980,26 @@ function createSpecExamples(taskKind, supportedIntents) {
1927
1980
  const examples = [];
1928
1981
  const hasIntent = (mode) => supportedIntents.some((item) => item.mode === mode);
1929
1982
  if (taskKind === 'image') {
1930
- if (hasIntent('prompt_only')) examples.push('lj-awb image create --model-group-code <code> --prompt "品牌吉祥物表情包四宫格" --ratio 1:1 --quality 1K --dry-run');
1931
- if (hasIntent('reference')) examples.push('lj-awb image create --model-group-code <code> --prompt "保持参考图主体,生成海报" --resource image:reference=./ref.png --dry-run');
1983
+ if (hasIntent('prompt_only')) examples.push('lj-awb create image --model-group-code <code> --prompt "品牌吉祥物表情包四宫格" --ratio 1:1 --quality 1K --dry-run');
1984
+ if (hasIntent('reference')) examples.push('lj-awb create image --model-group-code <code> --prompt "保持参考图主体,生成海报" --resource image:reference=./ref.png --dry-run');
1932
1985
  } else {
1933
- if (hasIntent('prompt_only')) examples.push('lj-awb video create --model-group-code <code> --prompt "机器人站在白色展台中央,缓慢转身" --duration 6 --dry-run');
1986
+ if (hasIntent('prompt_only')) examples.push('lj-awb create video --model-group-code <code> --prompt "机器人站在白色展台中央,缓慢转身" --duration 6 --dry-run');
1934
1987
  if (hasIntent('reference')) {
1935
1988
  const referenceIntent = supportedIntents.find((item) => item.mode === 'reference');
1936
- examples.push('lj-awb video create --model-group-code <code> --prompt "人物转身看向镜头" --resource image:reference=./hero.png --duration 5 --dry-run');
1989
+ examples.push('lj-awb create video --model-group-code <code> --prompt "人物转身看向镜头" --resource image:reference=./hero.png --duration 5 --dry-run');
1937
1990
  if (referenceIntent?.resourceSyntax?.some((item) => String(item).startsWith('audio:reference'))) {
1938
- examples.push('lj-awb video create --model-group-code <code> --prompt "让 <<<hero>>> 跟随音乐节奏转身" --resource image:reference:hero=./hero.png --resource audio:reference=./music.mp3 --duration 5 --dry-run');
1991
+ examples.push('lj-awb create video --model-group-code <code> --prompt "让 <<<hero>>> 跟随音乐节奏转身" --resource image:reference:hero=./hero.png --resource audio:reference=./music.mp3 --duration 5 --dry-run');
1939
1992
  }
1940
1993
  }
1941
1994
  if (hasIntent('frames')) {
1942
1995
  const framesIntent = supportedIntents.find((item) => item.mode === 'frames');
1943
- examples.push('lj-awb video create --model-group-code <code> --prompt "镜头缓慢推进" --resource image:first_frame=./first.png --duration 5 --dry-run');
1944
- examples.push('lj-awb video create --model-group-code <code> --prompt "镜头缓慢推进" --resource image:first_frame=asset:<assetId> --duration 5 --dry-run');
1996
+ examples.push('lj-awb create video --model-group-code <code> --prompt "镜头缓慢推进" --resource image:first_frame=./first.png --duration 5 --dry-run');
1997
+ examples.push('lj-awb create video --model-group-code <code> --prompt "镜头缓慢推进" --resource image:first_frame=asset:<assetId> --duration 5 --dry-run');
1945
1998
  if (framesIntent?.resourceUsages?.includes('last_frame')) {
1946
- examples.push('lj-awb video create --model-group-code <code> --prompt "从第一张过渡到第二张" --resource image:first_frame=./first.png --resource image:last_frame=./last.png --duration 5 --dry-run');
1999
+ examples.push('lj-awb create video --model-group-code <code> --prompt "从第一张过渡到第二张" --resource image:first_frame=./first.png --resource image:last_frame=./last.png --duration 5 --dry-run');
1947
2000
  }
1948
2001
  }
1949
- if (hasIntent('storyboard')) examples.push('lj-awb video create --model-group-code <code> --resources-json ./storyboard.json --duration 5 --dry-run');
2002
+ if (hasIntent('storyboard')) examples.push('lj-awb create video --model-group-code <code> --resources-json ./storyboard.json --duration 5 --dry-run');
1950
2003
  }
1951
2004
  return examples;
1952
2005
  }
@@ -2019,7 +2072,7 @@ function modelOptionsResources(inputModes = []) {
2019
2072
  mode: 'frames',
2020
2073
  mediaType: 'IMAGE',
2021
2074
  usage: supportedFrameMode.optionResourceUsages,
2022
- sources: supportedFrameMode.sourceKinds,
2075
+ valueShapes: supportedFrameMode.sourceKinds,
2023
2076
  ...supportedFrameMode.optionResourceLimits,
2024
2077
  formatPolicy: framePolicy.summary,
2025
2078
  webpSupported: framePolicy.webpSupported,
@@ -2036,7 +2089,7 @@ function modelOptionsResources(inputModes = []) {
2036
2089
  mode: optionResourceMode(sourceMode),
2037
2090
  mediaType,
2038
2091
  usage: optionResourceUsage(sourceMode),
2039
- sources: item.sources,
2092
+ valueShapes: item.valueShapes,
2040
2093
  fileTypes: sourceMode === 'subject_reference' ? undefined : item.fileTypes,
2041
2094
  formatPolicy: imagePolicy?.summary,
2042
2095
  webpSupported: imagePolicy?.webpSupported,
@@ -2074,7 +2127,7 @@ export function modelInputGuide() {
2074
2127
  { field: 'resources[].type', values: ['image', 'video', 'audio', 'subject'], description: '资源本体类型;subject 表示已创建的主体对象。' },
2075
2128
  { field: 'resources[].usage', values: ['first_frame', 'last_frame', 'reference', 'keyframe'], description: '素材用途。' },
2076
2129
  { field: 'resources[].reference_key', values: ['custom string'], description: '仅视频 reference 资源需要占位绑定时使用;图片生图 image:reference 不使用 reference_key。subject reference 必须传。' },
2077
- { field: 'resources[].source.kind', values: ['url', 'asset_id'], description: '默认建议使用 urlasset_id 表示平台资产或主体对象 ID' },
2130
+ { field: 'resources[].source.kind', values: ['url', 'asset_id'], description: '只接受 urlasset_id 两个枚举值。本地文件、http(s) URL、material backendPath 一律传 kind=url,CLI 按 value 自动识别;asset_id 表示平台资产或主体对象 ID。注意:模型 resources[].valueShapes 列出的 local_file / http_url / backendPath 是 value 形状分类,不是 kind 枚举。' },
2078
2131
  { field: 'resources[].source.value', description: '资源值,必填。url 传素材地址;asset_id 传资源 ID。' },
2079
2132
  { field: 'resources[].order', description: '仅 usage=keyframe 时需要,且同一请求内不能重复。' },
2080
2133
  { field: 'resources[].duration', description: '仅 keyframe 场景下用于表达该帧持续时长,可传小数秒。' },
@@ -2133,8 +2186,8 @@ export async function modelCreateSpec(kwargs = {}) {
2133
2186
  schemaVersion: 1,
2134
2187
  modelGroupCode,
2135
2188
  taskKind: context.taskKind,
2136
- createCommand: context.taskKind === 'image' ? 'image create' : 'video create',
2137
- feeCommand: context.taskKind === 'image' ? 'image fee' : 'video fee',
2189
+ createCommand: context.taskKind === 'image' ? 'create image' : 'create video',
2190
+ feeCommand: context.taskKind === 'image' ? 'create image-fee' : 'create video-fee',
2138
2191
  statusCommandTaskType: context.taskKind === 'image' ? 'IMAGE_CREATE' : 'VIDEO_GROUP',
2139
2192
  optionsCommand: `model options --model-group-code ${modelGroupCode}`,
2140
2193
  model: context.model,
@@ -2156,7 +2209,35 @@ async function readJsonMaybeFile(value, fallback) {
2156
2209
  return JSON.parse(fileText);
2157
2210
  }
2158
2211
 
2159
- async function collectFileSpecs(kwargs = {}, defaultSceneType = TASK_UPLOAD_SCENE.DEFAULT) {
2212
+ const AUDIO_VIDEO_EXTENSIONS = new Set([
2213
+ '.aac',
2214
+ '.aiff',
2215
+ '.avi',
2216
+ '.flac',
2217
+ '.m4a',
2218
+ '.m4v',
2219
+ '.mkv',
2220
+ '.mov',
2221
+ '.mp3',
2222
+ '.mp4',
2223
+ '.mpeg',
2224
+ '.mpg',
2225
+ '.oga',
2226
+ '.ogg',
2227
+ '.ogv',
2228
+ '.wav',
2229
+ '.webm',
2230
+ ]);
2231
+
2232
+ function defaultUploadSceneForFile(filePath, mimeType = '') {
2233
+ const normalizedMime = String(mimeType || '').toLowerCase();
2234
+ if (normalizedMime.startsWith('audio/') || normalizedMime.startsWith('video/')) return TASK_UPLOAD_SCENE.VIDEO_CREATE;
2235
+ const ext = path.extname(String(filePath || '')).toLowerCase();
2236
+ if (AUDIO_VIDEO_EXTENSIONS.has(ext)) return TASK_UPLOAD_SCENE.VIDEO_CREATE;
2237
+ return TASK_UPLOAD_SCENE.DEFAULT;
2238
+ }
2239
+
2240
+ async function collectFileSpecs(kwargs = {}, defaultSceneType = null) {
2160
2241
  const specs = [];
2161
2242
  for (const file of parseListArg(kwargs.file)) specs.push({ file });
2162
2243
  for (const file of parseListArg(kwargs.files)) specs.push({ file });
@@ -2169,11 +2250,14 @@ async function collectFileSpecs(kwargs = {}, defaultSceneType = TASK_UPLOAD_SCEN
2169
2250
  }
2170
2251
  }
2171
2252
  return specs
2172
- .map((item) => ({
2173
- file: trimToNull(item.file ?? item.path ?? item.filePath),
2174
- sceneType: trimToNull(item.sceneType ?? kwargs.sceneType) ?? defaultSceneType,
2175
- projectNo: trimToNull(item.projectNo ?? kwargs.projectNo) ?? '',
2176
- }))
2253
+ .map((item) => {
2254
+ const file = trimToNull(item.file ?? item.path ?? item.filePath);
2255
+ return {
2256
+ file,
2257
+ sceneType: trimToNull(item.sceneType ?? kwargs.sceneType) ?? defaultSceneType ?? defaultUploadSceneForFile(file),
2258
+ projectNo: trimToNull(item.projectNo ?? kwargs.projectNo) ?? '',
2259
+ };
2260
+ })
2177
2261
  .filter((item) => item.file);
2178
2262
  }
2179
2263
 
@@ -2182,7 +2266,7 @@ function dryRunBackendPath(filePath, sceneType) {
2182
2266
  }
2183
2267
 
2184
2268
  export async function uploadFilesCommand(kwargs = {}) {
2185
- const specs = await collectFileSpecs(kwargs, trimToNull(kwargs.sceneType) ?? TASK_UPLOAD_SCENE.DEFAULT);
2269
+ const specs = await collectFileSpecs(kwargs, trimToNull(kwargs.sceneType));
2186
2270
  if (!specs.length) throw argumentError('缺少上传文件', '传 --file <path> 或 --files a.png,b.mp4。');
2187
2271
  if (toBool(kwargs.dryRun)) {
2188
2272
  const files = [];
@@ -2212,7 +2296,7 @@ export async function uploadLocalFile(filePath, options = {}) {
2212
2296
  throw argumentError(`文件不存在:${filePath}`);
2213
2297
  }
2214
2298
  const buffer = await fs.readFile(inspected.filePath);
2215
- const sceneType = options.sceneType ?? TASK_UPLOAD_SCENE.DEFAULT;
2299
+ const sceneType = options.sceneType ?? defaultUploadSceneForFile(inspected.filePath, inspected.mimeType);
2216
2300
  const groupId = crypto.randomUUID().replaceAll('-', '');
2217
2301
  const secret = await awbApi.fetchUploadSecret({
2218
2302
  sceneType,
@@ -2259,6 +2343,113 @@ export async function uploadLocalFile(filePath, options = {}) {
2259
2343
  });
2260
2344
  }
2261
2345
 
2346
+ function isHttpUrl(value) {
2347
+ return /^https?:\/\//i.test(String(value || '').trim());
2348
+ }
2349
+
2350
+ function isUploadedMaterialReference(value) {
2351
+ const text = trimToNull(value);
2352
+ if (!text) return false;
2353
+ if (text.startsWith('material/') || text.startsWith('/material/')) return true;
2354
+ if (!isHttpUrl(text)) return false;
2355
+ try {
2356
+ const url = new URL(text);
2357
+ return url.hostname.endsWith('.myqcloud.com');
2358
+ } catch {
2359
+ return false;
2360
+ }
2361
+ }
2362
+
2363
+ function remoteFileNameFromUrl(remoteUrl, fallback = 'voice-audio') {
2364
+ try {
2365
+ const pathname = decodeURIComponent(new URL(remoteUrl).pathname);
2366
+ const baseName = safeFileName(path.basename(pathname));
2367
+ if (baseName && baseName !== '.' && baseName !== '/') return baseName;
2368
+ } catch {}
2369
+ return `${fallback}.mp3`;
2370
+ }
2371
+
2372
+ function extensionFromContentType(contentType = '') {
2373
+ const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
2374
+ if (normalized === 'audio/mpeg') return '.mp3';
2375
+ if (normalized === 'audio/wav' || normalized === 'audio/x-wav') return '.wav';
2376
+ if (normalized === 'audio/mp4') return '.m4a';
2377
+ if (normalized === 'audio/aac') return '.aac';
2378
+ if (normalized === 'audio/ogg') return '.ogg';
2379
+ if (normalized === 'video/mp4') return '.mp4';
2380
+ return '';
2381
+ }
2382
+
2383
+ function isSupportedRemoteVoiceDownload(contentType = '', fileName = '') {
2384
+ const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
2385
+ const extension = path.extname(fileName).toLowerCase();
2386
+ if (REMOTE_VOICE_MIME_TYPES.has(normalized)) return true;
2387
+ if ((!normalized || normalized === 'application/octet-stream') && REMOTE_VOICE_EXTENSIONS.has(extension)) return true;
2388
+ return false;
2389
+ }
2390
+
2391
+ async function downloadRemoteFileToTemp(remoteUrl, options = {}) {
2392
+ const timeoutMs = Math.max(1_000, toInt(options.timeoutMs, REMOTE_VOICE_DOWNLOAD_TIMEOUT_MS));
2393
+ const maxBytes = Math.max(1, toInt(options.maxBytes, REMOTE_VOICE_DOWNLOAD_MAX_BYTES));
2394
+ const controller = new AbortController();
2395
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2396
+ let dir = null;
2397
+ let response;
2398
+ try {
2399
+ response = await fetch(remoteUrl, { signal: controller.signal });
2400
+ if (!response.ok) {
2401
+ throw new LingjingAwbCliError(`下载远程音频失败:${response.status} ${response.statusText}`, {
2402
+ type: 'http_error',
2403
+ exitCode: 30,
2404
+ details: { url: remoteUrl, status: response.status },
2405
+ });
2406
+ }
2407
+ const contentType = response.headers.get('content-type') || '';
2408
+ let fileName = remoteFileNameFromUrl(remoteUrl, options.fallbackName || 'voice-audio');
2409
+ if (!path.extname(fileName)) fileName = `${fileName}${extensionFromContentType(contentType) || '.mp3'}`;
2410
+ if (!isSupportedRemoteVoiceDownload(contentType, fileName)) {
2411
+ throw argumentError(
2412
+ `远程音频类型不支持:${contentType || path.extname(fileName) || 'unknown'}`,
2413
+ 'create subject-voice 只支持 mp3、wav、m4a、aac、ogg 音频或 mp4 视频来源。',
2414
+ );
2415
+ }
2416
+ const contentLength = toInt(response.headers.get('content-length'), 0);
2417
+ if (contentLength > maxBytes) {
2418
+ throw argumentError(`远程音频过大:${contentLength} bytes`, `最大支持 ${maxBytes} bytes。`);
2419
+ }
2420
+ dir = await fs.mkdtemp(path.join(tmpdir(), 'lj-awb-voice-'));
2421
+ const filePath = path.join(dir, fileName);
2422
+ const file = await fs.open(filePath, 'w');
2423
+ let size = 0;
2424
+ try {
2425
+ if (!response.body) throw new Error('empty response body');
2426
+ for await (const chunk of response.body) {
2427
+ const buffer = Buffer.from(chunk);
2428
+ size += buffer.length;
2429
+ if (size > maxBytes) {
2430
+ throw argumentError(`远程音频过大:超过 ${maxBytes} bytes`);
2431
+ }
2432
+ await file.write(buffer);
2433
+ }
2434
+ } finally {
2435
+ await file.close();
2436
+ }
2437
+ return { filePath, dir, contentType, size };
2438
+ } catch (error) {
2439
+ if (dir) await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
2440
+ if (error instanceof LingjingAwbCliError) throw error;
2441
+ const isAbort = error?.name === 'AbortError';
2442
+ throw new LingjingAwbCliError(`下载远程音频失败:${error.message}`, {
2443
+ type: 'network_error',
2444
+ exitCode: 30,
2445
+ hint: isAbort ? `远程下载超过 ${timeoutMs}ms,请改用本地 --file 或已上传 material 路径。` : '',
2446
+ details: { url: remoteUrl, causeCode: error?.cause?.code },
2447
+ });
2448
+ } finally {
2449
+ clearTimeout(timeout);
2450
+ }
2451
+ }
2452
+
2262
2453
  function resolveCustomBizId(explicit) {
2263
2454
  return trimToNull(explicit);
2264
2455
  }
@@ -2330,7 +2521,10 @@ function normalizeResourceSource(source, type, index, options = {}) {
2330
2521
  ? explicitKind.toLowerCase()
2331
2522
  : (rawValue?.startsWith('asset:') || type === 'subject' ? 'asset_id' : 'url');
2332
2523
  if (!['url', 'asset_id'].includes(kind)) {
2333
- throw argumentError(`resource[${index}] source.kind 不支持:${kind}`, '支持 url、asset_id。');
2524
+ throw argumentError(
2525
+ `resource[${index}] source.kind 不支持:${explicitKind}`,
2526
+ '只支持 url、asset_id 两个值。本地文件 / http(s) URL / material backendPath 都统一传 kind=url(CLI 按 value 自动识别);平台资产或主体对象传 kind=asset_id。',
2527
+ );
2334
2528
  }
2335
2529
  const value = kind === 'asset_id' ? assetValue : rawValue;
2336
2530
  if (!value) {
@@ -2765,7 +2959,7 @@ export async function imageFee(kwargs = {}) {
2765
2959
  const built = await buildImageRequest(kwargs, { dryRun: true, includeCustomBizId: false });
2766
2960
  return compactRecord({
2767
2961
  dryRun: true,
2768
- action: 'image fee',
2962
+ action: 'create image-fee',
2769
2963
  request: built.request,
2770
2964
  ...(built.resourceConversions.length ? { resourceConversions: built.resourceConversions } : {}),
2771
2965
  });
@@ -2820,13 +3014,13 @@ export async function imageCreate(kwargs = {}) {
2820
3014
  const built = await buildImageRequest(kwargs, { dryRun: true });
2821
3015
  return compactRecord({
2822
3016
  dryRun: true,
2823
- action: 'image create',
3017
+ action: 'create image',
2824
3018
  request: built.request,
2825
3019
  ...(built.localFiles.length ? { localFiles: built.localFiles } : {}),
2826
3020
  ...(built.resourceConversions.length ? { resourceConversions: built.resourceConversions } : {}),
2827
3021
  });
2828
3022
  }
2829
- ensureConfirmed(kwargs, '正式生图会消耗积分,需要确认', { action: 'image create' });
3023
+ ensureConfirmed(kwargs, '正式生图会消耗积分,需要确认', { action: 'create image' });
2830
3024
  const built = await buildImageRequest(kwargs);
2831
3025
  const feePayload = await awbApi.fetchImageFee(built.request).catch(() => null);
2832
3026
  const estimate = await pointEstimate(feePayload, built.projectGroupNo).catch(() => ({}));
@@ -2919,7 +3113,7 @@ export async function videoFee(kwargs = {}) {
2919
3113
  const built = await buildVideoRequest(kwargs, { dryRun: true, includeCustomBizId: false });
2920
3114
  return compactRecord({
2921
3115
  dryRun: true,
2922
- action: 'video fee',
3116
+ action: 'create video-fee',
2923
3117
  request: built.request,
2924
3118
  ...(built.resourceConversions.length ? { resourceConversions: built.resourceConversions } : {}),
2925
3119
  });
@@ -2937,13 +3131,13 @@ export async function videoCreate(kwargs = {}) {
2937
3131
  const built = await buildVideoRequest(kwargs, { dryRun: true });
2938
3132
  return compactRecord({
2939
3133
  dryRun: true,
2940
- action: 'video create',
3134
+ action: 'create video',
2941
3135
  request: built.request,
2942
3136
  ...(built.localFiles.length ? { localFiles: built.localFiles } : {}),
2943
3137
  ...(built.resourceConversions.length ? { resourceConversions: built.resourceConversions } : {}),
2944
3138
  });
2945
3139
  }
2946
- ensureConfirmed(kwargs, '正式生视频会消耗积分,需要确认', { action: 'video create' });
3140
+ ensureConfirmed(kwargs, '正式生视频会消耗积分,需要确认', { action: 'create video' });
2947
3141
  const built = await buildVideoRequest(kwargs);
2948
3142
  const feePayload = await awbApi.fetchVideoFee(built.request).catch(() => null);
2949
3143
  const estimate = await pointEstimate(feePayload, built.projectGroupNo).catch(() => ({}));
@@ -3079,7 +3273,7 @@ export async function imageCreateBatch(kwargs = {}) {
3079
3273
  }
3080
3274
  return { dryRun: true, count: items.length, results };
3081
3275
  }
3082
- ensureConfirmed(kwargs, '批量生图会多次消耗积分,需要确认', { action: 'image create-batch', count: items.length });
3276
+ ensureConfirmed(kwargs, '批量生图会多次消耗积分,需要确认', { action: 'create image-batch', count: items.length });
3083
3277
  const results = await runConcurrent(items, kwargs.concurrency ?? 1, async (item, index) => ({
3084
3278
  inputIndex: index,
3085
3279
  status: 'success',
@@ -3098,7 +3292,7 @@ export async function videoCreateBatch(kwargs = {}) {
3098
3292
  }
3099
3293
  return { dryRun: true, count: items.length, results };
3100
3294
  }
3101
- ensureConfirmed(kwargs, '批量生视频会多次消耗积分,需要确认', { action: 'video create-batch', count: items.length });
3295
+ ensureConfirmed(kwargs, '批量生视频会多次消耗积分,需要确认', { action: 'create video-batch', count: items.length });
3102
3296
  const results = await runConcurrent(items, kwargs.concurrency ?? 1, async (item, index) => ({
3103
3297
  inputIndex: index,
3104
3298
  status: 'success',
@@ -3311,6 +3505,7 @@ function normalizeAssetGroup(item = {}) {
3311
3505
  groupId: item?.id ?? item?.groupId ?? item?.assetGroupsId ?? null,
3312
3506
  name: item?.name ?? item?.groupName ?? null,
3313
3507
  description: item?.description ?? null,
3508
+ platform: item?.platform ?? null,
3314
3509
  projectName: item?.projectName ?? null,
3315
3510
  };
3316
3511
  }
@@ -3384,12 +3579,64 @@ function normalizeSubjectTagIds(tagList) {
3384
3579
  return uniqueNonEmpty(tagList.map((item) => (typeof item === 'string' ? item : item?.tagId ?? item?.id ?? item?.tagCode)));
3385
3580
  }
3386
3581
 
3582
+ function normalizeSubjectTaskMeta(item = {}) {
3583
+ const rawTaskStatus = item?.taskStatus ?? item?.task_status ?? null;
3584
+ const taskStatus = rawTaskStatus ? taskStatusText(rawTaskStatus) : null;
3585
+ const errorMessage = trimToNull(
3586
+ item?.errorMessage
3587
+ ?? item?.error_msg
3588
+ ?? item?.errorMsg
3589
+ ?? item?.resultMessage
3590
+ ?? item?.result_msg
3591
+ ?? item?.resultMsg
3592
+ ?? item?.message
3593
+ ?? item?.msg,
3594
+ );
3595
+ return {
3596
+ taskStatus,
3597
+ errorMessage,
3598
+ isTerminal: taskStatus ? isTerminalTaskStatus(taskStatus) : false,
3599
+ isSuccess: taskStatus ? isSuccessTaskStatus(taskStatus) : false,
3600
+ };
3601
+ }
3602
+
3603
+ function subjectResourceStatus(externalId, taskMeta) {
3604
+ if (externalId) return 'ready';
3605
+ if (taskMeta?.taskStatus && taskMeta.isTerminal) {
3606
+ return taskMeta.isSuccess ? 'missing_external_id' : 'failed';
3607
+ }
3608
+ return 'pending_external_id';
3609
+ }
3610
+
3611
+ function subjectFailureDetails(record, kind = 'subject') {
3612
+ if (!record) return null;
3613
+ const taskStatus = taskStatusText(record.taskStatus);
3614
+ if (!taskStatus || !isTerminalTaskStatus(taskStatus)) return null;
3615
+ const fallback = kind === 'voice'
3616
+ ? '主体音色创建失败'
3617
+ : '主体创建失败';
3618
+ if (!isSuccessTaskStatus(taskStatus)) {
3619
+ return {
3620
+ taskStatus,
3621
+ errorMessage: record.errorMessage || fallback,
3622
+ };
3623
+ }
3624
+ if (!record.externalId) {
3625
+ return {
3626
+ taskStatus,
3627
+ errorMessage: record.errorMessage || `${fallback}:任务已终态但未返回externalId`,
3628
+ };
3629
+ }
3630
+ return null;
3631
+ }
3632
+
3387
3633
  function normalizeSubjectRecord(item = {}) {
3388
3634
  const referList = Array.isArray(item?.elementReferList) ? item.elementReferList : [];
3389
3635
  const videoList = Array.isArray(item?.elementVideoList) ? item.elementVideoList : [];
3390
3636
  const elementId = item?.id ?? item?.subjectId ?? item?.elementId ?? null;
3391
3637
  const externalId = item?.externalId ?? item?.external_id ?? null;
3392
3638
  const name = item?.elementName ?? item?.name ?? item?.subjectName ?? null;
3639
+ const taskMeta = normalizeSubjectTaskMeta(item);
3393
3640
  return {
3394
3641
  subjectId: externalId ?? elementId,
3395
3642
  elementId,
@@ -3412,6 +3659,33 @@ function normalizeSubjectRecord(item = {}) {
3412
3659
  tagIds: normalizeSubjectTagIds(item?.tagList),
3413
3660
  ...(trimToNull(item?.elementVoiceId) ? { voiceId: trimToNull(item.elementVoiceId) } : {}),
3414
3661
  ...(externalId && name ? { nextRefSubject: `${name}=${externalId}` } : {}),
3662
+ taskStatus: taskMeta.taskStatus,
3663
+ errorMessage: taskMeta.errorMessage,
3664
+ isTerminal: externalId ? true : taskMeta.isTerminal,
3665
+ status: subjectResourceStatus(externalId, taskMeta),
3666
+ createdAt: item?.gmtCreate ?? item?.createdAt ?? null,
3667
+ };
3668
+ }
3669
+
3670
+ function normalizeSubjectVoiceRecord(item = {}) {
3671
+ const voiceRecordId = item?.voiceRecordId ?? item?.id ?? item?.reqTaskId ?? item?.voiceTaskId ?? null;
3672
+ const reqTaskId = item?.reqTaskId ?? voiceRecordId;
3673
+ const externalId = item?.externalId ?? item?.external_id ?? item?.voice_id ?? item?.voiceId ?? null;
3674
+ const name = item?.voiceName ?? item?.name ?? null;
3675
+ const taskMeta = normalizeSubjectTaskMeta(item);
3676
+ return {
3677
+ voiceRecordId,
3678
+ reqTaskId,
3679
+ voiceId: externalId,
3680
+ externalId,
3681
+ name,
3682
+ voiceUrl: item?.voiceUrl ?? item?.voice_url ?? item?.audioUrl ?? item?.audio_url ?? null,
3683
+ videoId: item?.videoId ?? item?.video_id ?? null,
3684
+ status: subjectResourceStatus(externalId, taskMeta),
3685
+ taskStatus: taskMeta.taskStatus,
3686
+ errorMessage: taskMeta.errorMessage,
3687
+ isTerminal: externalId ? true : taskMeta.isTerminal,
3688
+ ...(externalId ? { nextVoiceArg: `--voice-id ${externalId}` } : {}),
3415
3689
  createdAt: item?.gmtCreate ?? item?.createdAt ?? null,
3416
3690
  };
3417
3691
  }
@@ -3439,45 +3713,55 @@ export async function assetMatchActor(kwargs = {}) {
3439
3713
  }
3440
3714
 
3441
3715
  export async function assetGroupList(kwargs = {}) {
3716
+ const platform = requireAssetPlatform(kwargs.platform);
3442
3717
  const payload = await awbApi.listAssetGroups({
3443
3718
  name: kwargs.name ?? '',
3719
+ platform,
3444
3720
  pageNumber: toInt(kwargs.pageNumber, 1),
3445
3721
  pageSize: toInt(kwargs.pageSize, 20),
3446
3722
  ...(parseListArg(kwargs.groupIds).length ? { groupIds: parseListArg(kwargs.groupIds) } : {}),
3447
3723
  });
3448
- return { groups: extractAssetGroupRows(payload), raw: toBool(kwargs.includeRaw) ? payload : undefined };
3724
+ return { platform, groups: extractAssetGroupRows(payload), raw: toBool(kwargs.includeRaw) ? payload : undefined };
3449
3725
  }
3450
3726
 
3451
3727
  export async function assetGroupGet(kwargs = {}) {
3452
3728
  const groupId = requireValue(kwargs, 'groupId', 'group-id');
3453
- const payload = await awbApi.getAssetGroup(groupId);
3454
- return payload && typeof payload === 'object' && !Array.isArray(payload)
3455
- ? normalizeAssetGroup(payload)
3456
- : extractAssetGroupRows(payload)[0] ?? { groupId };
3729
+ const platform = requireAssetPlatform(kwargs.platform);
3730
+ const payload = await awbApi.getAssetGroup(groupId, { platform });
3731
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
3732
+ const group = normalizeAssetGroup(payload);
3733
+ return { ...group, platform: group.platform ?? platform };
3734
+ }
3735
+ return extractAssetGroupRows(payload)[0] ?? { groupId, platform };
3457
3736
  }
3458
3737
 
3459
3738
  export async function assetGroupCreate(kwargs = {}) {
3460
3739
  const name = requireValue(kwargs, 'name');
3740
+ const platform = requireAssetPlatform(kwargs.platform);
3461
3741
  const body = {
3462
3742
  name,
3463
3743
  description: kwargs.description ?? '',
3744
+ platform,
3464
3745
  projectName: kwargs.projectName ?? 'default',
3465
3746
  };
3466
- if (toBool(kwargs.dryRun)) return { dryRun: true, action: 'asset group-create', request: body };
3467
- ensureConfirmed(kwargs, '创建素材组是云端写入动作,需要确认', { action: 'asset group-create', body });
3747
+ if (toBool(kwargs.dryRun)) return { dryRun: true, action: 'create asset-group', request: body };
3748
+ ensureConfirmed(kwargs, '创建素材组是云端写入动作,需要确认', { action: 'create asset-group', body });
3468
3749
  const payload = await awbApi.createAssetGroup(body);
3469
- return { created: true, groupId: payload?.id ?? payload?.groupId ?? payload ?? null, name, projectName: body.projectName };
3750
+ return { created: true, groupId: payload?.id ?? payload?.groupId ?? payload ?? null, name, platform, projectName: body.projectName };
3470
3751
  }
3471
3752
 
3472
3753
  export async function assetGroupUpdate(kwargs = {}) {
3473
3754
  const groupId = requireValue(kwargs, 'groupId', 'group-id');
3474
- const body = {};
3755
+ const platform = requireAssetPlatform(kwargs.platform);
3756
+ const body = { platform };
3475
3757
  if (kwargs.name != null) body.name = kwargs.name;
3476
3758
  if (kwargs.description != null) body.description = kwargs.description;
3477
3759
  if (kwargs.projectName != null) body.projectName = kwargs.projectName;
3478
- if (!Object.keys(body).length) throw argumentError('缺少素材组更新字段', '至少传 --name、--description --project-name。');
3479
- if (toBool(kwargs.dryRun)) return { dryRun: true, action: 'asset group-update', groupId, request: body };
3480
- ensureConfirmed(kwargs, '更新素材组是云端写入动作,需要确认', { action: 'asset group-update', groupId, body });
3760
+ if (!['name', 'description', 'projectName'].some((key) => Object.prototype.hasOwnProperty.call(body, key))) {
3761
+ throw argumentError('缺少素材组更新字段', '至少传 --name、--description --project-name;--platform 只用于定位平台资产组。');
3762
+ }
3763
+ if (toBool(kwargs.dryRun)) return { dryRun: true, action: 'create asset-group-update', groupId, request: body };
3764
+ ensureConfirmed(kwargs, '更新素材组是云端写入动作,需要确认', { action: 'create asset-group-update', groupId, body });
3481
3765
  await awbApi.updateAssetGroup(groupId, body);
3482
3766
  return { updated: true, groupId, ...body };
3483
3767
  }
@@ -3491,29 +3775,30 @@ function extractAssetId(payload) {
3491
3775
  export async function assetRegister(kwargs = {}) {
3492
3776
  const groupId = requireValue(kwargs, 'groupId', 'group-id');
3493
3777
  const name = requireValue(kwargs, 'name');
3778
+ const platform = requireAssetPlatform(kwargs.platform);
3494
3779
  const localFile = trimToNull(kwargs.file);
3495
3780
  const assetPath = trimToNull(kwargs.backendPath) ?? normalizeCosAssetPath(kwargs.url);
3496
3781
  if (!localFile && !assetPath) throw argumentError('缺少素材路径', '传 --file、--backend-path 或 --url。');
3497
3782
  if (toBool(kwargs.dryRun)) {
3498
3783
  return {
3499
3784
  dryRun: true,
3500
- action: 'asset register',
3785
+ action: 'create asset',
3501
3786
  request: {
3502
3787
  assetGroupsId: groupId,
3503
- url: localFile ? normalizeCosAssetPath(dryRunBackendPath(localFile, TASK_UPLOAD_SCENE.SUBJECT)) : assetPath,
3788
+ url: localFile ? normalizeCosAssetPath(dryRunBackendPath(localFile, TASK_UPLOAD_SCENE.ASSET_REVIEW)) : assetPath,
3504
3789
  name,
3505
- ...(kwargs.platform ? { platform: kwargs.platform } : {}),
3790
+ platform,
3506
3791
  },
3507
3792
  localFile: localFile ? await inspectLocalFile(localFile) : null,
3508
3793
  };
3509
3794
  }
3510
- ensureConfirmed(kwargs, '注册素材是云端写入动作,需要确认', { action: 'asset register', groupId, name });
3511
- const uploaded = localFile ? await uploadLocalFile(localFile, { sceneType: TASK_UPLOAD_SCENE.SUBJECT }) : null;
3795
+ ensureConfirmed(kwargs, '注册素材是云端写入动作,需要确认', { action: 'create asset', groupId, name });
3796
+ const uploaded = localFile ? await uploadLocalFile(localFile, { sceneType: TASK_UPLOAD_SCENE.ASSET_REVIEW }) : null;
3512
3797
  const body = {
3513
3798
  assetGroupsId: groupId,
3514
3799
  url: normalizeCosAssetPath(uploaded?.backendPath ?? assetPath),
3515
3800
  name,
3516
- ...(kwargs.platform ? { platform: kwargs.platform } : {}),
3801
+ platform,
3517
3802
  };
3518
3803
  const payload = await awbApi.registerAsset(body);
3519
3804
  return {
@@ -3521,6 +3806,7 @@ export async function assetRegister(kwargs = {}) {
3521
3806
  assetId: extractAssetId(payload),
3522
3807
  groupId,
3523
3808
  name,
3809
+ platform,
3524
3810
  assetPath: body.url,
3525
3811
  ...(uploaded ? { upload: uploaded } : {}),
3526
3812
  };
@@ -3542,6 +3828,194 @@ export async function subjectList(kwargs = {}) {
3542
3828
  };
3543
3829
  }
3544
3830
 
3831
+ export async function subjectVoiceList(kwargs = {}) {
3832
+ const keyword = trimToNull(kwargs.keyword ?? kwargs.name);
3833
+ const payload = keyword
3834
+ ? await awbApi.listVoicesByName(keyword)
3835
+ : await awbApi.listVoices();
3836
+ const voices = paginateRows(
3837
+ normalizeRows(payload).map((item) => normalizeSubjectVoiceRecord(item)),
3838
+ kwargs.pageNumber,
3839
+ kwargs.pageSize,
3840
+ );
3841
+ return {
3842
+ voices,
3843
+ ...(toBool(kwargs.includeRaw) ? { raw: payload } : {}),
3844
+ };
3845
+ }
3846
+
3847
+ function subjectVoiceSource(kwargs = {}) {
3848
+ const file = trimToNull(kwargs.file);
3849
+ const voiceUrlArg = trimToNull(kwargs.voiceUrl);
3850
+ const audioUrlArg = trimToNull(kwargs.audioUrl);
3851
+ if (voiceUrlArg && audioUrlArg) {
3852
+ throw argumentError('音色来源只能选择一种', '--voice-url 和 --audio-url 是同一含义的别名,只能传一个。');
3853
+ }
3854
+ const voiceUrl = voiceUrlArg ?? audioUrlArg ?? trimToNull(kwargs.url);
3855
+ const videoId = trimToNull(kwargs.videoId);
3856
+ const sourceCount = [file, voiceUrl, videoId].filter(Boolean).length;
3857
+ if (sourceCount === 0) {
3858
+ throw argumentError('缺少音色来源', '传 --file <audio>、--voice-url/--audio-url <material path> 或 --video-id <id>。');
3859
+ }
3860
+ if (sourceCount > 1) {
3861
+ throw argumentError('音色来源只能选择一种', '--file、--voice-url/--audio-url、--video-id 三者只能传一个。');
3862
+ }
3863
+ const shouldUploadRemoteUrl = Boolean(voiceUrl && isHttpUrl(voiceUrl) && !isUploadedMaterialReference(voiceUrl));
3864
+ return { file, voiceUrl, videoId, shouldUploadRemoteUrl };
3865
+ }
3866
+
3867
+ function buildSubjectVoiceCreateBody(kwargs, source, uploaded = null) {
3868
+ const name = requireValue(kwargs, 'name');
3869
+ const voiceUrl = uploaded
3870
+ ? normalizeCosAssetPath(uploaded.backendPath)
3871
+ : normalizeCosAssetPath(source.voiceUrl);
3872
+ return compactRecord({
3873
+ reqTaskId: trimToNull(kwargs.reqTaskId),
3874
+ voiceName: name,
3875
+ voiceUrl,
3876
+ videoId: source.videoId,
3877
+ });
3878
+ }
3879
+
3880
+ function extractCreatedVoiceRecordId(payload) {
3881
+ if (typeof payload === 'string') return payload;
3882
+ if (!payload || typeof payload !== 'object') return null;
3883
+ return payload.id
3884
+ ?? payload.voiceRecordId
3885
+ ?? payload.reqTaskId
3886
+ ?? payload.voiceTaskId
3887
+ ?? payload.data?.id
3888
+ ?? payload.data?.voiceRecordId
3889
+ ?? payload.data?.reqTaskId
3890
+ ?? null;
3891
+ }
3892
+
3893
+ async function fetchSubjectVoiceByReqTaskId(reqTaskId, options = {}) {
3894
+ if (!reqTaskId) return null;
3895
+ const payload = await awbApi.getVoiceByReqTaskId(reqTaskId).catch((error) => {
3896
+ if (options.optional) return null;
3897
+ throw error;
3898
+ });
3899
+ return payload && typeof payload === 'object' ? normalizeSubjectVoiceRecord(payload) : null;
3900
+ }
3901
+
3902
+ async function waitForSubjectVoiceExternalId(reqTaskId, options = {}) {
3903
+ const waitSeconds = Math.max(0, toInt(options.waitSeconds, 0));
3904
+ const pollIntervalMs = Math.max(500, toInt(options.pollIntervalMs, 2000));
3905
+ const deadline = Date.now() + waitSeconds * 1000;
3906
+ let latest = await fetchSubjectVoiceByReqTaskId(reqTaskId);
3907
+ while (waitSeconds > 0 && !latest?.externalId && !subjectFailureDetails(latest, 'voice') && Date.now() < deadline) {
3908
+ await sleep(pollIntervalMs);
3909
+ latest = await fetchSubjectVoiceByReqTaskId(reqTaskId);
3910
+ }
3911
+ return latest;
3912
+ }
3913
+
3914
+ function subjectVoiceWaitPayload(voiceRecordId, voice) {
3915
+ const externalId = voice?.externalId ?? null;
3916
+ return {
3917
+ voiceRecordId,
3918
+ reqTaskId: voice?.reqTaskId ?? voiceRecordId,
3919
+ voiceId: externalId,
3920
+ externalId,
3921
+ nextVoiceArg: externalId ? `--voice-id ${externalId}` : null,
3922
+ status: voice?.status ?? (externalId ? 'ready' : 'pending_external_id'),
3923
+ taskStatus: voice?.taskStatus ?? null,
3924
+ errorMessage: voice?.errorMessage ?? null,
3925
+ isTerminal: externalId ? true : Boolean(voice?.isTerminal),
3926
+ voice,
3927
+ };
3928
+ }
3929
+
3930
+ export async function subjectVoiceCreate(kwargs = {}) {
3931
+ const name = requireValue(kwargs, 'name');
3932
+ const source = subjectVoiceSource(kwargs);
3933
+ if (toBool(kwargs.dryRun)) {
3934
+ const localFiles = source.file ? [await inspectLocalFile(source.file)] : [];
3935
+ let dryRunSource = source;
3936
+ if (source.file) {
3937
+ dryRunSource = { ...source, voiceUrl: dryRunBackendPath(source.file, TASK_UPLOAD_SCENE.SUBJECT) };
3938
+ } else if (source.shouldUploadRemoteUrl) {
3939
+ dryRunSource = {
3940
+ ...source,
3941
+ voiceUrl: dryRunBackendPath(remoteFileNameFromUrl(source.voiceUrl), TASK_UPLOAD_SCENE.SUBJECT),
3942
+ };
3943
+ }
3944
+ return {
3945
+ dryRun: true,
3946
+ action: 'create subject-voice',
3947
+ name,
3948
+ request: buildSubjectVoiceCreateBody(kwargs, dryRunSource),
3949
+ localFiles,
3950
+ remoteUpload: source.shouldUploadRemoteUrl || undefined,
3951
+ remoteUrl: source.shouldUploadRemoteUrl ? source.voiceUrl : undefined,
3952
+ nextVoiceArg: '--voice-id <externalId>',
3953
+ };
3954
+ }
3955
+ ensureConfirmed(kwargs, '创建主体音色是云端写入动作,需要确认', { action: 'create subject-voice', name });
3956
+ let uploaded = null;
3957
+ let tempDownload = null;
3958
+ try {
3959
+ if (source.file) {
3960
+ uploaded = await uploadLocalFile(source.file, { sceneType: TASK_UPLOAD_SCENE.SUBJECT });
3961
+ } else if (source.shouldUploadRemoteUrl) {
3962
+ tempDownload = await downloadRemoteFileToTemp(source.voiceUrl, { fallbackName: safeFileName(name) || 'voice-audio' });
3963
+ uploaded = await uploadLocalFile(tempDownload.filePath, { sceneType: TASK_UPLOAD_SCENE.SUBJECT });
3964
+ }
3965
+ } finally {
3966
+ if (tempDownload?.dir) await fs.rm(tempDownload.dir, { recursive: true, force: true }).catch(() => {});
3967
+ }
3968
+ const body = buildSubjectVoiceCreateBody(kwargs, source, uploaded);
3969
+ const payload = await awbApi.createVoice(body);
3970
+ const voiceRecordId = extractCreatedVoiceRecordId(payload);
3971
+ if (!voiceRecordId) throw new LingjingAwbCliError('音色创建完成但未拿到 voiceRecordId', { type: 'api_error', exitCode: 1, details: payload });
3972
+ const voice = await fetchSubjectVoiceByReqTaskId(voiceRecordId, { optional: true });
3973
+ const waitPayload = subjectVoiceWaitPayload(voiceRecordId, voice);
3974
+ return {
3975
+ created: true,
3976
+ name,
3977
+ voiceRecordId,
3978
+ reqTaskId: waitPayload.reqTaskId,
3979
+ voiceId: waitPayload.voiceId,
3980
+ externalId: waitPayload.externalId,
3981
+ nextVoiceArg: waitPayload.nextVoiceArg,
3982
+ status: waitPayload.status,
3983
+ request: body,
3984
+ ...(uploaded ? { upload: uploaded } : {}),
3985
+ voice,
3986
+ };
3987
+ }
3988
+
3989
+ function requireSubjectVoiceRecordId(kwargs = {}) {
3990
+ const value = trimToNull(kwargs.voiceRecordId ?? kwargs.reqTaskId);
3991
+ if (!value) throw argumentError('缺少参数:--voice-record-id', '传 create subject-voice 返回的 voiceRecordId;也可用 --req-task-id。');
3992
+ return value;
3993
+ }
3994
+
3995
+ export async function subjectVoiceWait(kwargs = {}) {
3996
+ const voiceRecordId = requireSubjectVoiceRecordId(kwargs);
3997
+ const voice = await waitForSubjectVoiceExternalId(voiceRecordId, kwargs);
3998
+ const payload = subjectVoiceWaitPayload(voiceRecordId, voice);
3999
+ const failure = subjectFailureDetails(voice, 'voice');
4000
+ if (failure) {
4001
+ throw new LingjingAwbCliError(failure.errorMessage, {
4002
+ type: 'subject_voice_failed',
4003
+ exitCode: 1,
4004
+ hint: '主体音色创建任务已失败,请根据 error.details.errorMessage 调整音频/视频来源后重新创建。',
4005
+ details: payload,
4006
+ });
4007
+ }
4008
+ if (!payload.externalId) {
4009
+ throw new LingjingAwbCliError('主体音色 externalId 尚未回填', {
4010
+ type: 'subject_voice_still_pending',
4011
+ exitCode: 20,
4012
+ hint: '稍后重试 create subject-voice-wait,或用 create subject-voice-list --name 查询。',
4013
+ details: payload,
4014
+ });
4015
+ }
4016
+ return payload;
4017
+ }
4018
+
3545
4019
  const SUBJECT_SLOT_ALIASES = {
3546
4020
  primary: 'primary',
3547
4021
  'three-view': 'three-view',
@@ -3619,15 +4093,37 @@ function tagListFromArg(value) {
3619
4093
  .filter((item) => item.tagId);
3620
4094
  }
3621
4095
 
3622
- function buildSubjectCreateBody(kwargs, specs, assets) {
4096
+ const SUBJECT_MODEL_CODE_HINT = '传 --model-code tx|vidu;KeLing / 可灵主体明确传 tx,Vidu 主体明确传 vidu。';
4097
+
4098
+ function resolveSubjectCreateModelCode(kwargs = {}) {
4099
+ const explicitModelCode = trimToNull(kwargs.modelCode ?? kwargs.model_code);
4100
+ if (explicitModelCode === 'tx' || explicitModelCode === 'vidu') {
4101
+ return { modelCode: explicitModelCode };
4102
+ }
4103
+ if (explicitModelCode) {
4104
+ throw argumentError(
4105
+ `主体 modelCode 不支持:${explicitModelCode}`,
4106
+ SUBJECT_MODEL_CODE_HINT,
4107
+ );
4108
+ }
4109
+
4110
+ throw argumentError(
4111
+ '缺少主体 modelCode',
4112
+ SUBJECT_MODEL_CODE_HINT,
4113
+ );
4114
+ }
4115
+
4116
+ function buildSubjectCreateBody(kwargs, specs, assets, modelCodeResolution = resolveSubjectCreateModelCode(kwargs)) {
3623
4117
  const name = requireValue(kwargs, 'name');
4118
+ const description = trimToNull(kwargs.description ?? kwargs.elementDescription) ?? name;
3624
4119
  const primary = assets.find((item) => item.isPrimary);
3625
4120
  const referAssets = assets.filter((item) => item.assetPath);
4121
+ const { modelCode } = modelCodeResolution;
3626
4122
  return compactRecord({
3627
4123
  reqTaskId: trimToNull(kwargs.reqTaskId),
3628
- modelCode: trimToNull(kwargs.modelCode),
4124
+ modelCode,
3629
4125
  elementName: name,
3630
- elementDescription: trimToNull(kwargs.description ?? kwargs.elementDescription),
4126
+ elementDescription: description,
3631
4127
  elementFrontalImage: primary?.assetPath ?? null,
3632
4128
  elementReferList: referAssets.map((item) => ({
3633
4129
  imageUrl: item.assetPath,
@@ -3645,9 +4141,12 @@ function extractCreatedElementId(payload) {
3645
4141
  return payload.id ?? payload.elementId ?? payload.subjectId ?? payload.reqTaskId ?? payload.data?.id ?? payload.data?.elementId ?? null;
3646
4142
  }
3647
4143
 
3648
- async function fetchSubjectByReqTaskId(reqTaskId) {
4144
+ async function fetchSubjectByReqTaskId(reqTaskId, options = {}) {
3649
4145
  if (!reqTaskId) return null;
3650
- const payload = await awbApi.getElementByReqTaskId(reqTaskId).catch(() => null);
4146
+ const payload = await awbApi.getElementByReqTaskId(reqTaskId).catch((error) => {
4147
+ if (options.optional) return null;
4148
+ throw error;
4149
+ });
3651
4150
  return payload && typeof payload === 'object' ? normalizeSubjectRecord(payload) : null;
3652
4151
  }
3653
4152
 
@@ -3656,7 +4155,7 @@ async function waitForSubjectExternalId(reqTaskId, options = {}) {
3656
4155
  const pollIntervalMs = Math.max(500, toInt(options.pollIntervalMs, 2000));
3657
4156
  const deadline = Date.now() + waitSeconds * 1000;
3658
4157
  let latest = await fetchSubjectByReqTaskId(reqTaskId);
3659
- while (waitSeconds > 0 && !latest?.externalId && Date.now() < deadline) {
4158
+ while (waitSeconds > 0 && !latest?.externalId && !subjectFailureDetails(latest, 'subject') && Date.now() < deadline) {
3660
4159
  await sleep(pollIntervalMs);
3661
4160
  latest = await fetchSubjectByReqTaskId(reqTaskId);
3662
4161
  }
@@ -3670,7 +4169,10 @@ function subjectWaitPayload(elementId, subject) {
3670
4169
  subjectId: externalId ?? elementId,
3671
4170
  externalId,
3672
4171
  nextRefSubject: externalId && subject?.name ? `${subject.name}=${externalId}` : null,
3673
- status: externalId ? 'ready' : 'pending_external_id',
4172
+ status: subject?.status ?? (externalId ? 'ready' : 'pending_external_id'),
4173
+ taskStatus: subject?.taskStatus ?? null,
4174
+ errorMessage: subject?.errorMessage ?? null,
4175
+ isTerminal: externalId ? true : Boolean(subject?.isTerminal),
3674
4176
  subject,
3675
4177
  };
3676
4178
  }
@@ -3681,6 +4183,7 @@ export async function subjectPublish(kwargs = {}) {
3681
4183
  if (!specs.some((item) => item.isPrimary)) {
3682
4184
  throw argumentError('缺少主体主参考图', '传 --resource primary:<path|url>(或 --resource three-view:<path|url> 自动升为主图)。');
3683
4185
  }
4186
+ const modelCodeResolution = resolveSubjectCreateModelCode(kwargs);
3684
4187
  if (toBool(kwargs.dryRun)) {
3685
4188
  const assets = specs.map((item) => ({
3686
4189
  label: item.label,
@@ -3692,15 +4195,15 @@ export async function subjectPublish(kwargs = {}) {
3692
4195
  }));
3693
4196
  return {
3694
4197
  dryRun: true,
3695
- action: 'subject publish',
4198
+ action: 'create subject',
3696
4199
  name,
3697
- request: buildSubjectCreateBody(kwargs, specs, assets),
4200
+ request: buildSubjectCreateBody(kwargs, specs, assets, modelCodeResolution),
3698
4201
  assets,
3699
4202
  localFiles: await inspectLocalFiles(specs.map((item) => item.file).filter(Boolean)),
3700
4203
  nextRefSubject: `${name}=<externalId>`,
3701
4204
  };
3702
4205
  }
3703
- ensureConfirmed(kwargs, '创建平台主体 element 是云端写入动作,需要确认', { action: 'subject publish', name });
4206
+ ensureConfirmed(kwargs, '创建平台主体 element 是云端写入动作,需要确认', { action: 'create subject', name });
3704
4207
  const assets = [];
3705
4208
  for (const spec of specs) {
3706
4209
  const uploaded = spec.file ? await uploadLocalFile(spec.file, { sceneType: TASK_UPLOAD_SCENE.SUBJECT }) : null;
@@ -3714,11 +4217,11 @@ export async function subjectPublish(kwargs = {}) {
3714
4217
  ...(uploaded ? { upload: uploaded } : {}),
3715
4218
  });
3716
4219
  }
3717
- const body = buildSubjectCreateBody(kwargs, specs, assets);
4220
+ const body = buildSubjectCreateBody(kwargs, specs, assets, modelCodeResolution);
3718
4221
  const payload = await awbApi.createElement(body);
3719
4222
  const elementId = extractCreatedElementId(payload);
3720
4223
  if (!elementId) throw new LingjingAwbCliError('主体创建完成但未拿到 elementId', { type: 'api_error', exitCode: 1, details: payload });
3721
- const subject = await fetchSubjectByReqTaskId(elementId);
4224
+ const subject = await fetchSubjectByReqTaskId(elementId, { optional: true });
3722
4225
  const waitPayload = subjectWaitPayload(elementId, subject);
3723
4226
  return {
3724
4227
  created: true,
@@ -3738,11 +4241,20 @@ export async function subjectWait(kwargs = {}) {
3738
4241
  const elementId = requireValue(kwargs, 'elementId', 'element-id');
3739
4242
  const subject = await waitForSubjectExternalId(elementId, kwargs);
3740
4243
  const payload = subjectWaitPayload(elementId, subject);
4244
+ const failure = subjectFailureDetails(subject, 'subject');
4245
+ if (failure) {
4246
+ throw new LingjingAwbCliError(failure.errorMessage, {
4247
+ type: 'subject_failed',
4248
+ exitCode: 1,
4249
+ hint: '主体创建任务已失败,请根据 error.details.errorMessage 调整参考资源/模型后重新创建。',
4250
+ details: payload,
4251
+ });
4252
+ }
3741
4253
  if (!payload.externalId) {
3742
4254
  throw new LingjingAwbCliError('主体 externalId 尚未回填', {
3743
4255
  type: 'subject_still_pending',
3744
4256
  exitCode: 20,
3745
- hint: '稍后重试 subject wait,或用 subject list --name 查询。',
4257
+ hint: '稍后重试 create subject-wait,或用 create subject-list --name 查询。',
3746
4258
  details: payload,
3747
4259
  });
3748
4260
  }
@@ -3758,7 +4270,7 @@ export async function subjectPublishBatch(kwargs = {}) {
3758
4270
  }
3759
4271
  return { dryRun: true, count: items.length, results };
3760
4272
  }
3761
- ensureConfirmed(kwargs, '批量发布主体资产是云端写入动作,需要确认', { action: 'subject publish-batch', count: items.length });
4273
+ ensureConfirmed(kwargs, '批量发布主体资产是云端写入动作,需要确认', { action: 'create subject-batch', count: items.length });
3762
4274
  const results = await runConcurrent(items, kwargs.concurrency ?? 1, async (item, index) => ({
3763
4275
  inputIndex: index,
3764
4276
  status: 'success',
@@ -3768,44 +4280,73 @@ export async function subjectPublishBatch(kwargs = {}) {
3768
4280
  }
3769
4281
 
3770
4282
  export async function subtitleRemove(kwargs = {}) {
3771
- const videoUrl = requireValue(kwargs, 'videoUrl', 'video-url');
4283
+ const built = await buildSubtitleRemovalRequest(kwargs, { dryRun: toBool(kwargs.dryRun) });
3772
4284
  if (toBool(kwargs.dryRun)) {
3773
- return { dryRun: true, action: 'video subtitle-remove', request: { input_type: 'url', video_url: videoUrl } };
3774
- }
3775
- ensureConfirmed(kwargs, '提交视频去字幕任务是云端写入动作,需要确认', { action: 'video subtitle-remove', videoUrl });
3776
- const payload = await awbApi.createSubtitleRemoveTask({
3777
- input_type: 'url',
3778
- video_url: videoUrl,
3779
- ...(kwargs.name ? { name: kwargs.name } : {}),
3780
- ...(kwargs.taskId ? { task_id: kwargs.taskId } : {}),
3781
- ...(kwargs.callbackUrl ? { callback_url: kwargs.callbackUrl } : {}),
4285
+ return {
4286
+ dryRun: true,
4287
+ action: 'create video-subtitle-removal',
4288
+ request: built.request,
4289
+ materialEndpoint: '/api/material/creation/videoSubtitleRemoval',
4290
+ taskType: 'VIDEO_SUBTITLE_REMOVAL',
4291
+ };
4292
+ }
4293
+ ensureConfirmed(kwargs, '提交视频去字幕任务会通过 material 创建任务并消耗积分,需要确认', {
4294
+ action: 'create video-subtitle-removal',
4295
+ sourceTaskId: built.sourceTaskId,
3782
4296
  });
3783
- return {
4297
+ const feePayload = await awbApi.fetchVideoSubtitleRemovalFee(built.request).catch(() => null);
4298
+ const estimate = await pointEstimate(feePayload, built.projectGroupNo).catch(() => ({}));
4299
+ const payload = await awbApi.createVideoSubtitleRemovalTask(built.request);
4300
+ const result = normalizeCreatedTask(payload, {
4301
+ ...estimate,
3784
4302
  submitted: true,
3785
- remoteTaskId: payload?.remote_task_id ?? payload?.remoteTaskId ?? null,
3786
- publicId: payload?.public_id ?? payload?.publicId ?? payload?.id ?? null,
4303
+ taskType: 'VIDEO_SUBTITLE_REMOVAL',
4304
+ sourceTaskId: built.sourceTaskId,
4305
+ projectGroupNo: built.projectGroupNo,
4306
+ });
4307
+ await appendTaskRecord(kwargs, {
4308
+ taskId: result.taskId,
4309
+ taskType: 'VIDEO_SUBTITLE_REMOVAL',
4310
+ projectGroupNo: built.projectGroupNo,
4311
+ promptSummary: `去字幕 sourceTaskId=${built.sourceTaskId}`,
4312
+ });
4313
+ return {
4314
+ ...result,
4315
+ nextCommand: result.taskId
4316
+ ? `lj-awb task video-subtitle-status --task-id ${result.taskId}${built.projectGroupNo ? ` --project-group-no ${built.projectGroupNo}` : ''} -f json`
4317
+ : null,
4318
+ };
4319
+ }
4320
+
4321
+ async function buildSubtitleRemovalRequest(kwargs = {}, options = {}) {
4322
+ const sourceTaskId = trimToNull(kwargs.sourceTaskId);
4323
+ if (!sourceTaskId) {
4324
+ throw argumentError('缺少来源视频任务 ID', '传 --source-task-id <taskId>;必须是 material 中已成功的视频任务。');
4325
+ }
4326
+ const dryRun = Boolean(options.dryRun);
4327
+ const projectGroupNo = await resolveProjectGroupNo(kwargs.projectGroupNo, {
4328
+ allowNull: true,
4329
+ noNetwork: dryRun,
4330
+ noSave: dryRun,
4331
+ }).catch((error) => {
4332
+ if (dryRun) return null;
4333
+ throw error;
4334
+ });
4335
+ return {
4336
+ sourceTaskId,
4337
+ projectGroupNo,
4338
+ request: compactRecord({
4339
+ sourceTaskId,
4340
+ projectGroupNo,
4341
+ }),
3787
4342
  };
3788
4343
  }
3789
4344
 
3790
4345
  export async function subtitleStatus(kwargs = {}) {
3791
- const publicId = trimToNull(kwargs.publicId);
3792
- const remoteTaskId = trimToNull(kwargs.remoteTaskId);
3793
- if (!publicId && !remoteTaskId) throw argumentError('缺少去字幕任务 ID', '传 --public-id 或 --remote-task-id。');
3794
- const payload = publicId
3795
- ? await awbApi.fetchSubtitleTaskByPublicId(publicId)
3796
- : await awbApi.fetchSubtitleTaskByRemoteId(remoteTaskId);
3797
- const resultUrls = uniqueNonEmpty([
3798
- ...parseListArg(payload?.result_url ?? payload?.resultUrl),
3799
- ...parseListArg(payload?.output_url ?? payload?.outputUrl),
3800
- ...parseListArg(payload?.url),
3801
- ]);
3802
- return compactRecord({
3803
- publicId: publicId ?? payload?.public_id ?? payload?.publicId ?? payload?.id ?? null,
3804
- remoteTaskId: remoteTaskId ?? payload?.remote_task_id ?? payload?.remoteTaskId ?? null,
3805
- taskStatus: payload?.status ?? payload?.taskStatus ?? payload?.state ?? null,
3806
- isTerminal: isTerminalTaskStatus(payload?.status ?? payload?.taskStatus ?? payload?.state),
3807
- resultCount: resultUrls.length,
3808
- ...(resultUrls.length ? { resultUrls } : {}),
3809
- errorMessage: payload?.error_message ?? payload?.errorMessage ?? payload?.message ?? null,
4346
+ const taskId = requireValue(kwargs, 'taskId', 'task-id');
4347
+ return taskStatus({
4348
+ ...kwargs,
4349
+ taskId,
4350
+ taskType: 'VIDEO_SUBTITLE_REMOVAL',
3810
4351
  });
3811
4352
  }