feat(assets): track generation usage review state
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]}")
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user