11import {
22 defineCommand ,
3- imagePath ,
4- imageSyncPath ,
53 taskPath ,
64 detectOutputFormat ,
75 resolveOutputDir ,
@@ -20,6 +18,7 @@ import {
2018 BailianError ,
2119 resolveBooleanFlag ,
2220 resolveWatermark ,
21+ resolveImageEditApi ,
2322 ASYNC_FLAG ,
2423 CONCURRENT_FLAG ,
2524 redactDataUri ,
@@ -32,17 +31,6 @@ import { resolveImageSize } from "bailian-cli-runtime";
3231import { join } from "path" ;
3332import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE , BOOL_FLAG_WATERMARK } from "bailian-cli-runtime" ;
3433
35- const SYNC_MODEL_PREFIXES = [ "qwen-image-2.0" , "qwen-image-max" , "wan2.7-image" ] ;
36- const PROMPT_EXTEND_DEFAULT_PREFIXES = [ "qwen-image-2.0" , "qwen-image-max" ] ;
37-
38- function isSyncModel ( model : string ) : boolean {
39- return SYNC_MODEL_PREFIXES . some ( ( prefix ) => model . startsWith ( prefix ) ) ;
40- }
41-
42- function enablesPromptExtendByDefault ( model : string ) : boolean {
43- return PROMPT_EXTEND_DEFAULT_PREFIXES . some ( ( prefix ) => model . startsWith ( prefix ) ) ;
44- }
45-
4634const EDIT_FLAGS = {
4735 image : {
4836 type : "array" ,
@@ -77,6 +65,12 @@ const EDIT_FLAGS = {
7765 valueHint : "<text>" ,
7866 description : "Negative prompt to exclude unwanted content" ,
7967 } ,
68+ function : {
69+ type : "string" ,
70+ valueHint : "<name>" ,
71+ description :
72+ "wanx*-imageedit function (default: description_edit). Examples: stylization_all, description_edit" ,
73+ } ,
8074 promptExtend : {
8175 type : "boolean" ,
8276 valueHint : "<bool>" ,
@@ -114,6 +108,8 @@ export default defineCommand({
114108 '--image ./a.png --image ./b.png --prompt "Merge two images into one collage"' ,
115109 '--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro' ,
116110 '--image ./photo.png --prompt "Change the style" --model wan2.7-image' ,
111+ '--image ./photo.png --prompt "Place the subject on a table" --model wan2.5-i2i-preview' ,
112+ '--image ./photo.png --prompt "转换成绘本风格" --model wanx2.1-imageedit --function stylization_all' ,
117113 '--image ./photo.png --prompt "Replace the background with a beach" --watermark false' ,
118114 ] ,
119115 async run ( ctx ) {
@@ -128,7 +124,7 @@ export default defineCommand({
128124 const prompt = flags . prompt ;
129125
130126 const model = flags . model || settings . defaultImageModel || "qwen-image-2.0" ;
131- const useSync = isSyncModel ( model ) ;
127+ const route = resolveImageEditApi ( model ) ;
132128
133129 // Auto-upload local files (resolve all images in parallel)
134130 const resolvedImages = await Promise . all (
@@ -138,94 +134,152 @@ export default defineCommand({
138134
139135 const promptExtend = resolveBooleanFlag (
140136 flags . promptExtend ,
141- enablesPromptExtendByDefault ( model ) ? true : undefined ,
137+ route . promptExtendDefault ,
142138 "prompt-extend" ,
143139 ) ;
144140
145- // Build content: all images first, then text prompt
146- const contentItems : Array < { image ?: string ; text ?: string } > = resolvedImages . map (
147- ( u : string ) => ( { image : u } ) ,
148- ) ;
149- contentItems . push ( { text : prompt } ) ;
150-
151141 const watermark = resolveWatermark ( flags . watermark ) ;
152142
153- const body : DashScopeImageRequest = {
154- model,
155- input : {
156- messages : [
157- {
158- role : "user" ,
159- content : contentItems ,
160- } ,
161- ] ,
162- } ,
163- parameters : {
164- size : resolveImageSize ( flags . size , useSync ) ,
165- n,
166- seed : flags . seed ,
167- prompt_extend : promptExtend ,
168- watermark,
169- negative_prompt : flags . negativePrompt || undefined ,
170- } ,
143+ const parameters : NonNullable < DashScopeImageRequest [ "parameters" ] > = {
144+ size : resolveImageSize ( flags . size , route . sizeProfile ) ,
145+ n,
146+ seed : flags . seed ,
147+ prompt_extend : promptExtend ,
148+ watermark,
171149 } ;
172150
151+ let body : DashScopeImageRequest ;
152+ if ( route . inputStyle === "function-base-image" ) {
153+ const baseImageUrl = resolvedImages [ 0 ] ;
154+ if ( ! baseImageUrl ) {
155+ throw new BailianError (
156+ "wanx*-imageedit requires at least one --image as base_image_url." ,
157+ ExitCode . USAGE ,
158+ ) ;
159+ }
160+ body = {
161+ model,
162+ input : {
163+ function : flags . function || "description_edit" ,
164+ prompt,
165+ base_image_url : baseImageUrl ,
166+ } ,
167+ parameters,
168+ } ;
169+ } else if ( route . inputStyle === "prompt-images" ) {
170+ body = {
171+ model,
172+ input : {
173+ prompt,
174+ images : resolvedImages ,
175+ negative_prompt : flags . negativePrompt || undefined ,
176+ } ,
177+ parameters,
178+ } ;
179+ } else {
180+ const contentItems : Array < { image ?: string ; text ?: string } > = resolvedImages . map (
181+ ( imageUrl : string ) => ( { image : imageUrl } ) ,
182+ ) ;
183+ contentItems . push ( { text : prompt } ) ;
184+ body = {
185+ model,
186+ input : {
187+ messages : [
188+ {
189+ role : "user" ,
190+ content : contentItems ,
191+ } ,
192+ ] ,
193+ } ,
194+ parameters : {
195+ ...parameters ,
196+ negative_prompt : flags . negativePrompt || undefined ,
197+ } ,
198+ } ;
199+ }
200+
173201 // Remove undefined parameters
174202 stripUndefined ( body . parameters as Record < string , unknown > ) ;
175203
176204 const format = detectOutputFormat ( settings . output ) ;
177205
178206 if ( settings . dryRun ) {
179- const previewBody = {
180- ...body ,
181- input : {
182- messages : body . input . messages . map ( ( message ) => ( {
183- ...message ,
184- content : message . content . map ( ( item ) =>
185- item . image ? { ...item , image : redactDataUri ( item . image ) } : item ,
186- ) ,
187- } ) ) ,
188- } ,
189- } ;
190- emitResult ( { request : previewBody , mode : useSync ? "sync" : "async" } , format ) ;
207+ let previewBody : DashScopeImageRequest = body ;
208+ if ( "messages" in body . input ) {
209+ previewBody = {
210+ ...body ,
211+ input : {
212+ messages : body . input . messages . map ( ( message ) => ( {
213+ ...message ,
214+ content : message . content . map ( ( item ) =>
215+ item . image ? { ...item , image : redactDataUri ( item . image ) } : item ,
216+ ) ,
217+ } ) ) ,
218+ } ,
219+ } ;
220+ } else if ( "images" in body . input ) {
221+ previewBody = {
222+ ...body ,
223+ input : {
224+ ...body . input ,
225+ images : body . input . images ?. map ( ( imageUrl ) => redactDataUri ( imageUrl ) ) ,
226+ } ,
227+ } ;
228+ } else if ( "base_image_url" in body . input ) {
229+ previewBody = {
230+ ...body ,
231+ input : {
232+ ...body . input ,
233+ base_image_url : redactDataUri ( body . input . base_image_url ) ,
234+ mask_image_url : body . input . mask_image_url
235+ ? redactDataUri ( body . input . mask_image_url )
236+ : undefined ,
237+ } ,
238+ } ;
239+ }
240+ emitResult (
241+ { request : previewBody , mode : route . useSync ? "sync" : "async" , path : route . path } ,
242+ format ,
243+ ) ;
191244 return ;
192245 }
193246
194247 if ( ! settings . quiet ) {
195248 process . stderr . write (
196- `[Model: ${ model } ] [Mode: ${ useSync ? "sync" : "async" } ] [Images: ${ resolvedImages . length } ]\n` ,
249+ `[Model: ${ model } ] [Mode: ${ route . useSync ? "sync" : "async" } ] [Images: ${ resolvedImages . length } ]\n` ,
197250 ) ;
198251 }
199252
200253 const concurrent = getConcurrency ( flags ) ;
201254
202- if ( useSync ) {
203- await handleSyncMode ( ctx . client , settings , body , flags , format , concurrent ) ;
255+ if ( route . useSync ) {
256+ await handleSyncMode ( ctx . client , settings , route . path , body , flags , format , concurrent ) ;
204257 } else {
205- await handleAsyncMode ( ctx . client , settings , body , flags , format , concurrent ) ;
258+ await handleAsyncMode ( ctx . client , settings , route . path , body , flags , format , concurrent ) ;
206259 }
207260 } ,
208261} ) ;
209262
210263async function handleSyncMode (
211264 client : Client ,
212265 settings : Settings ,
266+ path : string ,
213267 body : DashScopeImageRequest ,
214268 flags : EditFlags ,
215269 format : OutputFormat ,
216270 concurrent : number ,
217271) : Promise < void > {
218272 const results = await runConcurrent ( concurrent , settings , ( ) =>
219273 client . requestJson < DashScopeImageSyncResponse > ( {
220- path : imageSyncPath ( ) ,
274+ path,
221275 method : "POST" ,
222276 body,
223277 } ) ,
224278 ) ;
225279
226280 const imageUrls = results
227- . flatMap ( ( r ) => r . output . choices || [ ] )
228- . flatMap ( ( c ) => c . message ?. content || [ ] )
281+ . flatMap ( ( result ) => result . output . choices || [ ] )
282+ . flatMap ( ( choice ) => choice . message ?. content || [ ] )
229283 . map ( ( item ) => item . image )
230284 . filter ( Boolean ) ;
231285
@@ -239,6 +293,7 @@ async function handleSyncMode(
239293async function handleAsyncMode (
240294 client : Client ,
241295 settings : Settings ,
296+ path : string ,
242297 body : DashScopeImageRequest ,
243298 flags : EditFlags ,
244299 format : OutputFormat ,
@@ -249,14 +304,14 @@ async function handleAsyncMode(
249304 settings ,
250305 ( ) =>
251306 client . requestJson < DashScopeAsyncResponse > ( {
252- path : imagePath ( ) ,
307+ path,
253308 method : "POST" ,
254309 body,
255310 async : true ,
256311 } ) ,
257312 "tasks" ,
258313 ) ;
259- const taskIds = responses . map ( ( r ) => r . output . task_id ) ;
314+ const taskIds = responses . map ( ( response ) => response . output . task_id ) ;
260315
261316 if ( flags . async ) {
262317 emitResult ( { task_ids : taskIds } , format ) ;
@@ -269,12 +324,12 @@ async function handleAsyncMode(
269324 url : client . url ( taskPath ( taskId ) ) ,
270325 intervalSec : pollInterval ,
271326 timeoutSec : settings . timeout ,
272- isComplete : ( d ) => ( d as DashScopeTaskResponse ) . output . task_status === "SUCCEEDED" ,
273- isFailed : ( d ) => ( d as DashScopeTaskResponse ) . output . task_status === "FAILED" ,
274- getStatus : ( d ) => ( d as DashScopeTaskResponse ) . output . task_status ,
275- getErrorMessage : ( d ) => {
276- const o = ( d as DashScopeTaskResponse ) . output ;
277- return o . message || o . code || undefined ;
327+ isComplete : ( data ) => ( data as DashScopeTaskResponse ) . output . task_status === "SUCCEEDED" ,
328+ isFailed : ( data ) => ( data as DashScopeTaskResponse ) . output . task_status === "FAILED" ,
329+ getStatus : ( data ) => ( data as DashScopeTaskResponse ) . output . task_status ,
330+ getErrorMessage : ( data ) => {
331+ const output = ( data as DashScopeTaskResponse ) . output ;
332+ return output . message || output . code || undefined ;
278333 } ,
279334 } ) ,
280335 ) ;
@@ -285,13 +340,13 @@ async function handleAsyncMode(
285340 for ( const result of results ) {
286341 if ( result . output . choices ) {
287342 const urls = result . output . choices
288- . flatMap ( ( c ) => c . message ?. content || [ ] )
343+ . flatMap ( ( choice ) => choice . message ?. content || [ ] )
289344 . map ( ( item ) => item . image )
290345 . filter ( Boolean ) ;
291346 imageUrls . push ( ...urls ) ;
292347 }
293348 if ( result . output . results ) {
294- const urls = result . output . results . map ( ( r ) => r . url ) . filter ( Boolean ) ;
349+ const urls = result . output . results . map ( ( item ) => item . url ) . filter ( Boolean ) ;
295350 if ( urls . length > 0 && imageUrls . length === 0 ) {
296351 imageUrls . push ( ...urls ) ;
297352 }
@@ -321,8 +376,8 @@ async function saveImages(
321376 // Parallel download all images
322377 const items =
323378 imageUrls . length > 1
324- ? imageUrls . map ( ( url , i ) => {
325- const filename = `${ prefix } _${ String ( i + 1 ) . padStart ( 3 , "0" ) } .png` ;
379+ ? imageUrls . map ( ( url , index ) => {
380+ const filename = `${ prefix } _${ String ( index + 1 ) . padStart ( 3 , "0" ) } .png` ;
326381 return { url, destPath : join ( outDir , filename ) } ;
327382 } )
328383 : [ { url : imageUrls [ 0 ] , destPath : join ( outDir , `${ prefix } .png` ) } ] ;
0 commit comments