From 47136f22233d81ae84f313feded81ad85dffe15e Mon Sep 17 00:00:00 2001 From: Abdelkouddous LHACHIMI Date: Mon, 16 Mar 2026 17:02:41 +0000 Subject: [PATCH] Adding Continuous mode --- .claude/settings.local.json | 7 ++ check_video_authenticity.py | 200 +++++++++++++++++++++++++++++------- 2 files changed, 172 insertions(+), 35 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9f73838 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:sightengine.com)" + ] + } +} diff --git a/check_video_authenticity.py b/check_video_authenticity.py index 1c1f1e0..ae9ee79 100644 --- a/check_video_authenticity.py +++ b/check_video_authenticity.py @@ -1,11 +1,11 @@ import os import sys +import time import requests -import json import tkinter as tk from tkinter import filedialog from rich.console import Console -from rich.progress import Progress, SpinnerColumn, TextColumn +from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn from rich.panel import Panel from rich.table import Table from rich import print @@ -15,7 +15,13 @@ from rich import print API_USER = "1020608869" API_SECRET = "M7WF4mAXwSs2gs869LC9PecvYCBQyBSz" -API_ENDPOINT = "https://api.sightengine.com/1.0/video/check-sync.json" +SYNC_ENDPOINT = "https://api.sightengine.com/1.0/video/check-sync.json" +ASYNC_ENDPOINT = "https://api.sightengine.com/1.0/video/check.json" +UPLOAD_ENDPOINT = "https://api.sightengine.com/1.0/upload/create-video.json" + +SIZE_THRESHOLD = 50 * 1024 * 1024 # 50 MB +POLL_INTERVAL = 5 # seconds between polls +POLL_TIMEOUT = 600 # max seconds to wait for async result console = Console() @@ -23,32 +29,150 @@ def select_video_file(): """Opens a file dialog to select a video file using Tkinter.""" root = tk.Tk() root.withdraw() # Hide the main window - + file_path = filedialog.askopenfilename( title="Select a video file to analyze", filetypes=[ - ("Video Files", "*.mp4 *.mov *.avi *.mkv *.webm *.flv *.wmv"), + ("Video Files", "*.mp4 *.mov *.avi *.mkv *.webm *.flv *.wmv"), ("All Files", "*.*") ] ) return file_path -def check_video(file_path): - """Checks the video using Sightengine API.""" - if not os.path.exists(file_path): - console.print(f"[bold red]Error:[/bold red] File not found: {file_path}") - return +def upload_video(file_path): + """Uploads a large video via the Sightengine Upload API. Returns the media_id.""" + # Step 1: create a signed upload URL + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + transient=True + ) as progress: + progress.add_task(description="Requesting upload URL...", total=None) + resp = requests.get(UPLOAD_ENDPOINT, params={ + 'api_user': API_USER, + 'api_secret': API_SECRET + }) + data = resp.json() + if data.get('status') != 'success': + raise RuntimeError(f"Upload URL error: {data.get('error', {}).get('message', 'Unknown')}") + + media_id = data['media']['id'] + upload_url = data['media']['upload_url'] + + # Step 2: PUT the file to the signed URL + file_size = os.path.getsize(file_path) + console.print(f"[blue]Uploading[/blue] {os.path.basename(file_path)} ({file_size / 1024 / 1024:.1f} MB)...") + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + TimeElapsedColumn(), + transient=True + ) as progress: + progress.add_task(description="Uploading video to Sightengine...", total=None) + with open(file_path, 'rb') as f: + put_resp = requests.put(upload_url, data=f, headers={'Content-Type': 'video/mp4'}) + + if put_resp.status_code not in (200, 204): + raise RuntimeError(f"Upload failed: HTTP {put_resp.status_code}") + + console.print(f"[green]Upload complete.[/green] Media ID: {media_id}") + return media_id + +def check_video_async(file_path): + """Submits a video for async analysis and polls until results are ready.""" + file_size = os.path.getsize(file_path) params = { 'models': 'genai', 'api_user': API_USER, 'api_secret': API_SECRET } - files = { - 'media': open(file_path, 'rb') + # Submit the job + if file_size >= SIZE_THRESHOLD: + media_id = upload_video(file_path) + console.print("[blue]Submitting async analysis job...[/blue]") + with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True) as progress: + progress.add_task(description="Submitting job...", total=None) + params['media_id'] = media_id + resp = requests.post(ASYNC_ENDPOINT, data=params) + else: + console.print("[blue]Submitting async analysis job (file upload)...[/blue]") + with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True) as progress: + progress.add_task(description="Uploading and submitting job...", total=None) + with open(file_path, 'rb') as f: + resp = requests.post(ASYNC_ENDPOINT, files={'media': f}, data=params) + + resp_data = resp.json() + if resp_data.get('status') not in ('success', 'submitted'): + error_msg = resp_data.get('error', {}).get('message', 'Unknown error') + raise RuntimeError(f"Async submission error: {error_msg}") + + request_id = resp_data.get('request', {}).get('id') or resp_data.get('media', {}).get('id') + if not request_id: + raise RuntimeError(f"No request ID in response: {resp_data}") + + console.print(f"[blue]Job submitted.[/blue] Request ID: {request_id}") + + # Poll for results + console.print(f"[blue]Waiting for results[/blue] (polling every {POLL_INTERVAL}s, timeout {POLL_TIMEOUT}s)...") + start = time.time() + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + TimeElapsedColumn(), + transient=True + ) as progress: + progress.add_task(description="Analyzing video...", total=None) + + while time.time() - start < POLL_TIMEOUT: + time.sleep(POLL_INTERVAL) + poll_resp = requests.get(ASYNC_ENDPOINT, params={ + 'request_id': request_id, + 'models': 'genai', + 'api_user': API_USER, + 'api_secret': API_SECRET + }) + poll_data = poll_resp.json() + status = poll_data.get('status') + + if status == 'finished': + return poll_data + elif status in ('error', 'failure'): + error_msg = poll_data.get('error', {}).get('message', 'Unknown error') + raise RuntimeError(f"Async analysis error: {error_msg}") + # status == 'processing' or similar — keep polling + + raise RuntimeError(f"Timed out waiting for async results after {POLL_TIMEOUT}s") + +def check_video(file_path): + """Checks the video using Sightengine API (sync, with async fallback for long videos).""" + if not os.path.exists(file_path): + console.print(f"[bold red]Error:[/bold red] File not found: {file_path}") + return + + file_size = os.path.getsize(file_path) + + # Large files go directly to async mode + if file_size >= SIZE_THRESHOLD: + console.print(f"[yellow]Large file ({file_size / 1024 / 1024:.1f} MB) — using async upload mode.[/yellow]") + try: + response_data = check_video_async(file_path) + display_results(file_path, response_data) + except Exception as e: + console.print(f"[bold red]System Error:[/bold red] {e}") + return + + # Small files: try sync first + params = { + 'models': 'genai', + 'api_user': API_USER, + 'api_secret': API_SECRET } + media_file = open(file_path, 'rb') try: with Progress( SpinnerColumn(), @@ -56,25 +180,37 @@ def check_video(file_path): transient=True ) as progress: progress.add_task(description="Uploading and analyzing video...", total=None) - response = requests.post(API_ENDPOINT, files=files, data=params) - - response_data = json.loads(response.text) - - if response_data['status'] == 'success': + response = requests.post(SYNC_ENDPOINT, files={'media': media_file}, data=params) + + response_data = response.json() + + if response_data.get('status') == 'success': display_results(file_path, response_data) + return + + error_msg = response_data.get('error', {}).get('message', 'Unknown error') + + # Fallback: video too long for sync mode + if 'too long' in error_msg.lower() or 'continuous moderation' in error_msg.lower(): + console.print(f"[yellow]Video too long for sync mode — switching to async mode...[/yellow]") + media_file.close() + try: + response_data = check_video_async(file_path) + display_results(file_path, response_data) + except Exception as e: + console.print(f"[bold red]System Error:[/bold red] {e}") else: - error_msg = response_data.get('error', {}).get('message', 'Unknown error') console.print(f"[bold red]API Error:[/bold red] {error_msg}") except Exception as e: console.print(f"[bold red]System Error:[/bold red] {e}") finally: - files['media'].close() + if not media_file.closed: + media_file.close() def display_results(file_path, data): """Displays the analysis results using Rich.""" - - # Calculate aggregate score (average of all frames for simplicity in this demo) + frames = data.get('data', {}).get('frames', []) if not frames: console.print("[yellow]No frames analyzed.[/yellow]") @@ -82,8 +218,7 @@ def display_results(file_path, data): total_ai_score = 0 count = 0 - - # Create detailed table for frames (showing first few or high scores) + table = Table(title="Frame Analysis (Sample)") table.add_column("Time (s)", justify="right", style="cyan", no_wrap=True) table.add_column("AI Probability", justify="right", style="magenta") @@ -97,19 +232,14 @@ def display_results(file_path, data): count += 1 if ai_score > max_ai_score: max_ai_score = ai_score - - # Add to table if it's a significant score or just to show some data - # Showing only frames with > 0.1 score or every 10th frame to keep it clean if many frames + if ai_score > 0.5 or count % 10 == 1: verdict = "[red]FAKE[/red]" if ai_score > 0.8 else ("[yellow]SUSPICIOUS[/yellow]" if ai_score > 0.4 else "[green]REAL[/green]") - # Assuming 1 frame per second roughly? API documentation says 'position' is likely offset position = frame.get('info', {}).get('position', 0) - # Provide string formatting for score table.add_row(str(position), f"{ai_score:.2%}", verdict) avg_ai_score = total_ai_score / count if count else 0 - - # Final Verdict Panel + final_verdict = "UNCERTAIN" color = "yellow" if max_ai_score > 0.85: @@ -118,22 +248,22 @@ def display_results(file_path, data): elif max_ai_score < 0.2: final_verdict = "AUTHENTIC" color = "green" - + request_info = data.get('request', {}) req_id = request_info.get('id', 'N/A') ops = request_info.get('operations', 0) - + panel_content = f""" [bold]File:[/bold] {os.path.basename(file_path)} [bold]Request ID:[/bold] {req_id} [bold]Operations Cost:[/bold] {ops} - + [bold]Max AI Score:[/bold] {max_ai_score:.2%} [bold]Average AI Score:[/bold] {avg_ai_score:.2%} - + [bold size=20 style={color}]VERDICT: {final_verdict}[/bold size=20 style={color}] """ - + console.print(Panel(panel_content, title="Analysis Result", expand=False)) console.print(table)