Dokumentacja end-to-end scenariusza "River — forma auto (lokalnie + AI)" w pliku pages/ai-vision-lab.html.
System wykrywa kontur formy na zdjęciach river table — działa zarówno dla form sfotografowanych "na prosto" jak i pod skosem / w perspektywie. Pod spodem hybryda: AI (Gemini przez Kie.ai lub Google bezpośrednio) + lokalny snap pikselowy + lokalne fallbacky.
ai-vision-lab.html
├── Turnstile widget renderuje się niewidocznie (site key w stałej)
├── els.engine dropdown czyta z DOM (Auto / Google / Kie.ai)
└── PROMPTS object zawiera 12 wariantów promptów, w tym segmentsPerspective
handleFile(file)
├── FileReader → dataURL
├── new Image() → wczytanie do RAM
├── state.imageElement = img
├── state.imageDataUrl = dataURL
├── state.contourBaseline = null ← czyszczę poprzedni quad
├── setContourTuneVisible(false) ← chowam suwak
└── drawCanvas() — pokazuje samo zdjęcie bez overlay
runSamLocalAnalysis() (linia ~2483)
├── buildImagePayload()
│ ├── tworzy <canvas>, skaluje obraz do maxEdge (1024 px dla samLocal)
│ ├── obraca o rotationDeg (z preview)
│ ├── enkoduje JPEG quality 70%
│ └── zwraca { base64, dataUrl, w, h, bytes, rotationDeg }
├── isAiArbiterEnabled() — czy checkbox AI arbiter ✓
│ └── domyślnie TAK dla samLocal (preset z applyTemplate)
└── try { tryAiPerspectiveContour(payload) } catch { fallback lokalny }
tryAiPerspectiveContour(payload)
├── turnstileToken = await getTurnstileToken() ← Cloudflare challenge
├── body = {
│ image: payload.base64,
│ prompt: PROMPTS.segmentsPerspective,
│ model: 'kie-gemini-2.5-pro',
│ temperature: 0, maxTokens: 1400, disableThinking: true,
│ turnstileToken
│ }
└── res = await callAI(body, { engine: selectedEngine() })
callAI(body, { engine }) — linia ~334
├── engine === 'auto' (domyślny):
│ ├── POST /api/gemini.php
│ ├── jeśli 429 → POST /api/kie.php z normalizeKieModel()
│ │ └── (strip prefiksu "kie-", walidacja allowlisty)
│ └── zwraca pierwszą udaną odpowiedź
├── engine === 'google':
│ └── POST /api/gemini.php (Twój klucz GEMINI_API_KEY)
└── engine === 'kie':
└── POST /api/kie.php (Twój klucz KIE_AI_API_KEY)
/api/kie.php
├── nr_api_init(['POST']) + rate limit 30/min
├── walidacja Turnstile (POST do challenges.cloudflare.com/turnstile/v0/siteverify)
│ └── 403 jeśli failed
├── walidacja modelu z allowlisty
├── walidacja image base64 (max 4 MB)
├── budowa OpenAI-compatible payload:
│ {
│ messages: [{
│ role: 'user',
│ content: [
│ { type: 'text', text: prompt },
│ { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,...' } }
│ ]
│ }],
│ temperature, max_tokens, response_format
│ }
├── curl → https://api.kie.ai/v1/chat/completions
├── normalizacja odpowiedzi do kształtu Gemini:
│ {
│ provider: 'kie',
│ candidates: [{ content: { parts: [{ text }] }, finishReason }],
│ usageMetadata,
│ rawResponse
│ }
└── echo JSON
tryAiPerspectiveContour kontynuacja:
├── data = await res.json()
├── text = extractText(data) ← czyta candidates[0].content.parts[N].text
├── parsed = tryParseJSON(text) ← { kontur_formy: [[x,y]×4], tryb_perspektywy, uwagi }
├── parsed = normalizePerspectiveFormContour(parsed)
│ ├── orderQuadPoints — sortuje 4 punkty: TL, TR, BR, BL po atan2 wokół centroidu
│ ├── isPerspectiveQuadUsable — sprawdza:
│ │ - 4 unikalne punkty
│ │ - area ≥ 8000 i ≥ 22% bbox area
│ │ - boki się nie przecinają (czy to convex quad)
│ └── jeśli niepoprawny → zwraca pusty quad
└── jeśli pusty quad → return null (caller idzie na fallback lokalny)
if (isQuadAxisAligned(contour, tolerance=35)) ← w skali 0-1000
isQuadAxisAligned(quad)
└── sprawdza 4 warunki (tolerancja ±35 jednostek = ±3.5%):
├── tl.x ≈ bl.x (lewy bok prawie pionowy)
├── tr.x ≈ br.x (prawy bok prawie pionowy)
├── tl.y ≈ tr.y (góra prawie pozioma)
└── br.y ≈ bl.y (dół prawie poziomy)
snapContourToInnerWhiteForm(contour) ← linia ~2666
├── buduje białą maskę pixel-perfect (threshold min ≥118, lum ≥158, sat ≤78)
├── próba 1: findInnerFormRectFromWhiteRuns
│ └── szuka "pasm" białych pikseli (rzędy/kolumny gdzie >X% białe)
├── próba 2 (fallback): findHorizontalEdge + findVerticalEdge
│ └── walk po luma map, szuka gradient dark→light w okolicy AI quada
├── zwraca: { points: [[x,y]×4 axis-aligned], method, shifts }
└── walidacja przez isPerspectiveQuadUsable
quad surowy od AI bez modyfikacji
console.log('Quad rotated/perspective → AI quad bez lokalnego snapu.')
W runSamLocalAnalysis:
├── state.lastParsed = ai.parsed
├── captureContourBaseline(ai.parsed.kontur_formy, 0)
│ ├── state.contourBaseline = quad.slice()
│ ├── state.contourTunePercent = 0
│ ├── els.contourInset.value = "0" ← suwak na 0%
│ └── setContourTuneVisible(true) ← pokazuje pasek
├── renderOutput(text, parsed, finishReason) ← JSON + key-value w panelu
├── drawCanvas(parsed)
│ ├── rysuje obraz
│ ├── rysuje siatkę alignmentu (jeśli włączona)
│ ├── rysuje quad jako zielony obrys
│ └── nakłada label "Forma · poziom drewna"
├── renderUsage(usage) ← tokens + koszt $
├── saveHistory(...) ← localStorage HISTORY_KEY
└── showStatus('success', `AI po skosie znalazło formę (Xms · Y tok · $Z)`)
els.contourInset 'input' event → onContourInsetChange()
├── pct = parseFloat(slider.value) ← np. 3.25
├── newQuad = shrinkQuadTowardCentroid(state.contourBaseline, pct/100)
│ └── każdy róg przesuwa się o pct% w stronę centroidu (zachowuje rotację)
├── state.lastParsed.kontur_formy = newQuad
├── state.lastParsed.kontur_formy_poziom_drewna = newQuad
├── state.lastParsed.kontur_formy_roboczy = newQuad
└── drawCanvas(state.lastParsed) ← real-time przerysowanie
Hierarchia degradacji:
1. tryAiPerspectiveContour → null (quad odrzucony) lub throw (429, parse error, transport)
↓
2. detectLocalBrightFormQuad(payload.dataUrl)
├── strict + balanced white masks
├── connected components → rotated bounding rects (16 wariantów: trim × inset)
├── snapQuadToLocalEdges → kandydaci z lokalnym scoringiem
└── jeśli AI arbiter ON → applyGeminiCandidateArbiter (drugie AI call wybierające najlepszego)
↓
3. SAM in-browser (Xenova/slimsam-77-uniform przez Web Worker)
└── buildSamAutoSeeds + runSamWorker + quadFromSamMask
↓
4. runLocalFastFormFallback (ostateczny: lokalna geometria bez AI)
↓
5. showStatus('error', 'Nie udało się...') — komunikat dla użytkownika
| Plik | Rola |
|---|---|
pages/ai-vision-lab.html | UI + cała logika JS (3900+ linii) |
api/gemini.php | Proxy do Google Gemini API + opcjonalnie Kie |
api/kie.php | Standalone proxy do Kie.ai (/v1/chat/completions) |
api/_common.php | Helpers (Turnstile init, CORS, rate limit, JSON body parsing) |
private/config/.env | Sekrety (KIE_AI_API_KEY, GEMINI_API_KEY, TURNSTILE_SECRET_KEY) |
_env.php | Loader env files do PHP getenv() |
config.php | Konfiguracja czytająca z env |
| Funkcja | Lokalizacja | Rola |
|---|---|---|
callAI(body, {engine}) | ~334 | Dispatcher silników (Google / Kie) z auto-fallback przy 429 |
normalizeKieModel(model) | ~328 | Strip prefiksu "kie-", walidacja allowlisty |
selectedEngine() | ~367 | Czyta wartość dropdowna |
tryAiPerspectiveContour(payload) | ~2458 | Pełna ścieżka AI → quad → snap/raw |
isQuadAxisAligned(quad, tol) | ~2415 | Czy quad praktycznie axis-aligned |
shrinkQuadTowardCentroid(quad, ratio) | ~2428 | Centroid-shrink (suwak inset) |
snapContourToInnerWhiteForm(contour) | ~2666 | Pixel-snap do białej maski (axis-aligned) |
detectLocalBrightFormQuad(dataUrl) | ~1735 | Lokalny detektor kandydatów (fallback bez AI) |
applyGeminiCandidateArbiter(payload, detected) | ~2293 | AI arbiter wybiera kandydata z listy |
runSamLocalAnalysis() | ~2483 | Główny orchestrator scenariusza "Forma auto" |
captureContourBaseline(quad, defaultPct) | ~2998 | Zapisuje baseline dla suwaka |
onContourInsetChange() | ~3022 | Handler suwaka — przelicza quad i odświeża canvas |
.envKIE_AI_API_KEY=<klucz z panelu kie.ai>
KIE_AI_BASE_URL=https://api.kie.ai
GEMINI_API_KEY=<klucz z aistudio.google.com>
TURNSTILE_SECRET_KEY=<secret z Cloudflare Turnstile>
Po zmianie .env zrestartuj PHP-FPM, żeby getenv() zwracało świeże wartości.
getTurnstileToken() musi być wywołane przed każdym requestemsnapContourToInnerWhiteForm używa state.imageElement (oryginalny img), nie payload-canvas. Jeśli kiedyś dodasz obrót preview, snap musi być świadom rotacji.[Vision Lab] AI engine=kie status=200 ← połączenie Kie OK
[Vision Lab] AI engine=auto/google status=429 ← Google quota wyczerpana
[Vision Lab] Google 429, próba Kie.ai. Primary body: ... ← auto-fallback
[Vision Lab] Axis-aligned quad → snap do wewnętrznej krawędzi OK. ← Branch A (forma na prosto)
[Vision Lab] Quad rotated/perspective → AI quad bez lokalnego snapu. ← Branch B (forma obrócona)
[Vision Lab] AI perspective: quad odrzucony przez isPerspectiveQuadUsable. ← AI dał zły quad
[Vision Lab] AI perspective błąd: ... ← throw → fallback lokalny
| Scenariusz w dropdown | ID | Backend | Use case |
|---|---|---|---|
| River — forma auto (lokalnie + AI) | samLocal | Hybryda AI perspective + axis-aligned snap | Główny workflow |
| River — kontur formy | segments | Gemini segmentation + inner-white snap | Tylko formy na wprost (axis-aligned) |
| River — kontur formy po skosie | segmentsPerspective | Gemini segmentation, surowy quad | Tylko formy obrócone (perspektywa) |
| River — forma + rzeka (legacy) | river | Stary prompt, do wycofania | — |
| Forma biała — 4 rogi | whiteForm | Lokalna detekcja białej maski (bez AI) | Backup gdy AI niedostępny |
| Detekcja elementów (dokładna) | objects | Gemini object detection z labelami | Analiza całej sceny |
| Wykrywanie ubytków drewna | defects | Gemini detection sęków/pęknięć | Po wycięciu drewna |
| Opis sceny (diagnostyka) | scene | Krótki opis zdjęcia | Sanity check że AI w ogóle widzi obraz |
| Analiza kadru | frame | Ocena czy zdjęcie nadaje się do pracy | Pre-check przed Run |
| Rozpoznawanie formy | form | Stary prompt formy, do wycofania | — |
| Refine — popraw 4 rogi | refine | User klika rogi, AI dociąga do krawędzi | Manual fallback |
| Własny prompt | custom | Dowolny prompt z textarea | Testy / development |
samLocal (Forma auto) jest dziś jedynym potrzebnym scenariuszem dla użytkownika końcowego. Pozostałe to narzędzia diagnostyczne.