Files
mediaplayer/sentence_api/tests/test_assessment_api.py
2026-08-16 15:39:52 +08:00

147 lines
4.4 KiB
Python

import io
import json
import wave
from dataclasses import replace
import numpy as np
from fastapi.testclient import TestClient
from sentence_api.config import Settings
from sentence_api.main import create_app
from sentence_api.models import SentenceBoundaryDocument
from sentence_api.repository import VideoRepository
from sentence_api.store import BoundaryStore
from sentence_api.transcription import Transcript
VIDEO_HASH = "c" * 64
class FakeTranscriber:
available = True
def transcribe(self, audio_path, language=None):
return Transcript(text="The meeting starts at nine.", segments=[])
def make_wav() -> bytes:
sample_rate = 16_000
time = np.arange(sample_rate * 2, dtype=np.float64) / sample_rate
samples = (np.sin(2 * np.pi * 220 * time) * 8000).astype("<i2")
output = io.BytesIO()
with wave.open(output, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
wav_file.writeframes(samples.tobytes())
return output.getvalue()
def make_client(tmp_path, client_api_key=""):
legacy_path = tmp_path / "legacy.json"
legacy_path.write_text(json.dumps({"videos": {}}), encoding="utf-8")
settings = replace(
Settings.from_env(),
data_dir=tmp_path / "data",
legacy_boundaries_file=legacy_path,
admin_api_key="test-admin-key",
client_api_key=client_api_key,
)
settings.ensure_directories()
repository = VideoRepository(settings.database_path)
stored_filename = f"{VIDEO_HASH}.mp4"
(settings.videos_dir / stored_filename).write_bytes(b"video-placeholder")
repository.upsert_upload(
video_hash=VIDEO_HASH,
title="Lesson",
filename="lesson.mp4",
stored_filename=stored_filename,
content_type="video/mp4",
size_bytes=17,
language="en",
)
repository.save_processing_result(
SentenceBoundaryDocument(
video_hash=VIDEO_HASH,
duration_ms=2000,
algorithm_version="test-v1",
sentences=[
{
"index": 0,
"start_ms": 0,
"end_ms": 2000,
"text": "The meeting starts at nine.",
"language": "en",
"reference_speech_duration_ms": 2000,
}
],
),
"The meeting starts at nine.",
)
app = create_app(
BoundaryStore(legacy_path),
settings=settings,
repository=repository,
transcriber=FakeTranscriber(),
)
return TestClient(app)
def test_assessment_returns_duration_and_content_breakdown(tmp_path):
client = make_client(tmp_path)
response = client.post(
f"/api/v1/videos/{VIDEO_HASH}/sentences/0/assessments",
files={"audio": ("reading.wav", make_wav(), "audio/wav")},
data={"language": "en"},
)
assert response.status_code == 200, response.text
payload = response.json()
assert payload["overall_score"] == 100
assert payload["passed"] is True
assert 0.99 <= payload["duration_ratio"] <= 1.02
assert payload["pronunciation_score"] is None
assert payload["details"]["phoneme_scoring"] == "not_enabled"
def test_admin_routes_require_key(tmp_path):
client = make_client(tmp_path)
response = client.delete(f"/api/v1/admin/videos/{VIDEO_HASH}")
assert response.status_code == 401
def test_catalog_and_range_streaming(tmp_path):
client = make_client(tmp_path)
catalog = client.get("/api/v1/videos")
stream = client.get(
f"/api/v1/videos/{VIDEO_HASH}/content",
headers={"Range": "bytes=0-4"},
)
assert catalog.status_code == 200
assert catalog.json()["videos"][0]["stream_url"].endswith(f"/{VIDEO_HASH}/content")
assert stream.status_code == 206
assert stream.content == b"video"
def test_assessment_client_key_is_enforced_when_configured(tmp_path):
client = make_client(tmp_path, client_api_key="tablet-key")
endpoint = f"/api/v1/videos/{VIDEO_HASH}/sentences/0/assessments"
unauthorized = client.post(
endpoint,
files={"audio": ("reading.wav", make_wav(), "audio/wav")},
)
authorized = client.post(
endpoint,
headers={"X-Client-Key": "tablet-key"},
files={"audio": ("reading.wav", make_wav(), "audio/wav")},
)
assert unauthorized.status_code == 401
assert authorized.status_code == 200