#!/usr/bin/env python3
"""
Batch match adaptations from a folder and generate HTML report.
"""
import sys
import json
from pathlib import Path
from datetime import datetime
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
# Add src to path
sys.path.insert(0, str(Path(__file__).parent / "src"))
from video_matcher.matcher import VideoMatcher
console = Console()
# Common video file extensions
VIDEO_EXTENSIONS = {'.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
def generate_html_report(results, output_path, folder_path):
"""Generate an HTML report from matching results."""
html_content = f"""
Video Matching Report - {datetime.now().strftime('%Y-%m-%d %H:%M')}
{len(results)}
Adaptations Processed
{sum(1 for r in results if r['matches'])}
Matched
{sum(1 for r in results if not r['matches'])}
No Matches
{sum(len(r['matches']) for r in results)}
Total Master Matches
{sum(1 for r in results for m in r.get('matches', []) if m.get('matching_method') == 'akaze')}
AKAZE Matches
{sum(1 for r in results for m in r.get('matches', []) if m.get('matching_method') == 'ai_vision')}
AI Vision Matches
"""
# Add each adaptation result
for result in results:
adaptation_name = result['adaptation_name']
matches = result['matches']
error = result.get('error')
match_class = 'no-matches' if not matches else ''
match_count = len(matches) if matches else 0
html_content += f"""
"""
if error:
html_content += f"""
Error: {error}
"""
elif not matches:
html_content += """
No matching masters found above threshold
"""
else:
html_content += """
"""
for idx, match in enumerate(matches, 1):
confidence = match['confidence'].lower().replace(' ', '-')
html_content += f"""
Duration
{match['master_duration']:.0f}s
Video Match
{match['video_percentage']:.1f}%
Frames
{match['matching_frames']}/{match['total_frames']}
Combined Score
{match['combined_score']:.1%}
Method
{match.get('matching_method', 'hash').upper().replace('_', ' ')}
"""
html_content += """
"""
html_content += """
"""
html_content += """
"""
# Write HTML file
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_content)
def batch_match_folder(folder_path, threshold=0.80, frame_threshold=0.80, min_avg_similarity=0.90, output_file=None):
"""
Match all videos in a folder against masters and generate report.
Args:
folder_path: Path to folder containing adaptation videos
threshold: Minimum percentage match threshold
frame_threshold: Frame similarity threshold
min_avg_similarity: Minimum average similarity of matched frames
output_file: Output HTML file path (default: auto-generated)
"""
folder_path = Path(folder_path)
if not folder_path.exists():
console.print(f"[red]✗[/red] Folder not found: {folder_path}")
return
if not folder_path.is_dir():
console.print(f"[red]✗[/red] Not a directory: {folder_path}")
return
# Find all video files
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]")
return
console.print(f"\n[bold]Found {len(video_files)} video file(s) to process[/bold]\n")
# Initialize matcher
matcher = VideoMatcher()
# 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.")
return
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),
threshold=threshold,
frame_threshold=frame_threshold,
min_avg_similarity=min_avg_similarity
)
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_{timestamp}.html"
output_path = Path(output_file)
# Generate HTML report
console.print(f"\n[cyan]Generating HTML report...[/cyan]")
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]")
if __name__ == '__main__':
import click
@click.command()
@click.argument('folder_path', type=click.Path(exists=True))
@click.option('--threshold', '-t', default=0.80, type=float, help='Minimum percentage match (0-1)')
@click.option('--frame-threshold', '-f', default=0.80, type=float, help='Frame similarity threshold (0-1)')
@click.option('--min-avg-similarity', '-m', default=0.90, type=float, help='Minimum average similarity of matched frames (0-1)')
@click.option('--output', '-o', default=None, help='Output HTML file path')
def main(folder_path, threshold, frame_threshold, min_avg_similarity, output):
"""Batch match all videos in a folder and generate HTML report."""
batch_match_folder(folder_path, threshold, frame_threshold, min_avg_similarity, output)
main()