feat(US3+4): video frame extraction + video-to-text — POST /api/v1/video/*

- app/models/video_models.py: ExtractFramesRequest, VideoToTextRequest,
  FrameInfo, VideoJobCallback, VideoAcceptedResponse
- app/services/video_service.py: interval+keyframe frame extraction,
  uniform-sample video-to-text, HTTP callback, temp file cleanup
- app/routers/video.py: size check helper (_check_video_size via head_object),
  BackgroundTasks enqueue for both endpoints
- tests: 6 service + 4 router tests, 10/10 passing
This commit is contained in:
wh
2026-04-10 16:00:08 +08:00
parent 2876c179ac
commit 0274bb470a
10 changed files with 560 additions and 1 deletions

View File

@@ -0,0 +1,71 @@
import pytest
from unittest.mock import AsyncMock, patch
from app.core.exceptions import VideoTooLargeError
def test_extract_frames_returns_202(client, mock_storage):
mock_storage.get_object_size = AsyncMock(return_value=10 * 1024 * 1024) # 10 MB
with patch("app.routers.video.BackgroundTasks.add_task"):
resp = client.post(
"/api/v1/video/extract-frames",
json={
"file_path": "video/test.mp4",
"source_id": 10,
"job_id": 42,
},
)
assert resp.status_code == 202
data = resp.json()
assert data["job_id"] == 42
def test_extract_frames_video_too_large_returns_400(client, mock_storage):
mock_storage.get_object_size = AsyncMock(return_value=300 * 1024 * 1024) # 300 MB > 200 MB
resp = client.post(
"/api/v1/video/extract-frames",
json={
"file_path": "video/big.mp4",
"source_id": 10,
"job_id": 99,
},
)
assert resp.status_code == 400
assert resp.json()["code"] == "VIDEO_TOO_LARGE"
def test_video_to_text_returns_202(client, mock_storage):
mock_storage.get_object_size = AsyncMock(return_value=10 * 1024 * 1024)
with patch("app.routers.video.BackgroundTasks.add_task"):
resp = client.post(
"/api/v1/video/to-text",
json={
"file_path": "video/test.mp4",
"source_id": 10,
"job_id": 43,
"start_sec": 0,
"end_sec": 60,
},
)
assert resp.status_code == 202
assert resp.json()["job_id"] == 43
def test_video_to_text_too_large_returns_400(client, mock_storage):
mock_storage.get_object_size = AsyncMock(return_value=300 * 1024 * 1024)
resp = client.post(
"/api/v1/video/to-text",
json={
"file_path": "video/big.mp4",
"source_id": 10,
"job_id": 99,
"start_sec": 0,
"end_sec": 60,
},
)
assert resp.status_code == 400
assert resp.json()["code"] == "VIDEO_TOO_LARGE"