feat(imajin-classifier): ✨ Add Library page with gallery enrichment scoring integration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
6e9e735348
commit
c2a2c01481
3 changed files with 108 additions and 4 deletions
|
|
@ -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")]
|
||||
|
|
|
|||
21
services/imajin-classifier/service/start.sh
Executable file
21
services/imajin-classifier/service/start.sh
Executable file
|
|
@ -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
|
||||
|
|
@ -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<T>(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<MaturityRating, string> = {
|
|||
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<MaturityRating | 'all'>('all');
|
||||
const [identityFilter, setIdentityFilter] = useState<string | 'all'>('all');
|
||||
const [framingFilter, setFramingFilter] = useState<FramingFilter>('all');
|
||||
const [styleFilter, setStyleFilter] = useState<StyleFilter>('all');
|
||||
const [gazeFilter, setGazeFilter] = useState<GazeFilter>('all');
|
||||
const [showStarredOnly, setShowStarredOnly] = useState(false);
|
||||
const [sort, setSort] = useState<SortKey>('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 {
|
|||
</>
|
||||
)}
|
||||
|
||||
<FilterLabel style={{ marginLeft: theme.spacing.md }}>Starred</FilterLabel>
|
||||
<FilterSep />
|
||||
<FilterLabel>Framing</FilterLabel>
|
||||
{(['all', 'headshot', 'medium-shot', 'full-body'] as const).map((f) => (
|
||||
<FilterChip key={f} $active={framingFilter === f} onClick={() => setFramingFilter(f)}>
|
||||
{f === 'all' ? 'All' : f}
|
||||
</FilterChip>
|
||||
))}
|
||||
|
||||
<FilterSep />
|
||||
<FilterLabel>Style</FilterLabel>
|
||||
{(['all', 'photorealistic', 'anime', 'illustrated'] as const).map((s) => (
|
||||
<FilterChip key={s} $active={styleFilter === s} onClick={() => setStyleFilter(s)}>
|
||||
{s === 'all' ? 'All' : s}
|
||||
</FilterChip>
|
||||
))}
|
||||
|
||||
<FilterSep />
|
||||
<FilterLabel>Gaze</FilterLabel>
|
||||
{(['all', 'at-camera', 'looking-away'] as const).map((g) => (
|
||||
<FilterChip key={g} $active={gazeFilter === g} onClick={() => setGazeFilter(g)}>
|
||||
{g === 'all' ? 'All' : g}
|
||||
</FilterChip>
|
||||
))}
|
||||
|
||||
<FilterSep />
|
||||
<FilterLabel>Starred</FilterLabel>
|
||||
<FilterChip $active={showStarredOnly} onClick={() => setShowStarredOnly((v) => !v)}>
|
||||
★ Starred
|
||||
</FilterChip>
|
||||
|
|
@ -379,9 +446,9 @@ export function Library(): ReactElement {
|
|||
)}
|
||||
</SearchWrapper>
|
||||
<FilterLabel>Sort</FilterLabel>
|
||||
{(['newest', 'oldest', 'score'] as const).map((s) => (
|
||||
{(['newest', 'oldest', 'score', 'sharpness'] as const).map((s) => (
|
||||
<FilterChip key={s} $active={sort === s} onClick={() => 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)}
|
||||
</FilterChip>
|
||||
))}
|
||||
</Filters>
|
||||
|
|
@ -439,6 +506,20 @@ export function Library(): ReactElement {
|
|||
{img.durationMs !== undefined && (
|
||||
<Badge>{img.durationMs < 1000 ? `${img.durationMs}ms` : `${(img.durationMs / 1000).toFixed(1)}s`}</Badge>
|
||||
)}
|
||||
{img.analysis?.framing && img.analysis.framing !== 'unknown' && (
|
||||
<Badge $color={theme.colors.textDim}>{img.analysis.framing}</Badge>
|
||||
)}
|
||||
{img.analysis?.style && img.analysis.style !== 'unknown' && (
|
||||
<Badge $color={theme.colors.textDim}>{img.analysis.style}</Badge>
|
||||
)}
|
||||
{img.analysis?.gaze === 'at-camera' && (
|
||||
<Badge $color={theme.colors.success}>eye contact</Badge>
|
||||
)}
|
||||
{(img.analysis?.vlm?.anatomyIssues?.length ?? 0) > 0 && (
|
||||
<WarningBadge title={img.analysis!.vlm!.anatomyIssues.join(', ')}>
|
||||
⚠ anatomy
|
||||
</WarningBadge>
|
||||
)}
|
||||
</CardMeta>
|
||||
<PromptText>{img.prompt}</PromptText>
|
||||
</ImageCard>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue