feat(assets): track generation usage review state

This commit is contained in:
Xiaoxia AI
2026-06-23 21:52:47 +08:00
parent 3b2be2f276
commit f2a9b0868b
8 changed files with 91 additions and 2 deletions
@@ -45,6 +45,9 @@ def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) ->
problem_assets = [asset for asset in assets if asset.status in {AssetStatus.ERROR, AssetStatus.UPLOADING, AssetStatus.PROCESSING}]
unclassified_assets = [asset for asset in ready_assets if asset.classification_status.value in {"pending", "failed"}]
risky_assets = [asset for asset in ready_assets if asset.quality_score is not None and asset.quality_score < 60]
used_assets = [asset for asset in ready_assets if int(asset.metadata.get("generation_use_count") or 0) > 0]
unused_assets = [asset for asset in ready_assets if int(asset.metadata.get("generation_use_count") or 0) == 0]
pending_review_assets = [asset for asset in ready_assets if asset.metadata.get("review_status") == "pending_review"]
total_duration = round(sum(float(asset.duration or 0) for asset in video_assets), 2)
estimated_video_count = max(0, min(len(video_assets), int(total_duration // 5) if total_duration else len(video_assets)))
@@ -124,6 +127,9 @@ def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) ->
AssetSmartViewItem(key="needs_attention", label="慎用素材", count=len(problem_assets) + len(risky_assets), description="导入未完成、失败或质量分偏低的素材"),
AssetSmartViewItem(key="unclassified", label="未分类素材", count=len(unclassified_assets), description="尚未完成分类或分类失败的 ready 素材"),
AssetSmartViewItem(key="recent", label="最近上传", count=min(len(assets), 10), description="最近进入素材库的素材,可用于快速复核"),
AssetSmartViewItem(key="unused", label="未使用素材", count=len(unused_assets), description="尚未参与生成的 ready 素材"),
AssetSmartViewItem(key="used", label="已使用素材", count=len(used_assets), description="已经参与过生成的素材"),
AssetSmartViewItem(key="pending_review", label="待复核素材", count=len(pending_review_assets), description="生成后待人工复核的素材"),
AssetSmartViewItem(key="voice", label="配音素材", count=len(voice_assets), description="可用于后续配音/旁白工作流的素材"),
]
@@ -139,6 +145,9 @@ def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) ->
voice_assets=len(voice_assets),
total_duration_seconds=total_duration,
estimated_video_count=estimated_video_count,
used_assets=len(used_assets),
unused_assets=len(unused_assets),
pending_review_assets=len(pending_review_assets),
smart_views=smart_views,
gaps=gaps,
)
+3
View File
@@ -27,5 +27,8 @@ class ProjectAssetDiagnosisResponse(BaseModel):
voice_assets: int
total_duration_seconds: float
estimated_video_count: int
used_assets: int
unused_assets: int
pending_review_assets: int
smart_views: list[AssetSmartViewItem]
gaps: list[AssetGapItem]
+3
View File
@@ -62,6 +62,9 @@ export interface ProjectAssetDiagnosis {
voice_assets: number;
total_duration_seconds: number;
estimated_video_count: number;
used_assets: number;
unused_assets: number;
pending_review_assets: number;
smart_views: Array<{ key: string; label: string; count: number; description: string }>;
gaps: Array<{ key: string; severity: 'critical' | 'warning' | 'info'; message: string; recommendation: string }>;
}
@@ -290,6 +290,12 @@ const ProjectAssets: React.FC = () => {
items = items.filter((item) => !item.metadata?.classification || item.classification_status === 'failed');
} else if (smartViewFilter === 'voice') {
items = items.filter((item) => item.mime_type?.startsWith('audio'));
} else if (smartViewFilter === 'unused') {
items = items.filter((item) => item.status === 'ready' && Number(item.metadata?.generation_use_count || 0) === 0);
} else if (smartViewFilter === 'used') {
items = items.filter((item) => Number(item.metadata?.generation_use_count || 0) > 0);
} else if (smartViewFilter === 'pending_review') {
items = items.filter((item) => item.metadata?.review_status === 'pending_review');
}
if (classificationFilter !== 'all') {
@@ -505,6 +511,9 @@ const ProjectAssets: React.FC = () => {
{ label: '慎用素材', value: 'needs_attention' },
{ label: '未分类素材', value: 'unclassified' },
{ label: '最近上传', value: 'recent' },
{ label: '未使用素材', value: 'unused' },
{ label: '已使用素材', value: 'used' },
{ label: '待复核素材', value: 'pending_review' },
{ label: '配音素材', value: 'voice' },
]}
onChange={setSmartViewFilter}
@@ -608,6 +617,9 @@ const ProjectAssets: React.FC = () => {
<Tag color="orange"> {diagnosisQuery.data.image_assets}</Tag>
<Tag color="cyan"> {diagnosisQuery.data.voice_assets}</Tag>
<Tag color="gold"> {diagnosisQuery.data.estimated_video_count}</Tag>
<Tag color="geekblue">使 {diagnosisQuery.data.unused_assets}</Tag>
<Tag color="lime">使 {diagnosisQuery.data.used_assets}</Tag>
<Tag color="volcano"> {diagnosisQuery.data.pending_review_assets}</Tag>
</Space>
<div style={{ marginTop: 12 }}>
<Space size={[8, 8]} wrap>
@@ -0,0 +1,10 @@
from datetime import datetime, timezone
def mark_asset_used_for_generation(asset) -> None:
asset.metadata = {
**asset.metadata,
"generation_use_count": int(asset.metadata.get("generation_use_count") or 0) + 1,
"last_used_at": datetime.now(timezone.utc).isoformat(),
"review_status": asset.metadata.get("review_status") or "pending_review",
}
@@ -8,6 +8,7 @@ from pathlib import Path
from urllib.parse import urlparse
import oss2
from worker_app.core.asset_usage import mark_asset_used_for_generation
from worker_app.celery_app import celery_app
from worker_app.db import SessionLocal
@@ -214,6 +215,9 @@ def generate_video(task_id: str) -> dict:
if source_downloaded:
_compose_from_asset(input_path, output_path)
source_asset = assets[0]
mark_asset_used_for_generation(source_asset)
asset_repo.update(source_asset)
else:
_create_fallback_clip(output_path, f"Xiaoxia Generated Video {task.id[:8]}")
+10 -2
View File
@@ -36,11 +36,13 @@ def test_asset_diagnosis_reports_missing_video_gap():
def test_asset_diagnosis_scores_ready_video_assets():
used_asset = _asset("video-1.mp4", "video/mp4", duration=8)
used_asset.metadata = {"generation_use_count": 1, "review_status": "pending_review"}
diagnosis = _build_diagnosis(
"workspace-1",
"project-1",
[
_asset("video-1.mp4", "video/mp4", duration=8),
used_asset,
_asset("video-2.mp4", "video/mp4", duration=8),
_asset("video-3.mov", "video/quicktime", duration=8),
_asset("voice.mp3", "audio/mpeg"),
@@ -51,7 +53,13 @@ def test_asset_diagnosis_scores_ready_video_assets():
assert diagnosis.video_assets == 3
assert diagnosis.voice_assets == 1
assert diagnosis.estimated_video_count == 3
assert {item.key: item.count for item in diagnosis.smart_views}["recommended"] == 3
assert diagnosis.used_assets == 1
assert diagnosis.unused_assets == 3
assert diagnosis.pending_review_assets == 1
smart_view_counts = {item.key: item.count for item in diagnosis.smart_views}
assert smart_view_counts["recommended"] == 3
assert smart_view_counts["used"] == 1
assert smart_view_counts["pending_review"] == 1
def test_asset_diagnosis_flags_unready_and_low_quality_assets():
+40
View File
@@ -0,0 +1,40 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "worker"))
from packages.domain import Asset, AssetStatus
from worker_app.core.asset_usage import mark_asset_used_for_generation
def _asset() -> Asset:
return Asset.create(
workspace_id="workspace-1",
project_id="project-1",
library_id="library-1",
name="video.mp4",
storage_key="uploads/video.mp4",
mime_type="video/mp4",
file_size=1024,
status=AssetStatus.READY,
)
def test_mark_asset_used_for_generation_sets_first_review_state():
asset = _asset()
mark_asset_used_for_generation(asset)
assert asset.metadata["generation_use_count"] == 1
assert asset.metadata["review_status"] == "pending_review"
assert asset.metadata["last_used_at"]
def test_mark_asset_used_for_generation_increments_without_overwriting_review_state():
asset = _asset()
asset.metadata = {"generation_use_count": 2, "review_status": "approved"}
mark_asset_used_for_generation(asset)
assert asset.metadata["generation_use_count"] == 3
assert asset.metadata["review_status"] == "approved"