@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.
- package/README.md +13 -16
- package/install.mjs +1 -0
- package/package.json +2 -2
- package/packages/awb-cli/package.json +2 -2
- package/packages/awb-core/package.json +1 -1
- package/packages/awb-core/src/api.js +33 -64
- package/packages/awb-core/src/commands.js +147 -67
- package/packages/awb-core/src/common.js +2 -1
- package/packages/awb-core/src/output.js +74 -25
- package/packages/awb-core/src/services.js +645 -104
- package/packages/awb-core/src/standalone.js +207 -116
- package/skills/lj-awb/SKILL.md +78 -131
- package/skills/lj-awb/VERSION +1 -1
- package/skills/lj-awb/compat.json +3 -3
- package/skills/lj-awb/modules/asset.md +29 -22
- package/skills/lj-awb/modules/create-contract.md +20 -11
- package/skills/lj-awb/modules/create.md +37 -0
- package/skills/lj-awb/modules/driver.md +109 -0
- package/skills/lj-awb/modules/evals.md +84 -36
- package/skills/lj-awb/modules/image.md +14 -14
- package/skills/lj-awb/modules/model.md +11 -29
- package/skills/lj-awb/modules/project.md +1 -0
- package/skills/lj-awb/modules/subject.md +24 -13
- package/skills/lj-awb/modules/task-manual.md +31 -28
- package/skills/lj-awb/modules/task.md +22 -3
- package/skills/lj-awb/modules/upload.md +10 -9
- package/skills/lj-awb/modules/video.md +25 -32
- package/skills/lj-awb/modules/workflows.md +81 -199
- package/skills/lj-awb/references/error-codes.md +13 -6
- package/skills/lj-awb/references/model-options-read.md +2 -2
- package/skills/lj-awb/references/output-fields.md +23 -18
|
@@ -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
|
|
902
|
-
if (!
|
|
903
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1911
|
-
const createCommand = taskKind === 'image' ? 'image
|
|
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
|
|
1931
|
-
if (hasIntent('reference')) examples.push('lj-awb image
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1944
|
-
examples.push('lj-awb video
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
2130
|
+
{ field: 'resources[].source.kind', values: ['url', 'asset_id'], description: '只接受 url、asset_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
|
|
2137
|
-
feeCommand: context.taskKind === 'image' ? 'image
|
|
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
|
-
|
|
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
|
|
2174
|
-
|
|
2175
|
-
|
|
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)
|
|
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 ??
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
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
|
|
3467
|
-
ensureConfirmed(kwargs, '创建素材组是云端写入动作,需要确认', { action: 'asset
|
|
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
|
|
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 (!
|
|
3479
|
-
|
|
3480
|
-
|
|
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
|
|
3785
|
+
action: 'create asset',
|
|
3501
3786
|
request: {
|
|
3502
3787
|
assetGroupsId: groupId,
|
|
3503
|
-
url: localFile ? normalizeCosAssetPath(dryRunBackendPath(localFile, TASK_UPLOAD_SCENE.
|
|
3788
|
+
url: localFile ? normalizeCosAssetPath(dryRunBackendPath(localFile, TASK_UPLOAD_SCENE.ASSET_REVIEW)) : assetPath,
|
|
3504
3789
|
name,
|
|
3505
|
-
|
|
3790
|
+
platform,
|
|
3506
3791
|
},
|
|
3507
3792
|
localFile: localFile ? await inspectLocalFile(localFile) : null,
|
|
3508
3793
|
};
|
|
3509
3794
|
}
|
|
3510
|
-
ensureConfirmed(kwargs, '注册素材是云端写入动作,需要确认', { action: 'asset
|
|
3511
|
-
const uploaded = localFile ? await uploadLocalFile(localFile, { sceneType: TASK_UPLOAD_SCENE.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4124
|
+
modelCode,
|
|
3629
4125
|
elementName: name,
|
|
3630
|
-
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(() =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4283
|
+
const built = await buildSubtitleRemovalRequest(kwargs, { dryRun: toBool(kwargs.dryRun) });
|
|
3772
4284
|
if (toBool(kwargs.dryRun)) {
|
|
3773
|
-
return {
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3786
|
-
|
|
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
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
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
|
}
|