Spaces:
Running on Zero
Running on Zero
| """ | |
| Run selected V5 emotion/sentiment inference on AppTek call-center samples. | |
| This script uses the final selected V5 model through EmotionPredictor and | |
| generates realistic call-center inference outputs. | |
| Run smoke test from ml-services: | |
| python -m src.inference.run_apptek_inference --max-calls 2 --max-duration-seconds 30 | |
| Run full AppTek sample: | |
| python -m src.inference.run_apptek_inference --max-calls 20 --max-duration-seconds 60 | |
| """ | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| from typing import Any, Dict, Optional | |
| import pandas as pd | |
| from src.inference.emotion_predictor import ( | |
| EmotionPredictor, | |
| SELECTED_EMOTION_MODEL_VERSION, | |
| ) | |
| PROJECT_ROOT = Path(__file__).resolve().parents[3] | |
| ML_SERVICES_ROOT = PROJECT_ROOT / "ml-services" | |
| APPTEK_METADATA_PATH = ( | |
| ML_SERVICES_ROOT / "data" / "processed" / "apptek" / "apptek_metadata.csv" | |
| ) | |
| APPTEK_RESULTS_DIR = ( | |
| ML_SERVICES_ROOT / "outputs" / "apptek" / "sentiment_results" | |
| ) | |
| APPTEK_SUMMARY_PATH = ( | |
| ML_SERVICES_ROOT / "outputs" / "apptek" / "apptek_sentiment_summary.csv" | |
| ) | |
| def result_to_dict(result: Any) -> Dict: | |
| """ | |
| Convert AudioSentimentResult to dictionary for JSON saving. | |
| Supports Pydantic v2, Pydantic v1, or dataclass-like objects. | |
| """ | |
| if hasattr(result, "model_dump"): | |
| return result.model_dump() | |
| if hasattr(result, "dict"): | |
| return result.dict() | |
| if hasattr(result, "__dict__"): | |
| return result.__dict__ | |
| raise TypeError(f"Cannot convert result to dict: {type(result)}") | |
| def run_apptek_inference( | |
| metadata_path: Path = APPTEK_METADATA_PATH, | |
| max_calls: Optional[int] = None, | |
| max_duration_seconds: float = 60.0, | |
| build_timeline: bool = True, | |
| ) -> None: | |
| """ | |
| Run selected emotion model on AppTek samples. | |
| """ | |
| if not metadata_path.exists(): | |
| raise FileNotFoundError( | |
| f"AppTek metadata not found: {metadata_path}\n" | |
| "Run python -m src.data.apptek_dataset first." | |
| ) | |
| metadata = pd.read_csv(metadata_path) | |
| if max_calls is not None: | |
| metadata = metadata.head(max_calls) | |
| APPTEK_RESULTS_DIR.mkdir(parents=True, exist_ok=True) | |
| APPTEK_SUMMARY_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| predictor = EmotionPredictor(max_duration_seconds=max_duration_seconds) | |
| summary_rows = [] | |
| print("\nRunning AppTek inference") | |
| print("-" * 80) | |
| print(f"Selected model: {SELECTED_EMOTION_MODEL_VERSION}") | |
| print(f"Metadata: {metadata_path}") | |
| print(f"Calls to process: {len(metadata)}") | |
| print(f"Max duration per call: {max_duration_seconds} seconds") | |
| print(f"Build timeline: {build_timeline}") | |
| print("-" * 80) | |
| for _, row in metadata.iterrows(): | |
| call_id = row["call_id"] | |
| audio_path = ML_SERVICES_ROOT / row["audio_path"] | |
| domain = row.get("selected_domain", row.get("domain", "")) | |
| raw_domain = row.get("raw_domain", domain) | |
| print(f"Processing {call_id} | domain={domain} | raw_domain={raw_domain} | audio={audio_path.name}") | |
| result = predictor.analyze_audio( | |
| audio_path=audio_path, | |
| call_id=call_id, | |
| build_timeline=build_timeline, | |
| ) | |
| result_dict = result_to_dict(result) | |
| # Add AppTek metadata context to output. | |
| result_dict["apptek_metadata"] = { | |
| "domain": domain, | |
| "raw_domain": raw_domain, | |
| "gender": row.get("gender", ""), | |
| "accent": row.get("accent", ""), | |
| "duration_seconds": float(row.get("duration_seconds", 0.0)), | |
| "text_preview": str(row.get("text", ""))[:500], | |
| } | |
| output_path = APPTEK_RESULTS_DIR / f"{call_id}_sentiment.json" | |
| with output_path.open("w", encoding="utf-8") as file: | |
| json.dump(result_dict, file, indent=2) | |
| summary_rows.append( | |
| { | |
| "call_id": call_id, | |
| "domain": domain, | |
| "raw_domain": raw_domain, | |
| "gender": row.get("gender", ""), | |
| "accent": row.get("accent", ""), | |
| "duration_seconds": row.get("duration_seconds", 0.0), | |
| "processed_duration_seconds": max_duration_seconds, | |
| "overall_audio_sentiment": result_dict.get("overall_audio_sentiment"), | |
| "dominant_emotion": result_dict.get("dominant_emotion"), | |
| "negative_emotion_probability": result_dict.get( | |
| "negative_emotion_probability" | |
| ), | |
| "anger_probability": result_dict.get("anger_probability"), | |
| "stress_probability": result_dict.get("stress_probability"), | |
| "sadness_probability": result_dict.get("sadness_probability"), | |
| "anxiety_probability": result_dict.get("anxiety_probability"), | |
| "calm_probability": result_dict.get("calm_probability"), | |
| "audio_escalation_score": result_dict.get("audio_escalation_score"), | |
| "risk_level": result_dict.get("risk_level"), | |
| "prediction_confidence": result_dict.get("prediction_confidence"), | |
| "confidence_level": result_dict.get("confidence_level"), | |
| "uncertain_prediction": result_dict.get("uncertain_prediction"), | |
| "model_version": result_dict.get("model_version"), | |
| "result_json": str(output_path.relative_to(ML_SERVICES_ROOT)), | |
| } | |
| ) | |
| summary_df = pd.DataFrame(summary_rows) | |
| summary_df.to_csv(APPTEK_SUMMARY_PATH, index=False) | |
| print("\nAppTek inference completed successfully.") | |
| print("-" * 80) | |
| print(f"Saved JSON results to: {APPTEK_RESULTS_DIR}") | |
| print(f"Saved summary CSV to: {APPTEK_SUMMARY_PATH}") | |
| print("-" * 80) | |
| print("\nSummary preview:") | |
| print( | |
| summary_df[ | |
| [ | |
| "call_id", | |
| "domain", | |
| "dominant_emotion", | |
| "overall_audio_sentiment", | |
| "audio_escalation_score", | |
| "risk_level", | |
| "confidence_level", | |
| ] | |
| ].head() | |
| ) | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description="Run selected V5 emotion inference on AppTek samples." | |
| ) | |
| parser.add_argument( | |
| "--metadata-path", | |
| type=Path, | |
| default=APPTEK_METADATA_PATH, | |
| help="Path to AppTek metadata CSV.", | |
| ) | |
| parser.add_argument( | |
| "--max-calls", | |
| type=int, | |
| default=None, | |
| help="Maximum number of AppTek calls to process.", | |
| ) | |
| parser.add_argument( | |
| "--max-duration-seconds", | |
| type=float, | |
| default=60.0, | |
| help="Maximum duration per call to process.", | |
| ) | |
| parser.add_argument( | |
| "--no-timeline", | |
| action="store_true", | |
| help="Disable sentiment timeline generation.", | |
| ) | |
| return parser.parse_args() | |
| if __name__ == "__main__": | |
| args = parse_args() | |
| run_apptek_inference( | |
| metadata_path=args.metadata_path, | |
| max_calls=args.max_calls, | |
| max_duration_seconds=args.max_duration_seconds, | |
| build_timeline=not args.no_timeline, | |
| ) |