Spaces:
Running
Running
added support for gifs
Browse files- app/main.py +20 -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:
|
| 483 |
advice: T.advice_ai,
|
| 484 |
tone: "ai",
|
| 485 |
};
|
| 486 |
}
|
| 487 |
if (aiScore >= 0.30) {
|
| 488 |
return {
|
| 489 |
-
verdict:
|
| 490 |
advice: T.advice_uncertain,
|
| 491 |
tone: "uncertain",
|
| 492 |
};
|
| 493 |
}
|
| 494 |
return {
|
| 495 |
-
verdict:
|
| 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
|
| 671 |
-
$("loading-text").textContent =
|
| 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();
|