CoolFace
Apppublic

Blablablab/audio-classification

sourceHugging Faceapache-2.0updated 1mo agoView on Hugging Face
0likes
cli.py394 linesDownload Raw Back to export
1"""2Export CLI3 4Command-line interface for exporting Potato annotations to various formats.5 6Usage:7    python -m potato.export --config config.yaml --format coco --output ./out/8    python -m potato.export --config config.yaml --format conll_2003 --output ./out/9    python -m potato.export --list-formats10"""11 12import argparse13import json14import os15import sys16import logging17import glob18 19import yaml20 21from .base import ExportContext22from .registry import export_registry23 24logger = logging.getLogger(__name__)25 26 27def load_annotations_from_output_dir(output_dir: str, schemas: list) -> list:28    """29    Load user annotations from the Potato output directory.30 31    Reads user_state.json files from each user subdirectory32    and flattens annotations into a list of records.33 34    Args:35        output_dir: Path to the annotation output directory36        schemas: List of annotation scheme configs37 38    Returns:39        List of annotation dicts40    """41    annotations = []42 43    if not os.path.isdir(output_dir):44        logger.warning(f"Output directory not found: {output_dir}")45        return annotations46 47    for user_dir in sorted(os.listdir(output_dir)):48        user_path = os.path.join(output_dir, user_dir)49        if not os.path.isdir(user_path):50            continue51 52        state_file = os.path.join(user_path, "user_state.json")53        if not os.path.exists(state_file):54            continue55 56        with open(state_file, "r") as f:57            user_state = json.load(f)58 59        user_id = user_state.get("user_id", user_dir)60 61        # Extract label annotations62        label_data = user_state.get("instance_id_to_label_to_value", {})63        span_data = user_state.get("instance_id_to_span_to_value", {})64 65        # Collect all instance IDs66        all_instances = set(label_data.keys()) | set(span_data.keys())67 68        for instance_id in all_instances:69            # Labels may be stored as a list of [[{schema, name}, value], ...]70            # or as a dict of {schema_name: {label_name: value}}.71            # Normalize to dict format.72            raw_labels = label_data.get(instance_id, {})73            if isinstance(raw_labels, list):74                labels_dict = {}75                for entry in raw_labels:76                    if isinstance(entry, (list, tuple)) and len(entry) == 2:77                        label_obj, value = entry78                        if isinstance(label_obj, dict):79                            schema = label_obj.get("schema", "")80                            name = label_obj.get("name", "")81                        else:82                            schema, name = str(label_obj), ""83                        labels_dict.setdefault(schema, {})[name] = value84                raw_labels = labels_dict85 86            record = {87                "instance_id": instance_id,88                "user_id": user_id,89                "labels": raw_labels,90                "spans": {},91                "links": {},92                "image_annotations": {},93            }94 95            # Process span data96            instance_spans = span_data.get(instance_id, {})97            for schema_name, span_list in instance_spans.items():98                if isinstance(span_list, list):99                    record["spans"][schema_name] = span_list100                elif isinstance(span_list, dict):101                    # Span data might be stored as a dict of span_id -> span_obj102                    record["spans"][schema_name] = list(span_list.values())103 104            # Extract image annotations from labels105            # Image annotations are stored as JSON strings in label values106            for schema_name, label_dict in record["labels"].items():107                schema_config = _find_schema(schemas, schema_name)108                if schema_config and schema_config.get("annotation_type") == "image_annotation":109                    # Image annotation data is stored in the label value110                    for label_key, value in label_dict.items():111                        if isinstance(value, str):112                            try:113                                parsed = json.loads(value)114                                if isinstance(parsed, list):115                                    record["image_annotations"][schema_name] = parsed116                            except (json.JSONDecodeError, TypeError):117                                pass118                        elif isinstance(value, list):119                            record["image_annotations"][schema_name] = value120 121            annotations.append(record)122 123    return annotations124 125 126def load_phase_responses_from_output_dir(output_dir: str) -> list:127    """128    Load phase/surveyflow responses from the Potato output directory.129 130    Reads phase_to_page_to_label_to_value from each user's user_state.json131    and flattens into a list of records.132 133    Returns:134        List of dicts with keys: user_id, phase, page, schema, label_name, value135    """136    responses = []137 138    if not os.path.isdir(output_dir):139        return responses140 141    for user_dir in sorted(os.listdir(output_dir)):142        user_path = os.path.join(output_dir, user_dir)143        if not os.path.isdir(user_path):144            continue145 146        state_file = os.path.join(user_path, "user_state.json")147        if not os.path.exists(state_file):148            continue149 150        with open(state_file, "r") as f:151            user_state = json.load(f)152 153        user_id = user_state.get("user_id", user_dir)154        phase_data = user_state.get("phase_to_page_to_label_to_value", {})155 156        for phase, pages in phase_data.items():157            for page, label_values in pages.items():158                # label_values is a list of [[{schema, name}, value], ...]159                if isinstance(label_values, list):160                    for entry in label_values:161                        if isinstance(entry, (list, tuple)) and len(entry) == 2:162                            label_obj, value = entry163                            if isinstance(label_obj, dict):164                                schema = label_obj.get("schema", "")165                                label_name = label_obj.get("name", "")166                            else:167                                schema, label_name = str(label_obj), ""168                            responses.append({169                                "user_id": user_id,170                                "phase": phase,171                                "page": page,172                                "schema": schema,173                                "label_name": label_name,174                                "value": value,175                            })176                elif isinstance(label_values, dict):177                    for label_obj, value in label_values.items():178                        responses.append({179                            "user_id": user_id,180                            "phase": phase,181                            "page": page,182                            "schema": str(label_obj),183                            "label_name": "",184                            "value": value,185                        })186 187    return responses188 189 190def load_items_from_data_files(config: dict, config_dir: str) -> dict:191    """192    Load item data from the data files specified in config.193 194    Args:195        config: Full Potato configuration dict196        config_dir: Directory containing the config file197 198    Returns:199        Dict mapping instance_id -> item data200    """201    items = {}202    item_props = config.get("item_properties", {})203    id_key = item_props.get("id_key", "id")204 205    data_files = config.get("data_files", [])206    if isinstance(data_files, str):207        data_files = [data_files]208 209    task_dir = config.get("task_dir", ".")210    base_dir = os.path.normpath(os.path.join(config_dir, task_dir))211 212    for data_file_entry in data_files:213        if isinstance(data_file_entry, dict):214            path = data_file_entry.get("path", "")215        else:216            path = str(data_file_entry)217 218        if not os.path.isabs(path):219            path = os.path.join(base_dir, path)220 221        if not os.path.exists(path):222            logger.warning(f"Data file not found: {path}")223            continue224 225        with open(path, "r") as f:226            for line_num, line in enumerate(f, 1):227                line = line.strip()228                if not line:229                    continue230                try:231                    item = json.loads(line)232                    item_id = str(item.get(id_key, f"item_{line_num}"))233                    items[item_id] = item234                except json.JSONDecodeError:235                    # Try CSV/TSV236                    logger.debug(f"Line {line_num} in {path} is not JSON, skipping")237 238    return items239 240 241def _find_schema(schemas: list, name: str) -> dict:242    """Find a schema config by name."""243    for s in schemas:244        if s.get("name") == name:245            return s246    return {}247 248 249def build_export_context(config_path: str) -> ExportContext:250    """251    Build an ExportContext from a Potato config file.252 253    Args:254        config_path: Path to YAML config file255 256    Returns:257        ExportContext ready for export258    """259    config_path = os.path.abspath(config_path)260    config_dir = os.path.dirname(config_path)261 262    with open(config_path, "r") as f:263        config = yaml.safe_load(f)264 265    schemas = config.get("annotation_schemes", [])266 267    # Determine output directory268    task_dir = config.get("task_dir", ".")269    base_dir = os.path.normpath(os.path.join(config_dir, task_dir))270    output_annotation_dir = config.get(271        "output_annotation_dir",272        os.path.join(base_dir, "annotation_output")273    )274    if not os.path.isabs(output_annotation_dir):275        output_annotation_dir = os.path.join(base_dir, output_annotation_dir)276 277    items = load_items_from_data_files(config, config_dir)278    annotations = load_annotations_from_output_dir(output_annotation_dir, schemas)279    phase_responses = load_phase_responses_from_output_dir(output_annotation_dir)280 281    return ExportContext(282        config=config,283        annotations=annotations,284        items=items,285        schemas=schemas,286        output_dir=output_annotation_dir,287        phase_responses=phase_responses,288    )289 290 291def main():292    parser = argparse.ArgumentParser(293        description="Export Potato annotations to standard formats"294    )295    parser.add_argument(296        "--config", "-c",297        help="Path to Potato YAML config file",298    )299    parser.add_argument(300        "--format", "-f",301        help="Export format (e.g., coco, yolo, pascal_voc, conll_2003, conll_u)",302    )303    parser.add_argument(304        "--output", "-o",305        help="Output directory",306        default="./export_output",307    )308    parser.add_argument(309        "--list-formats",310        action="store_true",311        help="List available export formats and exit",312    )313    parser.add_argument(314        "--option",315        action="append",316        default=[],317        help="Format-specific option as key=value (can be repeated)",318    )319    parser.add_argument(320        "--verbose", "-v",321        action="store_true",322        help="Enable verbose logging",323    )324 325    args = parser.parse_args()326 327    logging.basicConfig(328        level=logging.DEBUG if args.verbose else logging.INFO,329        format="%(levelname)s: %(message)s",330    )331 332    if args.list_formats:333        formats = export_registry.list_exporters()334        if not formats:335            print("No export formats registered.")336        else:337            print("Available export formats:\n")338            for fmt in formats:339                exts = ", ".join(fmt["file_extensions"])340                print(f"  {fmt['format_name']:15s} {fmt['description']}")341                print(f"  {'':15s} Extensions: {exts}")342                print()343        return344 345    if not args.config:346        parser.error("--config is required (unless using --list-formats)")347    if not args.format:348        parser.error("--format is required (unless using --list-formats)")349 350    if not os.path.exists(args.config):351        print(f"Error: Config file not found: {args.config}", file=sys.stderr)352        sys.exit(1)353 354    # Parse options355    options = {}356    for opt in args.option:357        if "=" in opt:358            k, v = opt.split("=", 1)359            options[k.strip()] = v.strip()360 361    # Build context362    print(f"Loading config from: {args.config}")363    context = build_export_context(args.config)364    print(f"Loaded {len(context.items)} items, {len(context.annotations)} annotations")365 366    # Export367    print(f"Exporting to {args.format} format...")368    result = export_registry.export(args.format, context, args.output, options)369 370    if result.success:371        print(f"\nExport successful!")372        print(f"Files written:")373        for f in result.files_written:374            print(f"  {f}")375        if result.stats:376            print(f"\nStatistics:")377            for k, v in result.stats.items():378                print(f"  {k}: {v}")379    else:380        print(f"\nExport failed!", file=sys.stderr)381        for err in result.errors:382            print(f"  ERROR: {err}", file=sys.stderr)383 384    if result.warnings:385        print(f"\nWarnings:")386        for w in result.warnings:387            print(f"  WARNING: {w}")388 389    sys.exit(0 if result.success else 1)390 391 392if __name__ == "__main__":393    main()394