ustwo-api / scripts /test_english_e2e.py
asdfasdfqrqwer's picture
Deploy from GitHub 2026-04-23T03:56:31Z
c857b85
Raw
History Blame Contribute Delete
7.75 kB
#!/usr/bin/env python3
"""์˜์–ด 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"
# ๊ฐ์ •๋ณ„ ๋งค์นญ ํ…์ŠคํŠธ (RAVDESS๋Š” ํ…์ŠคํŠธ ์—†์œผ๋ฏ€๋กœ ์ˆ˜๋™ ์ž‘์„ฑ)
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)}๊ฐœ")
# Stage1Output ์ƒ์„ฑ
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",
),
)
# Stage2 config (์˜์–ด ์ „์šฉ)
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 = []
# 1. EmotionResult ๊ฐœ์ˆ˜ ํ™•์ธ
if len(output.emotions) != len(segments):
errors.append(f"EmotionResult ๊ฐœ์ˆ˜ ๋ถˆ์ผ์น˜: {len(output.emotions)} != {len(segments)}")
else:
logger.info(f"[PASS] EmotionResult ๊ฐœ์ˆ˜: {len(output.emotions)}")
# 2. SpeakerSummary ํ™•์ธ
if len(output.speaker_summaries) > 0:
logger.info(f"[PASS] SpeakerSummary ์ƒ์„ฑ: {len(output.speaker_summaries)}๋ช…")
else:
errors.append("SpeakerSummary๊ฐ€ ๋น„์–ด์žˆ์Œ")
# 3. ๊ฐ EmotionResult ์œ ํšจ์„ฑ ํ™•์ธ
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 ์œ ํšจ")
# 4. JSON ์ง๋ ฌํ™” ํ…Œ์ŠคํŠธ
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}")
# 5. ๊ฒฐ๊ณผ ํ…Œ์ด๋ธ”
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}")
# SpeakerSummary ์ถœ๋ ฅ
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)