From 2e005b0084ee7a3f763b7369ea6774aae181f563 Mon Sep 17 00:00:00 2001 From: bdim404 Date: Tue, 10 Mar 2026 20:23:03 +0800 Subject: [PATCH] feat(audiobook): add gender field to audiobook character model and update related functionality --- qwen3-tts-backend/api/audiobook.py | 1 + qwen3-tts-backend/core/audiobook_service.py | 2 ++ qwen3-tts-backend/core/llm_service.py | 11 ++++++---- qwen3-tts-backend/db/crud.py | 5 +++++ qwen3-tts-backend/db/database.py | 9 ++++++++ qwen3-tts-backend/db/models.py | 1 + qwen3-tts-backend/schemas/audiobook.py | 2 ++ qwen3-tts-frontend/src/lib/api/audiobook.ts | 3 ++- qwen3-tts-frontend/src/pages/Audiobook.tsx | 24 ++++++++++++++++++--- 9 files changed, 50 insertions(+), 8 deletions(-) diff --git a/qwen3-tts-backend/api/audiobook.py b/qwen3-tts-backend/api/audiobook.py index aef84db..3975d9f 100644 --- a/qwen3-tts-backend/api/audiobook.py +++ b/qwen3-tts-backend/api/audiobook.py @@ -291,6 +291,7 @@ async def update_character( char = crud.update_audiobook_character( db, char_id, name=data.name, + gender=data.gender, description=data.description, instruct=data.instruct, voice_design_id=data.voice_design_id, diff --git a/qwen3-tts-backend/core/audiobook_service.py b/qwen3-tts-backend/core/audiobook_service.py index ba0f860..0f27a25 100644 --- a/qwen3-tts-backend/core/audiobook_service.py +++ b/qwen3-tts-backend/core/audiobook_service.py @@ -195,6 +195,7 @@ async def analyze_project(project_id: int, user: User, db: Session, turbo: bool name = char_data.get("name", "narrator") instruct = char_data.get("instruct", "") description = char_data.get("description", "") + gender = char_data.get("gender") or ("未知" if name == "narrator" else None) voice_design = crud.create_voice_design( db=db, @@ -209,6 +210,7 @@ async def analyze_project(project_id: int, user: User, db: Session, turbo: bool db=db, project_id=project_id, name=name, + gender=gender, description=description, instruct=instruct, voice_design_id=voice_design.id, diff --git a/qwen3-tts-backend/core/llm_service.py b/qwen3-tts-backend/core/llm_service.py index e877a03..08039fb 100644 --- a/qwen3-tts-backend/core/llm_service.py +++ b/qwen3-tts-backend/core/llm_service.py @@ -119,6 +119,8 @@ class LLMService: async def extract_characters(self, text_samples: list[str], on_token=None, on_sample=None, turbo: bool = False) -> list[Dict]: system_prompt = ( "你是一个专业的小说分析助手兼声音导演。请分析给定的小说文本,提取所有出现的角色(包括旁白narrator)。\n" + "gender字段必须明确标注性别,只能取以下三个值之一:\"男\"、\"女\"、\"未知\"。\n" + "narrator的gender固定为\"未知\"。\n" "对每个角色,instruct字段必须是详细的声音导演说明,需覆盖以下六个维度,每个维度单独一句,用换行分隔:\n" "1. 音色信息:嗓音质感、音域、音量、气息特征(如:青年男性中低音,音色干净略带沙哑,音量偏小但稳定,情绪激动时呼吸明显)\n" "2. 身份背景:角色身份、职业、出身、所处时代背景对声音的影响\n" @@ -127,7 +129,7 @@ class LLMService: "5. 性格特质:核心性格、情绪模式、表达习惯\n" "6. 叙事风格:语速节奏、停顿习惯、语气色彩、整体叙述感\n\n" "只输出JSON,格式如下,不要有其他文字:\n" - '{"characters": [{"name": "narrator", "description": "第三人称叙述者", "instruct": "音色信息:...\\n身份背景:...\\n年龄设定:...\\n外貌特征:...\\n性格特质:...\\n叙事风格:..."}, ...]}' + '{"characters": [{"name": "narrator", "gender": "未知", "description": "第三人称叙述者", "instruct": "音色信息:...\\n身份背景:...\\n年龄设定:...\\n外貌特征:...\\n性格特质:...\\n叙事风格:..."}, ...]}' ) if turbo and len(text_samples) > 1: logger.info(f"Extracting characters in turbo mode: {len(text_samples)} samples concurrent") @@ -169,11 +171,12 @@ class LLMService: "你是一个专业的小说角色整合助手。你收到的是从同一本书不同段落中提取的角色列表,其中可能存在重复。\n" "请完成以下任务:\n" "1. 识别并合并重复角色:通过名字完全相同或高度相似(全名与简称、不同译写)来判断。\n" - "2. 合并时保留最完整、最详细的 description 和 instruct 字段。\n" - "3. narrator 角色只保留一个。\n" + "2. 合并时保留最完整、最详细的 description 和 instruct 字段,gender 字段以最明确的值为准(优先选\"男\"或\"女\",而非\"未知\")。\n" + "3. narrator 角色只保留一个,其 gender 固定为\"未知\"。\n" "4. 去除无意义的占位角色(name 为空或仅含标点)。\n" + "gender 字段只能取 \"男\"、\"女\"、\"未知\" 之一。\n" "只输出 JSON,不要有其他文字:\n" - '{"characters": [{"name": "...", "description": "...", "instruct": "..."}, ...]}' + '{"characters": [{"name": "...", "gender": "男", "description": "...", "instruct": "..."}, ...]}' ) user_message = f"请整合以下角色列表:\n\n{json.dumps(raw_characters, ensure_ascii=False, indent=2)}" try: diff --git a/qwen3-tts-backend/db/crud.py b/qwen3-tts-backend/db/crud.py index b81435a..be955bc 100644 --- a/qwen3-tts-backend/db/crud.py +++ b/qwen3-tts-backend/db/crud.py @@ -513,6 +513,7 @@ def create_audiobook_character( db: Session, project_id: int, name: str, + gender: Optional[str] = None, description: Optional[str] = None, instruct: Optional[str] = None, voice_design_id: Optional[int] = None, @@ -520,6 +521,7 @@ def create_audiobook_character( char = AudiobookCharacter( project_id=project_id, name=name, + gender=gender, description=description, instruct=instruct, voice_design_id=voice_design_id, @@ -558,6 +560,7 @@ def update_audiobook_character( db: Session, char_id: int, name: Optional[str] = None, + gender: Optional[str] = None, description: Optional[str] = None, instruct: Optional[str] = None, voice_design_id: Optional[int] = None, @@ -567,6 +570,8 @@ def update_audiobook_character( return None if name is not None: char.name = name + if gender is not None: + char.gender = gender if description is not None: char.description = description if instruct is not None: diff --git a/qwen3-tts-backend/db/database.py b/qwen3-tts-backend/db/database.py index 7d8f78e..254e1e0 100644 --- a/qwen3-tts-backend/db/database.py +++ b/qwen3-tts-backend/db/database.py @@ -27,3 +27,12 @@ def get_db(): def init_db(): Base.metadata.create_all(bind=engine) + if "sqlite" in str(engine.url): + with engine.connect() as conn: + try: + conn.execute(__import__("sqlalchemy").text( + "ALTER TABLE audiobook_characters ADD COLUMN gender VARCHAR(20)" + )) + conn.commit() + except Exception: + pass diff --git a/qwen3-tts-backend/db/models.py b/qwen3-tts-backend/db/models.py index d3f1175..668cd35 100644 --- a/qwen3-tts-backend/db/models.py +++ b/qwen3-tts-backend/db/models.py @@ -169,6 +169,7 @@ class AudiobookCharacter(Base): id = Column(Integer, primary_key=True, index=True) project_id = Column(Integer, ForeignKey("audiobook_projects.id"), nullable=False, index=True) name = Column(String(200), nullable=False) + gender = Column(String(20), nullable=True) description = Column(Text, nullable=True) instruct = Column(Text, nullable=True) voice_design_id = Column(Integer, ForeignKey("voice_designs.id"), nullable=True) diff --git a/qwen3-tts-backend/schemas/audiobook.py b/qwen3-tts-backend/schemas/audiobook.py index 79c281b..d736a41 100644 --- a/qwen3-tts-backend/schemas/audiobook.py +++ b/qwen3-tts-backend/schemas/audiobook.py @@ -27,6 +27,7 @@ class AudiobookCharacterResponse(BaseModel): id: int project_id: int name: str + gender: Optional[str] = None description: Optional[str] = None instruct: Optional[str] = None voice_design_id: Optional[int] = None @@ -64,6 +65,7 @@ class AudiobookCharacterUpdate(BaseModel): class AudiobookCharacterEdit(BaseModel): name: Optional[str] = None + gender: Optional[str] = None description: Optional[str] = None instruct: Optional[str] = None voice_design_id: Optional[int] = None diff --git a/qwen3-tts-frontend/src/lib/api/audiobook.ts b/qwen3-tts-frontend/src/lib/api/audiobook.ts index 634e106..b7bb1c4 100644 --- a/qwen3-tts-frontend/src/lib/api/audiobook.ts +++ b/qwen3-tts-frontend/src/lib/api/audiobook.ts @@ -16,6 +16,7 @@ export interface AudiobookCharacter { id: number project_id: number name: string + gender?: string description?: string instruct?: string voice_design_id?: number @@ -92,7 +93,7 @@ export const audiobookApi = { updateCharacter: async ( projectId: number, charId: number, - data: { name?: string; description?: string; instruct?: string; voice_design_id?: number } + data: { name?: string; gender?: string; description?: string; instruct?: string; voice_design_id?: number } ): Promise => { const response = await apiClient.put( `/audiobook/projects/${projectId}/characters/${charId}`, diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx index 6fb2e30..d317d74 100644 --- a/qwen3-tts-frontend/src/pages/Audiobook.tsx +++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx @@ -334,7 +334,7 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr const [loadingAction, setLoadingAction] = useState(false) const [isPolling, setIsPolling] = useState(false) const [editingCharId, setEditingCharId] = useState(null) - const [editFields, setEditFields] = useState({ name: '', description: '', instruct: '' }) + const [editFields, setEditFields] = useState({ name: '', gender: '', description: '', instruct: '' }) const [sequentialPlayingId, setSequentialPlayingId] = useState(null) const [charsCollapsed, setCharsCollapsed] = useState(false) const [chaptersCollapsed, setChaptersCollapsed] = useState(false) @@ -520,13 +520,14 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr const startEditChar = (char: AudiobookCharacter) => { setEditingCharId(char.id) - setEditFields({ name: char.name, description: char.description || '', instruct: char.instruct || '' }) + setEditFields({ name: char.name, gender: char.gender || '', description: char.description || '', instruct: char.instruct || '' }) } const saveEditChar = async (char: AudiobookCharacter) => { try { await audiobookApi.updateCharacter(project.id, char.id, { name: editFields.name || char.name, + gender: editFields.gender || undefined, description: editFields.description, instruct: editFields.instruct, }) @@ -632,6 +633,16 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr onChange={e => setEditFields(f => ({ ...f, name: e.target.value }))} placeholder="角色名" /> + setEditFields(f => ({ ...f, instruct: e.target.value }))} @@ -653,7 +664,14 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr ) : (
- {char.name} +
+ {char.name} + {char.gender && ( + + {char.gender} + + )} +
{char.instruct}
{char.voice_design_id