Skip to content

Commit d8aa89d

Browse files
committed
feat: add image/audio finetune
1 parent 8fd072b commit d8aa89d

14 files changed

Lines changed: 256 additions & 294 deletions

File tree

packages/cli/tests/e2e/dataset.e2e.test.ts

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
216216
});
217217

218218
test("dataset upload --schema image --no-validate --dry-run 采用 1GB 媒体上限", async () => {
219-
// image/video schemas raise the upload cap to 1 GiB (vs 300 MB for text).
219+
// image schema raises the upload cap to 1 GiB (vs 300 MB for text).
220220
// --no-validate keeps this offline (the jsonl fixture is not a real zip).
221221
const file = join(__dirname, ".dataset-valid.jsonl");
222222
const { stdout, stderr, exitCode } = await runCli([
@@ -238,28 +238,59 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
238238
expect(data.max_bytes).toBe(1024 * 1024 * 1024);
239239
});
240240

241-
test.each(["tts", "image", "video"])(
242-
"dataset upload --dry-run 接受媒体 schema %s",
243-
async (schema) => {
244-
const file = join(__dirname, ".dataset-valid.jsonl");
245-
const { stdout, stderr, exitCode } = await runCli([
246-
"dataset",
247-
"upload",
248-
"--file",
249-
file,
250-
"--schema",
251-
schema,
252-
"--no-validate",
253-
"--dry-run",
254-
"--output",
255-
"json",
256-
]);
257-
expect(exitCode, stderr).toBe(0);
258-
const data = parseStdoutJson<{ action: string; schema: string }>(stdout);
259-
expect(data.action).toBe("dataset.upload");
260-
expect(data.schema).toBe(schema);
261-
},
262-
);
241+
test.each(["tts", "image"])("dataset upload --dry-run 接受媒体 schema %s", async (schema) => {
242+
const file = join(__dirname, ".dataset-valid.jsonl");
243+
const { stdout, stderr, exitCode } = await runCli([
244+
"dataset",
245+
"upload",
246+
"--file",
247+
file,
248+
"--schema",
249+
schema,
250+
"--no-validate",
251+
"--dry-run",
252+
"--output",
253+
"json",
254+
]);
255+
expect(exitCode, stderr).toBe(0);
256+
const data = parseStdoutJson<{ action: string; schema: string }>(stdout);
257+
expect(data.action).toBe("dataset.upload");
258+
expect(data.schema).toBe(schema);
259+
});
260+
261+
test("dataset validate --schema video 拒绝(视频生成入口已隐藏)", async () => {
262+
const file = join(__dirname, ".dataset-valid.jsonl");
263+
const { stdout, stderr, exitCode } = await runCli([
264+
"dataset",
265+
"validate",
266+
"--file",
267+
file,
268+
"--schema",
269+
"video",
270+
"--output",
271+
"json",
272+
]);
273+
expect(exitCode, stdout + stderr).not.toBe(0);
274+
expect(`${stdout}\n${stderr}`).toMatch(/--schema video is not supported/);
275+
});
276+
277+
test("dataset upload --schema video 拒绝(视频生成入口已隐藏)", async () => {
278+
const file = join(__dirname, ".dataset-valid.jsonl");
279+
const { stdout, stderr, exitCode } = await runCli([
280+
"dataset",
281+
"upload",
282+
"--file",
283+
file,
284+
"--schema",
285+
"video",
286+
"--no-validate",
287+
"--dry-run",
288+
"--output",
289+
"json",
290+
]);
291+
expect(exitCode, stdout + stderr).not.toBe(0);
292+
expect(`${stdout}\n${stderr}`).toMatch(/--schema video is not supported/);
293+
});
263294
});
264295

265296
describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (DashScope)", () => {

packages/cli/tests/e2e/deploy.e2e.test.ts

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -56,69 +56,6 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
5656
expect(data.body.capacity).toBe(1);
5757
});
5858

59-
test("deploy create --dry-run 组装视频 LoRA 的 aigc_config", async () => {
60-
const { stdout, stderr, exitCode } = await runCli([
61-
"deploy",
62-
"create",
63-
"--model",
64-
"wan2.5-i2v-preview-ft-xxx",
65-
"--name",
66-
"my-video-lora",
67-
"--aigc-prompt",
68-
"a cat surfing",
69-
"--aigc-lora-prompt-default",
70-
"trigger-word",
71-
"--aigc-use-input-prompt",
72-
"true",
73-
"--dry-run",
74-
"--output",
75-
"json",
76-
]);
77-
expect(exitCode, stderr).toBe(0);
78-
const data = parseStdoutJson<{
79-
action: string;
80-
body: {
81-
plan: string;
82-
aigc_config?: {
83-
use_input_prompt?: boolean;
84-
prompt?: string;
85-
lora_prompt_default?: string;
86-
};
87-
};
88-
}>(stdout);
89-
expect(data.action).toBe("deploy.create");
90-
expect(data.body.plan).toBe("lora");
91-
expect(data.body.aigc_config).toEqual({
92-
use_input_prompt: true,
93-
prompt: "a cat surfing",
94-
lora_prompt_default: "trigger-word",
95-
});
96-
});
97-
98-
test("deploy create --aigc-* 仅对 plan=lora 有效(plan=ptu 时报错)", async () => {
99-
const { stdout, stderr, exitCode } = await runCli([
100-
"deploy",
101-
"create",
102-
"--model",
103-
"wan2.5-i2v-preview-ft-xxx",
104-
"--name",
105-
"my-video-lora",
106-
"--plan",
107-
"ptu",
108-
"--input-tpm",
109-
"10000",
110-
"--output-tpm",
111-
"1000",
112-
"--aigc-prompt",
113-
"a cat surfing",
114-
"--dry-run",
115-
"--output",
116-
"json",
117-
]);
118-
expect(exitCode, stdout + stderr).not.toBe(0);
119-
expect(`${stdout}\n${stderr}`).toMatch(/--aigc-\* flags are only valid for plan=lora/);
120-
});
121-
12259
test("deploy create --plan mu --deploy-spec --dry-run 透传 deploy_spec", async () => {
12360
const { stdout, stderr, exitCode } = await runCli([
12461
"deploy",

packages/commands/src/commands/dataset/upload.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const UPLOAD_FLAGS = {
1818
file: {
1919
type: "string",
2020
valueHint: "<path>",
21-
description: "Local dataset file (.jsonl or .zip; ≤300MB text, ≤1GB image/video)",
21+
description: "Local dataset file (.jsonl or .zip; ≤300MB text, ≤1GB image)",
2222
required: true,
2323
},
2424
purpose: {
@@ -30,7 +30,7 @@ const UPLOAD_FLAGS = {
3030
type: "string",
3131
valueHint: "<s>",
3232
description:
33-
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
33+
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record.',
3434
},
3535
noValidate: {
3636
type: "switch",
@@ -46,7 +46,7 @@ export default defineCommand({
4646
description: "Upload a dataset file (.jsonl or .zip) to Bailian",
4747
auth: "apiKey",
4848
usageArgs:
49-
"--file <path> [--purpose <name>] [--schema <chatml|dpo|cpt|tts|image|video>] [--no-validate] [--full-validate]",
49+
"--file <path> [--purpose <name>] [--schema <chatml|dpo|cpt|tts|image>] [--no-validate] [--full-validate]",
5050
flags: UPLOAD_FLAGS,
5151
exampleArgs: [
5252
"--file train.jsonl",
@@ -58,27 +58,32 @@ export default defineCommand({
5858
"--file train.jsonl --no-validate",
5959
],
6060
notes: [
61-
"Supports .jsonl (text) and .zip (audio/image/video archives with a",
62-
"data.jsonl manifest). Six record schemas are recognized: chatml =",
63-
"{messages:[...]} (SFT); dpo = {messages:[...], chosen, rejected};",
64-
'cpt = {text:"..."} (continual pre-training, raw text); tts =',
65-
'{wav_fn:"train/xxx.wav", text:"..."} (audio fine-tuning); image =',
66-
'{img_path:"..."} (image generation); video = {first_frame_path:"...",',
67-
'video_path:"..."} (video generation). With no --schema, a record',
68-
"carrying wav_fn is validated as TTS, img_path as image, video_path /",
69-
"first_frame_path as video, chosen/rejected as DPO, text (no messages)",
70-
"as CPT, otherwise ChatML. Upload cap: 300MB text, 1GB image/video.",
71-
"Upload uses the OpenAI-compatible /compatible-mode/v1/files endpoint so",
72-
"the purpose tag is persisted (the DashScope-native /api/v1/files drops it).",
61+
"Supports .jsonl (text) and .zip (audio/image archives with a data.jsonl",
62+
"manifest). Five record schemas are recognized: chatml = {messages:[...]}",
63+
'(SFT); dpo = {messages:[...], chosen, rejected}; cpt = {text:"..."}',
64+
'(continual pre-training, raw text); tts = {wav_fn:"train/xxx.wav",',
65+
'text:"..."} (audio fine-tuning); image = {img_path:"..."} (image',
66+
"generation). With no --schema, a record carrying wav_fn is validated as",
67+
"TTS, img_path as image, chosen/rejected as DPO, text (no messages) as CPT,",
68+
"otherwise ChatML. Upload cap: 300MB text, 1GB image. Upload uses the",
69+
"OpenAI-compatible /compatible-mode/v1/files endpoint so the purpose tag is",
70+
"persisted (the DashScope-native /api/v1/files drops it).",
7371
],
7472
async run(ctx) {
7573
const { identity, settings, flags } = ctx;
7674
const filePath = flags.file;
7775
const purpose = flags.purpose || "fine-tune";
7876
const schema = parseDatasetSchemaFlag(flags.schema);
77+
if (schema === "video") {
78+
throw new BailianError(
79+
`--schema video is not supported.`,
80+
ExitCode.USAGE,
81+
`Supported schemas: chatml, dpo, cpt, tts, image.`,
82+
);
83+
}
7984
const format = detectOutputFormat(settings.output);
80-
// Image / video schemas allow larger ZIPs (1 GB vs 300 MB for text).
81-
const isMediaSchema = schema === "image" || schema === "video";
85+
// Image schema allows larger ZIPs (1 GB vs 300 MB for text).
86+
const isMediaSchema = schema === "image";
8287

8388
if (!flags.noValidate) {
8489
const maxBytes = isMediaSchema ? MAX_MEDIA_ZIP_BYTES : MAX_DATASET_BYTES;

packages/commands/src/commands/dataset/validate.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ const VALIDATE_FLAGS = {
3636
type: "string",
3737
valueHint: "<s>",
3838
description:
39-
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), "image" (image generation), or "video" (video generation). Default auto-detects per record.',
39+
'Record schema: "chatml" (SFT), "dpo" (chosen/rejected), "cpt" (raw text), "tts" (audio), or "image" (image generation). Default auto-detects per record.',
4040
},
4141
} satisfies FlagsDef;
4242

4343
export default defineCommand({
4444
description: "Locally validate a dataset file (.jsonl or .zip) without uploading",
4545
// 纯本地校验,不触网、不需 API key(与 `pipeline validate` 一致)。
4646
auth: "none",
47-
usageArgs: "--file <path> [--full-validate] [--schema <chatml|dpo|cpt|tts|image|video>]",
47+
usageArgs: "--file <path> [--full-validate] [--schema <chatml|dpo|cpt|tts|image>]",
4848
flags: VALIDATE_FLAGS,
4949
exampleArgs: [
5050
"--file train.jsonl",
@@ -60,19 +60,25 @@ export default defineCommand({
6060
"Schemas: chatml = {messages:[...]} (SFT); dpo = {messages:[...], chosen,",
6161
'rejected}; cpt = {text:"..."} (continual pre-training, raw text);',
6262
'tts = {wav_fn:"train/xxx.wav", text:"..."} (audio fine-tuning);',
63-
'image = {img_path:"..."} (image generation); video =',
64-
'{first_frame_path:"...", video_path:"..."} (video generation). With no',
65-
"--schema, a record carrying wav_fn is validated as TTS, img_path as",
66-
"image, video_path / first_frame_path as video, chosen/rejected as DPO,",
67-
"text (no messages) as CPT, otherwise ChatML. Pass --schema to require a",
68-
"specific shape on every record. ZIP archives (.zip) are validated",
69-
"structurally (data.jsonl present, media references resolve) in addition",
70-
"to per-record content checks. Use --full-validate to JSON.parse every line.",
63+
'image = {img_path:"..."} (image generation). With no --schema, a record',
64+
"carrying wav_fn is validated as TTS, img_path as image, chosen/rejected",
65+
"as DPO, text (no messages) as CPT, otherwise ChatML. Pass --schema to",
66+
"require a specific shape on every record. ZIP archives (.zip) are",
67+
"validated structurally (data.jsonl present, media references resolve) in",
68+
"addition to per-record content checks. Use --full-validate to JSON.parse",
69+
"every line.",
7170
],
7271
async run(ctx) {
7372
const { settings, flags } = ctx;
7473
const filePath = flags.file;
7574
const schema = parseDatasetSchemaFlag(flags.schema);
75+
if (schema === "video") {
76+
throw new BailianError(
77+
`--schema video is not supported.`,
78+
ExitCode.USAGE,
79+
`Supported schemas: chatml, dpo, cpt, tts, image.`,
80+
);
81+
}
7682
const format = detectOutputFormat(settings.output);
7783

7884
if (settings.dryRun) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Deploy-domain constants — billing plans, billing methods and template
3+
* charge types. Centralised here so no `deploy` command carries a magic
4+
* string for these server-contract values.
5+
*/
6+
7+
/** Billing plan (`--plan` value, matches the server's deployment plan). */
8+
export const DEPLOY_PLAN = {
9+
/** Token-billed; the CLI default. */
10+
LORA: "lora",
11+
/** Token-billed, provisioned throughput. */
12+
PTU: "ptu",
13+
/** Model-unit-billed. */
14+
MU: "mu",
15+
} as const;
16+
17+
export type DeployPlan = (typeof DEPLOY_PLAN)[keyof typeof DEPLOY_PLAN];
18+
19+
/** CLI default plan when `--plan` is omitted. */
20+
export const DEFAULT_DEPLOY_PLAN: DeployPlan = DEPLOY_PLAN.LORA;
21+
22+
/** Billing method (`billing_method`, plan=mu only). */
23+
export const BILLING_METHOD = {
24+
/** Post-paid (currently the only server-supported value). */
25+
POST_PAY: "POST_PAY",
26+
/** Pre-paid. */
27+
PRE_PAY: "PRE_PAY",
28+
} as const;
29+
30+
export type BillingMethod = (typeof BILLING_METHOD)[keyof typeof BILLING_METHOD];
31+
32+
/** Default billing method for plan=mu when `--billing-method` is omitted. */
33+
export const DEFAULT_BILLING_METHOD: BillingMethod = BILLING_METHOD.POST_PAY;
34+
35+
/** Template charge type (`charge_type`) returned by the deployable-models catalog. */
36+
export const CHARGE_TYPE = {
37+
POST_PAID: "post_paid",
38+
PRE_PAID: "pre_paid",
39+
} as const;
40+
41+
export type ChargeType = (typeof CHARGE_TYPE)[keyof typeof CHARGE_TYPE];

0 commit comments

Comments
 (0)