ATS-27 commited on
Commit
adad1d3
·
1 Parent(s): dec3b83

Refreshed backend assets and scripts via Git LFS

Browse files
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
.gitattributes CHANGED
@@ -33,18 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- backend/test_noise.png filter=lfs diff=lfs merge=lfs -text
37
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/Pepsi[[:space:]]can[[:space:]]real.webp filter=lfs diff=lfs merge=lfs -text
38
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/Scenery-1.jpg filter=lfs diff=lfs merge=lfs -text
39
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/Scenery-2.jpg filter=lfs diff=lfs merge=lfs -text
40
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/WIN_20240923_10_49_05_Pro.jpg filter=lfs diff=lfs merge=lfs -text
41
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/ai-generated-face.png filter=lfs diff=lfs merge=lfs -text
42
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/digital[[:space:]]AI[[:space:]]painting.jpg filter=lfs diff=lfs merge=lfs -text
43
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/skynews-zelenskyy-deepfake_5708613.jpg filter=lfs diff=lfs merge=lfs -text
44
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/stable_diffusion[[:space:]]image.jpg filter=lfs diff=lfs merge=lfs -text
45
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/wonderland-girl-generated-by-Fotor-ai-art-generator.webp filter=lfs diff=lfs merge=lfs -text
46
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Images/yard.jpg filter=lfs diff=lfs merge=lfs -text
47
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Videos/229254_tiny.mp4 filter=lfs diff=lfs merge=lfs -text
48
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Videos/WIN_20260225_20_51_10_Pro.mp4 filter=lfs diff=lfs merge=lfs -text
49
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Videos/WIN_20260315_16_53_03_Pro.mp4 filter=lfs diff=lfs merge=lfs -text
50
- video_bundle/Sample[[:space:]]Images[[:space:]]and[[:space:]]Videos/Videos/video_307231ea-7a0a-49cf-a74b-616bc9c80f7b.mp4 filter=lfs diff=lfs merge=lfs -text
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.webp filter=lfs diff=lfs merge=lfs -text
37
+ *.jpg filter=lfs diff=lfs merge=lfs -text
38
+ *.png filter=lfs diff=lfs merge=lfs -text
39
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
backend/.DS_Store CHANGED
Binary files a/backend/.DS_Store and b/backend/.DS_Store differ
 
backend/model_service.py CHANGED
@@ -32,13 +32,20 @@ REPO_DIR = ROOT / 'repo_inspect'
32
  INTEGRATION_ASSETS_DIR = ROOT / 'integration_assets'
33
  VIDEO_BUNDLE_DIR = ROOT / 'video_bundle'
34
  VIDEO_CODE_DIR = VIDEO_BUNDLE_DIR / 'Video'
35
- for path in [REPO_DIR, INTEGRATION_ASSETS_DIR, VIDEO_BUNDLE_DIR, VIDEO_CODE_DIR]:
36
- if str(path) not in sys.path:
37
- sys.path.insert(0, str(path))
 
 
38
 
39
  from ethical_assessment import EthicalAssessment, format_ethical_report, get_simple_status # noqa: E402
40
  from video_model import ResNetLSTM, GradCAM as VideoGradCAM, overlay_cam # noqa: E402
41
 
 
 
 
 
 
42
  PRIMARY_WEIGHTS_PATH = ROOT / 'models_adv' / 'best_model_weights.pt'
43
  FALLBACK_WEIGHTS_PATH = ROOT / 'integration_assets' / 'best_model_weights.pt'
44
  WEIGHTS_PATH = PRIMARY_WEIGHTS_PATH if PRIMARY_WEIGHTS_PATH.exists() else FALLBACK_WEIGHTS_PATH
@@ -152,6 +159,7 @@ class InferenceArtifacts:
152
  artifacts: list[dict[str, Any]]
153
  source_analysis: dict[str, Any] | None
154
  ethical: dict[str, Any] | None
 
155
 
156
 
157
  @dataclass
@@ -203,8 +211,11 @@ def detect_backbone(state_dict: dict[str, torch.Tensor]) -> str:
203
 
204
 
205
  state_dict = torch.load(WEIGHTS_PATH, map_location=DEVICE)
206
- model = EfficientNetFFTFusion(backbone=detect_backbone(state_dict)).to(DEVICE)
207
- model.load_state_dict(state_dict, strict=False)
 
 
 
208
  model.eval()
209
  MODEL_INFO = {'model_type': 'efficientnet_fft', 'backbone': detect_backbone(state_dict), 'optimal_threshold': THRESHOLD_AI}
210
 
@@ -217,7 +228,7 @@ if GAN_DIFF_WEIGHTS_PATH.exists() and GAN_DIFF_CONFIG_PATH.exists():
217
  gan_diff_state = torch.load(GAN_DIFF_WEIGHTS_PATH, map_location=DEVICE)
218
  if isinstance(gan_diff_state, dict) and 'model_state' in gan_diff_state:
219
  gan_diff_state = gan_diff_state['model_state']
220
- gan_diff_model.load_state_dict(gan_diff_state, strict=False)
221
  GAN_DIFF_MODEL = gan_diff_model.to(DEVICE)
222
  GAN_DIFF_MODEL.eval()
223
 
@@ -233,8 +244,11 @@ if VIDEO_WEIGHTS_PATH.exists():
233
  temporal_pool=VIDEO_CONFIG.get('temporal_pool', 'mean'),
234
  pretrained=False,
235
  backbone_name=VIDEO_CONFIG.get('backbone', 'xception'),
236
- ).to(DEVICE)
237
- VIDEO_MODEL.load_state_dict(video_ckpt['model_state'], strict=False)
 
 
 
238
  VIDEO_MODEL.eval()
239
 
240
 
@@ -338,10 +352,15 @@ def analyze_fft(image: Image.Image) -> dict[str, Any]:
338
 
339
 
340
  def overlay_gradcam_from_pil(pil_image: Image.Image) -> tuple[Image.Image, np.ndarray]:
 
 
 
341
  input_tensor = transform(pil_image).unsqueeze(0).to(DEVICE)
 
342
  target_layer = model.backbone.conv_head
343
  grad_cam = SimpleGradCAM(model, target_layer)
344
  try:
 
345
  cam = grad_cam.generate_cam(input_tensor, target_class=1)
346
  finally:
347
  grad_cam.close()
@@ -417,8 +436,12 @@ def predict_source_from_pil(image: Image.Image) -> dict[str, Any] | None:
417
  probs = torch.softmax(logits, dim=1)[0].cpu().numpy()
418
  pred_idx = int(np.argmax(probs))
419
  labels = {int(k): str(v) for k, v in id_to_label.items()} if isinstance(id_to_label, dict) else {0: 'gan', 1: 'diffusion'}
 
 
420
  return {
421
- 'predictedSource': labels.get(pred_idx, 'unknown'),
 
 
422
  'ganProbability': round(float(probs[0]), 6),
423
  'diffusionProbability': round(float(probs[1]), 6),
424
  }
@@ -503,6 +526,7 @@ def analyze_video_file(video_path: str, filename: str, content_type: str, size_b
503
  pil_frames = [Image.fromarray(frame) for frame in frames]
504
  frame_tensors = [video_transform(frame).unsqueeze(0) for frame in pil_frames]
505
  clip = torch.stack([tensor.squeeze(0) for tensor in frame_tensors], dim=0).unsqueeze(0).to(DEVICE)
 
506
 
507
  with torch.no_grad():
508
  frame_logits, video_logits = VIDEO_MODEL(clip)
@@ -548,6 +572,20 @@ def analyze_video_file(video_path: str, filename: str, content_type: str, size_b
548
  lead_image = pil_frames[lead_idx]
549
  ethical = run_ethical_assessment(np.array(lead_image.convert('RGB'))) if verdict != 'authentic' else None
550
  source_analysis = predict_source_from_pil(lead_image) if verdict != 'authentic' else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  metadata = {
552
  'mimeType': content_type,
553
  'dimensions': f'{lead_image.width} × {lead_image.height} px',
@@ -599,6 +637,7 @@ def analyze_video_file(video_path: str, filename: str, content_type: str, size_b
599
  'metadata': metadata,
600
  'sourceAnalysis': source_analysis,
601
  'ethical': ethical,
 
602
  'raw': {
603
  'probabilityAi': round(fused_video_score, 6),
604
  'rawSequenceProbabilityAi': round(prob_fake, 6),
@@ -627,6 +666,20 @@ def format_result(image: Image.Image, filename: str, content_type: str, size_byt
627
  ethical = run_ethical_assessment(np.array(image.convert('RGB'))) if verdict != 'authentic' else None
628
  artifacts = build_artifacts(probability_ai, fft_summary, metadata, heatmap_regions, ethical)
629
  source_analysis = predict_source_from_pil(image) if verdict != 'authentic' else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
  fft_branch_score = round(min(99.9, fft_summary['bands']['high'] / max(fft_summary['bands']['low'], 1e-6) * 40), 2)
631
  model_breakdown = [
632
  {'model': 'EfficientNet-B2 Spatial', 'score': round(probability_ai * 100, 2), 'weight': 0.55},
@@ -635,7 +688,7 @@ def format_result(image: Image.Image, filename: str, content_type: str, size_byt
635
  if source_analysis:
636
  model_breakdown.append({'model': f"AI Source: {source_analysis['predictedSource'].upper()}", 'score': round(max(source_analysis['ganProbability'], source_analysis['diffusionProbability']) * 100, 2), 'weight': 0.20})
637
  metadata['heatmapPreview'] = image_to_base64(overlay_image)
638
- return InferenceArtifacts(probability_ai, probability_authentic, verdict, round(confidence_score, 2), heatmap_regions, fft_summary, metadata, model_breakdown, artifacts, source_analysis, ethical)
639
 
640
 
641
  @app.get('/api/health')
@@ -721,6 +774,7 @@ async def analyze_media(file: UploadFile = File(...)) -> JSONResponse:
721
  'metadata': artifacts.metadata,
722
  'sourceAnalysis': artifacts.source_analysis,
723
  'ethical': artifacts.ethical,
 
724
  'raw': {
725
  'probabilityAi': round(artifacts.probability_ai, 6),
726
  'probabilityAuthentic': round(artifacts.probability_authentic, 6),
 
32
  INTEGRATION_ASSETS_DIR = ROOT / 'integration_assets'
33
  VIDEO_BUNDLE_DIR = ROOT / 'video_bundle'
34
  VIDEO_CODE_DIR = VIDEO_BUNDLE_DIR / 'Video'
35
+ for path in [ROOT, VIDEO_CODE_DIR, VIDEO_BUNDLE_DIR, REPO_DIR, INTEGRATION_ASSETS_DIR]:
36
+ path_str = str(path)
37
+ if path_str in sys.path:
38
+ sys.path.remove(path_str)
39
+ sys.path.insert(0, path_str)
40
 
41
  from ethical_assessment import EthicalAssessment, format_ethical_report, get_simple_status # noqa: E402
42
  from video_model import ResNetLSTM, GradCAM as VideoGradCAM, overlay_cam # noqa: E402
43
 
44
+ try:
45
+ from neurosymbolic import run_neurosymbolic_assessment # type: ignore # noqa: E402
46
+ except Exception:
47
+ run_neurosymbolic_assessment = None
48
+
49
  PRIMARY_WEIGHTS_PATH = ROOT / 'models_adv' / 'best_model_weights.pt'
50
  FALLBACK_WEIGHTS_PATH = ROOT / 'integration_assets' / 'best_model_weights.pt'
51
  WEIGHTS_PATH = PRIMARY_WEIGHTS_PATH if PRIMARY_WEIGHTS_PATH.exists() else FALLBACK_WEIGHTS_PATH
 
159
  artifacts: list[dict[str, Any]]
160
  source_analysis: dict[str, Any] | None
161
  ethical: dict[str, Any] | None
162
+ neurosymbolic: dict[str, Any] | None
163
 
164
 
165
  @dataclass
 
211
 
212
 
213
  state_dict = torch.load(WEIGHTS_PATH, map_location=DEVICE)
214
+ model = EfficientNetFFTFusion(backbone=detect_backbone(state_dict))
215
+ model.load_state_dict(state_dict, strict=True)
216
+ for parameter in model.parameters():
217
+ parameter.requires_grad_(True)
218
+ model = model.to(DEVICE)
219
  model.eval()
220
  MODEL_INFO = {'model_type': 'efficientnet_fft', 'backbone': detect_backbone(state_dict), 'optimal_threshold': THRESHOLD_AI}
221
 
 
228
  gan_diff_state = torch.load(GAN_DIFF_WEIGHTS_PATH, map_location=DEVICE)
229
  if isinstance(gan_diff_state, dict) and 'model_state' in gan_diff_state:
230
  gan_diff_state = gan_diff_state['model_state']
231
+ gan_diff_model.load_state_dict(gan_diff_state, strict=True)
232
  GAN_DIFF_MODEL = gan_diff_model.to(DEVICE)
233
  GAN_DIFF_MODEL.eval()
234
 
 
244
  temporal_pool=VIDEO_CONFIG.get('temporal_pool', 'mean'),
245
  pretrained=False,
246
  backbone_name=VIDEO_CONFIG.get('backbone', 'xception'),
247
+ )
248
+ VIDEO_MODEL.load_state_dict(video_ckpt['model_state'], strict=True)
249
+ for parameter in VIDEO_MODEL.parameters():
250
+ parameter.requires_grad_(True)
251
+ VIDEO_MODEL = VIDEO_MODEL.to(DEVICE)
252
  VIDEO_MODEL.eval()
253
 
254
 
 
352
 
353
 
354
  def overlay_gradcam_from_pil(pil_image: Image.Image) -> tuple[Image.Image, np.ndarray]:
355
+ model.eval()
356
+ for parameter in model.parameters():
357
+ parameter.requires_grad_(True)
358
  input_tensor = transform(pil_image).unsqueeze(0).to(DEVICE)
359
+ input_tensor.requires_grad_(True)
360
  target_layer = model.backbone.conv_head
361
  grad_cam = SimpleGradCAM(model, target_layer)
362
  try:
363
+ model.zero_grad(set_to_none=True)
364
  cam = grad_cam.generate_cam(input_tensor, target_class=1)
365
  finally:
366
  grad_cam.close()
 
436
  probs = torch.softmax(logits, dim=1)[0].cpu().numpy()
437
  pred_idx = int(np.argmax(probs))
438
  labels = {int(k): str(v) for k, v in id_to_label.items()} if isinstance(id_to_label, dict) else {0: 'gan', 1: 'diffusion'}
439
+ predicted_source = labels.get(pred_idx, 'unknown')
440
+ top_prob = float(probs[pred_idx])
441
  return {
442
+ 'predictedSource': predicted_source,
443
+ 'label': predicted_source,
444
+ 'top_prob': round(top_prob, 6),
445
  'ganProbability': round(float(probs[0]), 6),
446
  'diffusionProbability': round(float(probs[1]), 6),
447
  }
 
526
  pil_frames = [Image.fromarray(frame) for frame in frames]
527
  frame_tensors = [video_transform(frame).unsqueeze(0) for frame in pil_frames]
528
  clip = torch.stack([tensor.squeeze(0) for tensor in frame_tensors], dim=0).unsqueeze(0).to(DEVICE)
529
+ clip.requires_grad_(True)
530
 
531
  with torch.no_grad():
532
  frame_logits, video_logits = VIDEO_MODEL(clip)
 
572
  lead_image = pil_frames[lead_idx]
573
  ethical = run_ethical_assessment(np.array(lead_image.convert('RGB'))) if verdict != 'authentic' else None
574
  source_analysis = predict_source_from_pil(lead_image) if verdict != 'authentic' else None
575
+ neurosymbolic = None
576
+ if verdict != 'authentic' and run_neurosymbolic_assessment is not None:
577
+ try:
578
+ neurosymbolic = run_neurosymbolic_assessment(
579
+ np.array(lead_image.convert('RGB')),
580
+ fused_video_score,
581
+ source_analysis,
582
+ {
583
+ 'risk_score': ethical['riskScore'],
584
+ 'status': ethical['status'],
585
+ } if ethical else None,
586
+ )
587
+ except Exception:
588
+ neurosymbolic = None
589
  metadata = {
590
  'mimeType': content_type,
591
  'dimensions': f'{lead_image.width} × {lead_image.height} px',
 
637
  'metadata': metadata,
638
  'sourceAnalysis': source_analysis,
639
  'ethical': ethical,
640
+ 'neurosymbolic': neurosymbolic,
641
  'raw': {
642
  'probabilityAi': round(fused_video_score, 6),
643
  'rawSequenceProbabilityAi': round(prob_fake, 6),
 
666
  ethical = run_ethical_assessment(np.array(image.convert('RGB'))) if verdict != 'authentic' else None
667
  artifacts = build_artifacts(probability_ai, fft_summary, metadata, heatmap_regions, ethical)
668
  source_analysis = predict_source_from_pil(image) if verdict != 'authentic' else None
669
+ neurosymbolic = None
670
+ if verdict != 'authentic' and run_neurosymbolic_assessment is not None:
671
+ try:
672
+ neurosymbolic = run_neurosymbolic_assessment(
673
+ np.array(image.convert('RGB')),
674
+ probability_ai,
675
+ source_analysis,
676
+ {
677
+ 'risk_score': ethical['riskScore'],
678
+ 'status': ethical['status'],
679
+ } if ethical else None,
680
+ )
681
+ except Exception:
682
+ neurosymbolic = None
683
  fft_branch_score = round(min(99.9, fft_summary['bands']['high'] / max(fft_summary['bands']['low'], 1e-6) * 40), 2)
684
  model_breakdown = [
685
  {'model': 'EfficientNet-B2 Spatial', 'score': round(probability_ai * 100, 2), 'weight': 0.55},
 
688
  if source_analysis:
689
  model_breakdown.append({'model': f"AI Source: {source_analysis['predictedSource'].upper()}", 'score': round(max(source_analysis['ganProbability'], source_analysis['diffusionProbability']) * 100, 2), 'weight': 0.20})
690
  metadata['heatmapPreview'] = image_to_base64(overlay_image)
691
+ return InferenceArtifacts(probability_ai, probability_authentic, verdict, round(confidence_score, 2), heatmap_regions, fft_summary, metadata, model_breakdown, artifacts, source_analysis, ethical, neurosymbolic)
692
 
693
 
694
  @app.get('/api/health')
 
774
  'metadata': artifacts.metadata,
775
  'sourceAnalysis': artifacts.source_analysis,
776
  'ethical': artifacts.ethical,
777
+ 'neurosymbolic': artifacts.neurosymbolic,
778
  'raw': {
779
  'probabilityAi': round(artifacts.probability_ai, 6),
780
  'probabilityAuthentic': round(artifacts.probability_authentic, 6),
backend/repro.png CHANGED

Git LFS Details

  • SHA256: e6bf2fe869ed8aedb05bccd0cb9fc94a4f94e65d1c835d5f96eada9cefc32820
  • Pointer size: 128 Bytes
  • Size of remote file: 759 Bytes
backend/repro2.png CHANGED

Git LFS Details

  • SHA256: 1acb4aa76d676724076127ffd6941dbc1815ad06ad4fa49d8f68fe391b3ee552
  • Pointer size: 129 Bytes
  • Size of remote file: 1.01 kB
backend/test_gray.png CHANGED

Git LFS Details

  • SHA256: 6936dbd6e73edb1920227014baacc47d132a3d583fc5fc7f7322635ec9b4f6c4
  • Pointer size: 128 Bytes
  • Size of remote file: 759 Bytes
backend/test_input.png CHANGED

Git LFS Details

  • SHA256: 6936dbd6e73edb1920227014baacc47d132a3d583fc5fc7f7322635ec9b4f6c4
  • Pointer size: 128 Bytes
  • Size of remote file: 759 Bytes
neurosymbolic.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Neurosymbolic reasoning layer for AI-generated image decisions.
3
+
4
+ This module complements neural predictions with transparent symbolic rules.
5
+ It does not replace the neural model; it explains and supports the decision.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import numpy as np
13
+
14
+ from detector import rgb_to_gray, extract_residual, fft_stats, lbp_entropy
15
+
16
+
17
+ def _clip01(value: float) -> float:
18
+ return max(0.0, min(1.0, float(value)))
19
+
20
+
21
+ def run_neurosymbolic_assessment(
22
+ img_arr: np.ndarray,
23
+ ai_confidence: float,
24
+ source_pred: Optional[Dict[str, Any]] = None,
25
+ ethical_assessment: Optional[Dict[str, Any]] = None,
26
+ ) -> Dict[str, Any]:
27
+ """Run symbolic rules over image statistics and model outputs."""
28
+ if img_arr.dtype != np.float32 and img_arr.dtype != np.float64:
29
+ img_arr = img_arr.astype(np.float32)
30
+
31
+ if img_arr.max() > 1.0:
32
+ img_arr = img_arr / 255.0
33
+
34
+ g = rgb_to_gray(img_arr)
35
+ residual = extract_residual(g)
36
+ residual_std = float(np.std(residual))
37
+ _, hf_ratio = fft_stats(g)
38
+ entropy = float(lbp_entropy((g * 255).astype(np.uint8)))
39
+
40
+ confidence = _clip01(ai_confidence)
41
+ score = 0.0
42
+ fired_rules: List[Dict[str, Any]] = []
43
+
44
+ def add_rule(rule_id: str, reason: str, weight: float) -> None:
45
+ nonlocal score
46
+ score += weight
47
+ fired_rules.append({"rule": rule_id, "reason": reason, "weight": float(weight)})
48
+
49
+ if confidence >= 0.90:
50
+ add_rule("R1_STRONG_MODEL_CONFIDENCE", "Neural classifier confidence >= 90%.", 0.30)
51
+ elif confidence >= 0.75:
52
+ add_rule("R1B_MEDIUM_MODEL_CONFIDENCE", "Neural classifier confidence >= 75%.", 0.20)
53
+
54
+ if hf_ratio >= 0.22:
55
+ add_rule("R2_HIGH_FREQUENCY_SPIKE", "FFT high-frequency ratio suggests synthetic artifacts.", 0.18)
56
+
57
+ if residual_std >= 0.09:
58
+ add_rule("R3_RESIDUAL_NOISE_PATTERN", "Residual noise variance is higher than expected for camera images.", 0.16)
59
+
60
+ if entropy <= 4.10 or entropy >= 6.40:
61
+ add_rule("R4_TEXTURE_REGULARITY_ANOMALY", "LBP entropy indicates atypical texture regularity.", 0.12)
62
+
63
+ if isinstance(source_pred, dict):
64
+ source_label = str(source_pred.get("label", "unknown")).lower()
65
+ top_prob = float(source_pred.get("top_prob", 0.0))
66
+ if source_label in {"gan", "diffusion"} and top_prob >= 0.70:
67
+ add_rule(
68
+ "R5_SOURCE_MODEL_AGREEMENT",
69
+ f"Source model supports {source_label.upper()} with {top_prob:.1%} confidence.",
70
+ 0.14,
71
+ )
72
+
73
+ if isinstance(ethical_assessment, dict):
74
+ risk_score = float(ethical_assessment.get("risk_score", 0.0))
75
+ if risk_score >= 0.70:
76
+ add_rule("R6_ETHICAL_RISK_ALIGNMENT", "Ethical risk score is elevated and consistent with AI suspicion.", 0.10)
77
+
78
+ symbolic_score = _clip01(score)
79
+ decision = "SYMBOLIC_SUPPORTS_AI" if symbolic_score >= 0.55 else "SYMBOLIC_UNCERTAIN"
80
+
81
+ return {
82
+ "decision": decision,
83
+ "symbolic_score": symbolic_score,
84
+ "features": {
85
+ "residual_std": residual_std,
86
+ "hf_ratio": float(hf_ratio),
87
+ "lbp_entropy": entropy,
88
+ "ai_confidence": confidence,
89
+ },
90
+ "fired_rules": fired_rules,
91
+ }
92
+
93
+
94
+ def format_neurosymbolic_report(result: Optional[Dict[str, Any]]) -> str:
95
+ """Format neurosymbolic output for UI textboxes."""
96
+ if not result:
97
+ return "Neurosymbolic analysis unavailable."
98
+
99
+ lines: List[str] = []
100
+ lines.append("Neurosymbolic Analysis")
101
+ lines.append(f"Decision: {result.get('decision', 'UNKNOWN')}")
102
+ lines.append(f"Symbolic score: {float(result.get('symbolic_score', 0.0)):.2%}")
103
+
104
+ features = result.get("features", {})
105
+ lines.append(
106
+ "Evidence features: "
107
+ f"residual_std={float(features.get('residual_std', 0.0)):.4f}, "
108
+ f"hf_ratio={float(features.get('hf_ratio', 0.0)):.4f}, "
109
+ f"lbp_entropy={float(features.get('lbp_entropy', 0.0)):.4f}"
110
+ )
111
+
112
+ fired_rules = result.get("fired_rules", [])
113
+ if fired_rules:
114
+ lines.append("Triggered rules:")
115
+ for item in fired_rules:
116
+ rule = str(item.get("rule", "RULE"))
117
+ reason = str(item.get("reason", ""))
118
+ weight = float(item.get("weight", 0.0))
119
+ lines.append(f"- {rule} (+{weight:.2f}): {reason}")
120
+ else:
121
+ lines.append("Triggered rules: none")
122
+
123
+ return "\n".join(lines)
src/.DS_Store CHANGED
Binary files a/src/.DS_Store and b/src/.DS_Store differ
 
src/assets/hero.png CHANGED

Git LFS Details

  • SHA256: 72a860570eddf1dd9988f26c7106c67be286bc9f2fd3303c465ce87edb1ae6cd
  • Pointer size: 130 Bytes
  • Size of remote file: 44.9 kB
src/components/AssessmentPanels.jsx ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrainCircuit, ShieldAlert, ShieldCheck, Scale, Sparkles } from 'lucide-react';
2
+ import styles from './AssessmentPanels.module.css';
3
+
4
+ function SectionCard({ title, icon: Icon, tone = 'neutral', subtitle, children }) {
5
+ return (
6
+ <section className={`${styles.card} ${styles[`tone_${tone}`]}`}>
7
+ <div className={styles.header}>
8
+ <div className={styles.titleWrap}>
9
+ <div className={styles.iconWrap}><Icon size={16} /></div>
10
+ <div>
11
+ <h3 className={styles.title}>{title}</h3>
12
+ {subtitle ? <p className={styles.subtitle}>{subtitle}</p> : null}
13
+ </div>
14
+ </div>
15
+ </div>
16
+ <div className={styles.body}>{children}</div>
17
+ </section>
18
+ );
19
+ }
20
+
21
+ function Row({ label, value, mono = false, strong = false }) {
22
+ return (
23
+ <div className={styles.row}>
24
+ <span className={styles.label}>{label}</span>
25
+ <span className={`${styles.value} ${mono ? styles.mono : ''} ${strong ? styles.strong : ''}`}>{value}</span>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ export default function AssessmentPanels({ result }) {
31
+ const ethical = result.ethical;
32
+ const neurosymbolic = result.neurosymbolic;
33
+ if (!ethical && !neurosymbolic) return null;
34
+
35
+ const ethicalTone = ethical ? (ethical.is_ethical ? 'positive' : 'critical') : 'neutral';
36
+ const nsTone = neurosymbolic?.decision === 'SYMBOLIC_SUPPORTS_AI' ? 'critical' : 'neutral';
37
+
38
+ return (
39
+ <div className={styles.grid}>
40
+ {neurosymbolic && (
41
+ <SectionCard
42
+ title="Neurosymbolic Analysis"
43
+ icon={BrainCircuit}
44
+ tone={nsTone}
45
+ subtitle="Symbolic rules layered over the model output for transparent forensic reasoning"
46
+ >
47
+ <Row label="Decision" value={neurosymbolic.decision?.replaceAll('_', ' ')} strong />
48
+ <Row label="Symbolic Score" value={`${((neurosymbolic.symbolic_score || 0) * 100).toFixed(1)}%`} />
49
+ {neurosymbolic.features && (
50
+ <>
51
+ <Row label="Residual Std" value={(neurosymbolic.features.residual_std || 0).toFixed(4)} mono />
52
+ <Row label="HF Ratio" value={(neurosymbolic.features.hf_ratio || 0).toFixed(4)} mono />
53
+ <Row label="LBP Entropy" value={(neurosymbolic.features.lbp_entropy || 0).toFixed(4)} mono />
54
+ </>
55
+ )}
56
+ <div className={styles.ruleBlock}>
57
+ <div className={styles.ruleTitle}><Sparkles size={14} />Triggered Rules</div>
58
+ {neurosymbolic.fired_rules?.length ? neurosymbolic.fired_rules.map((rule) => (
59
+ <div key={rule.rule} className={styles.ruleItem}>
60
+ <div className={styles.ruleHead}>
61
+ <span className={styles.ruleCode}>{rule.rule}</span>
62
+ <span className={styles.ruleWeight}>+{Number(rule.weight || 0).toFixed(2)}</span>
63
+ </div>
64
+ <p className={styles.ruleReason}>{rule.reason}</p>
65
+ </div>
66
+ )) : <p className={styles.empty}>No symbolic rules fired.</p>}
67
+ </div>
68
+ </SectionCard>
69
+ )}
70
+
71
+ {ethical && (
72
+ <SectionCard
73
+ title="Ethical Assessment"
74
+ icon={ethical.is_ethical ? ShieldCheck : ShieldAlert}
75
+ tone={ethicalTone}
76
+ subtitle="Risk-oriented interpretation layer for sensitive or harmful synthetic content"
77
+ >
78
+ <Row label="Status" value={ethical.status || 'UNKNOWN'} strong />
79
+ <Row label="Risk Score" value={`${((ethical.riskScore || 0) * 100).toFixed(1)}%`} />
80
+ <Row label="Simple Verdict" value={ethical.simpleStatus || 'Unavailable'} />
81
+ <div className={styles.flagBlock}>
82
+ <div className={styles.ruleTitle}><Scale size={14} />Flags</div>
83
+ <div className={styles.flagList}>
84
+ {ethical.flags?.length ? ethical.flags.map((flag) => (
85
+ <span key={flag} className={styles.flag}>{flag.replaceAll('_', ' ')}</span>
86
+ )) : <p className={styles.empty}>No ethical flags raised.</p>}
87
+ </div>
88
+ </div>
89
+ </SectionCard>
90
+ )}
91
+ </div>
92
+ );
93
+ }
src/components/AssessmentPanels.module.css ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .grid {
2
+ display: grid;
3
+ grid-template-columns: repeat(2, minmax(0, 1fr));
4
+ gap: 16px;
5
+ }
6
+
7
+ .card {
8
+ background: var(--bg-surface);
9
+ border: 1px solid var(--border);
10
+ border-radius: var(--radius-xl);
11
+ box-shadow: var(--shadow-sm);
12
+ padding: 18px;
13
+ }
14
+
15
+ .tone_positive {
16
+ border-color: rgba(0, 198, 122, 0.24);
17
+ box-shadow: 0 0 0 1px rgba(0, 198, 122, 0.08), var(--shadow-sm);
18
+ }
19
+
20
+ .tone_critical {
21
+ border-color: rgba(255, 71, 87, 0.24);
22
+ box-shadow: 0 0 0 1px rgba(255, 71, 87, 0.08), var(--shadow-sm);
23
+ }
24
+
25
+ .header {
26
+ margin-bottom: 14px;
27
+ }
28
+
29
+ .titleWrap {
30
+ display: flex;
31
+ align-items: flex-start;
32
+ gap: 12px;
33
+ }
34
+
35
+ .iconWrap {
36
+ width: 34px;
37
+ height: 34px;
38
+ border-radius: 12px;
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ background: var(--bg-surface-2);
43
+ color: var(--accent);
44
+ }
45
+
46
+ .title {
47
+ margin: 0;
48
+ color: var(--text-primary);
49
+ font-size: 16px;
50
+ font-weight: 700;
51
+ }
52
+
53
+ .subtitle {
54
+ margin: 4px 0 0;
55
+ color: var(--text-muted);
56
+ font-size: 12.5px;
57
+ line-height: 1.45;
58
+ }
59
+
60
+ .body {
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 10px;
64
+ }
65
+
66
+ .row {
67
+ display: flex;
68
+ justify-content: space-between;
69
+ gap: 12px;
70
+ padding: 10px 0;
71
+ border-bottom: 1px solid var(--border);
72
+ }
73
+
74
+ .label {
75
+ color: var(--text-secondary);
76
+ font-size: 12.5px;
77
+ font-weight: 600;
78
+ }
79
+
80
+ .value {
81
+ color: var(--text-primary);
82
+ font-size: 12.5px;
83
+ text-align: right;
84
+ }
85
+
86
+ .strong { font-weight: 700; }
87
+ .mono { font-family: var(--font-mono); }
88
+
89
+ .ruleBlock,
90
+ .flagBlock {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: 10px;
94
+ padding-top: 6px;
95
+ }
96
+
97
+ .ruleTitle {
98
+ display: inline-flex;
99
+ align-items: center;
100
+ gap: 8px;
101
+ color: var(--text-secondary);
102
+ font-size: 12px;
103
+ font-weight: 700;
104
+ text-transform: uppercase;
105
+ letter-spacing: 0.06em;
106
+ }
107
+
108
+ .ruleItem {
109
+ padding: 12px;
110
+ border-radius: var(--radius-md);
111
+ background: var(--bg-surface-2);
112
+ border: 1px solid var(--border);
113
+ }
114
+
115
+ .ruleHead {
116
+ display: flex;
117
+ justify-content: space-between;
118
+ gap: 12px;
119
+ margin-bottom: 6px;
120
+ }
121
+
122
+ .ruleCode {
123
+ color: var(--text-primary);
124
+ font-size: 11.5px;
125
+ font-weight: 700;
126
+ font-family: var(--font-mono);
127
+ }
128
+
129
+ .ruleWeight {
130
+ color: var(--accent);
131
+ font-size: 11.5px;
132
+ font-weight: 700;
133
+ }
134
+
135
+ .ruleReason {
136
+ margin: 0;
137
+ color: var(--text-secondary);
138
+ font-size: 12.5px;
139
+ line-height: 1.45;
140
+ }
141
+
142
+ .flagList {
143
+ display: flex;
144
+ flex-wrap: wrap;
145
+ gap: 8px;
146
+ }
147
+
148
+ .flag {
149
+ background: var(--bg-surface-2);
150
+ border: 1px solid var(--border);
151
+ border-radius: 999px;
152
+ padding: 6px 10px;
153
+ color: var(--text-primary);
154
+ font-size: 11.5px;
155
+ font-weight: 700;
156
+ text-transform: uppercase;
157
+ letter-spacing: 0.03em;
158
+ }
159
+
160
+ .empty {
161
+ margin: 0;
162
+ color: var(--text-muted);
163
+ font-size: 12.5px;
164
+ }
165
+
166
+ @media (max-width: 960px) {
167
+ .grid {
168
+ grid-template-columns: 1fr;
169
+ }
170
+ }
src/components/ForensicDashboard.jsx CHANGED
@@ -5,6 +5,7 @@ import MediaPanel from './MediaPanel';
5
  import VerdictCard from './VerdictCard';
6
  import TemporalTimeline from './TemporalTimeline';
7
  import TechnicalMetadata from './TechnicalMetadata';
 
8
  import { downloadReport } from '../utils/generateReport';
9
  import styles from './ForensicDashboard.module.css';
10
 
@@ -98,6 +99,16 @@ export default function ForensicDashboard({ result, previewUrl, onReset }) {
98
  </motion.div>
99
  )}
100
 
 
 
 
 
 
 
 
 
 
 
101
  <motion.div
102
  initial={{ opacity: 0, y: 16 }}
103
  animate={{ opacity: 1, y: 0 }}
 
5
  import VerdictCard from './VerdictCard';
6
  import TemporalTimeline from './TemporalTimeline';
7
  import TechnicalMetadata from './TechnicalMetadata';
8
+ import AssessmentPanels from './AssessmentPanels';
9
  import { downloadReport } from '../utils/generateReport';
10
  import styles from './ForensicDashboard.module.css';
11
 
 
99
  </motion.div>
100
  )}
101
 
102
+ {(result.neurosymbolic || result.ethical) && (
103
+ <motion.div
104
+ initial={{ opacity: 0, y: 16 }}
105
+ animate={{ opacity: 1, y: 0 }}
106
+ transition={{ delay: 0.35 }}
107
+ >
108
+ <AssessmentPanels result={result} />
109
+ </motion.div>
110
+ )}
111
+
112
  <motion.div
113
  initial={{ opacity: 0, y: 16 }}
114
  animate={{ opacity: 1, y: 0 }}
video_bundle/Sample Images and Videos/Images/IMG-20250106-WA0012.jpg CHANGED

Git LFS Details

  • SHA256: 61ebe42c01eb7b7d2d9e36960fba339aad4d5b2586863a2127bf68aecdbcab04
  • Pointer size: 130 Bytes
  • Size of remote file: 68.5 kB
video_bundle/Sample Images and Videos/Images/KYRA-AI.webp CHANGED

Git LFS Details

  • SHA256: d3eff30078fef3e7a290784cb2a3e4ba0a463b77b8b1c7117418303cfa7fc6e3
  • Pointer size: 130 Bytes
  • Size of remote file: 10.5 kB
video_bundle/Sample Images and Videos/Images/Painting.webp CHANGED

Git LFS Details

  • SHA256: b8ac9c0f3c33ec7a03906bf01ade364ffb35526549a59bfa56f480504d51bcb2
  • Pointer size: 130 Bytes
  • Size of remote file: 74.7 kB
video_bundle/Sample Images and Videos/Images/Woman-1.jpg CHANGED

Git LFS Details

  • SHA256: eada2316e14bc4a4c924ed0f3c6d61920324b377e37517213201dfb473999e23
  • Pointer size: 130 Bytes
  • Size of remote file: 54.8 kB