vicliv commited on
Commit
2b84807
·
1 Parent(s): c6e310c

added support for gifs

Browse files
Files changed (2) hide show
  1. app/main.py +20 -2
  2. app/static/index.html +20 -13
app/main.py CHANGED
@@ -13,7 +13,7 @@ from PIL import Image, ImageOps
13
 
14
  from .model import load_detector, predict_image
15
  from .screenshot import preprocess
16
- from .video import sample_frames
17
 
18
  MAX_IMAGE_SIZE_MB = 50
19
  MAX_VIDEO_SIZE_MB = 300
@@ -21,6 +21,7 @@ N_VIDEO_FRAMES = 5
21
 
22
  IMAGE_TYPES = {"image/jpeg", "image/jpg", "image/png", "image/webp"}
23
  VIDEO_TYPES = {"video/mp4", "video/quicktime", "video/webm", "video/x-matroska"}
 
24
 
25
  HF_REPORT_REPO = os.environ.get("HF_REPORT_REPO", "ComplexDataLab/openfake-reports")
26
  HF_TOKEN = os.environ.get("HF_TOKEN")
@@ -127,6 +128,23 @@ async def predict(file: UploadFile = File(...)):
127
  "frame_probs": probs,
128
  }
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  raise HTTPException(415, f"Unsupported media type: {content_type}")
131
 
132
 
@@ -153,7 +171,7 @@ async def report(
153
  # Read the uploaded file
154
  raw = await file.read()
155
  content_type = (file.content_type or "").lower()
156
- if content_type not in IMAGE_TYPES | VIDEO_TYPES:
157
  raise HTTPException(415, "Unsupported file type for reporting.")
158
 
159
  # Build report payload
 
13
 
14
  from .model import load_detector, predict_image
15
  from .screenshot import preprocess
16
+ from .video import sample_frames, sample_gif_frames
17
 
18
  MAX_IMAGE_SIZE_MB = 50
19
  MAX_VIDEO_SIZE_MB = 300
 
21
 
22
  IMAGE_TYPES = {"image/jpeg", "image/jpg", "image/png", "image/webp"}
23
  VIDEO_TYPES = {"video/mp4", "video/quicktime", "video/webm", "video/x-matroska"}
24
+ GIF_TYPES = {"image/gif"}
25
 
26
  HF_REPORT_REPO = os.environ.get("HF_REPORT_REPO", "ComplexDataLab/openfake-reports")
27
  HF_TOKEN = os.environ.get("HF_TOKEN")
 
128
  "frame_probs": probs,
129
  }
130
 
131
+ if content_type in GIF_TYPES:
132
+ if size_mb > MAX_IMAGE_SIZE_MB:
133
+ raise HTTPException(413, f"GIF exceeds {MAX_IMAGE_SIZE_MB} MB")
134
+ try:
135
+ frames = sample_gif_frames(raw, N_VIDEO_FRAMES)
136
+ except ValueError as e:
137
+ raise HTTPException(400, str(e))
138
+ probs = [predict_image(f) for f in frames]
139
+ p_fake = sum(probs) / len(probs)
140
+ return {
141
+ "media_type": "gif",
142
+ "p_fake": p_fake,
143
+ "reliability": 1.0 - p_fake,
144
+ "n_frames": len(frames),
145
+ "frame_probs": probs,
146
+ }
147
+
148
  raise HTTPException(415, f"Unsupported media type: {content_type}")
149
 
150
 
 
171
  # Read the uploaded file
172
  raw = await file.read()
173
  content_type = (file.content_type or "").lower()
174
+ if content_type not in IMAGE_TYPES | VIDEO_TYPES | GIF_TYPES:
175
  raise HTTPException(415, "Unsupported file type for reporting.")
176
 
177
  # Build report payload
app/static/index.html CHANGED
@@ -45,7 +45,7 @@
45
  <label id="dropzone" for="file-input"
46
  class="block border-2 border-dashed border-gray-300 rounded-xl p-10 text-center cursor-pointer hover:border-blue-400 transition-colors">
47
  <input id="file-input" type="file" class="hidden"
48
- accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm" />
49
  <div id="upload-prompt">
50
  <div class="mx-auto w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mb-4">
51
  <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
@@ -285,9 +285,9 @@
285
  const I18N = {
286
  en: {
287
  title: "Deepfake detector",
288
- subtitle: "Upload an image or short video to check if it's AI-generated.",
289
  upload_cta: "Choose a file or drag and drop",
290
- upload_hint: "JPG, PNG, MP4 up to 100 MB",
291
  analyze: "Analyze",
292
  clear: "Clear",
293
  analyzing: "Analyzing...",
@@ -297,10 +297,13 @@
297
  ai_label: "AI-generated",
298
  verdict_ai_image: "This image is likely AI-generated,",
299
  verdict_ai_video: "This video is likely AI-generated,",
 
300
  verdict_uncertain_image: "We're uncertain about this image,",
301
  verdict_uncertain_video: "We're uncertain about this video,",
 
302
  verdict_real_image: "This image is likely authentic,",
303
  verdict_real_video: "This video is likely authentic,",
 
304
  advice_ai: "you should not share it with your network.",
305
  advice_uncertain: "verify it from trusted sources before sharing.",
306
  advice_real: "but always cross-check important content.",
@@ -314,7 +317,7 @@
314
  preview_full: "Full image analyzed",
315
  preview_text_only: "Text-only screenshot — score softened",
316
  how_calculated_title: "How the score is computed",
317
- how_calculated_body: "We use a Swin Transformer V2 model fine-tuned to distinguish real photographs from AI-generated images. For videos, we sample 5 frames evenly across the duration and average the model's confidence. The score shown is the model's estimated probability that the content was generated by AI.",
318
  close: "Close",
319
  privacy_note: "Files are processed in memory and not stored.",
320
  disclaimer_mistakes: "The detector can make mistakes. Results are probabilistic and should not be treated as ground truth.",
@@ -359,9 +362,9 @@
359
  },
360
  fr: {
361
  title: "Détecteur d'hypertrucage",
362
- subtitle: "Téléversez une image ou une courte vidéo pour vérifier si elle est générée par IA.",
363
  upload_cta: "Choisissez un fichier ou glissez-déposez",
364
- upload_hint: "JPG, PNG, MP4 jusqu'à 100 Mo",
365
  analyze: "Analyser",
366
  clear: "Effacer",
367
  analyzing: "Analyse en cours...",
@@ -371,10 +374,13 @@
371
  ai_label: "IA générée",
372
  verdict_ai_image: "Cette image est probablement générée par IA,",
373
  verdict_ai_video: "Cette vidéo est probablement générée par IA,",
 
374
  verdict_uncertain_image: "Nous ne sommes pas certains pour cette image,",
375
  verdict_uncertain_video: "Nous ne sommes pas certains pour cette vidéo,",
 
376
  verdict_real_image: "Cette image est probablement authentique,",
377
  verdict_real_video: "Cette vidéo est probablement authentique,",
 
378
  advice_ai: "vous ne devriez pas la partager avec votre réseau.",
379
  advice_uncertain: "vérifiez auprès de sources fiables avant de partager.",
380
  advice_real: "vérifiez tout de même les contenus importants.",
@@ -388,7 +394,7 @@
388
  preview_full: "Image entière analysée",
389
  preview_text_only: "Capture texte uniquement — score atténué",
390
  how_calculated_title: "Comment le score est calculé",
391
- how_calculated_body: "Nous utilisons un modèle Swin Transformer V2 entraîné pour distinguer les vraies photographies des images générées par IA. Pour les vidéos, nous échantillonnons 5 images réparties uniformément sur la durée et faisons la moyenne de la confiance du modèle. Le score affiché correspond à la probabilité estimée que le contenu ait été généré par IA.",
392
  close: "Fermer",
393
  privacy_note: "Les fichiers sont traités en mémoire et ne sont pas conservés.",
394
  disclaimer_mistakes: "Le détecteur peut faire des erreurs. Les résultats sont probabilistes et ne doivent pas être considérés comme une vérité absolue.",
@@ -477,22 +483,23 @@
477
 
478
  function getVerdict(aiScore, mediaType) {
479
  const T = t();
 
480
  if (aiScore >= 0.60) {
481
  return {
482
- verdict: mediaType === "video" ? T.verdict_ai_video : T.verdict_ai_image,
483
  advice: T.advice_ai,
484
  tone: "ai",
485
  };
486
  }
487
  if (aiScore >= 0.30) {
488
  return {
489
- verdict: mediaType === "video" ? T.verdict_uncertain_video : T.verdict_uncertain_image,
490
  advice: T.advice_uncertain,
491
  tone: "uncertain",
492
  };
493
  }
494
  return {
495
- verdict: mediaType === "video" ? T.verdict_real_video : T.verdict_real_image,
496
  advice: T.advice_real,
497
  tone: "real",
498
  };
@@ -580,7 +587,7 @@
580
  const color = tones[v.tone];
581
  $("arc-fg").setAttribute("stroke", color);
582
  $("verdict-text").style.color = color;
583
- if (state.result.media_type === "video") {
584
  $("frames-info").textContent = t().frames_info.replace("{n}", state.result.n_frames);
585
  } else {
586
  $("frames-info").textContent = "";
@@ -667,8 +674,8 @@
667
  if (!state.file) return;
668
  state.loading = true;
669
  state.error = null;
670
- const isVideo = state.file.type.startsWith("video/");
671
- $("loading-text").textContent = isVideo ? t().analyzing_video : t().analyzing;
672
  showCard("loading-card");
673
 
674
  const form = new FormData();
 
45
  <label id="dropzone" for="file-input"
46
  class="block border-2 border-dashed border-gray-300 rounded-xl p-10 text-center cursor-pointer hover:border-blue-400 transition-colors">
47
  <input id="file-input" type="file" class="hidden"
48
+ accept="image/jpeg,image/png,image/webp,image/gif,video/mp4,video/quicktime,video/webm" />
49
  <div id="upload-prompt">
50
  <div class="mx-auto w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mb-4">
51
  <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
 
285
  const I18N = {
286
  en: {
287
  title: "Deepfake detector",
288
+ subtitle: "Upload an image, GIF, or short video to check if it's AI-generated.",
289
  upload_cta: "Choose a file or drag and drop",
290
+ upload_hint: "JPG, PNG, GIF, MP4 up to 100 MB",
291
  analyze: "Analyze",
292
  clear: "Clear",
293
  analyzing: "Analyzing...",
 
297
  ai_label: "AI-generated",
298
  verdict_ai_image: "This image is likely AI-generated,",
299
  verdict_ai_video: "This video is likely AI-generated,",
300
+ verdict_ai_gif: "This GIF is likely AI-generated,",
301
  verdict_uncertain_image: "We're uncertain about this image,",
302
  verdict_uncertain_video: "We're uncertain about this video,",
303
+ verdict_uncertain_gif: "We're uncertain about this GIF,",
304
  verdict_real_image: "This image is likely authentic,",
305
  verdict_real_video: "This video is likely authentic,",
306
+ verdict_real_gif: "This GIF is likely authentic,",
307
  advice_ai: "you should not share it with your network.",
308
  advice_uncertain: "verify it from trusted sources before sharing.",
309
  advice_real: "but always cross-check important content.",
 
317
  preview_full: "Full image analyzed",
318
  preview_text_only: "Text-only screenshot — score softened",
319
  how_calculated_title: "How the score is computed",
320
+ how_calculated_body: "We use a Swin Transformer V2 model fine-tuned to distinguish real photographs from AI-generated images. For videos and GIFs, we sample 5 frames evenly across the duration and average the model's confidence. The score shown is the model's estimated probability that the content was generated by AI.",
321
  close: "Close",
322
  privacy_note: "Files are processed in memory and not stored.",
323
  disclaimer_mistakes: "The detector can make mistakes. Results are probabilistic and should not be treated as ground truth.",
 
362
  },
363
  fr: {
364
  title: "Détecteur d'hypertrucage",
365
+ subtitle: "Téléversez une image, un GIF ou une courte vidéo pour vérifier si elle est générée par IA.",
366
  upload_cta: "Choisissez un fichier ou glissez-déposez",
367
+ upload_hint: "JPG, PNG, GIF, MP4 jusqu'à 100 Mo",
368
  analyze: "Analyser",
369
  clear: "Effacer",
370
  analyzing: "Analyse en cours...",
 
374
  ai_label: "IA générée",
375
  verdict_ai_image: "Cette image est probablement générée par IA,",
376
  verdict_ai_video: "Cette vidéo est probablement générée par IA,",
377
+ verdict_ai_gif: "Ce GIF est probablement généré par IA,",
378
  verdict_uncertain_image: "Nous ne sommes pas certains pour cette image,",
379
  verdict_uncertain_video: "Nous ne sommes pas certains pour cette vidéo,",
380
+ verdict_uncertain_gif: "Nous ne sommes pas certains pour ce GIF,",
381
  verdict_real_image: "Cette image est probablement authentique,",
382
  verdict_real_video: "Cette vidéo est probablement authentique,",
383
+ verdict_real_gif: "Ce GIF est probablement authentique,",
384
  advice_ai: "vous ne devriez pas la partager avec votre réseau.",
385
  advice_uncertain: "vérifiez auprès de sources fiables avant de partager.",
386
  advice_real: "vérifiez tout de même les contenus importants.",
 
394
  preview_full: "Image entière analysée",
395
  preview_text_only: "Capture texte uniquement — score atténué",
396
  how_calculated_title: "Comment le score est calculé",
397
+ how_calculated_body: "Nous utilisons un modèle Swin Transformer V2 entraîné pour distinguer les vraies photographies des images générées par IA. Pour les vidéos et les GIFs, nous échantillonnons 5 images réparties uniformément sur la durée et faisons la moyenne de la confiance du modèle. Le score affiché correspond à la probabilité estimée que le contenu ait été généré par IA.",
398
  close: "Fermer",
399
  privacy_note: "Les fichiers sont traités en mémoire et ne sont pas conservés.",
400
  disclaimer_mistakes: "Le détecteur peut faire des erreurs. Les résultats sont probabilistes et ne doivent pas être considérés comme une vérité absolue.",
 
483
 
484
  function getVerdict(aiScore, mediaType) {
485
  const T = t();
486
+ const key = mediaType === "video" ? "video" : mediaType === "gif" ? "gif" : "image";
487
  if (aiScore >= 0.60) {
488
  return {
489
+ verdict: T[`verdict_ai_${key}`],
490
  advice: T.advice_ai,
491
  tone: "ai",
492
  };
493
  }
494
  if (aiScore >= 0.30) {
495
  return {
496
+ verdict: T[`verdict_uncertain_${key}`],
497
  advice: T.advice_uncertain,
498
  tone: "uncertain",
499
  };
500
  }
501
  return {
502
+ verdict: T[`verdict_real_${key}`],
503
  advice: T.advice_real,
504
  tone: "real",
505
  };
 
587
  const color = tones[v.tone];
588
  $("arc-fg").setAttribute("stroke", color);
589
  $("verdict-text").style.color = color;
590
+ if (state.result.media_type === "video" || state.result.media_type === "gif") {
591
  $("frames-info").textContent = t().frames_info.replace("{n}", state.result.n_frames);
592
  } else {
593
  $("frames-info").textContent = "";
 
674
  if (!state.file) return;
675
  state.loading = true;
676
  state.error = null;
677
+ const isVideoOrGif = state.file.type.startsWith("video/") || state.file.type === "image/gif";
678
+ $("loading-text").textContent = isVideoOrGif ? t().analyzing_video : t().analyzing;
679
  showCard("loading-card");
680
 
681
  const form = new FormData();