From 5740f65a5ea772056298850d76252b4a9d004e4a Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 15 Dec 2025 10:26:29 +0100 Subject: [PATCH 01/20] feat(eachlabs): add EachLabs AI provider boilerplate infrastructure Add scaffolding for EachLabs partner integration with empty provider implementations ready for model additions: - Create eachlabs directories in image and video generation packages - Add provider configuration types and client stubs - Configure package.json exports and esbuild entry points - Add EachLabs as selectable partner in AI example app --- examples/ai/src/App.tsx | 17 ++++++++- examples/ai/src/eachlabsProviders.ts | 21 +++++++++++ .../esbuild/config.mjs | 3 +- .../package.json | 4 +++ .../src/eachlabs/createEachLabsClient.ts | 35 ++++++++++++++++++ .../src/eachlabs/createImageProvider.ts | 26 ++++++++++++++ .../src/eachlabs/index.ts | 11 ++++++ .../src/eachlabs/types.ts | 28 +++++++++++++++ .../esbuild/config.mjs | 2 +- .../package.json | 4 +++ .../src/eachlabs/createEachLabsClient.ts | 36 +++++++++++++++++++ .../src/eachlabs/createVideoProvider.ts | 26 ++++++++++++++ .../src/eachlabs/index.ts | 11 ++++++ .../src/eachlabs/types.ts | 26 ++++++++++++++ 14 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 examples/ai/src/eachlabsProviders.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/index.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/types.ts create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/createEachLabsClient.ts create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/createVideoProvider.ts create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/index.ts create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/types.ts diff --git a/examples/ai/src/App.tsx b/examples/ai/src/App.tsx index 6b224bb4..038de2f1 100644 --- a/examples/ai/src/App.tsx +++ b/examples/ai/src/App.tsx @@ -20,8 +20,9 @@ import { import { createFalAiProviders } from './falaiProviders'; import { createOpenAiProviders } from './openaiProviders'; import { createRunwareProviders } from './runwareProviders'; +import { createEachLabsProviders } from './eachlabsProviders'; -export type ProviderPartner = 'fal-ai' | 'openai' | 'runware'; +export type ProviderPartner = 'fal-ai' | 'openai' | 'runware' | 'eachlabs'; function App() { const cesdk = useRef(); @@ -218,6 +219,11 @@ function App() { ...middlewareOptions, proxyUrl: import.meta.env.VITE_RUNWARE_PROXY_URL }); + case 'eachlabs': + return createEachLabsProviders({ + ...middlewareOptions, + proxyUrl: import.meta.env.VITE_EACHLABS_PROXY_URL + }); default: return createFalAiProviders({ ...middlewareOptions, @@ -334,6 +340,8 @@ function App() { return 'OpenAI'; case 'runware': return 'Runware'; + case 'eachlabs': + return 'EachLabs'; default: return 'Fal.ai'; } @@ -366,6 +374,13 @@ function App() { setSearchParams({ archive: archiveType!, partner: 'runware' }); } }); + builder.Button('eachlabs', { + label: 'EachLabs', + isSelected: providerPartner === 'eachlabs', + onClick: () => { + setSearchParams({ archive: archiveType!, partner: 'eachlabs' }); + } + }); } }); }); diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts new file mode 100644 index 00000000..6c211467 --- /dev/null +++ b/examples/ai/src/eachlabsProviders.ts @@ -0,0 +1,21 @@ +// EachLabs provider namespace +// Providers will be added here when EachLabs models are implemented +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { Middleware } from '@imgly/plugin-ai-generation-web'; + +export interface EachLabsProviderOptions { + imageRateLimitMiddleware: Middleware; + videoRateLimitMiddleware: Middleware; + errorMiddleware: Middleware; + proxyUrl: string; +} + +export function createEachLabsProviders(_options: EachLabsProviderOptions) { + return { + text2image: [], + image2image: [], + text2video: [], + image2video: [] + }; +} diff --git a/packages/plugin-ai-image-generation-web/esbuild/config.mjs b/packages/plugin-ai-image-generation-web/esbuild/config.mjs index be9b47ce..46787600 100644 --- a/packages/plugin-ai-image-generation-web/esbuild/config.mjs +++ b/packages/plugin-ai-image-generation-web/esbuild/config.mjs @@ -30,7 +30,8 @@ export default ({ isDevelopment }) => { './src/index.ts', './src/fal-ai/index.ts', './src/open-ai/index.ts', - './src/runware/index.ts' + './src/runware/index.ts', + './src/eachlabs/index.ts' ]; config.outExtension = { '.js': '.mjs' }; config.outdir = './dist'; diff --git a/packages/plugin-ai-image-generation-web/package.json b/packages/plugin-ai-image-generation-web/package.json index 38bcf84f..2ecbbf3a 100644 --- a/packages/plugin-ai-image-generation-web/package.json +++ b/packages/plugin-ai-image-generation-web/package.json @@ -40,6 +40,10 @@ "./runware": { "import": "./dist/runware/index.mjs", "types": "./dist/runware/index.d.ts" + }, + "./eachlabs": { + "import": "./dist/eachlabs/index.mjs", + "types": "./dist/eachlabs/index.d.ts" } }, "homepage": "https://img.ly/products/creative-sdk", diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts new file mode 100644 index 00000000..61112eed --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts @@ -0,0 +1,35 @@ +// EachLabs API client +// TODO: Implement actual API integration +/* eslint-disable @typescript-eslint/no-unused-vars */ + +export interface EachLabsImageInferenceParams { + // TODO: Define parameters based on EachLabs API + model: string; + prompt: string; + width?: number; + height?: number; +} + +export interface EachLabsImageResult { + // TODO: Define result structure based on EachLabs API + imageURL: string; +} + +export interface EachLabsClient { + imageInference: ( + params: EachLabsImageInferenceParams, + abortSignal?: AbortSignal + ) => Promise; +} + +export function createEachLabsClient( + _proxyUrl: string, + _headers?: Record +): EachLabsClient { + return { + imageInference: async (): Promise => { + // TODO: Implement actual API call + throw new Error('EachLabs client not yet implemented'); + } + }; +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts new file mode 100644 index 00000000..ba7c49cc --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts @@ -0,0 +1,26 @@ +// EachLabs image provider factory +// TODO: Implement actual provider creation + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Middleware = (input: any, output: any) => any; + +export interface EachLabsProviderConfiguration { + /** + * The URL of the proxy server to use for API requests + */ + proxyUrl: string; + /** + * Enable debug mode for logging + */ + debug?: boolean; + /** + * Optional middlewares to apply to the provider + */ + middlewares?: Middleware[]; + /** + * History configuration + */ + history?: false | '@imgly/local' | '@imgly/indexedDB'; +} + +// Provider factory will be implemented here when models are added diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts new file mode 100644 index 00000000..3b93c7d0 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts @@ -0,0 +1,11 @@ +// EachLabs provider namespace +// Providers are added here via the partner-providers-eachlabs skill + +const EachLabs = { + // Models will be added here +}; + +export default EachLabs; + +// Re-export types +export type { EachLabsProviderConfiguration } from './types'; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/types.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/types.ts new file mode 100644 index 00000000..7246f030 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/types.ts @@ -0,0 +1,28 @@ +// Re-export client type from the HTTP client +export type { EachLabsClient } from './createEachLabsClient'; + +// Re-export configuration type from createImageProvider +export type { EachLabsProviderConfiguration } from './createImageProvider'; + +// Aspect ratio to dimensions mapping (all divisible by 64) +export const ASPECT_RATIO_MAP: Record< + string, + { width: number; height: number } +> = { + '1:1': { width: 1024, height: 1024 }, + '16:9': { width: 1344, height: 768 }, + '9:16': { width: 768, height: 1344 }, + '4:3': { width: 1152, height: 896 }, + '3:4': { width: 896, height: 1152 }, + '3:2': { width: 1152, height: 768 }, + '2:3': { width: 768, height: 1152 }, + '21:9': { width: 1536, height: 640 }, + '9:21': { width: 640, height: 1536 } +}; + +export function getImageDimensionsFromAspectRatio(aspectRatio: string): { + width: number; + height: number; +} { + return ASPECT_RATIO_MAP[aspectRatio] ?? { width: 1024, height: 1024 }; +} diff --git a/packages/plugin-ai-video-generation-web/esbuild/config.mjs b/packages/plugin-ai-video-generation-web/esbuild/config.mjs index 47374c81..2398af19 100644 --- a/packages/plugin-ai-video-generation-web/esbuild/config.mjs +++ b/packages/plugin-ai-video-generation-web/esbuild/config.mjs @@ -27,7 +27,7 @@ export default ({ isDevelopment }) => { const config = baseConfig(baseOptions); // Set entry points and output configuration - config.entryPoints = ['./src/index.ts', './src/fal-ai/index.ts', './src/runware/index.ts']; + config.entryPoints = ['./src/index.ts', './src/fal-ai/index.ts', './src/runware/index.ts', './src/eachlabs/index.ts']; config.outExtension = { '.js': '.mjs' }; config.outdir = './dist'; config.outbase = './src'; diff --git a/packages/plugin-ai-video-generation-web/package.json b/packages/plugin-ai-video-generation-web/package.json index 76bff1f3..e1c665bf 100644 --- a/packages/plugin-ai-video-generation-web/package.json +++ b/packages/plugin-ai-video-generation-web/package.json @@ -31,6 +31,10 @@ "./runware": { "import": "./dist/runware/index.mjs", "types": "./dist/runware/index.d.ts" + }, + "./eachlabs": { + "import": "./dist/eachlabs/index.mjs", + "types": "./dist/eachlabs/index.d.ts" } }, "homepage": "https://img.ly/products/creative-sdk", diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/createEachLabsClient.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/createEachLabsClient.ts new file mode 100644 index 00000000..a96931cd --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/createEachLabsClient.ts @@ -0,0 +1,36 @@ +// EachLabs API client +// TODO: Implement actual API integration +/* eslint-disable @typescript-eslint/no-unused-vars */ + +export interface EachLabsVideoInferenceParams { + // TODO: Define parameters based on EachLabs API + model: string; + prompt: string; + width?: number; + height?: number; + duration?: number; +} + +export interface EachLabsVideoResult { + // TODO: Define result structure based on EachLabs API + videoURL: string; +} + +export interface EachLabsClient { + videoInference: ( + params: EachLabsVideoInferenceParams, + abortSignal?: AbortSignal + ) => Promise; +} + +export function createEachLabsClient( + _proxyUrl: string, + _headers?: Record +): EachLabsClient { + return { + videoInference: async (): Promise => { + // TODO: Implement actual API call + throw new Error('EachLabs client not yet implemented'); + } + }; +} diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/createVideoProvider.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/createVideoProvider.ts new file mode 100644 index 00000000..0817b5e3 --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/createVideoProvider.ts @@ -0,0 +1,26 @@ +// EachLabs video provider factory +// TODO: Implement actual provider creation + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Middleware = (input: any, output: any) => any; + +export interface EachLabsProviderConfiguration { + /** + * The URL of the proxy server to use for API requests + */ + proxyUrl: string; + /** + * Enable debug mode for logging + */ + debug?: boolean; + /** + * Optional middlewares to apply to the provider + */ + middlewares?: Middleware[]; + /** + * History configuration + */ + history?: false | '@imgly/local' | '@imgly/indexedDB'; +} + +// Provider factory will be implemented here when models are added diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts new file mode 100644 index 00000000..3b93c7d0 --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts @@ -0,0 +1,11 @@ +// EachLabs provider namespace +// Providers are added here via the partner-providers-eachlabs skill + +const EachLabs = { + // Models will be added here +}; + +export default EachLabs; + +// Re-export types +export type { EachLabsProviderConfiguration } from './types'; diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/types.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/types.ts new file mode 100644 index 00000000..d8e56fdc --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/types.ts @@ -0,0 +1,26 @@ +// Re-export client type from the HTTP client +export type { EachLabsClient } from './createEachLabsClient'; + +// Re-export configuration type from createVideoProvider +export type { EachLabsProviderConfiguration } from './createVideoProvider'; + +// Video aspect ratio to dimensions mapping (all divisible by 64) +export const VIDEO_ASPECT_RATIO_MAP: Record< + string, + { width: number; height: number } +> = { + '16:9': { width: 1280, height: 720 }, + '9:16': { width: 720, height: 1280 }, + '1:1': { width: 1024, height: 1024 }, + '4:3': { width: 1024, height: 768 }, + '3:4': { width: 768, height: 1024 }, + '21:9': { width: 1344, height: 576 }, + '9:21': { width: 576, height: 1344 } +}; + +export function getVideoDimensionsFromAspectRatio(aspectRatio: string): { + width: number; + height: number; +} { + return VIDEO_ASPECT_RATIO_MAP[aspectRatio] ?? { width: 1280, height: 720 }; +} From 7bc27e04098576857ce25bed359f8b62c8075c2b Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Mon, 15 Dec 2025 15:13:10 +0100 Subject: [PATCH 02/20] feat(eachlabs): add EachLabs AI provider skill and discovery infrastructure Add Claude skill configuration for EachLabs provider integration: - SKILL.md with provider implementation guidelines - Discovery and implementation checklists - Model discovery script for API exploration - Initial provider specifications document --- .../DISCOVERY_CHECKLIST.md | 58 +++ .../IMPLEMENTATION_CHECKLIST.md | 438 ++++++++++++++++++ .../partner-providers-eachlabs/SKILL.md | 299 ++++++++++++ .../discover-models.mjs | 50 ++ specs/providers/eachlabs/providers.md | 106 +++++ 5 files changed, 951 insertions(+) create mode 100644 .claude/skills/partner-providers-eachlabs/DISCOVERY_CHECKLIST.md create mode 100644 .claude/skills/partner-providers-eachlabs/IMPLEMENTATION_CHECKLIST.md create mode 100644 .claude/skills/partner-providers-eachlabs/SKILL.md create mode 100644 .claude/skills/partner-providers-eachlabs/discover-models.mjs create mode 100644 specs/providers/eachlabs/providers.md diff --git a/.claude/skills/partner-providers-eachlabs/DISCOVERY_CHECKLIST.md b/.claude/skills/partner-providers-eachlabs/DISCOVERY_CHECKLIST.md new file mode 100644 index 00000000..f17e2986 --- /dev/null +++ b/.claude/skills/partner-providers-eachlabs/DISCOVERY_CHECKLIST.md @@ -0,0 +1,58 @@ +# Discovery Checklist + +## Quick Discovery + +Run the discovery script to get all available models: + +```bash +node .claude/skills/partner-providers-eachlabs/discover-models.mjs +``` + +This outputs a minimal JSON array sorted by `output_type`: + +```json +[ + { "slug": "flux-2-pro", "title": "Flux 2 Pro", "output_type": "image" }, + { "slug": "kling-v2-6-pro-text-to-video", "title": "Kling | v2.6 | Pro | Text to Video", "output_type": "video" } +] +``` + +## Workflow + +1. **Run the script** to get current API models +2. **Read `specs/providers/eachlabs/providers.md`** to see what's tracked +3. **Compare** the lists to identify: + - New models (in API but not in providers.md) + - Removed models (in providers.md but not in API) +4. **Report findings** to user with recommendations +5. **Update providers.md** with new models after user approval + +## Output Types + +| output_type | Meaning | +|-------------|---------| +| `image` | Single image output | +| `array` | Multiple images (batch) | +| `video` | Video output | +| `audio` | Audio output | +| `text` | Text output | +| `object` | Structured data | +| `code` | Training/code output | + +## Capability Detection + +Determine capability from slug patterns: + +| Pattern in slug | Capability | +|-----------------|------------| +| `text-to-video` | t2v | +| `image-to-video` | i2v | +| `text-to-image` | t2i | +| `-edit`, `-remix` | i2i | +| `reference-to-` | i2i or i2v | + +## Notes + +- No authentication required for the models API +- The script fetches up to 500 models +- Full model details (including `request_schema`) available via: `GET https://api.eachlabs.ai/v1/model?slug=` diff --git a/.claude/skills/partner-providers-eachlabs/IMPLEMENTATION_CHECKLIST.md b/.claude/skills/partner-providers-eachlabs/IMPLEMENTATION_CHECKLIST.md new file mode 100644 index 00000000..8e6bfabc --- /dev/null +++ b/.claude/skills/partner-providers-eachlabs/IMPLEMENTATION_CHECKLIST.md @@ -0,0 +1,438 @@ +# Implementation Checklist + +Step-by-step process for implementing EachLabs providers. + +## Prerequisites + +- [ ] User has approved which model to implement +- [ ] Model slug and capability are known from discovery phase + +## Key Principle: One Capability = One Provider + +**IMPORTANT**: Each capability requires a separate provider file. A model with both text-to-image AND image-to-image capabilities needs TWO provider implementations: + +``` +Flux2Pro.text2image.ts -> text-to-image provider +Flux2Pro.image2image.ts -> image-to-image provider (separate implementation) +``` + +The `providers.md` file tracks each capability as a separate row. Implement one capability at a time. + +## Implementation Steps + +### 1. Determine Provider Type + +Map capability to provider type: + +| Capability | Provider Type | File Pattern | Target Directory | +|------------|---------------|--------------|------------------| +| text-to-image | t2i | `{Model}.text2image.ts` | `plugin-ai-image-generation-web/src/eachlabs/` | +| image-to-image | i2i | `{Model}.image2image.ts` | `plugin-ai-image-generation-web/src/eachlabs/` | +| text-to-video | t2v | `{Model}.text2video.ts` | `plugin-ai-video-generation-web/src/eachlabs/` | +| image-to-video | i2v | `{Model}.image2video.ts` | `plugin-ai-video-generation-web/src/eachlabs/` | + +### 2. Read Required Documentation + +**Before writing any code**, read these files in order: + +1. **`specs/providers/patterns/ui-guidelines.md`** - CRITICAL + - Which parameters to expose in UI (prompt, aspect_ratio, image_url) + - Which parameters to NEVER expose (seed, cfg_scale, steps, etc.) + - Standard aspect ratios and dimensions + - JSON schema component reference + +2. **Existing fal.ai providers as reference** + - Similar models show the pattern to follow + - Look at how `request_schema` → OpenAPI conversion works + +### 3. Fetch Model Details from API (MANDATORY) + +**CRITICAL**: Always fetch the current model details from the API before implementation: + +``` +WebFetch: https://api.eachlabs.ai/v1/model?slug= +``` + +Extract: +- `request_schema` - JSON Schema for all input parameters +- `required` - Which fields are mandatory +- `properties` - All available parameters with types, enums, defaults +- `output_type` - Confirm expected output format + +### 4. Convert request_schema to OpenAPI Format + +EachLabs provides JSON Schema. Convert to OpenAPI with IMG.LY extensions: + +#### 4a. Create the OpenAPI wrapper + +```json +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs API", + "version": "1.0.0", + "description": "EachLabs API for " + }, + "components": { + "schemas": { + "Input": { + // Converted request_schema goes here + } + } + }, + "paths": {} +} +``` + +#### 4b. Add x-imgly-* extensions to properties + +For each property in request_schema: + +| Property Type | Add Extension | +|---------------|---------------| +| `string` (long text, prompt) | `"x-imgly-builder": { "component": "TextArea" }` | +| `string` (enum) | `"x-imgly-builder": { "component": "Select" }` + `"x-imgly-enum-labels": {...}` | +| `string` (url) | Handle via `renderCustomProperty` | +| `boolean` | `"x-imgly-builder": { "component": "Switch" }` | +| `integer`/`number` | `"x-imgly-builder": { "component": "Number" }` | + +#### 4c. Add property ordering + +Add `x-fal-order-properties` to control UI display order: + +```json +{ + "x-fal-order-properties": ["prompt", "image_size", "duration"] +} +``` + +**Order priority**: +1. `prompt` - Always first +2. `image_url` / `image_urls` - If present +3. `aspect_ratio` / `image_size` - Format/dimensions +4. `duration` - For video +5. Other user-facing options +6. Hidden parameters (seed, cfg_scale) - Don't include in order + +#### 4d. Create human-readable enum labels + +```json +"image_size": { + "type": "string", + "enum": ["square_hd", "landscape_4_3", "portrait_4_3"], + "x-imgly-enum-labels": { + "square_hd": "Square HD", + "landscape_4_3": "Landscape 4:3", + "portrait_4_3": "Portrait 4:3" + } +} +``` + +### 5. Decide Which Parameters to Expose + +Follow `specs/providers/patterns/ui-guidelines.md`: + +**ALWAYS expose:** +- `prompt` - Text input for generation +- `image_url` / `image_urls` - For I2I/I2V +- `aspect_ratio` / `image_size` - Format selection +- `duration` - For video + +**NEVER expose (hide from UI):** +- `seed` - Reproducibility parameter +- `cfg_scale` / `guidance_scale` - Technical parameter +- `steps` / `num_inference_steps` - Technical parameter +- `enhance_prompt` - Internal processing +- `sync_mode` - API implementation detail +- `enable_safety_checker` / `safety_tolerance` - Let backend handle + +To hide a parameter, either: +1. Don't include it in `x-fal-order-properties` +2. Set a sensible default and don't expose in schema + +### 6. Create Provider Files + +#### 6a. Create JSON Schema file: `{ModelName}.{capability}.json` + +```json +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs API", + "version": "1.0.0", + "description": "EachLabs API for Flux 2 Pro" + }, + "components": { + "schemas": { + "Flux2ProInput": { + "title": "Flux2ProInput", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "minLength": 1, + "description": "The prompt to generate an image from.", + "x-imgly-builder": { "component": "TextArea" } + }, + "image_size": { + "title": "Format", + "type": "string", + "enum": ["square_hd", "square", "portrait_4_3", "portrait_16_9", "landscape_4_3", "landscape_16_9"], + "default": "landscape_4_3", + "x-imgly-enum-labels": { + "square_hd": "Square HD (1024x1024)", + "square": "Square (512x512)", + "portrait_4_3": "Portrait 4:3", + "portrait_16_9": "Portrait 16:9", + "landscape_4_3": "Landscape 4:3", + "landscape_16_9": "Landscape 16:9" + }, + "x-imgly-builder": { "component": "Select" } + } + }, + "x-fal-order-properties": ["prompt", "image_size"], + "required": ["prompt"] + } + } + }, + "paths": {} +} +``` + +#### 6b. Create TypeScript file: `{ModelName}.{capability}.ts` + +```typescript +import { + CommonProviderConfiguration, + ImageOutput, + type Provider, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import schema from './Flux2Pro.text2image.json'; +import CreativeEditorSDK from '@cesdk/cesdk-js'; +import createImageProvider from './createImageProvider'; + +interface ProviderConfiguration + extends CommonProviderConfiguration {} + +type Flux2ProInput = { + prompt: string; + image_size?: 'square_hd' | 'square' | 'portrait_4_3' | 'portrait_16_9' | 'landscape_4_3' | 'landscape_16_9'; +}; + +export function Flux2Pro( + config: ProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const modelKey = 'eachlabs/flux-2-pro'; + + // Set translations + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(modelKey)}.prompt`]: 'Describe your image...', + [`libraries.${getPanelId(modelKey)}.history.label`]: 'Generated Images' + } + }); + + return getProvider(cesdk, config); + }; +} + +function getProvider( + cesdk: CreativeEditorSDK, + config: ProviderConfiguration +): Provider<'image', Flux2ProInput, ImageOutput> { + return createImageProvider( + { + modelSlug: 'flux-2-pro', + modelKey: 'eachlabs/flux-2-pro', + name: 'Flux 2 Pro', + // @ts-ignore - OpenAPI type compatibility + schema, + inputReference: '#/components/schemas/Flux2ProInput', + cesdk, + getImageSize: (input) => { + // Map image_size to dimensions + const sizeMap: Record = { + 'square_hd': { width: 1024, height: 1024 }, + 'square': { width: 512, height: 512 }, + 'portrait_4_3': { width: 768, height: 1024 }, + 'portrait_16_9': { width: 576, height: 1024 }, + 'landscape_4_3': { width: 1024, height: 768 }, + 'landscape_16_9': { width: 1024, height: 576 } + }; + return sizeMap[input.image_size ?? 'landscape_4_3']; + } + }, + config + ); +} + +export default Flux2Pro; +``` + +### 7. Update createImageProvider / createVideoProvider + +Ensure the EachLabs provider factories use the EachLabs client correctly: + +- `createImageProvider` should call `eachlabsClient.createPrediction` +- Poll for completion via `eachlabsClient.getPrediction` +- Extract output URL from response + +### 8. Register Provider + +Add export to the appropriate index file: + +```typescript +// packages/plugin-ai-image-generation-web/src/eachlabs/index.ts +import { Flux2Pro } from './Flux2Pro.text2image'; + +const EachLabs = { + Flux2Pro: { + Text2Image: Flux2Pro + // Image2Image: Flux2ProI2I // Added when I2I is implemented + } +}; + +export default EachLabs; +``` + +### 9. Update translations.json (REQUIRED) + +**CRITICAL**: Every provider needs UI translations for its properties. + +| Capability | Translations File | +|------------|-------------------| +| text-to-image, image-to-image | `packages/plugin-ai-image-generation-web/translations.json` | +| text-to-video, image-to-video | `packages/plugin-ai-video-generation-web/translations.json` | + +**Translation Key Pattern**: `ly.img.plugin-ai-{kind}-generation-web.{providerId}.property.{property-name}` + +Example: +```json +"ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.prompt": "Prompt", +"ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.prompt.placeholder": "Describe your image...", +"ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size": "Format", +"ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size.square_hd": "Square HD", +"ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size.landscape_4_3": "Landscape 4:3" +``` + +### 10. Add to Example App (REQUIRED) + +**CRITICAL**: Every new provider MUST be added to `examples/ai/src/eachlabsProviders.ts`: + +```typescript +import EachLabsImage from '@imgly/plugin-ai-image-generation-web/eachlabs'; +import EachLabsVideo from '@imgly/plugin-ai-video-generation-web/eachlabs'; + +export function createEachLabsProviders(options: EachLabsProviderOptions) { + const { imageRateLimitMiddleware, errorMiddleware, proxyUrl } = options; + + return { + text2image: [ + EachLabsImage.Flux2Pro.Text2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl + }) + ], + image2image: [], + text2video: [], + image2video: [] + }; +} +``` + +### 11. Update providers.md + +Change status from `planned` to `implemented`: + +```markdown +| Slug | Name | Capability | Status | +|------|------|------------|--------| +| flux-2-pro | Flux 2 Pro | text-to-image | implemented | +``` + +### 12. Update README Documentation (REQUIRED) + +Add provider to the plugin's README.md: + +1. **Import statement**: Add import example +2. **Providers section**: Add numbered provider entry with code example +3. **API Reference section**: Add provider function signatures +4. **Panel IDs section**: Add provider panel IDs +5. **Asset History section**: Add history source IDs + +### 13. Run Validation + +```bash +pnpm --filter "@imgly/plugin-ai-*" check:all +``` + +Fix any: +- [ ] TypeScript errors +- [ ] Lint errors +- [ ] Build failures + +### 14. Test (if demo available) + +```bash +cd examples/ai +pnpm dev +``` + +Verify the provider appears and generates correctly. + +## Common Issues + +| Issue | Solution | +|-------|----------| +| OpenAPI type errors | Add `// @ts-ignore` before `schema:` property | +| Missing paths property | Add `"paths": {}` to JSON schema | +| Unknown image_size values | Check EachLabs API docs for exact enum values | +| Provider not appearing | Check index.ts export and translations | + +## EachLabs-Specific Notes + +### Polling for Results + +EachLabs uses async predictions. The client must: +1. POST to `/v1/prediction` to start generation +2. Poll `GET /v1/prediction/{id}` until status is `success` or `failed` +3. Extract output URL from response + +### Image Size Mapping + +EachLabs uses named sizes like `square_hd`, `landscape_4_3`. Map these to actual dimensions: + +```typescript +const sizeMap: Record = { + 'square_hd': { width: 1024, height: 1024 }, + 'square': { width: 512, height: 512 }, + 'portrait_4_3': { width: 768, height: 1024 }, + 'portrait_16_9': { width: 576, height: 1024 }, + 'landscape_4_3': { width: 1024, height: 768 }, + 'landscape_16_9': { width: 1024, height: 576 } +}; +``` + +### Video Duration + +EachLabs video models often use string durations like `"5"` or `"10"` (seconds). Parse appropriately: + +```typescript +const duration = parseInt(input.duration ?? '5', 10); +``` + +## Critical Reminders + +1. **One capability = one provider file** - Never combine T2I and I2I in one file +2. **Always fetch model details from API** - Don't guess parameter names +3. **JSON schema must include `"paths": {}`** - OpenAPI type requirement +4. **Always use `// @ts-ignore` before schema** - TypeScript workaround +5. **Update providers.md for only the implemented capability** +6. **ALWAYS add to example app** +7. **ALWAYS add translations** +8. **ALWAYS update README documentation** diff --git a/.claude/skills/partner-providers-eachlabs/SKILL.md b/.claude/skills/partner-providers-eachlabs/SKILL.md new file mode 100644 index 00000000..bffaa38e --- /dev/null +++ b/.claude/skills/partner-providers-eachlabs/SKILL.md @@ -0,0 +1,299 @@ +--- +name: partner-providers-eachlabs +description: | + Discover and implement EachLabs AI providers. Use when: checking for new + EachLabs models, implementing EachLabs providers, updating providers.md status, + or working with EachLabs API integrations. +--- + +# EachLabs Provider Management + +Manage the lifecycle of EachLabs AI providers: discover new models via API and implement them as IMG.LY providers. + +## Key Advantages Over Other Providers + +EachLabs provides a **structured API** for model discovery: +1. **Programmatic model listing** - No need to scrape documentation +2. **request_schema** - Each model includes JSON Schema for inputs (similar to fal.ai) +3. **Consistent API patterns** - Unified prediction/polling interface across all models + +## Key Files + +- **Provider Tracking**: `specs/providers/eachlabs/providers.md` +- **Provider Specifications**: `specs/providers/` (schemas, architecture) +- **Provider Implementations**: + - Image: `packages/plugin-ai-image-generation-web/src/eachlabs/` + - Video: `packages/plugin-ai-video-generation-web/src/eachlabs/` +- **README Documentation**: + - Image: `packages/plugin-ai-image-generation-web/README.md` + - Video: `packages/plugin-ai-video-generation-web/README.md` + +## EachLabs API Reference + +### Base URL +``` +https://api.eachlabs.ai +``` + +### Authentication +- API Key via header: `X-API-Key: ` +- **Discovery endpoints do NOT require authentication:** + - `GET /v1/models` - List all models + - `GET /v1/model?slug=` - Get model details including `request_schema` +- **Prediction endpoints require API key:** + - `POST /v1/prediction` - Create prediction + - `GET /v1/prediction/{id}` - Get prediction status + +### Endpoints + +#### List All Models (No Auth Required) +``` +GET /v1/models?limit=500&offset=0&name= +``` + +Response: +```json +{ + "models": [ + { + "title": "Flux 2 Pro", + "slug": "flux-2-pro", + "version": "0.0.1", + "output_type": "image" + } + ] +} +``` + +#### Get Model Details (No Auth Required) +``` +GET /v1/model?slug= +``` + +Response includes `request_schema`: +```json +{ + "title": "Flux 2 Pro", + "slug": "flux-2-pro", + "version": "0.0.1", + "output_type": "image", + "request_schema": { + "type": "object", + "required": ["prompt"], + "properties": { + "prompt": { + "type": "string", + "default": "", + "description": "The prompt to generate an image from." + }, + "image_size": { + "type": "string", + "default": "landscape_4_3", + "enum": ["square_hd", "square", "portrait_4_3", "portrait_16_9", "landscape_4_3", "landscape_16_9"] + } + } + } +} +``` + +#### Create Prediction (Auth Required) +``` +POST /v1/prediction +Content-Type: application/json +X-API-Key: + +{ + "model": "flux-2-pro", + "version": "0.0.1", + "input": { ... } +} +``` + +#### Get Prediction Status (Auth Required) +``` +GET /v1/prediction/{id} +X-API-Key: +``` + +Response: +```json +{ + "id": "...", + "status": "success|processing|starting|failed|cancelled", + "output": { ... }, + "predict_time": 12.5, + "cost": 0.05 +} +``` + +## Workflow Overview + +This skill operates in two phases: + +1. **Discovery** (automatic): Fetch models via API and compare against tracked models +2. **Implementation** (user-approved): Create provider files for selected models + +## Phase 1: Discovery + +Run the discovery script to get available models: + +```bash +node .claude/skills/partner-providers-eachlabs/discover-models.mjs +``` + +This outputs a minimal JSON array with `slug`, `title`, and `output_type` for each model. Compare against `specs/providers/eachlabs/providers.md` to identify new models. + +For detailed steps, see `DISCOVERY_CHECKLIST.md`. + +### Model Categories + +Classify models by `output_type`: +- `image` - Image generation (text-to-image, image-to-image) +- `video` - Video generation (text-to-video, image-to-video) +- `audio` - Audio generation +- `text` - Text generation +- `array` - Multi-output (e.g., multiple images) + +### Determining Capability from Slug/Schema + +| Pattern | Capability | +|---------|------------| +| `-text-to-video` | text-to-video | +| `-image-to-video` | image-to-video | +| `image_url` in required | image-to-image or image-to-video | +| Only `prompt` required, output=image | text-to-image | +| Only `prompt` required, output=video | text-to-video | + +### Discovery Output Format + +``` +## Discovery Results + +### New Models Found +| Slug | Name | Type | Capability | Recommendation | +|------|------|------|------------|----------------| +| flux-2-pro | Flux 2 Pro | image | t2i | implement | + +### Updated Models +| Slug | Change | +|------|--------| +| ... | ... | + +### Summary +- X new models found +- Y recommended for implementation +- Z skipped (older versions, niche use cases) +``` + +After presenting results, ask: "Which models would you like me to implement?" + +## Phase 2: Implementation + +Only proceed after user approval. Follow `IMPLEMENTATION_CHECKLIST.md` to: + +1. Fetch model details to get `request_schema` +2. Convert `request_schema` to OpenAPI format (add x-imgly-* extensions) +3. Create provider TypeScript file and JSON schema +4. Export provider from `eachlabs/index.ts` +5. Add translations to `translations.json` +6. Add to example app (`examples/ai/src/eachlabsProviders.ts`) +7. Update `specs/providers/eachlabs/providers.md` status to "implemented" +8. Update README documentation +9. Run build checks: `pnpm --filter "@imgly/plugin-ai-*" check:all` + +### Schema Conversion + +EachLabs `request_schema` is JSON Schema format. Convert to OpenAPI with IMG.LY extensions: + +**Input (EachLabs):** +```json +{ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "The prompt" + }, + "image_size": { + "type": "string", + "enum": ["square", "landscape_4_3"] + } + } +} +``` + +**Output (OpenAPI with extensions):** +```json +{ + "openapi": "3.0.0", + "info": { "title": "EachLabs API", "version": "1.0.0" }, + "components": { + "schemas": { + "ModelNameInput": { + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "The prompt", + "x-imgly-builder": { "component": "TextArea" } + }, + "image_size": { + "title": "Format", + "type": "string", + "enum": ["square", "landscape_4_3"], + "x-imgly-enum-labels": { + "square": "Square", + "landscape_4_3": "Landscape 4:3" + }, + "x-imgly-builder": { "component": "Select" } + } + }, + "x-fal-order-properties": ["prompt", "image_size"], + "required": ["prompt"] + } + } + }, + "paths": {} +} +``` + +### UI Component Mapping + +| JSON Schema Type | x-imgly-builder component | +|------------------|---------------------------| +| `string` (long text) | `TextArea` | +| `string` (enum) | `Select` | +| `string` (url/image) | Custom via `renderCustomProperty` | +| `boolean` | `Switch` | +| `integer`/`number` | `Number` | + +## Status Values + +When updating providers.md: + +| Status | When to Use | +|--------|-------------| +| `implemented` | Provider code exists and works | +| `planned` | Will implement (high priority) | +| `skipped` | Intentionally not implementing (older version, limited use) | +| `undecided` | Needs discussion | + +## References + +### Skill Files (this folder) +- `discover-models.mjs` - **Discovery script** (run to find new models) +- `DISCOVERY_CHECKLIST.md` - Step-by-step discovery process +- `IMPLEMENTATION_CHECKLIST.md` - Step-by-step implementation process + +### Provider Specifications +- `specs/providers/README.md` - Overview of provider system +- `specs/providers/architecture.md` - How providers fit into the plugin system +- `specs/providers/patterns/ui-guidelines.md` - **CRITICAL**: Which parameters to expose/hide in UI +- `specs/providers/patterns/text-to-image.md` - T2I implementation pattern +- `specs/providers/patterns/image-to-image.md` - I2I implementation pattern +- `specs/providers/patterns/text-to-video.md` - T2V implementation pattern +- `specs/providers/patterns/image-to-video.md` - I2V implementation pattern + +### EachLabs-Specific +- `specs/providers/eachlabs/providers.md` - Model inventory and status diff --git a/.claude/skills/partner-providers-eachlabs/discover-models.mjs b/.claude/skills/partner-providers-eachlabs/discover-models.mjs new file mode 100644 index 00000000..a8b97ffc --- /dev/null +++ b/.claude/skills/partner-providers-eachlabs/discover-models.mjs @@ -0,0 +1,50 @@ +#!/usr/bin/env node +/** + * EachLabs Model Discovery Script + * + * Fetches models from EachLabs API and outputs a minimal JSON array + * for easy comparison with providers.md by the LLM. + * + * Usage: node discover-models.mjs + * + * Output: JSON array with essential fields only: + * - slug: unique identifier + * - title: display name + * - output_type: image/video/audio/text/array/object/code + */ + +const API_URL = 'https://api.eachlabs.ai/v1/models?limit=500'; + +async function fetchModels() { + const response = await fetch(API_URL); + if (!response.ok) { + throw new Error(`API request failed: ${response.status}`); + } + return response.json(); +} + +async function main() { + const models = await fetchModels(); + + // Extract only essential fields for discovery + const minimal = models.map(({ slug, title, output_type }) => ({ + slug, + title, + output_type + })); + + // Sort by output_type then slug for easier reading + minimal.sort((a, b) => { + if (a.output_type !== b.output_type) { + return a.output_type.localeCompare(b.output_type); + } + return a.slug.localeCompare(b.slug); + }); + + console.log(JSON.stringify(minimal, null, 2)); +} + +main().catch((error) => { + console.error('Error:', error.message); + process.exit(1); +}); diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md new file mode 100644 index 00000000..a7d4255c --- /dev/null +++ b/specs/providers/eachlabs/providers.md @@ -0,0 +1,106 @@ +# EachLabs Provider Inventory + +This document tracks EachLabs AI models for implementation, focusing on image and video generation. + +**Last Updated**: 2025-12-15 + +## API Reference + +- **List Models**: `GET https://api.eachlabs.ai/v1/models?limit=500` +- **Model Details**: `GET https://api.eachlabs.ai/v1/model?slug=` + +## Status Legend + +| Status | Meaning | +|--------|---------| +| implemented | Provider is implemented | +| planned | Implementation planned | +| skipped | Intentionally not implemented | + +## Output Type Legend + +| Output Type | Meaning | +|-------------|---------| +| image | Single image output | +| array | Multiple images output (batch) | +| video | Video output | + +--- + +## Image Generation (Text-to-Image) + +| Slug | Title | Output Type | Status | Notes | +|------|-------|-------------|--------|-------| +| flux-2-pro | Flux 2 Pro | image | planned | Latest Flux, high quality | +| flux-2 | Flux 2 | array | planned | Standard Flux 2 | +| flux-2-flex | Flux 2 \| Flex | image | planned | Prompt expansion enabled | +| gemini-3-pro-image-preview | Gemini 3 \| Pro \| Image Preview | array | planned | Google Gemini image generation | +| bytedance-seedream-v4-5-text-to-image | Bytedance \| Seedream \| v4.5 \| Text to Image | array | planned | Latest Seedream | +| nano-banana-pro | Nano Banana Pro | array | planned | Multi-style generation | +| openai-image-generation | GPT-1 \| Image Generation | image | planned | OpenAI GPT Image | + +## Image Generation (Image-to-Image / Edit) + +| Slug | Title | Output Type | Status | Notes | +|------|-------|-------------|--------|-------| +| flux-2-pro-edit | Flux 2 Pro \| Edit | image | planned | Edit with Flux 2 Pro | +| flux-2-edit | Flux 2 \| Edit | array | planned | Standard Flux 2 edit | +| flux-2-flex-edit | Flux 2 \| Flex \| Edit | image | planned | Flex edit variant | +| gemini-3-pro-image-preview-edit | Gemini 3 Pro \| Image Edit | array | planned | Google Gemini edit | +| bytedance-seedream-v4-5-edit | Bytedance \| Seedream \| v4.5 \| Edit | array | planned | Seedream edit | +| nano-banana-pro-edit | Nano Banana Pro \| Edit | array | planned | Multi-style edit | +| openai-image-edit | GPT-1 \| Image Edit | image | planned | OpenAI GPT Image edit | + +## Video Generation (Text-to-Video) + +| Slug | Title | Output Type | Status | Notes | +|------|-------|-------------|--------|-------| +| kling-v2-6-pro-text-to-video | Kling \| v2.6 \| Pro \| Text to Video | video | planned | Latest Kling, high quality | +| veo3-1-text-to-video | Veo 3.1 \| Text to Video | video | planned | Google Veo 3.1 | + +## Video Generation (Image-to-Video) + +| Slug | Title | Output Type | Status | Notes | +|------|-------|-------------|--------|-------| +| kling-v2-6-pro-image-to-video | Kling \| v2.6 \| Pro \| Image to Video | video | planned | Latest Kling I2V | +| kling-o1-image-to-video | Kling O1 \| Image to Video | video | planned | Kling O1 variant | +| veo3-1-image-to-video | Veo 3.1 \| Image to Video | video | planned | Google Veo 3.1 I2V | + +--- + +## Summary + +### Planned for Implementation + +**Image Generation (T2I):** 7 models +- flux-2-pro, flux-2, flux-2-flex +- gemini-3-pro-image-preview +- bytedance-seedream-v4-5-text-to-image +- nano-banana-pro +- openai-image-generation + +**Image Generation (I2I/Edit):** 7 models +- flux-2-pro-edit, flux-2-edit, flux-2-flex-edit +- gemini-3-pro-image-preview-edit +- bytedance-seedream-v4-5-edit +- nano-banana-pro-edit +- openai-image-edit + +**Video Generation (T2V):** 2 models +- kling-v2-6-pro-text-to-video +- veo3-1-text-to-video + +**Video Generation (I2V):** 3 models +- kling-v2-6-pro-image-to-video +- kling-o1-image-to-video +- veo3-1-image-to-video + +### Statistics + +| Category | Total Planned | +|----------|---------------| +| Image T2I | 7 | +| Image I2I | 7 | +| Video T2V | 2 | +| Video I2V | 3 | +| **Total** | **19** | From 75084eb65f20ef89c224b8813e27cba97e280711 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 16 Dec 2025 10:47:05 +0100 Subject: [PATCH 03/20] feat(eachlabs): implement NanoBananaPro text-to-image and image-to-image providers Adds complete EachLabs NanoBananaPro provider implementation with: - Text-to-image with 10 aspect ratios (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9, 9:21, 2.4:1) and multiple resolutions (1K, 2K, 4K) - Image-to-image transformation with resolution options - Full EachLabs API client with prediction polling - Image URL conversion utilities for blob/local URLs - Documentation and translations --- examples/ai/src/eachlabsProviders.ts | 23 +- .../plugin-ai-image-generation-web/README.md | 64 ++++ .../eachlabs/NanoBananaPro.image2image.json | 50 +++ .../src/eachlabs/NanoBananaPro.image2image.ts | 150 +++++++++ .../eachlabs/NanoBananaPro.text2image.json | 84 ++++++ .../src/eachlabs/NanoBananaPro.text2image.ts | 75 +++++ .../src/eachlabs/createEachLabsClient.ts | 257 +++++++++++++++- .../src/eachlabs/createImageProvider.ts | 284 +++++++++++++++++- .../src/eachlabs/index.ts | 8 +- .../src/eachlabs/types.ts | 3 +- .../src/eachlabs/utils.ts | 57 ++++ .../translations.json | 28 +- specs/providers/eachlabs/providers.md | 32 +- 13 files changed, 1069 insertions(+), 46 deletions(-) create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.image2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.image2image.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.text2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.text2image.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/utils.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index 6c211467..aff60196 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -1,7 +1,4 @@ -// EachLabs provider namespace -// Providers will be added here when EachLabs models are implemented -/* eslint-disable @typescript-eslint/no-unused-vars */ - +import EachLabsImage from '@imgly/plugin-ai-image-generation-web/eachlabs'; import { Middleware } from '@imgly/plugin-ai-generation-web'; export interface EachLabsProviderOptions { @@ -11,10 +8,22 @@ export interface EachLabsProviderOptions { proxyUrl: string; } -export function createEachLabsProviders(_options: EachLabsProviderOptions) { +export function createEachLabsProviders(options: EachLabsProviderOptions) { + const { imageRateLimitMiddleware, errorMiddleware, proxyUrl } = options; + return { - text2image: [], - image2image: [], + text2image: [ + EachLabsImage.NanoBananaPro.Text2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl + }) + ], + image2image: [ + EachLabsImage.NanoBananaPro.Image2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl + }) + ], text2video: [], image2video: [] }; diff --git a/packages/plugin-ai-image-generation-web/README.md b/packages/plugin-ai-image-generation-web/README.md index 974f0305..a9ae995e 100644 --- a/packages/plugin-ai-image-generation-web/README.md +++ b/packages/plugin-ai-image-generation-web/README.md @@ -1073,6 +1073,46 @@ Key features: - Fixed resolutions: 1024×1024, 1536×1024, 1024×1536 - Instruction-based editing for I2I +### EachLabs Providers + +EachLabs provides access to multiple AI models through a unified API. These providers require an EachLabs proxy URL for authentication. + +```typescript +import EachLabsImage from '@imgly/plugin-ai-image-generation-web/eachlabs'; +``` + +#### 22. NanoBananaPro (Text-to-Image) via EachLabs + +Nano Banana Pro multi-style image generation via EachLabs: + +```typescript +text2image: EachLabsImage.NanoBananaPro.Text2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- Multi-style image generation +- 10 aspect ratio options (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9, 9:21, 2.4:1) +- Multiple resolution options (1K, 2K, 4K) +- High-quality output + +#### 23. NanoBananaPro Edit (Image-to-Image) via EachLabs + +Nano Banana Pro image transformation via EachLabs: + +```typescript +image2image: EachLabsImage.NanoBananaPro.Image2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- Multi-style image transformation +- Multiple resolution options (1K, 2K, 4K) +- Supports up to 10 reference images +- Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page + ### Customizing Labels and Translations You can customize all labels and text in the AI image generation interface using the translation system. This allows you to provide better labels for your users in any language. @@ -1634,6 +1674,26 @@ RunwareImage.GptImage1.Text2Image(config: RunwareProviderConfiguration) RunwareImage.GptImage1.Image2Image(config: RunwareProviderConfiguration) ``` +### EachLabs Providers + +All EachLabs providers use the following configuration: + +```typescript +interface EachLabsProviderConfiguration { + proxyUrl: string; // HTTP endpoint URL for the EachLabs proxy + debug?: boolean; // Enable debug logging + middlewares?: any[]; // Optional middleware functions + history?: false | '@imgly/local' | '@imgly/indexedDB' | (string & {}); +} +``` + +#### NanoBananaPro.Text2Image / NanoBananaPro.Image2Image + +```typescript +EachLabsImage.NanoBananaPro.Text2Image(config: EachLabsProviderConfiguration) +EachLabsImage.NanoBananaPro.Image2Image(config: EachLabsProviderConfiguration) +``` + ## UI Integration The plugin automatically registers the following UI components: @@ -1746,6 +1806,8 @@ const myImageProvider = { - Runware NanoBanana2Pro.Image2Image: `ly.img.ai.runware/google/nano-banana-2-pro/image2image` - Runware GptImage1.Text2Image: `ly.img.ai.runware/openai/gpt-image-1` - Runware GptImage1.Image2Image: `ly.img.ai.runware/openai/gpt-image-1/image2image` + - EachLabs NanoBananaPro.Text2Image: `ly.img.ai.eachlabs/nano-banana-pro` + - EachLabs NanoBananaPro.Image2Image: `ly.img.ai.eachlabs/nano-banana-pro/edit` ### Asset History @@ -1779,6 +1841,8 @@ Generated images are automatically stored in asset sources with the following ID - Runware NanoBanana2Pro.Image2Image: `runware/google/nano-banana-2-pro/image2image.history` - Runware GptImage1.Text2Image: `runware/openai/gpt-image-1.history` - Runware GptImage1.Image2Image: `runware/openai/gpt-image-1/image2image.history` +- EachLabs NanoBananaPro.Text2Image: `eachlabs/nano-banana-pro.history` +- EachLabs NanoBananaPro.Image2Image: `eachlabs/nano-banana-pro/edit.history` ### Dock Integration diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.image2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.image2image.json new file mode 100644 index 00000000..05ca8b15 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.image2image.json @@ -0,0 +1,50 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Nano Banana Pro Edit API", + "version": "1.0.0", + "description": "Nano Banana Pro image-to-image transformation via EachLabs" + }, + "components": { + "schemas": { + "NanoBananaProImage2ImageInput": { + "title": "NanoBananaProImage2ImageInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "description": "Image to transform", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image transformation", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "resolution": { + "title": "Resolution", + "type": "string", + "enum": ["1K", "2K", "4K"], + "default": "1K", + "description": "Output resolution quality", + "x-imgly-enum-labels": { + "1K": "1K (Standard)", + "2K": "2K (High)", + "4K": "4K (Ultra)" + } + } + }, + "x-fal-order-properties": ["image_url", "prompt", "resolution"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.image2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.image2image.ts new file mode 100644 index 00000000..ebe3fc84 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.image2image.ts @@ -0,0 +1,150 @@ +import { + type Provider, + type ImageOutput, + CommonProperties, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import NanoBananaProImage2ImageSchema from './NanoBananaPro.image2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for Nano Banana Pro image-to-image + */ +export type NanoBananaProImage2ImageInput = { + prompt: string; + image_url?: string; + image_urls?: string[]; + resolution?: '1K' | '2K' | '4K'; +}; + +/** + * Nano Banana Pro Edit - Transform images using multi-style AI via EachLabs + * + * Features: + * - Multi-style image transformation + * - Multiple resolution options (1K, 2K, 4K) + * - Supports up to 10 reference images + */ +export function NanoBananaProImage2Image( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/nano-banana-pro/edit'; + + // Set translations for the panel + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Edit', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createImageProvider( + { + modelSlug: 'nano-banana-pro-edit', + modelVersion: '0.0.1', + providerId, + name: 'Nano Banana Pro Edit', + // @ts-ignore - JSON schema types are compatible at runtime + schema: NanoBananaProImage2ImageSchema, + inputReference: '#/components/schemas/NanoBananaProImage2ImageInput', + cesdk, + middleware: config.middlewares ?? [], + renderCustomProperty: CommonProperties.ImageUrl(providerId, { + cesdk + }), + supportedQuickActions: { + 'ly.img.editImage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.swapBackground': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.styleTransfer': { + mapInput: (input) => ({ + prompt: input.style, + image_url: input.uri + }) + }, + 'ly.img.artistTransfer': { + mapInput: (input) => ({ + prompt: input.artist, + image_url: input.uri + }) + }, + 'ly.img.createVariant': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPageWithPrompt': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.combineImages': { + mapInput: (input) => ({ + prompt: input.prompt, + image_urls: input.uris + }) + } + }, + getBlockInput: async (input) => { + // Get first image URL from either single or array input + const firstImageUrl = input.image_url ?? input.image_urls?.[0]; + if (firstImageUrl == null) { + throw new Error('No image URL provided'); + } + + // Get dimensions from input image for UI placeholder + const { width, height } = await getImageDimensionsFromURL( + firstImageUrl, + cesdk.engine + ); + return Promise.resolve({ + image: { + width, + height + } + }); + }, + mapInput: (input) => { + // Map to EachLabs API format + // nano-banana-pro-edit uses image_urls (array) for image input + const imageUrls = + input.image_urls ?? (input.image_url ? [input.image_url] : []); + return { + prompt: input.prompt, + image_urls: imageUrls, + resolution: input.resolution ?? '1K' + }; + } + }, + config + ); + }; +} + +export default NanoBananaProImage2Image; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.text2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.text2image.json new file mode 100644 index 00000000..6e296d12 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.text2image.json @@ -0,0 +1,84 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Nano Banana Pro API", + "version": "1.0.0", + "description": "Nano Banana Pro text-to-image generation via EachLabs" + }, + "components": { + "schemas": { + "NanoBananaProInput": { + "title": "NanoBananaProInput", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image generation", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "aspect_ratio": { + "title": "Format", + "type": "string", + "enum": [ + "1:1", + "16:9", + "9:16", + "4:3", + "3:4", + "3:2", + "2:3", + "21:9", + "9:21", + "2.4:1" + ], + "default": "1:1", + "description": "Aspect ratio for the generated image", + "x-imgly-enum-labels": { + "1:1": "Square (1:1)", + "16:9": "Widescreen (16:9)", + "9:16": "Mobile (9:16)", + "4:3": "Landscape (4:3)", + "3:4": "Portrait (3:4)", + "3:2": "Photo Landscape (3:2)", + "2:3": "Photo Portrait (2:3)", + "21:9": "Ultra-Wide (21:9)", + "9:21": "Ultra-Tall (9:21)", + "2.4:1": "Cinematic (2.4:1)" + }, + "x-imgly-enum-icons": { + "1:1": "@imgly/plugin/formats/ratio1by1", + "16:9": "@imgly/plugin/formats/ratio16by9", + "9:16": "@imgly/plugin/formats/ratio9by16", + "4:3": "@imgly/plugin/formats/ratio4by3", + "3:4": "@imgly/plugin/formats/ratio3by4", + "3:2": "@imgly/plugin/formats/ratio4by3", + "2:3": "@imgly/plugin/formats/ratio3by4", + "21:9": "@imgly/plugin/formats/ratio16by9", + "9:21": "@imgly/plugin/formats/ratio9by16", + "2.4:1": "@imgly/plugin/formats/ratio16by9" + } + }, + "resolution": { + "title": "Resolution", + "type": "string", + "enum": ["1K", "2K", "4K"], + "default": "1K", + "description": "Output resolution quality", + "x-imgly-enum-labels": { + "1K": "1K (Standard)", + "2K": "2K (High)", + "4K": "4K (Ultra)" + } + } + }, + "x-fal-order-properties": ["prompt", "aspect_ratio", "resolution"], + "required": ["prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.text2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.text2image.ts new file mode 100644 index 00000000..bbbb4d4f --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/NanoBananaPro.text2image.ts @@ -0,0 +1,75 @@ +import { + type Provider, + type ImageOutput, + addIconSetOnce +} from '@imgly/plugin-ai-generation-web'; +import { Icons } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import NanoBananaProSchema from './NanoBananaPro.text2image.json'; +import createImageProvider from './createImageProvider'; +import { + EachLabsProviderConfiguration, + getImageDimensionsFromAspectRatio +} from './types'; + +/** + * Input interface for Nano Banana Pro text-to-image + */ +export type NanoBananaProInput = { + prompt: string; + aspect_ratio?: + | '1:1' + | '16:9' + | '9:16' + | '4:3' + | '3:4' + | '3:2' + | '2:3' + | '21:9' + | '9:21' + | '2.4:1'; + resolution?: '1K' | '2K' | '4K'; +}; + +/** + * Nano Banana Pro - Multi-style AI image generation model via EachLabs + * + * Features: + * - 10 aspect ratio options + * - Multiple resolution options (1K, 2K, 4K) + * - High-quality multi-style generation + */ +export function NanoBananaPro( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + // Add aspect ratio icons + addIconSetOnce(cesdk, '@imgly/plugin/formats', Icons.Formats); + + return createImageProvider( + { + modelSlug: 'nano-banana-pro', + modelVersion: '0.0.1', + providerId: 'eachlabs/nano-banana-pro', + name: 'Nano Banana Pro', + // @ts-ignore - JSON schema types are compatible at runtime + schema: NanoBananaProSchema, + inputReference: '#/components/schemas/NanoBananaProInput', + cesdk, + getImageSize: (input) => + getImageDimensionsFromAspectRatio(input.aspect_ratio ?? '1:1'), + mapInput: (input) => ({ + prompt: input.prompt, + aspect_ratio: input.aspect_ratio ?? '1:1', + resolution: input.resolution ?? '1K' + }) + }, + config + ); + }; +} + +export default NanoBananaPro; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts index 61112eed..f61c50f1 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts @@ -1,20 +1,58 @@ -// EachLabs API client -// TODO: Implement actual API integration -/* eslint-disable @typescript-eslint/no-unused-vars */ +// EachLabs API client for image generation +/** + * Parameters for EachLabs image inference + */ export interface EachLabsImageInferenceParams { - // TODO: Define parameters based on EachLabs API + /** Model slug (e.g., 'nano-banana-pro') */ model: string; - prompt: string; - width?: number; - height?: number; + /** Model version */ + version: string; + /** Input parameters for the model */ + input: Record; } +/** + * EachLabs prediction status + */ +type PredictionStatus = + | 'success' + | 'processing' + | 'starting' + | 'failed' + | 'cancelled'; + +/** + * EachLabs prediction response (from GET /v1/prediction/{id}) + * Output can be: string (single URL), array (multiple URLs), or object + */ +interface PredictionResponse { + id: string; + status: PredictionStatus; + input?: Record; + output?: string | string[] | Record; + error?: string; + logs?: string | null; + metrics?: { + predict_time?: number; + cost?: number; + }; + urls?: { + cancel?: string; + get?: string; + }; +} + +/** + * Result from EachLabs image inference + */ export interface EachLabsImageResult { - // TODO: Define result structure based on EachLabs API imageURL: string; } +/** + * EachLabs API client interface + */ export interface EachLabsClient { imageInference: ( params: EachLabsImageInferenceParams, @@ -22,14 +60,207 @@ export interface EachLabsClient { ) => Promise; } +/** + * Poll interval in milliseconds + */ +const POLL_INTERVAL = 2000; + +/** + * Maximum poll attempts (5 minutes total with 2s interval) + */ +const MAX_POLL_ATTEMPTS = 150; + +/** + * Creates an EachLabs API client + * + * @param proxyUrl - The proxy URL to use for API requests + * @param headers - Optional additional headers + * @returns EachLabs client instance + */ export function createEachLabsClient( - _proxyUrl: string, - _headers?: Record + proxyUrl: string, + headers?: Record ): EachLabsClient { + const baseHeaders = { + 'Content-Type': 'application/json', + ...headers + }; + + /** + * Create a prediction + */ + async function createPrediction( + params: EachLabsImageInferenceParams, + abortSignal?: AbortSignal + ): Promise { + const response = await fetch(`${proxyUrl}/v1/prediction`, { + method: 'POST', + headers: baseHeaders, + body: JSON.stringify({ + model: params.model, + version: params.version, + input: params.input + }), + signal: abortSignal + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + throw new Error(`EachLabs API error: ${response.status} - ${errorText}`); + } + + const data = (await response.json()) as { predictionID?: string }; + if (!data.predictionID) { + throw new Error('EachLabs API did not return a prediction ID'); + } + + return data.predictionID; + } + + /** + * Get prediction status + */ + async function getPrediction( + predictionId: string, + abortSignal?: AbortSignal + ): Promise { + const response = await fetch(`${proxyUrl}/v1/prediction/${predictionId}`, { + method: 'GET', + headers: baseHeaders, + signal: abortSignal + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + throw new Error(`EachLabs API error: ${response.status} - ${errorText}`); + } + + return response.json() as Promise; + } + + /** + * Poll for prediction completion + */ + async function pollPrediction( + predictionId: string, + abortSignal?: AbortSignal + ): Promise { + let attempts = 0; + + // eslint-disable-next-line no-await-in-loop -- Polling requires sequential awaits + while (attempts < MAX_POLL_ATTEMPTS) { + if (abortSignal?.aborted) { + throw new Error('Request aborted'); + } + + // eslint-disable-next-line no-await-in-loop -- Polling requires sequential awaits + const prediction = await getPrediction(predictionId, abortSignal); + + if (prediction.status === 'success') { + return prediction; + } + + if (prediction.status === 'failed') { + throw new Error(prediction.error ?? 'Prediction failed'); + } + + if (prediction.status === 'cancelled') { + throw new Error('Prediction was cancelled'); + } + + // Wait before polling again + // eslint-disable-next-line no-await-in-loop -- Polling requires sequential awaits + await new Promise((resolve) => { + setTimeout(resolve, POLL_INTERVAL); + }); + attempts++; + } + + throw new Error('Prediction timed out'); + } + + /** + * Extract image URLs from prediction output + * Output can be: string (single URL), array (multiple URLs), or object with image keys + */ + function extractImageUrls(output: PredictionResponse['output']): string[] { + if (!output) { + return []; + } + + // Output is a single URL string + if (typeof output === 'string') { + if (output.startsWith('http')) { + return [output]; + } + return []; + } + + // Output is an array of URLs + if (Array.isArray(output)) { + return output.filter( + (item): item is string => + typeof item === 'string' && item.startsWith('http') + ); + } + + // Output is an object - check common keys + const obj = output as Record; + + // Try images array first + if (Array.isArray(obj.images) && obj.images.length > 0) { + return obj.images.filter( + (item): item is string => + typeof item === 'string' && item.startsWith('http') + ); + } + + // Try single image + if (typeof obj.image === 'string' && obj.image.startsWith('http')) { + return [obj.image]; + } + + // Check for other common output formats + const possibleKeys = ['url', 'image_url', 'result', 'output']; + for (const key of possibleKeys) { + const value = obj[key]; + if (typeof value === 'string' && value.startsWith('http')) { + return [value]; + } + if (Array.isArray(value)) { + const urls = value.filter( + (v): v is string => typeof v === 'string' && v.startsWith('http') + ); + if (urls.length > 0) { + return urls; + } + } + } + + return []; + } + return { - imageInference: async (): Promise => { - // TODO: Implement actual API call - throw new Error('EachLabs client not yet implemented'); + imageInference: async ( + params: EachLabsImageInferenceParams, + abortSignal?: AbortSignal + ): Promise => { + // Create prediction + const predictionId = await createPrediction(params, abortSignal); + + // Poll for completion + const prediction = await pollPrediction(predictionId, abortSignal); + + // Extract image URLs + const imageUrls = extractImageUrls(prediction.output); + + if (imageUrls.length === 0) { + // eslint-disable-next-line no-console + console.error('EachLabs response:', prediction); + throw new Error('No images found in EachLabs response'); + } + + return imageUrls.map((url) => ({ imageURL: url })); } }; } diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts index ba7c49cc..431763f8 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts @@ -1,26 +1,290 @@ -// EachLabs image provider factory -// TODO: Implement actual provider creation +import { type OpenAPIV3 } from 'openapi-types'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +import type { CreativeEngine } from '@cesdk/cesdk-js'; +import { + ImageOutput, + RenderCustomProperty, + GetBlockInput, + CommonProperties, + Provider, + Middleware, + mergeQuickActionsConfig +} from '@imgly/plugin-ai-generation-web'; +import { createEachLabsClient, EachLabsClient } from './createEachLabsClient'; +import { + convertImageUrlForEachLabs, + convertImageUrlArrayForEachLabs +} from './utils'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import { getImageDimensionsFromAspectRatio } from './types'; +import { ImageQuickActionSupportMap } from '../types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -type Middleware = (input: any, output: any) => any; +type MiddlewareType = Middleware; +/** + * Configuration for EachLabs image providers. + */ export interface EachLabsProviderConfiguration { /** - * The URL of the proxy server to use for API requests + * HTTP endpoint URL for the EachLabs proxy. The proxy handles API key injection. */ proxyUrl: string; /** - * Enable debug mode for logging + * Enable debug logging for provider creation and API calls. */ debug?: boolean; /** - * Optional middlewares to apply to the provider + * Middleware functions to process inputs/outputs. */ - middlewares?: Middleware[]; + middlewares?: MiddlewareType[]; /** - * History configuration + * Override provider's default history asset source. */ - history?: false | '@imgly/local' | '@imgly/indexedDB'; + history?: false | '@imgly/local' | '@imgly/indexedDB' | (string & {}); + /** + * Configure supported quick actions. + */ + supportedQuickActions?: { + [quickActionId: string]: + | Partial[string]> + | false + | null; + }; +} + +/** + * Options for creating an EachLabs image provider. + */ +interface CreateProviderOptions> { + /** + * EachLabs model slug (e.g., 'nano-banana-pro'). + */ + modelSlug: string; + /** + * EachLabs model version (e.g., '0.0.1'). + */ + modelVersion: string; + /** + * Unique provider identifier for registration. + */ + providerId: string; + /** + * Human-readable provider name displayed in the UI. + */ + name: string; + /** + * OpenAPI schema document describing the input parameters. + */ + schema: OpenAPIV3.Document; + /** + * JSON reference to the input schema (e.g., '#/components/schemas/Input'). + */ + inputReference: string; + /** + * User flow mode for the provider panel. + */ + useFlow?: 'placeholder' | 'generation-only'; + /** + * Initialization callback when the provider is registered. + */ + initialize?: (context: { + cesdk?: CreativeEditorSDK; + engine: CreativeEngine; + }) => void; + /** + * Custom property renderers for the input panel. + */ + renderCustomProperty?: RenderCustomProperty; + /** + * Quick actions this provider supports. + */ + supportedQuickActions?: ImageQuickActionSupportMap; + /** + * Get block dimensions from input parameters. + */ + getBlockInput?: GetBlockInput<'image', I>; + /** + * Extract image dimensions from input parameters. + */ + getImageSize?: (input: I) => { width: number; height: number }; + /** + * Transform input parameters to EachLabs API format. + */ + mapInput: (input: I) => Record; + /** + * Provider-specific middleware functions. + */ + middleware?: MiddlewareType[]; + /** + * Custom headers to include in API requests. + */ + headers?: Record; + /** + * CE.SDK instance for image URL conversion. + */ + cesdk?: CreativeEditorSDK; +} + +/** + * Creates an EachLabs image provider from schema. + */ +function createImageProvider< + I extends Record & { image_url?: string; image_urls?: string[] } +>( + options: CreateProviderOptions, + config: EachLabsProviderConfiguration +): Provider<'image', I, ImageOutput> { + const middleware = options.middleware ?? config.middlewares ?? []; + + let eachLabsClient: EachLabsClient | null = null; + + const provider: Provider<'image', I, ImageOutput> = { + id: options.providerId, + kind: 'image', + name: options.name, + configuration: config, + initialize: async (context) => { + eachLabsClient = createEachLabsClient(config.proxyUrl, options.headers); + options.initialize?.(context); + }, + input: { + quickActions: { + supported: mergeQuickActionsConfig( + options.supportedQuickActions ?? {}, + config.supportedQuickActions + ) + }, + panel: { + type: 'schema', + document: options.schema, + inputReference: options.inputReference, + includeHistoryLibrary: true, + orderExtensionKeyword: 'x-fal-order-properties', + renderCustomProperty: { + ...(options.cesdk != null + ? CommonProperties.ImageUrl(options.providerId, { + cesdk: options.cesdk + }) + : {}), + ...options.renderCustomProperty + }, + getBlockInput: (input) => { + if (options.getBlockInput != null) { + return options.getBlockInput(input); + } + if (options.getImageSize != null) { + const { width, height } = options.getImageSize(input); + return Promise.resolve({ + image: { width, height } + }); + } + // Try to extract from aspect_ratio + if (input.aspect_ratio != null) { + const dims = getImageDimensionsFromAspectRatio(input.aspect_ratio); + return Promise.resolve({ image: dims }); + } + // Default + return Promise.resolve({ image: { width: 1024, height: 1024 } }); + }, + userFlow: options.useFlow ?? 'placeholder' + } + }, + output: { + abortable: true, + middleware, + history: config.history ?? '@imgly/indexedDB', + generate: async ( + input: I, + { abortSignal }: { abortSignal?: AbortSignal } + ) => { + if (!eachLabsClient) { + throw new Error('Provider not initialized'); + } + + // Convert image URL if needed for image-to-image + let processedInput = input; + let imageDimensions: { width: number; height: number } | undefined; + + if (input.image_url != null) { + const convertedUrl = await convertImageUrlForEachLabs( + input.image_url, + options.cesdk + ); + processedInput = { ...input, image_url: convertedUrl }; + + // Get dimensions from input image for image-to-image + if (options.cesdk != null) { + const { width, height } = await getImageDimensionsFromURL( + input.image_url, + options.cesdk.engine + ); + imageDimensions = { width, height }; + } + } + + // Convert image URLs array if needed for multi-image inputs + if (input.image_urls != null && input.image_urls.length > 0) { + const convertedUrls = await convertImageUrlArrayForEachLabs( + input.image_urls, + options.cesdk + ); + processedInput = { ...processedInput, image_urls: convertedUrls }; + + // For multi-image, get dimensions from the first image if not already set + if ( + imageDimensions == null && + options.cesdk != null && + input.image_urls[0] != null + ) { + const { width, height } = await getImageDimensionsFromURL( + input.image_urls[0], + options.cesdk.engine + ); + imageDimensions = { width, height }; + } + } + + // Map input to EachLabs format + const eachLabsInput = options.mapInput(processedInput); + + // Call EachLabs imageInference via HTTP REST API + const images = await eachLabsClient.imageInference( + { + model: options.modelSlug, + version: options.modelVersion, + input: { + ...eachLabsInput, + // Always generate one image + num_images: 1, + // Use PNG output format + output_format: 'png' + } + }, + abortSignal + ); + + if (images != null && Array.isArray(images) && images.length > 0) { + const image = images[0]; + const url = image?.imageURL; + if (url != null) { + return { kind: 'image', url }; + } + } + + // eslint-disable-next-line no-console + console.error('Cannot extract generated image from response:', images); + throw new Error('Cannot find generated image'); + } + } + }; + + if (config.debug) { + // eslint-disable-next-line no-console + console.log('Created EachLabs Provider:', provider); + } + + return provider; } -// Provider factory will be implemented here when models are added +export default createImageProvider; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts index 3b93c7d0..709dba68 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts @@ -1,8 +1,14 @@ // EachLabs provider namespace // Providers are added here via the partner-providers-eachlabs skill +import { NanoBananaPro as NanoBananaProText2Image } from './NanoBananaPro.text2image'; +import { NanoBananaProImage2Image } from './NanoBananaPro.image2image'; + const EachLabs = { - // Models will be added here + NanoBananaPro: { + Text2Image: NanoBananaProText2Image, + Image2Image: NanoBananaProImage2Image + } }; export default EachLabs; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/types.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/types.ts index 7246f030..7423be94 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/types.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/types.ts @@ -17,7 +17,8 @@ export const ASPECT_RATIO_MAP: Record< '3:2': { width: 1152, height: 768 }, '2:3': { width: 768, height: 1152 }, '21:9': { width: 1536, height: 640 }, - '9:21': { width: 640, height: 1536 } + '9:21': { width: 640, height: 1536 }, + '2.4:1': { width: 2400, height: 1000 } }; export function getImageDimensionsFromAspectRatio(aspectRatio: string): { diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/utils.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/utils.ts new file mode 100644 index 00000000..2e8ebba2 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/utils.ts @@ -0,0 +1,57 @@ +import type CreativeEditorSDK from '@cesdk/cesdk-js'; + +/** + * Converts a blob: or buffer: URL to a data URI that EachLabs can accept + */ +export async function convertImageUrlForEachLabs( + imageUrl?: string, + cesdk?: CreativeEditorSDK +): Promise { + if (imageUrl == null) return undefined; + + // For blob URLs, convert to data URI + if (imageUrl.startsWith('blob:')) { + const response = await fetch(imageUrl); + const blob = await response.blob(); + return blobToDataUri(blob); + } + + // For buffer URLs, convert to data URI + if (cesdk != null && imageUrl.startsWith('buffer:')) { + const mimeType = await cesdk.engine.editor.getMimeType(imageUrl); + const length = cesdk.engine.editor.getBufferLength(imageUrl); + const data = cesdk.engine.editor.getBufferData(imageUrl, 0, length); + const buffer = new Uint8Array(data); + const blob = new Blob([buffer], { type: mimeType }); + return blobToDataUri(blob); + } + + // For data URIs and regular URLs, pass through + return imageUrl; +} + +function blobToDataUri(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} + +/** + * Converts an array of blob:/buffer: URLs to data URIs for EachLabs. + * Used for multi-image inputs like image-to-image generation. + */ +export async function convertImageUrlArrayForEachLabs( + imageUrls?: string[], + cesdk?: CreativeEditorSDK +): Promise { + if (imageUrls == null || !Array.isArray(imageUrls)) return undefined; + + const convertedUrls = await Promise.all( + imageUrls.map((url) => convertImageUrlForEachLabs(url, cesdk)) + ); + + return convertedUrls.filter((url): url is string => url != null); +} diff --git a/packages/plugin-ai-image-generation-web/translations.json b/packages/plugin-ai-image-generation-web/translations.json index a4d45f2d..998c377e 100644 --- a/packages/plugin-ai-image-generation-web/translations.json +++ b/packages/plugin-ai-image-generation-web/translations.json @@ -417,6 +417,32 @@ "ly.img.plugin-ai-image-generation-web.runware/openai/gpt-image-1-mini/image2image.property.format": "Format", "ly.img.plugin-ai-image-generation-web.runware/openai/gpt-image-1-mini/image2image.property.format.1024x1024": "Square (1024×1024)", "ly.img.plugin-ai-image-generation-web.runware/openai/gpt-image-1-mini/image2image.property.format.1536x1024": "Landscape (1536×1024)", - "ly.img.plugin-ai-image-generation-web.runware/openai/gpt-image-1-mini/image2image.property.format.1024x1536": "Portrait (1024×1536)" + "ly.img.plugin-ai-image-generation-web.runware/openai/gpt-image-1-mini/image2image.property.format.1024x1536": "Portrait (1024×1536)", + + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.prompt.placeholder": "Describe your image...", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio": "Format", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.1:1": "Square (1:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.16:9": "Widescreen (16:9)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.9:16": "Mobile (9:16)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.4:3": "Landscape (4:3)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.3:4": "Portrait (3:4)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.3:2": "Photo Landscape (3:2)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.2:3": "Photo Portrait (2:3)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.21:9": "Ultra-Wide (21:9)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.9:21": "Ultra-Tall (9:21)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.aspect_ratio.2.4:1": "Cinematic (2.4:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.resolution": "Resolution", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.resolution.1K": "1K (Standard)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.resolution.2K": "2K (High)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro.property.resolution.4K": "4K (Ultra)", + + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.image_url": "Source Image", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.prompt.placeholder": "Describe the changes...", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.resolution": "Resolution", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.resolution.1K": "1K (Standard)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.resolution.2K": "2K (High)", + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.resolution.4K": "4K (Ultra)" } } \ No newline at end of file diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index a7d4255c..5f5b45ba 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -36,7 +36,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | flux-2-flex | Flux 2 \| Flex | image | planned | Prompt expansion enabled | | gemini-3-pro-image-preview | Gemini 3 \| Pro \| Image Preview | array | planned | Google Gemini image generation | | bytedance-seedream-v4-5-text-to-image | Bytedance \| Seedream \| v4.5 \| Text to Image | array | planned | Latest Seedream | -| nano-banana-pro | Nano Banana Pro | array | planned | Multi-style generation | +| nano-banana-pro | Nano Banana Pro | array | implemented | Multi-style generation | | openai-image-generation | GPT-1 \| Image Generation | image | planned | OpenAI GPT Image | ## Image Generation (Image-to-Image / Edit) @@ -48,7 +48,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | flux-2-flex-edit | Flux 2 \| Flex \| Edit | image | planned | Flex edit variant | | gemini-3-pro-image-preview-edit | Gemini 3 Pro \| Image Edit | array | planned | Google Gemini edit | | bytedance-seedream-v4-5-edit | Bytedance \| Seedream \| v4.5 \| Edit | array | planned | Seedream edit | -| nano-banana-pro-edit | Nano Banana Pro \| Edit | array | planned | Multi-style edit | +| nano-banana-pro-edit | Nano Banana Pro \| Edit | array | implemented | Multi-style edit | | openai-image-edit | GPT-1 \| Image Edit | image | planned | OpenAI GPT Image edit | ## Video Generation (Text-to-Video) @@ -70,20 +70,26 @@ This document tracks EachLabs AI models for implementation, focusing on image an ## Summary +### Implemented + +**Image Generation (T2I):** 1 model +- nano-banana-pro + +**Image Generation (I2I/Edit):** 1 model +- nano-banana-pro-edit + ### Planned for Implementation -**Image Generation (T2I):** 7 models +**Image Generation (T2I):** 6 models - flux-2-pro, flux-2, flux-2-flex - gemini-3-pro-image-preview - bytedance-seedream-v4-5-text-to-image -- nano-banana-pro - openai-image-generation -**Image Generation (I2I/Edit):** 7 models +**Image Generation (I2I/Edit):** 6 models - flux-2-pro-edit, flux-2-edit, flux-2-flex-edit - gemini-3-pro-image-preview-edit - bytedance-seedream-v4-5-edit -- nano-banana-pro-edit - openai-image-edit **Video Generation (T2V):** 2 models @@ -97,10 +103,10 @@ This document tracks EachLabs AI models for implementation, focusing on image an ### Statistics -| Category | Total Planned | -|----------|---------------| -| Image T2I | 7 | -| Image I2I | 7 | -| Video T2V | 2 | -| Video I2V | 3 | -| **Total** | **19** | +| Category | Implemented | Planned | Total | +|----------|-------------|---------|-------| +| Image T2I | 1 | 6 | 7 | +| Image I2I | 1 | 6 | 7 | +| Video T2V | 0 | 2 | 2 | +| Video I2V | 0 | 3 | 3 | +| **Total** | **2** | **17** | **19** | From eef484a67e87802eafd07a73a406c12364430c64 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 16 Dec 2025 10:53:28 +0100 Subject: [PATCH 04/20] fix(fal-ai): add missing format icons to NanoBananaPro text-to-image provider Maps aspect ratio values to closest available icons for UI display. --- .../src/fal-ai/NanoBananaPro.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/plugin-ai-image-generation-web/src/fal-ai/NanoBananaPro.json b/packages/plugin-ai-image-generation-web/src/fal-ai/NanoBananaPro.json index fcfaf00c..d259ab31 100644 --- a/packages/plugin-ai-image-generation-web/src/fal-ai/NanoBananaPro.json +++ b/packages/plugin-ai-image-generation-web/src/fal-ai/NanoBananaPro.json @@ -75,6 +75,18 @@ "21:9": "Ultra-Wide (21:9)", "9:21": "Ultra-Tall (9:21)", "2.4:1": "Cinematic (2.4:1)" + }, + "x-imgly-enum-icons": { + "1:1": "@imgly/plugin/formats/ratio1by1", + "3:2": "@imgly/plugin/formats/ratio4by3", + "2:3": "@imgly/plugin/formats/ratio3by4", + "4:3": "@imgly/plugin/formats/ratio4by3", + "3:4": "@imgly/plugin/formats/ratio3by4", + "16:9": "@imgly/plugin/formats/ratio16by9", + "9:16": "@imgly/plugin/formats/ratio9by16", + "21:9": "@imgly/plugin/formats/ratio16by9", + "9:21": "@imgly/plugin/formats/ratio9by16", + "2.4:1": "@imgly/plugin/formats/ratio16by9" } }, "resolution": { From 6b01fb8b3b05d676e2bffc7aec592f75b1603263 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 16 Dec 2025 11:32:08 +0100 Subject: [PATCH 05/20] feat(eachlabs): implement Flux 2 Pro text-to-image and image-to-image providers Add Flux 2 Pro provider support via EachLabs API with both T2I and I2I capabilities. The I2I provider supports up to 4 reference images and includes quick action mappings for common editing workflows. --- examples/ai/src/eachlabsProviders.ts | 8 + .../src/eachlabs/Flux2Pro.image2image.json | 38 +++++ .../src/eachlabs/Flux2Pro.image2image.ts | 147 ++++++++++++++++++ .../src/eachlabs/Flux2Pro.text2image.json | 60 +++++++ .../src/eachlabs/Flux2Pro.text2image.ts | 78 ++++++++++ .../src/eachlabs/index.ts | 6 + .../translations.json | 16 +- specs/providers/eachlabs/providers.md | 24 +-- 8 files changed, 365 insertions(+), 12 deletions(-) create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.image2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.image2image.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.text2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.text2image.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index aff60196..28d6ad12 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -16,12 +16,20 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { EachLabsImage.NanoBananaPro.Text2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsImage.Flux2Pro.Text2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl }) ], image2image: [ EachLabsImage.NanoBananaPro.Image2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsImage.Flux2Pro.Image2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl }) ], text2video: [], diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.image2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.image2image.json new file mode 100644 index 00000000..4d00b768 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.image2image.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Flux 2 Pro Edit API", + "version": "1.0.0", + "description": "Flux 2 Pro image-to-image editing via EachLabs" + }, + "components": { + "schemas": { + "Flux2ProImage2ImageInput": { + "title": "Flux2ProImage2ImageInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "description": "Image to transform", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image editing", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + } + }, + "x-fal-order-properties": ["image_url", "prompt"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.image2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.image2image.ts new file mode 100644 index 00000000..257ed554 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.image2image.ts @@ -0,0 +1,147 @@ +import { + type Provider, + type ImageOutput, + CommonProperties, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Flux2ProImage2ImageSchema from './Flux2Pro.image2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for Flux 2 Pro image-to-image + */ +export type Flux2ProImage2ImageInput = { + prompt: string; + image_url?: string; + image_urls?: string[]; +}; + +/** + * Flux 2 Pro Edit - Transform images using AI via EachLabs + * + * Features: + * - High-quality image transformation + * - Supports up to 4 reference images + */ +export function Flux2ProImage2Image( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/flux-2-pro/edit'; + + // Set translations for the panel + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Edit', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createImageProvider( + { + modelSlug: 'flux-2-pro-edit', + modelVersion: '0.0.1', + providerId, + name: 'Flux 2 Pro Edit', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Flux2ProImage2ImageSchema, + inputReference: '#/components/schemas/Flux2ProImage2ImageInput', + cesdk, + middleware: config.middlewares ?? [], + renderCustomProperty: CommonProperties.ImageUrl(providerId, { + cesdk + }), + supportedQuickActions: { + 'ly.img.editImage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.swapBackground': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.styleTransfer': { + mapInput: (input) => ({ + prompt: input.style, + image_url: input.uri + }) + }, + 'ly.img.artistTransfer': { + mapInput: (input) => ({ + prompt: input.artist, + image_url: input.uri + }) + }, + 'ly.img.createVariant': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPageWithPrompt': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.combineImages': { + mapInput: (input) => ({ + prompt: input.prompt, + image_urls: input.uris + }) + } + }, + getBlockInput: async (input) => { + // Get first image URL from either single or array input + const firstImageUrl = input.image_url ?? input.image_urls?.[0]; + if (firstImageUrl == null) { + throw new Error('No image URL provided'); + } + + // Get dimensions from input image for UI placeholder + const { width, height } = await getImageDimensionsFromURL( + firstImageUrl, + cesdk.engine + ); + return Promise.resolve({ + image: { + width, + height + } + }); + }, + mapInput: (input) => { + // Map to EachLabs API format + // flux-2-pro-edit uses image_urls (array) for image input + const imageUrls = + input.image_urls ?? (input.image_url ? [input.image_url] : []); + return { + prompt: input.prompt, + image_urls: imageUrls + }; + } + }, + config + ); + }; +} + +export default Flux2ProImage2Image; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.text2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.text2image.json new file mode 100644 index 00000000..9b94973a --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.text2image.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Flux 2 Pro API", + "version": "1.0.0", + "description": "Flux 2 Pro text-to-image generation via EachLabs" + }, + "components": { + "schemas": { + "Flux2ProInput": { + "title": "Flux2ProInput", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image generation", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "image_size": { + "title": "Format", + "type": "string", + "enum": [ + "square_hd", + "square", + "portrait_4_3", + "portrait_16_9", + "landscape_4_3", + "landscape_16_9" + ], + "default": "landscape_4_3", + "description": "Output image format", + "x-imgly-enum-labels": { + "square_hd": "Square HD (1:1)", + "square": "Square (1:1)", + "portrait_4_3": "Portrait (3:4)", + "portrait_16_9": "Portrait (9:16)", + "landscape_4_3": "Landscape (4:3)", + "landscape_16_9": "Landscape (16:9)" + }, + "x-imgly-enum-icons": { + "square_hd": "@imgly/plugin/formats/ratio1by1", + "square": "@imgly/plugin/formats/ratio1by1", + "portrait_4_3": "@imgly/plugin/formats/ratio3by4", + "portrait_16_9": "@imgly/plugin/formats/ratio9by16", + "landscape_4_3": "@imgly/plugin/formats/ratio4by3", + "landscape_16_9": "@imgly/plugin/formats/ratio16by9" + } + } + }, + "x-fal-order-properties": ["prompt", "image_size"], + "required": ["prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.text2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.text2image.ts new file mode 100644 index 00000000..830492a1 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Pro.text2image.ts @@ -0,0 +1,78 @@ +import { + type Provider, + type ImageOutput, + addIconSetOnce +} from '@imgly/plugin-ai-generation-web'; +import { Icons } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Flux2ProSchema from './Flux2Pro.text2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +// Map EachLabs image_size enum to dimensions +const IMAGE_SIZE_MAP: Record = { + square_hd: { width: 1024, height: 1024 }, + square: { width: 512, height: 512 }, + portrait_4_3: { width: 896, height: 1152 }, + portrait_16_9: { width: 768, height: 1344 }, + landscape_4_3: { width: 1152, height: 896 }, + landscape_16_9: { width: 1344, height: 768 } +}; + +/** + * Input interface for Flux 2 Pro text-to-image + */ +export type Flux2ProInput = { + prompt: string; + image_size?: + | 'square_hd' + | 'square' + | 'portrait_4_3' + | 'portrait_16_9' + | 'landscape_4_3' + | 'landscape_16_9'; +}; + +/** + * Flux 2 Pro - High-quality text-to-image generation via EachLabs + * + * Features: + * - 6 image size options + * - High-quality generation from Black Forest Labs + */ +export function Flux2Pro( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + // Add aspect ratio icons + addIconSetOnce(cesdk, '@imgly/plugin/formats', Icons.Formats); + + return createImageProvider( + { + modelSlug: 'flux-2-pro', + modelVersion: '0.0.1', + providerId: 'eachlabs/flux-2-pro', + name: 'Flux 2 Pro', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Flux2ProSchema, + inputReference: '#/components/schemas/Flux2ProInput', + cesdk, + getImageSize: (input) => + IMAGE_SIZE_MAP[input.image_size ?? 'landscape_4_3'] ?? { + width: 1152, + height: 896 + }, + mapInput: (input) => ({ + prompt: input.prompt, + image_size: input.image_size ?? 'landscape_4_3' + }) + }, + config + ); + }; +} + +export default Flux2Pro; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts index 709dba68..5a525d69 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts @@ -3,11 +3,17 @@ import { NanoBananaPro as NanoBananaProText2Image } from './NanoBananaPro.text2image'; import { NanoBananaProImage2Image } from './NanoBananaPro.image2image'; +import { Flux2Pro as Flux2ProText2Image } from './Flux2Pro.text2image'; +import { Flux2ProImage2Image } from './Flux2Pro.image2image'; const EachLabs = { NanoBananaPro: { Text2Image: NanoBananaProText2Image, Image2Image: NanoBananaProImage2Image + }, + Flux2Pro: { + Text2Image: Flux2ProText2Image, + Image2Image: Flux2ProImage2Image } }; diff --git a/packages/plugin-ai-image-generation-web/translations.json b/packages/plugin-ai-image-generation-web/translations.json index 998c377e..65a73b41 100644 --- a/packages/plugin-ai-image-generation-web/translations.json +++ b/packages/plugin-ai-image-generation-web/translations.json @@ -443,6 +443,20 @@ "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.resolution": "Resolution", "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.resolution.1K": "1K (Standard)", "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.resolution.2K": "2K (High)", - "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.resolution.4K": "4K (Ultra)" + "ly.img.plugin-ai-image-generation-web.eachlabs/nano-banana-pro/edit.property.resolution.4K": "4K (Ultra)", + + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.prompt.placeholder": "Describe your image...", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size": "Format", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size.square_hd": "Square HD (1:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size.square": "Square (1:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size.portrait_4_3": "Portrait (3:4)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size.portrait_16_9": "Portrait (9:16)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size.landscape_4_3": "Landscape (4:3)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro.property.image_size.landscape_16_9": "Landscape (16:9)", + + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro/edit.property.image_url": "Source Image", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro/edit.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro/edit.property.prompt.placeholder": "Describe the changes..." } } \ No newline at end of file diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index 5f5b45ba..8a5d40d4 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -31,7 +31,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Slug | Title | Output Type | Status | Notes | |------|-------|-------------|--------|-------| -| flux-2-pro | Flux 2 Pro | image | planned | Latest Flux, high quality | +| flux-2-pro | Flux 2 Pro | image | implemented | Latest Flux, high quality | | flux-2 | Flux 2 | array | planned | Standard Flux 2 | | flux-2-flex | Flux 2 \| Flex | image | planned | Prompt expansion enabled | | gemini-3-pro-image-preview | Gemini 3 \| Pro \| Image Preview | array | planned | Google Gemini image generation | @@ -43,7 +43,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Slug | Title | Output Type | Status | Notes | |------|-------|-------------|--------|-------| -| flux-2-pro-edit | Flux 2 Pro \| Edit | image | planned | Edit with Flux 2 Pro | +| flux-2-pro-edit | Flux 2 Pro \| Edit | image | implemented | Edit with Flux 2 Pro | | flux-2-edit | Flux 2 \| Edit | array | planned | Standard Flux 2 edit | | flux-2-flex-edit | Flux 2 \| Flex \| Edit | image | planned | Flex edit variant | | gemini-3-pro-image-preview-edit | Gemini 3 Pro \| Image Edit | array | planned | Google Gemini edit | @@ -72,22 +72,24 @@ This document tracks EachLabs AI models for implementation, focusing on image an ### Implemented -**Image Generation (T2I):** 1 model +**Image Generation (T2I):** 2 models - nano-banana-pro +- flux-2-pro -**Image Generation (I2I/Edit):** 1 model +**Image Generation (I2I/Edit):** 2 models - nano-banana-pro-edit +- flux-2-pro-edit ### Planned for Implementation -**Image Generation (T2I):** 6 models -- flux-2-pro, flux-2, flux-2-flex +**Image Generation (T2I):** 5 models +- flux-2, flux-2-flex - gemini-3-pro-image-preview - bytedance-seedream-v4-5-text-to-image - openai-image-generation -**Image Generation (I2I/Edit):** 6 models -- flux-2-pro-edit, flux-2-edit, flux-2-flex-edit +**Image Generation (I2I/Edit):** 5 models +- flux-2-edit, flux-2-flex-edit - gemini-3-pro-image-preview-edit - bytedance-seedream-v4-5-edit - openai-image-edit @@ -105,8 +107,8 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Category | Implemented | Planned | Total | |----------|-------------|---------|-------| -| Image T2I | 1 | 6 | 7 | -| Image I2I | 1 | 6 | 7 | +| Image T2I | 2 | 5 | 7 | +| Image I2I | 2 | 5 | 7 | | Video T2V | 0 | 2 | 2 | | Video I2V | 0 | 3 | 3 | -| **Total** | **2** | **17** | **19** | +| **Total** | **4** | **15** | **19** | From 583f76fc3c70bd4aeb37cb99f961fb7d1b981884 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 16 Dec 2025 14:12:59 +0100 Subject: [PATCH 06/20] docs(eachlabs): add Flux 2 Pro provider documentation Add missing documentation for EachLabs Flux 2 Pro text-to-image and image-to-image providers including provider descriptions, API reference, panel IDs, and asset history IDs. --- .../plugin-ai-image-generation-web/README.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/plugin-ai-image-generation-web/README.md b/packages/plugin-ai-image-generation-web/README.md index a9ae995e..b00757a0 100644 --- a/packages/plugin-ai-image-generation-web/README.md +++ b/packages/plugin-ai-image-generation-web/README.md @@ -1113,6 +1113,36 @@ Key features: - Supports up to 10 reference images - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page +#### 24. Flux2Pro (Text-to-Image) via EachLabs + +Black Forest Labs' Flux 2 Pro high-quality text-to-image generation via EachLabs: + +```typescript +text2image: EachLabsImage.Flux2Pro.Text2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- High-quality image generation from Black Forest Labs +- 6 image size options (square HD, square, portrait 4:3/16:9, landscape 4:3/16:9) +- Professional-grade output quality + +#### 25. Flux2Pro Edit (Image-to-Image) via EachLabs + +Black Forest Labs' Flux 2 Pro image transformation via EachLabs: + +```typescript +image2image: EachLabsImage.Flux2Pro.Image2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- High-quality image transformation +- Supports up to 4 reference images +- Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page + ### Customizing Labels and Translations You can customize all labels and text in the AI image generation interface using the translation system. This allows you to provide better labels for your users in any language. @@ -1694,6 +1724,13 @@ EachLabsImage.NanoBananaPro.Text2Image(config: EachLabsProviderConfiguration) EachLabsImage.NanoBananaPro.Image2Image(config: EachLabsProviderConfiguration) ``` +#### Flux2Pro.Text2Image / Flux2Pro.Image2Image + +```typescript +EachLabsImage.Flux2Pro.Text2Image(config: EachLabsProviderConfiguration) +EachLabsImage.Flux2Pro.Image2Image(config: EachLabsProviderConfiguration) +``` + ## UI Integration The plugin automatically registers the following UI components: @@ -1808,6 +1845,8 @@ const myImageProvider = { - Runware GptImage1.Image2Image: `ly.img.ai.runware/openai/gpt-image-1/image2image` - EachLabs NanoBananaPro.Text2Image: `ly.img.ai.eachlabs/nano-banana-pro` - EachLabs NanoBananaPro.Image2Image: `ly.img.ai.eachlabs/nano-banana-pro/edit` + - EachLabs Flux2Pro.Text2Image: `ly.img.ai.eachlabs/flux-2-pro` + - EachLabs Flux2Pro.Image2Image: `ly.img.ai.eachlabs/flux-2-pro/edit` ### Asset History @@ -1843,6 +1882,8 @@ Generated images are automatically stored in asset sources with the following ID - Runware GptImage1.Image2Image: `runware/openai/gpt-image-1/image2image.history` - EachLabs NanoBananaPro.Text2Image: `eachlabs/nano-banana-pro.history` - EachLabs NanoBananaPro.Image2Image: `eachlabs/nano-banana-pro/edit.history` +- EachLabs Flux2Pro.Text2Image: `eachlabs/flux-2-pro.history` +- EachLabs Flux2Pro.Image2Image: `eachlabs/flux-2-pro/edit.history` ### Dock Integration From f6aadf2830e1179a28a4710092bf4d3b32e5eda2 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 16 Dec 2025 18:04:31 +0100 Subject: [PATCH 07/20] feat(eachlabs): implement OpenAI GPT Image text-to-image and image-to-image providers Add OpenAI GPT Image generation via EachLabs API with: - Text-to-image with 3 format options (Square, Landscape, Portrait) - Image-to-image with full quick actions support (edit, swap background, style transfer, artist transfer, variants, combine images, remix page) - Support for up to 16 reference images in I2I mode --- examples/ai/src/eachlabsProviders.ts | 8 + .../src/eachlabs/OpenAIImage.image2image.json | 38 +++++ .../src/eachlabs/OpenAIImage.image2image.ts | 156 ++++++++++++++++++ .../src/eachlabs/OpenAIImage.text2image.json | 51 ++++++ .../src/eachlabs/OpenAIImage.text2image.ts | 69 ++++++++ .../src/eachlabs/index.ts | 6 + .../translations.json | 13 +- specs/providers/eachlabs/providers.md | 22 +-- 8 files changed, 351 insertions(+), 12 deletions(-) create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.image2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.image2image.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.text2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.text2image.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index 28d6ad12..ebc49751 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -20,6 +20,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { EachLabsImage.Flux2Pro.Text2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsImage.OpenAIGptImage.Text2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl }) ], image2image: [ @@ -30,6 +34,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { EachLabsImage.Flux2Pro.Image2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsImage.OpenAIGptImage.Image2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl }) ], text2video: [], diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.image2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.image2image.json new file mode 100644 index 00000000..36bf0fda --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.image2image.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs OpenAI GPT Image Edit API", + "version": "1.0.0", + "description": "OpenAI GPT Image image-to-image editing via EachLabs" + }, + "components": { + "schemas": { + "OpenAIImageImage2ImageInput": { + "title": "OpenAIImageImage2ImageInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "description": "Image to transform", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image editing", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + } + }, + "x-fal-order-properties": ["image_url", "prompt"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.image2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.image2image.ts new file mode 100644 index 00000000..de83c4d2 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.image2image.ts @@ -0,0 +1,156 @@ +import { + type Provider, + type ImageOutput, + CommonProperties, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import OpenAIImageImage2ImageSchema from './OpenAIImage.image2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for OpenAI GPT Image image-to-image + */ +export type OpenAIImageImage2ImageInput = { + prompt: string; + image_url?: string; + image_urls?: string[]; +}; + +/** + * OpenAI GPT Image Edit - Transform images using AI via EachLabs + * + * Features: + * - High-quality image transformation + * - Supports multiple reference images (up to 16) + */ +export function OpenAIImageImage2Image( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/openai-gpt-image/edit'; + + // Set translations for the panel + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Edit', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createImageProvider( + { + modelSlug: 'openai-image-edit', + modelVersion: '0.0.1', + providerId, + name: 'OpenAI GPT Image Edit', + // @ts-ignore - JSON schema types are compatible at runtime + schema: OpenAIImageImage2ImageSchema, + inputReference: '#/components/schemas/OpenAIImageImage2ImageInput', + cesdk, + middleware: config.middlewares ?? [], + renderCustomProperty: CommonProperties.ImageUrl(providerId, { + cesdk + }), + supportedQuickActions: { + 'ly.img.editImage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.swapBackground': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.styleTransfer': { + mapInput: (input) => ({ + prompt: input.style, + image_url: input.uri + }) + }, + 'ly.img.artistTransfer': { + mapInput: (input) => ({ + prompt: input.artist, + image_url: input.uri + }) + }, + 'ly.img.createVariant': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPageWithPrompt': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.combineImages': { + mapInput: (input) => ({ + prompt: input.prompt, + image_urls: input.uris + }) + } + }, + getBlockInput: async (input) => { + // Get first image URL from either single or array input + const firstImageUrl = input.image_url ?? input.image_urls?.[0]; + if (firstImageUrl == null) { + throw new Error('No image URL provided'); + } + + // Get dimensions from input image for UI placeholder + const { width, height } = await getImageDimensionsFromURL( + firstImageUrl, + cesdk.engine + ); + return Promise.resolve({ + image: { + width, + height + } + }); + }, + mapInput: (input) => { + // Map to EachLabs API format + // openai-image-edit uses image_url_1 for the first image + const imageUrl = input.image_url ?? input.image_urls?.[0]; + const apiInput: Record = { + prompt: input.prompt + }; + + // Map image_urls array to image_url_1, image_url_2, etc. + if (input.image_urls && input.image_urls.length > 0) { + input.image_urls.forEach((url, index) => { + apiInput[`image_url_${index + 1}`] = url; + }); + } else if (imageUrl) { + apiInput.image_url_1 = imageUrl; + } + + return apiInput; + } + }, + config + ); + }; +} + +export default OpenAIImageImage2Image; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.text2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.text2image.json new file mode 100644 index 00000000..c7a40762 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.text2image.json @@ -0,0 +1,51 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs OpenAI GPT Image API", + "version": "1.0.0", + "description": "OpenAI GPT Image text-to-image generation via EachLabs" + }, + "components": { + "schemas": { + "OpenAIImageText2ImageInput": { + "title": "OpenAIImageText2ImageInput", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image generation", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "image_size": { + "title": "Format", + "type": "string", + "enum": [ + "1024x1024", + "1536x1024", + "1024x1536" + ], + "default": "1024x1024", + "description": "Output image format", + "x-imgly-enum-labels": { + "1024x1024": "Square", + "1536x1024": "Landscape", + "1024x1536": "Portrait" + }, + "x-imgly-enum-icons": { + "1024x1024": "@imgly/plugin/formats/ratio1by1", + "1536x1024": "@imgly/plugin/formats/ratio4by3", + "1024x1536": "@imgly/plugin/formats/ratio3by4" + } + } + }, + "x-fal-order-properties": ["prompt", "image_size"], + "required": ["prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.text2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.text2image.ts new file mode 100644 index 00000000..495e5ef6 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/OpenAIImage.text2image.ts @@ -0,0 +1,69 @@ +import { + type Provider, + type ImageOutput, + addIconSetOnce +} from '@imgly/plugin-ai-generation-web'; +import { Icons } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import OpenAIImageSchema from './OpenAIImage.text2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +// Map EachLabs image_size enum to dimensions +const IMAGE_SIZE_MAP: Record = { + '1024x1024': { width: 1024, height: 1024 }, + '1536x1024': { width: 1536, height: 1024 }, + '1024x1536': { width: 1024, height: 1536 } +}; + +/** + * Input interface for OpenAI GPT Image text-to-image + */ +export type OpenAIImageText2ImageInput = { + prompt: string; + image_size?: '1024x1024' | '1536x1024' | '1024x1536'; +}; + +/** + * OpenAI GPT Image - High-quality text-to-image generation via EachLabs + * + * Features: + * - 3 image size options (Square, Landscape, Portrait) + * - High-quality generation from OpenAI + */ +export function OpenAIImageText2Image( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + // Add aspect ratio icons + addIconSetOnce(cesdk, '@imgly/plugin/formats', Icons.Formats); + + return createImageProvider( + { + modelSlug: 'openai-image-generation', + modelVersion: '0.0.1', + providerId: 'eachlabs/openai-gpt-image', + name: 'OpenAI GPT Image', + // @ts-ignore - JSON schema types are compatible at runtime + schema: OpenAIImageSchema, + inputReference: '#/components/schemas/OpenAIImageText2ImageInput', + cesdk, + getImageSize: (input) => + IMAGE_SIZE_MAP[input.image_size ?? '1024x1024'] ?? { + width: 1024, + height: 1024 + }, + mapInput: (input) => ({ + prompt: input.prompt, + image_size: input.image_size ?? '1024x1024' + }) + }, + config + ); + }; +} + +export default OpenAIImageText2Image; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts index 5a525d69..970fe1ac 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts @@ -5,6 +5,8 @@ import { NanoBananaPro as NanoBananaProText2Image } from './NanoBananaPro.text2i import { NanoBananaProImage2Image } from './NanoBananaPro.image2image'; import { Flux2Pro as Flux2ProText2Image } from './Flux2Pro.text2image'; import { Flux2ProImage2Image } from './Flux2Pro.image2image'; +import { OpenAIImageText2Image } from './OpenAIImage.text2image'; +import { OpenAIImageImage2Image } from './OpenAIImage.image2image'; const EachLabs = { NanoBananaPro: { @@ -14,6 +16,10 @@ const EachLabs = { Flux2Pro: { Text2Image: Flux2ProText2Image, Image2Image: Flux2ProImage2Image + }, + OpenAIGptImage: { + Text2Image: OpenAIImageText2Image, + Image2Image: OpenAIImageImage2Image } }; diff --git a/packages/plugin-ai-image-generation-web/translations.json b/packages/plugin-ai-image-generation-web/translations.json index 65a73b41..e5d96361 100644 --- a/packages/plugin-ai-image-generation-web/translations.json +++ b/packages/plugin-ai-image-generation-web/translations.json @@ -457,6 +457,17 @@ "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro/edit.property.image_url": "Source Image", "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro/edit.property.prompt": "Prompt", - "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro/edit.property.prompt.placeholder": "Describe the changes..." + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-pro/edit.property.prompt.placeholder": "Describe the changes...", + + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image.property.prompt.placeholder": "Describe your image...", + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image.property.image_size": "Format", + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image.property.image_size.1024x1024": "Square", + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image.property.image_size.1536x1024": "Landscape", + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image.property.image_size.1024x1536": "Portrait", + + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image/edit.property.image_url": "Source Image", + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image/edit.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image/edit.property.prompt.placeholder": "Describe the changes..." } } \ No newline at end of file diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index 8a5d40d4..f65d7906 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -37,7 +37,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | gemini-3-pro-image-preview | Gemini 3 \| Pro \| Image Preview | array | planned | Google Gemini image generation | | bytedance-seedream-v4-5-text-to-image | Bytedance \| Seedream \| v4.5 \| Text to Image | array | planned | Latest Seedream | | nano-banana-pro | Nano Banana Pro | array | implemented | Multi-style generation | -| openai-image-generation | GPT-1 \| Image Generation | image | planned | OpenAI GPT Image | +| openai-image-generation | GPT-1 \| Image Generation | image | implemented | OpenAI GPT Image | ## Image Generation (Image-to-Image / Edit) @@ -49,7 +49,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | gemini-3-pro-image-preview-edit | Gemini 3 Pro \| Image Edit | array | planned | Google Gemini edit | | bytedance-seedream-v4-5-edit | Bytedance \| Seedream \| v4.5 \| Edit | array | planned | Seedream edit | | nano-banana-pro-edit | Nano Banana Pro \| Edit | array | implemented | Multi-style edit | -| openai-image-edit | GPT-1 \| Image Edit | image | planned | OpenAI GPT Image edit | +| openai-image-edit | GPT-1 \| Image Edit | image | implemented | OpenAI GPT Image edit | ## Video Generation (Text-to-Video) @@ -72,27 +72,27 @@ This document tracks EachLabs AI models for implementation, focusing on image an ### Implemented -**Image Generation (T2I):** 2 models +**Image Generation (T2I):** 3 models - nano-banana-pro - flux-2-pro +- openai-image-generation -**Image Generation (I2I/Edit):** 2 models +**Image Generation (I2I/Edit):** 3 models - nano-banana-pro-edit - flux-2-pro-edit +- openai-image-edit ### Planned for Implementation -**Image Generation (T2I):** 5 models +**Image Generation (T2I):** 4 models - flux-2, flux-2-flex - gemini-3-pro-image-preview - bytedance-seedream-v4-5-text-to-image -- openai-image-generation -**Image Generation (I2I/Edit):** 5 models +**Image Generation (I2I/Edit):** 4 models - flux-2-edit, flux-2-flex-edit - gemini-3-pro-image-preview-edit - bytedance-seedream-v4-5-edit -- openai-image-edit **Video Generation (T2V):** 2 models - kling-v2-6-pro-text-to-video @@ -107,8 +107,8 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Category | Implemented | Planned | Total | |----------|-------------|---------|-------| -| Image T2I | 2 | 5 | 7 | -| Image I2I | 2 | 5 | 7 | +| Image T2I | 3 | 4 | 7 | +| Image I2I | 3 | 4 | 7 | | Video T2V | 0 | 2 | 2 | | Video I2V | 0 | 3 | 3 | -| **Total** | **4** | **15** | **19** | +| **Total** | **6** | **13** | **19** | From 7590868c0aa2b0ad2dd095474522e6ff06840a54 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 16 Dec 2025 18:04:37 +0100 Subject: [PATCH 08/20] docs(eachlabs): add OpenAI GPT Image provider documentation Add documentation for OpenAI GPT Image T2I/I2I providers including API reference, panel IDs, and asset history entries. --- .../plugin-ai-image-generation-web/README.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/plugin-ai-image-generation-web/README.md b/packages/plugin-ai-image-generation-web/README.md index b00757a0..c05ff2c9 100644 --- a/packages/plugin-ai-image-generation-web/README.md +++ b/packages/plugin-ai-image-generation-web/README.md @@ -1143,6 +1143,36 @@ Key features: - Supports up to 4 reference images - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page +#### 26. OpenAI GPT Image (Text-to-Image) via EachLabs + +OpenAI's GPT Image text-to-image generation via EachLabs: + +```typescript +text2image: EachLabsImage.OpenAIGptImage.Text2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- High-quality image generation from OpenAI +- 3 image size options (Square, Landscape, Portrait) +- Professional-grade output quality + +#### 27. OpenAI GPT Image Edit (Image-to-Image) via EachLabs + +OpenAI's GPT Image image transformation via EachLabs: + +```typescript +image2image: EachLabsImage.OpenAIGptImage.Image2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- High-quality image transformation +- Supports up to 16 reference images +- Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page + ### Customizing Labels and Translations You can customize all labels and text in the AI image generation interface using the translation system. This allows you to provide better labels for your users in any language. @@ -1731,6 +1761,13 @@ EachLabsImage.Flux2Pro.Text2Image(config: EachLabsProviderConfiguration) EachLabsImage.Flux2Pro.Image2Image(config: EachLabsProviderConfiguration) ``` +#### OpenAIGptImage.Text2Image / OpenAIGptImage.Image2Image + +```typescript +EachLabsImage.OpenAIGptImage.Text2Image(config: EachLabsProviderConfiguration) +EachLabsImage.OpenAIGptImage.Image2Image(config: EachLabsProviderConfiguration) +``` + ## UI Integration The plugin automatically registers the following UI components: @@ -1847,6 +1884,8 @@ const myImageProvider = { - EachLabs NanoBananaPro.Image2Image: `ly.img.ai.eachlabs/nano-banana-pro/edit` - EachLabs Flux2Pro.Text2Image: `ly.img.ai.eachlabs/flux-2-pro` - EachLabs Flux2Pro.Image2Image: `ly.img.ai.eachlabs/flux-2-pro/edit` + - EachLabs OpenAIGptImage.Text2Image: `ly.img.ai.eachlabs/openai-gpt-image` + - EachLabs OpenAIGptImage.Image2Image: `ly.img.ai.eachlabs/openai-gpt-image/edit` ### Asset History @@ -1884,6 +1923,8 @@ Generated images are automatically stored in asset sources with the following ID - EachLabs NanoBananaPro.Image2Image: `eachlabs/nano-banana-pro/edit.history` - EachLabs Flux2Pro.Text2Image: `eachlabs/flux-2-pro.history` - EachLabs Flux2Pro.Image2Image: `eachlabs/flux-2-pro/edit.history` +- EachLabs OpenAIGptImage.Text2Image: `eachlabs/openai-gpt-image.history` +- EachLabs OpenAIGptImage.Image2Image: `eachlabs/openai-gpt-image/edit.history` ### Dock Integration From 9f5d7cc412a1c5a2b0403cae530f0b1d600d6b9f Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Tue, 16 Dec 2025 18:25:04 +0100 Subject: [PATCH 09/20] feat(eachlabs): implement Seedream v4.5 text-to-image and image-to-image providers Add ByteDance Seedream v4.5 providers via EachLabs API with support for text-to-image generation and image-to-image editing. Features: - 6 image size options (square HD, square, portrait/landscape 4:3/16:9) - Image-to-image supports up to 10 reference images - Full quick actions support (edit, background swap, style transfer, etc.) - Improved facial details and text generation over v4.0 --- examples/ai/src/eachlabsProviders.ts | 8 + .../plugin-ai-image-generation-web/README.md | 42 +++++ .../src/eachlabs/Seedream45.image2image.json | 38 +++++ .../src/eachlabs/Seedream45.image2image.ts | 148 ++++++++++++++++++ .../src/eachlabs/Seedream45.text2image.json | 60 +++++++ .../src/eachlabs/Seedream45.text2image.ts | 79 ++++++++++ .../src/eachlabs/index.ts | 6 + specs/providers/eachlabs/providers.md | 24 +-- 8 files changed, 393 insertions(+), 12 deletions(-) create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.image2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.image2image.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.text2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.text2image.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index ebc49751..d2b7c6d0 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -24,6 +24,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { EachLabsImage.OpenAIGptImage.Text2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsImage.Seedream45.Text2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl }) ], image2image: [ @@ -38,6 +42,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { EachLabsImage.OpenAIGptImage.Image2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsImage.Seedream45.Image2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl }) ], text2video: [], diff --git a/packages/plugin-ai-image-generation-web/README.md b/packages/plugin-ai-image-generation-web/README.md index c05ff2c9..5818af2e 100644 --- a/packages/plugin-ai-image-generation-web/README.md +++ b/packages/plugin-ai-image-generation-web/README.md @@ -1173,6 +1173,37 @@ Key features: - Supports up to 16 reference images - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page +#### 28. Seedream v4.5 (Text-to-Image) via EachLabs + +ByteDance's Seedream v4.5 text-to-image generation via EachLabs: + +```typescript +text2image: EachLabsImage.Seedream45.Text2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- High-quality image generation from ByteDance +- 6 image size options (square HD, square, portrait 4:3/16:9, landscape 4:3/16:9) +- Improved facial details and text generation over v4.0 + +#### 29. Seedream v4.5 Edit (Image-to-Image) via EachLabs + +ByteDance's Seedream v4.5 image transformation via EachLabs: + +```typescript +image2image: EachLabsImage.Seedream45.Image2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- High-quality image transformation +- Supports up to 10 reference images +- Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page +- Improved facial details and text generation over v4.0 + ### Customizing Labels and Translations You can customize all labels and text in the AI image generation interface using the translation system. This allows you to provide better labels for your users in any language. @@ -1768,6 +1799,13 @@ EachLabsImage.OpenAIGptImage.Text2Image(config: EachLabsProviderConfiguration) EachLabsImage.OpenAIGptImage.Image2Image(config: EachLabsProviderConfiguration) ``` +#### Seedream45.Text2Image / Seedream45.Image2Image + +```typescript +EachLabsImage.Seedream45.Text2Image(config: EachLabsProviderConfiguration) +EachLabsImage.Seedream45.Image2Image(config: EachLabsProviderConfiguration) +``` + ## UI Integration The plugin automatically registers the following UI components: @@ -1886,6 +1924,8 @@ const myImageProvider = { - EachLabs Flux2Pro.Image2Image: `ly.img.ai.eachlabs/flux-2-pro/edit` - EachLabs OpenAIGptImage.Text2Image: `ly.img.ai.eachlabs/openai-gpt-image` - EachLabs OpenAIGptImage.Image2Image: `ly.img.ai.eachlabs/openai-gpt-image/edit` + - EachLabs Seedream45.Text2Image: `ly.img.ai.eachlabs/seedream-v4.5` + - EachLabs Seedream45.Image2Image: `ly.img.ai.eachlabs/seedream-v4.5/edit` ### Asset History @@ -1925,6 +1965,8 @@ Generated images are automatically stored in asset sources with the following ID - EachLabs Flux2Pro.Image2Image: `eachlabs/flux-2-pro/edit.history` - EachLabs OpenAIGptImage.Text2Image: `eachlabs/openai-gpt-image.history` - EachLabs OpenAIGptImage.Image2Image: `eachlabs/openai-gpt-image/edit.history` +- EachLabs Seedream45.Text2Image: `eachlabs/seedream-v4.5.history` +- EachLabs Seedream45.Image2Image: `eachlabs/seedream-v4.5/edit.history` ### Dock Integration diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.image2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.image2image.json new file mode 100644 index 00000000..72880679 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.image2image.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Seedream v4.5 Edit API", + "version": "1.0.0", + "description": "Bytedance Seedream v4.5 image-to-image editing via EachLabs" + }, + "components": { + "schemas": { + "Seedream45Image2ImageInput": { + "title": "Seedream45Image2ImageInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "description": "Image to transform", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image editing", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + } + }, + "x-fal-order-properties": ["image_url", "prompt"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.image2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.image2image.ts new file mode 100644 index 00000000..e39a87b4 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.image2image.ts @@ -0,0 +1,148 @@ +import { + type Provider, + type ImageOutput, + CommonProperties, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Seedream45Image2ImageSchema from './Seedream45.image2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for Seedream v4.5 image-to-image + */ +export type Seedream45Image2ImageInput = { + prompt: string; + image_url?: string; + image_urls?: string[]; +}; + +/** + * Seedream v4.5 Edit - Transform images using AI via EachLabs + * + * Features: + * - High-quality image transformation + * - Supports up to 10 reference images + * - Improved facial details and text generation over v4.0 + */ +export function Seedream45Image2Image( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/seedream-v4.5/edit'; + + // Set translations for the panel + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Edit', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createImageProvider( + { + modelSlug: 'bytedance-seedream-v4-5-edit', + modelVersion: '0.0.1', + providerId, + name: 'Seedream v4.5 Edit', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Seedream45Image2ImageSchema, + inputReference: '#/components/schemas/Seedream45Image2ImageInput', + cesdk, + middleware: config.middlewares ?? [], + renderCustomProperty: CommonProperties.ImageUrl(providerId, { + cesdk + }), + supportedQuickActions: { + 'ly.img.editImage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.swapBackground': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.styleTransfer': { + mapInput: (input) => ({ + prompt: input.style, + image_url: input.uri + }) + }, + 'ly.img.artistTransfer': { + mapInput: (input) => ({ + prompt: input.artist, + image_url: input.uri + }) + }, + 'ly.img.createVariant': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPageWithPrompt': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.combineImages': { + mapInput: (input) => ({ + prompt: input.prompt, + image_urls: input.uris + }) + } + }, + getBlockInput: async (input) => { + // Get first image URL from either single or array input + const firstImageUrl = input.image_url ?? input.image_urls?.[0]; + if (firstImageUrl == null) { + throw new Error('No image URL provided'); + } + + // Get dimensions from input image for UI placeholder + const { width, height } = await getImageDimensionsFromURL( + firstImageUrl, + cesdk.engine + ); + return Promise.resolve({ + image: { + width, + height + } + }); + }, + mapInput: (input) => { + // Map to EachLabs API format + // bytedance-seedream-v4-5-edit uses image_urls (array) for image input + const imageUrls = + input.image_urls ?? (input.image_url ? [input.image_url] : []); + return { + prompt: input.prompt, + image_urls: imageUrls + }; + } + }, + config + ); + }; +} + +export default Seedream45Image2Image; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.text2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.text2image.json new file mode 100644 index 00000000..0d58fe65 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.text2image.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Seedream v4.5 API", + "version": "1.0.0", + "description": "Bytedance Seedream v4.5 text-to-image generation via EachLabs" + }, + "components": { + "schemas": { + "Seedream45Input": { + "title": "Seedream45Input", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image generation", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "image_size": { + "title": "Format", + "type": "string", + "enum": [ + "square_hd", + "square", + "portrait_4_3", + "portrait_16_9", + "landscape_4_3", + "landscape_16_9" + ], + "default": "landscape_4_3", + "description": "Output image format", + "x-imgly-enum-labels": { + "square_hd": "Square HD (1:1)", + "square": "Square (1:1)", + "portrait_4_3": "Portrait (3:4)", + "portrait_16_9": "Portrait (9:16)", + "landscape_4_3": "Landscape (4:3)", + "landscape_16_9": "Landscape (16:9)" + }, + "x-imgly-enum-icons": { + "square_hd": "@imgly/plugin/formats/ratio1by1", + "square": "@imgly/plugin/formats/ratio1by1", + "portrait_4_3": "@imgly/plugin/formats/ratio3by4", + "portrait_16_9": "@imgly/plugin/formats/ratio9by16", + "landscape_4_3": "@imgly/plugin/formats/ratio4by3", + "landscape_16_9": "@imgly/plugin/formats/ratio16by9" + } + } + }, + "x-fal-order-properties": ["prompt", "image_size"], + "required": ["prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.text2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.text2image.ts new file mode 100644 index 00000000..0fa8cdf5 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Seedream45.text2image.ts @@ -0,0 +1,79 @@ +import { + type Provider, + type ImageOutput, + addIconSetOnce +} from '@imgly/plugin-ai-generation-web'; +import { Icons } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Seedream45Schema from './Seedream45.text2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +// Map EachLabs image_size enum to dimensions +const IMAGE_SIZE_MAP: Record = { + square_hd: { width: 1024, height: 1024 }, + square: { width: 512, height: 512 }, + portrait_4_3: { width: 896, height: 1152 }, + portrait_16_9: { width: 768, height: 1344 }, + landscape_4_3: { width: 1152, height: 896 }, + landscape_16_9: { width: 1344, height: 768 } +}; + +/** + * Input interface for Seedream v4.5 text-to-image + */ +export type Seedream45Input = { + prompt: string; + image_size?: + | 'square_hd' + | 'square' + | 'portrait_4_3' + | 'portrait_16_9' + | 'landscape_4_3' + | 'landscape_16_9'; +}; + +/** + * Seedream v4.5 - High-quality text-to-image generation via EachLabs + * + * Features: + * - 6 image size options + * - High-quality generation from ByteDance + * - Improved facial details and text generation over v4.0 + */ +export function Seedream45( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + // Add aspect ratio icons + addIconSetOnce(cesdk, '@imgly/plugin/formats', Icons.Formats); + + return createImageProvider( + { + modelSlug: 'bytedance-seedream-v4-5-text-to-image', + modelVersion: '0.0.1', + providerId: 'eachlabs/seedream-v4.5', + name: 'Seedream v4.5', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Seedream45Schema, + inputReference: '#/components/schemas/Seedream45Input', + cesdk, + getImageSize: (input) => + IMAGE_SIZE_MAP[input.image_size ?? 'landscape_4_3'] ?? { + width: 1152, + height: 896 + }, + mapInput: (input) => ({ + prompt: input.prompt, + image_size: input.image_size ?? 'landscape_4_3' + }) + }, + config + ); + }; +} + +export default Seedream45; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts index 970fe1ac..1c002709 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts @@ -7,6 +7,8 @@ import { Flux2Pro as Flux2ProText2Image } from './Flux2Pro.text2image'; import { Flux2ProImage2Image } from './Flux2Pro.image2image'; import { OpenAIImageText2Image } from './OpenAIImage.text2image'; import { OpenAIImageImage2Image } from './OpenAIImage.image2image'; +import { Seedream45 as Seedream45Text2Image } from './Seedream45.text2image'; +import { Seedream45Image2Image } from './Seedream45.image2image'; const EachLabs = { NanoBananaPro: { @@ -20,6 +22,10 @@ const EachLabs = { OpenAIGptImage: { Text2Image: OpenAIImageText2Image, Image2Image: OpenAIImageImage2Image + }, + Seedream45: { + Text2Image: Seedream45Text2Image, + Image2Image: Seedream45Image2Image } }; diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index f65d7906..bc0e1096 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -2,7 +2,7 @@ This document tracks EachLabs AI models for implementation, focusing on image and video generation. -**Last Updated**: 2025-12-15 +**Last Updated**: 2025-12-16 ## API Reference @@ -35,7 +35,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | flux-2 | Flux 2 | array | planned | Standard Flux 2 | | flux-2-flex | Flux 2 \| Flex | image | planned | Prompt expansion enabled | | gemini-3-pro-image-preview | Gemini 3 \| Pro \| Image Preview | array | planned | Google Gemini image generation | -| bytedance-seedream-v4-5-text-to-image | Bytedance \| Seedream \| v4.5 \| Text to Image | array | planned | Latest Seedream | +| bytedance-seedream-v4-5-text-to-image | Bytedance \| Seedream \| v4.5 \| Text to Image | array | implemented | Latest Seedream | | nano-banana-pro | Nano Banana Pro | array | implemented | Multi-style generation | | openai-image-generation | GPT-1 \| Image Generation | image | implemented | OpenAI GPT Image | @@ -47,7 +47,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | flux-2-edit | Flux 2 \| Edit | array | planned | Standard Flux 2 edit | | flux-2-flex-edit | Flux 2 \| Flex \| Edit | image | planned | Flex edit variant | | gemini-3-pro-image-preview-edit | Gemini 3 Pro \| Image Edit | array | planned | Google Gemini edit | -| bytedance-seedream-v4-5-edit | Bytedance \| Seedream \| v4.5 \| Edit | array | planned | Seedream edit | +| bytedance-seedream-v4-5-edit | Bytedance \| Seedream \| v4.5 \| Edit | array | implemented | Seedream edit | | nano-banana-pro-edit | Nano Banana Pro \| Edit | array | implemented | Multi-style edit | | openai-image-edit | GPT-1 \| Image Edit | image | implemented | OpenAI GPT Image edit | @@ -72,27 +72,27 @@ This document tracks EachLabs AI models for implementation, focusing on image an ### Implemented -**Image Generation (T2I):** 3 models +**Image Generation (T2I):** 4 models - nano-banana-pro - flux-2-pro - openai-image-generation +- bytedance-seedream-v4-5-text-to-image -**Image Generation (I2I/Edit):** 3 models +**Image Generation (I2I/Edit):** 4 models - nano-banana-pro-edit - flux-2-pro-edit - openai-image-edit +- bytedance-seedream-v4-5-edit ### Planned for Implementation -**Image Generation (T2I):** 4 models +**Image Generation (T2I):** 3 models - flux-2, flux-2-flex - gemini-3-pro-image-preview -- bytedance-seedream-v4-5-text-to-image -**Image Generation (I2I/Edit):** 4 models +**Image Generation (I2I/Edit):** 3 models - flux-2-edit, flux-2-flex-edit - gemini-3-pro-image-preview-edit -- bytedance-seedream-v4-5-edit **Video Generation (T2V):** 2 models - kling-v2-6-pro-text-to-video @@ -107,8 +107,8 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Category | Implemented | Planned | Total | |----------|-------------|---------|-------| -| Image T2I | 3 | 4 | 7 | -| Image I2I | 3 | 4 | 7 | +| Image T2I | 4 | 3 | 7 | +| Image I2I | 4 | 3 | 7 | | Video T2V | 0 | 2 | 2 | | Video I2V | 0 | 3 | 3 | -| **Total** | **6** | **13** | **19** | +| **Total** | **8** | **11** | **19** | From 843834b82528d72e5811c12aabfc3c5312b97206 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2025 09:21:55 +0100 Subject: [PATCH 10/20] feat(eachlabs): implement Gemini 3 Pro text-to-image and image-to-image providers Add Google Gemini 3 Pro image generation via EachLabs: Text-to-Image: - 8 aspect ratio options (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9) - 3 resolution options (1K, 2K, 4K) - Provider ID: eachlabs/gemini-3-pro-image Image-to-Image: - Supports up to 10 reference images - Full quick action support (edit, swap background, style transfer, etc.) - Provider ID: eachlabs/gemini-3-pro-image/edit --- examples/ai/src/eachlabsProviders.ts | 8 + .../src/eachlabs/Gemini3Pro.image2image.json | 38 +++++ .../src/eachlabs/Gemini3Pro.image2image.ts | 156 ++++++++++++++++++ .../src/eachlabs/Gemini3Pro.text2image.json | 78 +++++++++ .../src/eachlabs/Gemini3Pro.text2image.ts | 128 ++++++++++++++ .../src/eachlabs/index.ts | 6 + .../translations.json | 22 ++- 7 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.image2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.image2image.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.text2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.text2image.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index d2b7c6d0..b70942a9 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -28,6 +28,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { EachLabsImage.Seedream45.Text2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsImage.Gemini3Pro.Text2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl }) ], image2image: [ @@ -46,6 +50,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { EachLabsImage.Seedream45.Image2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsImage.Gemini3Pro.Image2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl }) ], text2video: [], diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.image2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.image2image.json new file mode 100644 index 00000000..5053972f --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.image2image.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Gemini 3 Pro Image Edit API", + "version": "1.0.0", + "description": "Google Gemini 3 Pro image-to-image editing via EachLabs" + }, + "components": { + "schemas": { + "Gemini3ProImage2ImageInput": { + "title": "Gemini3ProImage2ImageInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "description": "Image to transform", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image editing", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + } + }, + "x-fal-order-properties": ["image_url", "prompt"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.image2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.image2image.ts new file mode 100644 index 00000000..05dfddb3 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.image2image.ts @@ -0,0 +1,156 @@ +import { + type Provider, + type ImageOutput, + CommonProperties, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Gemini3ProImage2ImageSchema from './Gemini3Pro.image2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for Gemini 3 Pro image-to-image + */ +export type Gemini3ProImage2ImageInput = { + prompt: string; + image_url?: string; + image_urls?: string[]; +}; + +/** + * Gemini 3 Pro Image Edit - Transform images using Google Gemini via EachLabs + * + * Features: + * - High-quality image transformation + * - Supports multiple reference images (up to 10) + */ +export function Gemini3ProImage2Image( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/gemini-3-pro-image/edit'; + + // Set translations for the panel + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Edit', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createImageProvider( + { + modelSlug: 'gemini-3-pro-image-preview-edit', + modelVersion: '0.0.1', + providerId, + name: 'Gemini 3 Pro Image Edit', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Gemini3ProImage2ImageSchema, + inputReference: '#/components/schemas/Gemini3ProImage2ImageInput', + cesdk, + middleware: config.middlewares ?? [], + renderCustomProperty: CommonProperties.ImageUrl(providerId, { + cesdk + }), + supportedQuickActions: { + 'ly.img.editImage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.swapBackground': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.styleTransfer': { + mapInput: (input) => ({ + prompt: input.style, + image_url: input.uri + }) + }, + 'ly.img.artistTransfer': { + mapInput: (input) => ({ + prompt: input.artist, + image_url: input.uri + }) + }, + 'ly.img.createVariant': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPageWithPrompt': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.combineImages': { + mapInput: (input) => ({ + prompt: input.prompt, + image_urls: input.uris + }) + } + }, + getBlockInput: async (input) => { + // Get first image URL from either single or array input + const firstImageUrl = input.image_url ?? input.image_urls?.[0]; + if (firstImageUrl == null) { + throw new Error('No image URL provided'); + } + + // Get dimensions from input image for UI placeholder + const { width, height } = await getImageDimensionsFromURL( + firstImageUrl, + cesdk.engine + ); + return Promise.resolve({ + image: { + width, + height + } + }); + }, + mapInput: (input) => { + // Map to EachLabs API format + // gemini-3-pro-image-preview-edit uses image_urls array + const imageUrl = input.image_url ?? input.image_urls?.[0]; + const apiInput: Record = { + prompt: input.prompt, + num_images: 1, + output_format: 'png' + }; + + // Map to image_urls array format + if (input.image_urls && input.image_urls.length > 0) { + apiInput.image_urls = input.image_urls; + } else if (imageUrl) { + apiInput.image_urls = [imageUrl]; + } + + return apiInput; + } + }, + config + ); + }; +} + +export default Gemini3ProImage2Image; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.text2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.text2image.json new file mode 100644 index 00000000..fb5be420 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.text2image.json @@ -0,0 +1,78 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Gemini 3 Pro Image API", + "version": "1.0.0", + "description": "Google Gemini 3 Pro text-to-image generation via EachLabs" + }, + "components": { + "schemas": { + "Gemini3ProText2ImageInput": { + "title": "Gemini3ProText2ImageInput", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image generation", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "aspect_ratio": { + "title": "Format", + "type": "string", + "enum": [ + "1:1", + "16:9", + "9:16", + "4:3", + "3:4", + "3:2", + "2:3", + "21:9" + ], + "default": "1:1", + "description": "Output image aspect ratio", + "x-imgly-enum-labels": { + "1:1": "Square", + "16:9": "Widescreen", + "9:16": "Mobile", + "4:3": "Landscape 4:3", + "3:4": "Portrait 3:4", + "3:2": "Photo Landscape", + "2:3": "Photo Portrait", + "21:9": "Ultra-Wide" + }, + "x-imgly-enum-icons": { + "1:1": "@imgly/plugin/formats/ratio1by1", + "16:9": "@imgly/plugin/formats/ratio16by9", + "9:16": "@imgly/plugin/formats/ratio9by16", + "4:3": "@imgly/plugin/formats/ratio4by3", + "3:4": "@imgly/plugin/formats/ratio3by4", + "3:2": "@imgly/plugin/formats/ratio4by3", + "2:3": "@imgly/plugin/formats/ratio3by4", + "21:9": "@imgly/plugin/formats/ratio16by9" + } + }, + "resolution": { + "title": "Resolution", + "type": "string", + "enum": ["1K", "2K", "4K"], + "default": "1K", + "description": "Output image resolution", + "x-imgly-enum-labels": { + "1K": "Standard (1K)", + "2K": "HD (2K)", + "4K": "Ultra HD (4K)" + } + } + }, + "x-fal-order-properties": ["prompt", "aspect_ratio", "resolution"], + "required": ["prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.text2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.text2image.ts new file mode 100644 index 00000000..a934be66 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Gemini3Pro.text2image.ts @@ -0,0 +1,128 @@ +import { + type Provider, + type ImageOutput, + addIconSetOnce +} from '@imgly/plugin-ai-generation-web'; +import { Icons } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Gemini3ProSchema from './Gemini3Pro.text2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +// Map aspect ratio and resolution to approximate dimensions +// Gemini accepts aspect_ratio string directly, but we need dimensions for UI +const ASPECT_RATIO_DIMENSIONS: Record< + string, + Record +> = { + '1:1': { + '1K': { width: 1024, height: 1024 }, + '2K': { width: 2048, height: 2048 }, + '4K': { width: 4096, height: 4096 } + }, + '16:9': { + '1K': { width: 1344, height: 768 }, + '2K': { width: 2688, height: 1536 }, + '4K': { width: 5376, height: 3072 } + }, + '9:16': { + '1K': { width: 768, height: 1344 }, + '2K': { width: 1536, height: 2688 }, + '4K': { width: 3072, height: 5376 } + }, + '4:3': { + '1K': { width: 1152, height: 896 }, + '2K': { width: 2304, height: 1792 }, + '4K': { width: 4608, height: 3584 } + }, + '3:4': { + '1K': { width: 896, height: 1152 }, + '2K': { width: 1792, height: 2304 }, + '4K': { width: 3584, height: 4608 } + }, + '3:2': { + '1K': { width: 1152, height: 768 }, + '2K': { width: 2304, height: 1536 }, + '4K': { width: 4608, height: 3072 } + }, + '2:3': { + '1K': { width: 768, height: 1152 }, + '2K': { width: 1536, height: 2304 }, + '4K': { width: 3072, height: 4608 } + }, + '21:9': { + '1K': { width: 1536, height: 640 }, + '2K': { width: 3072, height: 1280 }, + '4K': { width: 6144, height: 2560 } + } +}; + +/** + * Input interface for Gemini 3 Pro text-to-image + */ +export type Gemini3ProText2ImageInput = { + prompt: string; + aspect_ratio?: + | '1:1' + | '16:9' + | '9:16' + | '4:3' + | '3:4' + | '3:2' + | '2:3' + | '21:9'; + resolution?: '1K' | '2K' | '4K'; +}; + +/** + * Gemini 3 Pro Image - Google's advanced text-to-image generation via EachLabs + * + * Features: + * - 8 aspect ratio options + * - 3 resolution options (1K, 2K, 4K) + * - High-quality generation from Google Gemini + */ +export function Gemini3ProText2Image( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + // Add aspect ratio icons + addIconSetOnce(cesdk, '@imgly/plugin/formats', Icons.Formats); + + return createImageProvider( + { + modelSlug: 'gemini-3-pro-image-preview', + modelVersion: '0.0.1', + providerId: 'eachlabs/gemini-3-pro-image', + name: 'Gemini 3 Pro Image', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Gemini3ProSchema, + inputReference: '#/components/schemas/Gemini3ProText2ImageInput', + cesdk, + getImageSize: (input) => { + const aspectRatio = input.aspect_ratio ?? '1:1'; + const resolution = input.resolution ?? '1K'; + return ( + ASPECT_RATIO_DIMENSIONS[aspectRatio]?.[resolution] ?? { + width: 1024, + height: 1024 + } + ); + }, + mapInput: (input) => ({ + prompt: input.prompt, + aspect_ratio: input.aspect_ratio ?? '1:1', + resolution: input.resolution ?? '1K', + num_images: 1, + output_format: 'png' + }) + }, + config + ); + }; +} + +export default Gemini3ProText2Image; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts index 1c002709..16366b58 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts @@ -9,6 +9,8 @@ import { OpenAIImageText2Image } from './OpenAIImage.text2image'; import { OpenAIImageImage2Image } from './OpenAIImage.image2image'; import { Seedream45 as Seedream45Text2Image } from './Seedream45.text2image'; import { Seedream45Image2Image } from './Seedream45.image2image'; +import { Gemini3ProText2Image } from './Gemini3Pro.text2image'; +import { Gemini3ProImage2Image } from './Gemini3Pro.image2image'; const EachLabs = { NanoBananaPro: { @@ -26,6 +28,10 @@ const EachLabs = { Seedream45: { Text2Image: Seedream45Text2Image, Image2Image: Seedream45Image2Image + }, + Gemini3Pro: { + Text2Image: Gemini3ProText2Image, + Image2Image: Gemini3ProImage2Image } }; diff --git a/packages/plugin-ai-image-generation-web/translations.json b/packages/plugin-ai-image-generation-web/translations.json index e5d96361..2cb7e4e0 100644 --- a/packages/plugin-ai-image-generation-web/translations.json +++ b/packages/plugin-ai-image-generation-web/translations.json @@ -468,6 +468,26 @@ "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image/edit.property.image_url": "Source Image", "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image/edit.property.prompt": "Prompt", - "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image/edit.property.prompt.placeholder": "Describe the changes..." + "ly.img.plugin-ai-image-generation-web.eachlabs/openai-gpt-image/edit.property.prompt.placeholder": "Describe the changes...", + + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.prompt.placeholder": "Describe your image...", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.aspect_ratio": "Format", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.aspect_ratio.1:1": "Square", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.aspect_ratio.16:9": "Widescreen", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.aspect_ratio.9:16": "Mobile", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.aspect_ratio.4:3": "Landscape 4:3", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.aspect_ratio.3:4": "Portrait 3:4", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.aspect_ratio.3:2": "Photo Landscape", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.aspect_ratio.2:3": "Photo Portrait", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.aspect_ratio.21:9": "Ultra-Wide", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.resolution": "Resolution", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.resolution.1K": "Standard (1K)", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.resolution.2K": "HD (2K)", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image.property.resolution.4K": "Ultra HD (4K)", + + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image/edit.property.image_url": "Source Image", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image/edit.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image/edit.property.prompt.placeholder": "Describe the changes..." } } \ No newline at end of file From d2da989fc7c4472a288a37b41fc701327df6baaf Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2025 09:22:05 +0100 Subject: [PATCH 11/20] docs(eachlabs): add Gemini 3 Pro provider documentation - Add provider documentation to README (sections 30-31, API reference) - Update panel IDs and asset history sections - Mark gemini-3-pro-image-preview as implemented in providers.md - Fix ui-guidelines.md to document correct available format icons --- .../plugin-ai-image-generation-web/README.md | 42 +++++++++++++++++++ specs/providers/eachlabs/providers.md | 22 +++++----- specs/providers/patterns/ui-guidelines.md | 11 +++-- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/packages/plugin-ai-image-generation-web/README.md b/packages/plugin-ai-image-generation-web/README.md index 5818af2e..411ee1d4 100644 --- a/packages/plugin-ai-image-generation-web/README.md +++ b/packages/plugin-ai-image-generation-web/README.md @@ -1204,6 +1204,37 @@ Key features: - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page - Improved facial details and text generation over v4.0 +#### 30. Gemini 3 Pro Image (Text-to-Image) via EachLabs + +Google's Gemini 3 Pro text-to-image generation via EachLabs: + +```typescript +text2image: EachLabsImage.Gemini3Pro.Text2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- High-quality image generation from Google Gemini +- 8 aspect ratio options (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9) +- Multiple resolution options (1K, 2K, 4K) +- Professional-grade output quality + +#### 31. Gemini 3 Pro Image Edit (Image-to-Image) via EachLabs + +Google's Gemini 3 Pro image transformation via EachLabs: + +```typescript +image2image: EachLabsImage.Gemini3Pro.Image2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- High-quality image transformation +- Supports up to 10 reference images +- Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page + ### Customizing Labels and Translations You can customize all labels and text in the AI image generation interface using the translation system. This allows you to provide better labels for your users in any language. @@ -1806,6 +1837,13 @@ EachLabsImage.Seedream45.Text2Image(config: EachLabsProviderConfiguration) EachLabsImage.Seedream45.Image2Image(config: EachLabsProviderConfiguration) ``` +#### Gemini3Pro.Text2Image / Gemini3Pro.Image2Image + +```typescript +EachLabsImage.Gemini3Pro.Text2Image(config: EachLabsProviderConfiguration) +EachLabsImage.Gemini3Pro.Image2Image(config: EachLabsProviderConfiguration) +``` + ## UI Integration The plugin automatically registers the following UI components: @@ -1926,6 +1964,8 @@ const myImageProvider = { - EachLabs OpenAIGptImage.Image2Image: `ly.img.ai.eachlabs/openai-gpt-image/edit` - EachLabs Seedream45.Text2Image: `ly.img.ai.eachlabs/seedream-v4.5` - EachLabs Seedream45.Image2Image: `ly.img.ai.eachlabs/seedream-v4.5/edit` + - EachLabs Gemini3Pro.Text2Image: `ly.img.ai.eachlabs/gemini-3-pro-image` + - EachLabs Gemini3Pro.Image2Image: `ly.img.ai.eachlabs/gemini-3-pro-image/edit` ### Asset History @@ -1967,6 +2007,8 @@ Generated images are automatically stored in asset sources with the following ID - EachLabs OpenAIGptImage.Image2Image: `eachlabs/openai-gpt-image/edit.history` - EachLabs Seedream45.Text2Image: `eachlabs/seedream-v4.5.history` - EachLabs Seedream45.Image2Image: `eachlabs/seedream-v4.5/edit.history` +- EachLabs Gemini3Pro.Text2Image: `eachlabs/gemini-3-pro-image.history` +- EachLabs Gemini3Pro.Image2Image: `eachlabs/gemini-3-pro-image/edit.history` ### Dock Integration diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index bc0e1096..dbaf45e5 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -34,7 +34,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | flux-2-pro | Flux 2 Pro | image | implemented | Latest Flux, high quality | | flux-2 | Flux 2 | array | planned | Standard Flux 2 | | flux-2-flex | Flux 2 \| Flex | image | planned | Prompt expansion enabled | -| gemini-3-pro-image-preview | Gemini 3 \| Pro \| Image Preview | array | planned | Google Gemini image generation | +| gemini-3-pro-image-preview | Gemini 3 \| Pro \| Image Preview | array | implemented | Google Gemini image generation | | bytedance-seedream-v4-5-text-to-image | Bytedance \| Seedream \| v4.5 \| Text to Image | array | implemented | Latest Seedream | | nano-banana-pro | Nano Banana Pro | array | implemented | Multi-style generation | | openai-image-generation | GPT-1 \| Image Generation | image | implemented | OpenAI GPT Image | @@ -46,7 +46,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | flux-2-pro-edit | Flux 2 Pro \| Edit | image | implemented | Edit with Flux 2 Pro | | flux-2-edit | Flux 2 \| Edit | array | planned | Standard Flux 2 edit | | flux-2-flex-edit | Flux 2 \| Flex \| Edit | image | planned | Flex edit variant | -| gemini-3-pro-image-preview-edit | Gemini 3 Pro \| Image Edit | array | planned | Google Gemini edit | +| gemini-3-pro-image-preview-edit | Gemini 3 Pro \| Image Edit | array | implemented | Google Gemini edit | | bytedance-seedream-v4-5-edit | Bytedance \| Seedream \| v4.5 \| Edit | array | implemented | Seedream edit | | nano-banana-pro-edit | Nano Banana Pro \| Edit | array | implemented | Multi-style edit | | openai-image-edit | GPT-1 \| Image Edit | image | implemented | OpenAI GPT Image edit | @@ -72,27 +72,27 @@ This document tracks EachLabs AI models for implementation, focusing on image an ### Implemented -**Image Generation (T2I):** 4 models +**Image Generation (T2I):** 5 models - nano-banana-pro - flux-2-pro - openai-image-generation - bytedance-seedream-v4-5-text-to-image +- gemini-3-pro-image-preview -**Image Generation (I2I/Edit):** 4 models +**Image Generation (I2I/Edit):** 5 models - nano-banana-pro-edit - flux-2-pro-edit - openai-image-edit - bytedance-seedream-v4-5-edit +- gemini-3-pro-image-preview-edit ### Planned for Implementation -**Image Generation (T2I):** 3 models +**Image Generation (T2I):** 2 models - flux-2, flux-2-flex -- gemini-3-pro-image-preview -**Image Generation (I2I/Edit):** 3 models +**Image Generation (I2I/Edit):** 2 models - flux-2-edit, flux-2-flex-edit -- gemini-3-pro-image-preview-edit **Video Generation (T2V):** 2 models - kling-v2-6-pro-text-to-video @@ -107,8 +107,8 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Category | Implemented | Planned | Total | |----------|-------------|---------|-------| -| Image T2I | 4 | 3 | 7 | -| Image I2I | 4 | 3 | 7 | +| Image T2I | 5 | 2 | 7 | +| Image I2I | 5 | 2 | 7 | | Video T2V | 0 | 2 | 2 | | Video I2V | 0 | 3 | 3 | -| **Total** | **8** | **11** | **19** | +| **Total** | **10** | **9** | **19** | diff --git a/specs/providers/patterns/ui-guidelines.md b/specs/providers/patterns/ui-guidelines.md index 709dd4f4..714def42 100644 --- a/specs/providers/patterns/ui-guidelines.md +++ b/specs/providers/patterns/ui-guidelines.md @@ -238,10 +238,13 @@ Icons available from `@imgly/plugin/formats`: | Portrait 9:16 | `ratio9by16` | | Landscape 4:3 | `ratio4by3` | | Portrait 3:4 | `ratio3by4` | -| Landscape 3:2 | `ratio3by2` | -| Portrait 2:3 | `ratio2by3` | -| Ultra-wide 21:9 | `ratio21by9` | -| Ultra-tall 9:21 | `ratio9by21` | +| Free/Custom | `ratioFree` | + +**Note:** Only these 6 icons exist. For other aspect ratios, use the closest matching icon: +- 3:2 (Photo Landscape) → use `ratio4by3` +- 2:3 (Photo Portrait) → use `ratio3by4` +- 21:9 (Ultra-wide) → use `ratio16by9` +- 9:21 (Ultra-tall) → use `ratio9by16` Usage: `@imgly/plugin/formats/{key}` From b30761fdf1599f2da48b2d8ec5c1393e9e3f1a6f Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2025 15:47:48 +0100 Subject: [PATCH 12/20] feat(eachlabs): add image upload support for inference Add storage upload functionality to EachLabs clients, aligning with fal.ai and runware implementations. Images are now uploaded to EachLabs storage via the /api/v1/upload endpoint instead of being converted to data URIs. - Add storage.upload() method to image and video clients - Add uploadImageInputToEachLabsIfNeeded() utility functions - Update createImageProvider to use upload instead of data URI conversion - Keep legacy convertImageUrlForEachLabs() for backward compatibility --- .../src/eachlabs/createEachLabsClient.ts | 64 ++++ .../src/eachlabs/createImageProvider.ts | 18 +- .../src/eachlabs/utils.ts | 87 +++++ .../src/eachlabs/createEachLabsClient.ts | 310 +++++++++++++++++- .../src/eachlabs/utils.ts | 84 +++++ 5 files changed, 541 insertions(+), 22 deletions(-) create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/utils.ts diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts index f61c50f1..406c649e 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/createEachLabsClient.ts @@ -50,6 +50,25 @@ export interface EachLabsImageResult { imageURL: string; } +/** + * Upload response from EachLabs storage API + */ +interface UploadResponse { + url: string; +} + +/** + * EachLabs storage interface for file uploads + */ +export interface EachLabsStorage { + /** + * Upload a file to EachLabs storage + * @param file - The file to upload + * @returns The URL of the uploaded file + */ + upload: (file: File) => Promise; +} + /** * EachLabs API client interface */ @@ -58,6 +77,10 @@ export interface EachLabsClient { params: EachLabsImageInferenceParams, abortSignal?: AbortSignal ) => Promise; + /** + * Storage API for uploading files + */ + storage: EachLabsStorage; } /** @@ -240,7 +263,48 @@ export function createEachLabsClient( return []; } + /** + * Upload a file to EachLabs storage + */ + async function uploadFile(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + + // Use the storage upload endpoint via proxy + const uploadUrl = `${proxyUrl}/api/v1/upload`; + + const response = await fetch(uploadUrl, { + method: 'POST', + headers: { + // Don't set Content-Type header - let the browser set it with the boundary + ...Object.fromEntries( + Object.entries(baseHeaders).filter( + ([key]) => key.toLowerCase() !== 'content-type' + ) + ) + }, + body: formData + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + throw new Error( + `EachLabs storage upload error: ${response.status} - ${errorText}` + ); + } + + const data = (await response.json()) as UploadResponse; + if (!data.url) { + throw new Error('EachLabs storage did not return a URL'); + } + + return data.url; + } + return { + storage: { + upload: uploadFile + }, imageInference: async ( params: EachLabsImageInferenceParams, abortSignal?: AbortSignal diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts index 431763f8..48b531bf 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/createImageProvider.ts @@ -12,8 +12,8 @@ import { } from '@imgly/plugin-ai-generation-web'; import { createEachLabsClient, EachLabsClient } from './createEachLabsClient'; import { - convertImageUrlForEachLabs, - convertImageUrlArrayForEachLabs + uploadImageInputToEachLabsIfNeeded, + uploadImageArrayToEachLabsIfNeeded } from './utils'; import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; import { getImageDimensionsFromAspectRatio } from './types'; @@ -202,16 +202,17 @@ function createImageProvider< throw new Error('Provider not initialized'); } - // Convert image URL if needed for image-to-image + // Upload image URL if needed for image-to-image let processedInput = input; let imageDimensions: { width: number; height: number } | undefined; if (input.image_url != null) { - const convertedUrl = await convertImageUrlForEachLabs( + const uploadedUrl = await uploadImageInputToEachLabsIfNeeded( + eachLabsClient, input.image_url, options.cesdk ); - processedInput = { ...input, image_url: convertedUrl }; + processedInput = { ...input, image_url: uploadedUrl }; // Get dimensions from input image for image-to-image if (options.cesdk != null) { @@ -223,13 +224,14 @@ function createImageProvider< } } - // Convert image URLs array if needed for multi-image inputs + // Upload image URLs array if needed for multi-image inputs if (input.image_urls != null && input.image_urls.length > 0) { - const convertedUrls = await convertImageUrlArrayForEachLabs( + const uploadedUrls = await uploadImageArrayToEachLabsIfNeeded( + eachLabsClient, input.image_urls, options.cesdk ); - processedInput = { ...processedInput, image_urls: convertedUrls }; + processedInput = { ...processedInput, image_urls: uploadedUrls }; // For multi-image, get dimensions from the first image if not already set if ( diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/utils.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/utils.ts index 2e8ebba2..1a04c159 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/utils.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/utils.ts @@ -1,6 +1,92 @@ +import { mimeTypeToExtension } from '@imgly/plugin-utils'; import type CreativeEditorSDK from '@cesdk/cesdk-js'; +import { EachLabsClient } from './createEachLabsClient'; /** + * Uploads an image to EachLabs storage if needed. + * Handles blob: and buffer: URLs by uploading them to EachLabs storage. + * Regular URLs and data URIs are passed through unchanged. + * + * @param client - The EachLabs client instance + * @param imageUrl - The image URL to process + * @param cesdk - Optional CE.SDK instance for buffer URL handling + * @returns The uploaded URL or the original URL if no upload was needed + */ +export async function uploadImageInputToEachLabsIfNeeded( + client: EachLabsClient, + imageUrl?: string, + cesdk?: CreativeEditorSDK +): Promise { + if (imageUrl == null) return undefined; + + // For blob URLs, fetch the blob and upload to storage + if (imageUrl.startsWith('blob:')) { + const mimeType = + cesdk != null + ? await cesdk.engine.editor.getMimeType(imageUrl) + : 'image/png'; + const imageUrlResponse = await fetch(imageUrl); + const imageUrlBlob = await imageUrlResponse.blob(); + const imageUrlFile = new File( + [imageUrlBlob], + `image.${mimeTypeToExtension(mimeType)}`, + { + type: mimeType + } + ); + return client.storage.upload(imageUrlFile); + } + + // For buffer URLs, extract data from CE.SDK and upload to storage + if (cesdk != null && imageUrl.startsWith('buffer:')) { + const mimeType = await cesdk.engine.editor.getMimeType(imageUrl); + const length = cesdk.engine.editor.getBufferLength(imageUrl); + const data = cesdk.engine.editor.getBufferData(imageUrl, 0, length); + // Create a new Uint8Array with a proper ArrayBuffer to avoid SharedArrayBuffer issues + const buffer = new Uint8Array(data); + const imageUrlFile = new File( + [buffer], + `image.${mimeTypeToExtension(mimeType)}`, + { + type: mimeType + } + ); + return client.storage.upload(imageUrlFile); + } + + // For data URIs and regular URLs, pass through unchanged + return imageUrl; +} + +/** + * Uploads an array of images to EachLabs storage if needed. + * Used for multi-image inputs like image-to-image generation. + * + * @param client - The EachLabs client instance + * @param imageUrls - Array of image URLs to process + * @param cesdk - Optional CE.SDK instance for buffer URL handling + * @returns Array of uploaded URLs + */ +export async function uploadImageArrayToEachLabsIfNeeded( + client: EachLabsClient, + imageUrls?: string[], + cesdk?: CreativeEditorSDK +): Promise { + if (imageUrls == null || !Array.isArray(imageUrls)) return undefined; + + const uploadedUrls = await Promise.all( + imageUrls.map((url) => + uploadImageInputToEachLabsIfNeeded(client, url, cesdk) + ) + ); + + return uploadedUrls.filter((url): url is string => url != null); +} + +// Keep legacy functions for backward compatibility but mark them as deprecated + +/** + * @deprecated Use uploadImageInputToEachLabsIfNeeded instead * Converts a blob: or buffer: URL to a data URI that EachLabs can accept */ export async function convertImageUrlForEachLabs( @@ -40,6 +126,7 @@ function blobToDataUri(blob: Blob): Promise { } /** + * @deprecated Use uploadImageArrayToEachLabsIfNeeded instead * Converts an array of blob:/buffer: URLs to data URIs for EachLabs. * Used for multi-image inputs like image-to-image generation. */ diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/createEachLabsClient.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/createEachLabsClient.ts index a96931cd..6e5a6b84 100644 --- a/packages/plugin-ai-video-generation-web/src/eachlabs/createEachLabsClient.ts +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/createEachLabsClient.ts @@ -1,36 +1,318 @@ -// EachLabs API client -// TODO: Implement actual API integration -/* eslint-disable @typescript-eslint/no-unused-vars */ +// EachLabs API client for video generation export interface EachLabsVideoInferenceParams { - // TODO: Define parameters based on EachLabs API + /** Model slug (e.g., 'kling-video') */ model: string; - prompt: string; - width?: number; - height?: number; - duration?: number; + /** Model version */ + version: string; + /** Input parameters for the model */ + input: Record; +} + +/** + * EachLabs prediction status + */ +type PredictionStatus = + | 'success' + | 'processing' + | 'starting' + | 'failed' + | 'cancelled'; + +/** + * EachLabs prediction response (from GET /v1/prediction/{id}) + */ +interface PredictionResponse { + id: string; + status: PredictionStatus; + input?: Record; + output?: string | string[] | Record; + error?: string; + logs?: string | null; + metrics?: { + predict_time?: number; + cost?: number; + }; + urls?: { + cancel?: string; + get?: string; + }; } export interface EachLabsVideoResult { - // TODO: Define result structure based on EachLabs API videoURL: string; } +/** + * Upload response from EachLabs storage API + */ +interface UploadResponse { + url: string; +} + +/** + * EachLabs storage interface for file uploads + */ +export interface EachLabsStorage { + /** + * Upload a file to EachLabs storage + * @param file - The file to upload + * @returns The URL of the uploaded file + */ + upload: (file: File) => Promise; +} + export interface EachLabsClient { videoInference: ( params: EachLabsVideoInferenceParams, abortSignal?: AbortSignal ) => Promise; + /** + * Storage API for uploading files + */ + storage: EachLabsStorage; } +/** + * Poll interval in milliseconds + */ +const POLL_INTERVAL = 3000; + +/** + * Maximum poll attempts (10 minutes total with 3s interval for video generation) + */ +const MAX_POLL_ATTEMPTS = 200; + +/** + * Creates an EachLabs API client for video generation + * + * @param proxyUrl - The proxy URL to use for API requests + * @param headers - Optional additional headers + * @returns EachLabs client instance + */ export function createEachLabsClient( - _proxyUrl: string, - _headers?: Record + proxyUrl: string, + headers?: Record ): EachLabsClient { + const baseHeaders = { + 'Content-Type': 'application/json', + ...headers + }; + + /** + * Create a prediction + */ + async function createPrediction( + params: EachLabsVideoInferenceParams, + abortSignal?: AbortSignal + ): Promise { + const response = await fetch(`${proxyUrl}/v1/prediction`, { + method: 'POST', + headers: baseHeaders, + body: JSON.stringify({ + model: params.model, + version: params.version, + input: params.input + }), + signal: abortSignal + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + throw new Error(`EachLabs API error: ${response.status} - ${errorText}`); + } + + const data = (await response.json()) as { predictionID?: string }; + if (!data.predictionID) { + throw new Error('EachLabs API did not return a prediction ID'); + } + + return data.predictionID; + } + + /** + * Get prediction status + */ + async function getPrediction( + predictionId: string, + abortSignal?: AbortSignal + ): Promise { + const response = await fetch(`${proxyUrl}/v1/prediction/${predictionId}`, { + method: 'GET', + headers: baseHeaders, + signal: abortSignal + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + throw new Error(`EachLabs API error: ${response.status} - ${errorText}`); + } + + return response.json() as Promise; + } + + /** + * Poll for prediction completion + */ + async function pollPrediction( + predictionId: string, + abortSignal?: AbortSignal + ): Promise { + let attempts = 0; + + // eslint-disable-next-line no-await-in-loop -- Polling requires sequential awaits + while (attempts < MAX_POLL_ATTEMPTS) { + if (abortSignal?.aborted) { + throw new Error('Request aborted'); + } + + // eslint-disable-next-line no-await-in-loop -- Polling requires sequential awaits + const prediction = await getPrediction(predictionId, abortSignal); + + if (prediction.status === 'success') { + return prediction; + } + + if (prediction.status === 'failed') { + throw new Error(prediction.error ?? 'Prediction failed'); + } + + if (prediction.status === 'cancelled') { + throw new Error('Prediction was cancelled'); + } + + // Wait before polling again + // eslint-disable-next-line no-await-in-loop -- Polling requires sequential awaits + await new Promise((resolve) => { + setTimeout(resolve, POLL_INTERVAL); + }); + attempts++; + } + + throw new Error('Prediction timed out'); + } + + /** + * Extract video URLs from prediction output + */ + function extractVideoUrls(output: PredictionResponse['output']): string[] { + if (!output) { + return []; + } + + // Output is a single URL string + if (typeof output === 'string') { + if (output.startsWith('http')) { + return [output]; + } + return []; + } + + // Output is an array of URLs + if (Array.isArray(output)) { + return output.filter( + (item): item is string => + typeof item === 'string' && item.startsWith('http') + ); + } + + // Output is an object - check common keys + const obj = output as Record; + + // Try video/videos keys first + if (typeof obj.video === 'string' && obj.video.startsWith('http')) { + return [obj.video]; + } + + if (Array.isArray(obj.videos) && obj.videos.length > 0) { + return obj.videos.filter( + (item): item is string => + typeof item === 'string' && item.startsWith('http') + ); + } + + // Check for other common output formats + const possibleKeys = ['url', 'video_url', 'result', 'output']; + for (const key of possibleKeys) { + const value = obj[key]; + if (typeof value === 'string' && value.startsWith('http')) { + return [value]; + } + if (Array.isArray(value)) { + const urls = value.filter( + (v): v is string => typeof v === 'string' && v.startsWith('http') + ); + if (urls.length > 0) { + return urls; + } + } + } + + return []; + } + + /** + * Upload a file to EachLabs storage + */ + async function uploadFile(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + + // Use the storage upload endpoint via proxy + const uploadUrl = `${proxyUrl}/api/v1/upload`; + + const response = await fetch(uploadUrl, { + method: 'POST', + headers: { + // Don't set Content-Type header - let the browser set it with the boundary + ...Object.fromEntries( + Object.entries(baseHeaders).filter( + ([key]) => key.toLowerCase() !== 'content-type' + ) + ) + }, + body: formData + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'Unknown error'); + throw new Error( + `EachLabs storage upload error: ${response.status} - ${errorText}` + ); + } + + const data = (await response.json()) as UploadResponse; + if (!data.url) { + throw new Error('EachLabs storage did not return a URL'); + } + + return data.url; + } + return { - videoInference: async (): Promise => { - // TODO: Implement actual API call - throw new Error('EachLabs client not yet implemented'); + storage: { + upload: uploadFile + }, + videoInference: async ( + params: EachLabsVideoInferenceParams, + abortSignal?: AbortSignal + ): Promise => { + // Create prediction + const predictionId = await createPrediction(params, abortSignal); + + // Poll for completion + const prediction = await pollPrediction(predictionId, abortSignal); + + // Extract video URLs + const videoUrls = extractVideoUrls(prediction.output); + + if (videoUrls.length === 0) { + // eslint-disable-next-line no-console + console.error('EachLabs response:', prediction); + throw new Error('No videos found in EachLabs response'); + } + + return videoUrls.map((url) => ({ videoURL: url })); } }; } diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/utils.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/utils.ts new file mode 100644 index 00000000..0d34daca --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/utils.ts @@ -0,0 +1,84 @@ +import { mimeTypeToExtension } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +import { EachLabsClient } from './createEachLabsClient'; + +/** + * Uploads an image to EachLabs storage if needed. + * Handles blob: and buffer: URLs by uploading them to EachLabs storage. + * Regular URLs and data URIs are passed through unchanged. + * + * @param client - The EachLabs client instance + * @param imageUrl - The image URL to process + * @param cesdk - Optional CE.SDK instance for buffer URL handling + * @returns The uploaded URL or the original URL if no upload was needed + */ +export async function uploadImageInputToEachLabsIfNeeded( + client: EachLabsClient, + imageUrl?: string, + cesdk?: CreativeEditorSDK +): Promise { + if (imageUrl == null) return undefined; + + // For blob URLs, fetch the blob and upload to storage + if (imageUrl.startsWith('blob:')) { + const mimeType = + cesdk != null + ? await cesdk.engine.editor.getMimeType(imageUrl) + : 'image/png'; + const imageUrlResponse = await fetch(imageUrl); + const imageUrlBlob = await imageUrlResponse.blob(); + const imageUrlFile = new File( + [imageUrlBlob], + `image.${mimeTypeToExtension(mimeType)}`, + { + type: mimeType + } + ); + return client.storage.upload(imageUrlFile); + } + + // For buffer URLs, extract data from CE.SDK and upload to storage + if (cesdk != null && imageUrl.startsWith('buffer:')) { + const mimeType = await cesdk.engine.editor.getMimeType(imageUrl); + const length = cesdk.engine.editor.getBufferLength(imageUrl); + const data = cesdk.engine.editor.getBufferData(imageUrl, 0, length); + // Create a new Uint8Array with a proper ArrayBuffer to avoid SharedArrayBuffer issues + const buffer = new Uint8Array(data); + const imageUrlFile = new File( + [buffer], + `image.${mimeTypeToExtension(mimeType)}`, + { + type: mimeType + } + ); + return client.storage.upload(imageUrlFile); + } + + // For data URIs and regular URLs, pass through unchanged + return imageUrl; +} + +/** + * Uploads an array of images to EachLabs storage if needed. + * Used for multi-image inputs like image-to-video generation. + * + * @param client - The EachLabs client instance + * @param imageUrls - Array of image URLs to process + * @param cesdk - Optional CE.SDK instance for buffer URL handling + * @returns Array of uploaded URLs + */ +export async function uploadImageArrayToEachLabsIfNeeded( + client: EachLabsClient, + imageUrls?: string[], + cesdk?: CreativeEditorSDK +): Promise { + if (imageUrls == null || !Array.isArray(imageUrls)) return undefined; + + const uploadedUrls = await Promise.all( + imageUrls.map((url) => + uploadImageInputToEachLabsIfNeeded(client, url, cesdk) + ) + ); + + return uploadedUrls.filter((url): url is string => url != null); +} From 84a55e6366c627eb55c916fd66a737b7f1ad62f9 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2025 16:09:17 +0100 Subject: [PATCH 13/20] feat(eachlabs): implement Flux 2 text-to-image and image-to-image providers Add standard Flux 2 providers from Black Forest Labs with 6 image size options and full quick actions support for the image-to-image variant. --- examples/ai/src/eachlabsProviders.ts | 8 + .../plugin-ai-image-generation-web/README.md | 53 ++++++- .../src/eachlabs/Flux2.image2image.json | 38 +++++ .../src/eachlabs/Flux2.image2image.ts | 147 ++++++++++++++++++ .../src/eachlabs/Flux2.text2image.json | 60 +++++++ .../src/eachlabs/Flux2.text2image.ts | 78 ++++++++++ .../src/eachlabs/index.ts | 6 + specs/providers/eachlabs/providers.md | 24 +-- 8 files changed, 397 insertions(+), 17 deletions(-) create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.image2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.image2image.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.text2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.text2image.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index b70942a9..6ffae8f0 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -21,6 +21,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl }), + EachLabsImage.Flux2.Text2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl + }), EachLabsImage.OpenAIGptImage.Text2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl @@ -43,6 +47,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl }), + EachLabsImage.Flux2.Image2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl + }), EachLabsImage.OpenAIGptImage.Image2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl diff --git a/packages/plugin-ai-image-generation-web/README.md b/packages/plugin-ai-image-generation-web/README.md index 411ee1d4..8d39bb16 100644 --- a/packages/plugin-ai-image-generation-web/README.md +++ b/packages/plugin-ai-image-generation-web/README.md @@ -1143,7 +1143,37 @@ Key features: - Supports up to 4 reference images - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page -#### 26. OpenAI GPT Image (Text-to-Image) via EachLabs +#### 26. Flux2 (Text-to-Image) via EachLabs + +Black Forest Labs' Flux 2 standard text-to-image generation via EachLabs: + +```typescript +text2image: EachLabsImage.Flux2.Text2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- Standard Flux 2 image generation from Black Forest Labs +- 6 image size options (square HD, square, portrait 4:3/16:9, landscape 4:3/16:9) +- Good quality output with faster generation + +#### 27. Flux2 Edit (Image-to-Image) via EachLabs + +Black Forest Labs' Flux 2 image transformation via EachLabs: + +```typescript +image2image: EachLabsImage.Flux2.Image2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- Standard Flux 2 image transformation +- Supports up to 3 reference images +- Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page + +#### 28. OpenAI GPT Image (Text-to-Image) via EachLabs OpenAI's GPT Image text-to-image generation via EachLabs: @@ -1158,7 +1188,7 @@ Key features: - 3 image size options (Square, Landscape, Portrait) - Professional-grade output quality -#### 27. OpenAI GPT Image Edit (Image-to-Image) via EachLabs +#### 29. OpenAI GPT Image Edit (Image-to-Image) via EachLabs OpenAI's GPT Image image transformation via EachLabs: @@ -1173,7 +1203,7 @@ Key features: - Supports up to 16 reference images - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page -#### 28. Seedream v4.5 (Text-to-Image) via EachLabs +#### 30. Seedream v4.5 (Text-to-Image) via EachLabs ByteDance's Seedream v4.5 text-to-image generation via EachLabs: @@ -1188,7 +1218,7 @@ Key features: - 6 image size options (square HD, square, portrait 4:3/16:9, landscape 4:3/16:9) - Improved facial details and text generation over v4.0 -#### 29. Seedream v4.5 Edit (Image-to-Image) via EachLabs +#### 31. Seedream v4.5 Edit (Image-to-Image) via EachLabs ByteDance's Seedream v4.5 image transformation via EachLabs: @@ -1204,7 +1234,7 @@ Key features: - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page - Improved facial details and text generation over v4.0 -#### 30. Gemini 3 Pro Image (Text-to-Image) via EachLabs +#### 32. Gemini 3 Pro Image (Text-to-Image) via EachLabs Google's Gemini 3 Pro text-to-image generation via EachLabs: @@ -1220,7 +1250,7 @@ Key features: - Multiple resolution options (1K, 2K, 4K) - Professional-grade output quality -#### 31. Gemini 3 Pro Image Edit (Image-to-Image) via EachLabs +#### 33. Gemini 3 Pro Image Edit (Image-to-Image) via EachLabs Google's Gemini 3 Pro image transformation via EachLabs: @@ -1823,6 +1853,13 @@ EachLabsImage.Flux2Pro.Text2Image(config: EachLabsProviderConfiguration) EachLabsImage.Flux2Pro.Image2Image(config: EachLabsProviderConfiguration) ``` +#### Flux2.Text2Image / Flux2.Image2Image + +```typescript +EachLabsImage.Flux2.Text2Image(config: EachLabsProviderConfiguration) +EachLabsImage.Flux2.Image2Image(config: EachLabsProviderConfiguration) +``` + #### OpenAIGptImage.Text2Image / OpenAIGptImage.Image2Image ```typescript @@ -1960,6 +1997,8 @@ const myImageProvider = { - EachLabs NanoBananaPro.Image2Image: `ly.img.ai.eachlabs/nano-banana-pro/edit` - EachLabs Flux2Pro.Text2Image: `ly.img.ai.eachlabs/flux-2-pro` - EachLabs Flux2Pro.Image2Image: `ly.img.ai.eachlabs/flux-2-pro/edit` + - EachLabs Flux2.Text2Image: `ly.img.ai.eachlabs/flux-2` + - EachLabs Flux2.Image2Image: `ly.img.ai.eachlabs/flux-2/edit` - EachLabs OpenAIGptImage.Text2Image: `ly.img.ai.eachlabs/openai-gpt-image` - EachLabs OpenAIGptImage.Image2Image: `ly.img.ai.eachlabs/openai-gpt-image/edit` - EachLabs Seedream45.Text2Image: `ly.img.ai.eachlabs/seedream-v4.5` @@ -2003,6 +2042,8 @@ Generated images are automatically stored in asset sources with the following ID - EachLabs NanoBananaPro.Image2Image: `eachlabs/nano-banana-pro/edit.history` - EachLabs Flux2Pro.Text2Image: `eachlabs/flux-2-pro.history` - EachLabs Flux2Pro.Image2Image: `eachlabs/flux-2-pro/edit.history` +- EachLabs Flux2.Text2Image: `eachlabs/flux-2.history` +- EachLabs Flux2.Image2Image: `eachlabs/flux-2/edit.history` - EachLabs OpenAIGptImage.Text2Image: `eachlabs/openai-gpt-image.history` - EachLabs OpenAIGptImage.Image2Image: `eachlabs/openai-gpt-image/edit.history` - EachLabs Seedream45.Text2Image: `eachlabs/seedream-v4.5.history` diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.image2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.image2image.json new file mode 100644 index 00000000..5dc8eb2d --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.image2image.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Flux 2 Edit API", + "version": "1.0.0", + "description": "Flux 2 image-to-image editing via EachLabs" + }, + "components": { + "schemas": { + "Flux2Image2ImageInput": { + "title": "Flux2Image2ImageInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "description": "Image to transform", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image editing", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + } + }, + "x-fal-order-properties": ["image_url", "prompt"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.image2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.image2image.ts new file mode 100644 index 00000000..15be884e --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.image2image.ts @@ -0,0 +1,147 @@ +import { + type Provider, + type ImageOutput, + CommonProperties, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Flux2Image2ImageSchema from './Flux2.image2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for Flux 2 image-to-image + */ +export type Flux2Image2ImageInput = { + prompt: string; + image_url?: string; + image_urls?: string[]; +}; + +/** + * Flux 2 Edit - Transform images using AI via EachLabs + * + * Features: + * - Standard Flux 2 image transformation + * - Supports up to 3 reference images + */ +export function Flux2Image2Image( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/flux-2/edit'; + + // Set translations for the panel + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Edit', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createImageProvider( + { + modelSlug: 'flux-2-edit', + modelVersion: '0.0.1', + providerId, + name: 'Flux 2 Edit', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Flux2Image2ImageSchema, + inputReference: '#/components/schemas/Flux2Image2ImageInput', + cesdk, + middleware: config.middlewares ?? [], + renderCustomProperty: CommonProperties.ImageUrl(providerId, { + cesdk + }), + supportedQuickActions: { + 'ly.img.editImage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.swapBackground': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.styleTransfer': { + mapInput: (input) => ({ + prompt: input.style, + image_url: input.uri + }) + }, + 'ly.img.artistTransfer': { + mapInput: (input) => ({ + prompt: input.artist, + image_url: input.uri + }) + }, + 'ly.img.createVariant': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPageWithPrompt': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.combineImages': { + mapInput: (input) => ({ + prompt: input.prompt, + image_urls: input.uris + }) + } + }, + getBlockInput: async (input) => { + // Get first image URL from either single or array input + const firstImageUrl = input.image_url ?? input.image_urls?.[0]; + if (firstImageUrl == null) { + throw new Error('No image URL provided'); + } + + // Get dimensions from input image for UI placeholder + const { width, height } = await getImageDimensionsFromURL( + firstImageUrl, + cesdk.engine + ); + return Promise.resolve({ + image: { + width, + height + } + }); + }, + mapInput: (input) => { + // Map to EachLabs API format + // flux-2-edit uses image_urls (array) for image input + const imageUrls = + input.image_urls ?? (input.image_url ? [input.image_url] : []); + return { + prompt: input.prompt, + image_urls: imageUrls + }; + } + }, + config + ); + }; +} + +export default Flux2Image2Image; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.text2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.text2image.json new file mode 100644 index 00000000..6738ddae --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.text2image.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Flux 2 API", + "version": "1.0.0", + "description": "Flux 2 text-to-image generation via EachLabs" + }, + "components": { + "schemas": { + "Flux2Input": { + "title": "Flux2Input", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image generation", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "image_size": { + "title": "Format", + "type": "string", + "enum": [ + "square_hd", + "square", + "portrait_4_3", + "portrait_16_9", + "landscape_4_3", + "landscape_16_9" + ], + "default": "landscape_4_3", + "description": "Output image format", + "x-imgly-enum-labels": { + "square_hd": "Square HD (1:1)", + "square": "Square (1:1)", + "portrait_4_3": "Portrait (3:4)", + "portrait_16_9": "Portrait (9:16)", + "landscape_4_3": "Landscape (4:3)", + "landscape_16_9": "Landscape (16:9)" + }, + "x-imgly-enum-icons": { + "square_hd": "@imgly/plugin/formats/ratio1by1", + "square": "@imgly/plugin/formats/ratio1by1", + "portrait_4_3": "@imgly/plugin/formats/ratio3by4", + "portrait_16_9": "@imgly/plugin/formats/ratio9by16", + "landscape_4_3": "@imgly/plugin/formats/ratio4by3", + "landscape_16_9": "@imgly/plugin/formats/ratio16by9" + } + } + }, + "x-fal-order-properties": ["prompt", "image_size"], + "required": ["prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.text2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.text2image.ts new file mode 100644 index 00000000..8e400c84 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2.text2image.ts @@ -0,0 +1,78 @@ +import { + type Provider, + type ImageOutput, + addIconSetOnce +} from '@imgly/plugin-ai-generation-web'; +import { Icons } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Flux2Schema from './Flux2.text2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +// Map EachLabs image_size enum to dimensions +const IMAGE_SIZE_MAP: Record = { + square_hd: { width: 1024, height: 1024 }, + square: { width: 512, height: 512 }, + portrait_4_3: { width: 896, height: 1152 }, + portrait_16_9: { width: 768, height: 1344 }, + landscape_4_3: { width: 1152, height: 896 }, + landscape_16_9: { width: 1344, height: 768 } +}; + +/** + * Input interface for Flux 2 text-to-image + */ +export type Flux2Input = { + prompt: string; + image_size?: + | 'square_hd' + | 'square' + | 'portrait_4_3' + | 'portrait_16_9' + | 'landscape_4_3' + | 'landscape_16_9'; +}; + +/** + * Flux 2 - Text-to-image generation via EachLabs + * + * Features: + * - 6 image size options + * - Standard Flux 2 generation from Black Forest Labs + */ +export function Flux2( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + // Add aspect ratio icons + addIconSetOnce(cesdk, '@imgly/plugin/formats', Icons.Formats); + + return createImageProvider( + { + modelSlug: 'flux-2', + modelVersion: '0.0.1', + providerId: 'eachlabs/flux-2', + name: 'Flux 2', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Flux2Schema, + inputReference: '#/components/schemas/Flux2Input', + cesdk, + getImageSize: (input) => + IMAGE_SIZE_MAP[input.image_size ?? 'landscape_4_3'] ?? { + width: 1152, + height: 896 + }, + mapInput: (input) => ({ + prompt: input.prompt, + image_size: input.image_size ?? 'landscape_4_3' + }) + }, + config + ); + }; +} + +export default Flux2; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts index 16366b58..81931f20 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts @@ -5,6 +5,8 @@ import { NanoBananaPro as NanoBananaProText2Image } from './NanoBananaPro.text2i import { NanoBananaProImage2Image } from './NanoBananaPro.image2image'; import { Flux2Pro as Flux2ProText2Image } from './Flux2Pro.text2image'; import { Flux2ProImage2Image } from './Flux2Pro.image2image'; +import { Flux2 as Flux2Text2Image } from './Flux2.text2image'; +import { Flux2Image2Image } from './Flux2.image2image'; import { OpenAIImageText2Image } from './OpenAIImage.text2image'; import { OpenAIImageImage2Image } from './OpenAIImage.image2image'; import { Seedream45 as Seedream45Text2Image } from './Seedream45.text2image'; @@ -21,6 +23,10 @@ const EachLabs = { Text2Image: Flux2ProText2Image, Image2Image: Flux2ProImage2Image }, + Flux2: { + Text2Image: Flux2Text2Image, + Image2Image: Flux2Image2Image + }, OpenAIGptImage: { Text2Image: OpenAIImageText2Image, Image2Image: OpenAIImageImage2Image diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index dbaf45e5..f2304e41 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -32,7 +32,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Slug | Title | Output Type | Status | Notes | |------|-------|-------------|--------|-------| | flux-2-pro | Flux 2 Pro | image | implemented | Latest Flux, high quality | -| flux-2 | Flux 2 | array | planned | Standard Flux 2 | +| flux-2 | Flux 2 | array | implemented | Standard Flux 2 | | flux-2-flex | Flux 2 \| Flex | image | planned | Prompt expansion enabled | | gemini-3-pro-image-preview | Gemini 3 \| Pro \| Image Preview | array | implemented | Google Gemini image generation | | bytedance-seedream-v4-5-text-to-image | Bytedance \| Seedream \| v4.5 \| Text to Image | array | implemented | Latest Seedream | @@ -44,7 +44,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Slug | Title | Output Type | Status | Notes | |------|-------|-------------|--------|-------| | flux-2-pro-edit | Flux 2 Pro \| Edit | image | implemented | Edit with Flux 2 Pro | -| flux-2-edit | Flux 2 \| Edit | array | planned | Standard Flux 2 edit | +| flux-2-edit | Flux 2 \| Edit | array | implemented | Standard Flux 2 edit | | flux-2-flex-edit | Flux 2 \| Flex \| Edit | image | planned | Flex edit variant | | gemini-3-pro-image-preview-edit | Gemini 3 Pro \| Image Edit | array | implemented | Google Gemini edit | | bytedance-seedream-v4-5-edit | Bytedance \| Seedream \| v4.5 \| Edit | array | implemented | Seedream edit | @@ -72,27 +72,29 @@ This document tracks EachLabs AI models for implementation, focusing on image an ### Implemented -**Image Generation (T2I):** 5 models +**Image Generation (T2I):** 6 models - nano-banana-pro - flux-2-pro +- flux-2 - openai-image-generation - bytedance-seedream-v4-5-text-to-image - gemini-3-pro-image-preview -**Image Generation (I2I/Edit):** 5 models +**Image Generation (I2I/Edit):** 6 models - nano-banana-pro-edit - flux-2-pro-edit +- flux-2-edit - openai-image-edit - bytedance-seedream-v4-5-edit - gemini-3-pro-image-preview-edit ### Planned for Implementation -**Image Generation (T2I):** 2 models -- flux-2, flux-2-flex +**Image Generation (T2I):** 1 model +- flux-2-flex -**Image Generation (I2I/Edit):** 2 models -- flux-2-edit, flux-2-flex-edit +**Image Generation (I2I/Edit):** 1 model +- flux-2-flex-edit **Video Generation (T2V):** 2 models - kling-v2-6-pro-text-to-video @@ -107,8 +109,8 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Category | Implemented | Planned | Total | |----------|-------------|---------|-------| -| Image T2I | 5 | 2 | 7 | -| Image I2I | 5 | 2 | 7 | +| Image T2I | 6 | 1 | 7 | +| Image I2I | 6 | 1 | 7 | | Video T2V | 0 | 2 | 2 | | Video I2V | 0 | 3 | 3 | -| **Total** | **10** | **9** | **19** | +| **Total** | **12** | **7** | **19** | From 93ee5836a083b9e6ecf4dfce6704a80c5553060b Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2025 17:24:07 +0100 Subject: [PATCH 14/20] feat(eachlabs): add translations for Flux 2 and Seedream v4.5 providers Add missing i18n translations for the EachLabs Flux 2 and Seedream v4.5 text-to-image and image-to-image providers including prompt, format (image_size), and source image property labels. --- .../translations.json | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/plugin-ai-image-generation-web/translations.json b/packages/plugin-ai-image-generation-web/translations.json index 2cb7e4e0..2b1f5faf 100644 --- a/packages/plugin-ai-image-generation-web/translations.json +++ b/packages/plugin-ai-image-generation-web/translations.json @@ -488,6 +488,34 @@ "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image/edit.property.image_url": "Source Image", "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image/edit.property.prompt": "Prompt", - "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image/edit.property.prompt.placeholder": "Describe the changes..." + "ly.img.plugin-ai-image-generation-web.eachlabs/gemini-3-pro-image/edit.property.prompt.placeholder": "Describe the changes...", + + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2.property.prompt.placeholder": "Describe your image...", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2.property.image_size": "Format", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2.property.image_size.square_hd": "Square HD (1:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2.property.image_size.square": "Square (1:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2.property.image_size.portrait_4_3": "Portrait (3:4)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2.property.image_size.portrait_16_9": "Portrait (9:16)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2.property.image_size.landscape_4_3": "Landscape (4:3)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2.property.image_size.landscape_16_9": "Landscape (16:9)", + + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2/edit.property.image_url": "Source Image", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2/edit.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2/edit.property.prompt.placeholder": "Describe the changes...", + + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.prompt.placeholder": "Describe your image...", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.image_size": "Format", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.image_size.square_hd": "Square HD (1:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.image_size.square": "Square (1:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.image_size.portrait_4_3": "Portrait (3:4)", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.image_size.portrait_16_9": "Portrait (9:16)", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.image_size.landscape_4_3": "Landscape (4:3)", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.image_size.landscape_16_9": "Landscape (16:9)", + + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5/edit.property.image_url": "Source Image", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5/edit.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5/edit.property.prompt.placeholder": "Describe the changes..." } } \ No newline at end of file From 96a4cdac065fc87d544db0529136bc1a41133313 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2025 17:24:14 +0100 Subject: [PATCH 15/20] fix(examples): add eachlabs to provider detection in translation test Add .eachlabs/ pattern to the provider-specific translation detection in testTranslations utility so EachLabs provider translations show the @ prefix instead of & when testing translations. --- examples/ai/src/utils/testTranslations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ai/src/utils/testTranslations.ts b/examples/ai/src/utils/testTranslations.ts index 04f651c8..199fb381 100644 --- a/examples/ai/src/utils/testTranslations.ts +++ b/examples/ai/src/utils/testTranslations.ts @@ -24,7 +24,7 @@ export function testAllTranslations(cesdk: CreativeEditorSDK) { // Process image generation translations (provider-specific) with @ prefix Object.entries(imageTranslations.en).forEach(([key, value]) => { // Check if it's a provider-specific translation - if (key.includes('.fal-ai/') || key.includes('.open-ai/') || key.includes('.runware/')) { + if (key.includes('.fal-ai/') || key.includes('.open-ai/') || key.includes('.runware/') || key.includes('.eachlabs/')) { allTranslations[key] = `@${value}`; } else { // Generic property that might be redefined @@ -34,7 +34,7 @@ export function testAllTranslations(cesdk: CreativeEditorSDK) { // Process video generation translations (provider-specific) with @ prefix Object.entries(videoTranslations.en).forEach(([key, value]) => { - if (key.includes('.fal-ai/') || key.includes('.runware/')) { + if (key.includes('.fal-ai/') || key.includes('.runware/') || key.includes('.eachlabs/')) { allTranslations[key] = `@${value}`; } else { allTranslations[key] = `&${value}`; From bafe59b0afd3358e6286492a57e93904d240ad21 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2025 17:48:44 +0100 Subject: [PATCH 16/20] feat(eachlabs): implement Flux 2 Flex text-to-image and image-to-image providers Add Flux 2 Flex providers with automatic prompt expansion capability: - Text-to-image with 6 image size options - Image-to-image with auto image size detection - Full quick actions support for I2I variant --- examples/ai/src/eachlabsProviders.ts | 8 + .../plugin-ai-image-generation-web/README.md | 54 ++++++- .../src/eachlabs/Flux2Flex.image2image.json | 38 +++++ .../src/eachlabs/Flux2Flex.image2image.ts | 152 ++++++++++++++++++ .../src/eachlabs/Flux2Flex.text2image.json | 60 +++++++ .../src/eachlabs/Flux2Flex.text2image.ts | 81 ++++++++++ .../src/eachlabs/index.ts | 6 + .../translations.json | 14 ++ specs/providers/eachlabs/providers.md | 22 ++- 9 files changed, 416 insertions(+), 19 deletions(-) create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.image2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.image2image.ts create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.text2image.json create mode 100644 packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.text2image.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index 6ffae8f0..6fe10f2d 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -25,6 +25,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl }), + EachLabsImage.Flux2Flex.Text2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl + }), EachLabsImage.OpenAIGptImage.Text2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl @@ -51,6 +55,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl }), + EachLabsImage.Flux2Flex.Image2Image({ + middlewares: [imageRateLimitMiddleware, errorMiddleware], + proxyUrl + }), EachLabsImage.OpenAIGptImage.Image2Image({ middlewares: [imageRateLimitMiddleware, errorMiddleware], proxyUrl diff --git a/packages/plugin-ai-image-generation-web/README.md b/packages/plugin-ai-image-generation-web/README.md index 8d39bb16..1887f298 100644 --- a/packages/plugin-ai-image-generation-web/README.md +++ b/packages/plugin-ai-image-generation-web/README.md @@ -1173,7 +1173,38 @@ Key features: - Supports up to 3 reference images - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page -#### 28. OpenAI GPT Image (Text-to-Image) via EachLabs +#### 28. Flux2Flex (Text-to-Image) via EachLabs + +Black Forest Labs' Flux 2 Flex text-to-image generation with automatic prompt expansion via EachLabs: + +```typescript +text2image: EachLabsImage.Flux2Flex.Text2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- Automatic prompt expansion using the model's knowledge +- 6 image size options (square HD, square, portrait 4:3/16:9, landscape 4:3/16:9) +- High-quality Flux 2 generation from Black Forest Labs + +#### 29. Flux2Flex Edit (Image-to-Image) via EachLabs + +Black Forest Labs' Flux 2 Flex image transformation with automatic prompt expansion via EachLabs: + +```typescript +image2image: EachLabsImage.Flux2Flex.Image2Image({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy' +}) +``` + +Key features: +- Automatic prompt expansion using the model's knowledge +- Automatic image size detection ("auto" mode) +- Supports up to 10 reference images +- Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page + +#### 30. OpenAI GPT Image (Text-to-Image) via EachLabs OpenAI's GPT Image text-to-image generation via EachLabs: @@ -1188,7 +1219,7 @@ Key features: - 3 image size options (Square, Landscape, Portrait) - Professional-grade output quality -#### 29. OpenAI GPT Image Edit (Image-to-Image) via EachLabs +#### 31. OpenAI GPT Image Edit (Image-to-Image) via EachLabs OpenAI's GPT Image image transformation via EachLabs: @@ -1203,7 +1234,7 @@ Key features: - Supports up to 16 reference images - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page -#### 30. Seedream v4.5 (Text-to-Image) via EachLabs +#### 32. Seedream v4.5 (Text-to-Image) via EachLabs ByteDance's Seedream v4.5 text-to-image generation via EachLabs: @@ -1218,7 +1249,7 @@ Key features: - 6 image size options (square HD, square, portrait 4:3/16:9, landscape 4:3/16:9) - Improved facial details and text generation over v4.0 -#### 31. Seedream v4.5 Edit (Image-to-Image) via EachLabs +#### 33. Seedream v4.5 Edit (Image-to-Image) via EachLabs ByteDance's Seedream v4.5 image transformation via EachLabs: @@ -1234,7 +1265,7 @@ Key features: - Full canvas quick actions support: edit image, swap background, style transfer, artist styles, create variants, combine images, remix page - Improved facial details and text generation over v4.0 -#### 32. Gemini 3 Pro Image (Text-to-Image) via EachLabs +#### 34. Gemini 3 Pro Image (Text-to-Image) via EachLabs Google's Gemini 3 Pro text-to-image generation via EachLabs: @@ -1250,7 +1281,7 @@ Key features: - Multiple resolution options (1K, 2K, 4K) - Professional-grade output quality -#### 33. Gemini 3 Pro Image Edit (Image-to-Image) via EachLabs +#### 35. Gemini 3 Pro Image Edit (Image-to-Image) via EachLabs Google's Gemini 3 Pro image transformation via EachLabs: @@ -1860,6 +1891,13 @@ EachLabsImage.Flux2.Text2Image(config: EachLabsProviderConfiguration) EachLabsImage.Flux2.Image2Image(config: EachLabsProviderConfiguration) ``` +#### Flux2Flex.Text2Image / Flux2Flex.Image2Image + +```typescript +EachLabsImage.Flux2Flex.Text2Image(config: EachLabsProviderConfiguration) +EachLabsImage.Flux2Flex.Image2Image(config: EachLabsProviderConfiguration) +``` + #### OpenAIGptImage.Text2Image / OpenAIGptImage.Image2Image ```typescript @@ -1999,6 +2037,8 @@ const myImageProvider = { - EachLabs Flux2Pro.Image2Image: `ly.img.ai.eachlabs/flux-2-pro/edit` - EachLabs Flux2.Text2Image: `ly.img.ai.eachlabs/flux-2` - EachLabs Flux2.Image2Image: `ly.img.ai.eachlabs/flux-2/edit` + - EachLabs Flux2Flex.Text2Image: `ly.img.ai.eachlabs/flux-2-flex` + - EachLabs Flux2Flex.Image2Image: `ly.img.ai.eachlabs/flux-2-flex/edit` - EachLabs OpenAIGptImage.Text2Image: `ly.img.ai.eachlabs/openai-gpt-image` - EachLabs OpenAIGptImage.Image2Image: `ly.img.ai.eachlabs/openai-gpt-image/edit` - EachLabs Seedream45.Text2Image: `ly.img.ai.eachlabs/seedream-v4.5` @@ -2044,6 +2084,8 @@ Generated images are automatically stored in asset sources with the following ID - EachLabs Flux2Pro.Image2Image: `eachlabs/flux-2-pro/edit.history` - EachLabs Flux2.Text2Image: `eachlabs/flux-2.history` - EachLabs Flux2.Image2Image: `eachlabs/flux-2/edit.history` +- EachLabs Flux2Flex.Text2Image: `eachlabs/flux-2-flex.history` +- EachLabs Flux2Flex.Image2Image: `eachlabs/flux-2-flex/edit.history` - EachLabs OpenAIGptImage.Text2Image: `eachlabs/openai-gpt-image.history` - EachLabs OpenAIGptImage.Image2Image: `eachlabs/openai-gpt-image/edit.history` - EachLabs Seedream45.Text2Image: `eachlabs/seedream-v4.5.history` diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.image2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.image2image.json new file mode 100644 index 00000000..d0865386 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.image2image.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Flux 2 Flex Edit API", + "version": "1.0.0", + "description": "Flux 2 Flex image-to-image editing via EachLabs with prompt expansion" + }, + "components": { + "schemas": { + "Flux2FlexImage2ImageInput": { + "title": "Flux2FlexImage2ImageInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "description": "Image to transform", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image editing", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + } + }, + "x-fal-order-properties": ["image_url", "prompt"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.image2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.image2image.ts new file mode 100644 index 00000000..2c3b5059 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.image2image.ts @@ -0,0 +1,152 @@ +import { + type Provider, + type ImageOutput, + CommonProperties, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Flux2FlexImage2ImageSchema from './Flux2Flex.image2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for Flux 2 Flex image-to-image + */ +export type Flux2FlexImage2ImageInput = { + prompt: string; + image_url?: string; + image_urls?: string[]; +}; + +/** + * Flux 2 Flex Edit - Transform images using AI via EachLabs with prompt expansion + * + * Features: + * - Automatic prompt expansion using the model's knowledge + * - Automatic image size detection ("auto" mode) + * - Supports up to 10 reference images + */ +export function Flux2FlexImage2Image( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/flux-2-flex/edit'; + + // Set translations for the panel + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Edit', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createImageProvider( + { + modelSlug: 'flux-2-flex-edit', + modelVersion: '0.0.1', + providerId, + name: 'Flux 2 Flex Edit', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Flux2FlexImage2ImageSchema, + inputReference: '#/components/schemas/Flux2FlexImage2ImageInput', + cesdk, + middleware: config.middlewares ?? [], + renderCustomProperty: CommonProperties.ImageUrl(providerId, { + cesdk + }), + supportedQuickActions: { + 'ly.img.editImage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.swapBackground': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.styleTransfer': { + mapInput: (input) => ({ + prompt: input.style, + image_url: input.uri + }) + }, + 'ly.img.artistTransfer': { + mapInput: (input) => ({ + prompt: input.artist, + image_url: input.uri + }) + }, + 'ly.img.createVariant': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPage': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.remixPageWithPrompt': { + mapInput: (input) => ({ + prompt: input.prompt, + image_url: input.uri + }) + }, + 'ly.img.combineImages': { + mapInput: (input) => ({ + prompt: input.prompt, + image_urls: input.uris + }) + } + }, + getBlockInput: async (input) => { + // Get first image URL from either single or array input + const firstImageUrl = input.image_url ?? input.image_urls?.[0]; + if (firstImageUrl == null) { + throw new Error('No image URL provided'); + } + + // Get dimensions from input image for UI placeholder + const { width, height } = await getImageDimensionsFromURL( + firstImageUrl, + cesdk.engine + ); + return Promise.resolve({ + image: { + width, + height + } + }); + }, + mapInput: (input) => { + // Map to EachLabs API format + // flux-2-flex-edit uses image_urls (array) for image input + const imageUrls = + input.image_urls ?? (input.image_url ? [input.image_url] : []); + return { + prompt: input.prompt, + image_urls: imageUrls, + // Use auto image size to let the model determine output dimensions + image_size: 'auto', + // Enable prompt expansion by default (key Flex feature) + enable_prompt_expansion: true + }; + } + }, + config + ); + }; +} + +export default Flux2FlexImage2Image; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.text2image.json b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.text2image.json new file mode 100644 index 00000000..0c2dd7fa --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.text2image.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Flux 2 Flex API", + "version": "1.0.0", + "description": "Flux 2 Flex text-to-image generation via EachLabs with prompt expansion" + }, + "components": { + "schemas": { + "Flux2FlexInput": { + "title": "Flux2FlexInput", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Text description for image generation", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "image_size": { + "title": "Format", + "type": "string", + "enum": [ + "square_hd", + "square", + "portrait_4_3", + "portrait_16_9", + "landscape_4_3", + "landscape_16_9" + ], + "default": "landscape_4_3", + "description": "Output image format", + "x-imgly-enum-labels": { + "square_hd": "Square HD (1:1)", + "square": "Square (1:1)", + "portrait_4_3": "Portrait (3:4)", + "portrait_16_9": "Portrait (9:16)", + "landscape_4_3": "Landscape (4:3)", + "landscape_16_9": "Landscape (16:9)" + }, + "x-imgly-enum-icons": { + "square_hd": "@imgly/plugin/formats/ratio1by1", + "square": "@imgly/plugin/formats/ratio1by1", + "portrait_4_3": "@imgly/plugin/formats/ratio3by4", + "portrait_16_9": "@imgly/plugin/formats/ratio9by16", + "landscape_4_3": "@imgly/plugin/formats/ratio4by3", + "landscape_16_9": "@imgly/plugin/formats/ratio16by9" + } + } + }, + "x-fal-order-properties": ["prompt", "image_size"], + "required": ["prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.text2image.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.text2image.ts new file mode 100644 index 00000000..3a11ec75 --- /dev/null +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/Flux2Flex.text2image.ts @@ -0,0 +1,81 @@ +import { + type Provider, + type ImageOutput, + addIconSetOnce +} from '@imgly/plugin-ai-generation-web'; +import { Icons } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Flux2FlexSchema from './Flux2Flex.text2image.json'; +import createImageProvider from './createImageProvider'; +import { EachLabsProviderConfiguration } from './types'; + +// Map EachLabs image_size enum to dimensions +const IMAGE_SIZE_MAP: Record = { + square_hd: { width: 1024, height: 1024 }, + square: { width: 512, height: 512 }, + portrait_4_3: { width: 896, height: 1152 }, + portrait_16_9: { width: 768, height: 1344 }, + landscape_4_3: { width: 1152, height: 896 }, + landscape_16_9: { width: 1344, height: 768 } +}; + +/** + * Input interface for Flux 2 Flex text-to-image + */ +export type Flux2FlexInput = { + prompt: string; + image_size?: + | 'square_hd' + | 'square' + | 'portrait_4_3' + | 'portrait_16_9' + | 'landscape_4_3' + | 'landscape_16_9'; +}; + +/** + * Flux 2 Flex - Text-to-image generation via EachLabs with prompt expansion + * + * Features: + * - 6 image size options + * - Automatic prompt expansion using the model's knowledge + * - High-quality Flux 2 generation from Black Forest Labs + */ +export function Flux2Flex( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + // Add aspect ratio icons + addIconSetOnce(cesdk, '@imgly/plugin/formats', Icons.Formats); + + return createImageProvider( + { + modelSlug: 'flux-2-flex', + modelVersion: '0.0.1', + providerId: 'eachlabs/flux-2-flex', + name: 'Flux 2 Flex', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Flux2FlexSchema, + inputReference: '#/components/schemas/Flux2FlexInput', + cesdk, + getImageSize: (input) => + IMAGE_SIZE_MAP[input.image_size ?? 'landscape_4_3'] ?? { + width: 1152, + height: 896 + }, + mapInput: (input) => ({ + prompt: input.prompt, + image_size: input.image_size ?? 'landscape_4_3', + // Enable prompt expansion by default (key Flex feature) + enable_prompt_expansion: true + }) + }, + config + ); + }; +} + +export default Flux2Flex; diff --git a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts index 81931f20..353f553d 100644 --- a/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-image-generation-web/src/eachlabs/index.ts @@ -7,6 +7,8 @@ import { Flux2Pro as Flux2ProText2Image } from './Flux2Pro.text2image'; import { Flux2ProImage2Image } from './Flux2Pro.image2image'; import { Flux2 as Flux2Text2Image } from './Flux2.text2image'; import { Flux2Image2Image } from './Flux2.image2image'; +import { Flux2Flex as Flux2FlexText2Image } from './Flux2Flex.text2image'; +import { Flux2FlexImage2Image } from './Flux2Flex.image2image'; import { OpenAIImageText2Image } from './OpenAIImage.text2image'; import { OpenAIImageImage2Image } from './OpenAIImage.image2image'; import { Seedream45 as Seedream45Text2Image } from './Seedream45.text2image'; @@ -27,6 +29,10 @@ const EachLabs = { Text2Image: Flux2Text2Image, Image2Image: Flux2Image2Image }, + Flux2Flex: { + Text2Image: Flux2FlexText2Image, + Image2Image: Flux2FlexImage2Image + }, OpenAIGptImage: { Text2Image: OpenAIImageText2Image, Image2Image: OpenAIImageImage2Image diff --git a/packages/plugin-ai-image-generation-web/translations.json b/packages/plugin-ai-image-generation-web/translations.json index 2b1f5faf..9486cf9b 100644 --- a/packages/plugin-ai-image-generation-web/translations.json +++ b/packages/plugin-ai-image-generation-web/translations.json @@ -504,6 +504,20 @@ "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2/edit.property.prompt": "Prompt", "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2/edit.property.prompt.placeholder": "Describe the changes...", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex.property.prompt.placeholder": "Describe your image...", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex.property.image_size": "Format", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex.property.image_size.square_hd": "Square HD (1:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex.property.image_size.square": "Square (1:1)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex.property.image_size.portrait_4_3": "Portrait (3:4)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex.property.image_size.portrait_16_9": "Portrait (9:16)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex.property.image_size.landscape_4_3": "Landscape (4:3)", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex.property.image_size.landscape_16_9": "Landscape (16:9)", + + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex/edit.property.image_url": "Source Image", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex/edit.property.prompt": "Prompt", + "ly.img.plugin-ai-image-generation-web.eachlabs/flux-2-flex/edit.property.prompt.placeholder": "Describe the changes...", + "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.prompt": "Prompt", "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.prompt.placeholder": "Describe your image...", "ly.img.plugin-ai-image-generation-web.eachlabs/seedream-v4.5.property.image_size": "Format", diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index f2304e41..6c264a92 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -33,7 +33,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an |------|-------|-------------|--------|-------| | flux-2-pro | Flux 2 Pro | image | implemented | Latest Flux, high quality | | flux-2 | Flux 2 | array | implemented | Standard Flux 2 | -| flux-2-flex | Flux 2 \| Flex | image | planned | Prompt expansion enabled | +| flux-2-flex | Flux 2 \| Flex | image | implemented | Prompt expansion enabled | | gemini-3-pro-image-preview | Gemini 3 \| Pro \| Image Preview | array | implemented | Google Gemini image generation | | bytedance-seedream-v4-5-text-to-image | Bytedance \| Seedream \| v4.5 \| Text to Image | array | implemented | Latest Seedream | | nano-banana-pro | Nano Banana Pro | array | implemented | Multi-style generation | @@ -45,7 +45,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an |------|-------|-------------|--------|-------| | flux-2-pro-edit | Flux 2 Pro \| Edit | image | implemented | Edit with Flux 2 Pro | | flux-2-edit | Flux 2 \| Edit | array | implemented | Standard Flux 2 edit | -| flux-2-flex-edit | Flux 2 \| Flex \| Edit | image | planned | Flex edit variant | +| flux-2-flex-edit | Flux 2 \| Flex \| Edit | image | implemented | Flex edit variant | | gemini-3-pro-image-preview-edit | Gemini 3 Pro \| Image Edit | array | implemented | Google Gemini edit | | bytedance-seedream-v4-5-edit | Bytedance \| Seedream \| v4.5 \| Edit | array | implemented | Seedream edit | | nano-banana-pro-edit | Nano Banana Pro \| Edit | array | implemented | Multi-style edit | @@ -72,30 +72,26 @@ This document tracks EachLabs AI models for implementation, focusing on image an ### Implemented -**Image Generation (T2I):** 6 models +**Image Generation (T2I):** 7 models - nano-banana-pro - flux-2-pro - flux-2 +- flux-2-flex - openai-image-generation - bytedance-seedream-v4-5-text-to-image - gemini-3-pro-image-preview -**Image Generation (I2I/Edit):** 6 models +**Image Generation (I2I/Edit):** 7 models - nano-banana-pro-edit - flux-2-pro-edit - flux-2-edit +- flux-2-flex-edit - openai-image-edit - bytedance-seedream-v4-5-edit - gemini-3-pro-image-preview-edit ### Planned for Implementation -**Image Generation (T2I):** 1 model -- flux-2-flex - -**Image Generation (I2I/Edit):** 1 model -- flux-2-flex-edit - **Video Generation (T2V):** 2 models - kling-v2-6-pro-text-to-video - veo3-1-text-to-video @@ -109,8 +105,8 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Category | Implemented | Planned | Total | |----------|-------------|---------|-------| -| Image T2I | 6 | 1 | 7 | -| Image I2I | 6 | 1 | 7 | +| Image T2I | 7 | 0 | 7 | +| Image I2I | 7 | 0 | 7 | | Video T2V | 0 | 2 | 2 | | Video I2V | 0 | 3 | 3 | -| **Total** | **12** | **7** | **19** | +| **Total** | **14** | **5** | **19** | From 81b4ae94a33f911beac7fd6b4adf4ed9c7531717 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Wed, 17 Dec 2025 19:06:17 +0100 Subject: [PATCH 17/20] feat(eachlabs): implement Kling v2.6 Pro text-to-video and image-to-video providers Add EachLabs video generation support with Kling v2.6 Pro model: - Text-to-video with aspect ratio (16:9, 9:16, 1:1) and duration (5s, 10s) - Image-to-video with duration options and quick action support - Updated createVideoProvider factory for EachLabs video providers - Added translations with placeholder text for prompts --- examples/ai/src/eachlabsProviders.ts | 22 +- .../plugin-ai-video-generation-web/README.md | 75 ++++++ .../src/eachlabs/KlingV26Pro.image2video.json | 49 ++++ .../src/eachlabs/KlingV26Pro.image2video.ts | 97 ++++++++ .../src/eachlabs/KlingV26Pro.text2video.json | 58 +++++ .../src/eachlabs/KlingV26Pro.text2video.ts | 84 +++++++ .../src/eachlabs/createVideoProvider.ts | 221 +++++++++++++++++- .../src/eachlabs/index.ts | 13 +- .../translations.json | 17 +- specs/providers/eachlabs/providers.md | 24 +- 10 files changed, 633 insertions(+), 27 deletions(-) create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.image2video.json create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.image2video.ts create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.text2video.json create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.text2video.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index 6fe10f2d..7709f818 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -1,4 +1,5 @@ import EachLabsImage from '@imgly/plugin-ai-image-generation-web/eachlabs'; +import EachLabsVideo from '@imgly/plugin-ai-video-generation-web/eachlabs'; import { Middleware } from '@imgly/plugin-ai-generation-web'; export interface EachLabsProviderOptions { @@ -9,7 +10,12 @@ export interface EachLabsProviderOptions { } export function createEachLabsProviders(options: EachLabsProviderOptions) { - const { imageRateLimitMiddleware, errorMiddleware, proxyUrl } = options; + const { + imageRateLimitMiddleware, + videoRateLimitMiddleware, + errorMiddleware, + proxyUrl + } = options; return { text2image: [ @@ -72,7 +78,17 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { proxyUrl }) ], - text2video: [], - image2video: [] + text2video: [ + EachLabsVideo.KlingV26ProTextToVideo({ + middlewares: [videoRateLimitMiddleware, errorMiddleware], + proxyUrl + }) + ], + image2video: [ + EachLabsVideo.KlingV26ProImageToVideo({ + middlewares: [videoRateLimitMiddleware, errorMiddleware], + proxyUrl + }) + ] }; } diff --git a/packages/plugin-ai-video-generation-web/README.md b/packages/plugin-ai-video-generation-web/README.md index c2fd35ea..2e93d427 100644 --- a/packages/plugin-ai-video-generation-web/README.md +++ b/packages/plugin-ai-video-generation-web/README.md @@ -598,6 +598,52 @@ Key features: - Resolutions: 1280×720, 720×1280, 1792×1024 (7:4), 1024×1792 (4:7) - Durations: 4, 8, or 12 seconds +### EachLabs Providers + +EachLabs provides access to multiple AI video models through a unified API. These providers require an EachLabs proxy URL for authentication. + +#### 18. KlingV26ProTextToVideo (Text-to-Video) via EachLabs + +Kling v2.6 Pro text-to-video model accessed through EachLabs: + +```typescript +import EachLabsVideo from '@imgly/plugin-ai-video-generation-web/eachlabs'; + +text2video: EachLabsVideo.KlingV26ProTextToVideo({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy', + // Optional: Configure default values + middlewares: [rateLimitMiddleware, errorMiddleware] +}) +``` + +Key features: +- Kling v2.6 Pro - latest high-quality video generation +- Aspect ratios: 16:9, 9:16, 1:1 +- Duration: 5 or 10 seconds +- Native audio generation (Chinese/English) +- Async delivery with polling + +#### 19. KlingV26ProImageToVideo (Image-to-Video) via EachLabs + +Kling v2.6 Pro image-to-video model: + +```typescript +import EachLabsVideo from '@imgly/plugin-ai-video-generation-web/eachlabs'; + +image2video: EachLabsVideo.KlingV26ProImageToVideo({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy', + // Optional: Configure default values + middlewares: [rateLimitMiddleware, errorMiddleware] +}) +``` + +Key features: +- Transform existing images into videos +- Duration: 5 or 10 seconds +- Native audio generation (Chinese/English) +- Canvas quick-action integration +- Maintains image aspect ratio + ### Feature Control You can control various aspects of the video generation plugin using the Feature API: @@ -1012,6 +1058,31 @@ RunwareVideo.Sora2Pro.Text2Video(config: RunwareProviderConfiguration) RunwareVideo.Sora2Pro.Image2Video(config: RunwareProviderConfiguration) ``` +### EachLabs Providers + +All EachLabs video providers use the following configuration: + +```typescript +interface EachLabsProviderConfiguration { + proxyUrl: string; // HTTP endpoint URL for the EachLabs proxy + debug?: boolean; // Enable debug logging + middlewares?: any[]; // Optional middleware functions + history?: false | '@imgly/local' | '@imgly/indexedDB' | (string & {}); +} +``` + +#### KlingV26ProTextToVideo + +```typescript +EachLabsVideo.KlingV26ProTextToVideo(config: EachLabsProviderConfiguration) +``` + +#### KlingV26ProImageToVideo + +```typescript +EachLabsVideo.KlingV26ProImageToVideo(config: EachLabsProviderConfiguration) +``` + ## UI Integration The plugin automatically registers the following UI components: @@ -1048,6 +1119,8 @@ The plugin automatically registers the following UI components: - Runware Sora2.Image2Video: `ly.img.ai.runware/openai/sora-2/image2video` - Runware Sora2Pro.Text2Video: `ly.img.ai.runware/openai/sora-2-pro` - Runware Sora2Pro.Image2Video: `ly.img.ai.runware/openai/sora-2-pro/image2video` + - EachLabs KlingV26ProTextToVideo: `ly.img.ai.eachlabs/kling-v2-6-pro-text-to-video` + - EachLabs KlingV26ProImageToVideo: `ly.img.ai.eachlabs/kling-v2-6-pro-image-to-video` ### Asset History @@ -1075,6 +1148,8 @@ Generated videos are automatically stored in asset sources with the following ID - Runware Sora2.Image2Video: `runware/openai/sora-2/image2video.history` - Runware Sora2Pro.Text2Video: `runware/openai/sora-2-pro.history` - Runware Sora2Pro.Image2Video: `runware/openai/sora-2-pro/image2video.history` +- EachLabs KlingV26ProTextToVideo: `eachlabs/kling-v2-6-pro-text-to-video.history` +- EachLabs KlingV26ProImageToVideo: `eachlabs/kling-v2-6-pro-image-to-video.history` ### Dock Integration diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.image2video.json b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.image2video.json new file mode 100644 index 00000000..9694074c --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.image2video.json @@ -0,0 +1,49 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Kling v2.6 Pro Image to Video API", + "version": "1.0.0", + "description": "Kling v2.6 Pro image-to-video generation via EachLabs" + }, + "components": { + "schemas": { + "KlingV26ProImageToVideoInput": { + "title": "KlingV26ProImageToVideoInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "format": "uri", + "description": "URL of the image to be used for the video", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Motion/action description for video generation", + "x-imgly-builder": { + "component": "TextArea" + } + }, + "duration": { + "title": "Duration", + "type": "string", + "enum": ["5", "10"], + "default": "5", + "description": "The duration of the generated video in seconds", + "x-imgly-enum-labels": { + "5": "5 seconds", + "10": "10 seconds" + } + } + }, + "x-fal-order-properties": ["image_url", "prompt", "duration"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.image2video.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.image2video.ts new file mode 100644 index 00000000..39bd7f22 --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.image2video.ts @@ -0,0 +1,97 @@ +import { + type Provider, + type VideoOutput, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import KlingV26ProImageToVideoSchema from './KlingV26Pro.image2video.json'; +import createVideoProvider from './createVideoProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for Kling v2.6 Pro image-to-video + */ +export type KlingV26ProImageToVideoInput = { + image_url: string; + prompt: string; + duration?: '5' | '10'; +}; + +/** + * Kling v2.6 Pro - High-quality image-to-video generation via EachLabs + * + * Features: + * - 5 or 10 second video duration + * - Native audio generation support (Chinese/English) + * - High-quality video generation from Kuaishou + */ +export function KlingV26ProImageToVideo( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/kling-v2-6-pro-image-to-video'; + + // Set translations for image selection UI + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Animate', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createVideoProvider( + { + modelSlug: 'kling-v2-6-pro-image-to-video', + modelVersion: '0.0.1', + providerId, + name: 'Kling v2.6 Pro', + // @ts-ignore - JSON schema types are compatible at runtime + schema: KlingV26ProImageToVideoSchema, + inputReference: '#/components/schemas/KlingV26ProImageToVideoInput', + cesdk, + supportedQuickActions: { + 'ly.img.createVideo': { + mapInput: (input) => ({ + image_url: input.uri, + prompt: '' + }) + } + }, + getBlockInput: async (input) => { + const { width, height } = await getImageDimensionsFromURL( + input.image_url, + cesdk.engine + ); + const duration = + input.duration != null ? parseInt(input.duration, 10) : 5; + + return { + video: { + width, + height, + duration + } + }; + }, + mapInput: (input) => ({ + image_url: input.image_url, + prompt: input.prompt, + duration: input.duration ?? '5', + // Enable audio generation by default + generate_audio: true, + // Default negative prompt + negative_prompt: 'blur, distort, and low quality' + }) + }, + config + ); + }; +} + +export default KlingV26ProImageToVideo; diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.text2video.json b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.text2video.json new file mode 100644 index 00000000..a890dee7 --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.text2video.json @@ -0,0 +1,58 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Kling v2.6 Pro Text to Video API", + "version": "1.0.0", + "description": "Kling v2.6 Pro text-to-video generation via EachLabs" + }, + "components": { + "schemas": { + "KlingV26ProTextToVideoInput": { + "title": "KlingV26ProTextToVideoInput", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "User's video generation instruction", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "duration": { + "title": "Duration", + "type": "string", + "enum": ["5", "10"], + "default": "10", + "description": "The duration of the generated video in seconds", + "x-imgly-enum-labels": { + "5": "5 seconds", + "10": "10 seconds" + } + }, + "aspect_ratio": { + "title": "Aspect Ratio", + "type": "string", + "enum": ["16:9", "9:16", "1:1"], + "default": "16:9", + "description": "The aspect ratio of the generated video frame", + "x-imgly-enum-labels": { + "16:9": "Landscape (16:9)", + "9:16": "Portrait (9:16)", + "1:1": "Square (1:1)" + }, + "x-imgly-enum-icons": { + "16:9": "@imgly/plugin/formats/ratio16by9", + "9:16": "@imgly/plugin/formats/ratio9by16", + "1:1": "@imgly/plugin/formats/ratio1by1" + } + } + }, + "x-fal-order-properties": ["prompt", "duration", "aspect_ratio"], + "required": ["prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.text2video.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.text2video.ts new file mode 100644 index 00000000..843e2607 --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingV26Pro.text2video.ts @@ -0,0 +1,84 @@ +import { + type Provider, + type VideoOutput, + addIconSetOnce +} from '@imgly/plugin-ai-generation-web'; +import { Icons } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import KlingV26ProTextToVideoSchema from './KlingV26Pro.text2video.json'; +import createVideoProvider from './createVideoProvider'; +import { + EachLabsProviderConfiguration, + getVideoDimensionsFromAspectRatio +} from './types'; + +/** + * Input interface for Kling v2.6 Pro text-to-video + */ +export type KlingV26ProTextToVideoInput = { + prompt: string; + duration?: '5' | '10'; + aspect_ratio?: '16:9' | '9:16' | '1:1'; +}; + +/** + * Kling v2.6 Pro - High-quality text-to-video generation via EachLabs + * + * Features: + * - 5 or 10 second video duration + * - 3 aspect ratio options + * - Native audio generation support (Chinese/English) + * - High-quality video generation from Kuaishou + */ +export function KlingV26ProTextToVideo( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + // Add aspect ratio icons + addIconSetOnce(cesdk, '@imgly/plugin/formats', Icons.Formats); + + return createVideoProvider( + { + modelSlug: 'kling-v2-6-pro-text-to-video', + modelVersion: '0.0.1', + providerId: 'eachlabs/kling-v2-6-pro-text-to-video', + name: 'Kling v2.6 Pro', + // @ts-ignore - JSON schema types are compatible at runtime + schema: KlingV26ProTextToVideoSchema, + inputReference: '#/components/schemas/KlingV26ProTextToVideoInput', + cesdk, + getBlockInput: (input) => { + const aspectRatio = input.aspect_ratio ?? '16:9'; + const { width, height } = + getVideoDimensionsFromAspectRatio(aspectRatio); + const duration = + input.duration != null ? parseInt(input.duration, 10) : 10; + + return Promise.resolve({ + video: { + width, + height, + duration + } + }); + }, + mapInput: (input) => ({ + prompt: input.prompt, + duration: input.duration ?? '10', + aspect_ratio: input.aspect_ratio ?? '16:9', + // Enable audio generation by default + generate_audio: true, + // Default negative prompt + negative_prompt: 'blur, distort, and low quality', + cfg_scale: 0.5 + }) + }, + config + ); + }; +} + +export default KlingV26ProTextToVideo; diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/createVideoProvider.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/createVideoProvider.ts index 0817b5e3..4394be13 100644 --- a/packages/plugin-ai-video-generation-web/src/eachlabs/createVideoProvider.ts +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/createVideoProvider.ts @@ -1,26 +1,227 @@ -// EachLabs video provider factory -// TODO: Implement actual provider creation +import { type OpenAPIV3 } from 'openapi-types'; +import CreativeEditorSDK, { CreativeEngine } from '@cesdk/cesdk-js'; +import { + Provider, + RenderCustomProperty, + VideoOutput, + GetBlockInput, + CommonProperties, + Middleware, + mergeQuickActionsConfig +} from '@imgly/plugin-ai-generation-web'; +import { createEachLabsClient, EachLabsClient } from './createEachLabsClient'; +import { uploadImageInputToEachLabsIfNeeded } from './utils'; +import { VideoQuickActionSupportMap } from '../types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -type Middleware = (input: any, output: any) => any; +type MiddlewareType = Middleware; +/** + * Configuration for EachLabs video providers. + */ export interface EachLabsProviderConfiguration { /** - * The URL of the proxy server to use for API requests + * HTTP endpoint URL for the EachLabs proxy. The proxy handles API key injection. */ proxyUrl: string; /** - * Enable debug mode for logging + * Enable debug logging for provider creation and API calls. */ debug?: boolean; /** - * Optional middlewares to apply to the provider + * Middleware functions to process inputs/outputs. */ - middlewares?: Middleware[]; + middlewares?: MiddlewareType[]; /** - * History configuration + * Override provider's default history asset source. */ - history?: false | '@imgly/local' | '@imgly/indexedDB'; + history?: false | '@imgly/local' | '@imgly/indexedDB' | (string & {}); + /** + * Configure supported quick actions. + */ + supportedQuickActions?: { + [quickActionId: string]: + | Partial[string]> + | false + | null; + }; +} + +/** + * Options for creating an EachLabs video provider. + */ +interface CreateProviderOptions> { + /** + * EachLabs model slug (e.g., 'kling-v2-6-pro-text-to-video'). + */ + modelSlug: string; + /** + * EachLabs model version (e.g., '0.0.1'). + */ + modelVersion: string; + /** + * Unique provider identifier for registration. + */ + providerId: string; + /** + * Human-readable provider name displayed in the UI. + */ + name: string; + /** + * OpenAPI schema document describing the input parameters. + */ + schema: OpenAPIV3.Document; + /** + * JSON reference to the input schema (e.g., '#/components/schemas/Input'). + */ + inputReference: string; + /** + * User flow mode for the provider panel. + */ + useFlow?: 'placeholder' | 'generation-only'; + /** + * Initialization callback when the provider is registered. + */ + initialize?: (context: { + cesdk?: CreativeEditorSDK; + engine: CreativeEngine; + }) => void; + /** + * Custom property renderers for the input panel. + */ + renderCustomProperty?: RenderCustomProperty; + /** + * Quick actions this provider supports. + */ + supportedQuickActions?: VideoQuickActionSupportMap; + /** + * Get block dimensions from input parameters. + */ + getBlockInput: GetBlockInput<'video', I>; + /** + * Transform input parameters to EachLabs API format. + */ + mapInput: (input: I) => Record; + /** + * Provider-specific middleware functions. + */ + middleware?: MiddlewareType[]; + /** + * Custom headers to include in API requests. + */ + headers?: Record; + /** + * CE.SDK instance for image URL conversion. + */ + cesdk?: CreativeEditorSDK; +} + +/** + * Creates an EachLabs video provider from schema. + */ +function createVideoProvider< + I extends Record & { image_url?: string } +>( + options: CreateProviderOptions, + config: EachLabsProviderConfiguration +): Provider<'video', I, VideoOutput> { + const middleware = options.middleware ?? config.middlewares ?? []; + + let eachLabsClient: EachLabsClient | null = null; + + const provider: Provider<'video', I, VideoOutput> = { + id: options.providerId, + kind: 'video', + name: options.name, + configuration: config, + initialize: async (context) => { + eachLabsClient = createEachLabsClient(config.proxyUrl, options.headers); + options.initialize?.(context); + }, + input: { + quickActions: { + supported: mergeQuickActionsConfig( + options.supportedQuickActions ?? {}, + config.supportedQuickActions + ) + }, + panel: { + type: 'schema', + document: options.schema, + inputReference: options.inputReference, + includeHistoryLibrary: true, + orderExtensionKeyword: 'x-fal-order-properties', + renderCustomProperty: { + ...(options.cesdk != null + ? CommonProperties.ImageUrl(options.providerId, { + cesdk: options.cesdk + }) + : {}), + ...options.renderCustomProperty + }, + getBlockInput: options.getBlockInput, + userFlow: options.useFlow ?? 'placeholder' + } + }, + output: { + abortable: true, + middleware, + history: config.history ?? '@imgly/indexedDB', + generate: async ( + input: I, + { abortSignal }: { abortSignal?: AbortSignal } + ) => { + if (!eachLabsClient) { + throw new Error('Provider not initialized'); + } + + // Upload image URL if needed for image-to-video + let processedInput = input; + + if (input.image_url != null) { + const uploadedUrl = await uploadImageInputToEachLabsIfNeeded( + eachLabsClient, + input.image_url, + options.cesdk + ); + processedInput = { ...input, image_url: uploadedUrl }; + } + + // Map input to EachLabs format + const eachLabsInput = options.mapInput(processedInput); + + // Call EachLabs videoInference via HTTP REST API + const videos = await eachLabsClient.videoInference( + { + model: options.modelSlug, + version: options.modelVersion, + input: eachLabsInput + }, + abortSignal + ); + + if (videos != null && Array.isArray(videos) && videos.length > 0) { + const video = videos[0]; + const url = video?.videoURL; + if (url != null) { + return { kind: 'video', url }; + } + } + + // eslint-disable-next-line no-console + console.error('Cannot extract generated video from response:', videos); + throw new Error('Cannot find generated video'); + }, + generationHintText: 'ly.img.ai.video.generation.hint' + } + }; + + if (config.debug) { + // eslint-disable-next-line no-console + console.log('Created EachLabs Video Provider:', provider); + } + + return provider; } -// Provider factory will be implemented here when models are added +export default createVideoProvider; diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts index 3b93c7d0..5cb1aa6a 100644 --- a/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts @@ -1,11 +1,18 @@ -// EachLabs provider namespace -// Providers are added here via the partner-providers-eachlabs skill +// EachLabs video provider namespace + +import { KlingV26ProTextToVideo } from './KlingV26Pro.text2video'; +import { KlingV26ProImageToVideo } from './KlingV26Pro.image2video'; const EachLabs = { - // Models will be added here + KlingV26ProTextToVideo, + KlingV26ProImageToVideo }; export default EachLabs; // Re-export types export type { EachLabsProviderConfiguration } from './types'; + +// Re-export individual providers for direct imports +export { KlingV26ProTextToVideo } from './KlingV26Pro.text2video'; +export { KlingV26ProImageToVideo } from './KlingV26Pro.image2video'; diff --git a/packages/plugin-ai-video-generation-web/translations.json b/packages/plugin-ai-video-generation-web/translations.json index faf4c19e..1908f003 100644 --- a/packages/plugin-ai-video-generation-web/translations.json +++ b/packages/plugin-ai-video-generation-web/translations.json @@ -205,6 +205,21 @@ "ly.img.plugin-ai-video-generation-web.runware/openai/sora-2-pro/image2video.property.duration": "Duration", "ly.img.plugin-ai-video-generation-web.runware/openai/sora-2-pro/image2video.property.duration.4": "4 seconds", "ly.img.plugin-ai-video-generation-web.runware/openai/sora-2-pro/image2video.property.duration.8": "8 seconds", - "ly.img.plugin-ai-video-generation-web.runware/openai/sora-2-pro/image2video.property.duration.12": "12 seconds" + "ly.img.plugin-ai-video-generation-web.runware/openai/sora-2-pro/image2video.property.duration.12": "12 seconds", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-text-to-video.property.prompt": "Prompt", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-text-to-video.property.prompt.placeholder": "Describe your video scene...", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-text-to-video.property.duration": "Duration", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-text-to-video.property.duration.5": "5 seconds", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-text-to-video.property.duration.10": "10 seconds", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-text-to-video.property.aspect_ratio": "Aspect Ratio", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-text-to-video.property.aspect_ratio.16:9": "16:9 (Landscape)", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-text-to-video.property.aspect_ratio.9:16": "9:16 (Portrait)", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-text-to-video.property.aspect_ratio.1:1": "1:1 (Square)", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.image_url": "Source Image", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.prompt": "Prompt", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.prompt.placeholder": "Describe the motion...", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.duration": "Duration", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.duration.5": "5 seconds", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.duration.10": "10 seconds" } } \ No newline at end of file diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index 6c264a92..113788ad 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -2,7 +2,7 @@ This document tracks EachLabs AI models for implementation, focusing on image and video generation. -**Last Updated**: 2025-12-16 +**Last Updated**: 2025-12-17 ## API Reference @@ -55,14 +55,14 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Slug | Title | Output Type | Status | Notes | |------|-------|-------------|--------|-------| -| kling-v2-6-pro-text-to-video | Kling \| v2.6 \| Pro \| Text to Video | video | planned | Latest Kling, high quality | +| kling-v2-6-pro-text-to-video | Kling \| v2.6 \| Pro \| Text to Video | video | implemented | Latest Kling, high quality | | veo3-1-text-to-video | Veo 3.1 \| Text to Video | video | planned | Google Veo 3.1 | ## Video Generation (Image-to-Video) | Slug | Title | Output Type | Status | Notes | |------|-------|-------------|--------|-------| -| kling-v2-6-pro-image-to-video | Kling \| v2.6 \| Pro \| Image to Video | video | planned | Latest Kling I2V | +| kling-v2-6-pro-image-to-video | Kling \| v2.6 \| Pro \| Image to Video | video | implemented | Latest Kling I2V | | kling-o1-image-to-video | Kling O1 \| Image to Video | video | planned | Kling O1 variant | | veo3-1-image-to-video | Veo 3.1 \| Image to Video | video | planned | Google Veo 3.1 I2V | @@ -90,14 +90,18 @@ This document tracks EachLabs AI models for implementation, focusing on image an - bytedance-seedream-v4-5-edit - gemini-3-pro-image-preview-edit +**Video Generation (T2V):** 1 model +- kling-v2-6-pro-text-to-video + +**Video Generation (I2V):** 1 model +- kling-v2-6-pro-image-to-video + ### Planned for Implementation -**Video Generation (T2V):** 2 models -- kling-v2-6-pro-text-to-video +**Video Generation (T2V):** 1 model - veo3-1-text-to-video -**Video Generation (I2V):** 3 models -- kling-v2-6-pro-image-to-video +**Video Generation (I2V):** 2 models - kling-o1-image-to-video - veo3-1-image-to-video @@ -107,6 +111,6 @@ This document tracks EachLabs AI models for implementation, focusing on image an |----------|-------------|---------|-------| | Image T2I | 7 | 0 | 7 | | Image I2I | 7 | 0 | 7 | -| Video T2V | 0 | 2 | 2 | -| Video I2V | 0 | 3 | 3 | -| **Total** | **14** | **5** | **19** | +| Video T2V | 1 | 1 | 2 | +| Video I2V | 1 | 2 | 3 | +| **Total** | **16** | **3** | **19** | From 481d7c3924877020913e52a8f035f0846afee182 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Thu, 18 Dec 2025 10:27:07 +0100 Subject: [PATCH 18/20] feat(eachlabs): implement Veo 3.1 text-to-video and image-to-video providers Add Google Veo 3.1 video generation providers via EachLabs API: - Text-to-video with aspect ratio (16:9, 9:16), resolution (720p, 1080p), and audio options - Image-to-video with resolution options and canvas quick-action integration - Both providers generate 8-second videos with optional audio --- examples/ai/src/eachlabsProviders.ts | 8 ++ .../plugin-ai-video-generation-web/README.md | 59 +++++++++++ .../src/eachlabs/Veo31.image2video.json | 55 ++++++++++ .../src/eachlabs/Veo31.image2video.ts | 100 ++++++++++++++++++ .../src/eachlabs/Veo31.text2video.json | 62 +++++++++++ .../src/eachlabs/Veo31.text2video.ts | 85 +++++++++++++++ .../src/eachlabs/index.ts | 8 +- .../translations.json | 18 +++- specs/providers/eachlabs/providers.md | 22 ++-- 9 files changed, 403 insertions(+), 14 deletions(-) create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.image2video.json create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.image2video.ts create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.text2video.json create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.text2video.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index 7709f818..5a624e7b 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -82,12 +82,20 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { EachLabsVideo.KlingV26ProTextToVideo({ middlewares: [videoRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsVideo.Veo31TextToVideo({ + middlewares: [videoRateLimitMiddleware, errorMiddleware], + proxyUrl }) ], image2video: [ EachLabsVideo.KlingV26ProImageToVideo({ middlewares: [videoRateLimitMiddleware, errorMiddleware], proxyUrl + }), + EachLabsVideo.Veo31ImageToVideo({ + middlewares: [videoRateLimitMiddleware, errorMiddleware], + proxyUrl }) ] }; diff --git a/packages/plugin-ai-video-generation-web/README.md b/packages/plugin-ai-video-generation-web/README.md index 2e93d427..54d650aa 100644 --- a/packages/plugin-ai-video-generation-web/README.md +++ b/packages/plugin-ai-video-generation-web/README.md @@ -644,6 +644,49 @@ Key features: - Canvas quick-action integration - Maintains image aspect ratio +#### 20. Veo31TextToVideo (Text-to-Video) via EachLabs + +Google's Veo 3.1 text-to-video model accessed through EachLabs: + +```typescript +import EachLabsVideo from '@imgly/plugin-ai-video-generation-web/eachlabs'; + +text2video: EachLabsVideo.Veo31TextToVideo({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy', + // Optional: Configure default values + middlewares: [rateLimitMiddleware, errorMiddleware] +}) +``` + +Key features: +- Google's Veo 3.1 - high-quality text-to-video generation +- Aspect ratios: 16:9, 9:16 +- Resolution: 720p or 1080p +- 8-second video duration +- Optional audio generation +- Async delivery with polling + +#### 21. Veo31ImageToVideo (Image-to-Video) via EachLabs + +Google's Veo 3.1 image-to-video model: + +```typescript +import EachLabsVideo from '@imgly/plugin-ai-video-generation-web/eachlabs'; + +image2video: EachLabsVideo.Veo31ImageToVideo({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy', + // Optional: Configure default values + middlewares: [rateLimitMiddleware, errorMiddleware] +}) +``` + +Key features: +- Transform existing images into videos +- Resolution: 720p or 1080p +- 8-second video duration +- Optional audio generation +- Canvas quick-action integration + ### Feature Control You can control various aspects of the video generation plugin using the Feature API: @@ -1083,6 +1126,18 @@ EachLabsVideo.KlingV26ProTextToVideo(config: EachLabsProviderConfiguration) EachLabsVideo.KlingV26ProImageToVideo(config: EachLabsProviderConfiguration) ``` +#### Veo31TextToVideo + +```typescript +EachLabsVideo.Veo31TextToVideo(config: EachLabsProviderConfiguration) +``` + +#### Veo31ImageToVideo + +```typescript +EachLabsVideo.Veo31ImageToVideo(config: EachLabsProviderConfiguration) +``` + ## UI Integration The plugin automatically registers the following UI components: @@ -1121,6 +1176,8 @@ The plugin automatically registers the following UI components: - Runware Sora2Pro.Image2Video: `ly.img.ai.runware/openai/sora-2-pro/image2video` - EachLabs KlingV26ProTextToVideo: `ly.img.ai.eachlabs/kling-v2-6-pro-text-to-video` - EachLabs KlingV26ProImageToVideo: `ly.img.ai.eachlabs/kling-v2-6-pro-image-to-video` + - EachLabs Veo31TextToVideo: `ly.img.ai.eachlabs/veo3-1-text-to-video` + - EachLabs Veo31ImageToVideo: `ly.img.ai.eachlabs/veo3-1-image-to-video` ### Asset History @@ -1150,6 +1207,8 @@ Generated videos are automatically stored in asset sources with the following ID - Runware Sora2Pro.Image2Video: `runware/openai/sora-2-pro/image2video.history` - EachLabs KlingV26ProTextToVideo: `eachlabs/kling-v2-6-pro-text-to-video.history` - EachLabs KlingV26ProImageToVideo: `eachlabs/kling-v2-6-pro-image-to-video.history` +- EachLabs Veo31TextToVideo: `eachlabs/veo3-1-text-to-video.history` +- EachLabs Veo31ImageToVideo: `eachlabs/veo3-1-image-to-video.history` ### Dock Integration diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.image2video.json b/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.image2video.json new file mode 100644 index 00000000..237edfa1 --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.image2video.json @@ -0,0 +1,55 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Veo 3.1 Image to Video API", + "version": "1.0.0", + "description": "Google Veo 3.1 image-to-video generation via EachLabs" + }, + "components": { + "schemas": { + "Veo31ImageToVideoInput": { + "title": "Veo31ImageToVideoInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "format": "uri", + "description": "URL of the input image to animate", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "The text prompt describing the video you want to generate", + "x-imgly-builder": { + "component": "TextArea" + } + }, + "resolution": { + "title": "Resolution", + "type": "string", + "enum": ["720p", "1080p"], + "default": "720p", + "description": "The resolution of the generated video", + "x-imgly-enum-labels": { + "720p": "720p", + "1080p": "1080p" + } + }, + "generate_audio": { + "title": "Generate Audio", + "type": "boolean", + "default": true, + "description": "Whether to generate audio for the video" + } + }, + "x-fal-order-properties": ["image_url", "prompt", "resolution", "generate_audio"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.image2video.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.image2video.ts new file mode 100644 index 00000000..e9f2e449 --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.image2video.ts @@ -0,0 +1,100 @@ +import { + type Provider, + type VideoOutput, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Veo31ImageToVideoSchema from './Veo31.image2video.json'; +import createVideoProvider from './createVideoProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for Veo 3.1 image-to-video + */ +export type Veo31ImageToVideoInput = { + image_url: string; + prompt: string; + resolution?: '720p' | '1080p'; + generate_audio?: boolean; +}; + +/** + * Veo 3.1 - Google's image-to-video generation via EachLabs + * + * Features: + * - 8 second video duration + * - 720p or 1080p resolution + * - Optional audio generation + * - Animates input image based on prompt + */ +export function Veo31ImageToVideo( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/veo3-1-image-to-video'; + + // Set translations for image selection UI + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Animate', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createVideoProvider( + { + modelSlug: 'veo3-1-image-to-video', + modelVersion: '0.0.1', + providerId, + name: 'Veo 3.1', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Veo31ImageToVideoSchema, + inputReference: '#/components/schemas/Veo31ImageToVideoInput', + cesdk, + supportedQuickActions: { + 'ly.img.createVideo': { + mapInput: (input) => ({ + image_url: input.uri, + prompt: '' + }) + } + }, + getBlockInput: async (input) => { + const { width, height } = await getImageDimensionsFromURL( + input.image_url, + cesdk.engine + ); + const duration = 8; // Veo 3.1 generates 8 second videos + + return { + video: { + width, + height, + duration + } + }; + }, + mapInput: (input) => ({ + image_url: input.image_url, + prompt: input.prompt, + resolution: input.resolution ?? '720p', + generate_audio: input.generate_audio ?? true, + duration: 8, + // Enable auto-fix by default for better results + auto_fix: true, + // Use 16:9 by default (image will be cropped if needed) + aspect_ratio: '16:9' + }) + }, + config + ); + }; +} + +export default Veo31ImageToVideo; diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.text2video.json b/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.text2video.json new file mode 100644 index 00000000..207d1258 --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.text2video.json @@ -0,0 +1,62 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Veo 3.1 Text to Video API", + "version": "1.0.0", + "description": "Google Veo 3.1 text-to-video generation via EachLabs" + }, + "components": { + "schemas": { + "Veo31TextToVideoInput": { + "title": "Veo31TextToVideoInput", + "type": "object", + "properties": { + "prompt": { + "title": "Prompt", + "type": "string", + "description": "The text prompt describing the video you want to generate", + "minLength": 1, + "x-imgly-builder": { + "component": "TextArea" + } + }, + "aspect_ratio": { + "title": "Aspect Ratio", + "type": "string", + "enum": ["16:9", "9:16"], + "default": "16:9", + "description": "The aspect ratio of the generated video", + "x-imgly-enum-labels": { + "16:9": "Landscape (16:9)", + "9:16": "Portrait (9:16)" + }, + "x-imgly-enum-icons": { + "16:9": "@imgly/plugin/formats/ratio16by9", + "9:16": "@imgly/plugin/formats/ratio9by16" + } + }, + "resolution": { + "title": "Resolution", + "type": "string", + "enum": ["720p", "1080p"], + "default": "720p", + "description": "The resolution of the generated video", + "x-imgly-enum-labels": { + "720p": "720p", + "1080p": "1080p" + } + }, + "generate_audio": { + "title": "Generate Audio", + "type": "boolean", + "default": true, + "description": "Whether to generate audio for the video" + } + }, + "x-fal-order-properties": ["prompt", "aspect_ratio", "resolution", "generate_audio"], + "required": ["prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.text2video.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.text2video.ts new file mode 100644 index 00000000..46d23dd0 --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/Veo31.text2video.ts @@ -0,0 +1,85 @@ +import { + type Provider, + type VideoOutput, + addIconSetOnce +} from '@imgly/plugin-ai-generation-web'; +import { Icons } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import Veo31TextToVideoSchema from './Veo31.text2video.json'; +import createVideoProvider from './createVideoProvider'; +import { + EachLabsProviderConfiguration, + getVideoDimensionsFromAspectRatio +} from './types'; + +/** + * Input interface for Veo 3.1 text-to-video + */ +export type Veo31TextToVideoInput = { + prompt: string; + aspect_ratio?: '16:9' | '9:16'; + resolution?: '720p' | '1080p'; + generate_audio?: boolean; +}; + +/** + * Veo 3.1 - Google's text-to-video generation via EachLabs + * + * Features: + * - 8 second video duration + * - 2 aspect ratio options (16:9, 9:16) + * - 720p or 1080p resolution + * - Optional audio generation + */ +export function Veo31TextToVideo( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + // Add aspect ratio icons + addIconSetOnce(cesdk, '@imgly/plugin/formats', Icons.Formats); + + return createVideoProvider( + { + modelSlug: 'veo3-1-text-to-video', + modelVersion: '0.0.1', + providerId: 'eachlabs/veo3-1-text-to-video', + name: 'Veo 3.1', + // @ts-ignore - JSON schema types are compatible at runtime + schema: Veo31TextToVideoSchema, + inputReference: '#/components/schemas/Veo31TextToVideoInput', + cesdk, + getBlockInput: (input) => { + const aspectRatio = input.aspect_ratio ?? '16:9'; + const { width, height } = + getVideoDimensionsFromAspectRatio(aspectRatio); + const duration = 8; // Veo 3.1 generates 8 second videos + + return Promise.resolve({ + video: { + width, + height, + duration + } + }); + }, + mapInput: (input) => ({ + prompt: input.prompt, + aspect_ratio: input.aspect_ratio ?? '16:9', + resolution: input.resolution ?? '720p', + generate_audio: input.generate_audio ?? true, + duration: 8, + // Enable auto-fix by default for better results + auto_fix: true, + // Enable prompt enhancement + enhance_prompt: true + }) + }, + config + ); + }; +} + +export default Veo31TextToVideo; diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts index 5cb1aa6a..199d5b5f 100644 --- a/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts @@ -2,10 +2,14 @@ import { KlingV26ProTextToVideo } from './KlingV26Pro.text2video'; import { KlingV26ProImageToVideo } from './KlingV26Pro.image2video'; +import { Veo31TextToVideo } from './Veo31.text2video'; +import { Veo31ImageToVideo } from './Veo31.image2video'; const EachLabs = { KlingV26ProTextToVideo, - KlingV26ProImageToVideo + KlingV26ProImageToVideo, + Veo31TextToVideo, + Veo31ImageToVideo }; export default EachLabs; @@ -16,3 +20,5 @@ export type { EachLabsProviderConfiguration } from './types'; // Re-export individual providers for direct imports export { KlingV26ProTextToVideo } from './KlingV26Pro.text2video'; export { KlingV26ProImageToVideo } from './KlingV26Pro.image2video'; +export { Veo31TextToVideo } from './Veo31.text2video'; +export { Veo31ImageToVideo } from './Veo31.image2video'; diff --git a/packages/plugin-ai-video-generation-web/translations.json b/packages/plugin-ai-video-generation-web/translations.json index 1908f003..43a42b79 100644 --- a/packages/plugin-ai-video-generation-web/translations.json +++ b/packages/plugin-ai-video-generation-web/translations.json @@ -220,6 +220,22 @@ "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.prompt.placeholder": "Describe the motion...", "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.duration": "Duration", "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.duration.5": "5 seconds", - "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.duration.10": "10 seconds" + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-v2-6-pro-image-to-video.property.duration.10": "10 seconds", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-text-to-video.property.prompt": "Prompt", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-text-to-video.property.prompt.placeholder": "Describe your video scene...", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-text-to-video.property.aspect_ratio": "Aspect Ratio", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-text-to-video.property.aspect_ratio.16:9": "16:9 (Landscape)", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-text-to-video.property.aspect_ratio.9:16": "9:16 (Portrait)", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-text-to-video.property.resolution": "Resolution", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-text-to-video.property.resolution.720p": "720p (HD)", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-text-to-video.property.resolution.1080p": "1080p (Full HD)", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-text-to-video.property.generate_audio": "Generate Audio", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.image_url": "Source Image", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.prompt": "Prompt", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.prompt.placeholder": "Describe the motion...", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.resolution": "Resolution", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.resolution.720p": "720p (HD)", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.resolution.1080p": "1080p (Full HD)", + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.generate_audio": "Generate Audio" } } \ No newline at end of file diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index 113788ad..d5d177d4 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -56,7 +56,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Slug | Title | Output Type | Status | Notes | |------|-------|-------------|--------|-------| | kling-v2-6-pro-text-to-video | Kling \| v2.6 \| Pro \| Text to Video | video | implemented | Latest Kling, high quality | -| veo3-1-text-to-video | Veo 3.1 \| Text to Video | video | planned | Google Veo 3.1 | +| veo3-1-text-to-video | Veo 3.1 \| Text to Video | video | implemented | Google Veo 3.1 | ## Video Generation (Image-to-Video) @@ -64,7 +64,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an |------|-------|-------------|--------|-------| | kling-v2-6-pro-image-to-video | Kling \| v2.6 \| Pro \| Image to Video | video | implemented | Latest Kling I2V | | kling-o1-image-to-video | Kling O1 \| Image to Video | video | planned | Kling O1 variant | -| veo3-1-image-to-video | Veo 3.1 \| Image to Video | video | planned | Google Veo 3.1 I2V | +| veo3-1-image-to-video | Veo 3.1 \| Image to Video | video | implemented | Google Veo 3.1 I2V | --- @@ -90,20 +90,18 @@ This document tracks EachLabs AI models for implementation, focusing on image an - bytedance-seedream-v4-5-edit - gemini-3-pro-image-preview-edit -**Video Generation (T2V):** 1 model +**Video Generation (T2V):** 2 models - kling-v2-6-pro-text-to-video +- veo3-1-text-to-video -**Video Generation (I2V):** 1 model +**Video Generation (I2V):** 2 models - kling-v2-6-pro-image-to-video +- veo3-1-image-to-video ### Planned for Implementation -**Video Generation (T2V):** 1 model -- veo3-1-text-to-video - -**Video Generation (I2V):** 2 models +**Video Generation (I2V):** 1 model - kling-o1-image-to-video -- veo3-1-image-to-video ### Statistics @@ -111,6 +109,6 @@ This document tracks EachLabs AI models for implementation, focusing on image an |----------|-------------|---------|-------| | Image T2I | 7 | 0 | 7 | | Image I2I | 7 | 0 | 7 | -| Video T2V | 1 | 1 | 2 | -| Video I2V | 1 | 2 | 3 | -| **Total** | **16** | **3** | **19** | +| Video T2V | 2 | 0 | 2 | +| Video I2V | 2 | 1 | 3 | +| **Total** | **18** | **1** | **19** | From fe1ed27e73f4522ebd50edc12661c3f121ccd42b Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Thu, 18 Dec 2025 12:19:04 +0100 Subject: [PATCH 19/20] feat(eachlabs): implement Kling O1 image-to-video provider Add new EachLabs provider for Kling O1 image-to-video generation which transforms images into 5 or 10 second videos using Kuaishou's Kling O1 model. --- examples/ai/src/eachlabsProviders.ts | 4 + .../plugin-ai-video-generation-web/README.md | 32 ++++++- .../src/eachlabs/KlingO1.image2video.json | 49 ++++++++++ .../src/eachlabs/KlingO1.image2video.ts | 94 +++++++++++++++++++ .../src/eachlabs/index.ts | 3 + .../translations.json | 8 +- specs/providers/eachlabs/providers.md | 14 +-- 7 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/KlingO1.image2video.json create mode 100644 packages/plugin-ai-video-generation-web/src/eachlabs/KlingO1.image2video.ts diff --git a/examples/ai/src/eachlabsProviders.ts b/examples/ai/src/eachlabsProviders.ts index 5a624e7b..c68ff772 100644 --- a/examples/ai/src/eachlabsProviders.ts +++ b/examples/ai/src/eachlabsProviders.ts @@ -93,6 +93,10 @@ export function createEachLabsProviders(options: EachLabsProviderOptions) { middlewares: [videoRateLimitMiddleware, errorMiddleware], proxyUrl }), + EachLabsVideo.KlingO1ImageToVideo({ + middlewares: [videoRateLimitMiddleware, errorMiddleware], + proxyUrl + }), EachLabsVideo.Veo31ImageToVideo({ middlewares: [videoRateLimitMiddleware, errorMiddleware], proxyUrl diff --git a/packages/plugin-ai-video-generation-web/README.md b/packages/plugin-ai-video-generation-web/README.md index 54d650aa..5fafae53 100644 --- a/packages/plugin-ai-video-generation-web/README.md +++ b/packages/plugin-ai-video-generation-web/README.md @@ -644,7 +644,27 @@ Key features: - Canvas quick-action integration - Maintains image aspect ratio -#### 20. Veo31TextToVideo (Text-to-Video) via EachLabs +#### 20. KlingO1ImageToVideo (Image-to-Video) via EachLabs + +Kling O1 image-to-video model: + +```typescript +import EachLabsVideo from '@imgly/plugin-ai-video-generation-web/eachlabs'; + +image2video: EachLabsVideo.KlingO1ImageToVideo({ + proxyUrl: 'http://your-eachlabs-proxy.com/api/proxy', + // Optional: Configure default values + middlewares: [rateLimitMiddleware, errorMiddleware] +}) +``` + +Key features: +- Transform existing images into videos +- Duration: 5 or 10 seconds +- Canvas quick-action integration +- Maintains image aspect ratio + +#### 22. Veo31TextToVideo (Text-to-Video) via EachLabs Google's Veo 3.1 text-to-video model accessed through EachLabs: @@ -666,7 +686,7 @@ Key features: - Optional audio generation - Async delivery with polling -#### 21. Veo31ImageToVideo (Image-to-Video) via EachLabs +#### 23. Veo31ImageToVideo (Image-to-Video) via EachLabs Google's Veo 3.1 image-to-video model: @@ -1126,6 +1146,12 @@ EachLabsVideo.KlingV26ProTextToVideo(config: EachLabsProviderConfiguration) EachLabsVideo.KlingV26ProImageToVideo(config: EachLabsProviderConfiguration) ``` +#### KlingO1ImageToVideo + +```typescript +EachLabsVideo.KlingO1ImageToVideo(config: EachLabsProviderConfiguration) +``` + #### Veo31TextToVideo ```typescript @@ -1176,6 +1202,7 @@ The plugin automatically registers the following UI components: - Runware Sora2Pro.Image2Video: `ly.img.ai.runware/openai/sora-2-pro/image2video` - EachLabs KlingV26ProTextToVideo: `ly.img.ai.eachlabs/kling-v2-6-pro-text-to-video` - EachLabs KlingV26ProImageToVideo: `ly.img.ai.eachlabs/kling-v2-6-pro-image-to-video` + - EachLabs KlingO1ImageToVideo: `ly.img.ai.eachlabs/kling-o1-image-to-video` - EachLabs Veo31TextToVideo: `ly.img.ai.eachlabs/veo3-1-text-to-video` - EachLabs Veo31ImageToVideo: `ly.img.ai.eachlabs/veo3-1-image-to-video` @@ -1207,6 +1234,7 @@ Generated videos are automatically stored in asset sources with the following ID - Runware Sora2Pro.Image2Video: `runware/openai/sora-2-pro/image2video.history` - EachLabs KlingV26ProTextToVideo: `eachlabs/kling-v2-6-pro-text-to-video.history` - EachLabs KlingV26ProImageToVideo: `eachlabs/kling-v2-6-pro-image-to-video.history` +- EachLabs KlingO1ImageToVideo: `eachlabs/kling-o1-image-to-video.history` - EachLabs Veo31TextToVideo: `eachlabs/veo3-1-text-to-video.history` - EachLabs Veo31ImageToVideo: `eachlabs/veo3-1-image-to-video.history` diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/KlingO1.image2video.json b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingO1.image2video.json new file mode 100644 index 00000000..a32f4a1c --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingO1.image2video.json @@ -0,0 +1,49 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "EachLabs Kling O1 Image to Video API", + "version": "1.0.0", + "description": "Kling O1 image-to-video generation via EachLabs" + }, + "components": { + "schemas": { + "KlingO1ImageToVideoInput": { + "title": "KlingO1ImageToVideoInput", + "type": "object", + "properties": { + "image_url": { + "title": "Input Image", + "type": "string", + "format": "uri", + "description": "URL of the image to be used as the start frame", + "x-imgly-builder": { + "component": "ImageUrl" + } + }, + "prompt": { + "title": "Prompt", + "type": "string", + "description": "Motion/action description for video generation", + "x-imgly-builder": { + "component": "TextArea" + } + }, + "duration": { + "title": "Duration", + "type": "string", + "enum": ["5", "10"], + "default": "5", + "description": "The duration of the generated video in seconds", + "x-imgly-enum-labels": { + "5": "5 seconds", + "10": "10 seconds" + } + } + }, + "x-fal-order-properties": ["image_url", "prompt", "duration"], + "required": ["image_url", "prompt"] + } + } + }, + "paths": {} +} diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/KlingO1.image2video.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingO1.image2video.ts new file mode 100644 index 00000000..f64978ae --- /dev/null +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/KlingO1.image2video.ts @@ -0,0 +1,94 @@ +import { + type Provider, + type VideoOutput, + getPanelId +} from '@imgly/plugin-ai-generation-web'; +import { getImageDimensionsFromURL } from '@imgly/plugin-utils'; +import type CreativeEditorSDK from '@cesdk/cesdk-js'; +// @ts-ignore - JSON import +import KlingO1ImageToVideoSchema from './KlingO1.image2video.json'; +import createVideoProvider from './createVideoProvider'; +import { EachLabsProviderConfiguration } from './types'; + +/** + * Input interface for Kling O1 image-to-video + */ +export type KlingO1ImageToVideoInput = { + image_url: string; + prompt: string; + duration?: '5' | '10'; +}; + +/** + * Kling O1 - Image-to-video generation via EachLabs + * + * Features: + * - 5 or 10 second video duration + * - Uses start_image_url as input for video generation + * - High-quality video generation from Kuaishou + */ +export function KlingO1ImageToVideo( + config: EachLabsProviderConfiguration +): (context: { + cesdk: CreativeEditorSDK; +}) => Promise> { + return async ({ cesdk }: { cesdk: CreativeEditorSDK }) => { + const providerId = 'eachlabs/kling-o1-image-to-video'; + + // Set translations for image selection UI + cesdk.i18n.setTranslations({ + en: { + [`panel.${getPanelId(providerId)}.imageSelection`]: + 'Select Image To Animate', + [`libraries.${getPanelId(providerId)}.history.label`]: + 'Generated From Image' + } + }); + + return createVideoProvider( + { + modelSlug: 'kling-o1-image-to-video', + modelVersion: '0.0.1', + providerId, + name: 'Kling O1', + // @ts-ignore - JSON schema types are compatible at runtime + schema: KlingO1ImageToVideoSchema, + inputReference: '#/components/schemas/KlingO1ImageToVideoInput', + cesdk, + supportedQuickActions: { + 'ly.img.createVideo': { + mapInput: (input) => ({ + image_url: input.uri, + prompt: '' + }) + } + }, + getBlockInput: async (input) => { + const { width, height } = await getImageDimensionsFromURL( + input.image_url, + cesdk.engine + ); + const duration = + input.duration != null ? parseInt(input.duration, 10) : 5; + + return { + video: { + width, + height, + duration + } + }; + }, + mapInput: (input) => ({ + // Map image_url to start_image_url for EachLabs API + start_image_url: input.image_url, + prompt: input.prompt, + duration: input.duration ?? '5' + }) + }, + config + ); + }; +} + +export default KlingO1ImageToVideo; diff --git a/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts b/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts index 199d5b5f..4baf8e62 100644 --- a/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts +++ b/packages/plugin-ai-video-generation-web/src/eachlabs/index.ts @@ -2,12 +2,14 @@ import { KlingV26ProTextToVideo } from './KlingV26Pro.text2video'; import { KlingV26ProImageToVideo } from './KlingV26Pro.image2video'; +import { KlingO1ImageToVideo } from './KlingO1.image2video'; import { Veo31TextToVideo } from './Veo31.text2video'; import { Veo31ImageToVideo } from './Veo31.image2video'; const EachLabs = { KlingV26ProTextToVideo, KlingV26ProImageToVideo, + KlingO1ImageToVideo, Veo31TextToVideo, Veo31ImageToVideo }; @@ -20,5 +22,6 @@ export type { EachLabsProviderConfiguration } from './types'; // Re-export individual providers for direct imports export { KlingV26ProTextToVideo } from './KlingV26Pro.text2video'; export { KlingV26ProImageToVideo } from './KlingV26Pro.image2video'; +export { KlingO1ImageToVideo } from './KlingO1.image2video'; export { Veo31TextToVideo } from './Veo31.text2video'; export { Veo31ImageToVideo } from './Veo31.image2video'; diff --git a/packages/plugin-ai-video-generation-web/translations.json b/packages/plugin-ai-video-generation-web/translations.json index 43a42b79..673a2e35 100644 --- a/packages/plugin-ai-video-generation-web/translations.json +++ b/packages/plugin-ai-video-generation-web/translations.json @@ -236,6 +236,12 @@ "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.resolution": "Resolution", "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.resolution.720p": "720p (HD)", "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.resolution.1080p": "1080p (Full HD)", - "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.generate_audio": "Generate Audio" + "ly.img.plugin-ai-video-generation-web.eachlabs/veo3-1-image-to-video.property.generate_audio": "Generate Audio", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-o1-image-to-video.property.image_url": "Source Image", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-o1-image-to-video.property.prompt": "Prompt", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-o1-image-to-video.property.prompt.placeholder": "Describe the motion...", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-o1-image-to-video.property.duration": "Duration", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-o1-image-to-video.property.duration.5": "5 seconds", + "ly.img.plugin-ai-video-generation-web.eachlabs/kling-o1-image-to-video.property.duration.10": "10 seconds" } } \ No newline at end of file diff --git a/specs/providers/eachlabs/providers.md b/specs/providers/eachlabs/providers.md index d5d177d4..16900b83 100644 --- a/specs/providers/eachlabs/providers.md +++ b/specs/providers/eachlabs/providers.md @@ -63,7 +63,7 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Slug | Title | Output Type | Status | Notes | |------|-------|-------------|--------|-------| | kling-v2-6-pro-image-to-video | Kling \| v2.6 \| Pro \| Image to Video | video | implemented | Latest Kling I2V | -| kling-o1-image-to-video | Kling O1 \| Image to Video | video | planned | Kling O1 variant | +| kling-o1-image-to-video | Kling O1 \| Image to Video | video | implemented | Kling O1 variant | | veo3-1-image-to-video | Veo 3.1 \| Image to Video | video | implemented | Google Veo 3.1 I2V | --- @@ -94,14 +94,10 @@ This document tracks EachLabs AI models for implementation, focusing on image an - kling-v2-6-pro-text-to-video - veo3-1-text-to-video -**Video Generation (I2V):** 2 models +**Video Generation (I2V):** 3 models - kling-v2-6-pro-image-to-video -- veo3-1-image-to-video - -### Planned for Implementation - -**Video Generation (I2V):** 1 model - kling-o1-image-to-video +- veo3-1-image-to-video ### Statistics @@ -110,5 +106,5 @@ This document tracks EachLabs AI models for implementation, focusing on image an | Image T2I | 7 | 0 | 7 | | Image I2I | 7 | 0 | 7 | | Video T2V | 2 | 0 | 2 | -| Video I2V | 2 | 1 | 3 | -| **Total** | **18** | **1** | **19** | +| Video I2V | 3 | 0 | 3 | +| **Total** | **19** | **0** | **19** | From fffd7e7690986ed4a68fb38baf2f54f6018c9742 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Thu, 18 Dec 2025 15:30:06 +0100 Subject: [PATCH 20/20] docs: add EachLabs partner integration to changelog --- CHANGELOG-AI.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG-AI.md b/CHANGELOG-AI.md index a609daa5..5d3655d5 100644 --- a/CHANGELOG-AI.md +++ b/CHANGELOG-AI.md @@ -2,6 +2,12 @@ ## [Unreleased] +### New Features + +- [all] **EachLabs Partner Integration**: Added EachLabs as a new AI provider service with unified API access to multiple image and video generation models. + - **Image Generation**: Flux 2 Pro, Flux 2, Flux 2 Flex, Nano Banana Pro, OpenAI GPT Image, Seedream v4.5, Gemini 3 Pro (all with text-to-image and image-to-image variants) + - **Video Generation**: Kling v2.6 Pro, Kling O1, Veo 3.1 (text-to-video and image-to-video variants) + ## [0.2.14] - 2025-12-15 ## [0.2.13] - 2025-12-15