@@ -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 < string , SubgenreConfig >
}
const GENRE_CONFIGS : Record < string , GenreGroup > = {
'玄幻' : {
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 (
< button
type = "button"
onClick = { onClick }
className = { ` px-2.5 py-1 rounded-full text-xs border transition-colors ${
selected
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background text-muted-foreground border-border hover:border-primary/50 hover:text-foreground'
} ` }
>
{ label }
< / button >
)
}
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 , setPremis e ] = useState ( '' )
const [ styl e, setStyl e ] = useState ( '' )
const [ protagonistType , setProtagonistTyp e ] = useState ( '' )
const [ ton e, setTon e ] = 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 ( '' ) ; setPremis e ( '' ) ; setStyl e ( '' )
setNumCharacters ( 5 ) ; setNumChapters ( 8 )
setTitle ( '' ) ; setGenre ( '' ) ; setSubgenre ( '' ) ; setProtagonistTyp e ( '' ) ; setTon e ( '' )
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 ( ! prem ise ) { toast . error ( '请输入 故事简介' ) ; return }
if ( ! synops is) { 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 (
< Dialog open = { open } onOpenChange = { v = > { if ( ! v ) { reset ( ) ; onClose ( ) } } } >
< DialogContent className = "sm:max-w-lg " >
< DialogHeader >
< DialogContent className = "sm:max-w-2xl max-h-[90vh] flex flex-co l" >
< DialogHeader className = "shrink-0" >
< DialogTitle > AI 生 成 剧 本 < / DialogTitle >
< / DialogHeader >
< div className = "space-y-3 pt -1" >
< Input placeholder = "作品标题" value = { title } onChange = { e = > setTitle ( e . target . value ) } / >
< div className = "flex gap-2" >
< select
className = "flex-1 h-9 rounded-md border border-input bg-background px-3 py-1 text-sm"
value = { genre }
onChange = { e = > setGenre ( e . target . value ) }
>
{ GENRE_OPTIONS . map ( g = > < option key = { g } value = { g } > { g } < / option > ) }
< / select >
< Input
className = "flex-1"
placeholder = "子类型(可选,如:升级流)"
value = { subgenre }
onChange = { e = > setSubgenre ( e . target . value ) }
/ >
< div className = "flex-1 overflow-y-auto space-y-4 pr -1" >
< div classNam e= "space-y-1" >
< p className = "text-xs text-muted-foreground" > 作 品 标 题 < / p >
< Input placeholder = "输入作品标题" value = { title } onChange = { e = > setTitle ( e . target . value ) } / >
< / div >
< Textarea
p laceholder = "故事简介(描述世界观、主角、核心冲突等)"
rows = { 4 }
value = { premise }
onChange = { e = > setPremise ( e . target . value ) }
/ >
< Input placeholder = "写作风格(可选,如:热血、轻松幽默、黑暗沉郁)" value = { style } onChange = { e = > setStyle ( e . target . value ) } / >
< div c lassName = "space-y-2" >
< p className = "text-xs text-muted-foreground" > 故 事 类 型 < / p >
< div className = "flex flex-wrap gap-1.5" >
{ genreKeys . map ( g = > (
< Chip key = { g } label = { GENRE_CONFIGS [ g ] . label } selected = { genre === g } onClick = { ( ) = > handleGenreSelect ( g ) } / >
) ) }
< / div >
< / div >
{ genre && subgenreKeys . length > 0 && (
< div className = "space-y-2" >
< p className = "text-xs text-muted-foreground" > 子 类 型 < / p >
< div className = "flex flex-wrap gap-1.5" >
{ subgenreKeys . map ( s = > (
< Chip key = { s } label = { s } selected = { subgenre === s } onClick = { ( ) = > handleSubgenreSelect ( s ) } / >
) ) }
< / div >
< / div >
) }
{ subgenreConfig && (
< >
< div className = "space-y-2" >
< p className = "text-xs text-muted-foreground" > 主 角 类 型 < / p >
< div className = "flex flex-wrap gap-1.5" >
{ subgenreConfig . protagonistTypes . map ( p = > (
< Chip key = { p } label = { p } selected = { protagonistType === p } onClick = { ( ) = > setProtagonistType ( protagonistType === p ? '' : p ) } / >
) ) }
< / div >
< / div >
< div className = "space-y-2" >
< p className = "text-xs text-muted-foreground" > 故 事 基 调 < / p >
< div className = "flex flex-wrap gap-1.5" >
{ subgenreConfig . tones . map ( t = > (
< Chip key = { t } label = { t } selected = { tone === t } onClick = { ( ) = > setTone ( tone === t ? '' : t ) } / >
) ) }
< / div >
< / div >
< div className = "space-y-2" >
< p className = "text-xs text-muted-foreground" > 冲 突 规 模 < / p >
< div className = "flex flex-wrap gap-1.5" >
{ subgenreConfig . conflictScales . map ( c = > (
< Chip key = { c } label = { c } selected = { conflictScale === c } onClick = { ( ) = > setConflictScale ( conflictScale === c ? '' : c ) } / >
) ) }
< / div >
< / div >
< / >
) }
< div className = "flex gap-3" >
< label className = "flex-1 flex flex-col gap-1 text-sm" >
< span className = "text-muted-foreground text-xs" > 角 色 数 量 ( 2 - 10 ) < / span >
< Input
type = "number"
min = { 2 }
max = { 10 }
value = { numCharacters }
onChange = { e = > setNumCharacters ( Math . min ( 10 , Math . max ( 2 , Number ( e . target . value ) ) ) ) }
/ >
< Input type = "number" min = { 2 } max = { 10 } value = { numCharacters } onChange = { e = > setNumCharacters ( Math . min ( 10 , Math . max ( 2 , Number ( e . target . value ) ) ) ) } / >
< / label >
< label className = "flex-1 flex flex-col gap-1 text-sm" >
< span className = "text-muted-foreground text-xs" > 章 节 数 量 ( 2 - 30 ) < / span >
< Input
type = "number"
min = { 2 }
max = { 30 }
value = { numChapters }
onChange = { e = > setNumChapters ( Math . min ( 30 , Math . max ( 2 , Number ( e . target . value ) ) ) ) }
/ >
< Input type = "number" min = { 2 } max = { 30 } value = { numChapters } onChange = { e = > setNumChapters ( Math . min ( 30 , Math . max ( 2 , Number ( e . target . value ) ) ) ) } / >
< / label >
< / div >
< div className = "flex justify-end gap-2 pt-1" >
< Button size = "sm" variant = "outline" onClick = { ( ) = > { reset ( ) ; onClose ( ) } } disabled = { loading } > 取 消 < / Button >
< Button size = "sm" onClick = { handleCreate } disabled = { loading } >
< div className = "flex justify-end" >
< Button size = "sm" variant = "outline" onClick = { handleGenerateSynopsis } disabled = { ! genre || generatingSynopsis } >
{ generatingSynopsis ? < Loader2 className = "h-3 w-3 animate-spin mr-1" / > : null }
{ generatingSynopsis ? '生成中...' : '生成故事简介' }
< / Button >
< / div >
{ synopsis && (
< div className = "space-y-2" >
< p className = "text-xs text-muted-foreground" > 故 事 简 介 ( 可 编 辑 ) < / p >
< Textarea rows = { 6 } value = { synopsis } onChange = { e = > setSynopsis ( e . target . value ) } className = "text-sm" / >
< / div >
) }
< / div >
< div className = "flex justify-between gap-2 pt-3 shrink-0 border-t" >
< Button size = "sm" variant = "ghost" onClick = { ( ) = > { reset ( ) ; onClose ( ) } } disabled = { loading } > 取 消 < / Button >
< div className = "flex gap-2" >
{ synopsis && (
< Button size = "sm" variant = "outline" onClick = { handleGenerateSynopsis } disabled = { generatingSynopsis } >
< RotateCcw className = "h-3 w-3 mr-1" / >
重 新 生 成
< / Button >
) }
< Button size = "sm" onClick = { handleCreate } disabled = { loading || ! synopsis || ! title } >
{ loading ? < Loader2 className = "h-3 w-3 animate-spin mr-1" / > : null }
{ loading ? '创建中...' : '生成剧本' }
< / Button >