完成极速成品和脚本优化

This commit is contained in:
Azmat@qq.com
2026-08-25 11:13:07 +08:00
parent 00fc454db7
commit e2ec2d14af
46 changed files with 4734 additions and 432 deletions
+132 -1
View File
@@ -10,6 +10,7 @@ from .models import (
ExportJob,
Project,
ProjectStage,
QuickCreateJob,
ScriptSegment,
ScriptTemplate,
ScriptVersion,
@@ -329,6 +330,132 @@ class ExportJobSerializer(serializers.ModelSerializer):
read_only_fields = fields
class QuickCreateJobSerializer(serializers.ModelSerializer):
"""极速成片轮询响应:只下发用户需要的四阶段进度和最终可播结果。"""
project_id = serializers.UUIDField(source="project.id", read_only=True)
product_id = serializers.UUIDField(source="project.product_id", read_only=True)
product_name = serializers.CharField(source="project.product.title", read_only=True)
title = serializers.CharField(source="project.name", read_only=True)
product_images = serializers.SerializerMethodField()
phase_index = serializers.SerializerMethodField()
settings = serializers.SerializerMethodField()
result = serializers.SerializerMethodField()
class Meta:
model = QuickCreateJob
fields = [
"id",
"project_id",
"product_id",
"product_name",
"title",
"product_images",
"status",
"phase",
"phase_index",
"settings",
"progress",
"message",
"error_message",
"result",
"created_at",
"updated_at",
"completed_at",
]
read_only_fields = fields
def get_product_images(self, obj) -> list[dict]:
product = getattr(obj.project, "product", None)
if product is None:
return []
return [
{
"asset_id": str(image.asset_id),
"url": _asset_preview_url(image.asset),
}
for image in product.images.all()
]
def get_phase_index(self, obj) -> int:
return {
QuickCreateJob.Phase.PRODUCT: 0,
QuickCreateJob.Phase.SCRIPT: 1,
QuickCreateJob.Phase.ASSETS: 2,
QuickCreateJob.Phase.PRODUCTION: 3,
QuickCreateJob.Phase.COMPLETE: 3,
}.get(obj.phase, 0)
def get_settings(self, obj) -> dict:
wizard = dict((obj.project.metadata or {}).get("wizard") or {})
return {
"aspect_ratio": str(wizard.get("aspect_ratio") or "9:16"),
"resolution": str(wizard.get("resolution") or "720p").lower(),
"total_duration": int(wizard.get("total_duration") or 15),
"video_model_config_id": str(wizard.get("video_model_config_id") or ""),
"video_model_name": str(wizard.get("video_model_name") or ""),
"video_model_label": str(wizard.get("video_model_label") or ""),
}
def get_result(self, obj) -> dict | None:
if obj.status != QuickCreateJob.Status.SUCCEEDED:
return None
project = obj.project
settings = self.get_settings(obj)
video_url = _final_video_url(project)
segments = list(project.video_segments.all())
if not video_url and len(segments) == 1:
version = segments[0].adopted_version
if version is not None and is_playable_video(version.asset):
video_url = _asset_preview_url(version.asset)
first_shot = next(
(shot for shot in project.storyboard_shots.all() if shot.adopted_version_id),
None,
)
poster_url = _asset_preview_url(first_shot.adopted_version.asset) if first_shot else ""
script = next((version for version in project.script_versions.all() if version.is_adopted), None)
script_meta = (script.metadata or {}) if script is not None else {}
format_label = {"oral": "口播展示", "drama": "短剧演绎", "vlog": "Vlog种草"}.get(
script_meta.get("presentation_format"), "智能推荐"
)
structure_label = {
"pain": "痛点解决",
"contrast": "前后对比",
"review": "测评验证",
"scene": "场景种草",
}.get(script_meta.get("video_structure"), "智能推荐")
groups = list(project.base_asset_groups.all())
person = next((group for group in groups if group.kind == BaseAssetGroup.Kind.PERSON and group.adopted_asset_id), None)
scene = next((group for group in groups if group.kind == BaseAssetGroup.Kind.SCENE and group.adopted_asset_id), None)
duration = sum(segment.target_duration_seconds for segment in segments)
video_segments = []
for segment in sorted(segments, key=lambda item: item.sort_order):
version = segment.adopted_version
clip_url = _asset_preview_url(version.asset) if version is not None and is_playable_video(version.asset) else ""
video_segments.append(
{
"id": str(segment.id),
"sort_order": segment.sort_order,
"duration_seconds": segment.target_duration_seconds or 15,
"video_url": clip_url,
"poster_url": poster_url,
}
)
return {
"video_url": video_url,
"poster_url": poster_url,
"duration_seconds": duration or 15,
"aspect_ratio": settings["aspect_ratio"],
"resolution": settings["resolution"],
"video_model": settings["video_model_label"] or settings["video_model_name"],
"structure": structure_label,
"presentation": format_label,
"person": ((person.metadata or {}).get("label") or "自动匹配") if person else "自动匹配",
"scene": ((scene.metadata or {}).get("label") or "自动匹配") if scene else "自动匹配",
"video_segments": video_segments,
}
class ScriptSegmentSerializer(serializers.ModelSerializer):
class Meta:
model = ScriptSegment
@@ -359,13 +486,14 @@ class ProjectListSerializer(serializers.ModelSerializer):
video_segment_count = serializers.IntegerField(read_only=True, default=0)
# 合成成片地址:项目列表的播放按钮据此直接播成片(没合成过为空 → 退回进流水线)
final_video_url = serializers.SerializerMethodField()
quick_create = 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",
"final_video_url", "quick_create",
"is_deleted", "purged_at", "created_at", "updated_at",
]
@@ -375,6 +503,9 @@ class ProjectListSerializer(serializers.ModelSerializer):
def get_final_video_url(self, obj) -> str:
return _final_video_url(obj)
def get_quick_create(self, obj) -> bool:
return bool((obj.metadata or {}).get("quick_create"))
class ProjectSerializer(serializers.ModelSerializer):
stages = ProjectStageSerializer(many=True, read_only=True)