style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 40s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 45s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m43s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m15s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m54s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m58s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m29s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m17s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m7s
AI Code Review / AI Code Review (pull_request) Failing after 4m42s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m33s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m44s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 8m20s
CI/CD Pipeline / CI Gate (pull_request) Successful in 11s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1014h55m31s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1014h55m32s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1014h55m32s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1014h55m33s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1015h4m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1015h4m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1015h4m32s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1015h4m33s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1015h4m34s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 1015h28m49s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1015h28m50s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1015h37m48s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1015h37m50s

This commit is contained in:
CI Bot
2026-08-05 00:22:43 +00:00
parent f86498b702
commit 00d19ccc9b
5 changed files with 64 additions and 43 deletions
+1 -1
View File
@@ -127,4 +127,4 @@ class SmartMatchResponse(BaseModel):
"""智能选素材响应。"""
items: list[SmartMatchItem]
total_candidates: int = Field(default=0, ge=0, description="参与评分的候选素材总数")
total_candidates: int = Field(default=0, ge=0, description="参与评分的候选素材总数")
-6
View File
@@ -1441,12 +1441,6 @@
/* textarea removed in Q5 */
.xx-smart-match-tip {
font-size: 12px;
color: var(--text-tertiary, #94a3b8);
+1 -1
View File
@@ -24,9 +24,9 @@ from .entities import (
from .generated_video import GeneratedVideo
from .generation_task import GenerationTask, GenerationTaskStatus
from .job import Job, JobStatus, JobType
from .smart_match import SmartMatchResult, score_asset, smart_select_assets
from .tag import Tag
from .template_clip_config import ClipType, TemplateClipConfig, TransitionEffect
from .smart_match import SmartMatchResult, smart_select_assets, score_asset
from .title_library import TitleLibraryItem
from .voice_library import VoiceLibraryItem
+1 -3
View File
@@ -133,9 +133,7 @@ def smart_select_assets(
按得分降序排列的 SmartMatchResult 列表
"""
# Step 1: 过滤 ready 状态
ready_assets = [
a for a in assets if _get_enum_value(a, "status") == "ready"
]
ready_assets = [a for a in assets if _get_enum_value(a, "status") == "ready"]
# Step 2: 按 kind 过滤
if kind:
+61 -32
View File
@@ -1,24 +1,26 @@
"""Tests for packages/domain/smart_match.py — 统一智能选素材算法。"""
import pytest
from datetime import datetime, timezone, timedelta
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any
import pytest
from packages.domain.smart_match import (
SmartMatchResult,
_diversity_select,
_duration_bucket,
score_asset,
smart_select_assets,
_duration_bucket,
_diversity_select,
SmartMatchResult,
)
# ── Fixtures ─────────────────────────────────────────────────────────────────
@dataclass
class FakeAsset:
"""Minimal Asset-like object for testing."""
id: str
project_id: str = "proj-1"
library_id: str = "lib-1"
@@ -46,6 +48,7 @@ NOW = datetime(2026, 8, 5, 12, 0, 0, tzinfo=timezone.utc)
# ── score_asset tests ────────────────────────────────────────────────────────
class TestScoreAsset:
def test_high_quality_asset_scores_high(self):
asset = FakeAsset(id="a1", quality_score=95, duration=15)
@@ -58,9 +61,13 @@ class TestScoreAsset:
def test_low_quality_asset_scores_lower(self):
asset_good = FakeAsset(id="a1", quality_score=95, duration=15)
asset_bad = FakeAsset(id="a2", quality_score=20, duration=15,
created_at=NOW - timedelta(days=60),
metadata={"generation_use_count": 10})
asset_bad = FakeAsset(
id="a2",
quality_score=20,
duration=15,
created_at=NOW - timedelta(days=60),
metadata={"generation_use_count": 10},
)
score_good, _ = score_asset(asset_good, now=NOW)
score_bad, _ = score_asset(asset_bad, now=NOW)
assert score_bad < score_good
@@ -117,6 +124,7 @@ class TestScoreAsset:
# ── _duration_bucket tests ───────────────────────────────────────────────────
class TestDurationBucket:
def test_short(self):
assert _duration_bucket(5) == "short"
@@ -138,6 +146,7 @@ class TestDurationBucket:
# ── smart_select_assets tests ────────────────────────────────────────────────
class TestSmartSelectAssets:
def test_filters_non_ready_assets(self):
assets = [
@@ -160,10 +169,7 @@ class TestSmartSelectAssets:
assert results[0].asset.id == "a1"
def test_respects_limit(self):
assets = [
FakeAsset(id=f"a{i}", quality_score=50 + i, duration=15)
for i in range(20)
]
assets = [FakeAsset(id=f"a{i}", quality_score=50 + i, duration=15) for i in range(20)]
results = smart_select_assets(assets, limit=5)
assert len(results) == 5
@@ -205,10 +211,7 @@ class TestSmartSelectAssets:
assert len(buckets) >= 2 # at least 2 different duration buckets
def test_no_limit_returns_all(self):
assets = [
FakeAsset(id=f"a{i}", quality_score=50 + i, duration=15)
for i in range(10)
]
assets = [FakeAsset(id=f"a{i}", quality_score=50 + i, duration=15) for i in range(10)]
results = smart_select_assets(assets, limit=None)
assert len(results) == 10
@@ -231,6 +234,7 @@ class TestSmartSelectAssets:
def test_str_enum_status_handled(self):
"""Test that StrEnum-like status objects are handled correctly."""
class StrEnumLike:
def __init__(self, value):
self.value = value
@@ -243,6 +247,7 @@ class TestSmartSelectAssets:
# ── _diversity_select tests ──────────────────────────────────────────────────
class TestDiversitySelect:
def test_picks_from_all_buckets(self):
results = [
@@ -290,6 +295,7 @@ from app.dependencies import (
get_asset_repository,
get_project_repository,
)
from packages.domain import (
Asset,
AssetLibrary,
@@ -325,7 +331,7 @@ class _StubAssetRepo:
result = [a for a in self._assets if a.library_id == lid]
if status:
result = [a for a in result if (a.status.value if hasattr(a.status, "value") else a.status) in status]
return result[skip:skip + limit]
return result[skip : skip + limit]
def _make_app(asset_repo, lib_repo, proj_repo):
@@ -346,24 +352,41 @@ def _make_app(asset_repo, lib_repo, proj_repo):
def _make_test_data():
project = Project(id="proj-1", name="Test", owner_user_id="user-1")
library = AssetLibrary(
id="lib-1", project_id="proj-1", name="Videos",
id="lib-1",
project_id="proj-1",
name="Videos",
kind=AssetLibraryKind.VIDEO,
)
assets = [
Asset.create(
project_id="proj-1", library_id="lib-1", name="v1.mp4",
storage_key="k1", mime_type="video/mp4", quality_score=90,
duration=15, status=AssetStatus.READY,
project_id="proj-1",
library_id="lib-1",
name="v1.mp4",
storage_key="k1",
mime_type="video/mp4",
quality_score=90,
duration=15,
status=AssetStatus.READY,
),
Asset.create(
project_id="proj-1", library_id="lib-1", name="v2.mp4",
storage_key="k2", mime_type="video/mp4", quality_score=50,
duration=25, status=AssetStatus.READY,
project_id="proj-1",
library_id="lib-1",
name="v2.mp4",
storage_key="k2",
mime_type="video/mp4",
quality_score=50,
duration=25,
status=AssetStatus.READY,
),
Asset.create(
project_id="proj-1", library_id="lib-1", name="v3.mp4",
storage_key="k3", mime_type="video/mp4", quality_score=30,
duration=60, status=AssetStatus.READY,
project_id="proj-1",
library_id="lib-1",
name="v3.mp4",
storage_key="k3",
mime_type="video/mp4",
quality_score=30,
duration=60,
status=AssetStatus.READY,
),
]
return project, library, assets
@@ -409,8 +432,12 @@ class TestSmartMatchEndpoint:
project, library, assets = _make_test_data()
# Add an image asset
img_asset = Asset.create(
project_id="proj-1", library_id="lib-1", name="img.png",
storage_key="k4", mime_type="image/png", quality_score=95,
project_id="proj-1",
library_id="lib-1",
name="img.png",
storage_key="k4",
mime_type="image/png",
quality_score=95,
status=AssetStatus.READY,
)
assets.append(img_asset)
@@ -439,7 +466,9 @@ class TestSmartMatchEndpoint:
def test_empty_library_returns_empty_items(self):
project = Project(id="proj-1", name="Test", owner_user_id="user-1")
library = AssetLibrary(
id="lib-1", project_id="proj-1", name="Empty",
id="lib-1",
project_id="proj-1",
name="Empty",
kind=AssetLibraryKind.VIDEO,
)
app = _make_app(
@@ -452,4 +481,4 @@ class TestSmartMatchEndpoint:
assert resp.status_code == 200
data = resp.json()
assert data["items"] == []
assert data["total_candidates"] == 0
assert data["total_candidates"] == 0