feat: add voice studio prototype flow
This commit is contained in:
@@ -199,3 +199,147 @@ async def test_voice_session_abandon_blocks_future_turns(
|
||||
assert response.status_code == 409
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
async def test_voice_session_uploaded_audio_turn_uses_demo_transcript_hint(
|
||||
db_session,
|
||||
auth_token,
|
||||
):
|
||||
async def override_get_db():
|
||||
yield db_session
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
with (
|
||||
patch(
|
||||
"app.services.voice_session_service.generate_story_content",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_generate,
|
||||
patch(
|
||||
"app.services.voice_session_service.text_to_speech",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_tts,
|
||||
):
|
||||
mock_generate.return_value = StoryOutput(
|
||||
mode="generated",
|
||||
title="小鲸鱼找朋友",
|
||||
story_text="小鲸鱼在海面上遇见了一只会唱歌的海鸥。",
|
||||
cover_prompt_suggestion="温暖儿童绘本封面,小鲸鱼和海鸥",
|
||||
)
|
||||
mock_tts.return_value = b"fake-upload-audio"
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
try:
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
client.cookies.set("access_token", auth_token)
|
||||
|
||||
response = await client.post("/api/voice-sessions", json={})
|
||||
assert response.status_code == 201
|
||||
session_id = response.json()["id"]
|
||||
|
||||
response = await client.post(
|
||||
f"/api/voice-sessions/{session_id}/turns",
|
||||
files={
|
||||
"audio_file": ("turn.webm", b"fake-webm-audio", "audio/webm"),
|
||||
},
|
||||
data={
|
||||
"duration_ms": "3200",
|
||||
"transcript_hint": "我想听一个小鲸鱼找朋友的故事",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 202
|
||||
turn_data = response.json()
|
||||
assert turn_data["status"] == "audio_ready"
|
||||
assert turn_data["transcription_provider"] == "demo"
|
||||
|
||||
turn_id = turn_data["turn_id"]
|
||||
response = await client.get(
|
||||
f"/api/voice-sessions/{session_id}/turns/{turn_id}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
detail = response.json()
|
||||
assert detail["user_audio_ready"] is True
|
||||
assert detail["user_audio_url"].endswith("/user-audio")
|
||||
assert detail["transcription_provider"] == "demo"
|
||||
assert detail["assistant_audio_ready"] is True
|
||||
|
||||
response = await client.get(detail["user_audio_url"])
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"fake-webm-audio"
|
||||
assert response.headers["content-type"] == "audio/webm"
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
async def test_voice_session_list_orders_recent_sessions_first(
|
||||
db_session,
|
||||
auth_token,
|
||||
):
|
||||
async def override_get_db():
|
||||
yield db_session
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
with (
|
||||
patch(
|
||||
"app.services.voice_session_service.generate_story_content",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_generate,
|
||||
patch(
|
||||
"app.services.voice_session_service.text_to_speech",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_tts,
|
||||
):
|
||||
mock_generate.side_effect = [
|
||||
StoryOutput(
|
||||
mode="generated",
|
||||
title="第一场冒险",
|
||||
story_text="第一段故事。",
|
||||
cover_prompt_suggestion="封面一",
|
||||
),
|
||||
StoryOutput(
|
||||
mode="generated",
|
||||
title="第二场冒险",
|
||||
story_text="第二段故事。",
|
||||
cover_prompt_suggestion="封面二",
|
||||
),
|
||||
]
|
||||
mock_tts.side_effect = [b"audio-1", b"audio-2"]
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
try:
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
client.cookies.set("access_token", auth_token)
|
||||
|
||||
response = await client.post("/api/voice-sessions", json={})
|
||||
first_session_id = response.json()["id"]
|
||||
await client.post(
|
||||
f"/api/voice-sessions/{first_session_id}/turns/fallback",
|
||||
json={"transcript_text": "第一个故事"},
|
||||
)
|
||||
|
||||
response = await client.post("/api/voice-sessions", json={})
|
||||
second_session_id = response.json()["id"]
|
||||
await client.post(
|
||||
f"/api/voice-sessions/{second_session_id}/turns/fallback",
|
||||
json={"transcript_text": "第二个故事"},
|
||||
)
|
||||
|
||||
response = await client.get("/api/voice-sessions?limit=8")
|
||||
assert response.status_code == 200
|
||||
sessions = response.json()
|
||||
assert len(sessions) >= 2
|
||||
assert sessions[0]["id"] == second_session_id
|
||||
assert sessions[1]["id"] == first_session_id
|
||||
assert sessions[0]["total_turns"] == 1
|
||||
assert sessions[0]["last_turn_status"] == "audio_ready"
|
||||
|
||||
response = await client.get("/api/voice-sessions?active_only=true")
|
||||
assert response.status_code == 200
|
||||
active_sessions = response.json()
|
||||
assert {item["id"] for item in active_sessions} >= {
|
||||
first_session_id,
|
||||
second_session_id,
|
||||
}
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
Reference in New Issue
Block a user