Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/commands/src/commands/auth/login-api-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ interface ApiKeyLoginProfile {
baseUrl: string;
persistBaseUrl?: string;
defaultTextModel?: string;
defaultVideoModel?: string;
defaultImageToVideoModel?: string;
defaultReferenceToVideoModel?: string;
defaultImageModel?: string;
persistPatch?: AuthPersistPatch;
}
Expand Down Expand Up @@ -54,17 +57,18 @@ export async function validateAndPersistApiKey(
const persistBaseUrl = profile.persistBaseUrl
? normalizeModelBaseUrl(profile.persistBaseUrl)
: undefined;
const validationModel = profile.defaultTextModel || "qwen3.7-max";
const requestOpts = {
url: baseUrl + chatPath(),
method: "POST",
headers: { Authorization: `Bearer ${key}` },
timeout: Math.min(deps.settings.timeout, 30),
body: {
model: profile.defaultTextModel || "qwen3.7-max",
model: validationModel,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
enable_thinking: false,
enable_thinking: validationModel === "qwen3.8-max-preview",
},
};

Expand All @@ -88,6 +92,9 @@ export async function validateAndPersistApiKey(
api_key: key,
base_url: persistBaseUrl,
default_text_model: profile.defaultTextModel,
default_video_model: profile.defaultVideoModel,
default_image_to_video_model: profile.defaultImageToVideoModel,
default_reference_to_video_model: profile.defaultReferenceToVideoModel,
default_image_model: profile.defaultImageModel,
});
}
3 changes: 3 additions & 0 deletions packages/commands/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ export default defineCommand({
baseUrl: resolvedBaseUrl,
persistBaseUrl,
defaultTextModel: profilePreset?.defaultTextModel,
defaultVideoModel: profilePreset?.defaultVideoModel,
defaultImageToVideoModel: profilePreset?.defaultImageToVideoModel,
defaultReferenceToVideoModel: profilePreset?.defaultReferenceToVideoModel,
defaultImageModel: profilePreset?.defaultImageModel,
});
},
Expand Down
4 changes: 4 additions & 0 deletions packages/commands/src/commands/config/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const VALID_KEYS = [
"security_token",
"default_text_model",
"default_video_model",
"default_image_to_video_model",
"default_reference_to_video_model",
"default_image_model",
"default_speech_model",
"default_omni_model",
Expand Down Expand Up @@ -41,6 +43,8 @@ export const KEY_ALIASES: Record<string, string> = {
"security-token": "security_token",
"default-text-model": "default_text_model",
"default-video-model": "default_video_model",
"default-image-to-video-model": "default_image_to_video_model",
"default-reference-to-video-model": "default_reference_to_video_model",
"default-image-model": "default_image_model",
"default-speech-model": "default_speech_model",
"default-omni-model": "default_omni_model",
Expand Down
30 changes: 24 additions & 6 deletions packages/commands/src/commands/image/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
resolveWatermark,
ASYNC_FLAG,
CONCURRENT_FLAG,
redactDataUri,
} from "bailian-cli-core";
import { poll } from "bailian-cli-runtime";
import { downloadFile } from "bailian-cli-runtime";
Expand All @@ -31,10 +32,15 @@ import { resolveImageSize } from "bailian-cli-runtime";
import { join } from "path";
import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";

const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max", "wan2.7-image"];
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];

function isSyncModel(model: string): boolean {
return SYNC_MODEL_PREFIXES.some((p) => model.startsWith(p));
return SYNC_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix));
}

function enablesPromptExtendByDefault(model: string): boolean {
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
}

const EDIT_FLAGS = {
Expand Down Expand Up @@ -98,7 +104,7 @@ const EDIT_FLAGS = {
type EditFlags = ParsedFlags<typeof EDIT_FLAGS>;

export default defineCommand({
description: "Edit an existing image with text instructions (Qwen-Image)",
description: "Edit an existing image with text instructions (Qwen-Image / Wan 2.7)",
auth: "apiKey",
usageArgs: "--image <url> --prompt <text> [flags]",
flags: EDIT_FLAGS,
Expand All @@ -107,6 +113,7 @@ export default defineCommand({
'--image https://example.com/logo.png --prompt "Change color to blue" --n 3',
'--image ./a.png --image ./b.png --prompt "Merge two images into one collage"',
'--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro',
'--image ./photo.png --prompt "Change the style" --model wan2.7-image',
'--image ./photo.png --prompt "Replace the background with a beach" --watermark false',
],
async run(ctx) {
Expand All @@ -125,13 +132,13 @@ export default defineCommand({

// Auto-upload local files (resolve all images in parallel)
const resolvedImages = await Promise.all(
rawImages.map((img) => ctx.client.uploadFile(img, model)),
rawImages.map((image) => ctx.client.resolveImageInput(image, model)),
);
const n = flags.n ?? 1;

const promptExtend = resolveBooleanFlag(
flags.promptExtend,
useSync ? true : undefined,
enablesPromptExtendByDefault(model) ? true : undefined,
"prompt-extend",
);

Expand Down Expand Up @@ -169,7 +176,18 @@ export default defineCommand({
const format = detectOutputFormat(settings.output);

if (settings.dryRun) {
emitResult({ request: body, mode: useSync ? "sync" : "async" }, format);
const previewBody = {
...body,
input: {
messages: body.input.messages.map((message) => ({
...message,
content: message.content.map((item) =>
item.image ? { ...item, image: redactDataUri(item.image) } : item,
),
})),
},
};
emitResult({ request: previewBody, mode: useSync ? "sync" : "async" }, format);
return;
}

Expand Down
13 changes: 9 additions & 4 deletions packages/commands/src/commands/image/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,16 @@ import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bai

import { join } from "path";

// qwen-image-2.0 series uses the sync multimodal-generation endpoint
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];
// Qwen-Image 2.0 and Wan 2.7 use the sync multimodal-generation endpoint.
const SYNC_MODEL_PREFIXES = ["qwen-image-2.0", "qwen-image-max", "wan2.7-image"];
const PROMPT_EXTEND_DEFAULT_PREFIXES = ["qwen-image-2.0", "qwen-image-max"];

function isSyncModel(model: string): boolean {
return SYNC_MODEL_PREFIXES.some((p) => model.startsWith(p));
return SYNC_MODEL_PREFIXES.some((prefix) => model.startsWith(prefix));
}

function enablesPromptExtendByDefault(model: string): boolean {
return PROMPT_EXTEND_DEFAULT_PREFIXES.some((prefix) => model.startsWith(prefix));
}

const GENERATE_FLAGS = {
Expand Down Expand Up @@ -121,7 +126,7 @@ export default defineCommand({

const promptExtend = resolveBooleanFlag(
flags.promptExtend,
useSync ? true : undefined,
enablesPromptExtendByDefault(model) ? true : undefined,
"prompt-extend",
);

Expand Down
19 changes: 15 additions & 4 deletions packages/commands/src/commands/video/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
resolveWatermark,
ASYNC_FLAG,
CONCURRENT_FLAG,
redactDataUri,
} from "bailian-cli-core";
import { poll } from "bailian-cli-runtime";
import { downloadFile, formatBytes } from "bailian-cli-runtime";
Expand Down Expand Up @@ -103,16 +104,17 @@ export default defineCommand({

const model =
flags.model ||
settings.defaultVideoModel ||
(flags.image ? "happyhorse-1.1-i2v" : "happyhorse-1.1-t2v");
(flags.image
? settings.defaultImageToVideoModel || "happyhorse-1.1-i2v"
: settings.defaultVideoModel || "happyhorse-1.1-t2v");
const format = detectOutputFormat(settings.output);

const imageUrl = flags.image;

// Auto-upload local image file for i2v
let resolvedImageUrl: string | undefined;
if (imageUrl) {
resolvedImageUrl = await ctx.client.uploadFile(imageUrl, model);
resolvedImageUrl = await ctx.client.resolveImageInput(imageUrl, model);
}

const watermark = resolveWatermark(flags.watermark);
Expand All @@ -139,7 +141,16 @@ export default defineCommand({
};

if (settings.dryRun) {
emitResult({ request: body }, format);
const previewBody = resolvedImageUrl
? {
...body,
input: {
...body.input,
media: [{ type: "first_frame" as const, url: redactDataUri(resolvedImageUrl) }],
},
}
: body;
emitResult({ request: previewBody }, format);
return;
}

Expand Down
38 changes: 25 additions & 13 deletions packages/commands/src/commands/video/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
resolveWatermark,
ASYNC_FLAG,
CONCURRENT_FLAG,
redactDataUri,
} from "bailian-cli-core";
import { poll } from "bailian-cli-runtime";
import { downloadFile, formatBytes } from "bailian-cli-runtime";
Expand Down Expand Up @@ -117,40 +118,40 @@ export default defineCommand({
const imageVoices = flags.imageVoice || [];
const videoVoices = flags.videoVoice || [];

const model = flags.model || "happyhorse-1.1-r2v";
const model = flags.model || settings.defaultReferenceToVideoModel || "happyhorse-1.1-r2v";
const format = detectOutputFormat(settings.output);

// --- Resolve file URLs (auto-upload local files) ---
const media: DashScopeVideoRefRequest["input"]["media"] = [];

// Add reference images
for (let i = 0; i < images.length; i++) {
const resolved = await ctx.client.uploadFile(images[i]!, model);
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
const resolved = await ctx.client.resolveImageInput(images[imageIndex]!, model);
const entry: DashScopeVideoRefRequest["input"]["media"][number] = {
type: "reference_image",
url: resolved,
};

// Pair voice by position
if (imageVoices[i]) {
const resolvedVoice = await ctx.client.uploadFile(imageVoices[i]!, model);
if (imageVoices[imageIndex]) {
const resolvedVoice = await ctx.client.uploadFile(imageVoices[imageIndex]!, model);
entry.reference_voice = resolvedVoice;
}

media.push(entry);
}

// Add reference videos
for (let i = 0; i < refVideos.length; i++) {
const resolved = await ctx.client.uploadFile(refVideos[i]!, model);
for (let videoIndex = 0; videoIndex < refVideos.length; videoIndex++) {
const resolved = await ctx.client.uploadFile(refVideos[videoIndex]!, model);
const entry: DashScopeVideoRefRequest["input"]["media"][number] = {
type: "reference_video",
url: resolved,
};

// Pair voice by position
if (videoVoices[i]) {
const resolvedVoice = await ctx.client.uploadFile(videoVoices[i]!, model);
if (videoVoices[videoIndex]) {
const resolvedVoice = await ctx.client.uploadFile(videoVoices[videoIndex]!, model);
entry.reference_voice = resolvedVoice;
}

Expand Down Expand Up @@ -178,7 +179,18 @@ export default defineCommand({
};

if (settings.dryRun) {
emitResult({ request: body }, format);
const previewBody = {
...body,
input: {
...body.input,
media: body.input.media.map((item) => ({
...item,
url: redactDataUri(item.url),
reference_voice: item.reference_voice ? redactDataUri(item.reference_voice) : undefined,
})),
},
};
emitResult({ request: previewBody }, format);
return;
}

Expand Down Expand Up @@ -233,11 +245,11 @@ export default defineCommand({
);

const videos: Array<{ taskId: string; videoUrl: string }> = [];
for (let i = 0; i < results.length; i++) {
const result = results[i]!;
for (let resultIndex = 0; resultIndex < results.length; resultIndex++) {
const result = results[resultIndex]!;
const videoUrl =
result.output.video_url || (result.output.results && result.output.results[0]?.url);
if (videoUrl) videos.push({ taskId: taskIds[i]!, videoUrl });
if (videoUrl) videos.push({ taskId: taskIds[resultIndex]!, videoUrl });
}

if (videos.length === 0) {
Expand Down
41 changes: 17 additions & 24 deletions packages/commands/src/commands/vision/describe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,13 @@ import {
BailianError,
ExitCode,
isLocalFile,
imageFileToDataUri,
redactDataUri,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { readFileSync, existsSync } from "fs";
import { existsSync, statSync } from "fs";
import { extname } from "path";

const IMAGE_MIME_TYPES: Record<string, string> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
};

const VIDEO_EXTENSIONS = new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv"]);

function isVideoInput(input: string): boolean {
Expand All @@ -35,18 +30,7 @@ async function toImageUrl(image: string): Promise<string> {
if (image.startsWith("data:")) return image;
if (image.startsWith("http://") || image.startsWith("https://")) return image;
if (image.startsWith("oss://")) return image;

// Local file → data URI (for small files < 10MB, fallback)
if (!existsSync(image)) throw new BailianError(`File not found: ${image}`, ExitCode.USAGE);
const ext = extname(image).toLowerCase();
const mime = IMAGE_MIME_TYPES[ext];
if (!mime)
throw new BailianError(
`Unsupported image format "${ext}". Supported: jpg, jpeg, png, webp`,
ExitCode.USAGE,
);
const buf = readFileSync(image);
return `data:${mime};base64,${buf.toString("base64")}`;
return imageFileToDataUri(image);
}

export default defineCommand({
Expand Down Expand Up @@ -86,7 +70,10 @@ export default defineCommand({
const { settings, flags } = ctx;
let image = flags.image;
const videoInputs = flags.video ?? [];
const model = flags.model || "qwen3-vl-plus";
const model =
flags.model ||
(ctx.client.usesTokenPlanEndpoint() ? settings.defaultTextModel : undefined) ||
"qwen3-vl-plus";

// Auto-detect: if --image was given a video file, treat it as --video
if (image && isVideoInput(image)) {
Expand All @@ -102,7 +89,14 @@ export default defineCommand({

if (settings.dryRun) {
emitResult(
{ request: { prompt, image, video: videoInputs.length ? videoInputs : undefined, model } },
{
request: {
prompt,
image: image ? redactDataUri(image) : undefined,
video: videoInputs.length ? videoInputs.map(redactDataUri) : undefined,
model,
},
},
format,
);
return;
Expand Down Expand Up @@ -132,10 +126,9 @@ export default defineCommand({

let finalImageUrl = imageUrl;
if (isLocalFile(image) && imageUrl.startsWith("data:")) {
const { statSync } = await import("fs");
const fileSize = statSync(image).size;
if (fileSize > 5 * 1024 * 1024) {
finalImageUrl = await ctx.client.uploadFile(image, model);
finalImageUrl = await ctx.client.resolveImageInput(image, model);
}
}

Expand Down
Loading