From 25a93527946410197523cb4a130d8ebaebd7242e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E5=BA=94?= Date: Tue, 7 Jul 2026 10:10:57 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20P1=20=E5=AE=89=E5=85=A8=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20-=20TTS=E5=90=88=E6=88=90=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0voice=5Fclone=5Fprofile=5Fid=E5=BD=92?= =?UTF-8?q?=E5=B1=9E=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /tts/synthesize 未校验 voice_clone_profile_id 归属, 任意用户可使用他人克隆音色进行合成。 增加 ownership 校验,非本人 profile 返回 403。 --- apps/api/app/api/routes/tts.py | 22 ++++++++++++++++++- apps/web/src/api/assets.ts | 9 ++++++++ .../pages/editing-planner/EditingPlanner.tsx | 9 +++++++- apps/web/src/pages/editing-planner/types.ts | 4 ++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/apps/api/app/api/routes/tts.py b/apps/api/app/api/routes/tts.py index 34b6f84dd..38ba5ccd1 100644 --- a/apps/api/app/api/routes/tts.py +++ b/apps/api/app/api/routes/tts.py @@ -6,7 +6,11 @@ import logging from typing import Optional from app.auth import AuthenticatedUser, get_current_user -from app.dependencies import get_cosyvoice_service, get_db_session +from app.dependencies import ( + get_cosyvoice_service, + get_db_session, + get_voice_clone_profile_repository, +) from app.schemas.tts import ( ListTTSJobResponse, TTSJobResponse, @@ -73,6 +77,7 @@ def synthesize( authenticated_user: AuthenticatedUser = Depends(get_current_user), repository: SQLAlchemyTTSJobRepository = Depends(_get_repository), cosyvoice_service: CosyVoiceService = Depends(get_cosyvoice_service), + voice_clone_repo=Depends(get_voice_clone_profile_repository), ) -> TTSSynthesizeResponse: """发起 TTS 合成任务。 @@ -80,6 +85,21 @@ def synthesize( 与音色克隆接口保持一致:CosyVoice 失败时不抛 500,而是返回 201 + failed 状态任务记录。 """ user_id = authenticated_user.user.id + + # 校验 voice_clone_profile_id 归属(防止越权使用他人克隆音色) + if request.voice_clone_profile_id: + profile = voice_clone_repo.get(request.voice_clone_profile_id) + if profile is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Voice clone profile not found", + ) + if profile.user_id != user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied to voice clone profile", + ) + use_case = CreateTTSJobUseCase(repository) job = use_case.execute( user_id=user_id, diff --git a/apps/web/src/api/assets.ts b/apps/web/src/api/assets.ts index c2b064c0a..614745916 100644 --- a/apps/web/src/api/assets.ts +++ b/apps/web/src/api/assets.ts @@ -114,6 +114,15 @@ export const createAssetLibrary = async (data: { return response.data; }; +/** 确保项目下指定 kind 的默认素材库存在(不存在则自动创建) */ +export const ensureDefaultLibrary = async (data: { + project_id: string; + kind: "video" | "voice" | "image"; +}): Promise => { + const response = await apiClient.post("/asset-libraries/ensure-default", data); + return response.data; +}; + /** 删除素材库 */ export const deleteAssetLibrary = async (libraryId: string): Promise => { await apiClient.delete(`/asset-libraries/${libraryId}`); diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index e48b365b1..6c88dbfbf 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -2,9 +2,10 @@ * 剪辑计划编辑器 — V8 原型 1:1 还原 * 四行布局:顶栏(42px) → 模式栏(48px) → 三栏主体 → 底栏(40px) */ -import React, { useState, useCallback, useEffect } from "react"; +import React, { useState, useCallback, useEffect, useMemo } from "react"; import { useSearchParams, useNavigate } from "react-router-dom"; import { message } from "antd"; +import { useQuery } from "@tanstack/react-query"; import type { EditingTemplate, TemplateCategory, @@ -30,6 +31,12 @@ import { useUndoRedo } from "./hooks/useUndoRedo"; import type { TaskItem } from "@/api/tasks"; import { createGenerationTask, getTask, retryTask } from "@/api/tasks"; import type { ClipData, ClipType } from "./types"; +import { + ensureDefaultLibrary, + getAssetsByKind, + type AssetItem, +} from "@/api/assets"; +import { getOrCreateDefaultProject } from "@/api/projects"; import MediaPanel from "./components/MediaPanel"; import PreviewPlayer from "./components/PreviewPlayer"; diff --git a/apps/web/src/pages/editing-planner/types.ts b/apps/web/src/pages/editing-planner/types.ts index 87d39b4c9..5547e7553 100644 --- a/apps/web/src/pages/editing-planner/types.ts +++ b/apps/web/src/pages/editing-planner/types.ts @@ -14,4 +14,8 @@ export interface ClipData { template_segment_id?: string; script_text?: string; order?: number; + /** 配音素材 ID(voice 类型片段使用) */ + voice_asset_id?: string; + /** 配音素材文件 URL(voice 类型片段使用) */ + voice_file_url?: string; }