diff --git a/docs/advanced-section-controllers.md b/docs/advanced-section-controllers.md index e2df19f735..e0ad90f714 100644 --- a/docs/advanced-section-controllers.md +++ b/docs/advanced-section-controllers.md @@ -1,29 +1,30 @@ # Advanced Section Controllers (GenerationForm) -All controllers/nodes rendered inside the `` section in [GenerationForm.tsx](src/components/generation_v2/GenerationForm.tsx#L757-L1087). +All controllers/nodes rendered inside the `` section in [GenerationForm.tsx](src/components/generation_v2/GenerationForm.tsx#L1974-L2629). | # | Node Name | Label | Input Component | Description | Conditional | |---|-----------|-------|-----------------|-------------|-------------| -| 1 | `resolution` | Resolution | `SegmentedControlWrapper` | Video quality/resolution selector | Wan/Sora video ecosystems | -| 2 | `cfgScale` | CFG Scale | `SliderInput` | Controls how closely generation follows the text prompt | — | -| 3 | `sampler` | Sampler | `SelectInput` | Sampling method — each produces different results | — | -| 4 | `scheduler` | Scheduler | `SelectInput` | Controls the noise schedule during generation | SdCpp ecosystems | -| 5 | `steps` | Steps | `SliderInput` | Number of iterations spent generating | — | -| 6 | `movementAmplitude` | Movement Amplitude | `SegmentedControlWrapper` | Camera movement and subject action scale | Vidu | -| 7 | `seed` | Seed | `SeedInput` | Random seed for reproducibility | — | -| 8 | `clipSkip` | CLIP Skip | `SliderInput` | Skip CLIP layers | SD only | -| 9 | `denoise` | Denoise Strength | `SliderInput` | Denoising strength for img2img | img2img only (renders `null` when no meta) | -| 10 | `vae` | VAE | `ResourceSelectInput` | Additional color and detail improvements | SD only | -| 11 | `enhancedCompatibility` | Enhanced Compatibility | `Checkbox` | Toggle enhanced compatibility mode | — | -| 12 | `usePro` | Pro Mode | `Checkbox` | Higher quality generation (more credits) | Sora | -| 13 | `fluxUltraRaw` | Raw Mode | `Checkbox` | More natural, less processed look | Flux Ultra | -| 14 | `transparent` | Transparent Background | `Checkbox` | Generate image with transparent background | OpenAI | -| 15 | `quality` | Quality | `SelectInput` | Quality level selector | OpenAI | -| 16 | `enablePromptEnhancer` | Enhance prompt | `Checkbox` | Automatically improve prompt for better results | Video ecosystems | -| 17 | `draft` | Draft Mode | `Checkbox` | Generate faster at lower quality | Wan v2.2-5b | -| 18 | `shift` | Shift | `SliderInput` | Shift parameter | Wan v2.2, v2.2-5b | -| 19 | `interpolatorModel` | Interpolator | `SelectInput` | Interpolator model selector | Wan v2.2 | +| 1 | `cfgScale` | CFG Scale | `SliderInput` | Controls how closely generation follows the text prompt | — | +| 2 | `sampler` | Sampler | `SelectInput` | Sampling method — each produces different results | — | +| 3 | `scheduler` | Scheduler | `SelectInput` | Controls the noise schedule during generation | SdCpp ecosystems | +| 4 | `steps` | Steps | `SliderInput` | Number of iterations spent generating | — | +| 5 | `movementAmplitude` | Movement Amplitude | `SegmentedControlWrapper` | Camera movement and subject action scale | Vidu | +| 6 | `seed` | Seed | `SeedInput` | Random seed for reproducibility | — | +| 7 | `clipSkip` | CLIP Skip | `SliderInput` | Skip CLIP layers | SD only | +| 8 | `denoise` | Denoise Strength | `SliderInput` | Denoising strength for img2img | img2img only (renders `null` when no meta) | +| 9 | `vae` | VAE | `ResourceSelectInput` | Additional color and detail improvements | SD only | +| 10 | `enhancedCompatibility` | Enhanced Compatibility | `Checkbox` | Toggle enhanced compatibility mode | — | +| 11 | `usePro` | Pro Mode | `Checkbox` | Higher quality generation (more credits) | Sora | +| 12 | `fluxUltraRaw` | Raw Mode | `Checkbox` | More natural, less processed look | Flux Ultra | +| 13 | `transparent` | Transparent Background | `Checkbox` | Generate image with transparent background | OpenAI | +| 14 | `quality` | Quality | `SelectInput` | Quality level selector | OpenAI | +| 15 | `enablePromptEnhancer` | Enhance prompt | `Checkbox` | Automatically improve prompt for better results | Video ecosystems | +| 16 | `draft` | Draft Mode | `Checkbox` | Generate faster at lower quality | Wan v2.2-5b | +| 17 | `shift` | Shift | `SliderInput` | Shift parameter | Wan v2.2, v2.2-5b | +| 18 | `interpolatorModel` | Interpolator | `SelectInput` | Interpolator model selector | Wan v2.2 | -**Note:** There is also a commented-out duplicate `draft` controller (lines 1075–1086) labeled "Turbo Mode" for Wan v2.2 — currently inactive. +**Not in this section:** `resolution` renders in the main form body, directly above `aspectRatio` — not in Advanced. It is a quality tier on the video ecosystems and a base-resolution tier on the image ecosystems that expose it (Seedream, Nano Banana, Lens, HiDream-O1, and Krea 2's comfy builds, where 1K/2K scales the aspect-ratio dimensions). + +**Note:** There is also a commented-out duplicate `draft` controller (lines 2587–2598) labeled "Turbo Mode" for Wan v2.2 — currently inactive. All controllers use the `` pattern and only render when the current workflow/ecosystem graph exposes the corresponding node. diff --git a/docs/form-graph-port-plan.md b/docs/form-graph-port-plan.md index 9586b8eaee..b6d1785c94 100644 --- a/docs/form-graph-port-plan.md +++ b/docs/form-graph-port-plan.md @@ -442,7 +442,7 @@ home); full suite + typecheck + lint green; Briant reviews the final diff. | image: flux2 (dev/flex/pro/max) | DONE | DONE | mode by model.id; only dev carries resources | | image: flux2-klein (9B/9B-base/4B/4B-base) | DONE | DONE | FOUR ecosystems share the graph — mode from ecosystem, not model. negativePrompt is NOT a snippet target (v1's own comment claims it self-registers; the differential says no). Handler pins distilled steps/cfg even though the graph exposes a steps slider — v1 quirk, mirrored | | image: boogu (base/turbo/edit/editTurbo) | DONE | DONE | workflowVersions: version options are WORKFLOW-scoped and the MODEL WINS the workflow (probed — an edit checkpoint on txt2img parses as img2img:edit, model kept; v1's index-remap transform is dead code). The cross-workflow rewrite lives in `reconcile.ts` (`deriveWorkflowFromModel`, registry `workflowScopedVersions`) since a family cannot change the root workflow. negativePrompt only in base/edit modes | -| image: krea2 (fal/raw/turbo/editRaw/editTurbo) | DONE | DONE | locked checkpoint; version selector splits across FAL (creativity/styleReferences) and comfy (LoRA/cfg/steps) engines; img2img:edit narrows the picker to comfy builds and the lock substitutes FAL tiers to the edit default. NO cross-workflow pull (unlike boogu) | +| image: krea2 (fal/raw/turbo/editRaw/editTurbo) | DONE | DONE | locked checkpoint; version selector splits across FAL (creativity/styleReferences) and comfy (LoRA/cfg/steps) engines; img2img:edit narrows the picker to comfy builds and the lock substitutes FAL tiers to the edit default. 1K/2K resolution tier on the comfy builds only (`defFamily` AR, 2K doubles each side); FAL takes size + aspectRatio, so no tier. NO cross-workflow pull (unlike boogu) | | image: imagen4 | DONE | DONE | locked single version; negative prompt registers at top level | | image: pony-v7 | DONE | DONE | LoRAs, no negative prompt; ecosystem-defaults lock applies | | image: reve | DONE | DONE | locked; AR on txt2img only, edit takes 1-4 reference frames | diff --git a/src/components/form-graph/generation/ImageGenerationForm.tsx b/src/components/form-graph/generation/ImageGenerationForm.tsx index 637e8ad8ba..a11af56016 100644 --- a/src/components/form-graph/generation/ImageGenerationForm.tsx +++ b/src/components/form-graph/generation/ImageGenerationForm.tsx @@ -336,6 +336,23 @@ export function ImageGenerationForm({ store }: { store: GenerationStore }) { /> )} /> + ( +
+ Resolution + onChange(v as typeof value)} + data={(meta as { options: { label: string; value: string }[] }).options.map((o) => ({ + label: o.label, + value: o.value, + }))} + /> +
+ )} + /> - ( -
- Resolution - onChange(v as typeof value)} - data={(meta as { options: { label: string; value: string }[] }).options.map((o) => ({ - label: o.label, - value: o.value, - }))} - /> -
- )} - /> + {/* Resolution */} + ( +
+ Resolution + onChange(v as typeof value)} + data={meta.options.map((o: { label: string; value: string }) => ({ + label: o.label, + value: o.value, + }))} + /> +
+ )} + /> + {/* Aspect ratio */} - {/* Resolution (Wan/Sora video quality) */} - ( -
- Resolution - onChange(v as typeof value)} - data={meta.options.map((o: { label: string; value: string }) => ({ - label: o.label, - value: o.value, - }))} - /> -
- )} - /> - {/* Prime (Wan 3.0) */} { expect(stepsMax(g)).toBe(60); }); }); + +describe('krea2 base resolution tier', () => { + const dimsOf = (g: any) => { + const { width, height } = g.getSnapshot().aspectRatio ?? {}; + return { width, height }; + }; + + it('is offered on the comfy builds and defaults to 1K', () => { + const g = init('txt2img', krea2VersionIds.raw); + expect(g.hasNode('resolution')).toBe(true); + expect(g.getSnapshot().resolution).toBe('1K'); + expect(dimsOf(g)).toEqual({ width: 1024, height: 1024 }); + }); + + it('doubles every side at 2K, keeping each divisible by 32', () => { + const g = init('txt2img', krea2VersionIds.turbo); + g.set({ resolution: '2K', aspectRatio: '16:9' }); + expect(dimsOf(g)).toEqual({ width: 2752, height: 1536 }); + const { width, height } = dimsOf(g); + expect(width % 32).toBe(0); + expect(height % 32).toBe(0); + }); + + // FAL takes size + aspectRatio only, so a tier there would promise dimensions + // the orchestrator never reads. + it('is hidden on the FAL size tiers', () => { + expect(init('txt2img', krea2VersionIds.medium).hasNode('resolution')).toBe(false); + expect(init('txt2img', krea2VersionIds.large).hasNode('resolution')).toBe(false); + }); + + it('survives on edit, where a FAL version still runs a comfy build', () => { + expect(init('img2img:edit', krea2VersionIds.medium).hasNode('resolution')).toBe(true); + expect(init('txt2img', communityVersionId).hasNode('resolution')).toBe(true); + }); +}); diff --git a/src/shared/data-graph/generation/krea2-graph.ts b/src/shared/data-graph/generation/krea2-graph.ts index 2af502ad8d..dd55e938d4 100644 --- a/src/shared/data-graph/generation/krea2-graph.ts +++ b/src/shared/data-graph/generation/krea2-graph.ts @@ -136,12 +136,31 @@ const krea2AspectRatioDimensions = { '9:16': { width: 768, height: 1376 }, } satisfies Partial>; -const krea2AspectRatioOptions = ( - Object.keys(krea2AspectRatioDimensions) as (keyof typeof krea2AspectRatioDimensions)[] -).map((ratio) => { - const { width, height } = krea2AspectRatioDimensions[ratio]; - return { label: ratio, value: ratio, width, height }; -}); +/** 2K doubles each ~1MP bucket to ~4MP. The FAL tiers take `size` + `aspectRatio` only, so they get no tier. */ +const krea2ResolutionOptions = [ + { label: '1K', value: '1K' }, + { label: '2K', value: '2K' }, +] as const; + +const krea2AspectRatioOptionsFor = (scale: number) => + (Object.keys(krea2AspectRatioDimensions) as (keyof typeof krea2AspectRatioDimensions)[]).map( + (ratio) => { + const { width, height } = krea2AspectRatioDimensions[ratio]; + return { label: ratio, value: ratio, width: width * scale, height: height * scale }; + } + ); + +const krea2AspectRatioOptionsByResolution: Record< + string, + ReturnType +> = { + '1K': krea2AspectRatioOptionsFor(1), + '2K': krea2AspectRatioOptionsFor(2), +}; + +/** Edit runs a comfy build whichever version is picked, so it keeps the tier; unknown ids are community checkpoints, comfy-only. */ +const krea2UsesComfyEngine = (modelId?: number, workflow?: string) => + workflow === 'img2img:edit' || modelId === undefined || !krea2VersionIdToSize.has(modelId); /** Standard preferred ratios — substitute 4:5 for 3:4 since Krea lacks 3:4. */ const krea2PriorityRatios = ['16:9', '4:3', '1:1', '4:5', '9:16']; @@ -326,13 +345,23 @@ export const krea2Graph = new DataGraph< }, ['workflow'] ) + .node( + 'resolution', + (ctx) => ({ + ...enumNode({ options: krea2ResolutionOptions, defaultValue: '1K' }), + when: krea2UsesComfyEngine(ctx.model?.id, ctx.workflow), + }), + ['model', 'workflow'] + ) .node( 'aspectRatio', - aspectRatioNode({ - options: krea2AspectRatioOptions, - defaultValue: '1:1', - priorityOptions: krea2PriorityRatios, - }) + (ctx) => + aspectRatioNode({ + options: krea2AspectRatioOptionsByResolution[ctx.resolution ?? '1K'], + defaultValue: '1:1', + priorityOptions: krea2PriorityRatios, + }), + ['resolution'] ) // Unknown ids are community checkpoints. Only the comfy builds can load one via // `diffusionModel`, so they fall back off the FAL tiers — and to the full-step diff --git a/src/shared/form-graph/generation/__tests__/image-parity.test.ts b/src/shared/form-graph/generation/__tests__/image-parity.test.ts index a787e26dd5..44947aa6f6 100644 --- a/src/shared/form-graph/generation/__tests__/image-parity.test.ts +++ b/src/shared/form-graph/generation/__tests__/image-parity.test.ts @@ -203,6 +203,9 @@ const KREA2_ONLY_SHAPES: AnyRecord[] = [ styleReferences: [{ image: 'https://example.com/s.png', strength: 0.7 }, {}], }, { prompt: 'a cat', model: 3072332 }, // comfy turbo + { prompt: 'a cat', model: 3072332, resolution: '2K' }, // comfy 2K tier + { prompt: 'a cat', model: 2983023, resolution: '2K' }, // FAL: no tier to accept + { prompt: 'a cat', model: 2983023, images: [IMG], resolution: '2K' }, // edit keeps the tier { prompt: 'a cat', model: 2983023, images: [IMG] }, // fal model where edit rows substitute // community checkpoints (unknown ids): comfy-only, so they fall to the RAW // full-step build — and to editRaw on edit (kaydaxter's custom-checkpoint fix) diff --git a/src/shared/form-graph/generation/image/krea2.graph.ts b/src/shared/form-graph/generation/image/krea2.graph.ts index adc3ea4545..8a517f967f 100644 --- a/src/shared/form-graph/generation/image/krea2.graph.ts +++ b/src/shared/form-graph/generation/image/krea2.graph.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { branch, defineGraph } from 'form-graph'; +import { branch, defFamily, defineGraph } from 'form-graph'; import { checkpointDef } from '../checkpoint'; import { SEED, aspectRatioDef, enumDef, imagesDef, workflowScoped } from '../defs'; import { @@ -70,10 +70,29 @@ const krea2AspectRatioDimensions: Record { - const { width, height } = krea2AspectRatioDimensions[ratio]!; - return { label: ratio, value: ratio, width, height }; -}); +/** 2K doubles each ~1MP bucket to ~4MP. */ +const krea2ResolutionOptions = [ + { label: '1K', value: '1K' }, + { label: '2K', value: '2K' }, +] as const; + +const krea2AspectRatioOptionsFor = (scale: number) => + Object.keys(krea2AspectRatioDimensions).map((ratio) => { + const { width, height } = krea2AspectRatioDimensions[ratio]!; + return { label: ratio, value: ratio, width: width * scale, height: height * scale }; + }); + +const krea2AspectRatioOptionsByResolution: Record< + string, + ReturnType +> = { + '1K': krea2AspectRatioOptionsFor(1), + '2K': krea2AspectRatioOptionsFor(2), +}; + +/** Edit and community checkpoints are comfy-only, so they keep the tier too. */ +const krea2UsesComfyEngine = (modelId?: number, workflow?: string) => + workflow === 'img2img:edit' || modelId == null || !krea2VersionIdToSize.has(modelId); const krea2PriorityRatios = ['16:9', '4:3', '1:1', '4:5', '9:16']; @@ -207,6 +226,17 @@ const editRaw = defineGraph() /** Tagged: v1's `krea2Variant` computed becomes the branch key. */ const variants = branch('krea2Variant', variantOf, { fal, raw, turbo, editRaw, editTurbo }); +const RESOLUTION = enumDef({ options: krea2ResolutionOptions, default: '1K' }); + +const AR = defFamily((resolution: string) => + aspectRatioDef({ + options: + krea2AspectRatioOptionsByResolution[resolution] ?? krea2AspectRatioOptionsByResolution['1K']!, + default: '1:1', + priorityOptions: krea2PriorityRatios, + }) +); + export const krea2 = defineGraph({ scope: familyScope }) .field('model', ({ _ext }) => { const isEdit = _ext.workflow === 'img2img:edit'; @@ -218,14 +248,10 @@ export const krea2 = defineGraph({ scope: familyScope }) defaultModelId: isEdit ? KREA2_EDIT_DEFAULT_VERSION_ID : krea2VersionIds.raw, }); }) - .field( - 'aspectRatio', - aspectRatioDef({ - options: krea2AspectRatioOptions, - default: '1:1', - priorityOptions: krea2PriorityRatios, - }) + .field('resolution', ({ model, _ext }) => + krea2UsesComfyEngine(modelIdOf(model) ?? undefined, _ext.workflow) ? RESOLUTION : null ) + .field('aspectRatio', ({ resolution }) => AR(resolution ?? '1K')) .use(variants) // negativePrompt exists only in the comfy variants; its in-branch snippet // registration never fires