GLAM_Web_App / full_pipeline.py
unknown
initial commit
6a45c3a
Raw
History Blame Contribute Delete
13.5 kB
import argparse
import csv
import json
import os
import time
from pathlib import Path
from typing import Dict, List, Optional
os.environ.setdefault("HF_HUB_DISABLE_SYMLINKS_WARNING", "1")
os.environ.setdefault("HF_HUB_OFFLINE", "0")
os.environ.setdefault("TRANSFORMERS_OFFLINE", "0")
os.environ.setdefault("HF_DATASETS_OFFLINE", "0")
import librosa
import numpy as np
import torch
from reasoning_pipeline import aggregate_reasoning_summaries
from interface import separate_audio_file, load_wav2vec
DEFAULT_INPUT_DIR = Path("input_mixes")
DEFAULT_OUTPUT_DIR = Path("separated_audios")
DEFAULT_RUNS_DIR = Path("gnn_runs")
DEFAULT_PIPELINE_OUTPUT_DIR = Path("pipeline_results")
DEFAULT_GNN_CHECKPOINT = "best_audio_separation_model.pt"
SUPPORTED_EXTENSIONS = [".wav", ".mp3", ".flac"]
FRAME_SECONDS = 0.5
SR = 16000
WINDOW_SECONDS = 5.0
def find_audio_files(directory: Path) -> List[Path]:
directory = Path(directory)
if not directory.exists():
raise FileNotFoundError(f"Input directory not found: {directory}")
files = []
for ext in SUPPORTED_EXTENSIONS:
files.extend(sorted(directory.glob(f"*{ext}")))
return sorted(files)
def infer_on_audio_file(
audio_path: Path,
gnn_model,
processor,
wav2vec_model,
run_root: Path,
device: torch.device,
window_seconds: float = WINDOW_SECONDS,
frame_seconds: float = FRAME_SECONDS,
) -> Optional[Dict]:
import importlib
gnn = importlib.import_module("gnn")
build_chain_edge_index = getattr(gnn, "build_chain_edge_index")
PatientStateManager = getattr(gnn, "PatientStateManager")
torch_geometric_data = importlib.import_module("torch_geometric.data")
Data = getattr(torch_geometric_data, "Data")
y, _ = librosa.load(str(audio_path), sr=SR, mono=True)
audio_duration_s = float(len(y) / SR)
frame_len = int(frame_seconds * SR)
frames = []
for i in range(0, len(y), frame_len):
f = y[i:i + frame_len]
if len(f) < frame_len:
f = np.pad(f, (0, frame_len - len(f)), mode="constant")
frames.append(f.astype(np.float32))
if not frames:
return None
embeddings = []
batch_size = 8
with torch.no_grad():
for index in range(0, len(frames), batch_size):
batch = frames[index:index + batch_size]
inputs = processor(batch, sampling_rate=SR, return_tensors="pt", padding=True)
input_values = inputs.input_values.to(device)
out = wav2vec_model(input_values)
emb = out.last_hidden_state.mean(dim=1).cpu().numpy().astype(np.float32)
embeddings.append(emb)
X = np.concatenate(embeddings, axis=0) if embeddings else np.zeros((0, 768), dtype=np.float32)
if X.shape[0] == 0:
return None
edge_index = build_chain_edge_index(X.shape[0])
if X.shape[0] == 0:
return None
edge_index = build_chain_edge_index(X.shape[0])
data = Data(x=torch.tensor(X, dtype=torch.float32), edge_index=edge_index)
start = time.time()
with torch.no_grad():
data = data.to(device)
w_logits, c_logits = gnn_model(data)
infer_ms = (time.time() - start) * 1000.0
w_prob = float(torch.sigmoid(w_logits).view(-1)[0].cpu().item())
c_prob = float(torch.sigmoid(c_logits).view(-1)[0].cpu().item())
w_pred = int(w_prob >= 0.5)
c_pred = int(c_prob >= 0.5)
w_unc = c_unc = w_conf = c_conf = None
if hasattr(gnn_model, "log_var_wheeze"):
w_unc = float(np.exp(0.5 * float(gnn_model.log_var_wheeze.detach().cpu().item())))
w_conf = float(1.0 / (1.0 + w_unc))
if hasattr(gnn_model, "log_var_crackle"):
c_unc = float(np.exp(0.5 * float(gnn_model.log_var_crackle.detach().cpu().item())))
c_conf = float(1.0 / (1.0 + c_unc))
breathing_rate_bpm = None
if len(y) >= SR:
breathing_rate_bpm = float(np.nan) if len(y) == 0 else None
from gnn import estimate_breathing_rate_bpm
breathing_rate_bpm = estimate_breathing_rate_bpm(y, SR, audio_duration_s)
state_mgr = PatientStateManager(
ema_alpha=0.12,
low_delta=0.08,
high_delta=0.20,
min_samples_for_baseline=5,
force_established_after_s=10.0,
)
frames_per_window = max(1, int(round(window_seconds / frame_seconds)))
window_rows = []
for w_start in range(0, X.shape[0], frames_per_window):
Xw = X[w_start:w_start + frames_per_window]
if Xw.shape[0] == 0:
continue
ew = build_chain_edge_index(Xw.shape[0])
dw = Data(x=torch.tensor(Xw, dtype=torch.float32), edge_index=ew)
with torch.no_grad():
dw = dw.to(device)
w_l, c_l = gnn_model(dw)
w_p = float(torch.sigmoid(w_l).view(-1)[0].cpu().item())
c_p = float(torch.sigmoid(c_l).view(-1)[0].cpu().item())
w_pd = int(w_p >= 0.5)
c_pd = int(c_p >= 0.5)
start_sec = float(w_start * frame_seconds)
end_sec = float(min((w_start + Xw.shape[0]) * frame_seconds, audio_duration_s))
start_sample = int(start_sec * SR)
end_sample = int(end_sec * SR)
y_window = y[start_sample:end_sample] if end_sample > start_sample else np.array([], dtype=np.float32)
breathing_rate_window = None
if len(y_window) >= SR:
from gnn import estimate_breathing_rate_bpm
breathing_rate_window = estimate_breathing_rate_bpm(y_window, SR, max(end_sec - start_sec, 1e-6))
state_out = state_mgr.update_and_get_state(audio_path.stem, w_p, c_p, timestamp=start_sec)
window_rows.append({
"start_sec": round(start_sec, 3),
"end_sec": round(end_sec, 3),
"num_frames": int(Xw.shape[0]),
"wheeze_prob": round(w_p, 4),
"crackle_prob": round(c_p, 4),
"wheeze_pred": w_pd,
"crackle_pred": c_pd,
"patient_state": state_out.get("overall_state"),
"breathing_rate_bpm": None if breathing_rate_window is None else round(breathing_rate_window, 2),
"ig_topk": [],
"gxi_topk": [],
})
run_id = time.strftime("%Y%m%dT%H%M%SZ") + "_" + audio_path.stem
run_dir = run_root / run_id
run_dir.mkdir(parents=True, exist_ok=True)
output = {
"request_id": run_id,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"result": {
"audio_id": audio_path.stem,
"audio_duration_s": round(audio_duration_s, 3),
"wheeze": {
"probability": round(w_prob, 4),
"prediction": bool(w_pred),
"confidence": None if w_conf is None else round(w_conf, 4),
},
"crackle": {
"probability": round(c_prob, 4),
"prediction": bool(c_pred),
"confidence": None if c_conf is None else round(c_conf, 4),
},
"breathing_rate_bpm": None if breathing_rate_bpm is None else round(breathing_rate_bpm, 2),
"model_version": str(gnn_model.__class__.__name__),
"inference_time_ms": round(float(infer_ms), 2),
},
"reasoning": {
"thresholds": {"wheeze": 0.5, "crackle": 0.5},
"uncertainty_std": {"wheeze": w_unc, "crackle": c_unc},
"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)},
"cumulative_windows": {
"window_seconds": float(window_seconds),
"frame_seconds": float(frame_seconds),
"num_windows": int(len(window_rows)),
"frames_per_window": int(frames_per_window),
"wheeze_window_positive": int(sum(r["wheeze_pred"] for r in window_rows)),
"crackle_window_positive": int(sum(r["crackle_pred"] for r in window_rows)),
"wheeze_window_ratio": float(np.mean([r["wheeze_pred"] for r in window_rows])) if window_rows else 0.0,
"crackle_window_ratio": float(np.mean([r["crackle_pred"] for r in window_rows])) if window_rows else 0.0,
"wheeze_prob_mean_window": float(np.mean([r["wheeze_prob"] for r in window_rows])) if window_rows else 0.0,
"crackle_prob_mean_window": float(np.mean([r["crackle_prob"] for r in window_rows])) if window_rows else 0.0,
},
},
}
with open(run_dir / "result.json", "w", encoding="utf-8") as f:
json.dump(output, f, indent=2)
with open(run_dir / "reasoning.json", "w", encoding="utf-8") as f:
json.dump(output["reasoning"], f, indent=2)
with open(run_dir / "window_report.json", "w", encoding="utf-8") as f:
json.dump({"audio_id": audio_path.stem, "windows": window_rows}, f, indent=2)
with open(run_dir / "window_report.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=[
"start_sec",
"end_sec",
"num_frames",
"wheeze_prob",
"crackle_prob",
"wheeze_pred",
"crackle_pred",
"patient_state",
"breathing_rate_bpm",
"ig_topk",
"gxi_topk",
])
writer.writeheader()
for row in window_rows:
row_copy = dict(row)
row_copy["ig_topk"] = ";".join(map(str, row_copy["ig_topk"]))
row_copy["gxi_topk"] = ";".join(map(str, row_copy["gxi_topk"]))
writer.writerow(row_copy)
output["artifacts"] = {
"result_json": "result.json",
"reasoning_json": "reasoning.json",
"window_report_json": "window_report.json",
"window_report_csv": "window_report.csv",
"run_dir": str(run_dir),
}
return output
def run_full_pipeline(
input_dir: Path,
sep_output_dir: Path,
run_root: Path,
pipeline_output_dir: Path,
gnn_checkpoint: str,
patient_names: Optional[List[str]] = None,
device_str: str = "cuda" if torch.cuda.is_available() else "cpu",
) -> None:
device = torch.device(device_str)
sep_output_dir.mkdir(parents=True, exist_ok=True)
run_root.mkdir(parents=True, exist_ok=True)
pipeline_output_dir.mkdir(parents=True, exist_ok=True)
mixture_files = find_audio_files(input_dir)
if not mixture_files:
raise FileNotFoundError(f"No mixture files found in {input_dir}")
for mix_file in mixture_files:
print(f"Separating mixture: {mix_file.name}")
separate_audio_file(str(mix_file), sep_output_dir, patient_names=patient_names)
print("Loading GNN and Wav2Vec models...")
processor, wav2vec_model = load_wav2vec(device)
from gnn import load_gnn_model
gnn_model = load_gnn_model(gnn_checkpoint, device)
separated_files = find_audio_files(sep_output_dir)
if not separated_files:
raise FileNotFoundError(f"No separated audio files found in {sep_output_dir}")
behavior_results = []
for audio_file in separated_files:
print(f"Running GNN inference: {audio_file.name}")
result = infer_on_audio_file(
audio_file,
gnn_model,
processor,
wav2vec_model,
run_root,
device,
window_seconds=WINDOW_SECONDS,
frame_seconds=FRAME_SECONDS,
)
if result is not None:
behavior_results.append(result)
output_path = pipeline_output_dir / "full_pipeline_results.json"
with output_path.open("w", encoding="utf-8") as f:
json.dump(behavior_results, f, indent=2)
print("Aggregating reasoning summaries...")
run_dirs = [Path(item["artifacts"]["run_dir"]) for item in behavior_results if item.get("artifacts")]
aggregate_reasoning_summaries(run_dirs, pipeline_output_dir)
print(f"Saved full pipeline results to: {output_path}")
print(f"Saved reasoning summary to: {pipeline_output_dir}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run the full auditory separation + GNN reasoning pipeline.")
parser.add_argument("--input-dir", type=Path, default=DEFAULT_INPUT_DIR, help="Folder of mixture audio files.")
parser.add_argument("--sep-output-dir", type=Path, default=DEFAULT_OUTPUT_DIR, help="Folder for separated source audio.")
parser.add_argument("--run-root", type=Path, default=DEFAULT_RUNS_DIR, help="Base folder for GNN run artifacts.")
parser.add_argument("--pipeline-output-dir", type=Path, default=DEFAULT_PIPELINE_OUTPUT_DIR, help="Folder for pipeline summaries.")
parser.add_argument("--gnn-checkpoint", type=str, default=DEFAULT_GNN_CHECKPOINT, help="Path to the GNN checkpoint file.")
parser.add_argument("--patient-names", nargs="*", default=None, help="Optional names for separated sources.")
parser.add_argument("--device", type=str, default="cuda" if torch.cuda.is_available() else "cpu", help="Torch device to use.")
return parser.parse_args()
def main() -> None:
args = parse_args()
patient_names = args.patient_names[:3] if args.patient_names else None
run_full_pipeline(
input_dir=args.input_dir,
sep_output_dir=args.sep_output_dir,
run_root=args.run_root,
pipeline_output_dir=args.pipeline_output_dir,
gnn_checkpoint=args.gnn_checkpoint,
patient_names=patient_names,
device_str=args.device,
)
if __name__ == "__main__":
main()