doglanguage-mvp / tests /test_api.py
fromozu's picture
Simplify dog audio input flow
60987fc verified
Raw
History Blame Contribute Delete
4.85 kB
from io import BytesIO
import re
import numpy as np
import pytest
import soundfile as sf
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
@pytest.fixture(autouse=True)
def disable_llm_for_api_tests(monkeypatch):
monkeypatch.delenv("DOGVOICE_LLM_API_KEY", raising=False)
def test_health():
response = client.get("/health")
assert response.status_code == 200
assert response.json()["ok"] is True
assert response.json()["llm"]["configured"] is False
def test_analyze_generated_wav_and_feedback():
wav = _make_wav()
response = client.post(
"/analyze-dog-audio",
files={"file": ("bark.wav", wav, "audio/wav")},
data={"context_note": "门外有人经过,狗狗叫了几声。"},
)
assert response.status_code == 200
data = response.json()
assert data["ok"] is True
assert data["result_id"]
assert data["scene"] == "audio_only"
assert data["arousal"] in {"Low", "Medium", "High"}
assert data["arousal_label"] in {"低", "中", "高"}
assert data["valence"] in {"Negative", "Neutral", "Positive"}
assert data["valence_label"] in {"偏负向", "中性", "偏正向"}
assert data["confidence_label"] in {"低", "中", "高"}
assert data["model_backend_label"] in {"狗叫情绪识别模型", "备用声学特征分析"}
assert data["primary_intent"]["label"]
assert data["primary_intent"]["likelihood"] in {"较高", "中等", "较低"}
assert len(data["intent_candidates"]) == 3
assert data["sound_profile"]["signals"]
assert data["care"]["urgency_label"]
assert data["care"]["next_steps"]
assert data["rule_summary"]
assert data["user_context_note"] == "门外有人经过,狗狗叫了几声。"
assert data["professional_note"]
assert data["science_basis"]
assert data["behavior_knowledge"]
_assert_clean_visible_text(data["professional_note"])
_assert_clean_visible_text(" ".join(f"{item['label']} {item['text']}" for item in data["science_basis"]))
assert data["llm_interpretation"]["status"] == "disabled"
assert "High" not in data["summary"]
assert "Medium" not in data["summary"]
assert "Low" not in data["summary"]
assert "Positive" not in data["summary"]
assert "Neutral" not in data["summary"]
assert "Negative" not in data["summary"]
assert data["timeline"]
assert data["timeline"][0]["arousal_label"]
assert data["timeline"][0]["valence_label"]
feedback = client.post(
"/feedback",
data={
"result_id": data["result_id"],
"rating": "partial",
"actual_reason": "door_or_outside_noise",
"note": "测试反馈",
},
)
assert feedback.status_code == 200
assert feedback.json()["ok"] is True
followup = client.post(
"/follow-up",
data={
"result_id": data["result_id"],
"message": "它最近独处时也会叫,这个需要担心吗?",
},
)
assert followup.status_code == 200
followup_data = followup.json()
assert followup_data["ok"] is True
assert followup_data["reply"]
assert followup_data["professional_note"]
assert followup_data["science_basis"]
assert followup_data["behavior_knowledge"]
_assert_clean_visible_text(followup_data["professional_note"])
_assert_clean_visible_text(" ".join(f"{item['label']} {item['text']}" for item in followup_data["science_basis"]))
def test_rejects_short_wav():
wav = _make_wav(duration=0.5)
response = client.post(
"/analyze-dog-audio",
files={"file": ("short.wav", wav, "audio/wav")},
)
assert response.status_code == 400
assert response.json()["detail"]["ok"] is False
def test_legacy_scene_field_is_ignored():
wav = _make_wav()
response = client.post(
"/analyze-dog-audio",
files={"file": ("bark.wav", wav, "audio/wav")},
data={"scene": "food"},
)
assert response.status_code == 200
assert response.json()["scene"] == "audio_only"
def _make_wav(duration: float = 3.0, sample_rate: int = 22_050) -> BytesIO:
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
carrier = np.sin(2 * np.pi * 520 * t)
envelope = (np.sin(2 * np.pi * 3 * t) > 0).astype(float) * 0.45 + 0.05
signal = (carrier * envelope).astype(np.float32)
buffer = BytesIO()
sf.write(buffer, signal, sample_rate, format="WAV")
buffer.seek(0)
return buffer
def _assert_clean_visible_text(text: str) -> None:
for term in ("Dogbug", "Dogbark", "Dock", "提示词", "资料库检索结果", "专业依据摘要", "规则底稿", "声音需要结合场景解读", "当时场景", "选择场景"):
assert term not in text
assert not re.search(r"[A-Za-z]", text)