/** * Receptury ujęć — prompty modelu image-edit (EN) + opis dla operatora (PL). * * Zasady redakcyjne (audyt 2026-09-28, rozdzielenie ról referencji 2026-10): * - jedna instrukcja na temat, bez powtórzeń i wzajemnych sprzeczności * (np. „stationary sofa only” przy narożniku z funkcją spania); * - „Image N” odpowiada realnie wysłanemu obrazowi; niewysłanego obrazu * prompt nie wymienia; montaż opiera się na 4 jawnych slotach * (ShotRefSlots): kadr, próbka, tkanina na meblu, nóżki; * - role obrazów są rozdzielone: kadr (slot 1) niesie wyłącznie geometrię * i ujęcie — gdy jadą referencje tkaniny, jego tapicerka/tło/metki są * ignorowane; sloty 2–3 niosą tkaninę (bez nich tapicerka pochodzi * z kadru); * - tekst NIE opisuje struktury tkaniny (prążki, walesy, bouclé…) — * strukturę niosą obrazy; jedyna tekstowa nota to opcjonalny opis * operatora per kolekcja (fabrics.ai_prompt). Szablon jest neutralny * wobec tkaniny — to samo działa dla sztruksu i bouclé; * - bez fraz kadrujących typu „sofa centered/hero” — kadr i perspektywa * pochodzą z Image 1; * - tylko wytyczne studyjne: kąt, perspektywa, światło, materiał, zakazy dodawania. * * Ścieżka studyjna (domain/studio.ts) — izolacja mebla ze zdjęcia hali — * używa roomScenePrompt/materialSwapPrompt; bryła jest tam chroniona * pikselowo (bramka IoU), nie samym tekstem. */ import type { ShotRole } from "../domain/types.js"; export interface ShotRecipe { role: ShotRole; label: string; /** Prompt pozytywny. */ prompt: string; /** Sztywna lista zakazów; provider nie ma kanału negatywnego, więc `shotRecipePrompt` dokleja ją jako „Strictly no". */ negativePrompt: string; preserve: string[]; change: string[]; maxRefs: number; } /** * 4 jawne sloty referencji ujęcia (id z source_assets): * 1. geometryRefId — kadr mebla (kąt i bryła), * 2. fabricSwatchRefId — próbka producenta (fabric_shades.official_image_path), * 3. fabricRealRefId — tkanina na meblu (fabric_shades.photo_path), * 4. legRefId — nóżki (furniture_legs.image_asset_id). */ export interface ShotRefSlots { geometryRefId: string | null; fabricSwatchRefId: string | null; fabricRealRefId: string | null; legRefId: string | null; } export const EMPTY_REF_SLOTS: ShotRefSlots = { geometryRefId: null, fabricSwatchRefId: null, fabricRealRefId: null, legRefId: null }; /** Kolejność wysyłki do providera: geometria, próbka, tkanina na meblu, nóżki (bez duplikatów). */ export function slotRefOrder(slots: ShotRefSlots): string[] { return [...new Set([slots.geometryRefId, slots.fabricSwatchRefId, slots.fabricRealRefId, slots.legRefId].filter((id): id is string => Boolean(id)))]; } interface RecipeContext { productName: string; /** Liczba luźnych poduszek z POTWIERDZONEGO faktu; null = brak klauzuli w prompcie. */ cushionCount: number | null; /** ai_prompt wybranej nóżki (furniture_legs.ai_prompt). Pusty = nogi jak w referencji. */ legsNote: string; /** ai_prompt tkaniny bazowej (fabrics.ai_prompt). Pusty = struktura tkaniny wyłącznie z referencji. */ fabricPrompt?: string; /** Sztywna nota sylwetki z faktów (geometry.silhouette_note). */ silhouetteNote?: string; /** Jawne id zasobów w slotach. */ slots: ShotRefSlots; /** Realna kolejność `images` wysłanych do providera — z niej wynika numer „Image N". */ sentIds: string[]; } const PRESERVE = [ "identyczna bryła, proporcje i geometria mebla z kadru referencyjnego", "strona narożnika / szezlonga jak w referencji", "liczba i układ poduszek widocznych w referencji", "tkanina: splot, faktura i kolor jak na wysłanych referencjach", "szwy, wykończenie, widoczny stan mechanizmu", ]; const CHANGE_STUDIO = [ "tło: hala → bezszwowe białe tło studyjne", "miękkie, równe światło studyjne i cień kontaktowy", "usunięcie innych mebli, ludzi i oznaczeń", ]; /** Materiał — przeciw „plastikowemu” połyskowi obserwowanemu na masterach v1–v6. * Bez opisu struktury — splot i fakturę niosą referencje tkaniny (sloty 2–3) * albo kadr; szablon musi być neutralny dla każdej kolekcji. */ const MATTE = "The upholstery has a matte finish; no gloss, no plastic look."; const NEGATIVE = [ "distorted, redesigned or re-proportioned furniture body", "changed camera angle or perspective", "extra pillows, throws, props, people or text", "labels, logos or tags on the furniture", "fabric stripes, ribs, channels or patterns not present in the fabric references", "artifacts, puckering or broken stitching on seams", "blurred or smeared fabric texture", "glossy plastic upholstery", "wrong, missing or extra legs", ].join("; "); const STUDIO = "Seamless pure white background RGB(255, 255, 255), soft diffuse studio light, soft contact shadow on the floor."; /** Role, w których oparcie z poduszkami jest widoczne — tam pilnujemy liczby poduszek. */ const CUSHION_VISIBLE_ROLES = new Set(["hero", "packshot_front", "packshot_34", "side", "back"]); const ROLE_VIEW: Partial> = { packshot_front: "Straight front view.", packshot_34: "Three-quarter view.", side: "Side profile view.", back: "Rear view.", sleep: "Show the unfolded sleeping state exactly as in Image 1.", storage: "Show the open storage exactly as in Image 1, only its visible parts.", detail: "Close-up of the upholstery exactly as in Image 1.", }; const LABELS: Partial> = { hero: "Hero — aranżacja", packshot_front: "Packshot front", packshot_34: "Packshot 3/4", side: "Bok", back: "Tył", sleep: "Spanie — stan z referencji", storage: "Pojemnik — stan z referencji", detail: "Detal tkaniny", }; /** Numer „Image N" (1-based) slotu wśród faktycznie wysłanych obrazów; 0 = slot pusty lub niewysłany. */ function imageNo(ctx: RecipeContext, id: string | null): number { const i = id ? ctx.sentIds.indexOf(id) : -1; return i + 1; } /** Pozytywne klocki slotów — tylko obrazy faktycznie wysłane. Gdy jadą * referencje tkaniny, kadr jest wyłącznie wzorcem bryły i ujęcia (jego * tapicerka, tło i metki są ignorowane — u FUJI zdjęcia vendora pokazują * inną tkaninę niż docelowa). Bez referencji tkaniny kadr niesie też * tapicerkę. */ function slotClauses(ctx: RecipeContext): string { const g = imageNo(ctx, ctx.slots.geometryRefId) || 1; const s = imageNo(ctx, ctx.slots.fabricSwatchRefId); const r = imageNo(ctx, ctx.slots.fabricRealRefId); const l = imageNo(ctx, ctx.slots.legRefId); const hasFabricRef = s > 0 || r > 0; const geometry = hasFabricRef ? `Image ${g} defines the exact furniture geometry and camera angle only: silhouette, proportions, seams, cushions, chaise side and mechanism state. Keep its perspective; do not redesign, add or remove parts. Ignore the fabric shown on it, the background and any labels or tags.` : `Image ${g} defines the exact furniture geometry, camera angle and upholstery: silhouette, proportions, seams, cushions, chaise side and mechanism state. Keep its perspective; do not redesign, add or remove parts. Ignore the background and any labels or tags.`; const fabric = s && r ? `Image ${s} is the manufacturer's flat swatch; Image ${r} shows the same fabric on furniture. Together they define the upholstery: weave, texture, pile and colour. Re-upholster the furniture in this fabric, matching how the material sits on the body and catches light.` : s ? `Image ${s} is the manufacturer's flat swatch and defines the upholstery: weave, texture and colour. Re-upholster the furniture in this fabric.` : r ? `Image ${r} shows the exact upholstery fabric on furniture and defines its weave, texture, colour and how it sits on the body. Re-upholster the furniture in this fabric.` : ""; return [ geometry, fabric, l ? `Image ${l} defines the exact legs: fit them in the same positions, same count, height and finish.` : "", ].filter(Boolean).join(" "); } /** Fakty z katalogów/produktu — krótkie, bez powtórzeń klocków slotów. */ function factClauses(ctx: RecipeContext, role: ShotRole): string { const hasFabricRef = imageNo(ctx, ctx.slots.fabricSwatchRefId) > 0 || imageNo(ctx, ctx.slots.fabricRealRefId) > 0; const l = imageNo(ctx, ctx.slots.legRefId); const out: string[] = []; if (ctx.fabricPrompt) out.push(hasFabricRef ? `Fabric note: ${ctx.fabricPrompt}.` : `Upholstery: ${ctx.fabricPrompt}.`); if (role === "detail") return out.join(" "); const shape: string[] = []; if (CUSHION_VISIBLE_ROLES.has(role) && ctx.cushionCount != null && ctx.cushionCount > 0) { shape.push(`exactly ${ctx.cushionCount} loose back cushions`); } if (ctx.silhouetteNote) shape.push(ctx.silhouetteNote); if (shape.length) out.push(`${shape.join("; ")}.`.replace(/^./, (c) => c.toUpperCase())); if (ctx.legsNote && !l) out.push(`Legs: ${ctx.legsNote}.`); return out.join(" "); } export function buildShotRecipe(role: ShotRole, ctx: RecipeContext): ShotRecipe { const preserve = ctx.fabricPrompt ? PRESERVE.filter((p) => !p.startsWith("tkanina:")) : PRESERVE; const change = [ ...(role === "hero" ? ["aranżacja jasnego salonu (jedyne dozwolone odstępstwo sceny)"] : CHANGE_STUDIO), ...(ctx.fabricPrompt ? [`struktura tkaniny bazowej: ${ctx.fabricPrompt}`] : []), ...(ctx.legsNote && role !== "detail" ? [`nóżki: ${ctx.legsNote}`] : []), ]; const scene = role === "hero" ? `Place the furniture in a bright japandi living room: light walls, oak floor, soft side-window daylight, a low coffee table beside it and one plant. Keep the furniture's position, scale and perspective from Image ${imageNo(ctx, ctx.slots.geometryRefId) || 1}. 4:3 landscape.` : `${STUDIO} ${ROLE_VIEW[role] ?? ""}`.trim(); return { role, label: LABELS[role] ?? role, maxRefs: 4, preserve, change, prompt: [slotClauses(ctx), scene, factClauses(ctx, role), MATTE].filter(Boolean).join(" "), negativePrompt: NEGATIVE, }; } /** * Studio — wymiana tła wokół wyizolowanego mebla (hero). Mebel i tak * zostanie nałożony z oryginalnych pikseli (bramka IoU w studio.ts), * więc prompt prosi tylko o scenę zgodną z perspektywą i skalą. */ export function roomScenePrompt(): string { return "Image 1 shows a sofa on a white background. Replace only the white background with a bright japandi living room: light warm walls, light oak floor, soft daylight from a side window, a low wooden coffee table and one plant placed beside the sofa, never in front of it. Keep the sofa exactly as it is: same position, size, shape, fabric, colour and camera perspective. Add realistic soft floor contact shadows under the sofa. No people, no text, no extra pillows or throws."; } /** * Studio — zamiana materiału innej kolekcji w stałym obrysie. Obrys * i tło są potem przywracane pikselowo (tryb clamp), więc prompt dotyczy * wyłącznie tapicerki. */ export function materialSwapPrompt(role: ShotRole): string { const keep = role === "detail" ? "Keep the framing, seams, folds and lighting identical." : "Keep everything else identical: silhouette, proportions, seams, cushions, position, camera, lighting and background."; return `Image 1 is a product photo. Re-upholster every fabric surface in the fabric of Image 2: its weave, texture, pile and colour. ${keep} ${MATTE} Do not add or remove parts.`; } /** * Nakładka Asystenta Korekty (Shot Tuner) — ephemeral_overrides z payloadu * zadania. Receptura bazowa zostaje nietknięta; dopiski żyją tylko w tym * przebiegu. Provider image-edit nie ma osobnego kanału negatywnego — * zakazy dokładamy jako jawną klauzulę prohibicji na końcu promptu. */ export function applyShotOverrides( basePrompt: string, overrides: { prompt_additions?: string; negative_prompt_additions?: string } | null | undefined ): string { if (!overrides) return basePrompt; let prompt = basePrompt; if (overrides.prompt_additions?.trim()) prompt += ` ${overrides.prompt_additions.trim()}`; if (overrides.negative_prompt_additions?.trim()) { prompt += ` Strictly no: ${overrides.negative_prompt_additions.trim()}.`; } return prompt; }