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

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,18 @@ 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']);
49
61
 
50
62
  export function normalizeUserInfo(payload) {
51
63
  const data = payload && typeof payload === 'object' ? payload : {};
@@ -371,7 +383,6 @@ export async function doctor(kwargs = {}) {
371
383
  },
372
384
  origins: {
373
385
  apiOrigin: API_ORIGIN,
374
- assetEditOrigin: awbApi.ASSET_EDIT_ORIGIN,
375
386
  },
376
387
  auth: summarizeAuth(auth, authContext.accessKeyInfo),
377
388
  paths: {
@@ -683,6 +694,7 @@ function normalizeModelRows(payload, kind, options = {}) {
683
694
  const row = compactRecord({
684
695
  modelGroupCode,
685
696
  displayName: item?.modelName ?? item?.name ?? item?.label ?? null,
697
+ modelDesc: item?.modelDesc ?? null,
686
698
  provider: item?.provider ?? item?.vendor ?? item?.supplier ?? item?.componyName ?? null,
687
699
  enabled: item?.enabled ?? item?.available ?? item?.status ?? null,
688
700
  modelStatus: item?.modelStatus ?? null,
@@ -898,9 +910,9 @@ function resourceSourceKindForValidation(detail = {}) {
898
910
  }
899
911
 
900
912
  function sourceAllowedByRule(rule = {}, sourceKind) {
901
- const sources = Array.isArray(rule.sources) ? rule.sources : [];
902
- if (!sources.length) return true;
903
- return sources.includes(sourceKind);
913
+ const shapes = Array.isArray(rule.valueShapes) ? rule.valueShapes : [];
914
+ if (!shapes.length) return true;
915
+ return shapes.includes(sourceKind);
904
916
  }
905
917
 
906
918
  function resourceFormatForValidation(detail = {}) {
@@ -1033,7 +1045,7 @@ function assertResourceShapeAgainstModel(resourceDetails = [], resourceRules = [
1033
1045
  if (!sourceAllowedByRule(rule, sourceKind)) {
1034
1046
  throw argumentError(
1035
1047
  `模型不支持 ${resourceText(resource)} 的来源:${sourceKind}`,
1036
- `该资源允许来源:${(rule.sources || []).join(', ') || '未限制'}。`,
1048
+ `该资源允许的 value 形状:${(rule.valueShapes || []).join(', ') || '未限制'}。`,
1037
1049
  );
1038
1050
  }
1039
1051
  const format = resourceFormatForValidation(detail);
@@ -1759,7 +1771,7 @@ function createSpecResourceRequirements(inputModes) {
1759
1771
  intent: item.intent,
1760
1772
  resource: item.requiredResources,
1761
1773
  syntax: item.resourceSyntax,
1762
- sources: item.sourceKinds,
1774
+ valueShapes: item.sourceKinds,
1763
1775
  ...item.resourceLimits,
1764
1776
  promptBinding: item.promptBinding,
1765
1777
  constraints: item.resourceConstraints,
@@ -1898,7 +1910,7 @@ function createSpecValidationRules(taskKind) {
1898
1910
  ];
1899
1911
  if (taskKind === 'video') {
1900
1912
  rules.splice(4, 0, 'reference_key 仅用于视频 reference 资源的可选 prompt 占位符绑定;prompt 中出现 <<<key>>> 时,必须有同名 reference 资源。');
1901
- rules.push('subject reference 必须使用 asset:<externalId>,externalId 应来自 subject publish + subject wait。');
1913
+ rules.push('subject reference 必须使用 asset:<externalId>,externalId 应来自 create subject + create subject-wait。');
1902
1914
  } else {
1903
1915
  rules.push('图片任务 resources 只接受 image reference,且不接受 reference_key 或 <<<key>>>;请用自然 prompt 描述图一、图二、参考图、主体等映射。');
1904
1916
  }
@@ -1907,8 +1919,8 @@ function createSpecValidationRules(taskKind) {
1907
1919
 
1908
1920
  function createSpecPreflight(taskKind) {
1909
1921
  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';
1922
+ const feeCommand = taskKind === 'image' ? 'create image-fee' : 'create video-fee';
1923
+ const createCommand = taskKind === 'image' ? 'create image' : 'create video';
1912
1924
  return [
1913
1925
  'doctor --verify',
1914
1926
  `${modelListCommand} --model <keyword>`,
@@ -1927,26 +1939,26 @@ function createSpecExamples(taskKind, supportedIntents) {
1927
1939
  const examples = [];
1928
1940
  const hasIntent = (mode) => supportedIntents.some((item) => item.mode === mode);
1929
1941
  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');
1942
+ if (hasIntent('prompt_only')) examples.push('lj-awb create image --model-group-code <code> --prompt "品牌吉祥物表情包四宫格" --ratio 1:1 --quality 1K --dry-run');
1943
+ if (hasIntent('reference')) examples.push('lj-awb create image --model-group-code <code> --prompt "保持参考图主体,生成海报" --resource image:reference=./ref.png --dry-run');
1932
1944
  } else {
1933
- if (hasIntent('prompt_only')) examples.push('lj-awb video create --model-group-code <code> --prompt "机器人站在白色展台中央,缓慢转身" --duration 6 --dry-run');
1945
+ if (hasIntent('prompt_only')) examples.push('lj-awb create video --model-group-code <code> --prompt "机器人站在白色展台中央,缓慢转身" --duration 6 --dry-run');
1934
1946
  if (hasIntent('reference')) {
1935
1947
  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');
1948
+ examples.push('lj-awb create video --model-group-code <code> --prompt "人物转身看向镜头" --resource image:reference=./hero.png --duration 5 --dry-run');
1937
1949
  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');
1950
+ 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
1951
  }
1940
1952
  }
1941
1953
  if (hasIntent('frames')) {
1942
1954
  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');
1955
+ examples.push('lj-awb create video --model-group-code <code> --prompt "镜头缓慢推进" --resource image:first_frame=./first.png --duration 5 --dry-run');
1956
+ examples.push('lj-awb create video --model-group-code <code> --prompt "镜头缓慢推进" --resource image:first_frame=asset:<assetId> --duration 5 --dry-run');
1945
1957
  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');
1958
+ 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
1959
  }
1948
1960
  }
1949
- if (hasIntent('storyboard')) examples.push('lj-awb video create --model-group-code <code> --resources-json ./storyboard.json --duration 5 --dry-run');
1961
+ if (hasIntent('storyboard')) examples.push('lj-awb create video --model-group-code <code> --resources-json ./storyboard.json --duration 5 --dry-run');
1950
1962
  }
1951
1963
  return examples;
1952
1964
  }
@@ -2019,7 +2031,7 @@ function modelOptionsResources(inputModes = []) {
2019
2031
  mode: 'frames',
2020
2032
  mediaType: 'IMAGE',
2021
2033
  usage: supportedFrameMode.optionResourceUsages,
2022
- sources: supportedFrameMode.sourceKinds,
2034
+ valueShapes: supportedFrameMode.sourceKinds,
2023
2035
  ...supportedFrameMode.optionResourceLimits,
2024
2036
  formatPolicy: framePolicy.summary,
2025
2037
  webpSupported: framePolicy.webpSupported,
@@ -2036,7 +2048,7 @@ function modelOptionsResources(inputModes = []) {
2036
2048
  mode: optionResourceMode(sourceMode),
2037
2049
  mediaType,
2038
2050
  usage: optionResourceUsage(sourceMode),
2039
- sources: item.sources,
2051
+ valueShapes: item.valueShapes,
2040
2052
  fileTypes: sourceMode === 'subject_reference' ? undefined : item.fileTypes,
2041
2053
  formatPolicy: imagePolicy?.summary,
2042
2054
  webpSupported: imagePolicy?.webpSupported,
@@ -2074,7 +2086,7 @@ export function modelInputGuide() {
2074
2086
  { field: 'resources[].type', values: ['image', 'video', 'audio', 'subject'], description: '资源本体类型;subject 表示已创建的主体对象。' },
2075
2087
  { field: 'resources[].usage', values: ['first_frame', 'last_frame', 'reference', 'keyframe'], description: '素材用途。' },
2076
2088
  { 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' },
2089
+ { 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
2090
  { field: 'resources[].source.value', description: '资源值,必填。url 传素材地址;asset_id 传资源 ID。' },
2079
2091
  { field: 'resources[].order', description: '仅 usage=keyframe 时需要,且同一请求内不能重复。' },
2080
2092
  { field: 'resources[].duration', description: '仅 keyframe 场景下用于表达该帧持续时长,可传小数秒。' },
@@ -2133,8 +2145,8 @@ export async function modelCreateSpec(kwargs = {}) {
2133
2145
  schemaVersion: 1,
2134
2146
  modelGroupCode,
2135
2147
  taskKind: context.taskKind,
2136
- createCommand: context.taskKind === 'image' ? 'image create' : 'video create',
2137
- feeCommand: context.taskKind === 'image' ? 'image fee' : 'video fee',
2148
+ createCommand: context.taskKind === 'image' ? 'create image' : 'create video',
2149
+ feeCommand: context.taskKind === 'image' ? 'create image-fee' : 'create video-fee',
2138
2150
  statusCommandTaskType: context.taskKind === 'image' ? 'IMAGE_CREATE' : 'VIDEO_GROUP',
2139
2151
  optionsCommand: `model options --model-group-code ${modelGroupCode}`,
2140
2152
  model: context.model,
@@ -2259,6 +2271,113 @@ export async function uploadLocalFile(filePath, options = {}) {
2259
2271
  });
2260
2272
  }
2261
2273
 
2274
+ function isHttpUrl(value) {
2275
+ return /^https?:\/\//i.test(String(value || '').trim());
2276
+ }
2277
+
2278
+ function isUploadedMaterialReference(value) {
2279
+ const text = trimToNull(value);
2280
+ if (!text) return false;
2281
+ if (text.startsWith('material/') || text.startsWith('/material/')) return true;
2282
+ if (!isHttpUrl(text)) return false;
2283
+ try {
2284
+ const url = new URL(text);
2285
+ return url.hostname.endsWith('.myqcloud.com');
2286
+ } catch {
2287
+ return false;
2288
+ }
2289
+ }
2290
+
2291
+ function remoteFileNameFromUrl(remoteUrl, fallback = 'voice-audio') {
2292
+ try {
2293
+ const pathname = decodeURIComponent(new URL(remoteUrl).pathname);
2294
+ const baseName = safeFileName(path.basename(pathname));
2295
+ if (baseName && baseName !== '.' && baseName !== '/') return baseName;
2296
+ } catch {}
2297
+ return `${fallback}.mp3`;
2298
+ }
2299
+
2300
+ function extensionFromContentType(contentType = '') {
2301
+ const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
2302
+ if (normalized === 'audio/mpeg') return '.mp3';
2303
+ if (normalized === 'audio/wav' || normalized === 'audio/x-wav') return '.wav';
2304
+ if (normalized === 'audio/mp4') return '.m4a';
2305
+ if (normalized === 'audio/aac') return '.aac';
2306
+ if (normalized === 'audio/ogg') return '.ogg';
2307
+ if (normalized === 'video/mp4') return '.mp4';
2308
+ return '';
2309
+ }
2310
+
2311
+ function isSupportedRemoteVoiceDownload(contentType = '', fileName = '') {
2312
+ const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
2313
+ const extension = path.extname(fileName).toLowerCase();
2314
+ if (REMOTE_VOICE_MIME_TYPES.has(normalized)) return true;
2315
+ if ((!normalized || normalized === 'application/octet-stream') && REMOTE_VOICE_EXTENSIONS.has(extension)) return true;
2316
+ return false;
2317
+ }
2318
+
2319
+ async function downloadRemoteFileToTemp(remoteUrl, options = {}) {
2320
+ const timeoutMs = Math.max(1_000, toInt(options.timeoutMs, REMOTE_VOICE_DOWNLOAD_TIMEOUT_MS));
2321
+ const maxBytes = Math.max(1, toInt(options.maxBytes, REMOTE_VOICE_DOWNLOAD_MAX_BYTES));
2322
+ const controller = new AbortController();
2323
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2324
+ let dir = null;
2325
+ let response;
2326
+ try {
2327
+ response = await fetch(remoteUrl, { signal: controller.signal });
2328
+ if (!response.ok) {
2329
+ throw new LingjingAwbCliError(`下载远程音频失败:${response.status} ${response.statusText}`, {
2330
+ type: 'http_error',
2331
+ exitCode: 30,
2332
+ details: { url: remoteUrl, status: response.status },
2333
+ });
2334
+ }
2335
+ const contentType = response.headers.get('content-type') || '';
2336
+ let fileName = remoteFileNameFromUrl(remoteUrl, options.fallbackName || 'voice-audio');
2337
+ if (!path.extname(fileName)) fileName = `${fileName}${extensionFromContentType(contentType) || '.mp3'}`;
2338
+ if (!isSupportedRemoteVoiceDownload(contentType, fileName)) {
2339
+ throw argumentError(
2340
+ `远程音频类型不支持:${contentType || path.extname(fileName) || 'unknown'}`,
2341
+ 'create subject-voice 只支持 mp3、wav、m4a、aac、ogg 音频或 mp4 视频来源。',
2342
+ );
2343
+ }
2344
+ const contentLength = toInt(response.headers.get('content-length'), 0);
2345
+ if (contentLength > maxBytes) {
2346
+ throw argumentError(`远程音频过大:${contentLength} bytes`, `最大支持 ${maxBytes} bytes。`);
2347
+ }
2348
+ dir = await fs.mkdtemp(path.join(tmpdir(), 'lj-awb-voice-'));
2349
+ const filePath = path.join(dir, fileName);
2350
+ const file = await fs.open(filePath, 'w');
2351
+ let size = 0;
2352
+ try {
2353
+ if (!response.body) throw new Error('empty response body');
2354
+ for await (const chunk of response.body) {
2355
+ const buffer = Buffer.from(chunk);
2356
+ size += buffer.length;
2357
+ if (size > maxBytes) {
2358
+ throw argumentError(`远程音频过大:超过 ${maxBytes} bytes`);
2359
+ }
2360
+ await file.write(buffer);
2361
+ }
2362
+ } finally {
2363
+ await file.close();
2364
+ }
2365
+ return { filePath, dir, contentType, size };
2366
+ } catch (error) {
2367
+ if (dir) await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
2368
+ if (error instanceof LingjingAwbCliError) throw error;
2369
+ const isAbort = error?.name === 'AbortError';
2370
+ throw new LingjingAwbCliError(`下载远程音频失败:${error.message}`, {
2371
+ type: 'network_error',
2372
+ exitCode: 30,
2373
+ hint: isAbort ? `远程下载超过 ${timeoutMs}ms,请改用本地 --file 或已上传 material 路径。` : '',
2374
+ details: { url: remoteUrl, causeCode: error?.cause?.code },
2375
+ });
2376
+ } finally {
2377
+ clearTimeout(timeout);
2378
+ }
2379
+ }
2380
+
2262
2381
  function resolveCustomBizId(explicit) {
2263
2382
  return trimToNull(explicit);
2264
2383
  }
@@ -2330,7 +2449,10 @@ function normalizeResourceSource(source, type, index, options = {}) {
2330
2449
  ? explicitKind.toLowerCase()
2331
2450
  : (rawValue?.startsWith('asset:') || type === 'subject' ? 'asset_id' : 'url');
2332
2451
  if (!['url', 'asset_id'].includes(kind)) {
2333
- throw argumentError(`resource[${index}] source.kind 不支持:${kind}`, '支持 url、asset_id。');
2452
+ throw argumentError(
2453
+ `resource[${index}] source.kind 不支持:${explicitKind}`,
2454
+ '只支持 url、asset_id 两个值。本地文件 / http(s) URL / material backendPath 都统一传 kind=url(CLI 按 value 自动识别);平台资产或主体对象传 kind=asset_id。',
2455
+ );
2334
2456
  }
2335
2457
  const value = kind === 'asset_id' ? assetValue : rawValue;
2336
2458
  if (!value) {
@@ -2765,7 +2887,7 @@ export async function imageFee(kwargs = {}) {
2765
2887
  const built = await buildImageRequest(kwargs, { dryRun: true, includeCustomBizId: false });
2766
2888
  return compactRecord({
2767
2889
  dryRun: true,
2768
- action: 'image fee',
2890
+ action: 'create image-fee',
2769
2891
  request: built.request,
2770
2892
  ...(built.resourceConversions.length ? { resourceConversions: built.resourceConversions } : {}),
2771
2893
  });
@@ -2820,13 +2942,13 @@ export async function imageCreate(kwargs = {}) {
2820
2942
  const built = await buildImageRequest(kwargs, { dryRun: true });
2821
2943
  return compactRecord({
2822
2944
  dryRun: true,
2823
- action: 'image create',
2945
+ action: 'create image',
2824
2946
  request: built.request,
2825
2947
  ...(built.localFiles.length ? { localFiles: built.localFiles } : {}),
2826
2948
  ...(built.resourceConversions.length ? { resourceConversions: built.resourceConversions } : {}),
2827
2949
  });
2828
2950
  }
2829
- ensureConfirmed(kwargs, '正式生图会消耗积分,需要确认', { action: 'image create' });
2951
+ ensureConfirmed(kwargs, '正式生图会消耗积分,需要确认', { action: 'create image' });
2830
2952
  const built = await buildImageRequest(kwargs);
2831
2953
  const feePayload = await awbApi.fetchImageFee(built.request).catch(() => null);
2832
2954
  const estimate = await pointEstimate(feePayload, built.projectGroupNo).catch(() => ({}));
@@ -2919,7 +3041,7 @@ export async function videoFee(kwargs = {}) {
2919
3041
  const built = await buildVideoRequest(kwargs, { dryRun: true, includeCustomBizId: false });
2920
3042
  return compactRecord({
2921
3043
  dryRun: true,
2922
- action: 'video fee',
3044
+ action: 'create video-fee',
2923
3045
  request: built.request,
2924
3046
  ...(built.resourceConversions.length ? { resourceConversions: built.resourceConversions } : {}),
2925
3047
  });
@@ -2937,13 +3059,13 @@ export async function videoCreate(kwargs = {}) {
2937
3059
  const built = await buildVideoRequest(kwargs, { dryRun: true });
2938
3060
  return compactRecord({
2939
3061
  dryRun: true,
2940
- action: 'video create',
3062
+ action: 'create video',
2941
3063
  request: built.request,
2942
3064
  ...(built.localFiles.length ? { localFiles: built.localFiles } : {}),
2943
3065
  ...(built.resourceConversions.length ? { resourceConversions: built.resourceConversions } : {}),
2944
3066
  });
2945
3067
  }
2946
- ensureConfirmed(kwargs, '正式生视频会消耗积分,需要确认', { action: 'video create' });
3068
+ ensureConfirmed(kwargs, '正式生视频会消耗积分,需要确认', { action: 'create video' });
2947
3069
  const built = await buildVideoRequest(kwargs);
2948
3070
  const feePayload = await awbApi.fetchVideoFee(built.request).catch(() => null);
2949
3071
  const estimate = await pointEstimate(feePayload, built.projectGroupNo).catch(() => ({}));
@@ -3079,7 +3201,7 @@ export async function imageCreateBatch(kwargs = {}) {
3079
3201
  }
3080
3202
  return { dryRun: true, count: items.length, results };
3081
3203
  }
3082
- ensureConfirmed(kwargs, '批量生图会多次消耗积分,需要确认', { action: 'image create-batch', count: items.length });
3204
+ ensureConfirmed(kwargs, '批量生图会多次消耗积分,需要确认', { action: 'create image-batch', count: items.length });
3083
3205
  const results = await runConcurrent(items, kwargs.concurrency ?? 1, async (item, index) => ({
3084
3206
  inputIndex: index,
3085
3207
  status: 'success',
@@ -3098,7 +3220,7 @@ export async function videoCreateBatch(kwargs = {}) {
3098
3220
  }
3099
3221
  return { dryRun: true, count: items.length, results };
3100
3222
  }
3101
- ensureConfirmed(kwargs, '批量生视频会多次消耗积分,需要确认', { action: 'video create-batch', count: items.length });
3223
+ ensureConfirmed(kwargs, '批量生视频会多次消耗积分,需要确认', { action: 'create video-batch', count: items.length });
3102
3224
  const results = await runConcurrent(items, kwargs.concurrency ?? 1, async (item, index) => ({
3103
3225
  inputIndex: index,
3104
3226
  status: 'success',
@@ -3384,12 +3506,64 @@ function normalizeSubjectTagIds(tagList) {
3384
3506
  return uniqueNonEmpty(tagList.map((item) => (typeof item === 'string' ? item : item?.tagId ?? item?.id ?? item?.tagCode)));
3385
3507
  }
3386
3508
 
3509
+ function normalizeSubjectTaskMeta(item = {}) {
3510
+ const rawTaskStatus = item?.taskStatus ?? item?.task_status ?? null;
3511
+ const taskStatus = rawTaskStatus ? taskStatusText(rawTaskStatus) : null;
3512
+ const errorMessage = trimToNull(
3513
+ item?.errorMessage
3514
+ ?? item?.error_msg
3515
+ ?? item?.errorMsg
3516
+ ?? item?.resultMessage
3517
+ ?? item?.result_msg
3518
+ ?? item?.resultMsg
3519
+ ?? item?.message
3520
+ ?? item?.msg,
3521
+ );
3522
+ return {
3523
+ taskStatus,
3524
+ errorMessage,
3525
+ isTerminal: taskStatus ? isTerminalTaskStatus(taskStatus) : false,
3526
+ isSuccess: taskStatus ? isSuccessTaskStatus(taskStatus) : false,
3527
+ };
3528
+ }
3529
+
3530
+ function subjectResourceStatus(externalId, taskMeta) {
3531
+ if (externalId) return 'ready';
3532
+ if (taskMeta?.taskStatus && taskMeta.isTerminal) {
3533
+ return taskMeta.isSuccess ? 'missing_external_id' : 'failed';
3534
+ }
3535
+ return 'pending_external_id';
3536
+ }
3537
+
3538
+ function subjectFailureDetails(record, kind = 'subject') {
3539
+ if (!record) return null;
3540
+ const taskStatus = taskStatusText(record.taskStatus);
3541
+ if (!taskStatus || !isTerminalTaskStatus(taskStatus)) return null;
3542
+ const fallback = kind === 'voice'
3543
+ ? '主体音色创建失败'
3544
+ : '主体创建失败';
3545
+ if (!isSuccessTaskStatus(taskStatus)) {
3546
+ return {
3547
+ taskStatus,
3548
+ errorMessage: record.errorMessage || fallback,
3549
+ };
3550
+ }
3551
+ if (!record.externalId) {
3552
+ return {
3553
+ taskStatus,
3554
+ errorMessage: record.errorMessage || `${fallback}:任务已终态但未返回externalId`,
3555
+ };
3556
+ }
3557
+ return null;
3558
+ }
3559
+
3387
3560
  function normalizeSubjectRecord(item = {}) {
3388
3561
  const referList = Array.isArray(item?.elementReferList) ? item.elementReferList : [];
3389
3562
  const videoList = Array.isArray(item?.elementVideoList) ? item.elementVideoList : [];
3390
3563
  const elementId = item?.id ?? item?.subjectId ?? item?.elementId ?? null;
3391
3564
  const externalId = item?.externalId ?? item?.external_id ?? null;
3392
3565
  const name = item?.elementName ?? item?.name ?? item?.subjectName ?? null;
3566
+ const taskMeta = normalizeSubjectTaskMeta(item);
3393
3567
  return {
3394
3568
  subjectId: externalId ?? elementId,
3395
3569
  elementId,
@@ -3412,6 +3586,33 @@ function normalizeSubjectRecord(item = {}) {
3412
3586
  tagIds: normalizeSubjectTagIds(item?.tagList),
3413
3587
  ...(trimToNull(item?.elementVoiceId) ? { voiceId: trimToNull(item.elementVoiceId) } : {}),
3414
3588
  ...(externalId && name ? { nextRefSubject: `${name}=${externalId}` } : {}),
3589
+ taskStatus: taskMeta.taskStatus,
3590
+ errorMessage: taskMeta.errorMessage,
3591
+ isTerminal: externalId ? true : taskMeta.isTerminal,
3592
+ status: subjectResourceStatus(externalId, taskMeta),
3593
+ createdAt: item?.gmtCreate ?? item?.createdAt ?? null,
3594
+ };
3595
+ }
3596
+
3597
+ function normalizeSubjectVoiceRecord(item = {}) {
3598
+ const voiceRecordId = item?.voiceRecordId ?? item?.id ?? item?.reqTaskId ?? item?.voiceTaskId ?? null;
3599
+ const reqTaskId = item?.reqTaskId ?? voiceRecordId;
3600
+ const externalId = item?.externalId ?? item?.external_id ?? item?.voice_id ?? item?.voiceId ?? null;
3601
+ const name = item?.voiceName ?? item?.name ?? null;
3602
+ const taskMeta = normalizeSubjectTaskMeta(item);
3603
+ return {
3604
+ voiceRecordId,
3605
+ reqTaskId,
3606
+ voiceId: externalId,
3607
+ externalId,
3608
+ name,
3609
+ voiceUrl: item?.voiceUrl ?? item?.voice_url ?? item?.audioUrl ?? item?.audio_url ?? null,
3610
+ videoId: item?.videoId ?? item?.video_id ?? null,
3611
+ status: subjectResourceStatus(externalId, taskMeta),
3612
+ taskStatus: taskMeta.taskStatus,
3613
+ errorMessage: taskMeta.errorMessage,
3614
+ isTerminal: externalId ? true : taskMeta.isTerminal,
3615
+ ...(externalId ? { nextVoiceArg: `--voice-id ${externalId}` } : {}),
3415
3616
  createdAt: item?.gmtCreate ?? item?.createdAt ?? null,
3416
3617
  };
3417
3618
  }
@@ -3463,8 +3664,8 @@ export async function assetGroupCreate(kwargs = {}) {
3463
3664
  description: kwargs.description ?? '',
3464
3665
  projectName: kwargs.projectName ?? 'default',
3465
3666
  };
3466
- if (toBool(kwargs.dryRun)) return { dryRun: true, action: 'asset group-create', request: body };
3467
- ensureConfirmed(kwargs, '创建素材组是云端写入动作,需要确认', { action: 'asset group-create', body });
3667
+ if (toBool(kwargs.dryRun)) return { dryRun: true, action: 'create asset-group', request: body };
3668
+ ensureConfirmed(kwargs, '创建素材组是云端写入动作,需要确认', { action: 'create asset-group', body });
3468
3669
  const payload = await awbApi.createAssetGroup(body);
3469
3670
  return { created: true, groupId: payload?.id ?? payload?.groupId ?? payload ?? null, name, projectName: body.projectName };
3470
3671
  }
@@ -3476,8 +3677,8 @@ export async function assetGroupUpdate(kwargs = {}) {
3476
3677
  if (kwargs.description != null) body.description = kwargs.description;
3477
3678
  if (kwargs.projectName != null) body.projectName = kwargs.projectName;
3478
3679
  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 });
3680
+ if (toBool(kwargs.dryRun)) return { dryRun: true, action: 'create asset-group-update', groupId, request: body };
3681
+ ensureConfirmed(kwargs, '更新素材组是云端写入动作,需要确认', { action: 'create asset-group-update', groupId, body });
3481
3682
  await awbApi.updateAssetGroup(groupId, body);
3482
3683
  return { updated: true, groupId, ...body };
3483
3684
  }
@@ -3497,7 +3698,7 @@ export async function assetRegister(kwargs = {}) {
3497
3698
  if (toBool(kwargs.dryRun)) {
3498
3699
  return {
3499
3700
  dryRun: true,
3500
- action: 'asset register',
3701
+ action: 'create asset',
3501
3702
  request: {
3502
3703
  assetGroupsId: groupId,
3503
3704
  url: localFile ? normalizeCosAssetPath(dryRunBackendPath(localFile, TASK_UPLOAD_SCENE.SUBJECT)) : assetPath,
@@ -3507,7 +3708,7 @@ export async function assetRegister(kwargs = {}) {
3507
3708
  localFile: localFile ? await inspectLocalFile(localFile) : null,
3508
3709
  };
3509
3710
  }
3510
- ensureConfirmed(kwargs, '注册素材是云端写入动作,需要确认', { action: 'asset register', groupId, name });
3711
+ ensureConfirmed(kwargs, '注册素材是云端写入动作,需要确认', { action: 'create asset', groupId, name });
3511
3712
  const uploaded = localFile ? await uploadLocalFile(localFile, { sceneType: TASK_UPLOAD_SCENE.SUBJECT }) : null;
3512
3713
  const body = {
3513
3714
  assetGroupsId: groupId,
@@ -3542,6 +3743,194 @@ export async function subjectList(kwargs = {}) {
3542
3743
  };
3543
3744
  }
3544
3745
 
3746
+ export async function subjectVoiceList(kwargs = {}) {
3747
+ const keyword = trimToNull(kwargs.keyword ?? kwargs.name);
3748
+ const payload = keyword
3749
+ ? await awbApi.listVoicesByName(keyword)
3750
+ : await awbApi.listVoices();
3751
+ const voices = paginateRows(
3752
+ normalizeRows(payload).map((item) => normalizeSubjectVoiceRecord(item)),
3753
+ kwargs.pageNumber,
3754
+ kwargs.pageSize,
3755
+ );
3756
+ return {
3757
+ voices,
3758
+ ...(toBool(kwargs.includeRaw) ? { raw: payload } : {}),
3759
+ };
3760
+ }
3761
+
3762
+ function subjectVoiceSource(kwargs = {}) {
3763
+ const file = trimToNull(kwargs.file);
3764
+ const voiceUrlArg = trimToNull(kwargs.voiceUrl);
3765
+ const audioUrlArg = trimToNull(kwargs.audioUrl);
3766
+ if (voiceUrlArg && audioUrlArg) {
3767
+ throw argumentError('音色来源只能选择一种', '--voice-url 和 --audio-url 是同一含义的别名,只能传一个。');
3768
+ }
3769
+ const voiceUrl = voiceUrlArg ?? audioUrlArg ?? trimToNull(kwargs.url);
3770
+ const videoId = trimToNull(kwargs.videoId);
3771
+ const sourceCount = [file, voiceUrl, videoId].filter(Boolean).length;
3772
+ if (sourceCount === 0) {
3773
+ throw argumentError('缺少音色来源', '传 --file <audio>、--voice-url/--audio-url <material path> 或 --video-id <id>。');
3774
+ }
3775
+ if (sourceCount > 1) {
3776
+ throw argumentError('音色来源只能选择一种', '--file、--voice-url/--audio-url、--video-id 三者只能传一个。');
3777
+ }
3778
+ const shouldUploadRemoteUrl = Boolean(voiceUrl && isHttpUrl(voiceUrl) && !isUploadedMaterialReference(voiceUrl));
3779
+ return { file, voiceUrl, videoId, shouldUploadRemoteUrl };
3780
+ }
3781
+
3782
+ function buildSubjectVoiceCreateBody(kwargs, source, uploaded = null) {
3783
+ const name = requireValue(kwargs, 'name');
3784
+ const voiceUrl = uploaded
3785
+ ? normalizeCosAssetPath(uploaded.backendPath)
3786
+ : normalizeCosAssetPath(source.voiceUrl);
3787
+ return compactRecord({
3788
+ reqTaskId: trimToNull(kwargs.reqTaskId),
3789
+ voiceName: name,
3790
+ voiceUrl,
3791
+ videoId: source.videoId,
3792
+ });
3793
+ }
3794
+
3795
+ function extractCreatedVoiceRecordId(payload) {
3796
+ if (typeof payload === 'string') return payload;
3797
+ if (!payload || typeof payload !== 'object') return null;
3798
+ return payload.id
3799
+ ?? payload.voiceRecordId
3800
+ ?? payload.reqTaskId
3801
+ ?? payload.voiceTaskId
3802
+ ?? payload.data?.id
3803
+ ?? payload.data?.voiceRecordId
3804
+ ?? payload.data?.reqTaskId
3805
+ ?? null;
3806
+ }
3807
+
3808
+ async function fetchSubjectVoiceByReqTaskId(reqTaskId, options = {}) {
3809
+ if (!reqTaskId) return null;
3810
+ const payload = await awbApi.getVoiceByReqTaskId(reqTaskId).catch((error) => {
3811
+ if (options.optional) return null;
3812
+ throw error;
3813
+ });
3814
+ return payload && typeof payload === 'object' ? normalizeSubjectVoiceRecord(payload) : null;
3815
+ }
3816
+
3817
+ async function waitForSubjectVoiceExternalId(reqTaskId, options = {}) {
3818
+ const waitSeconds = Math.max(0, toInt(options.waitSeconds, 0));
3819
+ const pollIntervalMs = Math.max(500, toInt(options.pollIntervalMs, 2000));
3820
+ const deadline = Date.now() + waitSeconds * 1000;
3821
+ let latest = await fetchSubjectVoiceByReqTaskId(reqTaskId);
3822
+ while (waitSeconds > 0 && !latest?.externalId && !subjectFailureDetails(latest, 'voice') && Date.now() < deadline) {
3823
+ await sleep(pollIntervalMs);
3824
+ latest = await fetchSubjectVoiceByReqTaskId(reqTaskId);
3825
+ }
3826
+ return latest;
3827
+ }
3828
+
3829
+ function subjectVoiceWaitPayload(voiceRecordId, voice) {
3830
+ const externalId = voice?.externalId ?? null;
3831
+ return {
3832
+ voiceRecordId,
3833
+ reqTaskId: voice?.reqTaskId ?? voiceRecordId,
3834
+ voiceId: externalId,
3835
+ externalId,
3836
+ nextVoiceArg: externalId ? `--voice-id ${externalId}` : null,
3837
+ status: voice?.status ?? (externalId ? 'ready' : 'pending_external_id'),
3838
+ taskStatus: voice?.taskStatus ?? null,
3839
+ errorMessage: voice?.errorMessage ?? null,
3840
+ isTerminal: externalId ? true : Boolean(voice?.isTerminal),
3841
+ voice,
3842
+ };
3843
+ }
3844
+
3845
+ export async function subjectVoiceCreate(kwargs = {}) {
3846
+ const name = requireValue(kwargs, 'name');
3847
+ const source = subjectVoiceSource(kwargs);
3848
+ if (toBool(kwargs.dryRun)) {
3849
+ const localFiles = source.file ? [await inspectLocalFile(source.file)] : [];
3850
+ let dryRunSource = source;
3851
+ if (source.file) {
3852
+ dryRunSource = { ...source, voiceUrl: dryRunBackendPath(source.file, TASK_UPLOAD_SCENE.SUBJECT) };
3853
+ } else if (source.shouldUploadRemoteUrl) {
3854
+ dryRunSource = {
3855
+ ...source,
3856
+ voiceUrl: dryRunBackendPath(remoteFileNameFromUrl(source.voiceUrl), TASK_UPLOAD_SCENE.SUBJECT),
3857
+ };
3858
+ }
3859
+ return {
3860
+ dryRun: true,
3861
+ action: 'create subject-voice',
3862
+ name,
3863
+ request: buildSubjectVoiceCreateBody(kwargs, dryRunSource),
3864
+ localFiles,
3865
+ remoteUpload: source.shouldUploadRemoteUrl || undefined,
3866
+ remoteUrl: source.shouldUploadRemoteUrl ? source.voiceUrl : undefined,
3867
+ nextVoiceArg: '--voice-id <externalId>',
3868
+ };
3869
+ }
3870
+ ensureConfirmed(kwargs, '创建主体音色是云端写入动作,需要确认', { action: 'create subject-voice', name });
3871
+ let uploaded = null;
3872
+ let tempDownload = null;
3873
+ try {
3874
+ if (source.file) {
3875
+ uploaded = await uploadLocalFile(source.file, { sceneType: TASK_UPLOAD_SCENE.SUBJECT });
3876
+ } else if (source.shouldUploadRemoteUrl) {
3877
+ tempDownload = await downloadRemoteFileToTemp(source.voiceUrl, { fallbackName: safeFileName(name) || 'voice-audio' });
3878
+ uploaded = await uploadLocalFile(tempDownload.filePath, { sceneType: TASK_UPLOAD_SCENE.SUBJECT });
3879
+ }
3880
+ } finally {
3881
+ if (tempDownload?.dir) await fs.rm(tempDownload.dir, { recursive: true, force: true }).catch(() => {});
3882
+ }
3883
+ const body = buildSubjectVoiceCreateBody(kwargs, source, uploaded);
3884
+ const payload = await awbApi.createVoice(body);
3885
+ const voiceRecordId = extractCreatedVoiceRecordId(payload);
3886
+ if (!voiceRecordId) throw new LingjingAwbCliError('音色创建完成但未拿到 voiceRecordId', { type: 'api_error', exitCode: 1, details: payload });
3887
+ const voice = await fetchSubjectVoiceByReqTaskId(voiceRecordId, { optional: true });
3888
+ const waitPayload = subjectVoiceWaitPayload(voiceRecordId, voice);
3889
+ return {
3890
+ created: true,
3891
+ name,
3892
+ voiceRecordId,
3893
+ reqTaskId: waitPayload.reqTaskId,
3894
+ voiceId: waitPayload.voiceId,
3895
+ externalId: waitPayload.externalId,
3896
+ nextVoiceArg: waitPayload.nextVoiceArg,
3897
+ status: waitPayload.status,
3898
+ request: body,
3899
+ ...(uploaded ? { upload: uploaded } : {}),
3900
+ voice,
3901
+ };
3902
+ }
3903
+
3904
+ function requireSubjectVoiceRecordId(kwargs = {}) {
3905
+ const value = trimToNull(kwargs.voiceRecordId ?? kwargs.reqTaskId);
3906
+ if (!value) throw argumentError('缺少参数:--voice-record-id', '传 create subject-voice 返回的 voiceRecordId;也可用 --req-task-id。');
3907
+ return value;
3908
+ }
3909
+
3910
+ export async function subjectVoiceWait(kwargs = {}) {
3911
+ const voiceRecordId = requireSubjectVoiceRecordId(kwargs);
3912
+ const voice = await waitForSubjectVoiceExternalId(voiceRecordId, kwargs);
3913
+ const payload = subjectVoiceWaitPayload(voiceRecordId, voice);
3914
+ const failure = subjectFailureDetails(voice, 'voice');
3915
+ if (failure) {
3916
+ throw new LingjingAwbCliError(failure.errorMessage, {
3917
+ type: 'subject_voice_failed',
3918
+ exitCode: 1,
3919
+ hint: '主体音色创建任务已失败,请根据 error.details.errorMessage 调整音频/视频来源后重新创建。',
3920
+ details: payload,
3921
+ });
3922
+ }
3923
+ if (!payload.externalId) {
3924
+ throw new LingjingAwbCliError('主体音色 externalId 尚未回填', {
3925
+ type: 'subject_voice_still_pending',
3926
+ exitCode: 20,
3927
+ hint: '稍后重试 create subject-voice-wait,或用 create subject-voice-list --name 查询。',
3928
+ details: payload,
3929
+ });
3930
+ }
3931
+ return payload;
3932
+ }
3933
+
3545
3934
  const SUBJECT_SLOT_ALIASES = {
3546
3935
  primary: 'primary',
3547
3936
  'three-view': 'three-view',
@@ -3619,15 +4008,42 @@ function tagListFromArg(value) {
3619
4008
  .filter((item) => item.tagId);
3620
4009
  }
3621
4010
 
4011
+ function normalizeSubjectCreateModelCode(value) {
4012
+ const text = trimToNull(value);
4013
+ if (!text) return null;
4014
+ const normalized = text.toLowerCase();
4015
+ if (normalized === 'tx' || normalized === 'vidu') return normalized;
4016
+ return null;
4017
+ }
4018
+
4019
+ function resolveSubjectCreateModelCode(kwargs = {}) {
4020
+ const explicitModelCode = trimToNull(kwargs.modelCode ?? kwargs.model_code);
4021
+ const modelCode = normalizeSubjectCreateModelCode(explicitModelCode);
4022
+ if (modelCode) return { modelCode };
4023
+ if (explicitModelCode) {
4024
+ throw argumentError(
4025
+ `主体 modelCode 不支持:${explicitModelCode}`,
4026
+ '只支持 --model-code tx|vidu;KeLing / 可灵用 tx,Vidu 用 vidu。',
4027
+ );
4028
+ }
4029
+
4030
+ throw argumentError(
4031
+ '缺少主体 modelCode',
4032
+ '传 --model-code tx|vidu;KeLing / 可灵用 tx,Vidu 用 vidu。',
4033
+ );
4034
+ }
4035
+
3622
4036
  function buildSubjectCreateBody(kwargs, specs, assets) {
3623
4037
  const name = requireValue(kwargs, 'name');
4038
+ const description = trimToNull(kwargs.description ?? kwargs.elementDescription) ?? name;
3624
4039
  const primary = assets.find((item) => item.isPrimary);
3625
4040
  const referAssets = assets.filter((item) => item.assetPath);
4041
+ const { modelCode } = resolveSubjectCreateModelCode(kwargs);
3626
4042
  return compactRecord({
3627
4043
  reqTaskId: trimToNull(kwargs.reqTaskId),
3628
- modelCode: trimToNull(kwargs.modelCode),
4044
+ modelCode,
3629
4045
  elementName: name,
3630
- elementDescription: trimToNull(kwargs.description ?? kwargs.elementDescription),
4046
+ elementDescription: description,
3631
4047
  elementFrontalImage: primary?.assetPath ?? null,
3632
4048
  elementReferList: referAssets.map((item) => ({
3633
4049
  imageUrl: item.assetPath,
@@ -3645,9 +4061,12 @@ function extractCreatedElementId(payload) {
3645
4061
  return payload.id ?? payload.elementId ?? payload.subjectId ?? payload.reqTaskId ?? payload.data?.id ?? payload.data?.elementId ?? null;
3646
4062
  }
3647
4063
 
3648
- async function fetchSubjectByReqTaskId(reqTaskId) {
4064
+ async function fetchSubjectByReqTaskId(reqTaskId, options = {}) {
3649
4065
  if (!reqTaskId) return null;
3650
- const payload = await awbApi.getElementByReqTaskId(reqTaskId).catch(() => null);
4066
+ const payload = await awbApi.getElementByReqTaskId(reqTaskId).catch((error) => {
4067
+ if (options.optional) return null;
4068
+ throw error;
4069
+ });
3651
4070
  return payload && typeof payload === 'object' ? normalizeSubjectRecord(payload) : null;
3652
4071
  }
3653
4072
 
@@ -3656,7 +4075,7 @@ async function waitForSubjectExternalId(reqTaskId, options = {}) {
3656
4075
  const pollIntervalMs = Math.max(500, toInt(options.pollIntervalMs, 2000));
3657
4076
  const deadline = Date.now() + waitSeconds * 1000;
3658
4077
  let latest = await fetchSubjectByReqTaskId(reqTaskId);
3659
- while (waitSeconds > 0 && !latest?.externalId && Date.now() < deadline) {
4078
+ while (waitSeconds > 0 && !latest?.externalId && !subjectFailureDetails(latest, 'subject') && Date.now() < deadline) {
3660
4079
  await sleep(pollIntervalMs);
3661
4080
  latest = await fetchSubjectByReqTaskId(reqTaskId);
3662
4081
  }
@@ -3670,7 +4089,10 @@ function subjectWaitPayload(elementId, subject) {
3670
4089
  subjectId: externalId ?? elementId,
3671
4090
  externalId,
3672
4091
  nextRefSubject: externalId && subject?.name ? `${subject.name}=${externalId}` : null,
3673
- status: externalId ? 'ready' : 'pending_external_id',
4092
+ status: subject?.status ?? (externalId ? 'ready' : 'pending_external_id'),
4093
+ taskStatus: subject?.taskStatus ?? null,
4094
+ errorMessage: subject?.errorMessage ?? null,
4095
+ isTerminal: externalId ? true : Boolean(subject?.isTerminal),
3674
4096
  subject,
3675
4097
  };
3676
4098
  }
@@ -3681,6 +4103,7 @@ export async function subjectPublish(kwargs = {}) {
3681
4103
  if (!specs.some((item) => item.isPrimary)) {
3682
4104
  throw argumentError('缺少主体主参考图', '传 --resource primary:<path|url>(或 --resource three-view:<path|url> 自动升为主图)。');
3683
4105
  }
4106
+ const modelCodeResolution = resolveSubjectCreateModelCode(kwargs);
3684
4107
  if (toBool(kwargs.dryRun)) {
3685
4108
  const assets = specs.map((item) => ({
3686
4109
  label: item.label,
@@ -3692,15 +4115,18 @@ export async function subjectPublish(kwargs = {}) {
3692
4115
  }));
3693
4116
  return {
3694
4117
  dryRun: true,
3695
- action: 'subject publish',
4118
+ action: 'create subject',
3696
4119
  name,
4120
+ resolvedModelCode: modelCodeResolution.modelCode,
4121
+ resolvedFrom: modelCodeResolution.resolvedFrom,
4122
+ resolvedFromValue: modelCodeResolution.resolvedFromValue,
3697
4123
  request: buildSubjectCreateBody(kwargs, specs, assets),
3698
4124
  assets,
3699
4125
  localFiles: await inspectLocalFiles(specs.map((item) => item.file).filter(Boolean)),
3700
4126
  nextRefSubject: `${name}=<externalId>`,
3701
4127
  };
3702
4128
  }
3703
- ensureConfirmed(kwargs, '创建平台主体 element 是云端写入动作,需要确认', { action: 'subject publish', name });
4129
+ ensureConfirmed(kwargs, '创建平台主体 element 是云端写入动作,需要确认', { action: 'create subject', name });
3704
4130
  const assets = [];
3705
4131
  for (const spec of specs) {
3706
4132
  const uploaded = spec.file ? await uploadLocalFile(spec.file, { sceneType: TASK_UPLOAD_SCENE.SUBJECT }) : null;
@@ -3718,7 +4144,7 @@ export async function subjectPublish(kwargs = {}) {
3718
4144
  const payload = await awbApi.createElement(body);
3719
4145
  const elementId = extractCreatedElementId(payload);
3720
4146
  if (!elementId) throw new LingjingAwbCliError('主体创建完成但未拿到 elementId', { type: 'api_error', exitCode: 1, details: payload });
3721
- const subject = await fetchSubjectByReqTaskId(elementId);
4147
+ const subject = await fetchSubjectByReqTaskId(elementId, { optional: true });
3722
4148
  const waitPayload = subjectWaitPayload(elementId, subject);
3723
4149
  return {
3724
4150
  created: true,
@@ -3738,11 +4164,20 @@ export async function subjectWait(kwargs = {}) {
3738
4164
  const elementId = requireValue(kwargs, 'elementId', 'element-id');
3739
4165
  const subject = await waitForSubjectExternalId(elementId, kwargs);
3740
4166
  const payload = subjectWaitPayload(elementId, subject);
4167
+ const failure = subjectFailureDetails(subject, 'subject');
4168
+ if (failure) {
4169
+ throw new LingjingAwbCliError(failure.errorMessage, {
4170
+ type: 'subject_failed',
4171
+ exitCode: 1,
4172
+ hint: '主体创建任务已失败,请根据 error.details.errorMessage 调整参考资源/模型后重新创建。',
4173
+ details: payload,
4174
+ });
4175
+ }
3741
4176
  if (!payload.externalId) {
3742
4177
  throw new LingjingAwbCliError('主体 externalId 尚未回填', {
3743
4178
  type: 'subject_still_pending',
3744
4179
  exitCode: 20,
3745
- hint: '稍后重试 subject wait,或用 subject list --name 查询。',
4180
+ hint: '稍后重试 create subject-wait,或用 create subject-list --name 查询。',
3746
4181
  details: payload,
3747
4182
  });
3748
4183
  }
@@ -3758,7 +4193,7 @@ export async function subjectPublishBatch(kwargs = {}) {
3758
4193
  }
3759
4194
  return { dryRun: true, count: items.length, results };
3760
4195
  }
3761
- ensureConfirmed(kwargs, '批量发布主体资产是云端写入动作,需要确认', { action: 'subject publish-batch', count: items.length });
4196
+ ensureConfirmed(kwargs, '批量发布主体资产是云端写入动作,需要确认', { action: 'create subject-batch', count: items.length });
3762
4197
  const results = await runConcurrent(items, kwargs.concurrency ?? 1, async (item, index) => ({
3763
4198
  inputIndex: index,
3764
4199
  status: 'success',
@@ -3768,44 +4203,73 @@ export async function subjectPublishBatch(kwargs = {}) {
3768
4203
  }
3769
4204
 
3770
4205
  export async function subtitleRemove(kwargs = {}) {
3771
- const videoUrl = requireValue(kwargs, 'videoUrl', 'video-url');
4206
+ const built = await buildSubtitleRemovalRequest(kwargs, { dryRun: toBool(kwargs.dryRun) });
3772
4207
  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 } : {}),
4208
+ return {
4209
+ dryRun: true,
4210
+ action: 'create video-subtitle-removal',
4211
+ request: built.request,
4212
+ materialEndpoint: '/api/material/creation/videoSubtitleRemoval',
4213
+ taskType: 'VIDEO_SUBTITLE_REMOVAL',
4214
+ };
4215
+ }
4216
+ ensureConfirmed(kwargs, '提交视频去字幕任务会通过 material 创建任务并消耗积分,需要确认', {
4217
+ action: 'create video-subtitle-removal',
4218
+ sourceTaskId: built.sourceTaskId,
3782
4219
  });
3783
- return {
4220
+ const feePayload = await awbApi.fetchVideoSubtitleRemovalFee(built.request).catch(() => null);
4221
+ const estimate = await pointEstimate(feePayload, built.projectGroupNo).catch(() => ({}));
4222
+ const payload = await awbApi.createVideoSubtitleRemovalTask(built.request);
4223
+ const result = normalizeCreatedTask(payload, {
4224
+ ...estimate,
3784
4225
  submitted: true,
3785
- remoteTaskId: payload?.remote_task_id ?? payload?.remoteTaskId ?? null,
3786
- publicId: payload?.public_id ?? payload?.publicId ?? payload?.id ?? null,
4226
+ taskType: 'VIDEO_SUBTITLE_REMOVAL',
4227
+ sourceTaskId: built.sourceTaskId,
4228
+ projectGroupNo: built.projectGroupNo,
4229
+ });
4230
+ await appendTaskRecord(kwargs, {
4231
+ taskId: result.taskId,
4232
+ taskType: 'VIDEO_GROUP',
4233
+ projectGroupNo: built.projectGroupNo,
4234
+ promptSummary: `去字幕 sourceTaskId=${built.sourceTaskId}`,
4235
+ });
4236
+ return {
4237
+ ...result,
4238
+ nextCommand: result.taskId
4239
+ ? `lj-awb task wait --task-id ${result.taskId} --task-type VIDEO_GROUP${built.projectGroupNo ? ` --project-group-no ${built.projectGroupNo}` : ''} -f json`
4240
+ : null,
4241
+ };
4242
+ }
4243
+
4244
+ async function buildSubtitleRemovalRequest(kwargs = {}, options = {}) {
4245
+ const sourceTaskId = trimToNull(kwargs.sourceTaskId);
4246
+ if (!sourceTaskId) {
4247
+ throw argumentError('缺少来源视频任务 ID', '传 --source-task-id <taskId>;必须是 material 中已成功的视频任务。');
4248
+ }
4249
+ const dryRun = Boolean(options.dryRun);
4250
+ const projectGroupNo = await resolveProjectGroupNo(kwargs.projectGroupNo, {
4251
+ allowNull: true,
4252
+ noNetwork: dryRun,
4253
+ noSave: dryRun,
4254
+ }).catch((error) => {
4255
+ if (dryRun) return null;
4256
+ throw error;
4257
+ });
4258
+ return {
4259
+ sourceTaskId,
4260
+ projectGroupNo,
4261
+ request: compactRecord({
4262
+ sourceTaskId,
4263
+ projectGroupNo,
4264
+ }),
3787
4265
  };
3788
4266
  }
3789
4267
 
3790
4268
  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,
4269
+ const taskId = requireValue(kwargs, 'taskId', 'task-id');
4270
+ return taskStatus({
4271
+ ...kwargs,
4272
+ taskId,
4273
+ taskType: 'VIDEO_GROUP',
3810
4274
  });
3811
4275
  }