# AUDIT: ścieżki generacji RunComfy i gotowość na custom `workflow_api.json` **Data:** 2026-09-26 · **Tryb:** read-only (inspekcja kodu) · **Zakres:** `server/src/ai/*`, `server/src/worker.ts`, `server/src/domain/*`, `server/src/routes.ts`, `server/src/config.ts` Stan faktyczny: pipeline foto = **RunComfy Model API** (on-demand, czarna skrzynka `google/nano-banana/pro/edit`), NIE deployment ComfyUI. Kod nie zawiera żadnego śladu `workflow_api_json` / `overrides` nodów — jedyna koncepcja deploymentu istnieje w starym repo (`vilmax-cockpit/src/video/runcomfyClient.js`, tylko odczyt). --- ## CZĘŚĆ 1 — Inwentaryzacja promptów i referencji dla 8 ról ### 1.1 Które role są generowane torem foto Osiem ról zdefiniowanych **trzy razy niezależnie** (ryzyko rozjazdu): - `server/src/routes.ts:65-74` — `GENERATABLE_ROLES` - `server/src/domain/mediaReview.ts:13` — `OFFER_ROLES` - `server/src/domain/orchestrationContracts.ts:4` — `photoRoles` Wszystkie trzy listy = `hero, packshot_front, packshot_34, side, back, sleep, storage, detail`. Role `blueprint`, `channel_graphic`, `document`, `source_material` są odrzucane na wejściu (`routes.ts:1170`). ### 1.2 Klocki budulcowe promptu (`server/src/ai/recipes.ts`) | Element | Linie | Treść / rola | |---|---|---| | `NO_HALLUCINATION` | 67 | `Stationary sofa only, uniform upholstery across all cushions, strictly no recliners, no footrest mechanisms, no blankets, no throws, no extra decorative pillows.` — jedyna „warstwa negatywna", **wklejana w tekst pozytywny** | | `FAITHFUL_CORE` | 82 | `Recreate the exact furniture shown in the product reference photo. Preserve the identical geometry, proportions, upholstery, seams, cushions, chaise side, and the exact visible mechanical state. Do not redesign, reconstruct, or complete hidden parts. Replace only the surroundings: isolated on seamless solid pure white background RGB(255,255,255), soft even studio lighting, realistic soft ambient ground contact shadow. Remove factory clutter, other furniture, people and markings. Do not invent parts that are not visible; do not change the mechanism state; do not add pillows, text or props.` + `NO_HALLUCINATION` | | `multiRefMap(ctx)` | 90-117 | Jawne mapowanie `Image 1/2/3` → role, gdy wysłano ref tkaniny lub nóżek. Pusty string → fallback na `FAITHFUL_CORE` | | `multiRefCore(ctx)` | 119-123 | `multiRefMap` + `Isolated on a seamless pure solid white studio background RGB(255,255,255) with soft ambient contact shadows under the legs. No props, no background clutter, no text.` + `NO_HALLUCINATION` | | `materialClauses(ctx, role)` | 131-168 | Klauzule materiałowe: tkanina (`fabricPrompt`), sylwetka (`silhouetteNote` + liczba poduszek), nóżki (3 warianty). **Rola `detail` wraca wcześniej — bez sylwetki i bez nóg** | | `ROLE_VIEW` | 174-182 | Sufiks kadru/stanu per rola (tabela 1.3) | | `CUSHION_VISIBLE_ROLES` | 171 | `{hero, packshot_front, packshot_34, side, back}` — tylko tam doklejana jest klauzula `exactly N loose back cushions` | Treść mapowania referencji (`multiRefMap`, linie 98-116): ``` Multi-reference conditioning: - Image 1 defines the exact furniture geometry, proportions, cushion arrangement, and folds. - Image {f} is a real factory photo of the actual upholstery fabric on furniture — transfer the physical fabric behavior from it: weave scale, folds, creases and light reflection. (wariant "vilmax_photo") LUB: - Image {f} defines the exact upholstery fabric weave, tactile texture, and surface finish. (wariant "swatch") - Image {l} defines the exact furniture legs design, profile, material finish, and mounting. Recreate the furniture from Image 1 [keeping its geometry unchanged and upholstered in the fabric behaving exactly as in Image {f} | upholstered in the fabric of Image {f}] [standing firmly on the legs from Image {l}]. Preserve the identical chaise side and the exact visible mechanical state from Image 1. Do not redesign, reconstruct, or complete hidden parts. ``` Klauzule materiałowe (`materialClauses`): - **tkanina** (gdy `fabrics.ai_prompt` niepusty): z obrazem → `Supplementary fabric note: {ai_prompt} — Image {f} is authoritative for the actual weave, texture and finish; do NOT copy the fabric texture from Image 1.`; bez obrazu → `Upholstery fabric: {ai_prompt} — render exactly this fabric structure, wale/pile direction and sheen on every upholstered surface.` - **sylwetka**: `{silhouetteNote}; exactly {N} loose back cushions forming the backrest` → `Fixed silhouette — ... Do not change the cushion count or armrest shape.` (tylko role z `CUSHION_VISIBLE_ROLES`, fakt `cushions.loose_back_count` musi być `confirmed`) - **nóżki**: z obrazem → `Supplementary legs note: {legsNote} — Image {l} remains authoritative for leg design...`; tekstowo → `The furniture stands on {legsNote} — these legs are part of the product; render them mounted under the body...`; obraz bez notatki → `Use the exact legs shown in Image {l} — do not modify, add, remove or restyle them.`; brak wszystkiego → `Preserve the original legs exactly as they appear in the furniture reference photo — do not modify, add, remove or restyle any legs.` ### 1.3 Tabela 8 ról — szablon promptu Szablon wspólny dla wszystkich poza hero (`recipes.ts:224`): ``` {multiRefCore(ctx)} {materialClauses(ctx, role)} {ROLE_VIEW[role]} ``` | Rola | Sufiks `ROLE_VIEW` (linia) | Odstępstwa | |---|---|---| | `hero` | brak — własny szablon (209) | Scena: `...placed in a bright, warm japandi living room — light walls, wooden floor, soft daylight from a side window, a small coffee table and a plant, no clutter...` + `Sofa is the hero — centered, clearly readable, realistic scale. 4:3 landscape. No text, no people, do not add pillows.` + `NO_HALLUCINATION`. Zamiast `CHANGE_STUDIO` — aranżacja salonu; styl **zahardkodowany** (japandi) | | `packshot_front` | `Camera at seated eye level, straight front view, the sofa fills ~80% of frame, 4:3 landscape.` (175) | — | | `packshot_34` | `Three-quarter view showing the chaise extension exactly as in the reference, 4:3 landscape.` (176) | — | | `side` | `Side profile view exactly as in the reference.` (177) | — | | `back` | `Rear view of the sofa exactly as in the reference.` (178) | — | | `sleep` | `The furniture is shown in the unfolded configuration visible in the reference — reproduce exactly this unfolded state and visible surfaces; do not invent how the mechanism works.` (179) | brak klauzuli liczby poduszek (poza `CUSHION_VISIBLE_ROLES`) | | `storage` | `Reproduce the compartment and interior exactly as shown in the reference — only the visible parts; do not imagine hidden construction, hinges or mechanism.` (180) | jak `sleep` | | `detail` | `Close-up of the upholstery exactly as in the reference — reproduce the exact rib width, pile direction, density, sheen, color and visible seams; not a generic plush.` (181) | `materialClauses` bez sylwetki i bez nóg (`recipes.ts:142`) | ### 1.4 Negatywy — jak są doklejane Nie ma kanału negatywnego. Trzy źródła prohibicji lądują **w jednym stringu pozytywnym**: 1. `NO_HALLUCINATION` — na końcu `FAITHFUL_CORE` / `multiRefCore` / hero (zawsze). 2. Klauzule inline typu `Do not invent parts...`, `no text`, `No props`. 3. Groq: `applyShotOverrides` (`recipes.ts:244-255`) dokleja `negative_prompt_additions` jako ` Strictly no: {...}.` na samym końcu promptu; `prompt_additions` jako `, {...}` (przecinek — w teście widać artefakt `"...recliners., camera at..."`, `shotTuner.test.ts:14`). ### 1.5 Kolejność i role obrazów referencyjnych Montaż tablicy `images` — `worker.ts:618-637`: ``` orderedIds = unique( [primarySourceId, fabricRef.id, legImageId, ...payload.sourceIds] minus ephemeral disabled_ref_ids ) ``` | Pozycja | Źródło | Rola w prompcie | |---|---|---| | Image 1 | `payload.primarySourceId` — `shot_refs.ref_kind='primary'` danej roli (`shotRefs.ts:140-157`) | „exact furniture geometry, proportions, cushion arrangement, folds" — kotwica geometrii; tuner NIE może jej wyłączyć (`worker.ts:625-628`, `shotTuner.ts:90-91`) | | Image 2 | `resolveFabricRef` (`worker.ts:301-339`): kotwica = fakt `fabric.photographed_shade` (confirmed) → `fabric_shades.photo_path` („Zdjęcie Vilmax", `vilmax_photo`) → fallback `official_image_path` (`swatch`); bez kotwicy → pierwszy odcień z photo, potem z official. UWAGA: kolumny `*_path` trzymają **id `source_assets`**, nie ścieżki (`schema.sql:71-72`) | `vilmax_photo`: „real factory photo … transfer physical fabric behavior"; `swatch`: „defines the exact upholstery fabric weave, tactile texture, surface finish" | | Image 3 | `furniture_legs.image_asset_id` wskazane faktem `legs.furniture_leg_id` (`worker.ts:380-390`) | „exact furniture legs design, profile, material finish, mounting" | | Image 4+ | `payload.sourceIds` — pozostałe `shot_refs` (supporting) | **brak opisu w prompcie** — idą jako gołe obrazy; LLM tunera widzi je jako `supporting` (`routes.ts:1295-1302`) | Konwersja do payloadu (`worker.ts:453-526`): każda referencja → `sharp` rotate/resize/flatten/JPEG → `data:image/jpeg;base64,...`. Tiery degradacji: 1568px/q82 → 1024/q70 → 768/q60; budżet sumaryczny 8 MiB (`PROVIDER_IMAGES_BUDGET`, powód: twardy limit 10 MiB body Model API — komentarz `worker.ts:455-458`); przy przekroczeniu odrzuca od końca, nigdy primary. Limit liczby refów: `provider.capabilities.maxReferenceImages` = **16** dla nano-banana. ### 1.6 Czy istnieje izolacja ujęć? **Tylko tekstowo + doborem primary.** Mechanizmy: - Osobne `shot_refs` per rola — `primary` danej roli ma pokazywać dokładnie oczekiwany stan (komentarz `shotRefs.ts:8-15`, wymuszone w `routes.ts:1206-1211`: brak primary = HTTP 422). - Sufiks `ROLE_VIEW` mówi „exactly as in the reference". - `CUSHION_VISIBLE_ROLES` i wcześniejszy return dla `detail` ograniczają klauzule. **Czego NIE ma:** masek, kadrowania wejścia, ControlNet/depth, osobnych grafów. `detail` dostaje ten sam rdzeń co packshot — „odcięcie geometrii całej sofy" polega wyłącznie na tym, że operator przypisze zbliżenie jako primary tej roli. Jeśli primary `side` pokaże 3/4, prompt tego nie zablokuje. ### 1.7 Gdzie doklejany jest Asystent Groq - Endpoint podglądu: `POST /api/products/:slug/shots/:role/tune-preview` (`routes.ts:1256-1326`) → `tuneShotPrompt` (`shotTuner.ts:57-92`) → `ShotOverrides` (zod: `prompt_additions`, `negative_prompt_additions`, `disabled_ref_ids`, `explanation_pl`). - Endpoint zlecenia: `tune-generate` (`routes.ts:1331-1426`) → `jobs.payload.ephemeral_overrides`. - Zastosowanie w workerze (`worker.ts:620-658`): `disabled_ref_ids` filtruje `orderedIds` PRZED montażem images (nie rusza `shot_refs` w DB); `applyShotOverrides` dokleja teksty do `payload.prompt ?? shotRecipePrompt(...)` — działa też na `promptOverride` operatora. Nakładka trafia do provenance assetu (`worker.ts:578`). --- ## CZĘŚĆ 2 — Architektura obecnego transportera ### 2.1 Cykl życia requestu (`server/src/ai/runcomfy.ts`) ``` POST {RUNCOMFY_BASE_URL}/v1/models/{modelId} → { request_id } GET {base}/v1/requests/{request_id}/status → status GET {base}/v1/requests/{request_id}/result → output URLs (po completed) GET → pobranie binariów (worker) ``` - `base` = `cfg.runcomfyBaseUrl`, domyślnie `https://model-api.runcomfy.net` (`config.ts:46`); `modelId` domyślnie `google/nano-banana/pro/edit` (`config.ts:47`). - Nagłówki: `Authorization: Bearer {RUNCOMFY_API_KEY}`, `Content-Type: application/json` (`runcomfy.ts:61-64`). - Payload (profil nano-banana, `runcomfy.ts:23-31`): `{prompt, image_urls: [data URI...], aspect_ratio:"4:3", resolution:"2K"}`. Profil fallback (gpt-image-*, `runcomfy.ts:33-44`): `{prompt, images, aspect_ratio, output_format:"jpeg", quality:"low|medium|high", resolution:"1k"}`. - Obrazy: **wyłącznie data URI** w body. Brak publicznych URL-i, brak uploadu na storage RunComfy. - Odpowiedź submit: `data.request_id` → `jobs.provider_request_id` (`worker.ts:664-669`), potem `deferJob(5s)`. ### 2.2 Polling i wynik - Cykl workerowski (`worker.ts:674-682`): `provider.poll()` co **8 s** (`deferJob(8)`); pierwsze oczekiwanie 5 s po submicie. - Statusy (`runcomfy.ts:80-89`): `completed`/`succeeded` → fetch result; `failed`/`cancelled`/`canceled` → job failed; **wszystko inne → pending** (w tym nieznane statusy — cicho zakładamy „jeszcze trwa"). - Result (`runcomfy.ts:95-101`): `output.images[]` | `output.image` | `output_urls[]` → pierwszy niepusty; 0 URL-i = `failed`. - Download (`worker.ts:528-591`): `fetch(url)` bez auth → sha256 → `mediaDir/assets/{sha[0:2]}/{sha}.{jpg|png}` z flagą `wx` → `registerAsset` z pełnym provenance (prompt, `resolvedRefIds`, `primarySourceId`, `providerRequestId`, nakładka tunera). ### 2.3 Timeout i błędy - **Brak timeoutu HTTP** — ani submit, ani status, ani result, ani download (goły `fetch`, zero `AbortSignal`). - **Brak deadline'u zadania** — job w `waiting` poll'uje w nieskończoność; `waiting` nie zużywa `attempts` (`jobs.ts:81-103`). - Błąd HTTP submit/status/result → `throw` → `finishJob(retryable:true)` (`worker.ts:129-134`) → retry do `max_attempts=3` z backoffem `min(60, 5·attempts)` s (`jobs.ts:134-139`). Po wyczerpaniu: `status='error'`. - **Insufficient Funds**: brak obsługi — wyląduje jako `RunComfy HTTP 4xx: ` i przejdzie 3 bezcelowe retry. Trzeba dodać klasyfikację błędów (402/insufficient → non-retryable, jawny komunikat dla operatora). - Bramka kosztów: **tylko licznik dzienny** `VILMAL_MAX_PAID_JOBS_PER_DAY=10` (`routes.ts:1176-1191`, identyczna w tune-generate i blueprint). `unitCostUsd` w providerze jest zadeklarowane, ale **nigdzie nie konsumowane** — realnej wyceny per-zadanie nie ma. - Restart procesu: joby `running` → `waiting` (jeśli mają `provider_request_id`) albo `queued` (`worker.ts:62-66`) — wznawia się polling, nie nowa generacja. Submit utrwala `resolvedPrompt`/`resolvedRefIds` w payloadzie przed zleceniem (`worker.ts:653-663`). ### 2.4 Rozjazd konfiguracji `.env.example` podaje domyślny `RUNCOMFY_BASE_URL=https://api.runcomfy.com`, a `config.ts:46` faktycznie używa `https://model-api.runcomfy.net` — dokumentacja env jest nieaktualna. Dla Serverless będzie potrzebny **trzeci** host: `https://api.runcomfy.net` (patrz §3). --- ## CZĘŚĆ 3 — Gotowość na `workflow_api.json` (Serverless Deployment API) ### 3.1 Kontrakt docelowy (zweryfikowany: MCP + docs.runcomfy.com + stary klient) ``` POST https://api.runcomfy.net/prod/v1/deployments/{deployment_id}/inference GET .../deployments/{deployment_id}/requests/{request_id}/status GET .../deployments/{deployment_id}/requests/{request_id}/result POST .../deployments/{deployment_id}/requests/{request_id}/cancel ``` (dokumentacja pokazuje też wariant `/prod/v2/.../inference`; MCP `submit_request` backuje `/prod/v1` — do weryfikacji na żywym deploymentcie przed kodowaniem) - **Tryb overrides:** body `{ "overrides": { "": { "inputs": { "": } } } }` — łatka na zapisanym w chmurze `workflow_api.json` deploymentu. Pominięte nody = wartości domyślne z grafu. Node ID i nazwy inputów muszą istnieć (rejestr schematów: `payload.object_info_url` z `GET /prod/v2/deployments/{id}?includes=payload`; MCP: `get_deployment(include_payload=true)`). - **Tryb inline:** body `{ "workflow_api_json": {...} }` — pełny graf per request; `overrides` wtedy pomijamy. Dobry do rozwoju grafu przed zamrożeniem wersji deploymentu. - **Pliki wejściowe:** NIE ma API pre-uploadu assetów inferencji (endpointy uploadu datasetów służą treningowi LoRA). Węzeł `LoadImage` przyjmuje wartość `image` = **publiczny HTTPS URL albo data URI** wprost w overrides — nasz istniejący pipeline data-URI (`worker.ts:468-499`) przechodzi 1:1. Pytanie otwarte: limit wielkości body dla `/inference` (dla Model API było 10 MiB — stąd `PROVIDER_IMAGES_BUDGET` 8 MiB; dla Serverless trzeba potwierdzić). Publiczne URL-e wymagają hostingu — obecny serwer stoi na `127.0.0.1`; stary repo ma koncept `publicAssetHost` (podpisane URL-e) do ewentualnej adaptacji. - **Webhook:** `webhook_url` (+`webhook_intermediate_status`) — alternatywa dla poll'u 8 s; dziś nasz `jobs.status='waiting'` modeluje się naturalnie na webhook (job czeka, callback domyka). - **Wynik:** `outputs` kluczowane `node_id` → `{ images: [{url, filename}] }`; URLe hostowane 7 dni (stary `pickImageFromOutputs` w `vilmax-cockpit/src/video/runcomfyClient.js:197-208` pokazuje dokładny kształt). - **Statusy:** `in_queue` (z `queue_position`) → `in_progress` → `completed` / `failed` / `cancelled`. Inne nazwy niż w Model API — adapter musi normalizować (stary `normalizeRunComfyStatus` pokrywa to: `runcomfyClient.js:24-45`). ### 3.2 Mapowanie naszych danych na węzły grafu | Nasze dane | Węzeł ComfyUI | Override | |---|---|---| | `shotRecipePrompt` (pozytyw) | `CLIPTextEncode` (positive) | `{"6":{"inputs":{"text": ...}}}` | | `NO_HALLUCINATION` + `negative_prompt_additions` | `CLIPTextEncode` (negative) | drugi węzeł tekstowy — **wymaga rozszczepienia dzisiejszego mono-promptu** | | Image 1 (primary) | `LoadImage` | `{"N1":{"inputs":{"image": dataURI}}}` | | Image 2 (tkanina), Image 3 (nóżki), 4+ supporting | kolejne `LoadImage` / wejścia `IPAdapter`/`ControlNet` | analogicznie; `IPAdapter.weight`, `ControlNetApply.strength` jako osobne inputy | | seed/steps/cfg/denoise | `KSampler` | `{"3":{"inputs":{"seed":…,"steps":…,"cfg":…,"denoise":…}}}` — dziś w ogóle nieustawiane (czarna skrzynka) | | rozmiar/proporcje | `EmptyLatentImage` / węzeł resize | `aspect_ratio:"4:3"` jest dziś sztywne — w grafie jawne `width/height` | Kluczowa różnica semantyczna: dziś **negatywy są wstrzykiwane w pozytywny string** (Model API image-edit nie ma kanału negatywnego — komentarz `recipes.ts:241-243`). Prawdziwy graf daje osobny `CLIPTextEncode` — trzeba rozdzielić `FAITHFUL_CORE`/`multiRefCore` na część pozytywną i `NO_HALLUCINATION` + zakazy inline na negatywną. To jest największa zmiana w `recipes.ts`: `buildShotRecipe` powinno zwracać `{ positive, negative }` zamiast jednego `prompt`. ### 3.3 Gdzie ma mieszkać `workflow_api.json` **Rekomendacja: plik w repo** — `server/src/ai/workflows/photoshot.api.json` (+ wersja w stałej kodu i w provenance assetu). Uzasadnienie: - Graf to kontrakt kodu — ten sam poziom co `recipes.ts`; diff i review w Git, testy na obecność wymaganych node ID/inputów. - Deployment i tak wskazuje na workflow zapisane w chmurze (`workflow_id` + `workflow_version` przy `create_deployment`); plik w repo jest źródłem prawdy wgrywanym przez UI/MCP. Rejestracja deploymentu (id) trafia do `.env` jako `RUNCOMFY_DEPLOYMENT_ID`. - Baza danych: nie — wersjonowanie i drift trudniejsze; provenance ma wskazywać wersję grafu jak wskazuje dziś recepturę (`provenance.recipe`). - Tryb inline `workflow_api_json` w requestcie zostaje wariantem rozwojowym/benchmarkowym (wersjonowany plik per kandydat grafu, bez redeployu). ### 3.4 Jeden workflow czy per-rola? Dzisiejsza różnicowanie ról = **100% prompt + dobór/kolejność refów** — topologia pracy jest identyczna. Wniosek: - **Jeden kanoniczny graf** `photoshot` z parametrowanymi wejściami: `LoadImage×N` (geometry/fabric/legs/supporting1..k), 2×`CLIPTextEncode`, `KSampler`, opcjonalnie `IPAdapter`(fabric/legs) i `ControlNet`(depth z primary) — wszystko adresowane `overrides` per rola. - Rozszczepienie potrzebne dopiero, gdy topologia się różni: np. `detail` bez ControlNetu geometrii (zbliżenie nie potrzebuje kotwicy bryły), `hero` z dodatkowym `LoadImage` stylu sceny zamiast białego tła. Dwie opcje wtedy: (a) drugi deployment (`photoshot-scene`), (b) jeden graf z bypass-ami — prościej: osobne wersje grafu w tym samym katalogu `workflows/` i wybór per rola w workerze. - Nie mnożyć grafów „na zapas" — wariant powstaje tylko, gdy overrides nie wystarczają (inna ścieżka sygnału, nie inne wartości). ### 3.5 Co musi się zmienić w kodzie **`provider.ts`** — `ImageEditRequest` nie wystarcza: brak kanału negatywnego, seeda, parametrów samplera i adresowania nodów. Dwie opcje: - (a) równoległy interfejs `WorkflowProvider` z `submitWorkflow(req: { positive, negative, imagesByNode, sampler?, overrides, webhookUrl? })` — czystsze, nie łamie `ImageProvider` używanego przez blueprinty/testProvider; - (b) rozszerzyć `ImageEditRequest` o `negativePrompt`, `overrides` — mniej kodu, ale miesza dwa kontrakty. Rekomendacja: (a), z tym samym `poll(requestId)` — kształt `ProviderPollResult` (pending/done/failed + outputUrls) pokrywa oba API, worker się nie zmienia semantycznie. **`runcomfy.ts`** — nowy `createRunComfyDeploymentProvider` obok istniejącego `createRunComfyProvider`: - inny `baseUrl` (`api.runcomfy.net` vs `model-api.runcomfy.net`) i inne ścieżki (`/prod/v1/deployments/{id}/inference`, `/requests/{rid}/status|result`); - inna normalizacja statusów (`in_queue/in_progress` → pending) i wyniku (`outputs[node_id].images[].url`); - `deployment_id` trzymać w opcjach adaptera, NIE w `provider_request_id` (to pole jest jednowymiarowe; jeśli trzeba — format `dep:{id}:{rid}`); - klasyfikacja błędów: 402/insufficient-funds → non-retryable; 413 → podpowiedź „zmniejsz referencje"; timeout `AbortSignal.timeout` na każdym fetch; - `unitCostUsd` ustawić realnie (koszt sekundy GPU × oczekiwany czas, albo `null` = nieznany) i **zacząć konsumować** przy bramce kosztów. **`worker.ts`** — w `runGenerateShot`: - zamiast `images[]` → `imagesByNode` (primary→`LoadImage` geometrii, fabric→wejście tkaniny, legs→wejście nóg, supporting→dodatkowe sloty); zachować `resolvedRefIds` i regułę „indeksy w prompcie = faktyczne wysłanie" — w trybie nodów prompt może w ogóle przestać wymieniać `Image N` (role wiążą graf, nie tekst) — decyzja projektowa: uprościć `multiRefMap` do nazw ról semantycznych; - wywołać `shotRecipePrompt` w wariancie `{positive, negative}`; - przekazać `seed` (np. hash job.id — replayability w provenance), ewentualnie `denoise` per rola; - polling i download bez zmian koncepcyjnie; wyników może być >1 per node — `downloadOutputs` już obsługuje listę. **`recipes.ts`** — rozszczepić na `{ positivePrompt, negativePrompt }`: `NO_HALLUCINATION` i `Strictly no:` do negatywu; zdecydować los zakazów inline w `FAITHFUL_CORE` (zostawić w pozytywie jako instrukcje edycji, czy przenieść do negatywu — do zweryfikowania eksperymentalnie na 1-2 rolach). **`config.ts` / `.env.example`** — dodać `RUNCOMFY_DEPLOYMENT_ID`, `RUNCOMFY_SERVERLESS_BASE_URL` (default `https://api.runcomfy.net`), opcjonalnie `RUNCOMFY_WEBHOOK_URL`; naprawić rozjazd domyślnego base URL z `.env.example`. **`shotTuner.ts`/`routes.ts`** — `negative_prompt_additions` ma już dedykowany kanał w schemacie — zero zmian kontraktu; prompt bazowy pokazywany LLM-owi powinien być pozytywem nowej receptury. --- ## 4. Ustalenia dodatkowe (luki i niespójności znalezione przy okazji) 1. **Brak timeoutów i deadline'u** na wszystkich fetchach providera i pobieraniu wyników; job `waiting` może wisieć w nieskończoność. 2. **`Insufficient Funds` nieobsłużony** — 3 płatne retry bez sensu; brak klasyfikacji błędów. 3. **`unitCostUsd` martwe** — deklarowane, nigdy nie czytane; jedyna bramka to licznik dzienny. 4. **`recipe.maxRefs=4` martwe** — worker używa `provider.capabilities.maxReferenceImages` (16) i ignoruje pole receptury. 5. **`aspect_ratio` nigdy nie przekazywane** przez workera — zawsze default `"4:3"` adaptera; `quality` tylko gdy `payload.quality` (używa go wyłącznie blueprint `ai`). 6. **Hero: styl zahardkodowany** („japandi living room"), a `style_reference` nie może być shot ref (`shotRefs.ts:21-24` dopuszcza tylko `product_reference`/`vendor_reference`) — referencje stylu jadą jako nieopisane Image 4+. 7. **Trzykrotna definicja 8 ról** (`routes.ts`, `mediaReview.ts`, `orchestrationContracts.ts`) — ryzyko rozjazdu przy dodaniu roli. 8. **`applyShotOverrides` produkuje `.,`** — kropka bazowego promptu + przecinek dopiski (`shotTuner.test.ts:14` dokumentuje artefakt). 9. **Nieznane statusy providera → pending** — literówka/nowy status w API da wieczny polling zamiast błędu. 10. **`.env.example` ≠ `config.ts`** dla `RUNCOMFY_BASE_URL` (jak w §2.4). 11. `sleepingSurface` w `RecipeContext` — martwe pole (celowo, komentarz `recipes.ts:26`), do usunięcia przy refaktorze pod {positive,negative}. ## 5. Rekomendowana kolejność wdrożenia (bez zmian w tej sesji) 1. `recipes.ts`: rozszczep `buildShotRecipe` → `{ positive, negative, imagesByRole }` + testy. 2. `provider.ts` + `runcomfy.ts`: `WorkflowProvider`/`createRunComfyDeploymentProvider` z tym samym `poll()`; klasyfikacja błędów i timeouty. 3. `worker.ts`: mapowanie refów na nody, seed per job, provenance `workflowVersion` + `deploymentId`. 4. `config.ts`/`.env.example`: `RUNCOMFY_DEPLOYMENT_ID`, serverless base URL; korekta rozjazdu dokumentacji. 5. `server/src/ai/workflows/photoshot.api.json` — zaprojektować graf (nody: LoadImage geometrii/tkaniny/nóg, 2×CLIPTextEncode, KSampler, opcj. IPAdapter/ControlNet); zarejestrować workflow i deployment przez RunComfy UI/MCP (oddzielna zgoda — operacja płatna/zmiana konta). 6. Bramka kosztów: realne `unitCostUsd` × estimacja czasu GPU przed submit (osobna decyzja właściciela).