07aa03da24
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 23s
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
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 43s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 17s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m12s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m37s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m38s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m3s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m8s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m12s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m17s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m23s
AI Code Review / AI Code Review (pull_request) Successful in 6m26s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 16m23s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 3s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
208 lines
7.1 KiB
Python
208 lines
7.1 KiB
Python
"""Tests for enhanced dedup: text + structure dimensions (Issue #P2-后端3)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from unittest.mock import MagicMock
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mock heavy deps before importing dedup module
|
|
# ---------------------------------------------------------------------------
|
|
_ORIGINAL_MODULES = dict(sys.modules)
|
|
_MOCKED_MODULE_NAMES: list[str] = []
|
|
|
|
|
|
def _mock_if_absent(name: str, mock_obj=None):
|
|
"""仅在模块不在 sys.modules 中时注入 mock,并记录以便清理。"""
|
|
if name not in sys.modules:
|
|
sys.modules[name] = mock_obj if mock_obj is not None else MagicMock()
|
|
_MOCKED_MODULE_NAMES.append(name)
|
|
|
|
|
|
# Mock heavy deps
|
|
_mock_if_absent("ffmpeg")
|
|
_mock_if_absent("ffmpeg.utils")
|
|
_mock_if_absent("worker_app.celery_app")
|
|
_mock_if_absent("worker_app.db")
|
|
_mock_if_absent("packages.adapters.sqlalchemy_impl.generated_video_repository")
|
|
_mock_if_absent("packages.adapters.sqlalchemy_impl.models")
|
|
_mock_if_absent("packages.shared.storage")
|
|
|
|
# Mock cv2 and numpy if not available
|
|
try:
|
|
import cv2 as _cv2
|
|
|
|
if not isinstance(_cv2, MagicMock):
|
|
_HAS_CV2 = True
|
|
else:
|
|
_HAS_CV2 = False
|
|
except ImportError:
|
|
_HAS_CV2 = False
|
|
_mock_if_absent("cv2")
|
|
_mock_if_absent("numpy")
|
|
|
|
import pytest
|
|
|
|
from apps.worker.video_processing.dedup import (
|
|
STRUCTURE_WEIGHT,
|
|
TEXT_WEIGHT,
|
|
VISUAL_WEIGHT,
|
|
compute_structure_similarity,
|
|
compute_text_similarity,
|
|
)
|
|
|
|
|
|
class TestTextSimilarity:
|
|
"""Tests for compute_text_similarity."""
|
|
|
|
def test_identical_texts(self):
|
|
"""相同文本返回 1.0。"""
|
|
assert compute_text_similarity("你好世界", "你好世界") == 1.0
|
|
|
|
def test_empty_texts(self):
|
|
"""都为空返回 1.0。"""
|
|
assert compute_text_similarity("", "") == 1.0
|
|
|
|
def test_one_empty(self):
|
|
"""一个为空返回 0.0。"""
|
|
assert compute_text_similarity("你好", "") == 0.0
|
|
assert compute_text_similarity("", "你好") == 0.0
|
|
|
|
def test_completely_different(self):
|
|
"""完全不同文本返回低相似度。"""
|
|
sim = compute_text_similarity("你好世界", "abcdefgh")
|
|
assert sim < 0.3
|
|
|
|
def test_partial_overlap(self):
|
|
"""部分重叠文本返回中等相似度。"""
|
|
sim = compute_text_similarity("今天天气真好", "今天天气不错")
|
|
assert 0.3 < sim < 0.9
|
|
|
|
def test_case_insensitive(self):
|
|
"""英文大小写不敏感。"""
|
|
sim = compute_text_similarity("Hello World", "hello world")
|
|
assert sim == 1.0
|
|
|
|
def test_whitespace_ignored(self):
|
|
"""空白字符被忽略。"""
|
|
sim = compute_text_similarity("你好 世界", "你好世界")
|
|
assert sim == 1.0
|
|
|
|
def test_punctuation_ignored(self):
|
|
"""标点符号被忽略。"""
|
|
sim = compute_text_similarity("你好,世界!", "你好世界")
|
|
assert sim == 1.0
|
|
|
|
def test_long_texts(self):
|
|
"""长文本也能计算。"""
|
|
t1 = "这是一段很长的配音文本,用于测试文案查重功能"
|
|
t2 = "这是一段较长的配音文字,用于测试文案去重功能"
|
|
sim = compute_text_similarity(t1, t2)
|
|
assert 0.0 <= sim <= 1.0
|
|
|
|
|
|
class TestStructureSimilarity:
|
|
"""Tests for compute_structure_similarity."""
|
|
|
|
def test_identical_structures(self):
|
|
"""完全相同结构返回 1.0。"""
|
|
clips = [
|
|
{"clip_type": "video", "duration": 5.0},
|
|
{"clip_type": "title", "duration": 2.0},
|
|
{"clip_type": "video", "duration": 8.0},
|
|
]
|
|
assert compute_structure_similarity(clips, clips) == 1.0
|
|
|
|
def test_empty_clips(self):
|
|
"""都为空返回 1.0。"""
|
|
assert compute_structure_similarity([], []) == 1.0
|
|
|
|
def test_one_empty(self):
|
|
"""一个为空返回 0.0。"""
|
|
clips = [{"clip_type": "video", "duration": 5.0}]
|
|
assert compute_structure_similarity(clips, []) == 0.0
|
|
assert compute_structure_similarity([], clips) == 0.0
|
|
|
|
def test_different_count(self):
|
|
"""片段数不同,相似度降低。"""
|
|
clips1 = [
|
|
{"clip_type": "video", "duration": 5.0},
|
|
{"clip_type": "title", "duration": 2.0},
|
|
]
|
|
clips2 = [
|
|
{"clip_type": "video", "duration": 5.0},
|
|
{"clip_type": "title", "duration": 2.0},
|
|
{"clip_type": "video", "duration": 3.0},
|
|
{"clip_type": "title", "duration": 1.0},
|
|
]
|
|
sim = compute_structure_similarity(clips1, clips2)
|
|
assert 0.0 < sim < 0.8
|
|
|
|
def test_different_types(self):
|
|
"""片段类型不同,类型相似度低。"""
|
|
clips1 = [
|
|
{"clip_type": "video", "duration": 5.0},
|
|
{"clip_type": "video", "duration": 3.0},
|
|
]
|
|
clips2 = [
|
|
{"clip_type": "title", "duration": 5.0},
|
|
{"clip_type": "title", "duration": 3.0},
|
|
]
|
|
sim = compute_structure_similarity(clips1, clips2)
|
|
assert sim <= 0.6 # 类型全部不同,但数量和时长相同贡献 0.6
|
|
|
|
def test_different_duration_distribution(self):
|
|
"""时长分布不同,时长相似度低。"""
|
|
clips1 = [
|
|
{"clip_type": "video", "duration": 10.0}, # 占比 80%
|
|
{"clip_type": "title", "duration": 2.5}, # 占比 20%
|
|
]
|
|
clips2 = [
|
|
{"clip_type": "video", "duration": 2.0}, # 占比 20%
|
|
{"clip_type": "title", "duration": 8.0}, # 占比 80%
|
|
]
|
|
sim = compute_structure_similarity(clips1, clips2)
|
|
assert 0.7 < sim < 0.9 # 类型相同但时长分布不同,sim=0.82
|
|
|
|
def test_similar_structure(self):
|
|
"""相似结构返回较高相似度。"""
|
|
clips1 = [
|
|
{"clip_type": "video", "duration": 5.0},
|
|
{"clip_type": "title", "duration": 2.0},
|
|
{"clip_type": "video", "duration": 8.0},
|
|
]
|
|
clips2 = [
|
|
{"clip_type": "video", "duration": 5.5},
|
|
{"clip_type": "title", "duration": 2.2},
|
|
{"clip_type": "video", "duration": 7.5},
|
|
]
|
|
sim = compute_structure_similarity(clips1, clips2)
|
|
assert sim > 0.8
|
|
|
|
def test_single_clip(self):
|
|
"""单片段也能计算。"""
|
|
clips1 = [{"clip_type": "video", "duration": 10.0}]
|
|
clips2 = [{"clip_type": "video", "duration": 12.0}]
|
|
sim = compute_structure_similarity(clips1, clips2)
|
|
assert sim > 0.5 # 类型相同,数量相同,只是时长不同
|
|
|
|
|
|
class TestDimensionWeights:
|
|
"""Tests for dimension weight constants."""
|
|
|
|
def test_weights_sum_to_one(self):
|
|
"""多维度权重之和为 1.0。"""
|
|
assert abs(VISUAL_WEIGHT + TEXT_WEIGHT + STRUCTURE_WEIGHT - 1.0) < 1e-9
|
|
|
|
def test_visual_weight_is_half(self):
|
|
"""视觉权重为 0.5。"""
|
|
assert VISUAL_WEIGHT == 0.5
|
|
|
|
def test_text_weight_is_quarter(self):
|
|
"""文案权重为 0.25。"""
|
|
assert TEXT_WEIGHT == 0.25
|
|
|
|
def test_structure_weight_is_quarter(self):
|
|
"""结构权重为 0.25。"""
|
|
assert STRUCTURE_WEIGHT == 0.25
|