From 0a12f204ba601d0992140cc335bf018266002706 Mon Sep 17 00:00:00 2001 From: bdim404 Date: Fri, 13 Mar 2026 13:36:55 +0800 Subject: [PATCH] feat: add violence and eroticism level parameters to synopsis and script generation requests --- qwen3-tts-backend/api/audiobook.py | 10 +++++ qwen3-tts-backend/core/audiobook_service.py | 42 +++++++++++++++++++-- qwen3-tts-backend/core/llm_service.py | 34 ++++++++++++++--- qwen3-tts-backend/schemas/audiobook.py | 8 ++++ qwen3-tts-frontend/src/lib/api/audiobook.ts | 8 ++++ qwen3-tts-frontend/src/pages/Audiobook.tsx | 38 ++++++++++++++++++- 6 files changed, 129 insertions(+), 11 deletions(-) diff --git a/qwen3-tts-backend/api/audiobook.py b/qwen3-tts-backend/api/audiobook.py index 9cf7f8c..1eede74 100644 --- a/qwen3-tts-backend/api/audiobook.py +++ b/qwen3-tts-backend/api/audiobook.py @@ -180,6 +180,7 @@ async def generate_synopsis( "你是一位专业的小说策划师,擅长根据创作参数生成引人入胜的故事简介。" "请根据用户提供的类型、风格、主角、冲突等参数,生成一段200-400字的中文故事简介。" "简介需涵盖:世界观背景、主角基本情况、核心矛盾冲突、故事基调。" + "暴力程度和色情程度数值越高,简介中相关情节描写越多、越直接。" "直接输出简介正文,不要加任何前缀标题或说明文字。" ) parts = [f"类型:{data.genre}"] @@ -193,6 +194,10 @@ async def generate_synopsis( parts.append(f"冲突规模:{data.conflict_scale}") parts.append(f"角色数量:约{data.num_characters}个主要角色") parts.append(f"故事体量:约{data.num_chapters}章") + if data.violence_level > 0: + parts.append(f"暴力程度:{data.violence_level}/10") + if data.eroticism_level > 0: + parts.append(f"色情程度:{data.eroticism_level}/10") user_message = "\n".join(parts) + "\n\n请生成故事简介:" try: @@ -294,6 +299,7 @@ async def generate_synopsis_nsfw( "你是一位专业的成人小说策划师,擅长根据创作参数生成引人入胜的故事简介。" "请根据用户提供的类型、风格、主角、冲突等参数,生成一段200-400字的中文故事简介。" "简介需涵盖:世界观背景、主角基本情况、核心矛盾冲突、故事基调。" + "暴力程度和色情程度数值越高,简介中相关情节描写越多、越露骨直接。" "直接输出简介正文,不要加任何前缀标题或说明文字。" ) parts = [f"类型:{data.genre}"] @@ -307,6 +313,10 @@ async def generate_synopsis_nsfw( parts.append(f"冲突规模:{data.conflict_scale}") parts.append(f"角色数量:约{data.num_characters}个主要角色") parts.append(f"故事体量:约{data.num_chapters}章") + if data.violence_level > 0: + parts.append(f"暴力程度:{data.violence_level}/10") + if data.eroticism_level > 0: + parts.append(f"色情程度:{data.eroticism_level}/10") user_message = "\n".join(parts) + "\n\n请生成故事简介:" try: diff --git a/qwen3-tts-backend/core/audiobook_service.py b/qwen3-tts-backend/core/audiobook_service.py index 781439d..e88098a 100644 --- a/qwen3-tts-backend/core/audiobook_service.py +++ b/qwen3-tts-backend/core/audiobook_service.py @@ -208,6 +208,16 @@ def parse_ai_script(script_text: str, char_map: dict) -> list[dict]: elif content.startswith('"') and content.endswith('"'): content = content[1:-1].strip() + if emo_text is None: + emo_m = _EMO_RE.search(content) + if emo_m: + emo_text = emo_m.group(1) + try: + emo_alpha = float(emo_m.group(2)) + except ValueError: + emo_alpha = None + content = content[:emo_m.start()].strip() + character = speaker results.append({ @@ -252,6 +262,8 @@ async def generate_ai_script(project_id: int, user: User, db: Session) -> None: style = cfg.get("style", "") num_characters = cfg.get("num_characters", 5) num_chapters = cfg.get("num_chapters", 8) + violence_level = cfg.get("violence_level", 0) + eroticism_level = cfg.get("eroticism_level", 0) ps.append_line(key, f"\n[Step 1] 生成 {num_characters} 个角色...\n") ps.append_line(key, "") @@ -262,6 +274,7 @@ async def generate_ai_script(project_id: int, user: User, db: Session) -> None: characters_data = await llm.generate_story_characters( genre=genre, subgenre=subgenre, premise=premise, style=style, num_characters=num_characters, usage_callback=_log_usage, + violence_level=violence_level, eroticism_level=eroticism_level, ) has_narrator = any(c.get("name") in ("narrator", "旁白") for c in characters_data) @@ -370,9 +383,16 @@ async def generate_ai_script_chapters(project_id: int, user: User, db: Session) premise = cfg.get("premise", "") style = cfg.get("style", "") num_chapters = cfg.get("num_chapters", 8) + violence_level = cfg.get("violence_level", 0) + eroticism_level = cfg.get("eroticism_level", 0) - llm = _get_llm_service(db) - _llm_model = crud.get_system_setting(db, "llm_model") + is_nsfw = cfg.get("nsfw_mode", False) + if is_nsfw: + llm = _get_grok_service(db) + _llm_model = crud.get_system_setting(db, "grok_model") or "grok-4" + else: + llm = _get_llm_service(db) + _llm_model = crud.get_system_setting(db, "llm_model") _user_id = user.id def _log_usage(prompt_tokens: int, completion_tokens: int) -> None: @@ -400,6 +420,7 @@ async def generate_ai_script_chapters(project_id: int, user: User, db: Session) chapters_data = await llm.generate_chapter_outline( genre=genre, subgenre=subgenre, premise=premise, style=style, num_chapters=num_chapters, characters=characters_data, usage_callback=_log_usage, + violence_level=violence_level, eroticism_level=eroticism_level, ) ps.append_line(key, f"\n\n[完成] 大纲:{len(chapters_data)} 章") @@ -440,6 +461,7 @@ async def generate_ai_script_chapters(project_id: int, user: User, db: Session) genre=genre, premise=premise, chapter_index=idx, chapter_title=title, chapter_summary=summary, characters=characters_data, on_token=on_token, usage_callback=_log_usage, + violence_level=violence_level, eroticism_level=eroticism_level, ) chapter_obj.source_text = script_text @@ -530,9 +552,16 @@ async def continue_ai_script_chapters(project_id: int, additional_chapters: int, subgenre = cfg.get("subgenre", "") premise = cfg.get("premise", "") style = cfg.get("style", "") + violence_level = cfg.get("violence_level", 0) + eroticism_level = cfg.get("eroticism_level", 0) - llm = _get_llm_service(db) - _llm_model = crud.get_system_setting(db, "llm_model") + is_nsfw = cfg.get("nsfw_mode", False) + if is_nsfw: + llm = _get_grok_service(db) + _llm_model = crud.get_system_setting(db, "grok_model") or "grok-4" + else: + llm = _get_llm_service(db) + _llm_model = crud.get_system_setting(db, "llm_model") _user_id = user.id def _log_usage(prompt_tokens: int, completion_tokens: int) -> None: @@ -568,6 +597,7 @@ async def continue_ai_script_chapters(project_id: int, additional_chapters: int, genre=genre, subgenre=subgenre, premise=premise, style=style, existing_chapters=existing_chapters_data, additional_chapters=additional_chapters, characters=characters_data, usage_callback=_log_usage, + violence_level=violence_level, eroticism_level=eroticism_level, ) ps.append_line(key, f"\n\n[完成] 续写大纲:{len(new_chapters_data)} 章") @@ -596,6 +626,7 @@ async def continue_ai_script_chapters(project_id: int, additional_chapters: int, genre=genre, premise=premise, chapter_index=idx, chapter_title=title, chapter_summary=summary, characters=characters_data, on_token=on_token, usage_callback=_log_usage, + violence_level=violence_level, eroticism_level=eroticism_level, ) chapter_obj.source_text = script_text @@ -1521,6 +1552,8 @@ async def generate_ai_script_nsfw(project_id: int, user: User, db: Session) -> N style = cfg.get("style", "") num_characters = cfg.get("num_characters", 5) num_chapters = cfg.get("num_chapters", 8) + violence_level = cfg.get("violence_level", 0) + eroticism_level = cfg.get("eroticism_level", 0) ps.append_line(key, f"\n[Step 1] 生成 {num_characters} 个角色...\n") ps.append_line(key, "") @@ -1531,6 +1564,7 @@ async def generate_ai_script_nsfw(project_id: int, user: User, db: Session) -> N characters_data = await llm.generate_story_characters( genre=genre, subgenre=subgenre, premise=premise, style=style, num_characters=num_characters, usage_callback=_log_usage, + violence_level=violence_level, eroticism_level=eroticism_level, ) has_narrator = any(c.get("name") in ("narrator", "旁白") for c in characters_data) diff --git a/qwen3-tts-backend/core/llm_service.py b/qwen3-tts-backend/core/llm_service.py index 61b491c..54638f5 100644 --- a/qwen3-tts-backend/core/llm_service.py +++ b/qwen3-tts-backend/core/llm_service.py @@ -96,6 +96,9 @@ class LLMService: raw = "\n".join(inner).strip() if not raw: raise ValueError("LLM returned empty JSON after stripping markdown") + if not raw.startswith(("{", "[")): + logger.error(f"LLM refused or returned non-JSON. Raw (first 500): {raw[:500]}") + raise ValueError(f"LLM拒绝响应:{raw[:200]}") try: return json.loads(raw) except json.JSONDecodeError: @@ -245,6 +248,8 @@ class LLMService: style: str, num_characters: int, usage_callback: Optional[Callable[[int, int], None]] = None, + violence_level: int = 0, + eroticism_level: int = 0, ) -> list[Dict]: genre_label = f"{genre}{'/' + subgenre if subgenre else ''}" system_prompt = ( @@ -272,6 +277,10 @@ class LLMService: if style: parts.append(f"风格:{style}") parts.append(f"故事简介:{premise}") + if violence_level > 0: + parts.append(f"暴力程度:{violence_level}/10") + if eroticism_level > 0: + parts.append(f"色情程度:{eroticism_level}/10") parts.append(f"请为这个故事创作 {num_characters} 个主要角色,再加上旁白narrator,共 {num_characters + 1} 个角色。") user_message = "\n".join(parts) result = await self.stream_chat_json(system_prompt, user_message, max_tokens=4096, usage_callback=usage_callback) @@ -286,6 +295,8 @@ class LLMService: num_chapters: int, characters: list[Dict], usage_callback: Optional[Callable[[int, int], None]] = None, + violence_level: int = 0, + eroticism_level: int = 0, ) -> list[Dict]: system_prompt = ( "你是一个专业的故事创作助手。请根据给定的故事信息和角色列表,创作章节大纲。\n" @@ -295,12 +306,15 @@ class LLMService: ) genre_label = f"{genre}{'/' + subgenre if subgenre else ''}" char_names = [c.get("name", "") for c in characters if c.get("name") not in ("narrator", "旁白")] + violence_note = f"暴力程度:{violence_level}/10\n" if violence_level > 0 else "" + eroticism_note = f"色情程度:{eroticism_level}/10\n" if eroticism_level > 0 else "" user_message = ( f"故事类型:{genre_label}\n" + (f"风格:{style}\n" if style else "") + f"故事简介:{premise}\n" f"主要角色:{', '.join(char_names)}\n" - f"请创作 {num_chapters} 章的大纲。" + + violence_note + eroticism_note + + f"请创作 {num_chapters} 章的大纲。" ) result = await self.stream_chat_json(system_prompt, user_message, max_tokens=4096, usage_callback=usage_callback) return result.get("chapters", []) @@ -315,6 +329,8 @@ class LLMService: characters: list[Dict], on_token=None, usage_callback: Optional[Callable[[int, int], None]] = None, + violence_level: int = 0, + eroticism_level: int = 0, ) -> str: char_names = [c.get("name", "") for c in characters if c.get("name") not in ("narrator", "旁白")] names_str = "、".join(char_names) @@ -337,10 +353,13 @@ class LLMService: "- 每行为一个独立片段,不要有空行\n" "- 直接输出脚本内容,不要有其他说明文字" ) + violence_note = f"暴力程度:{violence_level}/10\n" if violence_level > 0 else "" + eroticism_note = f"色情程度:{eroticism_level}/10\n" if eroticism_level > 0 else "" user_message = ( f"故事类型:{genre}\n" - f"故事简介:{premise}\n\n" - f"第 {chapter_index + 1} 章:{chapter_title}\n" + f"故事简介:{premise}\n" + + violence_note + eroticism_note + + f"\n第 {chapter_index + 1} 章:{chapter_title}\n" f"章节内容:{chapter_summary}\n\n" "请创作这一章的完整对话脚本,包含旁白叙述和角色对话,内容充实,段落自然流畅。" ) @@ -358,6 +377,8 @@ class LLMService: additional_chapters: int, characters: list[Dict], usage_callback: Optional[Callable[[int, int], None]] = None, + violence_level: int = 0, + eroticism_level: int = 0, ) -> list[Dict]: system_prompt = ( "你是一个专业的故事创作助手。请根据已有章节大纲,续写新的章节大纲。\n" @@ -373,12 +394,15 @@ class LLMService: f"第{ch.get('index', i) + 1}章「{ch.get('title', '')}」:{ch.get('summary', '')}" for i, ch in enumerate(existing_chapters) ) + violence_note = f"暴力程度:{violence_level}/10\n" if violence_level > 0 else "" + eroticism_note = f"色情程度:{eroticism_level}/10\n" if eroticism_level > 0 else "" user_message = ( f"故事类型:{genre_label}\n" + (f"风格:{style}\n" if style else "") + f"故事简介:{premise}\n" - f"主要角色:{', '.join(char_names)}\n\n" - f"已有章节大纲(共{len(existing_chapters)}章):\n{existing_summary}\n\n" + f"主要角色:{', '.join(char_names)}\n" + + violence_note + eroticism_note + + f"\n已有章节大纲(共{len(existing_chapters)}章):\n{existing_summary}\n\n" f"请从第{start_index}章(索引{start_index})开始,续写{additional_chapters}章大纲,剧情要承接上文。" ) result = await self.stream_chat_json(system_prompt, user_message, max_tokens=4096, usage_callback=usage_callback) diff --git a/qwen3-tts-backend/schemas/audiobook.py b/qwen3-tts-backend/schemas/audiobook.py index 822bb0f..ba72d7a 100644 --- a/qwen3-tts-backend/schemas/audiobook.py +++ b/qwen3-tts-backend/schemas/audiobook.py @@ -17,6 +17,8 @@ class SynopsisGenerationRequest(BaseModel): conflict_scale: str = "" num_characters: int = 5 num_chapters: int = 8 + violence_level: int = 0 + eroticism_level: int = 0 class ScriptGenerationRequest(BaseModel): @@ -27,6 +29,8 @@ class ScriptGenerationRequest(BaseModel): style: str = "" num_characters: int = 5 num_chapters: int = 8 + violence_level: int = 0 + eroticism_level: int = 0 class AudiobookProjectResponse(BaseModel): @@ -141,6 +145,8 @@ class NsfwSynopsisGenerationRequest(BaseModel): conflict_scale: str = "" num_characters: int = 5 num_chapters: int = 8 + violence_level: int = 0 + eroticism_level: int = 0 class NsfwScriptGenerationRequest(BaseModel): @@ -151,3 +157,5 @@ class NsfwScriptGenerationRequest(BaseModel): style: str = "" num_characters: int = 5 num_chapters: int = 8 + violence_level: int = 0 + eroticism_level: int = 0 diff --git a/qwen3-tts-frontend/src/lib/api/audiobook.ts b/qwen3-tts-frontend/src/lib/api/audiobook.ts index 5855ef8..21760fd 100644 --- a/qwen3-tts-frontend/src/lib/api/audiobook.ts +++ b/qwen3-tts-frontend/src/lib/api/audiobook.ts @@ -8,6 +8,8 @@ export interface SynopsisGenerationRequest { conflict_scale?: string num_characters?: number num_chapters?: number + violence_level?: number + eroticism_level?: number } export interface ScriptGenerationRequest { @@ -18,6 +20,8 @@ export interface ScriptGenerationRequest { style?: string num_characters?: number num_chapters?: number + violence_level?: number + eroticism_level?: number } export interface AudiobookProject { @@ -86,6 +90,8 @@ export interface NsfwSynopsisGenerationRequest { conflict_scale?: string num_characters?: number num_chapters?: number + violence_level?: number + eroticism_level?: number } export interface NsfwScriptGenerationRequest { @@ -96,6 +102,8 @@ export interface NsfwScriptGenerationRequest { style?: string num_characters?: number num_chapters?: number + violence_level?: number + eroticism_level?: number } export const audiobookApi = { diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx index 7666091..3a35dd3 100644 --- a/qwen3-tts-frontend/src/pages/Audiobook.tsx +++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx @@ -526,6 +526,8 @@ function AIScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose: const [conflictScale, setConflictScale] = useState('') const [numCharacters, setNumCharacters] = useState(5) const [numChapters, setNumChapters] = useState(8) + const [violenceLevel, setViolenceLevel] = useState(0) + const [eroticismLevel, setEroticismLevel] = useState(0) const [synopsis, setSynopsis] = useState('') const [generatingSynopsis, setGeneratingSynopsis] = useState(false) const [loading, setLoading] = useState(false) @@ -536,7 +538,7 @@ function AIScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose: const reset = () => { setTitle(''); setGenre(''); setSubgenre(''); setProtagonistType(''); setTone('') - setConflictScale(''); setNumCharacters(5); setNumChapters(8); setSynopsis('') + setConflictScale(''); setNumCharacters(5); setNumChapters(8); setViolenceLevel(0); setEroticismLevel(0); setSynopsis('') } const handleGenreSelect = (g: string) => { @@ -558,6 +560,8 @@ function AIScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose: conflict_scale: conflictScale, num_characters: numCharacters, num_chapters: numChapters, + violence_level: violenceLevel, + eroticism_level: eroticismLevel, }) setSynopsis(result) } catch (e: any) { @@ -580,6 +584,8 @@ function AIScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose: style: tone, num_characters: numCharacters, num_chapters: numChapters, + violence_level: violenceLevel, + eroticism_level: eroticismLevel, } as ScriptGenerationRequest) toast.success('AI剧本生成任务已创建') reset() @@ -664,6 +670,17 @@ function AIScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose: +
+ + +
+
+
+ + +
+