From 6eb521dee4925e11e58684b3d7dedb23621dcecc Mon Sep 17 00:00:00 2001 From: bdim404 Date: Fri, 13 Mar 2026 11:42:30 +0800 Subject: [PATCH] feat: add synopsis generation endpoint and frontend integration --- qwen3-tts-backend/api/audiobook.py | 42 +++ qwen3-tts-backend/core/audiobook_service.py | 1 + qwen3-tts-backend/schemas/audiobook.py | 10 + qwen3-tts-frontend/src/lib/api/audiobook.ts | 15 + qwen3-tts-frontend/src/pages/Audiobook.tsx | 344 +++++++++++++++++--- 5 files changed, 360 insertions(+), 52 deletions(-) diff --git a/qwen3-tts-backend/api/audiobook.py b/qwen3-tts-backend/api/audiobook.py index 3508490..74466a3 100644 --- a/qwen3-tts-backend/api/audiobook.py +++ b/qwen3-tts-backend/api/audiobook.py @@ -24,6 +24,7 @@ from schemas.audiobook import ( AudiobookGenerateRequest, AudiobookAnalyzeRequest, ScriptGenerationRequest, + SynopsisGenerationRequest, ) from core.config import settings @@ -152,6 +153,47 @@ async def list_projects( return [_project_to_response(p) for p in projects] +@router.post("/projects/generate-synopsis") +async def generate_synopsis( + data: SynopsisGenerationRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + from db.crud import get_system_setting + if not get_system_setting(db, "llm_api_key") or not get_system_setting(db, "llm_base_url") or not get_system_setting(db, "llm_model"): + raise HTTPException(status_code=400, detail="LLM config not set. Please configure LLM API key first.") + + from core.audiobook_service import _get_llm_service + llm = _get_llm_service(db) + + system_prompt = ( + "你是一位专业的小说策划师,擅长根据创作参数生成引人入胜的故事简介。" + "请根据用户提供的类型、风格、主角、冲突等参数,生成一段200-400字的中文故事简介。" + "简介需涵盖:世界观背景、主角基本情况、核心矛盾冲突、故事基调。" + "直接输出简介正文,不要加任何前缀标题或说明文字。" + ) + parts = [f"类型:{data.genre}"] + if data.subgenre: + parts.append(f"子类型:{data.subgenre}") + if data.protagonist_type: + parts.append(f"主角类型:{data.protagonist_type}") + if data.tone: + parts.append(f"故事基调:{data.tone}") + if data.conflict_scale: + parts.append(f"冲突规模:{data.conflict_scale}") + parts.append(f"角色数量:约{data.num_characters}个主要角色") + parts.append(f"故事体量:约{data.num_chapters}章") + user_message = "\n".join(parts) + "\n\n请生成故事简介:" + + try: + synopsis = await llm.chat(system_prompt, user_message) + except Exception as e: + logger.error(f"Synopsis generation failed: {e}") + raise HTTPException(status_code=500, detail=f"LLM generation failed: {str(e)}") + + return {"synopsis": synopsis} + + @router.post("/projects/generate-script", response_model=AudiobookProjectResponse, status_code=status.HTTP_201_CREATED) async def create_ai_script_project( data: ScriptGenerationRequest, diff --git a/qwen3-tts-backend/core/audiobook_service.py b/qwen3-tts-backend/core/audiobook_service.py index cb7ed8a..2bdda2c 100644 --- a/qwen3-tts-backend/core/audiobook_service.py +++ b/qwen3-tts-backend/core/audiobook_service.py @@ -347,6 +347,7 @@ async def generate_ai_script_chapters(project_id: int, user: User, db: Session) key = str(project_id) ps.reset(key) + crud.update_audiobook_project_status(db, project_id, "analyzing") cfg = project.script_config try: diff --git a/qwen3-tts-backend/schemas/audiobook.py b/qwen3-tts-backend/schemas/audiobook.py index d2196f2..e985ef7 100644 --- a/qwen3-tts-backend/schemas/audiobook.py +++ b/qwen3-tts-backend/schemas/audiobook.py @@ -9,6 +9,16 @@ class AudiobookProjectCreate(BaseModel): source_text: Optional[str] = None +class SynopsisGenerationRequest(BaseModel): + genre: str + subgenre: str = "" + protagonist_type: str = "" + tone: str = "" + conflict_scale: str = "" + num_characters: int = 5 + num_chapters: int = 8 + + class ScriptGenerationRequest(BaseModel): title: str genre: str diff --git a/qwen3-tts-frontend/src/lib/api/audiobook.ts b/qwen3-tts-frontend/src/lib/api/audiobook.ts index 5ff612d..c52cb44 100644 --- a/qwen3-tts-frontend/src/lib/api/audiobook.ts +++ b/qwen3-tts-frontend/src/lib/api/audiobook.ts @@ -1,5 +1,15 @@ import apiClient from '@/lib/api' +export interface SynopsisGenerationRequest { + genre: string + subgenre?: string + protagonist_type?: string + tone?: string + conflict_scale?: string + num_characters?: number + num_chapters?: number +} + export interface ScriptGenerationRequest { title: string genre: string @@ -69,6 +79,11 @@ export interface LLMConfig { } export const audiobookApi = { + generateSynopsis: async (data: SynopsisGenerationRequest): Promise => { + const response = await apiClient.post<{ synopsis: string }>('/audiobook/projects/generate-synopsis', data) + return response.data.synopsis + }, + createAIScript: async (data: ScriptGenerationRequest): Promise => { const response = await apiClient.post('/audiobook/projects/generate-script', data) return response.data diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx index ae2219d..e228264 100644 --- a/qwen3-tts-frontend/src/pages/Audiobook.tsx +++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx @@ -11,6 +11,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u import { Navbar } from '@/components/Navbar' import { AudioPlayer } from '@/components/AudioPlayer' import { audiobookApi, type AudiobookProject, type AudiobookProjectDetail, type AudiobookCharacter, type AudiobookSegment, type ScriptGenerationRequest } from '@/lib/api/audiobook' +import { RotateCcw } from 'lucide-react' import apiClient, { formatApiError, adminApi } from '@/lib/api' import { useAuth } from '@/contexts/AuthContext' @@ -352,34 +353,231 @@ function CreateProjectDialog({ open, onClose, onCreated }: { open: boolean; onCl ) } -const GENRE_OPTIONS = ['玄幻', '武侠', '仙侠', '现代言情', '都市', '悬疑', '科幻', '历史', '恐怖'] +interface SubgenreConfig { + protagonistTypes: string[] + tones: string[] + conflictScales: string[] +} + +interface GenreGroup { + label: string + subgenres: Record +} + +const GENRE_CONFIGS: Record = { + '玄幻': { + label: '玄幻', + subgenres: { + '高武玄幻': { protagonistTypes: ['热血少年', '落魄天才', '废材逆袭', '穿越者', '系统宿主', '天生废体'], tones: ['热血激情', '升级爽文', '黑暗血腥', '轻松搞笑', '史诗宏大'], conflictScales: ['门派争斗', '宗门战争', '诸天争霸', '个人成长', '复仇雪耻'] }, + '修仙升级': { protagonistTypes: ['散修弟子', '资质平庸者', '天才修炼者', '转世神魂', '炼丹师', '剑修'], tones: ['清修飘逸', '热血激战', '奇幻神秘', '恩怨情仇', '历练成长'], conflictScales: ['渡劫飞升', '门派恩怨', '天道之争', '仙魔对决', '灵脉争夺'] }, + '洪荒流': { protagonistTypes: ['洪荒神兽', '人族菜鸡', '太古妖族', '上古大能', '仙界强者', '混沌生灵'], tones: ['宏大史诗', '爽文热血', '阴谋算计', '轻松恶搞', '诸圣博弈'], conflictScales: ['洪荒争霸', '诸圣博弈', '天道争夺', '人族崛起', '封神大战'] }, + '诸天万界': { protagonistTypes: ['穿梭者', '任务系统宿主', '时空旅行者', '多界强者', '轮回者'], tones: ['热血爽文', '智谋流', '搞笑无厘头', '严肃史诗', '全能碾压'], conflictScales: ['完成任务', '诸界争霸', '宇宙级威胁', '推翻天道', '救世传承'] }, + '异世大陆': { protagonistTypes: ['穿越者', '异世贵族', '平民逆袭', '战神转世', '魔法天才', '剑神传人'], tones: ['热血冒险', '宫廷权谋', '温馨治愈', '黑暗复仇', '轻松奇幻'], conflictScales: ['王国争霸', '异族入侵', '魔兽泛滥', '政治阴谋', '个人成长'] }, + }, + }, + '武侠': { + label: '武侠', + subgenres: { + '传统武侠': { protagonistTypes: ['江湖侠客', '名门弟子', '落魄世家子', '武学天才', '复仇者', '隐世高人'], tones: ['豪情壮志', '快意恩仇', '爱恨情仇', '正邪对决', '侠义热血'], conflictScales: ['门派之争', '江湖恩怨', '家仇国恨', '武林正邪', '武功秘籍争夺'] }, + '奇幻武侠': { protagonistTypes: ['习得神功者', '寻宝冒险者', '侠义少年', '隐世高手', '机缘得道者'], tones: ['奇幻冒险', '武侠浪漫', '热血激战', '神秘探索', '奇遇成长'], conflictScales: ['神兵寻觅', '秘籍争夺', '奇遇成长', '救世任务', '阵法宝物'] }, + '现代武侠': { protagonistTypes: ['隐居高手', '都市侠客', '特种兵出身者', '传承者', '武道研究者'], tones: ['现代都市感', '热血写实', '轻松幽默', '动作爽快', '家国情怀'], conflictScales: ['维护正义', '黑道对抗', '传承保护', '功夫切磋', '古武势力'] }, + }, + }, + '仙侠': { + label: '仙侠', + subgenres: { + '东方修真': { protagonistTypes: ['凡人弟子', '仙门弟子', '落魄仙人', '天骄后裔', '妖族转化', '古神血脉'], tones: ['清逸飘渺', '热血激战', '唯美浪漫', '奇幻神秘', '历练成长'], conflictScales: ['渡劫飞升', '仙魔大战', '道侣情缘', '天机命运', '宗门争霸'] }, + '剑仙传说': { protagonistTypes: ['剑道天才', '剑修散仙', '古剑传人', '剑灵融合者', '剑心通明者'], tones: ['飘逸洒脱', '剑意凛冽', '纵横江湖', '诗意浪漫', '独孤求败'], conflictScales: ['剑道证道', '至宝争夺', '仙凡之别', '剑与心境', '天下第一'] }, + '斩妖除魔': { protagonistTypes: ['除魔使者', '天师传人', '驱魔术士', '正道大侠', '神选之人'], tones: ['热血正义', '悬疑恐怖', '神话奇幻', '黑暗压抑', '守护信念'], conflictScales: ['妖魔肆虐', '鬼怪作乱', '邪道崛起', '地狱封印', '天地失衡'] }, + }, + }, + '现代言情': { + label: '现代言情', + subgenres: { + '甜宠': { protagonistTypes: ['普通女孩', '天真少女', '职场新人', '邻家女孩', '意外相遇者'], tones: ['甜蜜温馨', '欢笑爱情', '轻松浪漫', '治愈温暖', '撒糖日常'], conflictScales: ['误会化解', '追爱历程', '霸总求爱', '青涩初恋', '双向暗恋'] }, + '虐恋': { protagonistTypes: ['坚强女主', '深情错付者', '误解中的恋人', '身份悬殊者', '宿命相遇者'], tones: ['虐心煎熬', '深情执着', '痛苦成长', '悲剧唯美', '破镜重圆'], conflictScales: ['身世误解', '爱而不得', '命运阻隔', '家族反对', '情感纠葛'] }, + '婚姻生活': { protagonistTypes: ['已婚夫妻', '闪婚陌生人', '失婚重来者', '中年危机者', '再婚家庭成员'], tones: ['温馨现实', '矛盾磨合', '生活感悟', '烟火气息', '相濡以沫'], conflictScales: ['婚姻危机', '重建感情', '第三者风波', '家庭矛盾', '育儿分歧'] }, + '青春校园': { protagonistTypes: ['高中生', '大学生', '校园社团成员', '学霸', '运动员'], tones: ['青涩懵懂', '阳光活力', '笑泪交织', '追梦热血', '暗恋悸动'], conflictScales: ['学习压力', '暗恋表白', '友情裂痕', '家庭变故', '比赛竞争'] }, + }, + }, + '都市': { + label: '都市', + subgenres: { + '系统流': { protagonistTypes: ['普通小人物', '穿越重生者', '选中之人', '落魄天才', '被系统选中者'], tones: ['爽文升级', '轻松幽默', '热血励志', '全能碾压', '种田发展'], conflictScales: ['系统任务', '快速崛起', '实力碾压', '逆袭人生', '征服巅峰'] }, + '商战': { protagonistTypes: ['商界精英', '白手起家者', '家族继承人', '职场新人', '商业奇才'], tones: ['紧张激烈', '智谋对抗', '励志奋斗', '尔虞我诈', '商业传奇'], conflictScales: ['商业竞争', '家族权斗', '职场博弈', '资本角力', '企业并购'] }, + '重生逆袭': { protagonistTypes: ['重生者', '回到过去者', '带着记忆重来者', '复仇归来者', '先知者'], tones: ['爽文复仇', '轻松改变命运', '深情弥补', '步步为营', '大杀四方'], conflictScales: ['前世遗恨', '命运改写', '天才崛起', '恩仇清算', '守护挚爱'] }, + '官场': { protagonistTypes: ['基层公务员', '仕途奋斗者', '清官强者', '改革者', '下乡干部'], tones: ['权谋斗争', '写实主义', '正义坚守', '仕途险恶', '为民请命'], conflictScales: ['官场倾轧', '腐败反贪', '仕途升迁', '民生为先', '政治博弈'] }, + }, + }, + '悬疑': { + label: '悬疑', + subgenres: { + '推理侦探': { protagonistTypes: ['警探', '法医', '业余侦探', '记者', '侦探助手', '神探'], tones: ['烧脑推理', '扣人心弦', '悬念丛生', '冷静理性', '层层剥茧'], conflictScales: ['连环命案', '密室谜题', '真相揭露', '罪犯追踪', '系列悬案'] }, + '犯罪惊悚': { protagonistTypes: ['警察', '罪犯视角', '受害者幸存者', '卧底', '特工'], tones: ['黑暗紧张', '写实血腥', '心理较量', '暗流涌动', '生死边缘'], conflictScales: ['犯罪追击', '身份揭露', '背叛与救赎', '生死博弈', '组织渗透'] }, + '灵异恐怖': { protagonistTypes: ['普通人', '灵异体质者', '道士法师', '鬼怪', '民俗学者'], tones: ['恐怖诡异', '悬疑惊悚', '志怪神秘', '民俗风情', '冷汗直冒'], conflictScales: ['恶灵纠缠', '冤情揭露', '鬼怪作祟', '阴阳两界', '封印破解'] }, + '心理悬疑': { protagonistTypes: ['心理咨询师', '精神科医生', '犯罪侧写师', '创伤幸存者', '不可靠叙述者'], tones: ['心理压迫', '黑暗深沉', '哲思反思', '人性剖析', '迷雾重重'], conflictScales: ['心魔挣脱', '人格分裂', '记忆迷失', '操控与反制', '真相解谜'] }, + }, + }, + '科幻': { + label: '科幻', + subgenres: { + '星际战争': { protagonistTypes: ['星际舰长', '星际士兵', '精英飞行员', '星际难民', '外星协调者'], tones: ['史诗宏大', '战争残酷', '科技未来', '人性探索', '军事硬派'], conflictScales: ['星际战争', '殖民冲突', '物种灭绝威胁', '宇宙联邦争霸', '文明碰撞'] }, + '末世废土': { protagonistTypes: ['末世幸存者', '变异人类', '反抗组织领袖', '科学家', '拾荒者'], tones: ['黑暗压抑', '生存挣扎', '希望微光', '残酷现实', '人性测试'], conflictScales: ['末世求生', '物资争夺', '反抗统治', '文明重建', '变异扩散'] }, + '赛博朋克': { protagonistTypes: ['黑客', '赛博战士', '企业异见者', '增强人类', '数字游民'], tones: ['科技反乌托邦', '阴暗迷幻', '反体制', '霓虹暗夜', '数字哲学'], conflictScales: ['数据战争', '企业阴谋', '人机界限', '身份认同', '系统颠覆'] }, + '时间旅行': { protagonistTypes: ['时间旅行者', '时间管理局成员', '意外穿越者', '历史修正者', '时间悖论受害者'], tones: ['烧脑悬疑', '蝴蝶效应', '时间悖论', '浪漫跨时代', '命运游戏'], conflictScales: ['历史修正', '时间线保护', '因果悖论', '多重未来', '时间战争'] }, + '人工智能': { protagonistTypes: ['AI研究员', '觉醒AI', '人机协作者', '反AI组织成员', '数字意识体'], tones: ['哲学反思', '科技惊悚', '情感探索', '伦理困境', '奇点到来'], conflictScales: ['AI觉醒', '人机战争', '意识复制', '伦理边界', '数字文明'] }, + }, + }, + '历史': { + label: '历史', + subgenres: { + '架空历史': { protagonistTypes: ['穿越者', '改变历史者', '默默无闻小人物', '穿越官员', '现代人'], tones: ['宏大史诗', '权谋斗争', '家国情怀', '轻松穿越', '历史重构'], conflictScales: ['朝代更迭', '权力争夺', '外族入侵', '历史改变', '改革维新'] }, + '宫廷斗争': { protagonistTypes: ['妃嫔', '皇子', '太监谋士', '宫女逆袭者', '皇后'], tones: ['心机深沉', '权谋博弈', '悲情凄美', '步步惊心', '宫廷浮沉'], conflictScales: ['后宫争宠', '皇位争夺', '废立之争', '家族联姻', '宫廷政变'] }, + '战争史诗': { protagonistTypes: ['将领', '士兵', '谋士', '王侯', '战地医者'], tones: ['悲壮雄浑', '铁血热血', '英雄主义', '战争残酷', '家国大义'], conflictScales: ['国家存亡', '统一战争', '抗击外敌', '乱世争雄', '以少胜多'] }, + '历史正剧': { protagonistTypes: ['历史名人', '草根智者', '政治家', '民间义士', '文人墨客'], tones: ['厚重写实', '人文感悟', '历史还原', '时代悲歌', '英雄气节'], conflictScales: ['时代变革', '个人命运', '历史洪流', '民族大义', '文化传承'] }, + }, + }, + '恐怖': { + label: '恐怖', + subgenres: { + '都市灵异': { protagonistTypes: ['普通市民', '灵异体质者', '鬼怪猎人', '民俗研究者', '意外卷入者'], tones: ['惊悚诡异', '都市传说', '黑暗压抑', '悬疑恐惧', '无处可逃'], conflictScales: ['都市怪谈', '恶灵作祟', '冤魂纠缠', '超自然事件', '禁忌揭秘'] }, + '克苏鲁': { protagonistTypes: ['学者研究者', '猎奇者', '受选者', '意志坚强者', '古神信徒'], tones: ['宇宙级恐惧', '理智崩溃', '末日预感', '深渊凝视', '存在主义恐怖'], conflictScales: ['禁忌召唤', '理性崩溃', '古神苏醒', '深渊凝视', '人类渺小'] }, + '鬼屋探险': { protagonistTypes: ['探险者', '灵媒', '记者', '大胆学生', '真相追寻者'], tones: ['极度紧张', '恐怖刺激', '生死挣扎', '诡异氛围', '密室求生'], conflictScales: ['怨灵诅咒', '脱离险境', '真相揭露', '诡异机关', '黑暗守护者'] }, + '丧尸末日': { protagonistTypes: ['末日幸存者', '军人', '科学家', '普通家庭', '幸运儿'], tones: ['生存惊悚', '人性黑暗', '绝望与希望', '末日互助', '道德崩塌'], conflictScales: ['丧尸追击', '幸存者冲突', '疫苗寻找', '安全区争夺', '文明坚守'] }, + }, + }, + 'Fantasy': { + label: 'Fantasy', + subgenres: { + 'High Fantasy': { protagonistTypes: ['Chosen One', 'Magic User', 'Knight/Warrior', 'Royal Heir', 'Common Hero', 'Prophesied One'], tones: ['Epic', 'Heroic', 'Noble', 'Mythic', 'Adventure'], conflictScales: ['Personal Quest', 'Kingdom-wide', 'World-saving', 'Good vs Evil', 'Political Intrigue'] }, + 'Dark Fantasy': { protagonistTypes: ['Antihero', 'Cursed Individual', 'Monster Hunter', 'Corrupted Noble', 'Reluctant Chosen One', 'Morally Gray Wizard'], tones: ['Grim', 'Morally Ambiguous', 'Horror-tinged', 'Psychological', 'Brutal'], conflictScales: ['Personal Survival', 'Moral Choice', 'Power Corruption', 'Existential Horror', 'Societal Decay'] }, + 'Urban Fantasy': { protagonistTypes: ['Magical Detective', 'Hidden World Guardian', 'Modern Witch/Wizard', 'Supernatural Creature', 'Normal Person Discovered', 'Magic Shop Owner'], tones: ['Modern', 'Mysterious', 'Action-packed', 'Detective Noir', 'Secret World'], conflictScales: ['Personal', 'City-wide', 'Hidden Society', 'Supernatural Politics', 'World-threatening'] }, + 'Sword and Sorcery': { protagonistTypes: ['Warrior', 'Rogue', 'Barbarian', 'Mercenary', 'Wandering Wizard', 'Treasure Hunter'], tones: ['Adventurous', 'Gritty', 'Action-focused', 'Pulp', 'Personal'], conflictScales: ['Personal Gain', 'Adventure', 'Survival', 'Quest', 'Local Threat'] }, + 'Mythic Fantasy': { protagonistTypes: ['Demigod', 'Legendary Hero', 'Oracle', 'Divine Champion', 'Monster Slayer', 'Mythical Creature'], tones: ['Legendary', 'Epic', 'Mythological', 'Divine', 'Larger than Life'], conflictScales: ['Divine Politics', 'Legendary Quests', 'Fate of Gods', 'Mythic Prophecy', 'World-shaping'] }, + 'Fairy Tale': { protagonistTypes: ['Innocent Hero', 'Clever Trickster', 'Transformed Being', 'Royal Figure', 'Magical Helper', 'Common Person'], tones: ['Whimsical', 'Moral', 'Magical', 'Traditional', 'Transformative'], conflictScales: ['Personal Journey', 'Moral Test', 'Magical Challenge', 'Kingdom Fate', 'Breaking Curses'] }, + }, + }, + 'Sci-Fi': { + label: 'Sci-Fi', + subgenres: { + 'Space Opera': { protagonistTypes: ['Military Officer', 'Merchant Captain', 'Explorer', 'Diplomat', 'Rebel Leader', 'Imperial Noble'], tones: ['Optimistic', 'Dark', 'Political', 'Adventure-focused', 'Character-driven'], conflictScales: ['Personal', 'Planetary', 'Interstellar', 'Galactic', 'Species Survival'] }, + 'Cyberpunk': { protagonistTypes: ['Hacker', 'Street Mercenary', 'Corporate Defector', 'AI Researcher', 'Underground Activist', 'Augmented Human'], tones: ['Noir', 'Gritty', 'Anti-establishment', 'Dystopian', 'Tech-noir'], conflictScales: ['Personal', 'Street Level', 'Corporate', 'Systemic', 'Digital'] }, + 'Post-Apocalyptic': { protagonistTypes: ['Survivor', 'Wasteland Warrior', 'Community Leader', 'Scavenger', 'Medic', 'Former Military'], tones: ['Grim', 'Survival-focused', 'Hope in Darkness', 'Brutal', 'Revolutionary'], conflictScales: ['Personal Survival', 'Group Survival', 'Resource Control', 'Territory', 'Rebuilding Society'] }, + 'Hard Sci-Fi': { protagonistTypes: ['Scientist', 'Engineer', 'Astronaut', 'Research Team Leader', 'AI Researcher', 'Technical Specialist'], tones: ['Technical', 'Philosophical', 'Discovery-focused', 'Methodical', 'Realistic'], conflictScales: ['Personal', 'Technical', 'Scientific Discovery', 'Environmental', 'Existential'] }, + 'Biopunk': { protagonistTypes: ['Genetic Engineer', 'Modified Human', 'Underground Scientist', 'Corporate Whistleblower', 'Bio-hacker', 'Test Subject'], tones: ['Body Horror', 'Ethical Drama', 'Scientific', 'Anti-corporate', 'Transformative'], conflictScales: ['Personal', 'Medical', 'Ethical', 'Corporate', 'Species-wide'] }, + 'Time Travel': { protagonistTypes: ['Time Agent', 'Accidental Traveler', 'Historical Researcher', 'Timeline Guardian', 'Temporal Engineer'], tones: ['Complex', 'Mysterious', 'Philosophical', 'Adventure', 'Causality-focused'], conflictScales: ['Personal', 'Historical', 'Timeline Preservation', 'Paradox Prevention', 'Multi-temporal'] }, + }, + }, + 'Mystery': { + label: 'Mystery', + subgenres: { + 'Cozy Mystery': { protagonistTypes: ['Amateur Detective', 'Librarian', 'Shop Owner', 'Retired Professional', 'Local Resident', 'Hobby Enthusiast'], tones: ['Gentle', 'Puzzle-focused', 'Community-centered', 'Cozy', 'Character-driven'], conflictScales: ['Personal Mystery', 'Community Secret', 'Local Crime', 'Family Mystery', 'Historical Puzzle'] }, + 'Police Procedural': { protagonistTypes: ['Police Detective', 'Forensic Specialist', 'Police Captain', 'Crime Scene Investigator', 'FBI Agent'], tones: ['Realistic', 'Procedural', 'Professional', 'Methodical', 'Team-focused'], conflictScales: ['Individual Cases', 'Serial Crimes', 'Organized Crime', 'Corruption', 'Major Investigations'] }, + 'Hard-boiled': { protagonistTypes: ['Private Detective', 'Ex-Cop', 'Cynical Investigator', 'Tough Guy', 'Street-smart Detective', 'Noir Hero'], tones: ['Noir', 'Cynical', 'Gritty', 'Dark', 'Atmospheric'], conflictScales: ['Personal Cases', 'Corruption', 'Urban Crime', 'Moral Choices', 'Survival'] }, + 'Psychological Mystery': { protagonistTypes: ['Psychologist', 'Troubled Detective', 'Mental Health Professional', 'Unreliable Narrator', 'Psychological Profiler'], tones: ['Psychological', 'Introspective', 'Mind-bending', 'Complex', 'Character-focused'], conflictScales: ['Mental Mysteries', 'Psychological Crimes', 'Identity Issues', 'Memory Problems', 'Perception Puzzles'] }, + 'Historical Mystery': { protagonistTypes: ['Period Detective', 'Historical Figure', 'Scholar', 'Period Professional', 'Aristocrat', 'Common Person'], tones: ['Historical', 'Authentic', 'Period-appropriate', 'Cultural', 'Educational'], conflictScales: ['Personal Mysteries', 'Historical Events', 'Period Crimes', 'Social Issues', 'Political Intrigue'] }, + }, + }, + 'Horror': { + label: 'Horror', + subgenres: { + 'Gothic Horror': { protagonistTypes: ['Haunted Individual', 'Investigator', 'Innocent Victim', 'Cursed Person', 'Gothic Hero', 'Tormented Soul'], tones: ['Atmospheric', 'Psychological', 'Brooding', 'Mysterious', 'Melancholic'], conflictScales: ['Personal Haunting', 'Family Curse', 'Supernatural Threat', 'Psychological Terror', 'Ancient Evil'] }, + 'Cosmic Horror': { protagonistTypes: ['Academic Researcher', 'Occult Investigator', 'Unwitting Scholar', 'Cosmic Witness', 'Doomed Explorer', 'Sanity-threatened Individual'], tones: ['Existential', 'Unknowable', 'Cosmic', 'Dread-filled', 'Mind-breaking'], conflictScales: ['Cosmic Revelation', 'Sanity Destruction', 'Reality Breakdown', 'Ancient Awakening', 'Existential Horror'] }, + 'Psychological Horror': { protagonistTypes: ['Unreliable Narrator', 'Mentally Unstable', 'Paranoid Individual', 'Trauma Victim', 'Isolated Person'], tones: ['Psychological', 'Disturbing', 'Mind-bending', 'Paranoid', 'Introspective'], conflictScales: ['Mental Breakdown', 'Reality Distortion', 'Psychological Manipulation', 'Internal Terror', 'Sanity Loss'] }, + 'Supernatural Horror': { protagonistTypes: ['Paranormal Investigator', 'Haunted Individual', 'Psychic Medium', 'Skeptical Researcher', 'Spiritual Warrior', 'Innocent Victim'], tones: ['Supernatural', 'Eerie', 'Spiritual', 'Otherworldly', 'Paranormal'], conflictScales: ['Ghostly Haunting', 'Demonic Possession', 'Spiritual Warfare', 'Paranormal Investigation', 'Supernatural Threat'] }, + 'Slasher Horror': { protagonistTypes: ['Final Girl', 'Survivor', 'Potential Victim', 'Group Member', 'Resourceful Fighter'], tones: ['Suspenseful', 'Violent', 'Survival-focused', 'Intense', 'Action-horror'], conflictScales: ['Survival', 'Killer Hunt', 'Group Elimination', 'Escape Attempt', 'Final Confrontation'] }, + }, + }, + 'Thriller': { + label: 'Thriller', + subgenres: { + 'Espionage Thriller': { protagonistTypes: ['Secret Agent', 'Intelligence Officer', 'Double Agent', 'Spy Handler', 'Undercover Operative', 'Government Analyst'], tones: ['Sophisticated', 'International', 'High-stakes', 'Political', 'Covert'], conflictScales: ['International Conspiracy', 'Government Secrets', 'Spy Networks', 'National Security', 'Global Politics'] }, + 'Psychological Thriller': { protagonistTypes: ['Psychologist', 'Mentally Unstable', 'Manipulation Victim', 'Paranoid Individual', 'Mind Game Player', 'Psychological Profiler'], tones: ['Psychological', 'Mind-bending', 'Paranoid', 'Manipulative', 'Disturbing'], conflictScales: ['Mental Manipulation', 'Psychological Torture', 'Mind Control', 'Paranoid Delusions', 'Reality Breakdown'] }, + 'Action Thriller': { protagonistTypes: ['Action Hero', 'Special Forces', 'Mercenary', 'Bodyguard', 'Martial Artist'], tones: ['High-energy', 'Action-packed', 'Adrenaline-fueled', 'Physical', 'Fast-paced'], conflictScales: ['Physical Confrontation', 'High-speed Chases', 'Combat Situations', 'Rescue Missions', 'Survival Scenarios'] }, + 'Legal Thriller': { protagonistTypes: ['Lawyer', 'Judge', 'Legal Investigator', 'Prosecutor', 'Defense Attorney', 'Legal Whistleblower'], tones: ['Legal', 'Procedural', 'Justice-focused', 'Institutional', 'Courtroom-driven'], conflictScales: ['Legal Conspiracy', 'Courtroom Drama', 'Justice Corruption', 'Legal Cover-up', 'Judicial Manipulation'] }, + 'Techno-Thriller': { protagonistTypes: ['Computer Expert', 'Cyber Security Specialist', 'Tech Entrepreneur', 'Hacker', 'Scientist', 'Digital Investigator'], tones: ['High-tech', 'Fast-paced', 'Technical', 'Futuristic', 'Digital'], conflictScales: ['Cyber Attacks', 'Technological Threats', 'Digital Warfare', 'Scientific Disasters', 'Tech Conspiracies'] }, + 'Political Thriller': { protagonistTypes: ['Politician', 'Journalist', 'Government Insider', 'Whistleblower', 'Political Aide', 'Investigative Reporter'], tones: ['Political', 'Investigative', 'Institutional', 'Conspiratorial', 'Power-focused'], conflictScales: ['Political Corruption', 'Government Conspiracy', 'Electoral Manipulation', 'Institutional Cover-up', 'Power Abuse'] }, + }, + }, +} + +function Chip({ label, selected, onClick }: { label: string; selected: boolean; onClick: () => void }) { + return ( + + ) +} function AIScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose: () => void; onCreated: () => void }) { const [title, setTitle] = useState('') - const [genre, setGenre] = useState('玄幻') + const [genre, setGenre] = useState('') const [subgenre, setSubgenre] = useState('') - const [premise, setPremise] = useState('') - const [style, setStyle] = useState('') + const [protagonistType, setProtagonistType] = useState('') + const [tone, setTone] = useState('') + const [conflictScale, setConflictScale] = useState('') const [numCharacters, setNumCharacters] = useState(5) const [numChapters, setNumChapters] = useState(8) + const [synopsis, setSynopsis] = useState('') + const [generatingSynopsis, setGeneratingSynopsis] = useState(false) const [loading, setLoading] = useState(false) + const genreKeys = Object.keys(GENRE_CONFIGS) + const subgenreKeys = genre ? Object.keys(GENRE_CONFIGS[genre]?.subgenres ?? {}) : [] + const subgenreConfig = (genre && subgenre) ? GENRE_CONFIGS[genre]?.subgenres[subgenre] : null + const reset = () => { - setTitle(''); setGenre('玄幻'); setSubgenre(''); setPremise(''); setStyle('') - setNumCharacters(5); setNumChapters(8) + setTitle(''); setGenre(''); setSubgenre(''); setProtagonistType(''); setTone('') + setConflictScale(''); setNumCharacters(5); setNumChapters(8); setSynopsis('') + } + + const handleGenreSelect = (g: string) => { + setGenre(g); setSubgenre(''); setProtagonistType(''); setTone(''); setConflictScale(''); setSynopsis('') + } + const handleSubgenreSelect = (s: string) => { + setSubgenre(s); setProtagonistType(''); setTone(''); setConflictScale(''); setSynopsis('') + } + + const handleGenerateSynopsis = async () => { + if (!genre) { toast.error('请选择故事类型'); return } + setGeneratingSynopsis(true) + try { + const result = await audiobookApi.generateSynopsis({ + genre: subgenre ? `${genre} - ${subgenre}` : genre, + subgenre, + protagonist_type: protagonistType, + tone, + conflict_scale: conflictScale, + num_characters: numCharacters, + num_chapters: numChapters, + }) + setSynopsis(result) + } catch (e: any) { + toast.error(formatApiError(e)) + } finally { + setGeneratingSynopsis(false) + } } const handleCreate = async () => { if (!title) { toast.error('请输入作品标题'); return } - if (!premise) { toast.error('请输入故事简介'); return } + if (!synopsis) { toast.error('请先生成故事简介'); return } setLoading(true) try { await audiobookApi.createAIScript({ title, - genre, + genre: subgenre ? `${genre} - ${subgenre}` : genre, subgenre, - premise, - style, + premise: synopsis, + style: tone, num_characters: numCharacters, num_chapters: numChapters, } as ScriptGenerationRequest) @@ -396,59 +594,101 @@ function AIScriptDialog({ open, onClose, onCreated }: { open: boolean; onClose: return ( { if (!v) { reset(); onClose() } }}> - - + + AI 生成剧本 -
- setTitle(e.target.value)} /> -
- - setSubgenre(e.target.value)} - /> +
+
+

作品标题

+ setTitle(e.target.value)} />
-