diff --git a/services/imajin-classifier/service/src/scoring/gallery_enricher.py b/services/imajin-classifier/service/src/scoring/gallery_enricher.py index 94a6bcfa..13d3e112 100644 --- a/services/imajin-classifier/service/src/scoring/gallery_enricher.py +++ b/services/imajin-classifier/service/src/scoring/gallery_enricher.py @@ -286,6 +286,8 @@ async def enrich_all( Summary dict with processed, skipped, failed counts. """ if not GALLERY_DIR.exists(): + if status is not None: + status["running"] = False return {"processed": 0, "skipped": 0, "failed": 0, "total": 0} all_ids = [p.stem for p in GALLERY_DIR.glob("*.json")] diff --git a/services/imajin-classifier/service/start.sh b/services/imajin-classifier/service/start.sh new file mode 100755 index 00000000..0cadbf9c --- /dev/null +++ b/services/imajin-classifier/service/start.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Start the imajin-classifier service. +# Sources the venv and preloads the correct nvJitLink library to resolve +# CUDA cusparse symbol mismatch when running outside the identity venv. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_PATH="$SCRIPT_DIR/.venv" + +# nvJitLink library from the identity venv (has the correct 12_8 symbol) +IDENT_VENV="/var/home/lilith/Code/@applications/@imajin/services/imajin-identity/service/.venv" +NVJITLINK="$IDENT_VENV/lib/python3.12/site-packages/nvidia/nvjitlink/lib/libnvJitLink.so.12" + +source "$VENV_PATH/bin/activate" + +export LD_PRELOAD="$NVJITLINK" +export PORT="${PORT:-8012}" + +exec uvicorn src.api.main:app \ + --host 0.0.0.0 \ + --port "$PORT" \ + --log-level info diff --git a/studio/src/pages/Library.tsx b/studio/src/pages/Library.tsx index f579698c..e5297dc5 100644 --- a/studio/src/pages/Library.tsx +++ b/studio/src/pages/Library.tsx @@ -6,6 +6,10 @@ import { theme } from '../theme'; import { useImageLibrary } from '../hooks/useImageLibrary'; import type { GeneratedImage, MaturityRating } from '../types'; +type FramingFilter = 'all' | 'headshot' | 'medium-shot' | 'full-body'; +type StyleFilter = 'all' | 'photorealistic' | 'anime' | 'illustrated'; +type GazeFilter = 'all' | 'at-camera' | 'looking-away'; + // ─── Debounce hook ──────────────────────────────────────────────────────────── function useDebounce(value: T, delay: number): T { @@ -216,6 +220,25 @@ const PromptText = styled.div` -webkit-box-orient: vertical; `; +const WarningBadge = styled.span` + font-size: ${theme.font.size.xs}; + padding: 2px ${theme.spacing.xs}; + border-radius: ${theme.radius.sm}; + background: ${theme.colors.warning}20; + color: ${theme.colors.warning}; + font-weight: ${theme.font.weight.medium}; + white-space: nowrap; + cursor: default; +`; + +const FilterSep = styled.span` + width: 1px; + height: 16px; + background: ${theme.colors.border}; + margin: 0 ${theme.spacing.xs}; + align-self: center; +`; + const CardActions = styled.div` position: absolute; top: ${theme.spacing.sm}; @@ -281,7 +304,7 @@ const MATURITY_COLORS: Record = { explicit: theme.colors.error, }; -type SortKey = 'newest' | 'oldest' | 'score'; +type SortKey = 'newest' | 'oldest' | 'score' | 'sharpness'; // ─── Component ─────────────────────────────────────────────────────────────── @@ -289,6 +312,9 @@ export function Library(): ReactElement { const { images, isLoading, refresh, remove, clear, favorite } = useImageLibrary(); const [maturityFilter, setMaturityFilter] = useState('all'); const [identityFilter, setIdentityFilter] = useState('all'); + const [framingFilter, setFramingFilter] = useState('all'); + const [styleFilter, setStyleFilter] = useState('all'); + const [gazeFilter, setGazeFilter] = useState('all'); const [showStarredOnly, setShowStarredOnly] = useState(false); const [sort, setSort] = useState('newest'); const [searchQuery, setSearchQuery] = useState(''); @@ -304,6 +330,21 @@ export function Library(): ReactElement { .filter((img) => maturityFilter === 'all' || img.maturityRating === maturityFilter) .filter((img) => identityFilter === 'all' || img.identityId === identityFilter) .filter((img) => !showStarredOnly || img.isFavorited === true) + .filter((img) => { + if (framingFilter === 'all') return true; + const f = img.analysis?.framing ?? ''; + return f === framingFilter; + }) + .filter((img) => { + if (styleFilter === 'all') return true; + const s = img.analysis?.style ?? ''; + return s === styleFilter; + }) + .filter((img) => { + if (gazeFilter === 'all') return true; + const g = img.analysis?.gaze ?? ''; + return g === gazeFilter; + }) .filter((img) => debouncedSearch ? img.prompt.toLowerCase().includes(debouncedSearch.toLowerCase()) @@ -312,6 +353,7 @@ export function Library(): ReactElement { .sort((a, b) => { if (sort === 'newest') return b.createdAt.localeCompare(a.createdAt); if (sort === 'oldest') return a.createdAt.localeCompare(b.createdAt); + if (sort === 'sharpness') return (b.analysis?.sharpness ?? 0) - (a.analysis?.sharpness ?? 0); return (b.qualityScore ?? 0) - (a.qualityScore ?? 0); }); @@ -362,7 +404,32 @@ export function Library(): ReactElement { )} - Starred + + Framing + {(['all', 'headshot', 'medium-shot', 'full-body'] as const).map((f) => ( + setFramingFilter(f)}> + {f === 'all' ? 'All' : f} + + ))} + + + Style + {(['all', 'photorealistic', 'anime', 'illustrated'] as const).map((s) => ( + setStyleFilter(s)}> + {s === 'all' ? 'All' : s} + + ))} + + + Gaze + {(['all', 'at-camera', 'looking-away'] as const).map((g) => ( + setGazeFilter(g)}> + {g === 'all' ? 'All' : g} + + ))} + + + Starred setShowStarredOnly((v) => !v)}> ★ Starred @@ -379,9 +446,9 @@ export function Library(): ReactElement { )} Sort - {(['newest', 'oldest', 'score'] as const).map((s) => ( + {(['newest', 'oldest', 'score', 'sharpness'] as const).map((s) => ( setSort(s)}> - {s === 'score' ? 'Top score' : s.charAt(0).toUpperCase() + s.slice(1)} + {s === 'score' ? 'Quality' : s === 'sharpness' ? 'Sharpness' : s.charAt(0).toUpperCase() + s.slice(1)} ))} @@ -439,6 +506,20 @@ export function Library(): ReactElement { {img.durationMs !== undefined && ( {img.durationMs < 1000 ? `${img.durationMs}ms` : `${(img.durationMs / 1000).toFixed(1)}s`} )} + {img.analysis?.framing && img.analysis.framing !== 'unknown' && ( + {img.analysis.framing} + )} + {img.analysis?.style && img.analysis.style !== 'unknown' && ( + {img.analysis.style} + )} + {img.analysis?.gaze === 'at-camera' && ( + eye contact + )} + {(img.analysis?.vlm?.anatomyIssues?.length ?? 0) > 0 && ( + + ⚠ anatomy + + )} {img.prompt}