CoolFace
Apppublic

AI-Talent-Force/dev_caio

sourceHugging Facemitupdated 9mo agoView on Hugging Face
0likes
app.py1002 linesDownload Raw Back to root
1"""2ShortSmith v2 - Gradio Application3 4Hugging Face Space interface for video highlight extraction.5Features:6- Multi-modal analysis (visual + audio + motion)7- Domain-optimized presets8- Person-specific filtering (optional)9- Scene-aware clip cutting10- Batch testing with parameter variations11"""12 13import os14import sys15import tempfile16import shutil17import json18import zipfile19from pathlib import Path20import time21import traceback22from typing import List, Dict, Any, Optional23 24import gradio as gr25import pandas as pd26 27# Add project root to path28sys.path.insert(0, str(Path(__file__).parent))29 30# Initialize logging31try:32    from utils.logger import setup_logging, get_logger33    setup_logging(log_level="INFO", log_to_console=True)34    logger = get_logger("app")35except Exception:36    import logging37    logging.basicConfig(level=logging.INFO)38    logger = logging.getLogger("app")39 40 41# =============================================================================42# Shared Utilities43# =============================================================================44 45def build_metrics_output(result, domain: str, custom_prompt: Optional[str] = None) -> str:46    """47    Build formatted metrics output for testing and evaluation.48 49    Args:50        result: PipelineResult object51        domain: Content domain used for processing52        custom_prompt: Custom prompt used (if any)53 54    Returns:55        Formatted string with all metrics56    """57    lines = []58    lines.append("=" * 50)59    lines.append("AUTOMATED METRICS (System-Generated)")60    lines.append("=" * 50)61    lines.append("")62 63    # Processing Metrics64    lines.append("PROCESSING METRICS")65    lines.append("-" * 30)66    lines.append(f"processing_time_seconds: {result.processing_time:.2f}")67    lines.append(f"frames_analyzed: {len(result.visual_features)}")68    lines.append(f"scenes_detected: {len(result.scenes)}")69    lines.append(f"audio_segments_analyzed: {len(result.audio_features)}")70    lines.append(f"domain: {domain}")71    lines.append(f"custom_prompt: {custom_prompt if custom_prompt else 'none'}")72 73    # Count hooks from scores (estimate based on high-scoring segments)74    hooks_detected = sum(1 for s in result.scores if s.combined_score > 0.7) if result.scores else 075    lines.append(f"hooks_detected: {hooks_detected}")76 77    if result.metadata:78        lines.append(f"video_duration_seconds: {result.metadata.duration:.2f}")79        lines.append(f"video_resolution: {result.metadata.resolution}")80        lines.append(f"video_fps: {result.metadata.fps:.2f}")81 82    lines.append("")83 84    # Per Clip Metrics85    lines.append("PER CLIP METRICS")86    lines.append("-" * 30)87 88    for i, clip in enumerate(result.clips):89        lines.append("")90        lines.append(f"[Clip {i + 1}]")91        lines.append(f"  clip_id: {i + 1}")92        lines.append(f"  start_time: {clip.start_time:.2f}")93        lines.append(f"  end_time: {clip.end_time:.2f}")94        lines.append(f"  duration: {clip.duration:.2f}")95        lines.append(f"  hype_score: {clip.hype_score:.4f}")96        lines.append(f"  visual_score: {clip.visual_score:.4f}")97        lines.append(f"  audio_score: {clip.audio_score:.4f}")98        lines.append(f"  motion_score: {clip.motion_score:.4f}")99 100        # Hook info - derive from segment scores if available101        hook_type = "none"102        hook_confidence = 0.0103 104        # Find matching segment score for this clip105        for score in result.scores:106            if abs(score.start_time - clip.start_time) < 1.0:107                if score.combined_score > 0.7:108                    hook_confidence = score.combined_score109                    # Infer hook type based on dominant score110                    if score.audio_score > score.visual_score and score.audio_score > score.motion_score:111                        hook_type = "audio_peak"112                    elif score.motion_score > score.visual_score:113                        hook_type = "motion_spike"114                    else:115                        hook_type = "visual_highlight"116                break117 118        lines.append(f"  hook_type: {hook_type}")119        lines.append(f"  hook_confidence: {hook_confidence:.4f}")120 121        if clip.person_detected:122            lines.append(f"  person_detected: True")123            lines.append(f"  person_screen_time: {clip.person_screen_time:.4f}")124 125    lines.append("")126    lines.append("=" * 50)127    lines.append("END METRICS")128    lines.append("=" * 50)129 130    return "\n".join(lines)131 132 133# =============================================================================134# Single Video Processing135# =============================================================================136 137def process_video(138    video_file,139    domain,140    num_clips,141    clip_duration,142    reference_image,143    custom_prompt,144    progress=gr.Progress()145):146    """147    Main video processing function for single video mode.148 149    Args:150        video_file: Uploaded video file path151        domain: Content domain for scoring weights152        num_clips: Number of clips to extract153        clip_duration: Duration of each clip in seconds154        reference_image: Optional reference image for person filtering155        custom_prompt: Optional custom instructions156        progress: Gradio progress tracker157 158    Returns:159        Tuple of (status_message, clip1, clip2, clip3, log_text, metrics_text)160    """161    if video_file is None:162        return "Please upload a video first.", None, None, None, "", ""163 164    log_messages = []165 166    def log(msg):167        log_messages.append(f"[{time.strftime('%H:%M:%S')}] {msg}")168        logger.info(msg)169 170    try:171        video_path = Path(video_file)172        log(f"Processing video: {video_path.name}")173        progress(0.05, desc="Validating video...")174 175        # Import pipeline components176        from utils.helpers import validate_video_file, validate_image_file, format_duration177        from pipeline.orchestrator import PipelineOrchestrator178 179        # Validate video180        validation = validate_video_file(video_file)181        if not validation.is_valid:182            return f"Error: {validation.error_message}", None, None, None, "\n".join(log_messages), ""183 184        log(f"Video size: {validation.file_size / (1024*1024):.1f} MB")185 186        # Validate reference image if provided187        ref_path = None188        if reference_image is not None:189            ref_validation = validate_image_file(reference_image)190            if ref_validation.is_valid:191                ref_path = reference_image192                log(f"Reference image: {Path(reference_image).name}")193            else:194                log(f"Warning: Invalid reference image - {ref_validation.error_message}")195 196        # Map domain string to internal value197        domain_map = {198            "Sports": "sports",199            "Vlogs": "vlogs",200            "Music Videos": "music",201            "Podcasts": "podcasts",202            "Gaming": "gaming",203            "General": "general",204        }205        domain_value = domain_map.get(domain, "general")206        log(f"Domain: {domain_value}")207 208        # Create output directory209        output_dir = Path(tempfile.mkdtemp(prefix="shortsmith_output_"))210        log(f"Output directory: {output_dir}")211 212        # Progress callback to update UI during processing213        def on_progress(pipeline_progress):214            stage = pipeline_progress.stage.value215            pct = pipeline_progress.progress216            msg = pipeline_progress.message217            log(f"[{stage}] {msg}")218            # Map pipeline progress (0-1) to our range (0.1-0.9)219            mapped_progress = 0.1 + (pct * 0.8)220            progress(mapped_progress, desc=f"{stage}: {msg}")221 222        # Initialize pipeline223        progress(0.1, desc="Initializing AI models...")224        log("Initializing pipeline...")225        pipeline = PipelineOrchestrator(progress_callback=on_progress)226 227        # Process video228        progress(0.15, desc="Starting analysis...")229        log(f"Processing: {int(num_clips)} clips @ {int(clip_duration)}s each")230 231        result = pipeline.process(232            video_path=video_path,233            num_clips=int(num_clips),234            clip_duration=float(clip_duration),235            domain=domain_value,236            reference_image=ref_path,237            custom_prompt=custom_prompt.strip() if custom_prompt else None,238        )239 240        progress(0.9, desc="Extracting clips...")241 242        # Handle result243        if result.success:244            log(f"Processing complete in {result.processing_time:.1f}s")245 246            clip_paths = []247            for i, clip in enumerate(result.clips):248                if clip.clip_path.exists():249                    output_path = output_dir / f"highlight_{i+1}.mp4"250                    shutil.copy2(clip.clip_path, output_path)251                    clip_paths.append(str(output_path))252                    log(f"Clip {i+1}: {format_duration(clip.start_time)} - {format_duration(clip.end_time)} (score: {clip.hype_score:.2f})")253 254            status = f"Successfully extracted {len(clip_paths)} highlight clips!\nProcessing time: {result.processing_time:.1f}s"255 256            # Build metrics output257            metrics_output = build_metrics_output(result, domain_value, custom_prompt.strip() if custom_prompt else None)258 259            pipeline.cleanup()260            progress(1.0, desc="Done!")261 262            # Return up to 3 clips263            clip1 = clip_paths[0] if len(clip_paths) > 0 else None264            clip2 = clip_paths[1] if len(clip_paths) > 1 else None265            clip3 = clip_paths[2] if len(clip_paths) > 2 else None266 267            return status, clip1, clip2, clip3, "\n".join(log_messages), metrics_output268        else:269            log(f"Processing failed: {result.error_message}")270            pipeline.cleanup()271            return f"Error: {result.error_message}", None, None, None, "\n".join(log_messages), ""272 273    except Exception as e:274        error_msg = f"Unexpected error: {str(e)}"275        log(error_msg)276        log(traceback.format_exc())277        logger.exception("Pipeline error")278        return error_msg, None, None, None, "\n".join(log_messages), ""279 280 281# =============================================================================282# Batch Testing Functions283# =============================================================================284 285def generate_test_queue(286    videos: List[str],287    domains: List[str],288    durations: List[int],289    num_clips: int,290    ref_image: Optional[str],291    prompts: List[str],292    include_no_prompt: bool293) -> List[Dict[str, Any]]:294    """Generate all parameter combinations to test (cartesian product)."""295    # Build prompt list296    prompt_list = []297    if include_no_prompt:298        prompt_list.append(None)  # No prompt baseline299    prompt_list.extend([p.strip() for p in prompts if p and p.strip()])300 301    # If no prompts at all, use just None302    if not prompt_list:303        prompt_list = [None]304 305    # Map domain display names to internal values306    domain_map = {307        "Sports": "sports",308        "Vlogs": "vlogs",309        "Music Videos": "music",310        "Podcasts": "podcasts",311        "Gaming": "gaming",312        "General": "general",313    }314 315    queue = []316    test_id = 1317    for video in videos:318        video_name = Path(video).name if video else "unknown"319        for domain in domains:320            domain_value = domain_map.get(domain, "general")321            for duration in durations:322                for prompt in prompt_list:323                    queue.append({324                        "test_id": test_id,325                        "video_path": video,326                        "video_name": video_name,327                        "domain": domain,328                        "domain_value": domain_value,329                        "clip_duration": duration,330                        "num_clips": num_clips,331                        "reference_image": ref_image,332                        "custom_prompt": prompt,333                    })334                    test_id += 1335    return queue336 337 338def run_single_batch_test(config: Dict[str, Any], output_base_dir: Path) -> Dict[str, Any]:339    """Run a single test from the batch queue."""340    from utils.helpers import validate_video_file341    from pipeline.orchestrator import PipelineOrchestrator342 343    test_id = config["test_id"]344    video_path = config["video_path"]345    video_name = config["video_name"]346    domain_value = config["domain_value"]347    duration = config["clip_duration"]348    num_clips = config["num_clips"]349    ref_image = config["reference_image"]350    custom_prompt = config["custom_prompt"]351 352    # Create unique output folder for this test353    prompt_suffix = "no_prompt" if not custom_prompt else f"prompt_{hash(custom_prompt) % 1000}"354    test_folder = f"{Path(video_name).stem}_{domain_value}_{duration}s_{prompt_suffix}"355    output_dir = output_base_dir / test_folder356    output_dir.mkdir(parents=True, exist_ok=True)357 358    result_data = {359        "test_id": test_id,360        "video_name": video_name,361        "domain": domain_value,362        "clip_duration": duration,363        "custom_prompt": custom_prompt if custom_prompt else "none",364        "num_clips": num_clips,365        "status": "failed",366        "error": None,367        "processing_time": 0,368        "frames_analyzed": 0,369        "scenes_detected": 0,370        "hooks_detected": 0,371        "clips": [],372        "clip_paths": [],373    }374 375    try:376        # Validate video377        validation = validate_video_file(video_path)378        if not validation.is_valid:379            result_data["error"] = validation.error_message380            return result_data381 382        # Initialize and run pipeline383        pipeline = PipelineOrchestrator()384        result = pipeline.process(385            video_path=video_path,386            num_clips=num_clips,387            clip_duration=float(duration),388            domain=domain_value,389            reference_image=ref_image,390            custom_prompt=custom_prompt,391        )392 393        if result.success:394            result_data["status"] = "success"395            result_data["processing_time"] = round(result.processing_time, 2)396            result_data["frames_analyzed"] = len(result.visual_features)397            result_data["scenes_detected"] = len(result.scenes)398            result_data["hooks_detected"] = sum(1 for s in result.scores if s.combined_score > 0.7) if result.scores else 0399 400            # Copy clips and collect data401            for i, clip in enumerate(result.clips):402                if clip.clip_path.exists():403                    clip_output = output_dir / f"clip_{i+1}.mp4"404                    shutil.copy2(clip.clip_path, clip_output)405                    result_data["clip_paths"].append(str(clip_output))406 407                    # Find hook type for this clip408                    hook_type = "none"409                    hook_confidence = 0.0410                    for score in result.scores:411                        if abs(score.start_time - clip.start_time) < 1.0:412                            if score.combined_score > 0.7:413                                hook_confidence = score.combined_score414                                if score.audio_score > score.visual_score and score.audio_score > score.motion_score:415                                    hook_type = "audio_peak"416                                elif score.motion_score > score.visual_score:417                                    hook_type = "motion_spike"418                                else:419                                    hook_type = "visual_highlight"420                            break421 422                    result_data["clips"].append({423                        "clip_id": i + 1,424                        "start_time": round(clip.start_time, 2),425                        "end_time": round(clip.end_time, 2),426                        "duration": round(clip.duration, 2),427                        "hype_score": round(clip.hype_score, 4),428                        "visual_score": round(clip.visual_score, 4),429                        "audio_score": round(clip.audio_score, 4),430                        "motion_score": round(clip.motion_score, 4),431                        "hook_type": hook_type,432                        "hook_confidence": round(hook_confidence, 4),433                    })434        else:435            result_data["error"] = result.error_message436 437        pipeline.cleanup()438 439    except Exception as e:440        result_data["error"] = str(e)441        logger.exception(f"Batch test {test_id} failed")442 443    return result_data444 445 446def results_to_dataframe(results: List[Dict[str, Any]]) -> pd.DataFrame:447    """Convert batch results to a pandas DataFrame for display."""448    rows = []449    for r in results:450        row = {451            "Test ID": r["test_id"],452            "Video": r["video_name"],453            "Domain": r["domain"],454            "Duration": f"{r['clip_duration']}s",455            "Prompt": r["custom_prompt"][:20] + "..." if len(r["custom_prompt"]) > 20 else r["custom_prompt"],456            "Status": r["status"],457            "Time (s)": r["processing_time"],458            "Frames": r["frames_analyzed"],459            "Hooks": r["hooks_detected"],460        }461        # Add clip scores462        for i, clip in enumerate(r.get("clips", [])[:3]):463            row[f"Clip {i+1} Hype"] = clip.get("hype_score", 0)464        rows.append(row)465    return pd.DataFrame(rows)466 467 468def results_to_csv(results: List[Dict[str, Any]]) -> str:469    """Convert results to CSV format."""470    rows = []471    for r in results:472        row = {473            "test_id": r["test_id"],474            "video_name": r["video_name"],475            "domain": r["domain"],476            "clip_duration": r["clip_duration"],477            "custom_prompt": r["custom_prompt"],478            "num_clips": r["num_clips"],479            "status": r["status"],480            "error": r.get("error", ""),481            "processing_time": r["processing_time"],482            "frames_analyzed": r["frames_analyzed"],483            "scenes_detected": r["scenes_detected"],484            "hooks_detected": r["hooks_detected"],485        }486        # Add per-clip data487        for i in range(3):488            if i < len(r.get("clips", [])):489                clip = r["clips"][i]490                row[f"clip_{i+1}_start"] = clip["start_time"]491                row[f"clip_{i+1}_end"] = clip["end_time"]492                row[f"clip_{i+1}_hype"] = clip["hype_score"]493                row[f"clip_{i+1}_visual"] = clip["visual_score"]494                row[f"clip_{i+1}_audio"] = clip["audio_score"]495                row[f"clip_{i+1}_motion"] = clip["motion_score"]496                row[f"clip_{i+1}_hook_type"] = clip["hook_type"]497            else:498                row[f"clip_{i+1}_start"] = ""499                row[f"clip_{i+1}_end"] = ""500                row[f"clip_{i+1}_hype"] = ""501                row[f"clip_{i+1}_visual"] = ""502                row[f"clip_{i+1}_audio"] = ""503                row[f"clip_{i+1}_motion"] = ""504                row[f"clip_{i+1}_hook_type"] = ""505        rows.append(row)506 507    df = pd.DataFrame(rows)508    return df.to_csv(index=False)509 510 511def results_to_json(results: List[Dict[str, Any]]) -> str:512    """Convert results to JSON format."""513    # Remove clip_paths from export (they're temp files)514    export_results = []515    for r in results:516        r_copy = r.copy()517        r_copy.pop("clip_paths", None)518        export_results.append(r_copy)519    return json.dumps(export_results, indent=2)520 521 522def create_clips_zip(results: List[Dict[str, Any]]) -> Optional[str]:523    """Create a ZIP file of all extracted clips."""524    zip_path = Path(tempfile.mkdtemp()) / "batch_clips.zip"525 526    with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:527        for r in results:528            if r["status"] == "success":529                folder_name = f"{Path(r['video_name']).stem}_{r['domain']}_{r['clip_duration']}s"530                if r["custom_prompt"] != "none":531                    folder_name += f"_prompt"532                for clip_path in r.get("clip_paths", []):533                    if Path(clip_path).exists():534                        arcname = f"{folder_name}/{Path(clip_path).name}"535                        zf.write(clip_path, arcname)536 537    return str(zip_path) if zip_path.exists() else None538 539 540# Batch state (module level for simplicity)541batch_state = {542    "is_running": False,543    "should_cancel": False,544    "results": [],545    "output_dir": None,546}547 548 549def run_batch_tests(550    videos,551    domains,552    durations,553    num_clips,554    reference_image,555    include_no_prompt,556    prompt1,557    prompt2,558    prompt3,559    progress=gr.Progress()560):561    """Main batch testing function."""562    global batch_state563 564    # Validate inputs565    if not videos:566        return "Please upload at least one video.", None, "", "", None, None, None567 568    if not domains:569        return "Please select at least one domain.", None, "", "", None, None, None570 571    if not durations:572        return "Please select at least one duration.", None, "", "", None, None, None573 574    # Collect prompts575    prompts = [p for p in [prompt1, prompt2, prompt3] if p and p.strip()]576 577    # Generate test queue578    queue = generate_test_queue(579        videos=videos,580        domains=domains,581        durations=durations,582        num_clips=int(num_clips),583        ref_image=reference_image,584        prompts=prompts,585        include_no_prompt=include_no_prompt,586    )587 588    if not queue:589        return "No tests to run. Please check your configuration.", None, "", "", None, None, None590 591    # Initialize batch state592    batch_state["is_running"] = True593    batch_state["should_cancel"] = False594    batch_state["results"] = []595    batch_state["output_dir"] = Path(tempfile.mkdtemp(prefix="shortsmith_batch_"))596 597    total_tests = len(queue)598    log_messages = []599 600    def log(msg):601        log_messages.append(f"[{time.strftime('%H:%M:%S')}] {msg}")602        logger.info(msg)603 604    log(f"Starting batch testing: {total_tests} tests")605    log(f"Videos: {len(videos)}, Domains: {len(domains)}, Durations: {len(durations)}, Prompts: {len(prompts) + (1 if include_no_prompt else 0)}")606 607    # Run tests sequentially608    for i, test_config in enumerate(queue):609        if batch_state["should_cancel"]:610            log("Batch cancelled by user")611            break612 613        test_id = test_config["test_id"]614        video_name = test_config["video_name"]615        domain = test_config["domain_value"]616        duration = test_config["clip_duration"]617        prompt = test_config["custom_prompt"] or "no-prompt"618 619        log(f"[{i+1}/{total_tests}] Testing: {video_name} | {domain} | {duration}s | {prompt[:30]}...")620        progress((i + 1) / total_tests, desc=f"Test {i+1}/{total_tests}: {video_name}")621 622        # Run the test623        result = run_single_batch_test(test_config, batch_state["output_dir"])624        batch_state["results"].append(result)625 626        if result["status"] == "success":627            log(f"  ✓ Completed in {result['processing_time']}s")628        else:629            log(f"  ✗ Failed: {result.get('error', 'Unknown error')}")630 631    # Finalize632    batch_state["is_running"] = False633    completed = len([r for r in batch_state["results"] if r["status"] == "success"])634    failed = len([r for r in batch_state["results"] if r["status"] == "failed"])635 636    log(f"Batch complete: {completed} succeeded, {failed} failed")637 638    # Generate outputs639    results_df = results_to_dataframe(batch_state["results"])640    csv_content = results_to_csv(batch_state["results"])641    json_content = results_to_json(batch_state["results"])642 643    # Save CSV and JSON to files for download644    csv_path = batch_state["output_dir"] / "results.csv"645    json_path = batch_state["output_dir"] / "results.json"646    csv_path.write_text(csv_content)647    json_path.write_text(json_content)648 649    # Create ZIP of clips650    zip_path = create_clips_zip(batch_state["results"])651 652    status = f"Batch complete: {completed}/{total_tests} tests succeeded"653 654    return (655        status,656        results_df,657        "\n".join(log_messages),658        json_content,659        str(csv_path),660        str(json_path),661        zip_path,662    )663 664 665def cancel_batch():666    """Cancel the running batch."""667    global batch_state668    batch_state["should_cancel"] = True669    return "Cancelling batch... (will stop after current test completes)"670 671 672def calculate_queue_size(videos, domains, durations, include_no_prompt, prompt1, prompt2, prompt3):673    """Calculate and display the queue size."""674    num_videos = len(videos) if videos else 0675    num_domains = len(domains) if domains else 0676    num_durations = len(durations) if durations else 0677 678    prompts = [p for p in [prompt1, prompt2, prompt3] if p and p.strip()]679    num_prompts = len(prompts) + (1 if include_no_prompt else 0)680    if num_prompts == 0:681        num_prompts = 1  # Default to no-prompt if nothing selected682 683    total = num_videos * num_domains * num_durations * num_prompts684 685    return f"Queue: {num_videos} video(s) × {num_domains} domain(s) × {num_durations} duration(s) × {num_prompts} prompt(s) = **{total} tests**"686 687 688# =============================================================================689# Build Gradio Interface690# =============================================================================691 692with gr.Blocks(693    title="ShortSmith v2",694    theme=gr.themes.Soft(),695    css="""696    .container { max-width: 1200px; margin: auto; }697    .output-video { min-height: 200px; }698    """699) as demo:700 701    gr.Markdown("""702    # ShortSmith v2703    ### AI-Powered Video Highlight Extractor704 705    Upload a video and automatically extract the most engaging highlight clips using AI analysis.706    """)707 708    with gr.Tabs():709        # =================================================================710        # Tab 1: Single Video711        # =================================================================712        with gr.TabItem("Single Video"):713            with gr.Row():714                # Left column - Inputs715                with gr.Column(scale=1):716                    gr.Markdown("### Input")717 718                    video_input = gr.Video(719                        label="Upload Video",720                        sources=["upload"],721                    )722 723                    with gr.Accordion("Settings", open=True):724                        domain_dropdown = gr.Dropdown(725                            choices=["Sports", "Vlogs", "Music Videos", "Podcasts", "Gaming", "General"],726                            value="General",727                            label="Content Domain",728                            info="Select the type of content for optimized scoring"729                        )730 731                        with gr.Row():732                            num_clips_slider = gr.Slider(733                                minimum=1,734                                maximum=3,735                                value=3,736                                step=1,737                                label="Number of Clips",738                                info="How many highlight clips to extract"739                            )740                            duration_slider = gr.Slider(741                                minimum=5,742                                maximum=30,743                                value=15,744                                step=1,745                                label="Clip Duration (seconds)",746                                info="Target duration for each clip"747                            )748 749                    with gr.Accordion("Person Filtering (Optional)", open=False):750                        reference_image = gr.Image(751                            label="Reference Image",752                            type="filepath",753                            sources=["upload"],754                        )755                        gr.Markdown("*Upload a photo of a person to prioritize clips featuring them.*")756 757                    with gr.Accordion("Custom Instructions (Optional)", open=False):758                        custom_prompt = gr.Textbox(759                            label="Additional Instructions",760                            placeholder="E.g., 'Focus on crowd reactions' or 'Prioritize action scenes'",761                            lines=2,762                        )763 764                    process_btn = gr.Button(765                        "Extract Highlights",766                        variant="primary",767                        size="lg"768                    )769 770                # Right column - Outputs771                with gr.Column(scale=1):772                    gr.Markdown("### Output")773 774                    status_output = gr.Textbox(775                        label="Status",776                        lines=2,777                        interactive=False778                    )779 780                    gr.Markdown("#### Extracted Clips")781                    clip1_output = gr.Video(label="Clip 1", elem_classes=["output-video"])782                    clip2_output = gr.Video(label="Clip 2", elem_classes=["output-video"])783                    clip3_output = gr.Video(label="Clip 3", elem_classes=["output-video"])784 785                    with gr.Accordion("Processing Log", open=True):786                        log_output = gr.Textbox(787                            label="Log",788                            lines=10,789                            interactive=False,790                            show_copy_button=True791                        )792 793                    with gr.Accordion("Automated Metrics (System-Generated)", open=True):794                        metrics_output = gr.Textbox(795                            label="Metrics for Testing",796                            lines=20,797                            interactive=False,798                            show_copy_button=True,799                            info="Copy these metrics for evaluation spreadsheets"800                        )801 802            # Connect single video processing803            process_btn.click(804                fn=process_video,805                inputs=[806                    video_input,807                    domain_dropdown,808                    num_clips_slider,809                    duration_slider,810                    reference_image,811                    custom_prompt812                ],813                outputs=[814                    status_output,815                    clip1_output,816                    clip2_output,817                    clip3_output,818                    log_output,819                    metrics_output820                ],821                show_progress="full"822            )823 824        # =================================================================825        # Tab 2: Batch Testing826        # =================================================================827        with gr.TabItem("Batch Testing"):828            with gr.Row():829                # Left column - Configuration830                with gr.Column(scale=1):831                    gr.Markdown("### Batch Configuration")832 833                    batch_videos = gr.File(834                        label="Upload Video(s)",835                        file_count="multiple",836                        file_types=["video"],837                    )838 839                    gr.Markdown("#### Domains to Test")840                    batch_domains = gr.CheckboxGroup(841                        choices=["Sports", "Vlogs", "Music Videos", "Podcasts", "Gaming", "General"],842                        value=["General"],843                        label="Select domains",844                    )845 846                    gr.Markdown("#### Clip Durations to Test")847                    batch_durations = gr.CheckboxGroup(848                        choices=[10, 15, 20, 30],849                        value=[15],850                        label="Select durations (seconds)",851                    )852 853                    batch_num_clips = gr.Slider(854                        minimum=1,855                        maximum=3,856                        value=3,857                        step=1,858                        label="Number of Clips per Test",859                    )860 861                    with gr.Accordion("Custom Prompts", open=True):862                        batch_no_prompt = gr.Checkbox(863                            label="Include no-prompt baseline",864                            value=True,865                            info="Test without any custom prompt for comparison"866                        )867                        batch_prompt1 = gr.Textbox(868                            label="Prompt 1",869                            placeholder="E.g., 'Focus on action moments'",870                            lines=1,871                        )872                        batch_prompt2 = gr.Textbox(873                            label="Prompt 2",874                            placeholder="E.g., 'Find crowd reactions'",875                            lines=1,876                        )877                        batch_prompt3 = gr.Textbox(878                            label="Prompt 3",879                            placeholder="E.g., 'Prioritize emotional moments'",880                            lines=1,881                        )882 883                    with gr.Accordion("Reference Image (Optional)", open=False):884                        batch_ref_image = gr.Image(885                            label="Reference Image (applies to all tests)",886                            type="filepath",887                            sources=["upload"],888                        )889 890                    # Queue size indicator891                    queue_info = gr.Markdown("Queue: 0 tests")892 893                    with gr.Row():894                        batch_start_btn = gr.Button(895                            "Start Batch",896                            variant="primary",897                            size="lg"898                        )899                        batch_cancel_btn = gr.Button(900                            "Cancel",901                            variant="secondary",902                            size="lg"903                        )904 905                # Right column - Results906                with gr.Column(scale=1):907                    gr.Markdown("### Results")908 909                    batch_status = gr.Textbox(910                        label="Status",911                        lines=2,912                        interactive=False913                    )914 915                    batch_results_table = gr.Dataframe(916                        label="Test Results",917                        headers=["Test ID", "Video", "Domain", "Duration", "Prompt", "Status", "Time (s)", "Frames", "Hooks"],918                        interactive=False,919                    )920 921                    with gr.Accordion("Processing Log", open=True):922                        batch_log = gr.Textbox(923                            label="Log",924                            lines=15,925                            interactive=False,926                            show_copy_button=True927                        )928 929                    with gr.Accordion("Full Results (JSON)", open=False):930                        batch_json = gr.Textbox(931                            label="JSON Output",932                            lines=10,933                            interactive=False,934                            show_copy_button=True935                        )936 937                    gr.Markdown("#### Download Results")938                    with gr.Row():939                        csv_download = gr.File(label="CSV Results")940                        json_download = gr.File(label="JSON Results")941                        zip_download = gr.File(label="All Clips (ZIP)")942 943            # Update queue size when inputs change944            queue_inputs = [batch_videos, batch_domains, batch_durations, batch_no_prompt, batch_prompt1, batch_prompt2, batch_prompt3]945            for inp in queue_inputs:946                inp.change(947                    fn=calculate_queue_size,948                    inputs=queue_inputs,949                    outputs=queue_info950                )951 952            # Connect batch processing953            batch_start_btn.click(954                fn=run_batch_tests,955                inputs=[956                    batch_videos,957                    batch_domains,958                    batch_durations,959                    batch_num_clips,960                    batch_ref_image,961                    batch_no_prompt,962                    batch_prompt1,963                    batch_prompt2,964                    batch_prompt3,965                ],966                outputs=[967                    batch_status,968                    batch_results_table,969                    batch_log,970                    batch_json,971                    csv_download,972                    json_download,973                    zip_download,974                ],975                show_progress="full"976            )977 978            batch_cancel_btn.click(979                fn=cancel_batch,980                inputs=[],981                outputs=[batch_status]982            )983 984    gr.Markdown("""985    ---986    **ShortSmith v2** | Powered by Qwen2-VL, InsightFace, and Librosa |987    [GitHub](https://github.com) | Built with Gradio988    """)989 990# Launch the app991if __name__ == "__main__":992    demo.queue()993    demo.launch(994        server_name="0.0.0.0",995        server_port=7860,996        show_error=True997    )998else:999    # For HuggingFace Spaces1000    demo.queue()1001    demo.launch()1002