Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 50 additions & 4 deletions app/services/va_fusion.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from math import exp, sqrt
from math import exp, sqrt, log
from typing import Dict, Tuple, Optional

# Emotion anchors for Valence (V) and Arousal (A)
Expand Down Expand Up @@ -199,6 +199,33 @@ def apply_zero_prob_mask(
return out


def compute_entropy(probs: Dict[str, float]) -> float:
"""정규화된 엔트로피 계산 (0~1).

감정 분포가 균일할수록(모든 감정이 비슷한 확률) 1에 가깝고,
특정 감정에 집중될수록 0에 가깝습니다.

Args:
probs: 감정별 확률 딕셔너리 (합이 1일 필요 없음)

Returns:
정규화된 엔트로피 값 (0~1)
"""
eps = 1e-10
# 정규화
total = sum(max(0.0, p) for p in probs.values())
if total <= 0:
return 1.0 # 모든 값이 0이면 최대 엔트로피(균일)로 간주

normalized = {k: max(0.0, v) / total for k, v in probs.items()}

# 엔트로피 계산
h = -sum(p * log(p + eps) for p in normalized.values() if p > 0)
max_h = log(len(probs)) if len(probs) > 0 else 1.0 # 균등 분포일 때 최대 엔트로피

return h / max_h if max_h > 0 else 0.0


def fuse_VA(audio_probs: Dict[str, float], text_score: float, text_magnitude: float) -> Dict[str, object]:
"""Fuse audio (emotion probabilities) and text (score,magnitude) into composite VA.

Expand Down Expand Up @@ -231,8 +258,8 @@ def fuse_VA(audio_probs: Dict[str, float], text_score: float, text_magnitude: fl
"happy": pos * mag,
"sad": neg * mag,
"neutral": max(0.0, neutral_base),
"angry": neg * mag * 0.8,
"fear": neg * mag * 0.7,
"angry": neg * mag, # 부정 감정 동일 가중치
"fear": neg * mag, # 부정 감정 동일 가중치
"surprise": pos * mag * 0.8,
}
# 긍정 텍스트( v_text > 0 )일 때 happy 동적 가중(증가) + surprise 경감, 이후 재정규화
Expand Down Expand Up @@ -297,7 +324,26 @@ def fuse_VA(audio_probs: Dict[str, float], text_score: float, text_magnitude: fl
neutral_factor = max(0.3, neutral_base_factor - extra_down)
else:
neutral_factor = neutral_base_factor
composite_score["neutral"] = composite_score.get("neutral", 0.0) * neutral_factor * 0.7

# 충돌 감지: v_audio와 v_text 부호가 다르면 감정 상쇄 발생
# 이 경우 neutral이 과대 평가되므로 추가 억제
is_conflict = (v_audio * v_text) < 0
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conflict detection logic may incorrectly trigger when either v_audio or v_text is zero. When one of them is zero (neutral valence), the product will be zero, which is not less than zero, so is_conflict will be False. However, if you want to detect conflicts only when both have opposite non-zero signs, you should check that both values are non-zero before comparing signs. Consider checking if abs(v_audio) and abs(v_text) are above a small threshold before determining conflict.

Suggested change
is_conflict = (v_audio * v_text) < 0
threshold = 1e-3
is_conflict = (abs(v_audio) > threshold and abs(v_text) > threshold and (v_audio * v_text) < 0)

Copilot uses AI. Check for mistakes.
if is_conflict:
conflict_factor = 0.1 # 충돌 시 neutral 0.1배로 강하게 억제
else:
conflict_factor = 1.0

# 엔트로피 기반 억제: 감정 분포가 균일할수록(엔트로피 높음) neutral 추가 억제
entropy = compute_entropy(composite_score)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entropy computation is called on composite_score which contains weighted emotion scores, not normalized probabilities. Since compute_entropy normalizes internally, this works, but calling it on already heavily modified scores (after multiple weighted adjustments) may not accurately reflect the original distribution uniformity. Consider whether entropy should be computed on the pre-weighted composite_score or if a snapshot should be taken earlier in the fusion process for more meaningful entropy measurement.

Copilot uses AI. Check for mistakes.
if entropy > 0.8:
entropy_factor = 0.3 # 높은 엔트로피 시 0.3배
elif entropy > 0.6:
entropy_factor = 0.6 # 중간 엔트로피 시 0.6배
Comment on lines +338 to +341
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic numbers 0.8 and 0.6 for entropy thresholds lack explanation or justification. These thresholds determine when neutral suppression is applied, but there's no documentation explaining why these specific values were chosen or how they relate to the emotion distribution characteristics. Consider adding inline comments explaining the rationale for these threshold values, or defining them as named constants with descriptive names.

Copilot uses AI. Check for mistakes.
else:
entropy_factor = 1.0

# 최종 neutral 억제: 기존 + 충돌 + 엔트로피
composite_score["neutral"] = composite_score.get("neutral", 0.0) * neutral_factor * 0.7 * conflict_factor * entropy_factor
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compound multiplication of suppression factors (neutral_factor * 0.7 * conflict_factor * entropy_factor) can result in extremely small values. For example, with neutral_factor=0.3, conflict_factor=0.1, and entropy_factor=0.3, the neutral score gets multiplied by 0.3 * 0.7 * 0.1 * 0.3 = 0.0063, reducing it to less than 1% of its original value. This aggressive suppression might completely eliminate legitimate neutral emotions. Consider using additive or bounded multiplicative approaches, or adding a floor value to prevent over-suppression.

Suggested change
composite_score["neutral"] = composite_score.get("neutral", 0.0) * neutral_factor * 0.7 * conflict_factor * entropy_factor
# Prevent over-suppression: ensure neutral is not reduced below 5% of its original value
original_neutral = composite_score.get("neutral", 0.0)
suppressed_neutral = original_neutral * neutral_factor * 0.7 * conflict_factor * entropy_factor
composite_score["neutral"] = max(suppressed_neutral, original_neutral * 0.05)

Copilot uses AI. Check for mistakes.
composite_score["surprise"] = composite_score.get("surprise", 0.0) * 0.9

per_emotion_bps = _normalize_to_bps(composite_score)
Expand Down
4 changes: 2 additions & 2 deletions app/voice_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ async def _process_stt_and_nlp_background(self, file_content: bytes, filename: s
db = SessionLocal()
try:
logger.log_step("(비동기 작업) STT 작업 시작", category="async")
deadline = time.monotonic() + 20.0
deadline = time.monotonic() + 30.0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and get its context
fd "voice_service.py" -type f

Repository: safori-team/CARING-Back

Length of output: 237


🏁 Script executed:

# Read the relevant section around lines 306 and 319
cat -n app/voice_service.py | sed -n '300,330p'

Repository: safori-team/CARING-Back

Length of output: 1699


STT→NLP 전체 데드라인을 30초로 늘린 변경은 타당하나, 주석이 여전히 "20초 내"로 남아 있어 혼동을 야기합니다.

Line 319의 주석 (전체 stt->nlp 20초 내)(전체 stt->nlp 30초 내)로 갱신해 주세요.

🤖 Prompt for AI Agents
In app/voice_service.py around lines 306-319 the deadline variable was changed
to 30.0 seconds but the inline comment at line 319 still reads "(전체 stt->nlp 20초
내)"; update that comment to "(전체 stt->nlp 30초 내)" so the comment matches the new
30-second deadline.


# 1. STT 처리 (스레드 풀에서 실행하여 실제 병렬 처리 가능)
file_obj_for_stt = BytesIO(file_content)
Expand All @@ -322,7 +322,7 @@ def __init__(self, content, filename):
try:
stt_result = await asyncio.wait_for(stt_coro, timeout=remaining)
except asyncio.TimeoutError:
print(f"STT 타임아웃: voice_id={voice_id} after 20s")
print(f"STT 타임아웃: voice_id={voice_id} after 30s")
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated timeout message still uses a hardcoded string "30s" instead of using the actual timeout value from the deadline variable. If the timeout value changes in the future (line 306), this message will need manual updating. Consider using an f-string with the actual timeout value or defining it as a constant that both the deadline calculation and error message can reference.

Copilot uses AI. Check for mistakes.
logger.log_step("stt 타임아웃", category="async")
mark_text_done(db, voice_id)
try_aggregate(db, voice_id)
Expand Down