From 49b5a0834dc1bd3a93fd398206644b7620ecdb14 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 13 Aug 2026 18:26:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(cover):=20=E5=B0=81=E9=9D=A2?= =?UTF-8?q?=E7=94=9F=E6=88=90API=E4=BB=8Etemplates=5Feditor=E8=A7=A3?= =?UTF-8?q?=E8=80=A6=E5=88=B0generation=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/app/api/router.py | 6 ++ .../cover.py => generation_cover.py} | 48 ++++++++-- .../api/routes/templates_editor/__init__.py | 3 - .../api/routes/templates_editor/schemas.py | 23 ----- apps/web/src/api/generation/cover.ts | 35 +++++++ apps/web/src/api/generation/index.ts | 3 + .../web/src/api/template-editor/aiFeatures.ts | 14 +-- apps/web/src/api/template-editor/index.ts | 7 +- apps/web/src/api/template-editor/types.ts | 24 +---- .../src/pages/generate/hooks/useStep6Cover.ts | 2 +- apps/web/src/test/api/edit-plans.test.ts | 16 ---- tests/unit/test_generation_cover.py | 95 +++++++++++++++++++ 12 files changed, 182 insertions(+), 94 deletions(-) rename apps/api/app/api/routes/{templates_editor/cover.py => generation_cover.py} (82%) mode change 100755 => 100644 create mode 100644 apps/web/src/api/generation/cover.ts create mode 100644 tests/unit/test_generation_cover.py diff --git a/apps/api/app/api/router.py b/apps/api/app/api/router.py index 517063242..3b5f70faf 100755 --- a/apps/api/app/api/router.py +++ b/apps/api/app/api/router.py @@ -9,6 +9,7 @@ from app.api.routes.cover_templates import router as cover_templates_router from app.api.routes.duplication import router as duplication_router from app.api.routes.feature_flags import router as feature_flags_router from app.api.routes.generation_preview import router as generation_preview_router +from app.api.routes.generation_cover import router as generation_cover_router from app.api.routes.generation_tasks import router as generation_tasks_router from app.api.routes.health import router as health_check_router from app.api.routes.ingest_jobs import router as ingest_jobs_router @@ -98,6 +99,11 @@ api_router.include_router( prefix="/generation", tags=["Generation"], ) +api_router.include_router( + generation_cover_router, + prefix="/generation", + tags=["Generation"], +) api_router.include_router( titles_router, prefix="/titles", diff --git a/apps/api/app/api/routes/templates_editor/cover.py b/apps/api/app/api/routes/generation_cover.py old mode 100755 new mode 100644 similarity index 82% rename from apps/api/app/api/routes/templates_editor/cover.py rename to apps/api/app/api/routes/generation_cover.py index 59a5a6ad9..033de871f --- a/apps/api/app/api/routes/templates_editor/cover.py +++ b/apps/api/app/api/routes/generation_cover.py @@ -1,18 +1,22 @@ -"""封面管理路由. +"""封面生成路由 — Generation 模块. 端点: - POST /generate-cover AI 生成封面(从预览视频中抽帧) + +挂载路径: /api/v1/generation/generate-cover """ from __future__ import annotations import logging +from typing import Any, List, Optional from app.auth import AuthenticatedUser, get_current_user from app.dependencies import get_db_session, get_generated_video_repository from app.services.edit_plan_service import EditPlanService from app.services.edit_template_service import EditTemplateService -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query +from pydantic import BaseModel, Field from sqlalchemy.orm import Session from packages.adapters.sqlalchemy_impl.generation_task_repository import ( @@ -21,20 +25,44 @@ from packages.adapters.sqlalchemy_impl.generation_task_repository import ( from packages.application import ListGeneratedVideosByTaskUseCase from packages.domain.config_schemas import normalize_plan_config -from .dependencies import get_draft_plan_id, get_editor_services -from .schemas import ( - GenerateCoverRequest, - GenerateCoverResponse, -) +from .templates_editor.dependencies import get_draft_plan_id, get_editor_services logger = logging.getLogger(__name__) -router = APIRouter(tags=["Template Editor"]) +router = APIRouter(tags=["Generation"]) + + +# ── Schemas ────────────────────────────────────────────────────────────── + + +class GenerateCoverRequest(BaseModel): + """AI 封面生成请求体""" + + asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表(确定视频来源)") + cover_type: str = Field( + default="ai_frame", + description="封面类型: ai_frame / manual / upload / ai_regenerate", + ) + frame_time: Optional[float] = Field( + default=None, + ge=0.0, + description="手动选帧时间点(秒),仅 cover_type=manual 时有效", + ) + + +class GenerateCoverResponse(BaseModel): + """AI 封面生成响应体""" + + plan_id: str = Field(..., description="剪辑计划 ID") + cover: dict[str, Any] = Field(..., description="封面数据(type / image_url / frame_time 等)") + + +# ── Route ──────────────────────────────────────────────────────────────── @router.post("/generate-cover", response_model=GenerateCoverResponse) -def editor_generate_cover( - template_id: str, +def generate_cover( body: GenerateCoverRequest, + template_id: str = Query(..., description="模板 ID"), plan_id: str = Depends(get_draft_plan_id), services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services), db: Session = Depends(get_db_session), diff --git a/apps/api/app/api/routes/templates_editor/__init__.py b/apps/api/app/api/routes/templates_editor/__init__.py index c56f175b3..0ad746410 100755 --- a/apps/api/app/api/routes/templates_editor/__init__.py +++ b/apps/api/app/api/routes/templates_editor/__init__.py @@ -11,7 +11,6 @@ - bgm.py: BGM 管理 - effects.py: 转场 + 滤镜 - export.py: 导出配置 -- cover.py: 封面管理 + AI 生成封面 - subtitles.py: 字幕管理 - ai_features.py: AI 推荐 - generation.py: 生成(触发/进度/记录) @@ -31,7 +30,6 @@ from .adjustments import router as adjustments_router from .ai_features import router as ai_features_router from .bgm import router as bgm_router from .clips import router as clips_router -from .cover import router as cover_router from .dependencies import get_draft_plan_id, get_editor_services # noqa: F401 from .draft import router as draft_router from .effects import router as effects_router @@ -51,7 +49,6 @@ _sub_routers = [ bgm_router, effects_router, export_router, - cover_router, subtitles_router, ai_features_router, generation_router, diff --git a/apps/api/app/api/routes/templates_editor/schemas.py b/apps/api/app/api/routes/templates_editor/schemas.py index 0eea48fac..af09bbb26 100755 --- a/apps/api/app/api/routes/templates_editor/schemas.py +++ b/apps/api/app/api/routes/templates_editor/schemas.py @@ -99,29 +99,6 @@ class AIRecommendResponse(BaseModel): confidence: float = Field(..., ge=0.0, le=1.0, description="AI 推荐置信度 (0~1)") -# ── 封面生成 ──────────────────────────────────────────────────────────────── - - -class GenerateCoverRequest(BaseModel): - """AI 封面生成请求体""" - - asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表(确定视频来源)") - cover_type: str = Field( - default="ai_frame", - description="封面类型: ai_frame / manual / upload / ai_regenerate", - ) - frame_time: Optional[float] = Field( - default=None, - ge=0.0, - description="手动选帧时间点(秒),仅 cover_type=manual 时有效", - ) - - -class GenerateCoverResponse(BaseModel): - """AI 封面生成响应体""" - - plan_id: str = Field(..., description="剪辑计划 ID") - cover: dict[str, Any] = Field(..., description="封面数据(type / image_url / frame_time 等)") # ── BGM ──────────────────────────────────────────────────────────────────── diff --git a/apps/web/src/api/generation/cover.ts b/apps/web/src/api/generation/cover.ts new file mode 100644 index 000000000..0ee28db21 --- /dev/null +++ b/apps/web/src/api/generation/cover.ts @@ -0,0 +1,35 @@ +import apiClient from "../client" + +export interface GenerateCoverRequest { + asset_ids: string[] + cover_type?: "ai_frame" | "manual" | "upload" | "ai_regenerate" + frame_time?: number +} + +export interface GenerateCoverResponse { + plan_id: string + cover: { + scheme?: string + asset_id?: string + frame_time?: number + image_url?: string + thumbnail_url?: string + [key: string]: unknown + } +} + +/** AI 生成封面 — 从预览视频中抽帧 */ +export async function generateCover( + templateId: string, + data: GenerateCoverRequest, +): Promise { + const response = await apiClient.post( + "/generation/generate-cover", + { ...data, template_id: templateId }, + { + timeout: 300000, + params: { template_id: templateId }, + }, + ) + return response.data +} diff --git a/apps/web/src/api/generation/index.ts b/apps/web/src/api/generation/index.ts index 672dc9699..49270c2ca 100755 --- a/apps/web/src/api/generation/index.ts +++ b/apps/web/src/api/generation/index.ts @@ -10,3 +10,6 @@ export type { export { createPreview, getPreviewStatus } from "./preview" export { confirmGeneration } from "./confirm" + +export { generateCover } from "./cover" +export type { GenerateCoverRequest, GenerateCoverResponse } from "./cover" diff --git a/apps/web/src/api/template-editor/aiFeatures.ts b/apps/web/src/api/template-editor/aiFeatures.ts index ed4727c56..4f88d59f6 100644 --- a/apps/web/src/api/template-editor/aiFeatures.ts +++ b/apps/web/src/api/template-editor/aiFeatures.ts @@ -1,12 +1,10 @@ /** - * AI 推荐 + 封面生成 API + * AI 推荐 API */ import apiClient from "../client" import type { AIRecommendRequest, AIRecommendResponse, - GenerateCoverRequest, - GenerateCoverResponse, } from "./types" /** AI 推荐片段方案 */ @@ -18,13 +16,3 @@ export async function aiRecommendClips( return response.data } -/** AI 生成封面 */ -export async function generateCover( - templateId: string, - data: GenerateCoverRequest, -): Promise { - const response = await apiClient.post(`/templates/${templateId}/editor/generate-cover`, data, { - timeout: 300000, // 封面生成涉及 MediaKit 抽帧,最长 300 秒 - }) - return response.data -} diff --git a/apps/web/src/api/template-editor/index.ts b/apps/web/src/api/template-editor/index.ts index 751f9024b..af50dd265 100644 --- a/apps/web/src/api/template-editor/index.ts +++ b/apps/web/src/api/template-editor/index.ts @@ -27,9 +27,6 @@ export type { AIRecommendRequest, AIRecommendClipItem, AIRecommendResponse, - GenerateCoverRequest, - GenerateCoverResponse, - CoverResult, EditPlanClipStatus, EditPlanClip, CreateEditPlanClipRequest, @@ -81,8 +78,8 @@ export { createClipsFromAssets, } from "./clips" -// AI 推荐 + 封面生成 -export { aiRecommendClips, generateCover } from "./aiFeatures" +// AI 推荐 +export { aiRecommendClips } from "./aiFeatures" // 素材库 export { getMediaAssets, getMediaAsset } from "./mediaAssets" diff --git a/apps/web/src/api/template-editor/types.ts b/apps/web/src/api/template-editor/types.ts index cb7e162ba..9e1920e3d 100644 --- a/apps/web/src/api/template-editor/types.ts +++ b/apps/web/src/api/template-editor/types.ts @@ -244,7 +244,7 @@ export interface GeneratedVideo { updated_at?: string } -/* ── AI 推荐 & 封面生成 ── */ +/* ── AI 推荐 ── */ /** AI 推荐请求 */ export interface AIRecommendRequest { @@ -274,28 +274,6 @@ export interface AIRecommendResponse { confidence: number } -/** AI 封面生成请求 */ -export interface GenerateCoverRequest { - asset_ids: string[] - cover_type?: "ai_frame" | "manual" | "upload" | "ai_regenerate" - frame_time?: number -} - -/** AI 封面生成响应 */ -export interface GenerateCoverResponse { - plan_id: string - cover: CoverResult -} - -/** 封面生成结果 */ -export interface CoverResult { - scheme?: string - asset_id?: string - frame_time?: number - image_url?: string - thumbnail_url?: string -} - /* ── 片段 CRUD 相关 ── */ /** 片段状态 */ diff --git a/apps/web/src/pages/generate/hooks/useStep6Cover.ts b/apps/web/src/pages/generate/hooks/useStep6Cover.ts index fb8dc957d..9b8b804b2 100644 --- a/apps/web/src/pages/generate/hooks/useStep6Cover.ts +++ b/apps/web/src/pages/generate/hooks/useStep6Cover.ts @@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react" import { message } from "antd" import type { CoverConfig, CoverTemplate } from "../types/cover" -import { generateCover } from "@/api/template-editor" +import { generateCover } from "@/api/generation" import { fetchCoverTemplates, createCoverTemplate, diff --git a/apps/web/src/test/api/edit-plans.test.ts b/apps/web/src/test/api/edit-plans.test.ts index ab713a994..49124f59b 100644 --- a/apps/web/src/test/api/edit-plans.test.ts +++ b/apps/web/src/test/api/edit-plans.test.ts @@ -8,7 +8,6 @@ import { generateEditPlan, getGenerationStatus, aiRecommendClips, - generateCover, getEditPlanGenerations, getGenerationTaskResults, cancelGeneration, @@ -182,21 +181,6 @@ describe("editPlans API", () => { }) }) - describe("generateCover", () => { - it("should resolve successfully", async () => { - await expect(generateCover("test-planId")).resolves.not.toThrow() - }) - - it("should reject on API error", async () => { - mockGet.mockRejectedValue(new Error("Network error")) - mockPost.mockRejectedValue(new Error("Network error")) - mockPut.mockRejectedValue(new Error("Network error")) - mockDelete.mockRejectedValue(new Error("Network error")) - mockPatch.mockRejectedValue(new Error("Network error")) - - await expect(generateCover("test-planId")).rejects.toThrow() - }) - }) describe("getEditPlanGenerations", () => { it("should resolve successfully", async () => { diff --git a/tests/unit/test_generation_cover.py b/tests/unit/test_generation_cover.py new file mode 100644 index 000000000..e95de36fd --- /dev/null +++ b/tests/unit/test_generation_cover.py @@ -0,0 +1,95 @@ +"""Tests for generation cover route — schema validation and import checks.""" + +from __future__ import annotations + +import pytest + + +def test_generation_cover_router_importable(): + """新路由模块可以正确导入""" + from app.api.routes.generation_cover import router + + assert router is not None + # tags 应该是 Generation + assert "Generation" in router.tags + + +def test_generation_cover_route_path(): + """路由路径应为 /generate-cover""" + from app.api.routes.generation_cover import router + + paths = [route.path for route in router.routes] + assert "/generate-cover" in paths + + +def test_generation_cover_schemas_importable(): + """Schema 可以从新模块导入""" + from app.api.routes.generation_cover import GenerateCoverRequest, GenerateCoverResponse + + # 验证请求 schema 默认值 + req = GenerateCoverRequest() + assert req.asset_ids == [] + assert req.cover_type == "ai_frame" + assert req.frame_time is None + + # 验证响应 schema + resp = GenerateCoverResponse(plan_id="p1", cover={"image_url": "http://x"}) + assert resp.plan_id == "p1" + assert resp.cover["image_url"] == "http://x" + + +def test_generation_cover_schemas_not_in_templates_editor(): + """旧的 templates_editor/schemas.py 不再包含封面 schema""" + from app.api.routes.templates_editor import schemas as te_schemas + + assert not hasattr(te_schemas, "GenerateCoverRequest") + assert not hasattr(te_schemas, "GenerateCoverResponse") + + +def test_templates_editor_no_cover_router(): + """templates_editor 不再包含 cover_router""" + from app.api.routes.templates_editor import _sub_routers + + # cover_router 应该已被移除 + for sub in _sub_routers: + for route in sub.routes: + assert "generate-cover" not in getattr(route, "path", ""), ( + "templates_editor 不应再有 generate-cover 路由" + ) + + +def test_api_router_has_generation_cover(): + """api_router 应该包含 /api/v1/generation/generate-cover 路径""" + from app.api.router import api_router + + all_paths = [] + for route in api_router.routes: + if hasattr(route, "path"): + all_paths.append(route.path) + # 嵌套 router + if hasattr(route, "routes"): + for sub_route in route.routes: + if hasattr(sub_route, "path"): + all_paths.append(sub_route.path) + + # 应该能找到 generate-cover 路径 + cover_paths = [p for p in all_paths if "generate-cover" in p] + assert len(cover_paths) > 0, f"未找到 generate-cover 路由, 所有路径: {all_paths[:20]}" + + +def test_generation_cover_request_validation(): + """验证请求 schema 的字段约束""" + from app.api.routes.generation_cover import GenerateCoverRequest + + # frame_time 不允许负数 + with pytest.raises(Exception): + GenerateCoverRequest(frame_time=-1.0) + + # 合法的 frame_time + req = GenerateCoverRequest(frame_time=5.5) + assert req.frame_time == 5.5 + + # 自定义 cover_type + req2 = GenerateCoverRequest(cover_type="upload", asset_ids=["a1", "a2"]) + assert req2.cover_type == "upload" + assert req2.asset_ids == ["a1", "a2"] -- 2.54.0 From 26338f7f2d98b64034c40c76c0d50b583e350f2d Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 13 Aug 2026 10:34:28 +0000 Subject: [PATCH 2/3] style: auto-format with black + isort + prettier [skip ci-format-check] --- apps/api/app/api/router.py | 2 +- apps/web/src/api/template-editor/aiFeatures.ts | 6 +----- apps/web/src/test/api/edit-plans.test.ts | 1 - tests/unit/test_generation_cover.py | 4 +--- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/api/app/api/router.py b/apps/api/app/api/router.py index 3b5f70faf..53c675d97 100755 --- a/apps/api/app/api/router.py +++ b/apps/api/app/api/router.py @@ -8,8 +8,8 @@ from app.api.routes.classification_jobs import router as classification_jobs_rou from app.api.routes.cover_templates import router as cover_templates_router from app.api.routes.duplication import router as duplication_router from app.api.routes.feature_flags import router as feature_flags_router -from app.api.routes.generation_preview import router as generation_preview_router from app.api.routes.generation_cover import router as generation_cover_router +from app.api.routes.generation_preview import router as generation_preview_router from app.api.routes.generation_tasks import router as generation_tasks_router from app.api.routes.health import router as health_check_router from app.api.routes.ingest_jobs import router as ingest_jobs_router diff --git a/apps/web/src/api/template-editor/aiFeatures.ts b/apps/web/src/api/template-editor/aiFeatures.ts index 4f88d59f6..3c84ae03d 100644 --- a/apps/web/src/api/template-editor/aiFeatures.ts +++ b/apps/web/src/api/template-editor/aiFeatures.ts @@ -2,10 +2,7 @@ * AI 推荐 API */ import apiClient from "../client" -import type { - AIRecommendRequest, - AIRecommendResponse, -} from "./types" +import type { AIRecommendRequest, AIRecommendResponse } from "./types" /** AI 推荐片段方案 */ export async function aiRecommendClips( @@ -15,4 +12,3 @@ export async function aiRecommendClips( const response = await apiClient.post(`/templates/${templateId}/editor/ai-recommend`, data) return response.data } - diff --git a/apps/web/src/test/api/edit-plans.test.ts b/apps/web/src/test/api/edit-plans.test.ts index 49124f59b..e2327dd36 100644 --- a/apps/web/src/test/api/edit-plans.test.ts +++ b/apps/web/src/test/api/edit-plans.test.ts @@ -181,7 +181,6 @@ describe("editPlans API", () => { }) }) - describe("getEditPlanGenerations", () => { it("should resolve successfully", async () => { await expect(getEditPlanGenerations("test-planId")).resolves.not.toThrow() diff --git a/tests/unit/test_generation_cover.py b/tests/unit/test_generation_cover.py index e95de36fd..100812335 100644 --- a/tests/unit/test_generation_cover.py +++ b/tests/unit/test_generation_cover.py @@ -53,9 +53,7 @@ def test_templates_editor_no_cover_router(): # cover_router 应该已被移除 for sub in _sub_routers: for route in sub.routes: - assert "generate-cover" not in getattr(route, "path", ""), ( - "templates_editor 不应再有 generate-cover 路由" - ) + assert "generate-cover" not in getattr(route, "path", ""), "templates_editor 不应再有 generate-cover 路由" def test_api_router_has_generation_cover(): -- 2.54.0 From af88a8136be2b068d4de515591ac4a70c86e35d7 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 13 Aug 2026 18:46:08 +0800 Subject: [PATCH 3/3] fix(test): replace broad pytest.raises(Exception) with ValidationError (B017) --- tests/unit/test_generation_cover.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_generation_cover.py b/tests/unit/test_generation_cover.py index 100812335..1145d69bf 100644 --- a/tests/unit/test_generation_cover.py +++ b/tests/unit/test_generation_cover.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest +from pydantic import ValidationError def test_generation_cover_router_importable(): @@ -80,7 +81,7 @@ def test_generation_cover_request_validation(): from app.api.routes.generation_cover import GenerateCoverRequest # frame_time 不允许负数 - with pytest.raises(Exception): + with pytest.raises(ValidationError): GenerateCoverRequest(frame_time=-1.0) # 合法的 frame_time -- 2.54.0