diff --git a/apps/api/app/schemas/asset.py b/apps/api/app/schemas/asset.py index 2618963d8..b839a9e4c 100755 --- a/apps/api/app/schemas/asset.py +++ b/apps/api/app/schemas/asset.py @@ -127,4 +127,4 @@ class SmartMatchResponse(BaseModel): """智能选素材响应。""" items: list[SmartMatchItem] - total_candidates: int = Field(default=0, ge=0, description="参与评分的候选素材总数") \ No newline at end of file + total_candidates: int = Field(default=0, ge=0, description="参与评分的候选素材总数") diff --git a/apps/web/src/pages/generate/generate.css b/apps/web/src/pages/generate/generate.css index 27d8698fc..1804f3105 100644 --- a/apps/web/src/pages/generate/generate.css +++ b/apps/web/src/pages/generate/generate.css @@ -1441,12 +1441,6 @@ /* textarea removed in Q5 */ - - - - - - .xx-smart-match-tip { font-size: 12px; color: var(--text-tertiary, #94a3b8); diff --git a/packages/domain/__init__.py b/packages/domain/__init__.py index e35cdb14c..0a2aedece 100755 --- a/packages/domain/__init__.py +++ b/packages/domain/__init__.py @@ -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 diff --git a/packages/domain/smart_match.py b/packages/domain/smart_match.py index 12e31fc24..44f760452 100755 --- a/packages/domain/smart_match.py +++ b/packages/domain/smart_match.py @@ -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: diff --git a/tests/unit/test_smart_match.py b/tests/unit/test_smart_match.py index 0cbf50306..0fd0b3be3 100755 --- a/tests/unit/test_smart_match.py +++ b/tests/unit/test_smart_match.py @@ -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 \ No newline at end of file + assert data["total_candidates"] == 0