diff --git a/features/video-studio/frontend-demo/src/api/invisible-protect-api.ts b/features/video-studio/frontend-demo/src/api/invisible-protect-api.ts index 68e11a65c..4f71eac40 100644 --- a/features/video-studio/frontend-demo/src/api/invisible-protect-api.ts +++ b/features/video-studio/frontend-demo/src/api/invisible-protect-api.ts @@ -5,14 +5,17 @@ */ const IMAJIN_VIDEO_URL = 'http://localhost:8010'; -const IMAGE_ASSISTANT_URL = 'http://10.0.0.116:5220'; export interface PhotoItem { id: string; filename: string; - path: string; - mediaType: string; - thumbnailUrl: string | null; + original_url: string; // presigned S3 URL — pass as video_path to protect jobs + thumbnail_b64: string | null; // base64 JPEG from imajin-video manifest endpoint + duration_seconds: number; + file_size: number; + width: number; + height: number; + captured_at: string; } export interface DetectionResult { @@ -45,15 +48,15 @@ export interface ProtectJobResult { error: string | null; } -export async function listVideoPhotos(): Promise { +export async function listVideoPhotos(forceRefresh = false): Promise { try { - const resp = await fetch(`${IMAGE_ASSISTANT_URL}/api/photos?mediaType=video`); - if (!resp.ok) throw new Error(`Failed to list photos: ${resp.status}`); - const data: unknown = await resp.json(); - if (Array.isArray(data)) return data as PhotoItem[]; - const obj = data as Record; - const list = obj['photos'] ?? obj['items'] ?? []; - return list as PhotoItem[]; + const url = forceRefresh + ? `${IMAJIN_VIDEO_URL}/media/videos?force_refresh=true` + : `${IMAJIN_VIDEO_URL}/media/videos`; + const resp = await fetch(url); + if (!resp.ok) throw new Error(`Failed to list videos: ${resp.status}`); + const data = await resp.json() as { videos: PhotoItem[] }; + return data.videos; } catch (err) { throw err instanceof Error ? err diff --git a/features/video-studio/frontend-demo/src/components/InvisibleProtectionsDemo.tsx b/features/video-studio/frontend-demo/src/components/InvisibleProtectionsDemo.tsx index a36969c36..296acb750 100644 --- a/features/video-studio/frontend-demo/src/components/InvisibleProtectionsDemo.tsx +++ b/features/video-studio/frontend-demo/src/components/InvisibleProtectionsDemo.tsx @@ -13,6 +13,7 @@ const POLL_INTERVAL_MS = 5000; export function InvisibleProtectionsDemo(): ReactElement { const [photos, setPhotos] = useState([]); + const [photosLoading, setPhotosLoading] = useState(true); const [photosError, setPhotosError] = useState(null); const [availableOps, setAvailableOps] = useState([]); const [selectedPhoto, setSelectedPhoto] = useState(null); @@ -24,15 +25,21 @@ export function InvisibleProtectionsDemo(): ReactElement { const [submitError, setSubmitError] = useState(null); const pollRef = useRef | null>(null); + const loadPhotos = useCallback((force = false) => { + setPhotosLoading(true); + setPhotosError(null); + listVideoPhotos(force) + .then((items) => { setPhotos(items); setPhotosLoading(false); }) + .catch((err: unknown) => { setPhotosError(String(err)); setPhotosLoading(false); }); + }, []); + // Load gallery + available operations on mount useEffect(() => { - listVideoPhotos() - .then(setPhotos) - .catch((err: unknown) => setPhotosError(String(err))); + loadPhotos(); listAvailableProtections() .then(setAvailableOps) .catch(() => setAvailableOps(['metadata-strip', 'ml-cloak', 'detection-evasion'])); - }, []); + }, [loadPhotos]); // Poll active jobs every 5s useEffect(() => { @@ -75,7 +82,7 @@ export function InvisibleProtectionsDemo(): ReactElement { setSubmitting(true); setSubmitError(null); try { - const { job_id } = await submitProtectJob(selectedPhoto.path, selectedOps, outputMode); + const { job_id } = await submitProtectJob(selectedPhoto.original_url, selectedOps, outputMode); const initial: ProtectJobResult = { job_id, status: 'queued', @@ -98,17 +105,28 @@ export function InvisibleProtectionsDemo(): ReactElement {
{/* Gallery */}
-

Select Video

+
+

Select Video

+ +
{photosError ? (
Failed to load gallery: {photosError}
- - Is image-assistant running at 10.0.0.116:5220? - + Is imajin-video running at localhost:8010?
+ ) : photosLoading ? ( +
Loading videos — extracting thumbnails...
) : photos.length === 0 ? ( -
Loading videos...
+
No videos found. Import some via Image Assistant.
) : (
{photos.map((photo) => ( @@ -120,11 +138,11 @@ export function InvisibleProtectionsDemo(): ReactElement { ...s.photoCard, ...(selectedPhoto?.id === photo.id ? s.photoCardSelected : {}), }} - title={photo.filename} + title={`${photo.filename} · ${photo.duration_seconds.toFixed(1)}s`} > - {photo.thumbnailUrl ? ( + {photo.thumbnail_b64 ? ( {photo.filename} @@ -132,6 +150,7 @@ export function InvisibleProtectionsDemo(): ReactElement {
video
)} {photo.filename} + {photo.duration_seconds.toFixed(1)}s ))}
@@ -326,6 +345,26 @@ const s = { wordBreak: 'break-all' as const, lineHeight: 1.3, }, + photoDuration: { + fontSize: '10px', + color: '#555', + fontFamily: 'monospace', + }, + galleryHeader: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: '12px', + }, + refreshBtn: { + background: 'none', + border: '1px solid #333', + borderRadius: '4px', + color: '#666', + fontSize: '11px', + cursor: 'pointer', + padding: '3px 8px', + }, opPicker: { background: '#111', borderRadius: '8px',