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
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:
@@ -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="参与评分的候选素材总数")
|
||||
|
||||
@@ -1441,12 +1441,6 @@
|
||||
|
||||
/* textarea removed in Q5 */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.xx-smart-match-tip {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary, #94a3b8);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user