Blablablab/audio-classification
0
1"""2Codebook Exporter3 4Exports the project codebook (label/code taxonomy) to a CSV file with one row per5code. Designed for qualitative-research workflows where the codebook is a6deliverable in its own right.7 8Output columns:9 schema annotation_scheme name10 annotation_type schema type (radio, multiselect, span, hierarchical_multiselect)11 code label name12 parent parent code (for hierarchical schemas)13 description description / tooltip from the schema config14 color color hex if defined15 n_uses number of times this code was applied across all annotators16"""17 18import csv19import logging20import os21from typing import Optional, Tuple22 23from .base import BaseExporter, ExportContext, ExportResult24 25logger = logging.getLogger(__name__)26 27 28# Schemas that contribute codes to a codebook export.29CODEBOOK_SCHEMA_TYPES = {30 "radio", "multiselect", "select", "likert",31 "span", "hierarchical_multiselect", "tree_annotation",32}33 34 35class CodebookExporter(BaseExporter):36 format_name = "codebook"37 description = "Project codebook (CSV) with code names, hierarchy, and use counts"38 file_extensions = [".csv"]39 40 def can_export(self, context: ExportContext) -> Tuple[bool, str]:41 has_codeable_schema = any(42 s.get("annotation_type") in CODEBOOK_SCHEMA_TYPES43 for s in context.schemas44 )45 if not has_codeable_schema:46 return False, "No codeable schema (radio/multiselect/span/etc.) in config"47 return True, ""48 49 def export(self, context: ExportContext, output_path: str,50 options: Optional[dict] = None) -> ExportResult:51 options = options or {}52 os.makedirs(output_path, exist_ok=True)53 out_file = os.path.join(output_path, "codebook.csv")54 55 use_counts = self._count_label_uses(context)56 57 rows = []58 for scheme in context.schemas:59 atype = scheme.get("annotation_type")60 if atype not in CODEBOOK_SCHEMA_TYPES:61 continue62 schema_name = scheme.get("name", "")63 for code_row in self._iter_codes(scheme):64 code_row["schema"] = schema_name65 code_row["annotation_type"] = atype66 code_row["n_uses"] = use_counts.get(67 (schema_name, code_row["code"]), 068 )69 rows.append(code_row)70 71 fieldnames = [72 "schema", "annotation_type", "code", "parent",73 "description", "color", "n_uses",74 ]75 with open(out_file, "w", newline="", encoding="utf-8") as f:76 writer = csv.DictWriter(f, fieldnames=fieldnames)77 writer.writeheader()78 for r in rows:79 writer.writerow({k: r.get(k, "") for k in fieldnames})80 81 logger.info(f"Codebook exported to {out_file}: {len(rows)} codes")82 return ExportResult(83 success=True,84 format_name=self.format_name,85 files_written=[out_file],86 stats={"codes_exported": len(rows)},87 )88 89 @staticmethod90 def _iter_codes(scheme):91 """Yield {code, parent, description, color} dicts for a schema."""92 atype = scheme.get("annotation_type")93 94 if atype == "hierarchical_multiselect":95 yield from CodebookExporter._iter_hierarchical(scheme.get("labels", []), parent="")96 return97 if atype == "tree_annotation":98 yield from CodebookExporter._iter_hierarchical(scheme.get("labels", []), parent="")99 return100 101 labels = scheme.get("labels", [])102 for label in labels:103 if isinstance(label, dict):104 name = label.get("name", "")105 yield {106 "code": name,107 "parent": "",108 "description": label.get("description") or label.get("tooltip", ""),109 "color": label.get("color", ""),110 }111 else:112 yield {"code": str(label), "parent": "", "description": "", "color": ""}113 114 @staticmethod115 def _iter_hierarchical(nodes, parent):116 if not isinstance(nodes, list):117 return118 for node in nodes:119 if isinstance(node, dict):120 name = node.get("name", "")121 yield {122 "code": name,123 "parent": parent,124 "description": node.get("description") or node.get("tooltip", ""),125 "color": node.get("color", ""),126 }127 children = node.get("children") or node.get("labels") or []128 yield from CodebookExporter._iter_hierarchical(children, parent=name)129 else:130 yield {"code": str(node), "parent": parent, "description": "", "color": ""}131 132 @staticmethod133 def _count_label_uses(context):134 counts = {}135 for ann in context.annotations:136 labels = ann.get("labels", {}) or {}137 for schema_name, schema_payload in labels.items():138 names = []139 if isinstance(schema_payload, dict):140 names = [k for k, v in schema_payload.items() if v]141 elif isinstance(schema_payload, list):142 names = [str(x) for x in schema_payload]143 elif schema_payload not in (None, ""):144 names = [str(schema_payload)]145 for n in names:146 key = (schema_name, n)147 counts[key] = counts.get(key, 0) + 1148 149 spans = ann.get("spans", {}) or {}150 for schema_name, span_list in spans.items():151 for span in span_list or []:152 label = span.get("label") or span.get("annotation")153 if label:154 key = (schema_name, label)155 counts[key] = counts.get(key, 0) + 1156 157 return counts158 