diff --git a/tools/sprite-generation/gui/src/App.tsx b/tools/sprite-generation/gui/src/App.tsx index df0b402d..e77ab133 100644 --- a/tools/sprite-generation/gui/src/App.tsx +++ b/tools/sprite-generation/gui/src/App.tsx @@ -5,6 +5,7 @@ import { ReviewQueuePage } from './pages/ReviewQueuePage' import { CategoryPage } from './pages/CategoryPage' import { SpritePage } from './pages/SpritePage' import SpriteTheaterPage from './pages/SpriteTheaterPage' +import VariantPage from './pages/VariantPage' import { SpriteStream } from './SpriteStream' import { colors } from './pages/theme' @@ -69,6 +70,7 @@ export function App(): ReactElement { } /> } /> } /> + } /> } /> diff --git a/tools/sprite-generation/gui/src/SpriteStream.tsx b/tools/sprite-generation/gui/src/SpriteStream.tsx index 37ea49f8..dbbfa68f 100644 --- a/tools/sprite-generation/gui/src/SpriteStream.tsx +++ b/tools/sprite-generation/gui/src/SpriteStream.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState, useCallback, type ReactNode, type CSSProperties } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' -import { fetchRecentVariants, variantStreamUrl, rawImageUrl, variantImageUrl } from './api' +import { fetchRecentVariants, variantStreamUrl, bestImageUrl, rawImageUrl } from './api' import type { RecentVariant } from './types' import { colors } from './pages/theme' @@ -15,10 +15,6 @@ const GLOW_DURATION = 3000 const SCORE_FLASH_DURATION = 4000 const SHIFT_DURATION_MS = 500 -function extractFilename(path: string): string { - const parts = path.replace(/\\/g, '/').split('/') - return parts[parts.length - 1] -} function formatLabel(entityId: string): string { return entityId.replace(/_/g, ' ') @@ -137,16 +133,9 @@ function SpriteCard({ size: number }): ReactNode { const [loaded, setLoaded] = useState(false) - const filename = variant.processed_path - ? extractFilename(variant.processed_path) - : variant.raw_path - ? extractFilename(variant.raw_path) - : null - const primarySrc = filename - ? (variant.processed_path ? variantImageUrl(filename) : rawImageUrl(filename)) - : null - const fallbackSrc = filename - ? (variant.processed_path ? rawImageUrl(filename) : variantImageUrl(filename)) + const primarySrc = bestImageUrl(variant.processed_path, variant.raw_path) + const fallbackSrc = variant.raw_path + ? rawImageUrl(variant.raw_path.replace(/\\/g, '/').split('/').pop() ?? '') : null const [imgSrc, setImgSrc] = useState(primarySrc) const triedFallback = useRef(false) diff --git a/tools/sprite-generation/gui/src/api.ts b/tools/sprite-generation/gui/src/api.ts index c487d340..671f63d4 100644 --- a/tools/sprite-generation/gui/src/api.ts +++ b/tools/sprite-generation/gui/src/api.ts @@ -1,4 +1,4 @@ -import type { Sprite, Variant, Stats, GenerationRun, RecentVariant } from './types' +import type { Sprite, Variant, Stats, GenerationRun, RecentVariant, VariantScore } from './types' const API_BASE = '/api' @@ -158,6 +158,20 @@ export function variantImageUrl(path: string): string { return `/images/variants/${path}` } +/** Resolve the best available image URL for a variant. + * Prefers processed (background removed) over raw. */ +export function bestImageUrl(processedPath: string | null, rawPath: string | null): string | null { + if (processedPath) { + const filename = processedPath.split('/').pop() ?? processedPath + return variantImageUrl(filename) + } + if (rawPath) { + const filename = rawPath.split('/').pop() ?? rawPath + return rawImageUrl(filename) + } + return null +} + export interface Progress { total: number by_status: Record @@ -187,6 +201,46 @@ export async function fetchRecentVariants(limit = 30): Promise return request(buildUrl('/variants/recent', { limit })) } +export async function fetchVariant(variantId: number): Promise { + return request(buildUrl(`/variants/${variantId}`)) +} + +export async function fetchVariantScores(variantId: number): Promise { + return request(buildUrl(`/variants/${variantId}/scores`)) +} + export function variantStreamUrl(): string { return `${API_BASE}/stream/variants` } + +export interface PipelineState { + funnel: { + total_completed: number + total_processed: number + scoring: Record + approved: number + installed: number + } + failed_gates: Array<{ gate: string; count: number }> + sprite_coverage: Array<{ + sprite_id: string + entity_id: string + total_variants: number + processed: number + tier_counts: Record + all_passed: number + deficit: number + }> + recent_scores: Array<{ + variant_id: number + sprite_id: string + scorer_name: string + gate_passed: boolean + confidence: number + scored_at: string + }> +} + +export async function fetchPipeline(): Promise { + return request(buildUrl('/pipeline')) +} diff --git a/tools/sprite-generation/gui/src/types.ts b/tools/sprite-generation/gui/src/types.ts index f02476d8..b0aba56e 100644 --- a/tools/sprite-generation/gui/src/types.ts +++ b/tools/sprite-generation/gui/src/types.ts @@ -78,4 +78,26 @@ export interface RecentVariant { rating: number | null notes: string | null is_approved: boolean + scored_by: string | null + review_tier: number | null +} + +export interface TheaterPage { + items: RecentVariant[] + total: number +} + +export interface VariantScore { + id: number + variant_id: number + scorer_name: string + scorer_model: string + tier: number + gates: string | null + quality: string | null + gate_passed: number + confidence: number + failed_gate_reason: string | null + quality_floor_failed: number + scored_at: string }