diff --git a/apps/api/app/api/routes/edit_plans.py b/apps/api/app/api/routes/edit_plans.py index 5b074fce4..513c6b860 100644 --- a/apps/api/app/api/routes/edit_plans.py +++ b/apps/api/app/api/routes/edit_plans.py @@ -410,3 +410,124 @@ def get_generation_status( generation_task_id=gen_status["generation_task_id"], clips=clip_items, ) + + +# ── Timeline / Scene 端点(P2-6) ───────────────────────────────────────────── + + +class TimelineSceneResponse(BaseModel): + """时间线场景""" + + scene: str = Field(..., description="场景描述") + time: str = Field(..., description="时间范围,如 \"0:00 - 0:05\"") + duration: float = Field(..., ge=0, description="时长(秒)") + color: str = Field(..., description="展示颜色") + clip_id: str = Field(default="", description="关联的片段 ID") + clip_type: str = Field(default="", description="片段类型") + + +class TimelineResponse(BaseModel): + """时间线响应""" + + plan_id: str + total_duration: float + scenes: List[TimelineSceneResponse] + + +# clip_type → 颜色映射 +_CLIP_TYPE_COLORS = { + "intro": "#6366f1", + "title": "#6366f1", + "product": "#818cf8", + "showcase": "#10b981", + "scene": "#10b981", + "subtitle": "#f59e0b", + "text": "#f59e0b", + "cta": "#ef4444", + "outro": "#ef4444", + "voiceover": "#8b5cf6", + "transition": "#64748b", +} + +_DEFAULT_COLOR = "#6366f1" + + +def _format_time(seconds: float) -> str: + """将秒数格式化为 M:SS""" + m = int(seconds) // 60 + s = int(seconds) % 60 + return f"{m}:{s:02d}" + + +def _clip_type_to_scene_label(clip_type: str, text_content: str) -> str: + """根据 clip_type 和 text_content 生成场景描述""" + type_labels = { + "intro": "开场", + "title": "标题", + "product": "产品展示", + "showcase": "场景展示", + "scene": "场景", + "subtitle": "字幕", + "text": "文字", + "cta": "结尾 CTA", + "outro": "结尾", + "voiceover": "配音", + "transition": "转场", + } + label = type_labels.get(clip_type, clip_type or "片段") + if text_content: + # 截取前 20 个字符作为副标题 + short = text_content[:20].strip() + if short: + return f"{label} - {short}" + return label + + +@router.get( + "/{plan_id}/timeline", + response_model=TimelineResponse, +) +def get_plan_timeline( + plan_id: str, + db: Session = Depends(get_db_session), + current_user: AuthenticatedUser = Depends(get_current_user), +) -> TimelineResponse: + """获取剪辑计划的时间线场景数据 + + 返回按计划片段排序的时间线场景列表,供前端 GeneratePage 渲染使用。 + """ + svc = EditPlanService(db) + plan = svc.get_plan_or_raise(plan_id) + + clips = svc.list_clips(plan_id=plan_id, skip=0, limit=200) + # 按 order 排序 + clips.sort(key=lambda c: c.order) + + scenes: List[TimelineSceneResponse] = [] + current_time = 0.0 + + for clip in clips: + start = current_time + end = start + clip.duration + color = _CLIP_TYPE_COLORS.get(clip.clip_type, _DEFAULT_COLOR) + scene_label = _clip_type_to_scene_label(clip.clip_type, clip.text_content) + + scenes.append( + TimelineSceneResponse( + scene=scene_label, + time=f"{_format_time(start)} - {_format_time(end)}", + duration=clip.duration, + color=color, + clip_id=clip.id, + clip_type=clip.clip_type, + ) + ) + current_time = end + + total_duration = sum(s.duration for s in scenes) or plan.total_duration + + return TimelineResponse( + plan_id=plan_id, + total_duration=total_duration, + scenes=scenes, + )