ecbdd49dc1
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m13s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m14s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m24s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m55s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m55s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 54s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m19s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m41s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m21s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 5m1s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m53s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 52s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m23s
CI/CD Pipeline / Integration Tests (push) Successful in 4m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m20s
CI/CD Pipeline / Unit Tests (push) Failing after 12m24s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
## 任务1: 素材片段随机起始时间 - 修改 _calc_random_start_time 增加 used_segments 参数,避开已使用时间段 - 四个分配函数(ONE_TAKE/PIP/VOICE_OVER/VOICE_PIP)各自维护 used_segments - 同一素材被多个clip使用时,自动记录已用区间,下次随机时避开 - 如果无空间可用,回退到最后已用段之后或缩短时长 ## 任务2: MediaKit智能封面抽帧 - 新增 _extract_frames_via_mediakit 函数,上传视频到OSS后调用MediaKit API - 使用 SceneChange 策略智能选取关键帧 - 失败时自动降级到 ffmpeg 均匀抽帧 - 抽帧后下载帧图、可选叠加标题、上传OSS ## 测试 - 新增 8 个 _calc_random_start_time 单测 - 新增 4 个 distribute_assets 去重逻辑单测 - 新增 4 个 MediaKit 集成单测 - 修复 3 个因字体映射变更的测试断言
224 lines
7.2 KiB
Python
Executable File
224 lines
7.2 KiB
Python
Executable File
"""缩略图生成器单元测试 - 纯逻辑函数."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from video_processing.thumbnail_generator import _format_seek_time
|
|
|
|
|
|
class TestFormatSeekTime:
|
|
"""_format_seek_time 时间格式化测试."""
|
|
|
|
def test_zero_seconds(self):
|
|
"""0秒."""
|
|
result = _format_seek_time(0)
|
|
assert result == "00:00:00.00"
|
|
|
|
def test_less_than_one_second(self):
|
|
"""小于1秒."""
|
|
result = _format_seek_time(0.5)
|
|
assert result == "00:00:00.50"
|
|
|
|
def test_few_seconds(self):
|
|
"""几秒."""
|
|
result = _format_seek_time(5.5)
|
|
assert result == "00:00:05.50"
|
|
|
|
def test_one_minute(self):
|
|
"""1分钟."""
|
|
result = _format_seek_time(60.0)
|
|
assert result == "00:01:00.00"
|
|
|
|
def test_minutes_and_seconds(self):
|
|
"""分+秒."""
|
|
result = _format_seek_time(125.5)
|
|
assert result == "00:02:05.50"
|
|
|
|
def test_one_hour(self):
|
|
"""1小时."""
|
|
result = _format_seek_time(3600.0)
|
|
assert result == "01:00:00.00"
|
|
|
|
def test_hours_minutes_seconds(self):
|
|
"""时+分+秒."""
|
|
result = _format_seek_time(3725.25)
|
|
assert result == "01:02:05.25"
|
|
|
|
def test_long_duration(self):
|
|
"""长视频(2小时以上)."""
|
|
result = _format_seek_time(7384.12)
|
|
assert result == "02:03:04.12"
|
|
|
|
def test_precision_two_decimal(self):
|
|
"""两位小数精度."""
|
|
result = _format_seek_time(3.14159)
|
|
assert result == "00:00:03.14"
|
|
|
|
def test_always_two_digit_hours(self):
|
|
"""小时始终两位数字."""
|
|
result = _format_seek_time(3600 * 9)
|
|
assert result.startswith("09:")
|
|
|
|
def test_always_two_digit_minutes(self):
|
|
"""分钟始终两位数字."""
|
|
result = _format_seek_time(300) # 5分钟
|
|
parts = result.split(":")
|
|
assert parts[1] == "05"
|
|
|
|
def test_float_input(self):
|
|
"""浮点数输入."""
|
|
result = _format_seek_time(10.0)
|
|
assert isinstance(result, str)
|
|
assert result == "00:00:10.00"
|
|
|
|
def test_int_input(self):
|
|
"""整数输入."""
|
|
result = _format_seek_time(30)
|
|
assert result == "00:00:30.00"
|
|
|
|
def test_format_structure(self):
|
|
"""格式结构正确:HH:MM:SS.xx."""
|
|
result = _format_seek_time(3661.5)
|
|
# 格式: HH:MM:SS.xx
|
|
parts = result.split(":")
|
|
assert len(parts) == 3
|
|
assert "." in parts[2]
|
|
sec_parts = parts[2].split(".")
|
|
assert len(sec_parts) == 2
|
|
assert len(sec_parts[1]) == 2 # 两位小数
|
|
|
|
|
|
# ── MediaKit 智能抽帧集成测试 ──────────────────────────────────────────
|
|
|
|
|
|
class TestExtractFramesViaMediakit:
|
|
"""_extract_frames_via_mediakit 函数测试."""
|
|
|
|
def test_mediakit_not_configured_returns_none(self, tmp_path, monkeypatch):
|
|
"""MediaKit 未配置时返回 None."""
|
|
from video_processing.thumbnail_generator import _extract_frames_via_mediakit
|
|
|
|
# Mock get_mediakit_client 返回不可用客户端
|
|
class FakeClient:
|
|
is_available = False
|
|
|
|
monkeypatch.setattr(
|
|
"packages.shared.mediakit_client.get_mediakit_client",
|
|
lambda: FakeClient(),
|
|
)
|
|
|
|
video_file = tmp_path / "test.mp4"
|
|
video_file.write_bytes(b"fake video")
|
|
|
|
result = _extract_frames_via_mediakit(str(video_file), "plan1", 3)
|
|
assert result is None
|
|
|
|
def test_mediakit_success_returns_frames(self, tmp_path, monkeypatch):
|
|
"""MediaKit 成功时返回帧列表."""
|
|
from video_processing.thumbnail_generator import _extract_frames_via_mediakit
|
|
|
|
class FakeClient:
|
|
is_available = True
|
|
|
|
def extract_frames(self, video_url, strategy, max_frames):
|
|
return [
|
|
{"image_url": "https://example.com/frame1.jpg", "timestamp": 1.5},
|
|
{"image_url": "https://example.com/frame2.jpg", "timestamp": 3.2},
|
|
]
|
|
|
|
monkeypatch.setattr(
|
|
"packages.shared.mediakit_client.get_mediakit_client",
|
|
lambda: FakeClient(),
|
|
)
|
|
# Mock upload_to_oss
|
|
monkeypatch.setattr(
|
|
"video_processing.oss_helpers.upload_to_oss",
|
|
lambda path, key: f"https://oss.example.com/{key}",
|
|
)
|
|
# Mock httpx.get for downloading frame
|
|
import httpx
|
|
|
|
class FakeResponse:
|
|
status_code = 200
|
|
content = b"fake image data"
|
|
|
|
def raise_for_status(self):
|
|
pass
|
|
|
|
monkeypatch.setattr("httpx.get", lambda url, **kw: FakeResponse())
|
|
|
|
video_file = tmp_path / "test.mp4"
|
|
video_file.write_bytes(b"fake video")
|
|
|
|
result = _extract_frames_via_mediakit(str(video_file), "plan1", 2)
|
|
assert result is not None
|
|
assert len(result) == 2
|
|
assert result[0]["timestamp"] == 1.5
|
|
assert result[1]["timestamp"] == 3.2
|
|
|
|
def test_mediakit_failure_returns_none(self, tmp_path, monkeypatch):
|
|
"""MediaKit 调用失败时返回 None."""
|
|
from video_processing.thumbnail_generator import _extract_frames_via_mediakit
|
|
|
|
class FakeClient:
|
|
is_available = True
|
|
|
|
def extract_frames(self, video_url, strategy, max_frames):
|
|
return None
|
|
|
|
monkeypatch.setattr(
|
|
"packages.shared.mediakit_client.get_mediakit_client",
|
|
lambda: FakeClient(),
|
|
)
|
|
monkeypatch.setattr(
|
|
"video_processing.oss_helpers.upload_to_oss",
|
|
lambda path, key: f"https://oss.example.com/{key}",
|
|
)
|
|
|
|
video_file = tmp_path / "test.mp4"
|
|
video_file.write_bytes(b"fake video")
|
|
|
|
result = _extract_frames_via_mediakit(str(video_file), "plan1", 3)
|
|
assert result is None
|
|
|
|
|
|
class TestExtractAndUploadCoverFramesFallback:
|
|
"""extract_and_upload_cover_frames 降级逻辑测试."""
|
|
|
|
def test_fallback_to_ffmpeg_when_mediakit_fails(self, tmp_path, monkeypatch):
|
|
"""MediaKit 失败时降级到 ffmpeg 抽帧."""
|
|
from video_processing.thumbnail_generator import extract_and_upload_cover_frames
|
|
|
|
# Mock MediaKit 返回 None(未配置或失败)
|
|
class FakeClient:
|
|
is_available = False
|
|
|
|
monkeypatch.setattr(
|
|
"packages.shared.mediakit_client.get_mediakit_client",
|
|
lambda: FakeClient(),
|
|
)
|
|
|
|
# Mock ffmpeg 抽帧
|
|
monkeypatch.setattr(
|
|
"video_processing.thumbnail_generator.extract_first_frame",
|
|
lambda video_path, output_path, **kw: output_path,
|
|
)
|
|
# Mock upload
|
|
monkeypatch.setattr(
|
|
"video_processing.oss_helpers.upload_to_oss",
|
|
lambda path, key: f"https://oss.example.com/{key}",
|
|
)
|
|
# Mock probe_duration
|
|
monkeypatch.setattr(
|
|
"video_processing.ffmpeg_utils.probe_duration",
|
|
lambda path: 60.0,
|
|
)
|
|
|
|
video_file = tmp_path / "test.mp4"
|
|
video_file.write_bytes(b"fake video")
|
|
|
|
result = extract_and_upload_cover_frames(str(video_file), "plan1", num_frames=2)
|
|
assert len(result) == 2
|
|
assert all("url" in item for item in result)
|
|
assert all("position" in item for item in result)
|