style: auto-format with black + isort + prettier
CI/CD Pipeline / Check if frontend-only change (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 / Validate - Migration (alembic) (push) Successful in 1m41s
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 / Build Staging Web Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m34s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m49s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m10s

This commit is contained in:
CI Bot
2026-07-24 13:35:05 +00:00
parent f6a1798124
commit 2df7bc9dc8
15 changed files with 449 additions and 339 deletions
+132 -95
View File
@@ -8,7 +8,6 @@
from __future__ import annotations
import pytest
from video_processing.concat_engine import ConcatConfig, ConcatSegment
@@ -23,50 +22,62 @@ class TestConcatSegmentFromDict:
assert seg.has_audio is True
def test_full_fields(self):
seg = ConcatSegment.from_dict({
"video_path": "/tmp/b.mp4",
"start_time": 5.5,
"duration": 10.0,
"has_audio": False,
})
seg = ConcatSegment.from_dict(
{
"video_path": "/tmp/b.mp4",
"start_time": 5.5,
"duration": 10.0,
"has_audio": False,
}
)
assert seg.video_path == "/tmp/b.mp4"
assert seg.start_time == 5.5
assert seg.duration == 10.0
assert seg.has_audio is False
def test_negative_start_time_clamped(self):
seg = ConcatSegment.from_dict({
"video_path": "/tmp/a.mp4",
"start_time": -1.0,
})
seg = ConcatSegment.from_dict(
{
"video_path": "/tmp/a.mp4",
"start_time": -1.0,
}
)
assert seg.start_time == 0.0
def test_negative_duration_clamped(self):
seg = ConcatSegment.from_dict({
"video_path": "/tmp/a.mp4",
"duration": -5.0,
})
seg = ConcatSegment.from_dict(
{
"video_path": "/tmp/a.mp4",
"duration": -5.0,
}
)
assert seg.duration == 0.0
def test_invalid_start_time_type_falls_back(self):
seg = ConcatSegment.from_dict({
"video_path": "/tmp/a.mp4",
"start_time": "not_a_number",
})
seg = ConcatSegment.from_dict(
{
"video_path": "/tmp/a.mp4",
"start_time": "not_a_number",
}
)
assert seg.start_time == 0.0
def test_invalid_duration_type_falls_back(self):
seg = ConcatSegment.from_dict({
"video_path": "/tmp/a.mp4",
"duration": "abc",
})
seg = ConcatSegment.from_dict(
{
"video_path": "/tmp/a.mp4",
"duration": "abc",
}
)
assert seg.duration == 0.0
def test_start_time_none_falls_back(self):
seg = ConcatSegment.from_dict({
"video_path": "/tmp/a.mp4",
"start_time": None,
})
seg = ConcatSegment.from_dict(
{
"video_path": "/tmp/a.mp4",
"start_time": None,
}
)
assert seg.start_time == 0.0
def test_empty_video_path_stored(self):
@@ -94,97 +105,115 @@ class TestConcatConfigFromConfigDict:
assert cfg.segments == []
def test_single_segment(self):
cfg = ConcatConfig.from_config_dict({
"segments": [
{"video_path": "/tmp/a.mp4", "duration": 5.0},
],
})
cfg = ConcatConfig.from_config_dict(
{
"segments": [
{"video_path": "/tmp/a.mp4", "duration": 5.0},
],
}
)
assert len(cfg.segments) == 1
assert cfg.segments[0].video_path == "/tmp/a.mp4"
assert cfg.segments[0].duration == 5.0
def test_multiple_segments(self):
cfg = ConcatConfig.from_config_dict({
"segments": [
{"video_path": "/tmp/a.mp4"},
{"video_path": "/tmp/b.mp4", "start_time": 2.0},
{"video_path": "/tmp/c.mp4", "duration": 3.0, "has_audio": False},
],
})
cfg = ConcatConfig.from_config_dict(
{
"segments": [
{"video_path": "/tmp/a.mp4"},
{"video_path": "/tmp/b.mp4", "start_time": 2.0},
{"video_path": "/tmp/c.mp4", "duration": 3.0, "has_audio": False},
],
}
)
assert len(cfg.segments) == 3
assert cfg.segments[0].video_path == "/tmp/a.mp4"
assert cfg.segments[1].start_time == 2.0
assert cfg.segments[2].has_audio is False
def test_invalid_segments_filtered(self):
cfg = ConcatConfig.from_config_dict({
"segments": [
{"video_path": "/tmp/valid.mp4"},
{"video_path": ""}, # 空路径被过滤
{"not_video_path": "xxx"}, # 没有video_path被过滤
"not_a_dict", # 不是dict被过滤
None, # None被过滤
],
})
cfg = ConcatConfig.from_config_dict(
{
"segments": [
{"video_path": "/tmp/valid.mp4"},
{"video_path": ""}, # 空路径被过滤
{"not_video_path": "xxx"}, # 没有video_path被过滤
"not_a_dict", # 不是dict被过滤
None, # None被过滤
],
}
)
assert len(cfg.segments) == 1
assert cfg.segments[0].video_path == "/tmp/valid.mp4"
def test_segments_not_a_list(self):
cfg = ConcatConfig.from_config_dict({
"segments": "not_a_list",
})
cfg = ConcatConfig.from_config_dict(
{
"segments": "not_a_list",
}
)
assert cfg.segments == []
def test_output_params(self):
cfg = ConcatConfig.from_config_dict({
"segments": [],
"output_width": 1920,
"output_height": 1080,
"output_fps": 30.0,
"force_reencode": True,
})
cfg = ConcatConfig.from_config_dict(
{
"segments": [],
"output_width": 1920,
"output_height": 1080,
"output_fps": 30.0,
"force_reencode": True,
}
)
assert cfg.output_width == 1920
assert cfg.output_height == 1080
assert cfg.output_fps == 30.0
assert cfg.force_reencode is True
def test_negative_output_params_clamped(self):
cfg = ConcatConfig.from_config_dict({
"segments": [],
"output_width": -100,
"output_height": -50,
"output_fps": -1.0,
})
cfg = ConcatConfig.from_config_dict(
{
"segments": [],
"output_width": -100,
"output_height": -50,
"output_fps": -1.0,
}
)
assert cfg.output_width == 0
assert cfg.output_height == 0
assert cfg.output_fps == 0.0
def test_invalid_output_params_fall_back(self):
cfg = ConcatConfig.from_config_dict({
"segments": [],
"output_width": "abc",
"output_height": None,
"output_fps": "xyz",
})
cfg = ConcatConfig.from_config_dict(
{
"segments": [],
"output_width": "abc",
"output_height": None,
"output_fps": "xyz",
}
)
assert cfg.output_width == 0
assert cfg.output_height == 0
assert cfg.output_fps == 0.0
def test_transition_config(self):
cfg = ConcatConfig.from_config_dict({
"segments": [],
"transition": "crossfade",
"transition_duration": 1.0,
})
cfg = ConcatConfig.from_config_dict(
{
"segments": [],
"transition": "crossfade",
"transition_duration": 1.0,
}
)
assert cfg.transition == "crossfade"
assert cfg.transition_duration == 1.0
def test_transition_duration_minimum(self):
"""transition_duration 不能小于 0.1."""
cfg = ConcatConfig.from_config_dict({
"segments": [],
"transition_duration": 0.01,
})
cfg = ConcatConfig.from_config_dict(
{
"segments": [],
"transition_duration": 0.01,
}
)
assert cfg.transition_duration >= 0.1
def test_default_values(self):
@@ -198,16 +227,20 @@ class TestConcatConfigProperties:
"""has_effect / total_segments 属性."""
def test_has_effect_two_or_more_valid(self):
cfg = ConcatConfig(segments=[
ConcatSegment(video_path="/tmp/a.mp4"),
ConcatSegment(video_path="/tmp/b.mp4"),
])
cfg = ConcatConfig(
segments=[
ConcatSegment(video_path="/tmp/a.mp4"),
ConcatSegment(video_path="/tmp/b.mp4"),
]
)
assert cfg.has_effect is True
def test_no_effect_one_segment(self):
cfg = ConcatConfig(segments=[
ConcatSegment(video_path="/tmp/a.mp4"),
])
cfg = ConcatConfig(
segments=[
ConcatSegment(video_path="/tmp/a.mp4"),
]
)
assert cfg.has_effect is False
def test_no_effect_zero_segments(self):
@@ -215,18 +248,22 @@ class TestConcatConfigProperties:
assert cfg.has_effect is False
def test_no_effect_empty_paths(self):
cfg = ConcatConfig(segments=[
ConcatSegment(video_path=""),
ConcatSegment(video_path=""),
])
cfg = ConcatConfig(
segments=[
ConcatSegment(video_path=""),
ConcatSegment(video_path=""),
]
)
assert cfg.has_effect is False
def test_total_segments(self):
cfg = ConcatConfig(segments=[
ConcatSegment(video_path="/tmp/a.mp4"),
ConcatSegment(video_path=""),
ConcatSegment(video_path="/tmp/b.mp4"),
])
cfg = ConcatConfig(
segments=[
ConcatSegment(video_path="/tmp/a.mp4"),
ConcatSegment(video_path=""),
ConcatSegment(video_path="/tmp/b.mp4"),
]
)
assert cfg.total_segments == 2
def test_total_segments_empty(self):
+10 -8
View File
@@ -6,12 +6,12 @@ import pytest
from pydantic import ValidationError
from packages.domain.config_schemas import (
BGMSource,
BGMConfig,
CoverConfig,
CoverType,
DEFAULT_EDIT_PLAN_CONFIG,
DEFAULT_EDIT_TEMPLATE_CONFIG,
BGMConfig,
BGMSource,
CoverConfig,
CoverType,
EditPlanConfigSchema,
EditTemplateConfigSchema,
ExportConfig,
@@ -491,10 +491,12 @@ class TestNormalizeTemplateConfig:
assert result["transition_enabled"] is True
def test_updates_sections(self):
result = normalize_template_config({
"title": {"text": "模板标题"},
"bgm": {"enabled": True},
})
result = normalize_template_config(
{
"title": {"text": "模板标题"},
"bgm": {"enabled": True},
}
)
assert result["title"]["text"] == "模板标题"
assert result["bgm"]["enabled"] is True
@@ -35,7 +35,6 @@ from packages.domain.template_version import EditTemplateVersion
from packages.domain.title_library import TitleLibraryItem
from packages.domain.voice_library import VoiceLibraryItem
# ── EditingMode ───────────────────────────────────────────────────────────────
+2
View File
@@ -435,6 +435,7 @@ class TestAsset:
)
old_updated = asset.updated_at
import time
time.sleep(0.001)
asset.add_tag("tag-1")
assert asset.updated_at >= old_updated
@@ -450,6 +451,7 @@ class TestAsset:
asset.add_tag("tag-1")
old_updated = asset.updated_at
import time
time.sleep(0.001)
asset.remove_tag("tag-1")
assert asset.updated_at >= old_updated
-1
View File
@@ -6,7 +6,6 @@
"""
import pytest
from video_processing.intro_outro_engine import IntroOutroConfig
+2
View File
@@ -280,6 +280,7 @@ class TestJobTransitions:
job = Job.create(project_id="p1", job_type=JobType.VIDEO_COMPOSE)
old_updated = job.updated_at
import time
time.sleep(0.001)
job.transition_to(JobStatus.RUNNING)
assert job.updated_at >= old_updated
@@ -382,6 +383,7 @@ class TestJobProgress:
job = Job.create(project_id="p1", job_type=JobType.VIDEO_COMPOSE)
old_updated = job.updated_at
import time
time.sleep(0.001)
job.update_progress(50.0)
assert job.updated_at >= old_updated
+222 -165
View File
@@ -8,7 +8,6 @@
from __future__ import annotations
import pytest
from video_processing.multi_track_mixer import (
DEFAULT_VOLUMES,
MAX_AUDIO_TRACKS,
@@ -47,150 +46,180 @@ class TestAudioTrackFromDict:
"""AudioTrack.from_dict 构造逻辑."""
def test_basic(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
}
)
assert track.track_id == "t1"
assert track.track_type == "bgm"
assert track.audio_path == "/tmp/bgm.mp3"
assert track.volume == 0.3 # bgm 默认音量
def test_custom_volume(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "main",
"audio_path": "/tmp/main.wav",
"volume": 0.8,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "main",
"audio_path": "/tmp/main.wav",
"volume": 0.8,
}
)
assert track.volume == 0.8
def test_volume_clamped_to_zero(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "sfx",
"audio_path": "/tmp/sfx.wav",
"volume": -1.0,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "sfx",
"audio_path": "/tmp/sfx.wav",
"volume": -1.0,
}
)
assert track.volume == 0.0
def test_volume_clamped_to_max(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "sfx",
"audio_path": "/tmp/sfx.wav",
"volume": 3.0,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "sfx",
"audio_path": "/tmp/sfx.wav",
"volume": 3.0,
}
)
assert track.volume == 2.0
def test_invalid_volume_falls_back_to_default(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"volume": "not_a_number",
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"volume": "not_a_number",
}
)
assert track.volume == 0.3 # bgm 默认
def test_none_volume_falls_back(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "voiceover",
"audio_path": "/tmp/vo.wav",
"volume": None,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "voiceover",
"audio_path": "/tmp/vo.wav",
"volume": None,
}
)
assert track.volume == 1.0 # voiceover 默认
def test_unknown_track_type_default_volume(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "unknown_type",
"audio_path": "/tmp/a.wav",
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "unknown_type",
"audio_path": "/tmp/a.wav",
}
)
assert track.volume == 1.0 # 未知类型默认 1.0
def test_fade_in_fade_out(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"fade_in": 1.5,
"fade_out": 2.0,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"fade_in": 1.5,
"fade_out": 2.0,
}
)
assert track.fade_in == 1.5
assert track.fade_out == 2.0
def test_negative_fade_clamped(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"fade_in": -0.5,
"fade_out": -1.0,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"fade_in": -0.5,
"fade_out": -1.0,
}
)
assert track.fade_in == 0.0
assert track.fade_out == 0.0
def test_invalid_fade_falls_back(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"fade_in": "abc",
"fade_out": None,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"fade_in": "abc",
"fade_out": None,
}
)
assert track.fade_in == 0.0
assert track.fade_out == 0.0
def test_start_time_and_duration(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "sfx",
"audio_path": "/tmp/sfx.wav",
"start_time": 5.0,
"duration": 3.0,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "sfx",
"audio_path": "/tmp/sfx.wav",
"start_time": 5.0,
"duration": 3.0,
}
)
assert track.start_time == 5.0
assert track.duration == 3.0
def test_negative_start_time_clamped(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"start_time": -10.0,
"duration": -2.0,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"start_time": -10.0,
"duration": -2.0,
}
)
assert track.start_time == 0.0
assert track.duration == 0.0
def test_invalid_time_values_fall_back(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"start_time": "invalid",
"duration": "bad",
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"start_time": "invalid",
"duration": "bad",
}
)
assert track.start_time == 0.0
assert track.duration == 0.0
def test_enabled_default_true(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
}
)
assert track.enabled is True
def test_enabled_can_be_false(self):
track = AudioTrack.from_dict({
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"enabled": False,
})
track = AudioTrack.from_dict(
{
"track_id": "t1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"enabled": False,
}
)
assert track.enabled is False
@@ -212,104 +241,126 @@ class TestMultiTrackMixConfigFromDict:
assert cfg.tracks == []
def test_single_track(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": [
{
"track_id": "bgm1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"volume": 0.5,
},
],
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{
"track_id": "bgm1",
"track_type": "bgm",
"audio_path": "/tmp/bgm.mp3",
"volume": 0.5,
},
],
}
)
assert len(cfg.tracks) == 1
assert cfg.tracks[0].track_id == "bgm1"
assert cfg.tracks[0].volume == 0.5
def test_multiple_tracks(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": [
{"track_id": "m", "track_type": "main", "audio_path": "/tmp/m.wav"},
{"track_id": "b", "track_type": "bgm", "audio_path": "/tmp/b.mp3"},
{"track_id": "v", "track_type": "voiceover", "audio_path": "/tmp/v.wav"},
],
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"track_id": "m", "track_type": "main", "audio_path": "/tmp/m.wav"},
{"track_id": "b", "track_type": "bgm", "audio_path": "/tmp/b.mp3"},
{"track_id": "v", "track_type": "voiceover", "audio_path": "/tmp/v.wav"},
],
}
)
assert len(cfg.tracks) == 3
assert cfg.tracks[0].track_type == "main"
assert cfg.tracks[1].track_type == "bgm"
assert cfg.tracks[2].track_type == "voiceover"
def test_disabled_tracks_filtered(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": [
{"track_id": "a", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
{"track_id": "b", "track_type": "sfx", "audio_path": "/tmp/b.wav", "enabled": False},
{"track_id": "c", "track_type": "sfx", "audio_path": "/tmp/c.wav"},
],
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"track_id": "a", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
{"track_id": "b", "track_type": "sfx", "audio_path": "/tmp/b.wav", "enabled": False},
{"track_id": "c", "track_type": "sfx", "audio_path": "/tmp/c.wav"},
],
}
)
assert len(cfg.tracks) == 2
assert all(t.track_id != "b" for t in cfg.tracks)
def test_empty_audio_path_filtered(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": [
{"track_id": "valid", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
{"track_id": "empty", "track_type": "sfx", "audio_path": ""},
],
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"track_id": "valid", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
{"track_id": "empty", "track_type": "sfx", "audio_path": ""},
],
}
)
assert len(cfg.tracks) == 1
assert cfg.tracks[0].track_id == "valid"
def test_invalid_tracks_skipped(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": [
{"track_id": "ok", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
"not_a_dict",
None,
{"no_audio_path": "xxx"},
],
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [
{"track_id": "ok", "track_type": "sfx", "audio_path": "/tmp/a.wav"},
"not_a_dict",
None,
{"no_audio_path": "xxx"},
],
}
)
assert len(cfg.tracks) == 1
def test_tracks_not_a_list(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": "not_a_list",
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": "not_a_list",
}
)
assert cfg.tracks == []
def test_master_volume(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": [],
"master_volume": 0.8,
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"master_volume": 0.8,
}
)
assert cfg.master_volume == 0.8
def test_master_volume_clamped(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": [],
"master_volume": 3.0,
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"master_volume": 3.0,
}
)
assert cfg.master_volume == 2.0
cfg2 = MultiTrackMixConfig.from_config_dict({
"tracks": [],
"master_volume": -1.0,
})
cfg2 = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"master_volume": -1.0,
}
)
assert cfg2.master_volume == 0.0
def test_invalid_master_volume_falls_back(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": [],
"master_volume": "abc",
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"master_volume": "abc",
}
)
assert cfg.master_volume == 1.0
def test_normalize_and_max_output(self):
cfg = MultiTrackMixConfig.from_config_dict({
"tracks": [],
"normalize": False,
"max_output_volume": 2.0,
})
cfg = MultiTrackMixConfig.from_config_dict(
{
"tracks": [],
"normalize": False,
"max_output_volume": 2.0,
}
)
assert cfg.normalize is False
assert cfg.max_output_volume == 2.0
@@ -324,9 +375,11 @@ class TestMultiTrackMixConfigProperties:
"""has_effect 属性."""
def test_has_effect_with_tracks(self):
cfg = MultiTrackMixConfig(tracks=[
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3"),
])
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3"),
]
)
assert cfg.has_effect is True
def test_no_effect_empty(self):
@@ -334,13 +387,17 @@ class TestMultiTrackMixConfigProperties:
assert cfg.has_effect is False
def test_no_effect_all_disabled(self):
cfg = MultiTrackMixConfig(tracks=[
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3", enabled=False),
])
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(track_id="t1", track_type="bgm", audio_path="/tmp/a.mp3", enabled=False),
]
)
assert cfg.has_effect is False
def test_no_effect_empty_paths(self):
cfg = MultiTrackMixConfig(tracks=[
AudioTrack(track_id="t1", track_type="bgm", audio_path=""),
])
cfg = MultiTrackMixConfig(
tracks=[
AudioTrack(track_id="t1", track_type="bgm", audio_path=""),
]
)
assert cfg.has_effect is False
+1 -1
View File
@@ -6,8 +6,8 @@ import pytest
from packages.domain.quota import (
QUOTA_TIERS,
QuotaCheckResult,
QuotaChecker,
QuotaCheckResult,
QuotaDimension,
QuotaRegistry,
QuotaTier,
+1 -2
View File
@@ -10,7 +10,6 @@ from __future__ import annotations
from pathlib import Path
import pytest
from video_processing.render_subtitles import (
_build_ass_style,
_escape_ass_text,
@@ -49,7 +48,7 @@ class TestHexToAssColor:
def test_invalid_length_returns_default(self):
assert _hex_to_ass_color("#FFF") == "&H000000" # 3位
assert _hex_to_ass_color("") == "&H000000" # 空
assert _hex_to_ass_color("") == "&H000000" # 空
def test_mixed_case(self):
result = _hex_to_ass_color("#aBcDeF")
-1
View File
@@ -8,7 +8,6 @@ FFmpeg 抽帧与 OSS 上传由集成测试覆盖.
from __future__ import annotations
import pytest
from video_processing.thumbnail_generator import _format_seek_time
+2 -4
View File
@@ -104,10 +104,8 @@ class TestTransitionPresetLibrary:
def test_duration_constraints_valid(self):
"""时长约束:min <= default <= max"""
for preset in TRANSITION_PRESET_LIBRARY:
assert preset.min_duration <= preset.default_duration, \
f"{preset.id}: min > default"
assert preset.default_duration <= preset.max_duration, \
f"{preset.id}: default > max"
assert preset.min_duration <= preset.default_duration, f"{preset.id}: min > default"
assert preset.default_duration <= preset.max_duration, f"{preset.id}: default > max"
assert preset.min_duration >= 0, f"{preset.id}: min < 0"
def test_known_categories_exist(self):
+1
View File
@@ -216,6 +216,7 @@ class TestTTSJobTransitions:
job = TTSJob.create(user_id="u1", input_text="test")
old = job.updated_at
import time
time.sleep(0.001)
job.transition_to(TTSJobStatus.PROCESSING)
assert job.updated_at >= old
+1
View File
@@ -69,6 +69,7 @@ class TestHashPassword:
def test_hash_includes_salt(self):
"""加盐后与直接 SHA-256 不同"""
from hashlib import sha256
direct = sha256("mypass".encode()).hexdigest()
salted = _hash_password("mypass")
assert direct != salted
@@ -69,15 +69,11 @@ class TestVoiceCloneProfileCreate:
assert profile.name == name
def test_create_with_description(self):
profile = VoiceCloneProfile.create(
user_id="u1", name="test", description="温暖男声"
)
profile = VoiceCloneProfile.create(user_id="u1", name="test", description="温暖男声")
assert profile.description == "温暖男声"
def test_create_with_source_audio(self):
profile = VoiceCloneProfile.create(
user_id="u1", name="test", source_audio_url="http://audio.com/source.wav"
)
profile = VoiceCloneProfile.create(user_id="u1", name="test", source_audio_url="http://audio.com/source.wav")
assert profile.source_audio_url == "http://audio.com/source.wav"
def test_create_with_language(self):
@@ -251,6 +247,7 @@ class TestVoiceCloneProfileTransitions:
p = VoiceCloneProfile.create(user_id="u1", name="test")
old = p.updated_at
import time
time.sleep(0.001)
p.transition_to(VoiceCloneStatus.PROCESSING)
assert p.updated_at >= old
+72 -55
View File
@@ -6,7 +6,6 @@
"""
import pytest
from video_processing.watermark_engine import WATERMARK_POSITIONS, WatermarkConfig
@@ -44,46 +43,56 @@ class TestWatermarkConfigFromDict:
assert WatermarkConfig.from_dict({"enabled": False}) is None
def test_image_mode_without_path_returns_none(self):
result = WatermarkConfig.from_dict({
"enabled": True,
"mode": "image",
})
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "image",
}
)
assert result is None
def test_image_mode_with_empty_path_returns_none(self):
result = WatermarkConfig.from_dict({
"enabled": True,
"mode": "image",
"image_path": "",
})
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "image",
"image_path": "",
}
)
assert result is None
def test_text_mode_without_text_returns_none(self):
result = WatermarkConfig.from_dict({
"enabled": True,
"mode": "text",
})
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
}
)
assert result is None
def test_text_mode_with_empty_text_returns_none(self):
result = WatermarkConfig.from_dict({
"enabled": True,
"mode": "text",
"text": "",
})
result = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "",
}
)
assert result is None
def test_image_mode_success(self):
cfg = WatermarkConfig.from_dict({
"enabled": True,
"mode": "image",
"image_path": "/tmp/logo.png",
"scale": 0.3,
"opacity": 0.9,
"position": "top_left",
"margin_x": 30,
"margin_y": 30,
})
cfg = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "image",
"image_path": "/tmp/logo.png",
"scale": 0.3,
"opacity": 0.9,
"position": "top_left",
"margin_x": 30,
"margin_y": 30,
}
)
assert cfg is not None
assert cfg.mode == "image"
assert cfg.image_path == "/tmp/logo.png"
@@ -95,25 +104,29 @@ class TestWatermarkConfigFromDict:
def test_image_mode_image_key_fallback(self):
"""image 字段作为 image_path 的 fallback."""
cfg = WatermarkConfig.from_dict({
"enabled": True,
"mode": "image",
"image": "/tmp/fallback.png",
})
cfg = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "image",
"image": "/tmp/fallback.png",
}
)
assert cfg is not None
assert cfg.image_path == "/tmp/fallback.png"
def test_text_mode_success(self):
cfg = WatermarkConfig.from_dict({
"enabled": True,
"mode": "text",
"text": "hello world",
"font_size": 32,
"font_color": "red",
"position": "bottom_left",
"scroll": True,
"scroll_speed": 100,
})
cfg = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "hello world",
"font_size": 32,
"font_color": "red",
"position": "bottom_left",
"scroll": True,
"scroll_speed": 100,
}
)
assert cfg is not None
assert cfg.mode == "text"
assert cfg.text == "hello world"
@@ -124,21 +137,25 @@ class TestWatermarkConfigFromDict:
assert cfg.scroll_speed == 100
def test_invalid_position_falls_back_to_bottom_right(self):
cfg = WatermarkConfig.from_dict({
"enabled": True,
"mode": "text",
"text": "test",
"position": "invalid_position",
})
cfg = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "test",
"position": "invalid_position",
}
)
assert cfg is not None
assert cfg.position == "bottom_right"
def test_default_values_applied(self):
cfg = WatermarkConfig.from_dict({
"enabled": True,
"mode": "text",
"text": "test",
})
cfg = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "test",
}
)
assert cfg is not None
assert cfg.position == "bottom_right"
assert cfg.opacity == 0.8