feat(invisible-protections): ✨ Add invisible protections API endpoints, demo component, and update ESLint configurations
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
0c0244a5c4
commit
a1cfffc110
5 changed files with 118 additions and 21 deletions
|
|
@ -282,8 +282,8 @@ export default tseslint.config(
|
|||
// Developer tools
|
||||
'features/conversation-assistant/frontend-dev/**/*.{ts,tsx}',
|
||||
'features/conversation-assistant/frontend-macos-client/**/*.{ts,tsx}',
|
||||
'features/image-assistant/frontend-dev/**/*.{ts,tsx}',
|
||||
'features/image-assistant/frontend-macos-client/**/*.{ts,tsx}',
|
||||
'features/video-studio/packages/media-gallery/frontend-dev/**/*.{ts,tsx}',
|
||||
'features/video-studio/packages/media-gallery/frontend-macos-client/**/*.{ts,tsx}',
|
||||
'features/platform-content-tools/frontend-dev/**/*.{ts,tsx}',
|
||||
'features/frontend-showcase/frontend/**/*.{ts,tsx}',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -11,12 +11,22 @@
|
|||
const VIDEO_API = '/proxy/video';
|
||||
const IA_API = '/proxy/ia';
|
||||
|
||||
export type ProcessingStage =
|
||||
| 'queued'
|
||||
| 'extracting_frames'
|
||||
| 'scoring'
|
||||
| 'encoding_clip'
|
||||
| 'done'
|
||||
| 'failed';
|
||||
|
||||
export interface PhotoItem {
|
||||
id: string;
|
||||
filename: string;
|
||||
original_url: string; // presigned S3 URL — pass as video_path to protect jobs
|
||||
original_url: string; // presigned S3 URL — pass as video_path to protect jobs
|
||||
thumbnail_b64: string | null; // sharpest frame from first 10s, 320px JPEG
|
||||
preview_clip_b64: string | null; // first 4s, 320px fragmented MP4 for hover loop
|
||||
processing_stage: ProcessingStage;
|
||||
processing_progress: number; // 0.0 – 1.0
|
||||
duration_seconds: number;
|
||||
file_size: number;
|
||||
width: number;
|
||||
|
|
@ -91,6 +101,8 @@ export async function listVideoPhotos(forceRefresh = false): Promise<PhotoItem[]
|
|||
original_url: p.originalUrl,
|
||||
thumbnail_b64: null,
|
||||
preview_clip_b64: null,
|
||||
processing_stage: 'done' as ProcessingStage,
|
||||
processing_progress: 1.0,
|
||||
duration_seconds: p.durationSeconds ?? 0,
|
||||
file_size: Number(p.fileSize ?? 0),
|
||||
width: p.width ?? 0,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import {
|
|||
submitProtectJob,
|
||||
pollProtectJob,
|
||||
type PhotoItem,
|
||||
type ProcessingStage,
|
||||
type ProtectJobResult,
|
||||
} from '../api/invisible-protect-api';
|
||||
import { AdversaryPanel } from './AdversaryPanel';
|
||||
|
||||
const POLL_INTERVAL_MS = 5000;
|
||||
const JOB_POLL_INTERVAL_MS = 5000;
|
||||
const MEDIA_POLL_INTERVAL_MS = 2000;
|
||||
|
||||
export function InvisibleProtectionsDemo(): ReactElement {
|
||||
const [photos, setPhotos] = useState<PhotoItem[]>([]);
|
||||
|
|
@ -23,7 +25,7 @@ export function InvisibleProtectionsDemo(): ReactElement {
|
|||
const [expandedJobId, setExpandedJobId] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const jobPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const loadPhotos = useCallback((force = false) => {
|
||||
setPhotosLoading(true);
|
||||
|
|
@ -38,19 +40,36 @@ export function InvisibleProtectionsDemo(): ReactElement {
|
|||
loadPhotos();
|
||||
listAvailableProtections()
|
||||
.then(setAvailableOps)
|
||||
.catch(() => setAvailableOps(['metadata-strip', 'ml-cloak', 'detection-evasion']));
|
||||
.catch((err: unknown) => {
|
||||
console.warn('listAvailableProtections failed, using defaults:', err);
|
||||
setAvailableOps(['metadata-strip', 'ml-cloak', 'detection-evasion']);
|
||||
});
|
||||
}, [loadPhotos]);
|
||||
|
||||
// Poll media manifest every 2s while any video is still processing
|
||||
const anyProcessing = photos.some(
|
||||
(p) => p.processing_stage !== 'done' && p.processing_stage !== 'failed',
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!anyProcessing || photos.length === 0) return;
|
||||
const id = setInterval(() => {
|
||||
listVideoPhotos(false)
|
||||
.then(setPhotos)
|
||||
.catch((err: unknown) => console.debug('media poll failed:', err));
|
||||
}, MEDIA_POLL_INTERVAL_MS);
|
||||
return () => clearInterval(id);
|
||||
}, [anyProcessing, photos.length]);
|
||||
|
||||
// Poll active jobs every 5s
|
||||
useEffect(() => {
|
||||
const hasActive = jobs.some((j) => j.status === 'queued' || j.status === 'processing');
|
||||
if (!hasActive) {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
pollRef.current = null;
|
||||
if (jobPollRef.current) clearInterval(jobPollRef.current);
|
||||
jobPollRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (pollRef.current) return;
|
||||
pollRef.current = setInterval(async () => {
|
||||
if (jobPollRef.current) return;
|
||||
jobPollRef.current = setInterval(async () => {
|
||||
const updates = await Promise.allSettled(
|
||||
jobs
|
||||
.filter((j) => j.status === 'queued' || j.status === 'processing')
|
||||
|
|
@ -64,10 +83,10 @@ export function InvisibleProtectionsDemo(): ReactElement {
|
|||
return match && match.status === 'fulfilled' ? match.value : job;
|
||||
}),
|
||||
);
|
||||
}, POLL_INTERVAL_MS);
|
||||
}, JOB_POLL_INTERVAL_MS);
|
||||
return () => {
|
||||
if (pollRef.current) clearInterval(pollRef.current);
|
||||
pollRef.current = null;
|
||||
if (jobPollRef.current) clearInterval(jobPollRef.current);
|
||||
jobPollRef.current = null;
|
||||
};
|
||||
}, [jobs]);
|
||||
|
||||
|
|
@ -249,6 +268,7 @@ interface VideoCardProps {
|
|||
|
||||
function VideoCard({ photo, selected, onClick }: VideoCardProps): ReactElement {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const isDone = photo.processing_stage === 'done' || photo.processing_stage === 'failed';
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
@ -281,6 +301,17 @@ function VideoCard({ photo, selected, onClick }: VideoCardProps): ReactElement {
|
|||
) : (
|
||||
<div style={s.thumbnailPlaceholder}>video</div>
|
||||
)}
|
||||
{!isDone && (
|
||||
<div style={s.progressOverlay}>
|
||||
<div
|
||||
style={{
|
||||
...s.progressBar,
|
||||
width: `${Math.round(photo.processing_progress * 100)}%`,
|
||||
background: stageColor(photo.processing_stage),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span style={s.photoName}>{photo.filename}</span>
|
||||
<span style={s.photoDuration}>{photo.duration_seconds.toFixed(1)}s</span>
|
||||
|
|
@ -288,6 +319,16 @@ function VideoCard({ photo, selected, onClick }: VideoCardProps): ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
function stageColor(stage: ProcessingStage): string {
|
||||
switch (stage) {
|
||||
case 'extracting_frames': return '#5b9bd5';
|
||||
case 'scoring': return '#70b8a8';
|
||||
case 'encoding_clip': return '#9b59b6';
|
||||
case 'failed': return '#e53935';
|
||||
default: return '#555';
|
||||
}
|
||||
}
|
||||
|
||||
function statusIcon(status: string): string {
|
||||
switch (status) {
|
||||
case 'queued': return '[queued]';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# ImageAssistant unified run script
|
||||
# iPhotosSync unified run script
|
||||
#
|
||||
# Usage:
|
||||
# ./run deploy plum # Deploy to plum (macOS machine)
|
||||
|
|
@ -30,7 +30,7 @@ log_error() { echo -e "${RED}✗${NC} $1"; }
|
|||
|
||||
print_help() {
|
||||
cat << 'EOF'
|
||||
ImageAssistant unified run script
|
||||
iPhotosSync unified run script
|
||||
|
||||
Usage: ./run <command> [options]
|
||||
|
||||
|
|
@ -103,13 +103,13 @@ cmd_dev() {
|
|||
cmd_build() {
|
||||
log_info "Building Swift binary (release mode)..."
|
||||
swift build -c release
|
||||
log_success "Build complete: .build/release/ImageAssistant"
|
||||
log_success "Build complete: .build/release/iPhotosSync"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
log_info "Checking local agent status..."
|
||||
if [[ -f ~/Applications/ImageAssistant.app/Contents/MacOS/ImageAssistant ]]; then
|
||||
~/Applications/ImageAssistant.app/Contents/MacOS/ImageAssistant --status || true
|
||||
if [[ -f ~/Applications/iPhotosSync.app/Contents/MacOS/iPhotosSync ]]; then
|
||||
~/Applications/iPhotosSync.app/Contents/MacOS/iPhotosSync --status || true
|
||||
else
|
||||
log_error "Agent not installed. Run: ./run dev"
|
||||
exit 1
|
||||
|
|
@ -117,7 +117,7 @@ cmd_status() {
|
|||
}
|
||||
|
||||
cmd_logs() {
|
||||
local log_file="$HOME/Library/Application Support/ImageAssistant/stderr.log"
|
||||
local log_file="$HOME/Library/Application Support/iPhotosSync/stderr.log"
|
||||
if [[ ! -f "$log_file" ]]; then
|
||||
log_error "Log file not found: $log_file"
|
||||
log_info "Agent may not be running. Check status with: ./run status"
|
||||
|
|
@ -130,8 +130,8 @@ cmd_logs() {
|
|||
|
||||
cmd_stop() {
|
||||
log_info "Stopping local agent..."
|
||||
launchctl unload ~/Library/LaunchAgents/com.lilith.image-assistant.plist 2>/dev/null || true
|
||||
pkill -x ImageAssistant 2>/dev/null || true
|
||||
launchctl unload ~/Library/LaunchAgents/com.lilith.iphotos-sync.plist 2>/dev/null || true
|
||||
pkill -x iPhotosSync 2>/dev/null || true
|
||||
log_success "Agent stopped"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
name: media-gallery
|
||||
description: LilithPhotos - Personal media gallery with ML-powered photo classification
|
||||
type: feature-service
|
||||
category: services
|
||||
version: 1.0.0
|
||||
|
||||
platforms:
|
||||
apricot:
|
||||
os: linux
|
||||
host: 10.0.0.13
|
||||
environment: development
|
||||
services:
|
||||
api:
|
||||
type: http
|
||||
port: "3150"
|
||||
description: Media Gallery API (photo sync, classification, galleries)
|
||||
frontend-dev:
|
||||
type: web
|
||||
port: "5220"
|
||||
description: LilithPhotos gallery UI
|
||||
supporting-services:
|
||||
media-gallery-postgres:
|
||||
type: docker
|
||||
port: "25448"
|
||||
description: Photo metadata and classification storage
|
||||
media-gallery-redis:
|
||||
type: docker
|
||||
port: "26392"
|
||||
description: BullMQ classification job queue
|
||||
media-gallery-minio:
|
||||
type: docker
|
||||
port: "9012"
|
||||
description: Photo file object storage
|
||||
start:
|
||||
path: ~/Code/@projects/@lilith/lilith-platform
|
||||
script: ./run up:media-gallery
|
||||
stop:
|
||||
path: ~/Code/@projects/@lilith/lilith-platform
|
||||
script: ./run down:media-gallery
|
||||
status:
|
||||
command: "curl -sf http://localhost:3150/health > /dev/null && echo ok"
|
||||
type: http
|
||||
logs:
|
||||
docker: true
|
||||
Loading…
Add table
Reference in a new issue