| |
| """์์ด E2E ํ์ดํ๋ผ์ธ ํ
์คํธ. |
| |
| RAVDESS WAV + ์๋ ์์ฑ ํ
์คํธ๋ก Stage2 ์ ์ฒด ํ์ดํ๋ผ์ธ์ ํ
์คํธํ๋ค. |
| ๊ฐ์ ๋ณ 2๊ฐ์ฉ 14๊ฐ ์ธ๊ทธ๋จผํธ๋ฅผ ํต๊ณผ์์ผ audio + text + fusion ๋์์ ๊ฒ์ฆ. |
| |
| Usage: |
| python scripts/test_english_e2e.py |
| """ |
|
|
| from __future__ import annotations |
|
|
| import csv |
| import json |
| import logging |
| import sys |
| from pathlib import Path |
|
|
| PROJECT_ROOT = Path(__file__).parent.parent |
| sys.path.insert(0, str(PROJECT_ROOT)) |
|
|
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s - %(levelname)s - %(message)s", |
| ) |
| logger = logging.getLogger("test_english_e2e") |
|
|
| MANIFEST_PATH = PROJECT_ROOT / "data" / "ravdess" / "manifest.csv" |
|
|
| |
| EMOTION_TEXTS = { |
| "neutral": [ |
| "The meeting is scheduled for three o'clock.", |
| "I'll pick up the groceries on the way home.", |
| ], |
| "joy": [ |
| "I'm so happy to hear from you!", |
| "That's wonderful news, I'm thrilled!", |
| ], |
| "sadness": [ |
| "I miss you so much, it hurts.", |
| "I feel really down today, nothing is going right.", |
| ], |
| "anger": [ |
| "I can't believe you did that, I'm so angry!", |
| "This is completely unacceptable, stop it now!", |
| ], |
| "surprise": [ |
| "Oh my god, I can't believe it!", |
| "What?! I never expected that!", |
| ], |
| "fear": [ |
| "I'm scared, something doesn't feel right.", |
| "Please help, I'm terrified right now.", |
| ], |
| "disgust": [ |
| "That's absolutely disgusting, I feel sick.", |
| "This is revolting, I can't stand it.", |
| ], |
| } |
|
|
|
|
| def pick_samples(manifest_path: Path) -> list[dict]: |
| """๊ฐ์ ๋ณ 2๊ฐ์ฉ WAV ์ ํ.""" |
| by_emotion: dict[str, list[dict]] = {} |
| with open(manifest_path) as f: |
| for row in csv.DictReader(f): |
| emotion = row["emotion"] |
| if emotion not in by_emotion: |
| by_emotion[emotion] = [] |
| if len(by_emotion[emotion]) < 2: |
| by_emotion[emotion].append(row) |
|
|
| samples = [] |
| for emotion in EMOTION_TEXTS: |
| if emotion in by_emotion: |
| samples.extend(by_emotion[emotion]) |
| return samples |
|
|
|
|
| def run_e2e(): |
| """E2E ํ
์คํธ ์คํ.""" |
| from src.common.schemas import ( |
| Models, |
| ProcessingInfo, |
| Segment, |
| Stage1Output, |
| ) |
| from src.stage2.process import process |
|
|
| if not MANIFEST_PATH.exists(): |
| logger.error("manifest.csv๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ๋จผ์ prepare_ravdess.py๋ฅผ ์คํํ์ธ์.") |
| return False |
|
|
| samples = pick_samples(MANIFEST_PATH) |
| logger.info(f"์ ํ๋ ํ
์คํธ ์ํ: {len(samples)}๊ฐ") |
|
|
| |
| segments = [] |
| ground_truths = [] |
| for i, sample in enumerate(samples): |
| emotion = sample["emotion"] |
| texts = EMOTION_TEXTS[emotion] |
| text = texts[i % len(texts)] |
|
|
| segments.append(Segment( |
| segment_id=i, |
| speaker_id=f"speaker_{int(sample['actor_id']) % 2}", |
| start=float(i * 3.0), |
| end=float(i * 3.0 + 3.0), |
| text=text, |
| language="en", |
| audio_path=sample["clean_path"], |
| confidence=0.95, |
| )) |
| ground_truths.append(emotion) |
|
|
| stage1_output = Stage1Output( |
| call_id="ravdess_e2e_test", |
| duration=float(len(segments) * 3.0), |
| speakers=["speaker_0", "speaker_1"], |
| audio_path="data/ravdess/test_call.wav", |
| segments=segments, |
| processing_info=ProcessingInfo( |
| processing_time_sec=1.0, |
| models=Models( |
| diarization="pyannote/speaker-diarization-3.1", |
| asr="large-v3-turbo", |
| language_id="whisper", |
| alignment="whisperx", |
| ), |
| device="cpu", |
| ), |
| ) |
|
|
| |
| config = { |
| "audio_emotion": { |
| "model": "iic/emotion2vec_plus_base", |
| }, |
| "text_emotion": { |
| "korean_model": "searle-j/kote_for_easygoing_people", |
| "english_model": "j-hartmann/emotion-english-distilroberta-base", |
| }, |
| "fusion": { |
| "audio_weight": 0.6, |
| "text_weight": 0.4, |
| }, |
| "output_path": "data/e2e_test_output.json", |
| } |
|
|
| logger.info("Stage 2 process() ์คํ ์ค...") |
| output = process(stage1_output, config=config) |
|
|
| |
| logger.info("\n" + "=" * 70) |
| logger.info("E2E ํ
์คํธ ๊ฒฐ๊ณผ") |
| logger.info("=" * 70) |
|
|
| errors = [] |
|
|
| |
| if len(output.emotions) != len(segments): |
| errors.append(f"EmotionResult ๊ฐ์ ๋ถ์ผ์น: {len(output.emotions)} != {len(segments)}") |
| else: |
| logger.info(f"[PASS] EmotionResult ๊ฐ์: {len(output.emotions)}") |
|
|
| |
| if len(output.speaker_summaries) > 0: |
| logger.info(f"[PASS] SpeakerSummary ์์ฑ: {len(output.speaker_summaries)}๋ช
") |
| else: |
| errors.append("SpeakerSummary๊ฐ ๋น์ด์์") |
|
|
| |
| valid_emotions = {"neutral", "joy", "sadness", "anger", "surprise", "fear", "disgust"} |
| for em in output.emotions: |
| if em.fused_emotion not in valid_emotions: |
| errors.append(f"์ ํจํ์ง ์์ fused_emotion: {em.fused_emotion}") |
| if not (0.0 <= em.fused_confidence <= 1.0): |
| errors.append(f"fused_confidence ๋ฒ์ ์ด๊ณผ: {em.fused_confidence}") |
|
|
| if not any("์ ํจํ์ง ์์" in e for e in errors): |
| logger.info("[PASS] ๋ชจ๋ EmotionResult ์ ํจ") |
|
|
| |
| try: |
| json_str = output.model_dump_json(indent=2) |
| roundtrip = json.loads(json_str) |
| logger.info(f"[PASS] JSON ์ง๋ ฌํ/์ญ์ง๋ ฌํ ์ฑ๊ณต ({len(json_str)} bytes)") |
| except Exception as e: |
| errors.append(f"JSON ์ง๋ ฌํ ์คํจ: {e}") |
|
|
| |
| logger.info(f"\n{'Seg':>3s} | {'Ground Truth':>12s} | {'Audio':>10s} | {'Text':>10s} | {'Fused':>10s} | {'Conf':>5s}") |
| logger.info("-" * 65) |
| audio_correct = 0 |
| text_correct = 0 |
| fused_correct = 0 |
| for i, (em, gt) in enumerate(zip(output.emotions, ground_truths)): |
| a_match = "o" if em.audio_emotion == gt else "x" |
| t_match = "o" if em.text_emotion == gt else "x" |
| f_match = "o" if em.fused_emotion == gt else "x" |
| if em.audio_emotion == gt: |
| audio_correct += 1 |
| if em.text_emotion == gt: |
| text_correct += 1 |
| if em.fused_emotion == gt: |
| fused_correct += 1 |
| logger.info( |
| f"{i:3d} | {gt:>12s} | {em.audio_emotion:>8s} {a_match} | " |
| f"{em.text_emotion:>8s} {t_match} | {em.fused_emotion:>8s} {f_match} | " |
| f"{em.fused_confidence:.3f}" |
| ) |
|
|
| n = len(ground_truths) |
| logger.info(f"\nAccuracy: audio={audio_correct}/{n} text={text_correct}/{n} fused={fused_correct}/{n}") |
|
|
| |
| logger.info("\nSpeaker Summaries:") |
| for spk, summary in output.speaker_summaries.items(): |
| logger.info(f" {spk}: dominant={summary.dominant_emotion}, " |
| f"conf={summary.avg_confidence:.3f}, " |
| f"dist={summary.emotion_distribution}") |
|
|
| if errors: |
| logger.error(f"\nFAILED โ {len(errors)} errors:") |
| for e in errors: |
| logger.error(f" - {e}") |
| return False |
|
|
| logger.info(f"\n{'='*70}") |
| logger.info("E2E ํ
์คํธ PASS โ ์์ด ํ์ดํ๋ผ์ธ ์ ์ ๋์") |
| logger.info(f"{'='*70}") |
| return True |
|
|
|
|
| if __name__ == "__main__": |
| success = run_e2e() |
| sys.exit(0 if success else 1) |
|
|