完善二期清单
This commit is contained in:
@@ -11,6 +11,7 @@ from .models import (
|
||||
Project,
|
||||
ProjectStage,
|
||||
ScriptSegment,
|
||||
ScriptTemplate,
|
||||
ScriptVersion,
|
||||
StoryboardFrame,
|
||||
StoryboardShot,
|
||||
@@ -34,6 +35,41 @@ def _asset_preview_url(asset) -> str:
|
||||
return AssetFileSerializer().get_preview_url(primary) if primary else ""
|
||||
|
||||
|
||||
_VIDEO_SUFFIXES = (".mp4", ".mov", ".webm", ".m4v")
|
||||
|
||||
|
||||
def is_playable_video(asset) -> bool:
|
||||
"""主文件是不是真能塞进 <video> 播的视频。演示种子数据里有「导出任务标成功、挂的却是一张 PNG 海报」
|
||||
的成片,直接下发会让播放按钮打开一个放不出来的图。"""
|
||||
if asset is None:
|
||||
return False
|
||||
files = list(asset.files.all())
|
||||
primary = next((f for f in files if f.is_primary), files[0] if files else None)
|
||||
if primary is None:
|
||||
return False
|
||||
if (primary.content_type or "").startswith("video/"):
|
||||
return True
|
||||
return (primary.object_key or "").lower().endswith(_VIDEO_SUFFIXES)
|
||||
|
||||
|
||||
def _final_video_url(project) -> str:
|
||||
"""合成成片(最新一次成功的拼接导出)的可播放 URL;没合成过返回空串。
|
||||
在 Python 侧筛选而不再查库:列表页用 Prefetch 预取(见 ProjectViewSet.get_queryset),
|
||||
详情页 timeline 只有一条、export_jobs 数量极少,直接遍历不会放大查询。"""
|
||||
timeline = getattr(project, "timeline", None)
|
||||
if timeline is None:
|
||||
return ""
|
||||
succeeded = [
|
||||
job for job in timeline.export_jobs.all()
|
||||
if job.status == ExportJob.Status.SUCCEEDED and is_playable_video(job.output_asset)
|
||||
]
|
||||
if not succeeded:
|
||||
return ""
|
||||
dated = [job for job in succeeded if job.created_at is not None]
|
||||
latest = max(dated, key=lambda job: job.created_at) if dated else succeeded[-1]
|
||||
return _asset_preview_url(latest.output_asset)
|
||||
|
||||
|
||||
class ProjectStageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ProjectStage
|
||||
@@ -321,18 +357,24 @@ class ProjectListSerializer(serializers.ModelSerializer):
|
||||
cover_preview_url = serializers.SerializerMethodField()
|
||||
script_version_count = serializers.IntegerField(read_only=True, default=0)
|
||||
video_segment_count = serializers.IntegerField(read_only=True, default=0)
|
||||
# 合成成片地址:项目列表的播放按钮据此直接播成片(没合成过为空 → 退回进流水线)
|
||||
final_video_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = [
|
||||
"id", "name", "product", "product_title", "cover_preview_url",
|
||||
"status", "current_stage", "script_version_count", "video_segment_count",
|
||||
"final_video_url",
|
||||
"is_deleted", "purged_at", "created_at", "updated_at",
|
||||
]
|
||||
|
||||
def get_cover_preview_url(self, obj) -> str:
|
||||
return _asset_preview_url(getattr(obj.product, "cover_asset", None)) if obj.product_id else ""
|
||||
|
||||
def get_final_video_url(self, obj) -> str:
|
||||
return _final_video_url(obj)
|
||||
|
||||
|
||||
class ProjectSerializer(serializers.ModelSerializer):
|
||||
stages = ProjectStageSerializer(many=True, read_only=True)
|
||||
@@ -342,6 +384,8 @@ class ProjectSerializer(serializers.ModelSerializer):
|
||||
storyboard_versions = StoryboardVersionSerializer(many=True, read_only=True) # 过渡期保留(旧整版,新生成不再写)
|
||||
storyboard_shots = StoryboardShotSerializer(many=True, read_only=True)
|
||||
timeline = TimelineSerializer(read_only=True)
|
||||
# 合成成片地址(最新一次成功拼接):视频阶段的「播放成片 / 下载成片」直接用它
|
||||
final_video_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
@@ -363,7 +407,64 @@ class ProjectSerializer(serializers.ModelSerializer):
|
||||
"storyboard_shots",
|
||||
"video_segments",
|
||||
"timeline",
|
||||
"final_video_url",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "status", "current_stage", "failure_reason", "is_deleted", "purged_at", "created_at", "updated_at"]
|
||||
|
||||
def get_final_video_url(self, obj) -> str:
|
||||
return _final_video_url(obj)
|
||||
|
||||
|
||||
class ScriptTemplateSerializer(serializers.ModelSerializer):
|
||||
"""套路模板 · 列表与详情共用。写入只开放 name(其余字段由存模板端点从脚本抽)。"""
|
||||
|
||||
outline_text = serializers.SerializerMethodField()
|
||||
source_project_name = serializers.CharField(source="source_project.name", default="", read_only=True)
|
||||
shot_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ScriptTemplate
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"presentation_format",
|
||||
"video_structure",
|
||||
"persona",
|
||||
"total_duration",
|
||||
"outline",
|
||||
"outline_text",
|
||||
"shot_count",
|
||||
"cta",
|
||||
"scenes",
|
||||
"source_project",
|
||||
"source_project_name",
|
||||
"usage_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"presentation_format",
|
||||
"video_structure",
|
||||
"persona",
|
||||
"total_duration",
|
||||
"outline",
|
||||
"cta",
|
||||
"scenes",
|
||||
"source_project",
|
||||
"usage_count",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def get_outline_text(self, obj) -> str:
|
||||
from .services.templates import render_outline_text
|
||||
|
||||
return render_outline_text(
|
||||
{"outline": obj.outline, "cta": obj.cta}
|
||||
)
|
||||
|
||||
def get_shot_count(self, obj) -> int:
|
||||
return len(obj.outline or [])
|
||||
|
||||
Reference in New Issue
Block a user