Spaces:
Sleeping
Sleeping
| 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) | |
| 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) | |