CoolFace
Apppublic

liammatt5/GLAM_Web_App

sourceHugging Facemitupdated 3mo agoView on Hugging Face
0likes
full_pipeline.py342 linesDownload Raw Back to root
1import argparse2import csv3import json4import os5import time6from pathlib import Path7from typing import Dict, List, Optional8 9os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")10os.environ.setdefault("HF_HUB_OFFLINE", "0")11os.environ.setdefault("TRANSFORMERS_OFFLINE", "0")12os.environ.setdefault("HF_DATASETS_OFFLINE", "0")13 14import librosa15import numpy as np16import torch17from reasoning_pipeline import aggregate_reasoning_summaries18from interface import separate_audio_file, load_wav2vec19 20DEFAULT_INPUT_DIR = Path("input_mixes")21DEFAULT_OUTPUT_DIR = Path("separated_audios")22DEFAULT_RUNS_DIR = Path("gnn_runs")23DEFAULT_PIPELINE_OUTPUT_DIR = Path("pipeline_results")24DEFAULT_GNN_CHECKPOINT = "best_audio_separation_model.pt"25SUPPORTED_EXTENSIONS = [".wav", ".mp3", ".flac"]26FRAME_SECONDS = 0.527SR = 1600028WINDOW_SECONDS = 5.029 30 31def find_audio_files(directory: Path) -> List[Path]:32    directory = Path(directory)33    if not directory.exists():34        raise FileNotFoundError(f"Input directory not found: {directory}")35    files = []36    for ext in SUPPORTED_EXTENSIONS:37        files.extend(sorted(directory.glob(f"*{ext}")))38    return sorted(files)39 40 41def infer_on_audio_file(42    audio_path: Path,43    gnn_model,44    processor,45    wav2vec_model,46    run_root: Path,47    device: torch.device,48    window_seconds: float = WINDOW_SECONDS,49    frame_seconds: float = FRAME_SECONDS,50) -> Optional[Dict]:51    import importlib52 53    gnn = importlib.import_module("gnn")54    build_chain_edge_index = getattr(gnn, "build_chain_edge_index")55    PatientStateManager = getattr(gnn, "PatientStateManager")56    torch_geometric_data = importlib.import_module("torch_geometric.data")57    Data = getattr(torch_geometric_data, "Data")58 59    y, _ = librosa.load(str(audio_path), sr=SR, mono=True)60    audio_duration_s = float(len(y) / SR)61    frame_len = int(frame_seconds * SR)62 63    frames = []64    for i in range(0, len(y), frame_len):65        f = y[i:i + frame_len]66        if len(f) < frame_len:67            f = np.pad(f, (0, frame_len - len(f)), mode="constant")68        frames.append(f.astype(np.float32))69 70    if not frames:71        return None72 73    embeddings = []74    batch_size = 875    with torch.no_grad():76        for index in range(0, len(frames), batch_size):77            batch = frames[index:index + batch_size]78            inputs = processor(batch, sampling_rate=SR, return_tensors="pt", padding=True)79            input_values = inputs.input_values.to(device)80            out = wav2vec_model(input_values)81            emb = out.last_hidden_state.mean(dim=1).cpu().numpy().astype(np.float32)82            embeddings.append(emb)83 84    X = np.concatenate(embeddings, axis=0) if embeddings else np.zeros((0, 768), dtype=np.float32)85    if X.shape[0] == 0:86        return None87 88    edge_index = build_chain_edge_index(X.shape[0])89    if X.shape[0] == 0:90        return None91 92    edge_index = build_chain_edge_index(X.shape[0])93    data = Data(x=torch.tensor(X, dtype=torch.float32), edge_index=edge_index)94 95    start = time.time()96    with torch.no_grad():97        data = data.to(device)98        w_logits, c_logits = gnn_model(data)99    infer_ms = (time.time() - start) * 1000.0100 101    w_prob = float(torch.sigmoid(w_logits).view(-1)[0].cpu().item())102    c_prob = float(torch.sigmoid(c_logits).view(-1)[0].cpu().item())103    w_pred = int(w_prob >= 0.5)104    c_pred = int(c_prob >= 0.5)105 106    w_unc = c_unc = w_conf = c_conf = None107    if hasattr(gnn_model, "log_var_wheeze"):108        w_unc = float(np.exp(0.5 * float(gnn_model.log_var_wheeze.detach().cpu().item())))109        w_conf = float(1.0 / (1.0 + w_unc))110    if hasattr(gnn_model, "log_var_crackle"):111        c_unc = float(np.exp(0.5 * float(gnn_model.log_var_crackle.detach().cpu().item())))112        c_conf = float(1.0 / (1.0 + c_unc))113 114    breathing_rate_bpm = None115    if len(y) >= SR:116        breathing_rate_bpm = float(np.nan) if len(y) == 0 else None117        from gnn import estimate_breathing_rate_bpm118 119        breathing_rate_bpm = estimate_breathing_rate_bpm(y, SR, audio_duration_s)120 121    state_mgr = PatientStateManager(122        ema_alpha=0.12,123        low_delta=0.08,124        high_delta=0.20,125        min_samples_for_baseline=5,126        force_established_after_s=10.0,127    )128 129    frames_per_window = max(1, int(round(window_seconds / frame_seconds)))130    window_rows = []131    for w_start in range(0, X.shape[0], frames_per_window):132        Xw = X[w_start:w_start + frames_per_window]133        if Xw.shape[0] == 0:134            continue135        ew = build_chain_edge_index(Xw.shape[0])136        dw = Data(x=torch.tensor(Xw, dtype=torch.float32), edge_index=ew)137 138        with torch.no_grad():139            dw = dw.to(device)140            w_l, c_l = gnn_model(dw)141 142        w_p = float(torch.sigmoid(w_l).view(-1)[0].cpu().item())143        c_p = float(torch.sigmoid(c_l).view(-1)[0].cpu().item())144        w_pd = int(w_p >= 0.5)145        c_pd = int(c_p >= 0.5)146 147        start_sec = float(w_start * frame_seconds)148        end_sec = float(min((w_start + Xw.shape[0]) * frame_seconds, audio_duration_s))149        start_sample = int(start_sec * SR)150        end_sample = int(end_sec * SR)151        y_window = y[start_sample:end_sample] if end_sample > start_sample else np.array([], dtype=np.float32)152        breathing_rate_window = None153        if len(y_window) >= SR:154            from gnn import estimate_breathing_rate_bpm155 156            breathing_rate_window = estimate_breathing_rate_bpm(y_window, SR, max(end_sec - start_sec, 1e-6))157 158        state_out = state_mgr.update_and_get_state(audio_path.stem, w_p, c_p, timestamp=start_sec)159 160        window_rows.append({161            "start_sec": round(start_sec, 3),162            "end_sec": round(end_sec, 3),163            "num_frames": int(Xw.shape[0]),164            "wheeze_prob": round(w_p, 4),165            "crackle_prob": round(c_p, 4),166            "wheeze_pred": w_pd,167            "crackle_pred": c_pd,168            "patient_state": state_out.get("overall_state"),169            "breathing_rate_bpm": None if breathing_rate_window is None else round(breathing_rate_window, 2),170            "ig_topk": [],171            "gxi_topk": [],172        })173 174    run_id = time.strftime("%Y%m%dT%H%M%SZ") + "_" + audio_path.stem175    run_dir = run_root / run_id176    run_dir.mkdir(parents=True, exist_ok=True)177 178    output = {179        "request_id": run_id,180        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),181        "result": {182            "audio_id": audio_path.stem,183            "audio_duration_s": round(audio_duration_s, 3),184            "wheeze": {185                "probability": round(w_prob, 4),186                "prediction": bool(w_pred),187                "confidence": None if w_conf is None else round(w_conf, 4),188            },189            "crackle": {190                "probability": round(c_prob, 4),191                "prediction": bool(c_pred),192                "confidence": None if c_conf is None else round(c_conf, 4),193            },194            "breathing_rate_bpm": None if breathing_rate_bpm is None else round(breathing_rate_bpm, 2),195            "model_version": str(gnn_model.__class__.__name__),196            "inference_time_ms": round(float(infer_ms), 2),197        },198        "reasoning": {199            "thresholds": {"wheeze": 0.5, "crackle": 0.5},200            "uncertainty_std": {"wheeze": w_unc, "crackle": c_unc},201            "flags": {"short_audio": bool(audio_duration_s < frame_seconds), "near_threshold": bool(abs(w_prob - 0.5) <= 0.02 or abs(c_prob - 0.5) <= 0.02)},202            "cumulative_windows": {203                "window_seconds": float(window_seconds),204                "frame_seconds": float(frame_seconds),205                "num_windows": int(len(window_rows)),206                "frames_per_window": int(frames_per_window),207                "wheeze_window_positive": int(sum(r["wheeze_pred"] for r in window_rows)),208                "crackle_window_positive": int(sum(r["crackle_pred"] for r in window_rows)),209                "wheeze_window_ratio": float(np.mean([r["wheeze_pred"] for r in window_rows])) if window_rows else 0.0,210                "crackle_window_ratio": float(np.mean([r["crackle_pred"] for r in window_rows])) if window_rows else 0.0,211                "wheeze_prob_mean_window": float(np.mean([r["wheeze_prob"] for r in window_rows])) if window_rows else 0.0,212                "crackle_prob_mean_window": float(np.mean([r["crackle_prob"] for r in window_rows])) if window_rows else 0.0,213            },214        },215    }216 217    with open(run_dir / "result.json", "w", encoding="utf-8") as f:218        json.dump(output, f, indent=2)219    with open(run_dir / "reasoning.json", "w", encoding="utf-8") as f:220        json.dump(output["reasoning"], f, indent=2)221    with open(run_dir / "window_report.json", "w", encoding="utf-8") as f:222        json.dump({"audio_id": audio_path.stem, "windows": window_rows}, f, indent=2)223    with open(run_dir / "window_report.csv", "w", encoding="utf-8", newline="") as f:224        writer = csv.DictWriter(f, fieldnames=[225            "start_sec",226            "end_sec",227            "num_frames",228            "wheeze_prob",229            "crackle_prob",230            "wheeze_pred",231            "crackle_pred",232            "patient_state",233            "breathing_rate_bpm",234            "ig_topk",235            "gxi_topk",236        ])237        writer.writeheader()238        for row in window_rows:239            row_copy = dict(row)240            row_copy["ig_topk"] = ";".join(map(str, row_copy["ig_topk"]))241            row_copy["gxi_topk"] = ";".join(map(str, row_copy["gxi_topk"]))242            writer.writerow(row_copy)243 244    output["artifacts"] = {245        "result_json": "result.json",246        "reasoning_json": "reasoning.json",247        "window_report_json": "window_report.json",248        "window_report_csv": "window_report.csv",249        "run_dir": str(run_dir),250    }251    return output252 253 254def run_full_pipeline(255    input_dir: Path,256    sep_output_dir: Path,257    run_root: Path,258    pipeline_output_dir: Path,259    gnn_checkpoint: str,260    patient_names: Optional[List[str]] = None,261    device_str: str = "cuda" if torch.cuda.is_available() else "cpu",262) -> None:263    device = torch.device(device_str)264    sep_output_dir.mkdir(parents=True, exist_ok=True)265    run_root.mkdir(parents=True, exist_ok=True)266    pipeline_output_dir.mkdir(parents=True, exist_ok=True)267 268    mixture_files = find_audio_files(input_dir)269    if not mixture_files:270        raise FileNotFoundError(f"No mixture files found in {input_dir}")271 272    for mix_file in mixture_files:273        print(f"Separating mixture: {mix_file.name}")274        separate_audio_file(str(mix_file), sep_output_dir, patient_names=patient_names)275 276    print("Loading GNN and Wav2Vec models...")277    processor, wav2vec_model = load_wav2vec(device)278    from gnn import load_gnn_model279 280    gnn_model = load_gnn_model(gnn_checkpoint, device)281 282    separated_files = find_audio_files(sep_output_dir)283    if not separated_files:284        raise FileNotFoundError(f"No separated audio files found in {sep_output_dir}")285 286    behavior_results = []287    for audio_file in separated_files:288        print(f"Running GNN inference: {audio_file.name}")289        result = infer_on_audio_file(290            audio_file,291            gnn_model,292            processor,293            wav2vec_model,294            run_root,295            device,296            window_seconds=WINDOW_SECONDS,297            frame_seconds=FRAME_SECONDS,298        )299        if result is not None:300            behavior_results.append(result)301 302    output_path = pipeline_output_dir / "full_pipeline_results.json"303    with output_path.open("w", encoding="utf-8") as f:304        json.dump(behavior_results, f, indent=2)305 306    print("Aggregating reasoning summaries...")307    run_dirs = [Path(item["artifacts"]["run_dir"]) for item in behavior_results if item.get("artifacts")]308    aggregate_reasoning_summaries(run_dirs, pipeline_output_dir)309 310    print(f"Saved full pipeline results to: {output_path}")311    print(f"Saved reasoning summary to: {pipeline_output_dir}")312 313 314def parse_args() -> argparse.Namespace:315    parser = argparse.ArgumentParser(description="Run the full auditory separation + GNN reasoning pipeline.")316    parser.add_argument("--input-dir", type=Path, default=DEFAULT_INPUT_DIR, help="Folder of mixture audio files.")317    parser.add_argument("--sep-output-dir", type=Path, default=DEFAULT_OUTPUT_DIR, help="Folder for separated source audio.")318    parser.add_argument("--run-root", type=Path, default=DEFAULT_RUNS_DIR, help="Base folder for GNN run artifacts.")319    parser.add_argument("--pipeline-output-dir", type=Path, default=DEFAULT_PIPELINE_OUTPUT_DIR, help="Folder for pipeline summaries.")320    parser.add_argument("--gnn-checkpoint", type=str, default=DEFAULT_GNN_CHECKPOINT, help="Path to the GNN checkpoint file.")321    parser.add_argument("--patient-names", nargs="*", default=None, help="Optional names for separated sources.")322    parser.add_argument("--device", type=str, default="cuda" if torch.cuda.is_available() else "cpu", help="Torch device to use.")323    return parser.parse_args()324 325 326def main() -> None:327    args = parse_args()328    patient_names = args.patient_names[:3] if args.patient_names else None329    run_full_pipeline(330        input_dir=args.input_dir,331        sep_output_dir=args.sep_output_dir,332        run_root=args.run_root,333        pipeline_output_dir=args.pipeline_output_dir,334        gnn_checkpoint=args.gnn_checkpoint,335        patient_names=patient_names,336        device_str=args.device,337    )338 339 340if __name__ == "__main__":341    main()342