diff --git a/apps/mana/apps/web/src/lib/modules/comic/api/generate-panel.ts b/apps/mana/apps/web/src/lib/modules/comic/api/generate-panel.ts index c44a3e03d..3cfcdb833 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/api/generate-panel.ts +++ b/apps/mana/apps/web/src/lib/modules/comic/api/generate-panel.ts @@ -29,6 +29,29 @@ import type { ComicPanelMeta, ComicStory } from '../types'; */ export type PanelSize = '1024x1024' | '1024x1536'; +/** + * Models that can drive panel rendering. Same closed set as + * Wardrobe's Try-On picker so character consistency between a + * user's outfit try-ons and their comic panels stays comparable + * (different models ≈ different faces). + * + * - `openai/gpt-image-2` — existing default, mid-tier cost. + * Server-side transparent fallback to gpt-image-1 for + * unverified OpenAI orgs; see apps/api picture/routes.ts. + * - `google/gemini-3-pro-image-preview` — Nano Banana Pro. + * Strong character consistency across panels, higher cost. + * - `google/gemini-3.1-flash-image-preview` — Nano Banana 2. + * Newest + fast + cheap, good default for drafts. + * + * Credit tarifs are set by creditsFor() in picture/routes.ts. + */ +export type PanelModel = + | 'openai/gpt-image-2' + | 'google/gemini-3-pro-image-preview' + | 'google/gemini-3.1-flash-image-preview'; + +export const DEFAULT_PANEL_MODEL: PanelModel = 'openai/gpt-image-2'; + export interface RunPanelGenerateParams { story: ComicStory; panelPrompt: string; @@ -39,6 +62,10 @@ export interface RunPanelGenerateParams { sourceInput?: ComicPanelMeta['sourceInput']; quality?: 'low' | 'medium' | 'high'; size?: PanelSize; + /** Rendering backend — defaults to `DEFAULT_PANEL_MODEL`. Mirrored + * from Wardrobe so users can pick per-call without a story-level + * schema change. See `PanelModelPicker.svelte`. */ + model?: PanelModel; } export interface RunPanelGenerateResult { @@ -64,6 +91,7 @@ async function callGenerateWithReference(opts: { referenceMediaIds: string[]; quality: 'low' | 'medium' | 'high'; size: PanelSize; + model: PanelModel; }): Promise<{ imageUrl: string; mediaId: string; prompt: string; model: string }> { const token = await authStore.getValidToken(); const res = await fetch(`${getManaApiUrl()}/api/v1/picture/generate-with-reference`, { @@ -75,7 +103,7 @@ async function callGenerateWithReference(opts: { body: JSON.stringify({ prompt: opts.prompt, referenceMediaIds: opts.referenceMediaIds, - model: 'openai/gpt-image-2', + model: opts.model, quality: opts.quality, size: opts.size, n: 1, @@ -153,6 +181,7 @@ export async function runPanelGenerate( const effectiveSize: PanelSize = params.size ?? (story.style === 'webtoon' ? '1024x1536' : '1024x1024'); const effectiveQuality = params.quality ?? 'medium'; + const effectiveModel: PanelModel = params.model ?? DEFAULT_PANEL_MODEL; // Cap at 8 references (server limit). If the story somehow has more // in its characterMediaIds (shouldn't — UI caps at ~5), truncate and @@ -164,6 +193,7 @@ export async function runPanelGenerate( referenceMediaIds, quality: effectiveQuality, size: effectiveSize, + model: effectiveModel, }); const now = new Date().toISOString(); diff --git a/apps/mana/apps/web/src/lib/modules/comic/components/BatchPanelEditor.svelte b/apps/mana/apps/web/src/lib/modules/comic/components/BatchPanelEditor.svelte index 41caa0510..a0e20f007 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/components/BatchPanelEditor.svelte +++ b/apps/mana/apps/web/src/lib/modules/comic/components/BatchPanelEditor.svelte @@ -23,9 +23,15 @@ WarningCircle, X, } from '@mana/shared-icons'; - import { runPanelGenerate, type PanelSize } from '../api/generate-panel'; + import { + runPanelGenerate, + DEFAULT_PANEL_MODEL, + type PanelModel, + type PanelSize, + } from '../api/generate-panel'; import { MAX_PANELS_PER_STORY, PANEL_COUNT_WARN_THRESHOLD } from '../constants'; import type { ComicStory } from '../types'; + import PanelModelPicker from './PanelModelPicker.svelte'; interface Props { story: ComicStory; @@ -56,6 +62,7 @@ let rows = $state([emptyRow(), emptyRow()]); let quality = $state('medium'); + let model = $state(DEFAULT_PANEL_MODEL); // svelte-ignore state_referenced_locally let size = $state(story.style === 'webtoon' ? '1024x1536' : '1024x1024'); @@ -103,6 +110,7 @@ dialogue: row.dialogue.trim() || undefined, quality, size, + model, }); rowStatus[row.id] = { status: 'ok' }; return result.imageId; @@ -312,6 +320,8 @@ + (model = m)} disabled={submitting} /> +
Qualität: diff --git a/apps/mana/apps/web/src/lib/modules/comic/components/PanelEditor.svelte b/apps/mana/apps/web/src/lib/modules/comic/components/PanelEditor.svelte index 8ff80002a..2c1ad083d 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/components/PanelEditor.svelte +++ b/apps/mana/apps/web/src/lib/modules/comic/components/PanelEditor.svelte @@ -14,9 +14,15 @@ --> + +
+ Modell +
+ {#each OPTIONS as opt (opt.id)} + + {/each} +
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/comic/components/StoryboardSuggester.svelte b/apps/mana/apps/web/src/lib/modules/comic/components/StoryboardSuggester.svelte index 28bacbc97..ab9f69f54 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/components/StoryboardSuggester.svelte +++ b/apps/mana/apps/web/src/lib/modules/comic/components/StoryboardSuggester.svelte @@ -25,7 +25,12 @@ WarningCircle, X, } from '@mana/shared-icons'; - import { runPanelGenerate, type PanelSize } from '../api/generate-panel'; + import { + runPanelGenerate, + DEFAULT_PANEL_MODEL, + type PanelModel, + type PanelSize, + } from '../api/generate-panel'; import { suggestPanels, type StoryboardSourceModule, @@ -40,6 +45,7 @@ } from '../constants'; import type { ComicStory } from '../types'; import ReferenceInputPicker, { type ReferenceSelection } from './ReferenceInputPicker.svelte'; + import PanelModelPicker from './PanelModelPicker.svelte'; interface Props { story: ComicStory; @@ -65,6 +71,7 @@ const QUALITIES: readonly Quality[] = ['low', 'medium', 'high'] as const; const CREDIT_COST: Record = { low: 3, medium: 10, high: 25 }; let quality = $state('medium'); + let model = $state(DEFAULT_PANEL_MODEL); // svelte-ignore state_referenced_locally let size = $state(story.style === 'webtoon' ? '1024x1536' : '1024x1024'); @@ -123,6 +130,7 @@ dialogue: row.dialogue?.trim() || undefined, quality, size, + model, sourceInput: { module: selection.module, entryId: selection.entryId, @@ -391,6 +399,8 @@ {/if} + (model = m)} disabled={renderBusy} /> +
Qualität: diff --git a/apps/mana/apps/web/src/lib/modules/comic/tools.ts b/apps/mana/apps/web/src/lib/modules/comic/tools.ts index 27ac82079..4db0dc2d7 100644 --- a/apps/mana/apps/web/src/lib/modules/comic/tools.ts +++ b/apps/mana/apps/web/src/lib/modules/comic/tools.ts @@ -30,9 +30,19 @@ import { scopedForModule } from '$lib/data/scope'; import { decryptRecords, VaultLockedError } from '$lib/data/crypto'; import { meImagesTable } from '$lib/modules/profile/collections'; import { comicStoriesStore } from './stores/stories.svelte'; -import { runPanelGenerate } from './api/generate-panel'; +import { runPanelGenerate, DEFAULT_PANEL_MODEL, type PanelModel } from './api/generate-panel'; import { toStory } from './types'; import type { ComicStyle, LocalComicStory } from './types'; + +const VALID_MODELS: readonly PanelModel[] = [ + 'openai/gpt-image-2', + 'google/gemini-3-pro-image-preview', + 'google/gemini-3.1-flash-image-preview', +] as const; + +function isValidModel(v: unknown): v is PanelModel { + return typeof v === 'string' && (VALID_MODELS as readonly string[]).includes(v); +} import type { LocalMeImage } from '$lib/modules/profile/types'; import { getActiveSpace } from '$lib/data/scope'; @@ -265,6 +275,14 @@ export const comicTools: ModuleTool[] = [ required: false, enum: ['low', 'medium', 'high'], }, + { + name: 'model', + type: 'string', + description: + 'Rendering-Backend (Default openai/gpt-image-2). Alternativen: google/gemini-3-pro-image-preview (Nano Banana Pro), google/gemini-3.1-flash-image-preview (Nano Banana 2).', + required: false, + enum: [...VALID_MODELS], + }, ], async execute(params) { const storyId = String(params.storyId ?? '').trim(); @@ -283,6 +301,7 @@ export const comicTools: ModuleTool[] = [ : undefined; const quality = params.quality === 'low' || params.quality === 'high' ? params.quality : 'medium'; + const model = isValidModel(params.model) ? params.model : DEFAULT_PANEL_MODEL; try { // Load the story for runPanelGenerate — same code path as the @@ -306,6 +325,7 @@ export const comicTools: ModuleTool[] = [ caption, dialogue, quality: quality as 'low' | 'medium' | 'high', + model, }); return { diff --git a/docs/plans/comic-module.md b/docs/plans/comic-module.md index c5a22beed..a3f8fad0c 100644 --- a/docs/plans/comic-module.md +++ b/docs/plans/comic-module.md @@ -104,6 +104,27 @@ auseinanderdriften, adressieren wir das mit einer zusätzlichen "Anchor-Panel"-Referenz (erstes erzeugtes Panel wird Referenz für alle folgenden) — das ist M6+. +### 2.1 Image-Modell als Picker, nicht hartcodiert (nachgezogen) + +Comic nutzt die gleiche Model-Auswahl wie Wardrobe's Try-On: + +- `openai/gpt-image-2` — Default, mittlerer Preis, fällt server-seitig + auf gpt-image-1 zurück wenn die OpenAI-Org nicht verified ist. +- `google/gemini-3-pro-image-preview` — Nano Banana Pro, hohe + Charakter-Konsistenz, höherer Preis. +- `google/gemini-3.1-flash-image-preview` — Nano Banana 2, neuestes, + schnell, günstig. + +`PanelModelPicker` (Analog zu `TryOnModelPicker`) sitzt als +segmentierter Picker in PanelEditor / BatchPanelEditor / +StoryboardSuggester. Die Wahl ist per-Editor-Mount lokal; keine +Story-Level-Persistierung, weil ein Model-Flag auf der Row eine +Migration bräuchte und die Wahl meistens eh ad-hoc ist. + +MCP-Tool `comic.generatePanel` und Catalog-Tool `generate_comic_panel` +akzeptieren beide einen optionalen `model`-Parameter mit demselben +Enum. Default bleibt `openai/gpt-image-2`. + ### 3. Fünf Stil-Presets, Mapping im Client ```typescript diff --git a/packages/mana-tool-registry/src/modules/comic.ts b/packages/mana-tool-registry/src/modules/comic.ts index 13208ea2e..f4820ffbc 100644 --- a/packages/mana-tool-registry/src/modules/comic.ts +++ b/packages/mana-tool-registry/src/modules/comic.ts @@ -282,6 +282,21 @@ const generatePanelInput = z.object({ /** 1024×1024 square is the default; pass `1024x1536` for vertical * framings (e.g. webtoon tall panels). */ size: z.enum(['1024x1024', '1024x1536']).optional(), + /** Rendering backend. Same closed set as Wardrobe's Try-On picker: + * - `openai/gpt-image-2` (default) — mid-tier cost, strong + * structure, server-side fallback to gpt-image-1 if org is + * unverified. + * - `google/gemini-3-pro-image-preview` — Nano Banana Pro, strong + * character consistency, higher cost. + * - `google/gemini-3.1-flash-image-preview` — Nano Banana 2, + * newest + fast + cheap. */ + model: z + .enum([ + 'openai/gpt-image-2', + 'google/gemini-3-pro-image-preview', + 'google/gemini-3.1-flash-image-preview', + ]) + .default('openai/gpt-image-2'), }); const generatePanelOutput = z.object({ @@ -353,7 +368,7 @@ export const comicGeneratePanel: ToolSpec