Major Features: - 🖥️ Standalone desktop app (VideoMatcher.app) - double-click to run - 🎨 Black & gold branded UI (Montserrat font, #FFC407 accent) - 📁 Local file browser for master/adaptation folders - ⚡ Fast mode processing (10-20x faster, disables AKAZE/AI Vision) - 🤖 Smart AI Vision fallback (auto-retry when no matches found) - 📊 Real-time progress bars (fingerprinting & matching) - 💾 Local processing (no cloud, no authentication) - 📤 CSV export with master filenames Web Application (Enterprise): - 🌐 Flask web app with Azure AD authentication - 📦 Box.com integration for cloud storage - 🐳 Docker support for deployment - 🔐 JWT validation with httpOnly cookies - 🎯 REST API endpoints Enhancements: - Fixed master filename lookup (was showing "Unknown") - Automatic fingerprint recovery (detects missing files) - Improved CSV format (master file next to adaptation) - Port conflict handling (auto-finds available port) - Environment variable fixes for standalone mode Documentation: - Updated README with standalone app section - Added 10+ guide documents (UI improvements, fingerprint recovery, etc.) - Build instructions with PyInstaller - Comprehensive troubleshooting guide Technical: - PyInstaller build configuration (video_matcher.spec) - Launcher with environment setup (launcher.py) - Mock authentication for standalone mode - Video matcher service layer - Metadata parser and AKAZE video matching 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
118 lines
3.9 KiB
Python
Executable file
118 lines
3.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Fast batch matching without AKAZE - uses original perceptual hash only
|
|
"""
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
|
|
|
from video_matcher.matcher import VideoMatcher
|
|
from rich.console import Console
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
from datetime import datetime
|
|
|
|
console = Console()
|
|
|
|
if len(sys.argv) < 2:
|
|
console.print("[red]Usage: python batch_match_fast.py <folder_path> [output.html][/red]")
|
|
sys.exit(1)
|
|
|
|
folder_path = Path(sys.argv[1])
|
|
output_file = sys.argv[2] if len(sys.argv) > 2 else None
|
|
|
|
if not folder_path.exists():
|
|
console.print(f"[red]Folder not found: {folder_path}[/red]")
|
|
sys.exit(1)
|
|
|
|
# Find all video files
|
|
VIDEO_EXTENSIONS = {'.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
|
|
video_files = []
|
|
for ext in VIDEO_EXTENSIONS:
|
|
video_files.extend(folder_path.glob(f"*{ext}"))
|
|
video_files.extend(folder_path.glob(f"*{ext.upper()}"))
|
|
|
|
if not video_files:
|
|
console.print(f"[yellow]No video files found in {folder_path}[/yellow]")
|
|
sys.exit(1)
|
|
|
|
console.print(f"\n[bold]Found {len(video_files)} video file(s) to process[/bold]\n")
|
|
|
|
# Initialize matcher WITHOUT AKAZE (faster)
|
|
console.print("[cyan]Using fast mode (perceptual hash only)[/cyan]")
|
|
matcher = VideoMatcher(
|
|
use_akaze=False, # Disable AKAZE
|
|
use_metadata_filter=True, # Keep metadata filtering
|
|
enable_ai_vision=True # Keep AI Vision
|
|
)
|
|
|
|
# Check if we have masters
|
|
masters = matcher.list_masters()
|
|
if not masters:
|
|
console.print("[red]✗[/red] No master videos found in library.")
|
|
console.print("Use 'python cli.py add-master' to add masters first.")
|
|
sys.exit(1)
|
|
|
|
console.print(f"[cyan]Comparing against {len(masters)} master(s)...[/cyan]\n")
|
|
|
|
# Process each video
|
|
results = []
|
|
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(),
|
|
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
console=console
|
|
) as progress:
|
|
|
|
task = progress.add_task("[cyan]Processing adaptations...", total=len(video_files))
|
|
|
|
for video_file in video_files:
|
|
progress.update(task, description=f"[cyan]Processing {video_file.name}...")
|
|
|
|
try:
|
|
matches = matcher.match_adaptation(str(video_file))
|
|
|
|
results.append({
|
|
'adaptation_name': video_file.name,
|
|
'adaptation_path': str(video_file),
|
|
'matches': matches,
|
|
'error': None
|
|
})
|
|
|
|
except Exception as e:
|
|
console.print(f"[red]✗[/red] Error processing {video_file.name}: {e}")
|
|
results.append({
|
|
'adaptation_name': video_file.name,
|
|
'adaptation_path': str(video_file),
|
|
'matches': [],
|
|
'error': str(e)
|
|
})
|
|
|
|
progress.advance(task)
|
|
|
|
# Generate output filename if not specified
|
|
if output_file is None:
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
output_file = f"matching_report_fast_{timestamp}.html"
|
|
|
|
output_path = Path(output_file)
|
|
|
|
# Generate HTML report
|
|
console.print(f"\n[cyan]Generating HTML report...[/cyan]")
|
|
|
|
# Import the generation function from batch_match
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
from batch_match import generate_html_report
|
|
|
|
generate_html_report(results, output_path, str(folder_path))
|
|
|
|
# Summary
|
|
console.print(f"\n[bold green]✓ Report generated successfully![/bold green]")
|
|
console.print(f"\n[bold]Summary:[/bold]")
|
|
console.print(f" Total adaptations: {len(results)}")
|
|
console.print(f" Matched: {sum(1 for r in results if r['matches'])}")
|
|
console.print(f" No matches: {sum(1 for r in results if not r['matches'])}")
|
|
console.print(f" Total master matches: {sum(len(r['matches']) for r in results)}")
|
|
console.print(f"\n[bold cyan]📄 Report saved to:[/bold cyan] {output_path.absolute()}")
|
|
console.print(f"\n[dim]Open in browser: file://{output_path.absolute()}[/dim]")
|