diff --git a/CHANGELOG-AI.md b/CHANGELOG-AI.md index 51397c76..44e00255 100644 --- a/CHANGELOG-AI.md +++ b/CHANGELOG-AI.md @@ -2,6 +2,16 @@ ## [Unreleased] +### Fixed + +- [image-generation] **Runware NanoBanana2Pro Dimensions**: Fixed API errors when using non-square aspect ratios (16:9, 9:16, 4:3, 3:4) with Nano Banana 2 Pro provider. The model requires specific dimension combinations (e.g., 1376×768 for 16:9) that differ from the generic aspect ratio map. +- [image-generation] **Runware Seedream 4.0 Dimensions**: Fixed API errors when using non-square aspect ratios with Seedream 4.0 provider. At 1K resolution, only 1:1 is supported; now using 2K dimensions for all aspect ratios to ensure compatibility. + +### Improvements + +- [image-generation] **Runware Async Delivery**: Switched from synchronous requests to async delivery with polling to avoid timeouts on slower image generation models. +- [image-generation] **Runware NanoBanana2Pro I2I**: Added `resolution: '2k'` parameter to image-to-image generation for automatic aspect ratio detection from reference images. + ## [0.2.16] - 2026-01-16 ### New Features diff --git a/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.image2image.ts b/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.image2image.ts index d73ba42b..43629fb3 100644 --- a/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.image2image.ts +++ b/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.image2image.ts @@ -144,10 +144,12 @@ export function NanoBanana2ProImage2Image( // Map to Runware API format // Nano Banana 2 Pro uses inputs.referenceImages for image-to-image // Supports up to 14 reference images + // Resolution parameter tells the API to auto-detect aspect ratio from reference image const referenceImages = input.image_urls ?? (input.image_url ? [input.image_url] : []); return { positivePrompt: input.prompt, + resolution: '2k', inputs: { referenceImages } diff --git a/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.text2image.ts b/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.text2image.ts index cc3e6f04..0617d773 100644 --- a/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.text2image.ts +++ b/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.text2image.ts @@ -8,10 +8,26 @@ import type CreativeEditorSDK from '@cesdk/cesdk-js'; // @ts-ignore - JSON import import NanoBanana2ProSchema from './NanoBanana2Pro.text2image.json'; import createImageProvider from './createImageProvider'; -import { - RunwareProviderConfiguration, - getImageDimensionsFromAspectRatio -} from './types'; +import { RunwareProviderConfiguration } from './types'; + +/** + * Nano Banana 2 Pro (google:4@2) dimension constraints + * These are model-specific dimensions required by the Runware API. + * Source: https://runware.ai/docs/providers/google.md + * + * Note: Generic ASPECT_RATIO_MAP dimensions (e.g., 1344x768 for 16:9) are NOT valid + * for this model - it requires specific dimension combinations per resolution tier. + */ +const NANO_BANANA_2_PRO_DIMENSIONS: Record< + string, + { width: number; height: number } +> = { + '1:1': { width: 1024, height: 1024 }, + '16:9': { width: 1376, height: 768 }, + '9:16': { width: 768, height: 1376 }, + '4:3': { width: 1200, height: 896 }, + '3:4': { width: 896, height: 1200 } +}; /** * Input interface for Nano Banana 2 Pro text-to-image @@ -56,11 +72,10 @@ export function NanoBanana2Pro( cesdk, middleware: config.middlewares ?? [], getImageSize: (input) => - getImageDimensionsFromAspectRatio(input.aspect_ratio ?? '1:1'), + NANO_BANANA_2_PRO_DIMENSIONS[input.aspect_ratio ?? '1:1'], mapInput: (input) => { - const dims = getImageDimensionsFromAspectRatio( - input.aspect_ratio ?? '1:1' - ); + const dims = + NANO_BANANA_2_PRO_DIMENSIONS[input.aspect_ratio ?? '1:1']; return { positivePrompt: input.prompt, width: dims.width, diff --git a/packages/plugin-ai-image-generation-web/src/runware/Seedream4.text2image.ts b/packages/plugin-ai-image-generation-web/src/runware/Seedream4.text2image.ts index 84407b10..615d52e4 100644 --- a/packages/plugin-ai-image-generation-web/src/runware/Seedream4.text2image.ts +++ b/packages/plugin-ai-image-generation-web/src/runware/Seedream4.text2image.ts @@ -8,10 +8,22 @@ import type CreativeEditorSDK from '@cesdk/cesdk-js'; // @ts-ignore - JSON import import Seedream4Schema from './Seedream4.text2image.json'; import createImageProvider from './createImageProvider'; -import { - RunwareProviderConfiguration, - getImageDimensionsFromAspectRatio -} from './types'; +import { RunwareProviderConfiguration } from './types'; + +/** + * Seedream 4.0 (bytedance:5@0) dimension constraints - 2K resolution + * At 1K resolution, only 1024×1024 (1:1) is supported. + * For other aspect ratios, we use 2K dimensions. + * Source: https://runware.ai/docs/providers/bytedance.md + */ +const SEEDREAM4_DIMENSIONS: Record = + { + '1:1': { width: 2048, height: 2048 }, + '16:9': { width: 2560, height: 1440 }, + '9:16': { width: 1440, height: 2560 }, + '4:3': { width: 2304, height: 1728 }, + '3:4': { width: 1728, height: 2304 } + }; /** * Input interface for Seedream 4.0 text-to-image @@ -56,11 +68,9 @@ export function Seedream4( cesdk, middleware: config.middlewares ?? [], getImageSize: (input) => - getImageDimensionsFromAspectRatio(input.aspect_ratio ?? '1:1'), + SEEDREAM4_DIMENSIONS[input.aspect_ratio ?? '1:1'], mapInput: (input) => { - const dims = getImageDimensionsFromAspectRatio( - input.aspect_ratio ?? '1:1' - ); + const dims = SEEDREAM4_DIMENSIONS[input.aspect_ratio ?? '1:1']; return { positivePrompt: input.prompt, width: dims.width, diff --git a/packages/plugin-ai-image-generation-web/src/runware/createRunwareClient.ts b/packages/plugin-ai-image-generation-web/src/runware/createRunwareClient.ts index dda9d148..e3838744 100644 --- a/packages/plugin-ai-image-generation-web/src/runware/createRunwareClient.ts +++ b/packages/plugin-ai-image-generation-web/src/runware/createRunwareClient.ts @@ -2,8 +2,17 @@ * Runware HTTP REST API client * Uses the REST API instead of the WebSocket SDK * API documentation: https://runware.ai/docs/en/getting-started/how-to-connect + * + * Image generation uses async delivery with polling: + * 1. Submit task with deliveryMethod: "async" + * 2. Receive taskUUID in response + * 3. Poll with getResponse until status is "success" or "failed" */ +// Polling configuration +const POLL_INTERVAL_MS = 1000; // Poll every 1 second +const MAX_POLL_TIME_MS = 5 * 60 * 1000; // Maximum 5 minutes + export interface RunwareImageInferenceParams { model: string; positivePrompt: string; @@ -43,11 +52,13 @@ export type RunwareImageInferenceInput = export interface RunwareImageResult { taskType: string; taskUUID: string; - imageURL: string; + status?: 'processing' | 'success' | 'failed'; + imageURL?: string; imageUUID?: string; seed?: number; NSFWContent?: boolean; cost?: number; + errorMessage?: string; } export interface RunwareErrorResponse { @@ -77,10 +88,106 @@ function generateUUID(): string { }); } +function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + export function createRunwareClient( proxyUrl: string, headers?: Record ): RunwareClient { + /** + * Poll for image generation results using getResponse task + */ + // eslint-disable-next-line no-await-in-loop + async function pollForResult( + taskUUID: string, + abortSignal?: AbortSignal + ): Promise { + const startTime = Date.now(); + + // Polling must be sequential - each request depends on the previous result + /* eslint-disable no-await-in-loop */ + while (Date.now() - startTime < MAX_POLL_TIME_MS) { + // Check if aborted + if (abortSignal?.aborted) { + throw new Error('Image generation aborted'); + } + + // Poll for results + const pollBody = [ + { + taskType: 'getResponse', + taskUUID + } + ]; + + const pollResponse = await fetch(proxyUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...headers + }, + body: JSON.stringify(pollBody), + signal: abortSignal + }); + + if (!pollResponse.ok) { + const errorText = await pollResponse.text(); + throw new Error( + `Runware API polling error: ${pollResponse.status} - ${errorText}` + ); + } + + const pollResult = await pollResponse.json(); + + // Check for errors + if (pollResult.errors != null && pollResult.errors.length > 0) { + const error = pollResult.errors[0] as RunwareErrorResponse; + throw new Error(`Runware API error: ${error.errorMessage}`); + } + + if (pollResult.error != null) { + throw new Error( + `Runware API error: ${ + pollResult.error.errorMessage ?? pollResult.error + }` + ); + } + + const data = pollResult.data; + if (data != null && Array.isArray(data) && data.length > 0) { + const imageResult = data.find( + (item: any) => + item.taskType === 'imageInference' && item.taskUUID === taskUUID + ) as RunwareImageResult | undefined; + + if (imageResult != null) { + // Check status + if (imageResult.status === 'success' && imageResult.imageURL) { + return imageResult; + } + + if (imageResult.status === 'failed') { + throw new Error( + imageResult.errorMessage ?? 'Image generation failed' + ); + } + + // Still processing, continue polling + } + } + + // Wait before next poll + await sleep(POLL_INTERVAL_MS); + } + /* eslint-enable no-await-in-loop */ + + throw new Error('Image generation timed out'); + } + return { imageInference: async ( params: RunwareImageInferenceInput, @@ -88,13 +195,14 @@ export function createRunwareClient( ): Promise => { const taskUUID = generateUUID(); - // Build the request body as a JSON array with the imageInference task + // Build the request body with async delivery to avoid timeouts const requestBody = [ { taskType: 'imageInference', taskUUID, model: params.model, positivePrompt: params.positivePrompt, + deliveryMethod: 'async', // Required to avoid timeouts outputType: params.outputType ?? 'URL', outputFormat: params.outputFormat ?? 'PNG', numberResults: params.numberResults ?? 1, @@ -120,6 +228,7 @@ export function createRunwareClient( } ]; + // Submit the image generation task const response = await fetch(proxyUrl, { method: 'POST', headers: { @@ -150,7 +259,7 @@ export function createRunwareClient( ); } - // The response contains a data array with image results + // Verify we got the task acknowledgment const data = result.data; if (data == null || !Array.isArray(data)) { throw new Error( @@ -158,26 +267,19 @@ export function createRunwareClient( ); } - // Filter for imageInference results that match our taskUUID - const imageResults = data.filter( + const taskAck = data.find( (item: any) => item.taskType === 'imageInference' && item.taskUUID === taskUUID - ) as RunwareImageResult[]; - - if (imageResults.length === 0) { - // Fallback: if no exact match, try to get any imageInference results - const anyImageResults = data.filter( - (item: any) => item.taskType === 'imageInference' - ) as RunwareImageResult[]; + ); - if (anyImageResults.length > 0) { - return anyImageResults; - } - - throw new Error('No image results in Runware API response'); + if (taskAck == null) { + throw new Error('Image generation task was not acknowledged'); } - return imageResults; + // Poll for the result + const imageResult = await pollForResult(taskUUID, abortSignal); + + return [imageResult]; } }; } diff --git a/specs/providers/runware/api-patterns.md b/specs/providers/runware/api-patterns.md index ba3c157c..6036a15d 100644 --- a/specs/providers/runware/api-patterns.md +++ b/specs/providers/runware/api-patterns.md @@ -95,6 +95,14 @@ AIR (AI Resource Identifier) follows the pattern: All dimensions must be divisible by 64. Use the predefined mappings: +**⚠️ Important**: Some models require specific dimension combinations and do NOT accept +the generic `ASPECT_RATIO_MAP` values. Always check the model's documentation. Models +with specific requirements include: +- **Nano Banana 2 Pro** (`google:4@2`): Requires specific 1K/2K/4K dimension presets +- **Seedream 4.0** (`bytedance:5@0`): Only supports 1:1 at 1K; other ratios need 2K dimensions + +See `implementation-notes.md` for model-specific dimension tables. + ### Aspect Ratio Map ```typescript diff --git a/specs/providers/runware/implementation-notes.md b/specs/providers/runware/implementation-notes.md index bbd76992..6f97c8a0 100644 --- a/specs/providers/runware/implementation-notes.md +++ b/specs/providers/runware/implementation-notes.md @@ -518,11 +518,50 @@ getBlockInput: async (input) => { ### Known Model Constraints +#### Flexible Range Models (I2I) + +These models accept any dimensions within the specified range: + | Model | AIR | Width | Height | Multiple | |-------|-----|-------|--------|----------| | FLUX.2 [dev] | `runware:400@1` | 512-2048 | 512-2048 | 16 | | FLUX.2 [pro] | `bfl:5@1` | 256-1920 | 256-1920 | 16 | | FLUX.2 [flex] | `bfl:6@1` | 256-1920 | 256-1920 | 16 | +| Seedream 4.0 | `bytedance:5@0` | 128-2048 | 128-2048 | 64 | + +#### Fixed Dimension Models (T2I) + +These models require specific dimension combinations. The generic `ASPECT_RATIO_MAP` does NOT work: + +**Nano Banana 2 Pro** (`google:4@2`) - 1K Resolution: + +| Aspect Ratio | Width | Height | +|--------------|-------|--------| +| 1:1 | 1024 | 1024 | +| 16:9 | 1376 | 768 | +| 9:16 | 768 | 1376 | +| 4:3 | 1200 | 896 | +| 3:4 | 896 | 1200 | + +**Seedream 4.0** (`bytedance:5@0`) - 2K Resolution (1K only supports 1:1): + +| Aspect Ratio | Width | Height | +|--------------|-------|--------| +| 1:1 | 2048 | 2048 | +| 16:9 | 2560 | 1440 | +| 9:16 | 1440 | 2560 | +| 4:3 | 2304 | 1728 | +| 3:4 | 1728 | 2304 | + +**Seedream 4.5** (`bytedance:seedream@4.5`) - 2K Resolution: + +| Aspect Ratio | Width | Height | +|--------------|-------|--------| +| 1:1 | 2048 | 2048 | +| 16:9 | 2560 | 1440 | +| 9:16 | 1440 | 2560 | +| 4:3 | 2304 | 1728 | +| 3:4 | 1728 | 2304 | ### Extracting Constraints from API Docs