CoolFace
Apppublic

Blablablab/audio-classification

sourceHugging Faceapache-2.0updated 1mo agoView on Hugging Face
0likes
preview_cli.py500 linesDownload Raw Back to potato
1#!/usr/bin/env python32"""3Potato Preview CLI4 5A command-line tool for previewing annotation task configurations.6Helps administrators validate configs and see how schemas will render7without running the full server.8 9Usage:10    potato preview config.yaml              # Summary output (default)11    potato preview config.yaml --format html    # HTML output12    potato preview config.yaml --format json    # JSON output13 14    # Or run as module:15    python -m potato.preview_cli config.yaml16"""17 18import argparse19import json20import os21import sys22import yaml23import logging24from typing import Dict, Any, List, Tuple, Optional25 26# Set up logging27logging.basicConfig(level=logging.WARNING)28logger = logging.getLogger(__name__)29 30 31def load_config(config_path: str) -> Dict[str, Any]:32    """33    Load and parse a YAML configuration file.34 35    Args:36        config_path: Path to the configuration file37 38    Returns:39        Parsed configuration dictionary40 41    Raises:42        FileNotFoundError: If config file doesn't exist43        yaml.YAMLError: If config is invalid YAML44    """45    if not os.path.exists(config_path):46        raise FileNotFoundError(f"Configuration file not found: {config_path}")47 48    with open(config_path, 'r', encoding='utf-8') as f:49        config = yaml.safe_load(f)50 51    if not isinstance(config, dict):52        raise ValueError("Configuration must be a YAML object (dictionary)")53 54    return config55 56 57def validate_config(config: Dict[str, Any]) -> List[str]:58    """59    Validate configuration and return list of issues.60 61    Args:62        config: Configuration dictionary63 64    Returns:65        List of validation error/warning messages66    """67    issues = []68 69    # Required fields70    required = ['annotation_task_name', 'item_properties', 'task_dir', 'output_annotation_dir']71    for field in required:72        if field not in config:73            issues.append(f"ERROR: Missing required field '{field}'")74 75    # Data source validation76    has_data_files = config.get('data_files') and len(config.get('data_files', [])) > 077    has_data_directory = bool(config.get('data_directory'))78    if not has_data_files and not has_data_directory:79        issues.append("ERROR: Must have either 'data_files' or 'data_directory'")80 81    # Annotation schemes validation82    has_schemes = 'annotation_schemes' in config83    has_phases = 'phases' in config and config['phases']84 85    if not has_schemes and not has_phases:86        issues.append("ERROR: Must have either 'annotation_schemes' or 'phases'")87 88    if has_schemes and has_phases:89        # Check for potential conflict90        if isinstance(config['phases'], list):91            phases_with_schemes = [p.get('name', f'phase[{i}]')92                                  for i, p in enumerate(config['phases'])93                                  if 'annotation_schemes' in p]94        else:95            phases_with_schemes = [name for name, p in config['phases'].items()96                                  if name != 'order' and isinstance(p, dict) and 'annotation_schemes' in p]97 98        if phases_with_schemes:99            issues.append(f"ERROR: Both top-level and phase-level annotation_schemes found in: {', '.join(phases_with_schemes)}")100 101    return issues102 103 104def get_annotation_schemes(config: Dict[str, Any]) -> List[Dict[str, Any]]:105    """106    Extract all annotation schemes from config.107 108    Args:109        config: Configuration dictionary110 111    Returns:112        List of annotation scheme dictionaries113    """114    schemes = []115 116    if 'annotation_schemes' in config:117        schemes.extend(config['annotation_schemes'])118 119    if 'phases' in config and config['phases']:120        phases = config['phases']121        if isinstance(phases, list):122            for phase in phases:123                if 'annotation_schemes' in phase:124                    schemes.extend(phase['annotation_schemes'])125        else:126            for name, phase in phases.items():127                if name != 'order' and isinstance(phase, dict) and 'annotation_schemes' in phase:128                    schemes.extend(phase['annotation_schemes'])129 130    return schemes131 132 133def detect_keybinding_conflicts(schemes: List[Dict[str, Any]]) -> List[str]:134    """135    Detect keyboard shortcut conflicts across all schemes.136 137    Args:138        schemes: List of annotation scheme dictionaries139 140    Returns:141        List of conflict warning messages142    """143    conflicts = []144    global_keys = {}  # key -> (schema_name, label)145 146    for scheme in schemes:147        schema_name = scheme.get('name', 'unknown')148        labels = scheme.get('labels', [])149 150        for i, label_data in enumerate(labels):151            key_value = None152 153            # Check for explicit key_value154            if isinstance(label_data, dict) and 'key_value' in label_data:155                key_value = str(label_data['key_value'])156                label_name = label_data.get('name', f'label[{i}]')157            elif scheme.get('sequential_key_binding') and len(labels) <= 10:158                key_value = str((i + 1) % 10)159                label_name = label_data if isinstance(label_data, str) else label_data.get('name', f'label[{i}]')160            else:161                continue162 163            if key_value:164                key_id = f"{key_value}"165                if key_id in global_keys:166                    prev_schema, prev_label = global_keys[key_id]167                    if prev_schema != schema_name:  # Only warn for cross-schema conflicts168                        conflicts.append(169                            f"WARNING: Key '{key_value}' used by both "170                            f"'{prev_schema}:{prev_label}' and '{schema_name}:{label_name}'"171                        )172                else:173                    global_keys[key_id] = (schema_name, label_name)174 175    return conflicts176 177 178def generate_preview_html(schemes: List[Dict[str, Any]]) -> str:179    """180    Generate HTML preview for annotation schemes.181 182    Args:183        schemes: List of annotation scheme dictionaries184 185    Returns:186        HTML string with rendered schemes187    """188    from potato.server_utils.schemas.registry import schema_registry189 190    html_parts = []191    all_keybindings = []192 193    html_parts.append("""194<!DOCTYPE html>195<html>196<head>197    <title>Annotation Preview</title>198    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">199    <style>200        body { padding: 20px; font-family: system-ui, sans-serif; }201        .scheme-preview { border: 1px solid #ddd; padding: 20px; margin: 10px 0; border-radius: 8px; }202        .scheme-title { color: #333; margin-bottom: 10px; }203        .scheme-type { color: #666; font-size: 0.9em; }204        .error { color: #dc3545; background: #f8d7da; padding: 10px; border-radius: 4px; }205    </style>206</head>207<body>208<div class="container">209<h1>Annotation Preview</h1>210""")211 212    for idx, scheme in enumerate(schemes):213        scheme_name = scheme.get('name', 'unknown')214        scheme_type = scheme.get('annotation_type', 'unknown')215 216        html_parts.append(f"""217<div class="scheme-preview">218    <h3 class="scheme-title">{scheme_name}</h3>219    <p class="scheme-type">Type: {scheme_type}</p>220    <div class="scheme-content">221""")222 223        try:224            # Set annotation_id before generating (required by schema generators)225            scheme["annotation_id"] = idx226            html, keybindings = schema_registry.generate(scheme)227            html_parts.append(html)228            all_keybindings.extend(keybindings)229        except Exception as e:230            html_parts.append(f'<div class="error">Error generating preview: {str(e)}</div>')231 232        html_parts.append("</div></div>")233 234    # Add keybindings summary235    if all_keybindings:236        html_parts.append("<h2>Keyboard Shortcuts</h2><table class='table'><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>")237        for key, action in all_keybindings:238            html_parts.append(f"<tr><td><kbd>{key}</kbd></td><td>{action}</td></tr>")239        html_parts.append("</tbody></table>")240 241    html_parts.append("</div></body></html>")242 243    return "\n".join(html_parts)244 245 246def generate_preview_json(config: Dict[str, Any], schemes: List[Dict[str, Any]], issues: List[str]) -> str:247    """248    Generate JSON preview output.249 250    Args:251        config: Full configuration dictionary252        schemes: List of annotation schemes253        issues: List of validation issues254 255    Returns:256        JSON string with preview data257    """258    from potato.server_utils.schemas.registry import schema_registry259 260    result = {261        "task_name": config.get('annotation_task_name', 'Unknown'),262        "validation_issues": issues,263        "schema_count": len(schemes),264        "schemas": []265    }266 267    for idx, scheme in enumerate(schemes):268        schema_info = {269            "name": scheme.get('name'),270            "type": scheme.get('annotation_type'),271            "description": scheme.get('description'),272            "labels": None,273            "keybindings": [],274            "error": None275        }276 277        # Extract labels278        if 'labels' in scheme:279            labels = scheme['labels']280            schema_info['labels'] = [281                l if isinstance(l, str) else l.get('name', str(l))282                for l in labels283            ]284 285        # Try to generate and get keybindings286        try:287            # Set annotation_id before generating (required by schema generators)288            scheme["annotation_id"] = idx289            _, keybindings = schema_registry.generate(scheme)290            schema_info['keybindings'] = [{"key": k, "action": a} for k, a in keybindings]291        except Exception as e:292            schema_info['error'] = str(e)293 294        result['schemas'].append(schema_info)295 296    return json.dumps(result, indent=2)297 298 299def generate_preview_summary(config: Dict[str, Any], schemes: List[Dict[str, Any]],300                             issues: List[str], conflicts: List[str]) -> str:301    """302    Generate text summary preview.303 304    Args:305        config: Full configuration dictionary306        schemes: List of annotation schemes307        issues: List of validation issues308        conflicts: List of keybinding conflicts309 310    Returns:311        Text summary string312    """313    lines = []314    lines.append("=" * 60)315    lines.append(f"ANNOTATION TASK PREVIEW")316    lines.append("=" * 60)317    lines.append(f"Task Name: {config.get('annotation_task_name', 'Unknown')}")318    lines.append(f"Task Directory: {config.get('task_dir', 'Not set')}")319    lines.append("")320 321    # Validation issues322    if issues:323        lines.append("VALIDATION ISSUES:")324        for issue in issues:325            lines.append(f"  {issue}")326        lines.append("")327    else:328        lines.append("Validation: PASSED")329        lines.append("")330 331    # Keybinding conflicts332    if conflicts:333        lines.append("KEYBINDING CONFLICTS:")334        for conflict in conflicts:335            lines.append(f"  {conflict}")336        lines.append("")337 338    # Schema summary339    lines.append(f"ANNOTATION SCHEMAS ({len(schemes)} total):")340    lines.append("-" * 40)341 342    from potato.server_utils.schemas.registry import schema_registry343 344    for idx, scheme in enumerate(schemes):345        name = scheme.get('name', 'unknown')346        ann_type = scheme.get('annotation_type', 'unknown')347        desc = scheme.get('description', '')[:50]348 349        lines.append(f"  [{ann_type}] {name}")350        if desc:351            lines.append(f"          {desc}...")352 353        # Count labels if present354        if 'labels' in scheme:355            label_count = len(scheme['labels'])356            lines.append(f"          Labels: {label_count}")357 358        # Try to get keybindings359        try:360            # Set annotation_id before generating (required by schema generators)361            scheme["annotation_id"] = idx362            _, keybindings = schema_registry.generate(scheme)363            if keybindings:364                lines.append(f"          Keybindings: {len(keybindings)}")365        except Exception as e:366            lines.append(f"          ERROR: {str(e)}")367 368        lines.append("")369 370    lines.append("=" * 60)371    return "\n".join(lines)372 373 374def generate_layout_html(schemes: List[Dict[str, Any]]) -> str:375    """376    Generate just the task layout HTML snippet (no wrapper page).377 378    This outputs the HTML that would go inside {{ TASK_LAYOUT }} in the379    annotation template, allowing admins to prototype and debug their380    task layout without running the full server.381 382    Args:383        schemes: List of annotation scheme dictionaries384 385    Returns:386        HTML string with the annotation schema div and all schema forms387    """388    from potato.server_utils.schemas.registry import schema_registry389 390    html_parts = []391    html_parts.append('<div class="annotation_schema">')392 393    for idx, scheme in enumerate(schemes):394        # Set annotation_id before generating (required by schema generators)395        scheme["annotation_id"] = idx396        try:397            html, _ = schema_registry.generate(scheme)398            html_parts.append(html)399        except Exception as e:400            schema_name = scheme.get('name', 'unknown')401            html_parts.append(f'<!-- Error generating {schema_name}: {e} -->')402 403    html_parts.append('</div>')404    return "\n".join(html_parts)405 406 407def main():408    """Main entry point for preview CLI."""409    parser = argparse.ArgumentParser(410        description="Preview annotation task configuration",411        formatter_class=argparse.RawDescriptionHelpFormatter,412        epilog="""413Examples:414  python -m potato.preview_cli config.yaml              # Summary output415  python -m potato.preview_cli config.yaml --format html    # Full HTML page preview416  python -m potato.preview_cli config.yaml --format json    # JSON output417  python -m potato.preview_cli config.yaml --layout-only    # Just the task layout HTML snippet418 419  # Save HTML to file:420  python -m potato.preview_cli config.yaml --format html > preview.html421 422  # Get just the annotation schema div for embedding:423  python -m potato.preview_cli config.yaml --layout-only > task_layout.html424"""425    )426 427    parser.add_argument(428        'config_file',429        help='Path to YAML configuration file'430    )431    parser.add_argument(432        '--format', '-f',433        choices=['summary', 'html', 'json'],434        default='summary',435        help='Output format (default: summary)'436    )437    parser.add_argument(438        '--layout-only', '-l',439        action='store_true',440        help='Output only the task layout HTML snippet (no wrapper page). This is the HTML that goes inside {{ TASK_LAYOUT }}.'441    )442    parser.add_argument(443        '--verbose', '-v',444        action='store_true',445        help='Enable verbose output'446    )447 448    args = parser.parse_args()449 450    if args.verbose:451        logging.getLogger().setLevel(logging.DEBUG)452 453    try:454        # Load configuration455        config = load_config(args.config_file)456 457        # Validate458        issues = validate_config(config)459 460        # Get schemes461        schemes = get_annotation_schemes(config)462        if not schemes:463            print("WARNING: No annotation schemes found in configuration", file=sys.stderr)464 465        # Detect conflicts466        conflicts = detect_keybinding_conflicts(schemes)467 468        # Generate output469        if args.layout_only:470            # Output just the task layout HTML snippet471            print(generate_layout_html(schemes))472        elif args.format == 'html':473            print(generate_preview_html(schemes))474        elif args.format == 'json':475            print(generate_preview_json(config, schemes, issues))476        else:  # summary477            print(generate_preview_summary(config, schemes, issues, conflicts))478 479        # Exit with error code if there are issues (skip for layout-only mode)480        if not args.layout_only:481            error_count = len([i for i in issues if i.startswith('ERROR')])482            sys.exit(1 if error_count > 0 else 0)483 484    except FileNotFoundError as e:485        print(f"Error: {e}", file=sys.stderr)486        sys.exit(1)487    except yaml.YAMLError as e:488        print(f"Error: Invalid YAML in configuration file: {e}", file=sys.stderr)489        sys.exit(1)490    except Exception as e:491        print(f"Error: {e}", file=sys.stderr)492        if args.verbose:493            import traceback494            traceback.print_exc()495        sys.exit(1)496 497 498if __name__ == '__main__':499    main()500