From df47a9fc693c8b5d40c0524d135f72434388e84b Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 20 Jul 2026 01:38:57 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(phase9):=20AI=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=90=88=E9=9B=86=20-=20=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E6=A0=87=E9=A2=98/=E6=99=BA=E8=83=BD=E9=85=8D=E9=9F=B3/?= =?UTF-8?q?=E7=94=9F=E6=88=90=E4=BD=93=E9=AA=8C=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 变更内容 ### #585 智能标题生成 - 选择标题步骤新增AI智能生成标题区域 - 输入描述/关键词后一键生成多个标题候选 - 三种风格分类:吸睛标题/情感共鸣/知识干货 - 支持点击选用、换一批 - 暂用模板模拟数据,后端LLM接入后替换 ### #586 智能配音推荐 - 选择配音步骤新增AI智能推荐区域 - 根据标题内容风格智能匹配音色 - 情感类→温柔女声、知识类→沉稳男声、活力类→少年音 - 支持一键选用、换一批 ### #587 生成体验优化 - 生成进度卡片化,展示当前阶段(分析素材/剪辑合成/渲染中/即将完成) - 成功卡片:展示生成数量+快速查看结果 - 失败卡片:错误详情+一键重试+关闭按钮 - 进度条渐变样式优化 --- apps/web/src/pages/generate/GeneratePage.tsx | 462 ++++++++++++++++--- apps/web/src/pages/generate/generate.css | 422 +++++++++++++++++ 2 files changed, 830 insertions(+), 54 deletions(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 9db8f486e..6581ba83b 100755 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -294,6 +294,19 @@ const GeneratePage: React.FC = () => { /* 智能素材匹配:是否已执行过匹配 */ const [hasMatched, setHasMatched] = useState(false) + /* ── 智能标题生成 ── */ + const [aiTitleInput, setAiTitleInput] = useState("") + const [aiTitleGenerating, setAiTitleGenerating] = useState(false) + const [aiTitleResults, setAiTitleResults] = useState< + Array<{ title: string; highlight: string; style: "catchy" | "emotional" | "informative" }> + >([]) + const [hasGeneratedTitles, setHasGeneratedTitles] = useState(false) + + /* ── 智能配音推荐 ── */ + const [voiceRecommendLoading, setVoiceRecommendLoading] = useState(false) + const [voiceRecommendations, setVoiceRecommendations] = useState([]) + const [hasVoiceRecommend, setHasVoiceRecommend] = useState(false) + /* ── 标题设置 ── */ const [titleSettings, setTitleSettings] = useState(DEFAULT_TITLE_SETTINGS) @@ -584,6 +597,181 @@ const GeneratePage: React.FC = () => { setSmartSelectedIds([]) }, []) + /* ── 智能标题生成 ── */ + const AI_TITLE_TEMPLATES: Record = { + catchy: [ + "震惊!{topic}居然还能这样操作", + "99%的人都不知道的{topic}秘诀", + "{topic}的终极指南,看完直接封神", + "别再走弯路了!{topic}看这一篇就够", + "一个视频讲透{topic},建议收藏", + ], + emotional: [ + "致每一个在{topic}路上坚持的人", + "关于{topic},我想说句真心话", + "{topic}背后的故事,看完沉默了", + "为什么我劝你一定要了解{topic}", + "这才是{topic}最动人的样子", + ], + informative: [ + "{topic}完整科普:从入门到精通", + "深度解析{topic}的核心原理", + "{topic}行业趋势报告|2026最新版", + "三分钟带你全面了解{topic}", + "{topic}常见问题与解决方案汇总", + ], + } + + const extractTopic = (text: string): string => { + const keywords = text + .replace(/[,。!?、,.!?]/g, " ") + .split(/\s+/) + .filter(Boolean) + if (keywords.length === 0) return "这个话题" + // 取前3个关键词组合 + return keywords.slice(0, 3).join("") + } + + const handleGenerateAiTitles = useCallback(async () => { + if (!aiTitleInput.trim()) { + message.warning("请先输入视频描述或关键词") + return + } + setAiTitleGenerating(true) + setHasGeneratedTitles(true) + + // 模拟 AI 生成延迟 + await new Promise((resolve) => setTimeout(resolve, 1200)) + + const topic = extractTopic(aiTitleInput) + const results: Array<{ + title: string + highlight: string + style: "catchy" | "emotional" | "informative" + }> = [] + + const styles: Array<"catchy" | "emotional" | "informative"> = [ + "catchy", + "emotional", + "informative", + ] + styles.forEach((style) => { + const templates = AI_TITLE_TEMPLATES[style] + // 每种风格随机选2个 + const shuffled = [...templates].sort(() => Math.random() - 0.5).slice(0, 2) + shuffled.forEach((tpl) => { + const title = tpl.replace(/\{topic\}/g, topic) + const highlights = { + catchy: "吸睛标题", + emotional: "情感共鸣", + informative: "知识干货", + } + results.push({ + title, + highlight: highlights[style], + style, + }) + }) + }) + + // 打乱顺序 + results.sort(() => Math.random() - 0.5) + setAiTitleResults(results) + setAiTitleGenerating(false) + }, [aiTitleInput]) + + const handleSelectAiTitle = useCallback((title: string) => { + setTitleSettings((prev) => ({ ...prev, title, aiAutoSelect: false })) + message.success("已选用此标题") + }, []) + + const handleRefreshAiTitles = useCallback(async () => { + if (!aiTitleInput.trim()) return + setAiTitleGenerating(true) + await new Promise((resolve) => setTimeout(resolve, 800)) + // 重新生成一批 + const topic = extractTopic(aiTitleInput) + const results: typeof aiTitleResults = [] + const styles: Array<"catchy" | "emotional" | "informative"> = [ + "catchy", + "emotional", + "informative", + ] + const highlights = { catchy: "吸睛标题", emotional: "情感共鸣", informative: "知识干货" } + styles.forEach((style) => { + const templates = AI_TITLE_TEMPLATES[style] + const shuffled = [...templates].sort(() => Math.random() - 0.5).slice(0, 2) + shuffled.forEach((tpl) => { + results.push({ + title: tpl.replace(/\{topic\}/g, topic), + highlight: highlights[style], + style, + }) + }) + }) + results.sort(() => Math.random() - 0.5) + setAiTitleResults(results) + setAiTitleGenerating(false) + }, [aiTitleInput, aiTitleResults.length]) + + /* ── 智能配音推荐 ── */ + const handleVoiceRecommend = useCallback(async () => { + if (presetVoices.length === 0) return + setVoiceRecommendLoading(true) + setHasVoiceRecommend(true) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // 根据标题内容风格模拟推荐:情感类→温柔女声,知识类→沉稳男声,活力类→阳光少年 + const title = titleSettings.title.toLowerCase() + let recommended: string[] = [] + + const femaleVoices = presetVoices.filter((v) => v.gender === "female").map((v) => v.voice_id) + const maleVoices = presetVoices.filter((v) => v.gender === "male").map((v) => v.voice_id) + const childVoices = presetVoices.filter((v) => v.gender === "child").map((v) => v.voice_id) + + if (/情感|感人|温暖|治愈|故事|回忆/.test(title)) { + recommended = femaleVoices.slice(0, 3) + } else if (/教程|知识|科普|干货|讲解|分析/.test(title)) { + recommended = maleVoices.slice(0, 2).concat(femaleVoices.slice(0, 1)) + } else if (/活力|热血|运动|搞笑|有趣/.test(title)) { + recommended = childVoices.slice(0, 1).concat(maleVoices.slice(0, 1), femaleVoices.slice(0, 1)) + } else { + // 默认推荐前3个 + recommended = presetVoices.slice(0, 3).map((v) => v.voice_id) + } + + // 不足3个时补足 + if (recommended.length < 3) { + const others = presetVoices + .filter((v) => !recommended.includes(v.voice_id)) + .map((v) => v.voice_id) + recommended = recommended.concat(others.slice(0, 3 - recommended.length)) + } + + setVoiceRecommendations(recommended) + setVoiceRecommendLoading(false) + }, [presetVoices, titleSettings.title]) + + const handleSelectRecommendedVoice = useCallback((voiceId: string) => { + setVoiceMode("preset") + setSelectedVoice(voiceId) + }, []) + + /* ── 生成阶段映射 ── */ + const getGenerationPhase = (p: number) => { + if (p < 20) return { label: "分析素材与配置", icon: "🔍" } + if (p < 50) return { label: "智能剪辑合成", icon: "🎬" } + if (p < 80) return { label: "渲染视频中", icon: "⚡" } + return { label: "即将完成", icon: "✨" } + } + + /* 重新生成(失败后重试) */ + const handleRetryGenerate = useCallback(() => { + setGenerateError(null) + handleGenerate() + }, [handleGenerate]) + const handleCloneSuccess = useCallback( (voice: VoiceClone) => { addClone(voice) @@ -1502,6 +1690,90 @@ const GeneratePage: React.FC = () => {

📝 选择标题

+ {/* AI 智能生成标题 */} +
+
+ ✨ AI 智能生成标题 +
+
+ setAiTitleInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleGenerateAiTitles() + }} + /> + +
+ + {/* 生成结果 */} + {hasGeneratedTitles && !aiTitleGenerating && aiTitleResults.length > 0 && ( +
+
+ + 为你生成 {aiTitleResults.length} 个标题 + + +
+
+ {aiTitleResults.map((item, idx) => { + const isSelected = titleSettings.title === item.title + return ( +
handleSelectAiTitle(item.title)} + > +
{item.title}
+
{item.highlight}
+ {isSelected && ( +
+ +
+ )} +
+ ) + })} +
+
+ )} + + {/* 生成中 */} + {aiTitleGenerating && ( +
+ + AI 正在为你创作标题… +
+ )} +
+ +
+ 或手动选择 +
+ {/* AI 自动选择开关 */}
AI 自动选择标题 @@ -1693,6 +1965,77 @@ const GeneratePage: React.FC = () => {

🎙️ 选择配音

+ {/* AI 智能推荐配音 */} +
+
+ ✨ AI 智能推荐 + +
+ + {voiceRecommendLoading && ( +
+ + 根据视频内容为你匹配最合适的音色… +
+ )} + + {!voiceRecommendLoading && hasVoiceRecommend && voiceRecommendations.length > 0 && ( +
+ {voiceRecommendations.map((voiceId) => { + const v = presetVoices.find((pv) => pv.voice_id === voiceId) + if (!v) return null + const isSelected = voiceMode === "preset" && selectedVoice === v.voice_id + return ( +
handleSelectRecommendedVoice(v.voice_id)} + > +
+ {VOICE_GENDER_ICON[v.gender] ?? "✨"} +
+
+
{v.name}
+
{v.description}
+
+ {isSelected && ( +
+ +
+ )} +
+ ) + })} +
+ )} + + {!voiceRecommendLoading && !hasVoiceRecommend && ( +
+ 点击「智能推荐」,AI 根据视频内容匹配音色 +
+ )} +
+ +
+ 全部音色 +
+ {/* 配音方式选择卡片 — 从 API 预设音色动态生成 */}
{presetVoices.slice(0, 3).map((v) => ( @@ -2314,71 +2657,82 @@ const GeneratePage: React.FC = () => { {(generating || generated || generateError) && (
{generating && ( - <> -
+
+
+
+ +
+
+
+ {getGenerationPhase(progress).icon} {getGenerationPhase(progress).label} +
+
预计还需 1-2 分钟,请稍候…
+
+
{Math.round(progress)}%
+
+
- - - 正在生成视频,请稍候… {Math.round(progress)}% - - - )} - {generated && !generating && ( -
- -
- - 视频生成完成! - - - 可在右侧预览或前往成片库查看 - +
+ 💡 生成过程中可以切换到其他页面操作,完成后会自动通知
)} - {generateError && !generating && ( -
- +
+ +
+
+
视频生成完成!
+
+ 共生成 {generatedVideos.length} 条视频,可在右侧预览或前往成片库查看 +
+
+ +
+ )} + {generateError && !generating && ( +
+
+ +
+
+
生成失败
+
{typeof generateError === "string" ? generateError : JSON.stringify(generateError)} - +
+
+
+ +
)} diff --git a/apps/web/src/pages/generate/generate.css b/apps/web/src/pages/generate/generate.css index 64e9a06c5..2c2f0702c 100755 --- a/apps/web/src/pages/generate/generate.css +++ b/apps/web/src/pages/generate/generate.css @@ -273,6 +273,120 @@ color: var(--info-color); } +/* ============================================================ + AI 智能配音推荐 + ============================================================ */ +.xx-voice-recommend-section { + padding: 16px; + background: linear-gradient(135deg, #f0f4ff 0%, #faf5ff 100%); + border: 1px solid var(--border-primary, #e2e8f0); + border-radius: 14px; + margin-bottom: 16px; +} + +.xx-voice-recommend-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.xx-voice-recommend-label { + font-size: 14px; + font-weight: 600; + color: var(--text-primary, #1e293b); +} + +.xx-voice-recommend-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.xx-voice-recommend-card { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + background: #fff; + border: 2px solid var(--border-primary, #e2e8f0); + border-radius: 10px; + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} + +.xx-voice-recommend-card:hover { + border-color: var(--primary-color, #4f46e5); + transform: translateX(2px); +} + +.xx-voice-recommend-card.selected { + border-color: var(--primary-color, #4f46e5); + background: var(--primary-soft, #eef2ff); +} + +.xx-voice-recommend-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #a5b4fc, #c4b5fd); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; +} + +.xx-voice-recommend-info { + flex: 1; + min-width: 0; +} + +.xx-voice-recommend-name { + font-size: 13px; + font-weight: 500; + color: var(--text-primary, #1e293b); + margin-bottom: 2px; +} + +.xx-voice-recommend-desc { + font-size: 11px; + color: var(--text-tertiary, #94a3b8); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.xx-voice-recommend-check { + width: 20px; + height: 20px; + background: var(--primary-color, #4f46e5); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.xx-voice-recommend-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + font-size: 13px; + color: var(--text-secondary, #64748b); +} + +.xx-voice-recommend-empty { + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + font-size: 12px; + color: var(--text-tertiary, #94a3b8); +} + /* ============================================================ 配音卡片(步骤4 choice-list 变体) ============================================================ */ @@ -548,6 +662,147 @@ } } +/* ============================================================ + 生成进度 / 结果卡片 + ============================================================ */ +.xx-gen-progress-card { + padding: 16px; + background: linear-gradient(135deg, #eff6ff 0%, #eef2ff 100%); + border: 1px solid #bfdbfe; + border-radius: 12px; +} + +.xx-gen-progress-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.xx-gen-progress-icon { + width: 40px; + height: 40px; + border-radius: 50%; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + color: var(--primary-color, #4f46e5); + font-size: 18px; + flex-shrink: 0; +} + +.xx-gen-progress-info { + flex: 1; + min-width: 0; +} + +.xx-gen-progress-phase { + font-size: 14px; + font-weight: 600; + color: var(--text-primary, #1e293b); + margin-bottom: 2px; +} + +.xx-gen-progress-sub { + font-size: 12px; + color: var(--text-secondary, #64748b); +} + +.xx-gen-progress-percent { + font-size: 20px; + font-weight: 700; + color: var(--primary-color, #4f46e5); + flex-shrink: 0; +} + +.xx-gen-progress-bar { + width: 100%; + height: 6px; + background: rgba(79, 70, 229, 0.15); + border-radius: 3px; + overflow: hidden; + margin-bottom: 10px; +} + +.xx-gen-progress-bar-fill { + height: 100%; + background: linear-gradient(90deg, #4f46e5, #7c3aed); + border-radius: 3px; + transition: width 0.3s ease; +} + +.xx-gen-progress-tip { + font-size: 12px; + color: var(--text-tertiary, #94a3b8); + text-align: center; +} + +.xx-gen-success-card { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 12px; +} + +.xx-gen-success-icon { + flex-shrink: 0; +} + +.xx-gen-success-info { + flex: 1; + min-width: 0; +} + +.xx-gen-success-title { + font-size: 15px; + font-weight: 600; + color: #166534; + margin-bottom: 4px; +} + +.xx-gen-success-sub { + font-size: 12px; + color: #15803d; +} + +.xx-gen-error-card { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 12px; +} + +.xx-gen-error-icon { + flex-shrink: 0; + margin-top: 2px; +} + +.xx-gen-error-info { + flex: 1; + min-width: 0; +} + +.xx-gen-error-title { + font-size: 14px; + font-weight: 600; + color: #991b1b; + margin-bottom: 4px; +} + +.xx-gen-error-msg { + font-size: 12px; + color: #b91c1c; + line-height: 1.5; + word-break: break-all; +} + /* ============================================================ 确认生成(步骤5)摘要 ============================================================ */ @@ -1382,6 +1637,173 @@ align-items: center; } +/* ============================================================ + AI 智能生成标题 + ============================================================ */ +.xx-ai-title-section { + padding: 16px; + background: linear-gradient(135deg, #f0f4ff 0%, #faf5ff 100%); + border: 1px solid var(--border-primary, #e2e8f0); + border-radius: 14px; + margin-bottom: 16px; +} + +.xx-ai-title-header { + margin-bottom: 10px; +} + +.xx-ai-title-label { + font-size: 14px; + font-weight: 600; + color: var(--text-primary, #1e293b); +} + +.xx-ai-title-input-row { + display: flex; + gap: 10px; +} + +.xx-ai-title-input { + flex: 1; + padding: 10px 14px; + font-size: 13px; + border: 1px solid var(--border-primary, #e2e8f0); + border-radius: 10px; + background: #fff; + transition: border-color 0.2s; +} + +.xx-ai-title-input:focus { + outline: none; + border-color: var(--primary-color, #4f46e5); + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + +.xx-ai-title-input::placeholder { + color: var(--text-tertiary, #94a3b8); +} + +.xx-ai-title-results { + margin-top: 14px; +} + +.xx-ai-title-results-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.xx-ai-title-results-count { + font-size: 13px; + font-weight: 500; + color: var(--text-primary, #1e293b); +} + +.xx-ai-title-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 320px; + overflow-y: auto; + padding-right: 4px; +} + +.xx-ai-title-card { + position: relative; + padding: 12px 14px; + background: #fff; + border: 2px solid var(--border-primary, #e2e8f0); + border-radius: 10px; + cursor: pointer; + transition: all 0.2s ease; +} + +.xx-ai-title-card:hover { + border-color: var(--primary-color, #4f46e5); + transform: translateX(2px); +} + +.xx-ai-title-card.selected { + border-color: var(--primary-color, #4f46e5); + background: var(--primary-soft, #eef2ff); +} + +.xx-ai-title-card-text { + font-size: 13px; + color: var(--text-primary, #1e293b); + line-height: 1.5; + padding-right: 50px; +} + +.xx-ai-title-card-tag { + display: inline-block; + margin-top: 6px; + padding: 2px 8px; + font-size: 11px; + border-radius: 10px; + background: #f1f5f9; + color: var(--text-secondary, #64748b); +} + +.xx-ai-title-card.catchy .xx-ai-title-card-tag { + background: #fef3c7; + color: #b45309; +} + +.xx-ai-title-card.emotional .xx-ai-title-card-tag { + background: #fce7f3; + color: #be185d; +} + +.xx-ai-title-card.informative .xx-ai-title-card-tag { + background: #dbeafe; + color: #1d4ed8; +} + +.xx-ai-title-card-check { + position: absolute; + top: 50%; + right: 12px; + transform: translateY(-50%); + width: 20px; + height: 20px; + background: var(--primary-color, #4f46e5); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.xx-ai-title-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + font-size: 13px; + color: var(--text-secondary, #64748b); +} + +.xx-divider { + display: flex; + align-items: center; + margin: 16px 0; + color: var(--text-tertiary, #94a3b8); + font-size: 12px; +} + +.xx-divider::before, +.xx-divider::after { + content: ""; + flex: 1; + height: 1px; + background: var(--border-primary, #e2e8f0); +} + +.xx-divider span { + padding: 0 12px; +} + /* ============================================================ 标题设置(选择标题步骤) ============================================================ */ -- 2.54.0 From 31bc1d1996b770c89e702b2fa89faec1984d5944 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 20 Jul 2026 08:23:56 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(phase9):=20=E4=BF=AE=E5=A4=8Deslint?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E4=B8=8Eunit=20test=E6=97=B6=E5=BA=8F?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除未使用的CheckCircleOutlined import - 将SMART_MATCH_REASONS和AI_TITLE_TEMPLATES移到组件外,消除react-hooks/exhaustive-deps警告 - presetVoices使用useMemo包裹,避免引用不稳定导致的依赖警告 - 将handleRetryGenerate移到handleGenerate定义之后,修复TDZ初始化时序错误 --- apps/web/src/pages/generate/GeneratePage.tsx | 67 ++++++++++---------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 6581ba83b..02e846f65 100755 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -4,14 +4,13 @@ * 左右布局:左侧 generate-form + 右侧 generate-preview * 保留所有现有业务逻辑(API 调用、URL 参数、CloneModal、TTS 等) */ -import React, { useState, useRef, useCallback, useEffect } from "react" +import React, { useState, useRef, useCallback, useEffect, useMemo } from "react" import { useQuery, useMutation } from "@tanstack/react-query" import { Typography, message, Select, Modal } from "antd" import { AudioOutlined, ThunderboltOutlined, CheckCircleFilled, - CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined, PlayCircleOutlined, @@ -253,6 +252,30 @@ const SMART_MATCH_REASONS = [ "人物表情生动", ] +const AI_TITLE_TEMPLATES: Record = { + catchy: [ + "震惊!{topic}居然还能这样操作", + "99%的人都不知道的{topic}秘诀", + "{topic}的终极指南,看完直接封神", + "别再走弯路了!{topic}看这一篇就够", + "一个视频讲透{topic},建议收藏", + ], + emotional: [ + "致每一个在{topic}路上坚持的人", + "关于{topic},我想说句真心话", + "{topic}背后的故事,看完沉默了", + "为什么我劝你一定要了解{topic}", + "这才是{topic}最动人的样子", + ], + informative: [ + "{topic}完整科普:从入门到精通", + "深度解析{topic}的核心原理", + "{topic}行业趋势报告|2026最新版", + "三分钟带你全面了解{topic}", + "{topic}常见问题与解决方案汇总", + ], +} + /* ================================================================ 组件 ================================================================ */ @@ -466,7 +489,10 @@ const GeneratePage: React.FC = () => { queryKey: ["preset-voices"], queryFn: fetchPresetVoices, }) - const presetVoices: PresetVoiceItem[] = presetVoicesData?.items ?? [] + const presetVoices: PresetVoiceItem[] = useMemo( + () => presetVoicesData?.items ?? [], + [presetVoicesData], + ) /* ── TTS 自定义合成状态 ── */ const [customAudioUrl, setCustomAudioUrl] = useState(null) @@ -598,29 +624,6 @@ const GeneratePage: React.FC = () => { }, []) /* ── 智能标题生成 ── */ - const AI_TITLE_TEMPLATES: Record = { - catchy: [ - "震惊!{topic}居然还能这样操作", - "99%的人都不知道的{topic}秘诀", - "{topic}的终极指南,看完直接封神", - "别再走弯路了!{topic}看这一篇就够", - "一个视频讲透{topic},建议收藏", - ], - emotional: [ - "致每一个在{topic}路上坚持的人", - "关于{topic},我想说句真心话", - "{topic}背后的故事,看完沉默了", - "为什么我劝你一定要了解{topic}", - "这才是{topic}最动人的样子", - ], - informative: [ - "{topic}完整科普:从入门到精通", - "深度解析{topic}的核心原理", - "{topic}行业趋势报告|2026最新版", - "三分钟带你全面了解{topic}", - "{topic}常见问题与解决方案汇总", - ], - } const extractTopic = (text: string): string => { const keywords = text @@ -766,12 +769,6 @@ const GeneratePage: React.FC = () => { return { label: "即将完成", icon: "✨" } } - /* 重新生成(失败后重试) */ - const handleRetryGenerate = useCallback(() => { - setGenerateError(null) - handleGenerate() - }, [handleGenerate]) - const handleCloneSuccess = useCallback( (voice: VoiceClone) => { addClone(voice) @@ -1209,6 +1206,12 @@ const GeneratePage: React.FC = () => { smartSelectedIds, ]) + /* 重新生成(失败后重试) */ + const handleRetryGenerate = useCallback(() => { + setGenerateError(null) + handleGenerate() + }, [handleGenerate]) + /* ── 下载视频 ── */ const handleDownload = useCallback(async () => { if (!generatedVideos.length) return -- 2.54.0 From 07a93f809ece89e618c6628ac4f1ebdf53b786da Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 20 Jul 2026 09:27:39 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(phase9):=20=E7=A7=BB=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E7=9A=84aiTitleResults.length=E4=BE=9D?= =?UTF-8?q?=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/generate/GeneratePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 02e846f65..bc66203ca 100755 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -715,7 +715,7 @@ const GeneratePage: React.FC = () => { results.sort(() => Math.random() - 0.5) setAiTitleResults(results) setAiTitleGenerating(false) - }, [aiTitleInput, aiTitleResults.length]) + }, [aiTitleInput]) /* ── 智能配音推荐 ── */ const handleVoiceRecommend = useCallback(async () => { -- 2.54.0