feat(api): 成品视频接口补全查重字段 duplicate_rate/visual_similarity/match_count #1660 (#1678)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 6s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 14s
CI/CD Pipeline / Check push changed paths (push) Successful in 15s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 34s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 36s
CI/CD Pipeline / Build Staging API Image (push) Successful in 37s
CI/CD Pipeline / Integration Tests (push) Successful in 1m53s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m17s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 39s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Validate - Style (push) Successful in 3m4s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m53s
CI/CD Pipeline / Validate - Security (push) Successful in 6m19s
AI Code Review / AI Code Review (pull_request) Failing after 6m31s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m9s
CI/CD Pipeline / Unit Tests (push) Successful in 8m47s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 6m8s
CI/CD Pipeline / Canary Release to Production (push) Failing after 302h12m2s
CI/CD Pipeline / CI Gate (push) Failing after 302h12m6s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 302h12m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 302h17m26s
CI/CD Pipeline / Build Production Web Image (push) Failing after 302h12m6s
CI/CD Pipeline / Build Production API Image (push) Failing after 302h12m6s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 302h17m51s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 302h17m55s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 302h17m57s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 302h17m57s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 302h17m58s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 302h17m58s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 302h17m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 302h18m0s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 302h18m39s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 302h18m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 302h18m45s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 302h18m46s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 302h18m46s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 302h19m44s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 302h19m45s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 302h19m46s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 302h19m48s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 302h19m50s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 302h20m28s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 302h20m28s
CI/CD Pipeline / Frontend Lint (push) Failing after 302h20m52s
CI/CD Pipeline / PR Build Web Image (push) Failing after 302h21m7s
CI/CD Pipeline / PR Build API Image (push) Failing after 302h21m7s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 302h21m8s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 302h21m11s
CI/CD Pipeline / Deploy Production (push) Failing after 302h46m27s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 302h52m22s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 302h53m7s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 302h54m9s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 302h54m12s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 302h54m53s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 302h55m31s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 6s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 14s
CI/CD Pipeline / Check push changed paths (push) Successful in 15s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 34s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 36s
CI/CD Pipeline / Build Staging API Image (push) Successful in 37s
CI/CD Pipeline / Integration Tests (push) Successful in 1m53s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m17s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 39s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Validate - Style (push) Successful in 3m4s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m53s
CI/CD Pipeline / Validate - Security (push) Successful in 6m19s
AI Code Review / AI Code Review (pull_request) Failing after 6m31s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m9s
CI/CD Pipeline / Unit Tests (push) Successful in 8m47s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 6m8s
CI/CD Pipeline / Canary Release to Production (push) Failing after 302h12m2s
CI/CD Pipeline / CI Gate (push) Failing after 302h12m6s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 302h12m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 302h17m26s
CI/CD Pipeline / Build Production Web Image (push) Failing after 302h12m6s
CI/CD Pipeline / Build Production API Image (push) Failing after 302h12m6s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 302h17m51s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 302h17m55s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 302h17m57s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 302h17m57s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 302h17m58s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 302h17m58s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 302h17m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 302h18m0s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 302h18m39s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 302h18m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 302h18m45s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 302h18m46s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 302h18m46s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 302h19m44s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 302h19m45s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 302h19m46s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 302h19m48s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 302h19m50s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 302h20m28s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 302h20m28s
CI/CD Pipeline / Frontend Lint (push) Failing after 302h20m52s
CI/CD Pipeline / PR Build Web Image (push) Failing after 302h21m7s
CI/CD Pipeline / PR Build API Image (push) Failing after 302h21m7s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 302h21m8s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 302h21m11s
CI/CD Pipeline / Deploy Production (push) Failing after 302h46m27s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 302h52m22s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 302h53m7s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 302h54m9s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 302h54m12s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 302h54m53s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 302h55m31s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #1678.
This commit is contained in:
@@ -92,6 +92,9 @@ def _to_generated_video_response(item, download_url: str | None = None) -> Gener
|
||||
height=item.height,
|
||||
fps=item.fps,
|
||||
download_url=download_url,
|
||||
duplicate_rate=getattr(item, "duplicate_rate", None),
|
||||
visual_similarity=getattr(item, "visual_similarity", None),
|
||||
match_count=getattr(item, "match_count", None),
|
||||
)
|
||||
|
||||
|
||||
@@ -137,7 +140,6 @@ def _select_assets_from_library(
|
||||
return [a.id for a in ready_video_assets]
|
||||
|
||||
|
||||
|
||||
def _writeback_edit_plan_config(
|
||||
plan_id: str,
|
||||
task_id: str,
|
||||
@@ -162,7 +164,7 @@ def _writeback_edit_plan_config(
|
||||
current_config = plan_model.config if isinstance(plan_model.config, dict) else {}
|
||||
merged = dict(current_config)
|
||||
merged["generation_task_id"] = task_id
|
||||
|
||||
|
||||
# 检查标题是否发生变化,如果变化则清除 cover 字段强制重新生成封面
|
||||
if title_config:
|
||||
old_title_config = merged.get("title_config", {}) or {}
|
||||
@@ -174,10 +176,12 @@ def _writeback_edit_plan_config(
|
||||
del merged["cover"]
|
||||
logger.info(
|
||||
"[生成任务] 标题变化,清除旧封面: plan_id=%s old_title=%s new_title=%s",
|
||||
plan_id, old_title_text, new_title_text,
|
||||
plan_id,
|
||||
old_title_text,
|
||||
new_title_text,
|
||||
)
|
||||
merged["title_config"] = title_config
|
||||
|
||||
|
||||
plan_model.config = merged
|
||||
db.commit()
|
||||
logger.info(
|
||||
|
||||
@@ -53,6 +53,8 @@ def _to_video_response(item, storage: OSSStorageService | None = None) -> VideoI
|
||||
download_url=download_url,
|
||||
generated_at=format_utc_datetime(item.generated_at) if hasattr(item, "generated_at") else "",
|
||||
duplicate_rate=getattr(item, "duplicate_rate", None),
|
||||
visual_similarity=getattr(item, "visual_similarity", None),
|
||||
match_count=getattr(item, "match_count", None),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ class GeneratedVideoResponse(BaseModel):
|
||||
review_status: str = "pending_review"
|
||||
generation_params: dict = Field(default_factory=dict)
|
||||
download_url: str | None = None
|
||||
# #1660 查重率(百分比 0~100)/ 视觉相似度(0~1)/ 匹配帧数
|
||||
duplicate_rate: float | None = None
|
||||
visual_similarity: float | None = None
|
||||
match_count: int | None = None
|
||||
|
||||
|
||||
class GeneratedVideoDownloadUrlResponse(BaseModel):
|
||||
|
||||
@@ -22,7 +22,10 @@ class VideoItemResponse(BaseModel):
|
||||
generation_params: dict = Field(default_factory=dict)
|
||||
download_url: str | None = None
|
||||
generated_at: str = ""
|
||||
# #1660 查重率(百分比 0~100)/ 视觉相似度(0~1)/ 匹配帧数
|
||||
duplicate_rate: float | None = None
|
||||
visual_similarity: float | None = None
|
||||
match_count: int | None = None
|
||||
|
||||
|
||||
class ListVideosResponse(BaseModel):
|
||||
|
||||
@@ -31,8 +31,8 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
is_duplicate=video.is_duplicate,
|
||||
duplicate_of=video.duplicate_of,
|
||||
duplicate_rate=video.duplicate_rate,
|
||||
match_count=getattr(video, "match_count", 0),
|
||||
visual_similarity=getattr(video, "visual_similarity", 0.0),
|
||||
match_count=getattr(video, "match_count", None),
|
||||
visual_similarity=getattr(video, "visual_similarity", None),
|
||||
generated_at=video.generated_at,
|
||||
created_at=video.created_at,
|
||||
)
|
||||
@@ -64,8 +64,8 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
is_duplicate=getattr(model, "is_duplicate", False),
|
||||
duplicate_of=getattr(model, "duplicate_of", None),
|
||||
duplicate_rate=getattr(model, "duplicate_rate", None),
|
||||
match_count=getattr(model, "match_count", 0) or 0,
|
||||
visual_similarity=getattr(model, "visual_similarity", 0.0) or 0.0,
|
||||
match_count=getattr(model, "match_count", None),
|
||||
visual_similarity=getattr(model, "visual_similarity", None),
|
||||
generated_at=model.generated_at,
|
||||
created_at=model.created_at,
|
||||
)
|
||||
@@ -81,8 +81,8 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
model.is_duplicate = video.is_duplicate
|
||||
model.duplicate_of = video.duplicate_of
|
||||
model.duplicate_rate = video.duplicate_rate
|
||||
model.match_count = getattr(video, "match_count", 0)
|
||||
model.visual_similarity = getattr(video, "visual_similarity", 0.0)
|
||||
model.match_count = getattr(video, "match_count", None)
|
||||
model.visual_similarity = getattr(video, "visual_similarity", None)
|
||||
self.session.add(model)
|
||||
self.session.commit()
|
||||
return video
|
||||
@@ -232,8 +232,8 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
is_duplicate=getattr(model, "is_duplicate", False),
|
||||
duplicate_of=getattr(model, "duplicate_of", None),
|
||||
duplicate_rate=getattr(model, "duplicate_rate", None),
|
||||
match_count=getattr(model, "match_count", 0) or 0,
|
||||
visual_similarity=getattr(model, "visual_similarity", 0.0) or 0.0,
|
||||
match_count=getattr(model, "match_count", None),
|
||||
visual_similarity=getattr(model, "visual_similarity", None),
|
||||
generated_at=model.generated_at,
|
||||
created_at=model.created_at,
|
||||
)
|
||||
|
||||
@@ -27,8 +27,8 @@ class GeneratedVideo:
|
||||
is_duplicate: bool = False
|
||||
duplicate_of: str | None = None
|
||||
duplicate_rate: float | None = None
|
||||
match_count: int = 0
|
||||
visual_similarity: float = 0.0
|
||||
match_count: int | None = None
|
||||
visual_similarity: float | None = None
|
||||
generated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""#1660 成品视频 API 查重字段透传测试。
|
||||
|
||||
覆盖两套响应构造路径:
|
||||
- routes/videos.py::_to_video_response -> VideoItemResponse (/videos 列表)
|
||||
- routes/generation_tasks.py::_to_generated_video_response -> GeneratedVideoResponse
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from app.api.routes.generation_tasks import _to_generated_video_response
|
||||
from app.api.routes.videos import _to_video_response
|
||||
from app.schemas.generated_video import GeneratedVideoResponse
|
||||
from app.schemas.video_center import VideoItemResponse
|
||||
|
||||
|
||||
def _make_item(**overrides):
|
||||
base = dict(
|
||||
id="v1",
|
||||
project_id="p1",
|
||||
generation_task_id="t1",
|
||||
name="成片",
|
||||
file_url="oss://bucket/v1.mp4",
|
||||
file_size=1024,
|
||||
duration=12.5,
|
||||
thumbnail_url=None,
|
||||
width=1080,
|
||||
height=1920,
|
||||
fps=30.0,
|
||||
status="completed",
|
||||
review_status="pending_review",
|
||||
generation_params={},
|
||||
generated_at=None,
|
||||
duplicate_rate=None,
|
||||
match_count=None,
|
||||
visual_similarity=None,
|
||||
)
|
||||
base.update(overrides)
|
||||
return SimpleNamespace(**base)
|
||||
|
||||
|
||||
class TestVideoItemResponseDupFields:
|
||||
def test_passes_through_all_three_fields(self):
|
||||
item = _make_item(duplicate_rate=42.5, match_count=7, visual_similarity=0.83)
|
||||
resp = _to_video_response(item, storage=None)
|
||||
assert isinstance(resp, VideoItemResponse)
|
||||
assert resp.duplicate_rate == 42.5
|
||||
assert resp.match_count == 7
|
||||
assert resp.visual_similarity == 0.83
|
||||
|
||||
def test_legacy_video_without_fields_returns_none(self):
|
||||
"""老数据/实体无查重字段时保持 None(前端自动隐藏),不报错。"""
|
||||
item = SimpleNamespace(
|
||||
id="v2",
|
||||
project_id="p1",
|
||||
generation_task_id="t2",
|
||||
name="老视频",
|
||||
file_url="oss://bucket/v2.mp4",
|
||||
file_size=1,
|
||||
duration=1.0,
|
||||
thumbnail_url=None,
|
||||
width=720,
|
||||
height=1280,
|
||||
fps=24.0,
|
||||
status="completed",
|
||||
review_status="pending_review",
|
||||
generation_params={},
|
||||
)
|
||||
resp = _to_video_response(item, storage=None)
|
||||
assert resp.duplicate_rate is None
|
||||
assert resp.match_count is None
|
||||
assert resp.visual_similarity is None
|
||||
|
||||
def test_explicit_none_values_kept(self):
|
||||
item = _make_item()
|
||||
resp = _to_video_response(item, storage=None)
|
||||
assert resp.duplicate_rate is None
|
||||
assert resp.match_count is None
|
||||
assert resp.visual_similarity is None
|
||||
|
||||
def test_zero_match_count_is_valid_value(self):
|
||||
"""计算后确无匹配:match_count=0 / visual_similarity=0.0 是合法值,不能变 None。"""
|
||||
item = _make_item(duplicate_rate=0.0, match_count=0, visual_similarity=0.0)
|
||||
resp = _to_video_response(item, storage=None)
|
||||
assert resp.match_count == 0
|
||||
assert resp.visual_similarity == 0.0
|
||||
|
||||
|
||||
class TestGeneratedVideoResponseDupFields:
|
||||
def test_passes_through_all_three_fields(self):
|
||||
item = _make_item(duplicate_rate=15.2, match_count=3, visual_similarity=0.61)
|
||||
resp = _to_generated_video_response(item, download_url="https://dl/x")
|
||||
assert isinstance(resp, GeneratedVideoResponse)
|
||||
assert resp.duplicate_rate == 15.2
|
||||
assert resp.match_count == 3
|
||||
assert resp.visual_similarity == 0.61
|
||||
assert resp.download_url == "https://dl/x"
|
||||
|
||||
def test_missing_fields_default_none(self):
|
||||
item = SimpleNamespace(
|
||||
id="v3",
|
||||
project_id="p1",
|
||||
generation_task_id="t3",
|
||||
name="x",
|
||||
file_url="oss://x",
|
||||
file_size=1,
|
||||
duration=1.0,
|
||||
thumbnail_url=None,
|
||||
width=720,
|
||||
height=1280,
|
||||
fps=24.0,
|
||||
)
|
||||
resp = _to_generated_video_response(item)
|
||||
assert resp.duplicate_rate is None
|
||||
assert resp.match_count is None
|
||||
assert resp.visual_similarity is None
|
||||
|
||||
def test_storage_failure_falls_back_to_file_url(self):
|
||||
storage = MagicMock()
|
||||
storage.get_download_url.side_effect = RuntimeError("oss down")
|
||||
item = _make_item()
|
||||
resp = _to_video_response(item, storage=storage)
|
||||
assert resp.download_url == item.file_url
|
||||
Reference in New Issue
Block a user