style: auto-format with black + isort + prettier
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
This commit is contained in:
@@ -112,32 +112,44 @@ class TestDuplicateSegmentCreate:
|
||||
def test_create_similarity_boundary_values(self):
|
||||
# 0 和 100 都是合法的
|
||||
seg0 = DuplicateSegment.create(
|
||||
source_start=0.0, source_end=1.0,
|
||||
matched_video_id="v1", matched_video_name="v",
|
||||
matched_start=0.0, matched_end=1.0,
|
||||
source_start=0.0,
|
||||
source_end=1.0,
|
||||
matched_video_id="v1",
|
||||
matched_video_name="v",
|
||||
matched_start=0.0,
|
||||
matched_end=1.0,
|
||||
similarity=0.0,
|
||||
)
|
||||
assert seg0.similarity == 0.0
|
||||
|
||||
seg100 = DuplicateSegment.create(
|
||||
source_start=0.0, source_end=1.0,
|
||||
matched_video_id="v1", matched_video_name="v",
|
||||
matched_start=0.0, matched_end=1.0,
|
||||
source_start=0.0,
|
||||
source_end=1.0,
|
||||
matched_video_id="v1",
|
||||
matched_video_name="v",
|
||||
matched_start=0.0,
|
||||
matched_end=1.0,
|
||||
similarity=100.0,
|
||||
)
|
||||
assert seg100.similarity == 100.0
|
||||
|
||||
def test_create_unique_ids(self):
|
||||
seg1 = DuplicateSegment.create(
|
||||
source_start=0.0, source_end=1.0,
|
||||
matched_video_id="v1", matched_video_name="v",
|
||||
matched_start=0.0, matched_end=1.0,
|
||||
source_start=0.0,
|
||||
source_end=1.0,
|
||||
matched_video_id="v1",
|
||||
matched_video_name="v",
|
||||
matched_start=0.0,
|
||||
matched_end=1.0,
|
||||
similarity=50.0,
|
||||
)
|
||||
seg2 = DuplicateSegment.create(
|
||||
source_start=0.0, source_end=1.0,
|
||||
matched_video_id="v1", matched_video_name="v",
|
||||
matched_start=0.0, matched_end=1.0,
|
||||
source_start=0.0,
|
||||
source_end=1.0,
|
||||
matched_video_id="v1",
|
||||
matched_video_name="v",
|
||||
matched_start=0.0,
|
||||
matched_end=1.0,
|
||||
similarity=50.0,
|
||||
)
|
||||
assert seg1.id != seg2.id
|
||||
@@ -213,20 +225,19 @@ class TestDuplicationRecordCreate:
|
||||
|
||||
class TestDuplicationRecordStatus:
|
||||
def test_mark_processing(self):
|
||||
record = DuplicationRecord.create(
|
||||
user_id="u1", filename="t.mp4", file_size=100, storage_key="k1"
|
||||
)
|
||||
record = DuplicationRecord.create(user_id="u1", filename="t.mp4", file_size=100, storage_key="k1")
|
||||
record.mark_processing()
|
||||
assert record.status == "processing"
|
||||
|
||||
def test_mark_completed(self):
|
||||
record = DuplicationRecord.create(
|
||||
user_id="u1", filename="t.mp4", file_size=100, storage_key="k1"
|
||||
)
|
||||
record = DuplicationRecord.create(user_id="u1", filename="t.mp4", file_size=100, storage_key="k1")
|
||||
seg = DuplicateSegment.create(
|
||||
source_start=0.0, source_end=5.0,
|
||||
matched_video_id="v1", matched_video_name="v",
|
||||
matched_start=0.0, matched_end=5.0,
|
||||
source_start=0.0,
|
||||
source_end=5.0,
|
||||
matched_video_id="v1",
|
||||
matched_video_name="v",
|
||||
matched_start=0.0,
|
||||
matched_end=5.0,
|
||||
similarity=90.0,
|
||||
)
|
||||
record.mark_completed(duplicate_rate=25.5, duplicate_count=3, segments=[seg])
|
||||
@@ -236,26 +247,20 @@ class TestDuplicationRecordStatus:
|
||||
assert len(record.segments) == 1
|
||||
|
||||
def test_mark_completed_invalid_rate_raises(self):
|
||||
record = DuplicationRecord.create(
|
||||
user_id="u1", filename="t.mp4", file_size=100, storage_key="k1"
|
||||
)
|
||||
record = DuplicationRecord.create(user_id="u1", filename="t.mp4", file_size=100, storage_key="k1")
|
||||
with pytest.raises(ValueError, match="duplicate_rate must be between 0 and 100"):
|
||||
record.mark_completed(duplicate_rate=-1, duplicate_count=0, segments=[])
|
||||
with pytest.raises(ValueError, match="duplicate_rate must be between 0 and 100"):
|
||||
record.mark_completed(duplicate_rate=101, duplicate_count=0, segments=[])
|
||||
|
||||
def test_mark_failed(self):
|
||||
record = DuplicationRecord.create(
|
||||
user_id="u1", filename="t.mp4", file_size=100, storage_key="k1"
|
||||
)
|
||||
record = DuplicationRecord.create(user_id="u1", filename="t.mp4", file_size=100, storage_key="k1")
|
||||
record.mark_failed("网络超时")
|
||||
assert record.status == "failed"
|
||||
assert record.error_message == "网络超时"
|
||||
|
||||
def test_can_retry_only_failed(self):
|
||||
record = DuplicationRecord.create(
|
||||
user_id="u1", filename="t.mp4", file_size=100, storage_key="k1"
|
||||
)
|
||||
record = DuplicationRecord.create(user_id="u1", filename="t.mp4", file_size=100, storage_key="k1")
|
||||
assert record.can_retry() is False # pending
|
||||
|
||||
record.mark_processing()
|
||||
@@ -265,25 +270,27 @@ class TestDuplicationRecordStatus:
|
||||
assert record.can_retry() is True # failed
|
||||
|
||||
seg = DuplicateSegment.create(
|
||||
source_start=0.0, source_end=1.0,
|
||||
matched_video_id="v1", matched_video_name="v",
|
||||
matched_start=0.0, matched_end=1.0,
|
||||
source_start=0.0,
|
||||
source_end=1.0,
|
||||
matched_video_id="v1",
|
||||
matched_video_name="v",
|
||||
matched_start=0.0,
|
||||
matched_end=1.0,
|
||||
similarity=50.0,
|
||||
)
|
||||
record2 = DuplicationRecord.create(
|
||||
user_id="u1", filename="t.mp4", file_size=100, storage_key="k1"
|
||||
)
|
||||
record2 = DuplicationRecord.create(user_id="u1", filename="t.mp4", file_size=100, storage_key="k1")
|
||||
record2.mark_completed(10.0, 1, [seg])
|
||||
assert record2.can_retry() is False # completed
|
||||
|
||||
def test_reset_for_retry(self):
|
||||
record = DuplicationRecord.create(
|
||||
user_id="u1", filename="t.mp4", file_size=100, storage_key="k1"
|
||||
)
|
||||
record = DuplicationRecord.create(user_id="u1", filename="t.mp4", file_size=100, storage_key="k1")
|
||||
seg = DuplicateSegment.create(
|
||||
source_start=0.0, source_end=1.0,
|
||||
matched_video_id="v1", matched_video_name="v",
|
||||
matched_start=0.0, matched_end=1.0,
|
||||
source_start=0.0,
|
||||
source_end=1.0,
|
||||
matched_video_id="v1",
|
||||
matched_video_name="v",
|
||||
matched_start=0.0,
|
||||
matched_end=1.0,
|
||||
similarity=50.0,
|
||||
)
|
||||
record.mark_completed(50.0, 2, [seg])
|
||||
|
||||
@@ -140,15 +140,11 @@ class TestEditPlanClipStateTransitions:
|
||||
|
||||
class TestEditPlanClipProperties:
|
||||
def test_end_time(self):
|
||||
clip = EditPlanClip.create(
|
||||
plan_id="p1", clip_type="v", order=1, start_time=10.0, duration=5.0
|
||||
)
|
||||
clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, start_time=10.0, duration=5.0)
|
||||
assert clip.end_time == 15.0
|
||||
|
||||
def test_end_time_zero_duration(self):
|
||||
clip = EditPlanClip.create(
|
||||
plan_id="p1", clip_type="v", order=1, start_time=5.0, duration=0.0
|
||||
)
|
||||
clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, start_time=5.0, duration=0.0)
|
||||
assert clip.end_time == 5.0
|
||||
|
||||
def test_has_asset_true(self):
|
||||
|
||||
+70
-42
@@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.subtitle import SubtitleWord, SubtitleSegment, SubtitleTimeline
|
||||
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline, SubtitleWord
|
||||
|
||||
|
||||
class TestSubtitleWord:
|
||||
@@ -46,26 +46,32 @@ class TestSubtitleTimeline:
|
||||
assert tl.total_duration == 0.0
|
||||
|
||||
def test_segment_count(self):
|
||||
tl = SubtitleTimeline(segments=[
|
||||
SubtitleSegment(text="第一段", start=0.0, end=2.0),
|
||||
SubtitleSegment(text="第二段", start=2.0, end=5.0),
|
||||
])
|
||||
tl = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="第一段", start=0.0, end=2.0),
|
||||
SubtitleSegment(text="第二段", start=2.0, end=5.0),
|
||||
]
|
||||
)
|
||||
assert tl.segment_count == 2
|
||||
assert tl.total_chars == 6
|
||||
|
||||
def test_total_chars(self):
|
||||
tl = SubtitleTimeline(segments=[
|
||||
SubtitleSegment(text="abc", start=0, end=1),
|
||||
SubtitleSegment(text="defg", start=1, end=2),
|
||||
])
|
||||
tl = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="abc", start=0, end=1),
|
||||
SubtitleSegment(text="defg", start=1, end=2),
|
||||
]
|
||||
)
|
||||
assert tl.total_chars == 7
|
||||
|
||||
|
||||
class TestMergeShortSegments:
|
||||
def test_single_segment_no_change(self):
|
||||
tl = SubtitleTimeline(segments=[
|
||||
SubtitleSegment(text="你好", start=0.0, end=1.0),
|
||||
])
|
||||
tl = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="你好", start=0.0, end=1.0),
|
||||
]
|
||||
)
|
||||
result = tl.merge_short_segments(min_chars=8)
|
||||
assert result.segment_count == 1
|
||||
assert result.segments[0].text == "你好"
|
||||
@@ -76,12 +82,14 @@ class TestMergeShortSegments:
|
||||
assert result.segment_count == 0
|
||||
|
||||
def test_merge_short_segments(self):
|
||||
tl = SubtitleTimeline(segments=[
|
||||
SubtitleSegment(text="你好", start=0.0, end=1.0),
|
||||
SubtitleSegment(text="今天", start=1.0, end=2.0),
|
||||
SubtitleSegment(text="天气", start=2.0, end=3.0),
|
||||
SubtitleSegment(text="真好", start=3.0, end=4.0),
|
||||
])
|
||||
tl = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="你好", start=0.0, end=1.0),
|
||||
SubtitleSegment(text="今天", start=1.0, end=2.0),
|
||||
SubtitleSegment(text="天气", start=2.0, end=3.0),
|
||||
SubtitleSegment(text="真好", start=3.0, end=4.0),
|
||||
]
|
||||
)
|
||||
result = tl.merge_short_segments(min_chars=4)
|
||||
# 每段2字,min=4,应该每2段合并
|
||||
assert result.segment_count == 2
|
||||
@@ -94,26 +102,40 @@ class TestMergeShortSegments:
|
||||
|
||||
def test_remaining_merged_to_last(self):
|
||||
# 3段,每段2字,min=5 → 前5字合并,剩余1字并到最后
|
||||
tl = SubtitleTimeline(segments=[
|
||||
SubtitleSegment(text="一二", start=0, end=1),
|
||||
SubtitleSegment(text="三四", start=1, end=2),
|
||||
SubtitleSegment(text="五", start=2, end=3),
|
||||
])
|
||||
tl = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="一二", start=0, end=1),
|
||||
SubtitleSegment(text="三四", start=1, end=2),
|
||||
SubtitleSegment(text="五", start=2, end=3),
|
||||
]
|
||||
)
|
||||
result = tl.merge_short_segments(min_chars=5)
|
||||
assert result.segment_count == 1
|
||||
assert result.segments[0].text == "一二三四五"
|
||||
|
||||
def test_merge_with_words(self):
|
||||
tl = SubtitleTimeline(segments=[
|
||||
SubtitleSegment(text="你好", start=0.0, end=1.0, words=[
|
||||
SubtitleWord(text="你", start=0.0, end=0.5),
|
||||
SubtitleWord(text="好", start=0.5, end=1.0),
|
||||
]),
|
||||
SubtitleSegment(text="世界", start=1.0, end=2.0, words=[
|
||||
SubtitleWord(text="世", start=1.0, end=1.5),
|
||||
SubtitleWord(text="界", start=1.5, end=2.0),
|
||||
]),
|
||||
])
|
||||
tl = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(
|
||||
text="你好",
|
||||
start=0.0,
|
||||
end=1.0,
|
||||
words=[
|
||||
SubtitleWord(text="你", start=0.0, end=0.5),
|
||||
SubtitleWord(text="好", start=0.5, end=1.0),
|
||||
],
|
||||
),
|
||||
SubtitleSegment(
|
||||
text="世界",
|
||||
start=1.0,
|
||||
end=2.0,
|
||||
words=[
|
||||
SubtitleWord(text="世", start=1.0, end=1.5),
|
||||
SubtitleWord(text="界", start=1.5, end=2.0),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
result = tl.merge_short_segments(min_chars=10)
|
||||
assert result.segment_count == 1
|
||||
assert len(result.segments[0].words) == 4
|
||||
@@ -121,26 +143,32 @@ class TestMergeShortSegments:
|
||||
|
||||
class TestSplitLongSegments:
|
||||
def test_short_segments_no_split(self):
|
||||
tl = SubtitleTimeline(segments=[
|
||||
SubtitleSegment(text="短文本", start=0.0, end=1.0),
|
||||
])
|
||||
tl = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="短文本", start=0.0, end=1.0),
|
||||
]
|
||||
)
|
||||
result = tl.split_long_segments(max_chars=20)
|
||||
assert result.segment_count == 1
|
||||
|
||||
def test_split_by_punctuation(self):
|
||||
text = "今天天气真好。我们出去玩吧!"
|
||||
tl = SubtitleTimeline(segments=[
|
||||
SubtitleSegment(text=text, start=0.0, end=5.0),
|
||||
])
|
||||
tl = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text=text, start=0.0, end=5.0),
|
||||
]
|
||||
)
|
||||
result = tl.split_long_segments(max_chars=10)
|
||||
assert result.segment_count >= 2
|
||||
# 合并起来应该等于原文
|
||||
assert "".join(s.text for s in result.segments) == text
|
||||
|
||||
def test_split_preserves_time_order(self):
|
||||
tl = SubtitleTimeline(segments=[
|
||||
SubtitleSegment(text="一二三四五六七八九十。十一二三四五六七八九十。", start=0.0, end=10.0),
|
||||
])
|
||||
tl = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(text="一二三四五六七八九十。十一二三四五六七八九十。", start=0.0, end=10.0),
|
||||
]
|
||||
)
|
||||
result = tl.split_long_segments(max_chars=10)
|
||||
# 时间应该是递增的
|
||||
for i in range(len(result.segments) - 1):
|
||||
|
||||
@@ -40,16 +40,18 @@ class TestTtsConfigParse:
|
||||
assert config.speed == 1.0
|
||||
|
||||
def test_parse_enabled_true_with_all_fields(self):
|
||||
config = TtsConfig.parse({
|
||||
"enabled": True,
|
||||
"voice_id": "female_warm",
|
||||
"speed": 1.5,
|
||||
"pitch": 2.0,
|
||||
"volume": 0.9,
|
||||
"text": "你好世界",
|
||||
"align_mode": "subtitle",
|
||||
"overlap_mode": "mix",
|
||||
})
|
||||
config = TtsConfig.parse(
|
||||
{
|
||||
"enabled": True,
|
||||
"voice_id": "female_warm",
|
||||
"speed": 1.5,
|
||||
"pitch": 2.0,
|
||||
"volume": 0.9,
|
||||
"text": "你好世界",
|
||||
"align_mode": "subtitle",
|
||||
"overlap_mode": "mix",
|
||||
}
|
||||
)
|
||||
assert config.enabled is True
|
||||
assert config.voice_id == "female_warm"
|
||||
assert config.speed == 1.5
|
||||
|
||||
@@ -64,6 +64,7 @@ class TestVerificationCodeStatus:
|
||||
code = VerificationCode.create(recipient="test@example.com", code_type="email_bind", ttl_seconds=0)
|
||||
# 0秒TTL,立即可能过期(有极小概率因时间差没过)
|
||||
import time
|
||||
|
||||
time.sleep(0.01)
|
||||
assert code.is_expired is True
|
||||
assert code.is_valid is False
|
||||
|
||||
Reference in New Issue
Block a user