Compare commits

...

2 Commits

Author SHA1 Message Date
Kevin Wong
31469ca01d 更新 2026-02-04 16:56:16 +08:00
Kevin Wong
22ea3dd0db 更新 2026-02-04 16:54:59 +08:00
12 changed files with 200 additions and 89 deletions

View File

@@ -1,35 +1,5 @@
## 🧩 发布预览与播放修复 (14:10) # Day 17 - 前端重构与体验优化
### 内容
- 发布页作品预览兼容签名 URL 与相对路径
- 参考音频试听统一走 `resolveMediaUrl`
- 素材/BGM 选择在列表变化时自动回退有效项
- 录音预览 URL 回收、预览弹窗滚动状态恢复、全局任务提示挂载
### 涉及文件
- `frontend/src/app/publish/page.tsx`
- `frontend/src/hooks/useMediaPlayers.ts`
- `frontend/src/hooks/useBgm.ts`
- `frontend/src/hooks/useMaterials.ts`
- `frontend/src/components/home/RefAudioPanel.tsx`
- `frontend/src/components/VideoPreviewModal.tsx`
- `frontend/src/app/layout.tsx`
---
## 🧩 首页持久化修复 (12:20)
### 内容
- 接入 `useHomePersistence`,补齐 `isRestored` 恢复/保存逻辑
- 修复首页刷新后选择项恢复链路,`npm run build` 通过
### 涉及文件
- `frontend/src/app/page.tsx`
- `frontend/src/hooks/useHomePersistence.ts`
---
## 🧩 前端 UI 拆分 (11:00)
## 🧩 前端 UI 拆分 (09:10) ## 🧩 前端 UI 拆分 (09:10)
### 内容 ### 内容
@@ -96,6 +66,31 @@
--- ---
## 🖼️ 预览弹窗增强 (11:10)
### 内容
- 预览弹窗统一为可复用组件,支持标题与提示
- 发布页预览与素材预览共享弹窗样式
- 弹窗头部样式统一(图标 + 标题 + 关闭按钮)
### 涉及文件
- `frontend/src/components/VideoPreviewModal.tsx`
- `frontend/src/app/page.tsx`
- `frontend/src/app/publish/page.tsx`
---
## 🧭 术语统一 (11:20)
### 内容
- “视频预览” → “作品预览”
- “历史视频” → “历史作品”
- “选择要发布的视频” → “选择要发布的作品”
- “选择素材视频” → “视频素材”
- “选择配音方式” → “配音方式”
---
## 🧱 Phase 2 Hook 抽取 (11:45) ## 🧱 Phase 2 Hook 抽取 (11:45)
### 内容 ### 内容
@@ -117,25 +112,44 @@
--- ---
## 🖼️ 预览弹窗增强 (11:10) ## 🧩 首页持久化修复 (12:20)
### 内容 ### 内容
- 预览弹窗统一为可复用组件,支持标题与提示 - 接入 `useHomePersistence`,补齐 `isRestored` 恢复/保存逻辑
- 发布页预览与素材预览共享弹窗样式 - 修复首页刷新后选择项恢复链路,`npm run build` 通过
- 弹窗头部样式统一(图标 + 标题 + 关闭按钮)
### 涉及文件 ### 涉及文件
- `frontend/src/components/VideoPreviewModal.tsx`
- `frontend/src/app/page.tsx` - `frontend/src/app/page.tsx`
- `frontend/src/app/publish/page.tsx` - `frontend/src/hooks/useHomePersistence.ts`
--- ---
## 🧭 术语统一 (11:20) ## 🧩 发布预览与播放修复 (14:10)
### 内容 ### 内容
- “视频预览” → “作品预览” - 发布页作品预览兼容签名 URL 与相对路径
- “历史视频” → “历史作品” - 参考音频试听统一走 `resolveMediaUrl`
- “选择要发布的视频” → “选择要发布的作品” - 素材/BGM 选择在列表变化时自动回退有效项
- 录音预览 URL 回收、预览弹窗滚动状态恢复、全局任务提示挂载
### 涉及文件
- `frontend/src/app/publish/page.tsx`
- `frontend/src/hooks/useMediaPlayers.ts`
- `frontend/src/hooks/useBgm.ts`
- `frontend/src/hooks/useMaterials.ts`
- `frontend/src/components/home/RefAudioPanel.tsx`
- `frontend/src/components/VideoPreviewModal.tsx`
- `frontend/src/app/layout.tsx`
--- ---
## 🧩 标题同步与长度限制 (15:30)
### 内容
- 片头标题修改同步写入发布信息标题
- 标题输入兼容中文输入法,限制 15 字(发布信息同规则)
### 涉及文件
- `frontend/src/app/page.tsx`
- `frontend/src/components/home/TitleSubtitlePanel.tsx`
- `frontend/src/app/publish/page.tsx`

View File

@@ -220,6 +220,15 @@ import { formatDate } from '@/lib/media';
--- ---
## 标题输入规则
- 片头标题与发布信息标题统一限制 15 字。
- 中文输入法合成阶段不截断,合成结束后才校验长度。
- 首页片头标题修改会同步写入 `vigent_${storageKey}_publish_title`
- 避免使用 `maxLength` 强制截断输入法合成态。
---
## 新增页面 Checklist ## 新增页面 Checklist
1. [ ] 导入 `import api from '@/lib/axios'` 1. [ ] 导入 `import api from '@/lib/axios'`

View File

@@ -33,7 +33,8 @@ ViGent2 的前端界面,采用 Next.js 16 + TailwindCSS 构建。
- **一键克隆**: 选择参考音频后自动调用 Qwen3-TTS 服务。 - **一键克隆**: 选择参考音频后自动调用 Qwen3-TTS 服务。
### 4. 字幕与标题 [Day 13 新增] ### 4. 字幕与标题 [Day 13 新增]
- **片头标题**: 可选输入,视频开头显示 3 秒淡入淡出标题。 - **片头标题**: 可选输入,限制 15 字,视频开头显示 3 秒淡入淡出标题。
- **标题同步**: 首页片头标题修改会同步到发布信息标题。
- **逐字高亮字幕**: 卡拉OK效果默认开启可关闭。 - **逐字高亮字幕**: 卡拉OK效果默认开启可关闭。
- **自动对齐**: 基于 faster-whisper 生成字级别时间戳。 - **自动对齐**: 基于 faster-whisper 生成字级别时间戳。
- **样式预设**: 标题/字幕样式选择 + 预览 + 字号调节 (Day 16)。 - **样式预设**: 标题/字幕样式选择 + 预览 + 字号调节 (Day 16)。

View File

@@ -140,7 +140,7 @@ backend/
| 端点 | 方法 | 功能 | | 端点 | 方法 | 功能 |
|------|------|------| |------|------|------|
| `/api/materials` | POST | 上传素材视频 | ✅ | | `/api/materials` | POST | 上传视频素材 | ✅ |
| `/api/materials` | GET | 获取素材列表 | ✅ | | `/api/materials` | GET | 获取素材列表 | ✅ |
| `/api/videos/generate` | POST | 创建视频生成任务 | ✅ | | `/api/videos/generate` | POST | 创建视频生成任务 | ✅ |
| `/api/videos/tasks/{id}` | GET | 查询任务状态 | ✅ | | `/api/videos/tasks/{id}` | GET | 查询任务状态 | ✅ |
@@ -164,7 +164,7 @@ background_tasks.add_task(_process_video_generation, task_id, req, user_id)
| 页面 | 功能 | | 页面 | 功能 |
|------|------| |------|------|
| **素材库** | 上传/管理多场景素材视频 | | **素材库** | 上传/管理多场景视频素材 |
| **生成视频** | 输入文案、选择素材、生成预览 | | **生成视频** | 输入文案、选择素材、生成预览 |
| **任务中心** | 查看生成进度、下载视频 | | **任务中心** | 查看生成进度、下载视频 |
| **发布管理** | 绑定平台、一键发布、定时发布 | | **发布管理** | 绑定平台、一键发布、定时发布 |

View File

@@ -2,9 +2,9 @@
{ {
"id": "subtitle_classic_yellow", "id": "subtitle_classic_yellow",
"label": "经典黄字", "label": "经典黄字",
"font_file": "title/思源黑体/SourceHanSansCN-Bold思源黑体免费.otf", "font_file": "DingTalk JinBuTi.ttf",
"font_family": "SourceHanSansCN-Bold", "font_family": "DingTalkJinBuTi",
"font_size": 52, "font_size": 60,
"highlight_color": "#FFE600", "highlight_color": "#FFE600",
"normal_color": "#FFFFFF", "normal_color": "#FFFFFF",
"stroke_color": "#000000", "stroke_color": "#000000",

View File

@@ -1,4 +1,18 @@
[ [
{
"id": "title_pop",
"label": "站酷快乐体",
"font_file": "title/站酷快乐体.ttf",
"font_family": "ZCoolHappy",
"font_size": 90,
"color": "#FFFFFF",
"stroke_color": "#000000",
"stroke_size": 8,
"letter_spacing": 5,
"top_margin": 62,
"font_weight": 900,
"is_default": true
},
{ {
"id": "title_bold_white", "id": "title_bold_white",
"label": "黑体大标题", "label": "黑体大标题",
@@ -11,7 +25,7 @@
"letter_spacing": 4, "letter_spacing": 4,
"top_margin": 60, "top_margin": 60,
"font_weight": 900, "font_weight": 900,
"is_default": true "is_default": false
}, },
{ {
"id": "title_serif_gold", "id": "title_serif_gold",
@@ -40,19 +54,5 @@
"top_margin": 60, "top_margin": 60,
"font_weight": 900, "font_weight": 900,
"is_default": false "is_default": false
},
{
"id": "title_pop",
"label": "站酷快乐体",
"font_file": "title/站酷快乐体.ttf",
"font_family": "ZCoolHappy",
"font_size": 74,
"color": "#FFFFFF",
"stroke_color": "#000000",
"stroke_size": 8,
"letter_spacing": 5,
"top_margin": 62,
"font_weight": 900,
"is_default": false
} }
] ]

View File

@@ -35,6 +35,13 @@ import { PreviewPanel } from "@/components/home/PreviewPanel";
import { HistoryList } from "@/components/home/HistoryList"; import { HistoryList } from "@/components/home/HistoryList";
const API_BASE = getApiBaseUrl(); const API_BASE = getApiBaseUrl();
const TITLE_MAX_LENGTH = 15;
const clampTitle = (value: string) => value.slice(0, TITLE_MAX_LENGTH);
const applyTitleLimit = (prev: string, next: string) => {
if (next.length <= TITLE_MAX_LENGTH) return next;
if (prev.length >= TITLE_MAX_LENGTH) return prev;
return next.slice(0, TITLE_MAX_LENGTH);
};
const VOICES = [ const VOICES = [
{ id: "zh-CN-YunxiNeural", name: "云溪 (男声-年轻)" }, { id: "zh-CN-YunxiNeural", name: "云溪 (男声-年轻)" },
@@ -114,6 +121,8 @@ export default function Home() {
// 字幕和标题相关状态 // 字幕和标题相关状态
const [videoTitle, setVideoTitle] = useState<string>(""); const [videoTitle, setVideoTitle] = useState<string>("");
const isTitleComposingRef = useRef(false);
const titleCommittedRef = useRef("");
const [enableSubtitles, setEnableSubtitles] = useState<boolean>(true); const [enableSubtitles, setEnableSubtitles] = useState<boolean>(true);
const [selectedSubtitleStyleId, setSelectedSubtitleStyleId] = useState<string>(""); const [selectedSubtitleStyleId, setSelectedSubtitleStyleId] = useState<string>("");
const [selectedTitleStyleId, setSelectedTitleStyleId] = useState<string>(""); const [selectedTitleStyleId, setSelectedTitleStyleId] = useState<string>("");
@@ -307,6 +316,38 @@ export default function Home() {
selectedRefAudio, selectedRefAudio,
}); });
useEffect(() => {
if (isTitleComposingRef.current) return;
titleCommittedRef.current = videoTitle;
}, [videoTitle]);
const commitTitle = (value: string) => {
titleCommittedRef.current = value;
setVideoTitle(value);
if (typeof window !== 'undefined') {
localStorage.setItem(`vigent_${storageKey}_publish_title`, value);
}
};
const handleTitleChange = (value: string) => {
if (isTitleComposingRef.current) {
setVideoTitle(value);
return;
}
const nextValue = applyTitleLimit(titleCommittedRef.current, value);
commitTitle(nextValue);
};
const handleTitleCompositionStart = () => {
isTitleComposingRef.current = true;
};
const handleTitleCompositionEnd = (value: string) => {
isTitleComposingRef.current = false;
const nextValue = applyTitleLimit(titleCommittedRef.current, value);
commitTitle(nextValue);
};
// 加载素材列表和历史视频 // 加载素材列表和历史视频
useEffect(() => { useEffect(() => {
if (isAuthLoading) return; if (isAuthLoading) return;
@@ -564,11 +605,12 @@ export default function Home() {
console.log("[Home] AI生成结果:", data); console.log("[Home] AI生成结果:", data);
// 更新首页标题 // 更新首页标题
setVideoTitle(data.title || ""); const nextTitle = clampTitle(data.title || "");
setVideoTitle(nextTitle);
// 同步到发布页 localStorage // 同步到发布页 localStorage
console.log("[Home] 保存到 publish localStorage - title:", data.title, "tags:", data.tags); console.log("[Home] 保存到 publish localStorage - title:", data.title, "tags:", data.tags);
localStorage.setItem(`vigent_${storageKey}_publish_title`, data.title || ""); localStorage.setItem(`vigent_${storageKey}_publish_title`, nextTitle);
localStorage.setItem(`vigent_${storageKey}_publish_tags`, JSON.stringify(data.tags || [])); localStorage.setItem(`vigent_${storageKey}_publish_tags`, JSON.stringify(data.tags || []));
} catch (err: any) { } catch (err: any) {
@@ -706,7 +748,9 @@ export default function Home() {
showStylePreview={showStylePreview} showStylePreview={showStylePreview}
onTogglePreview={() => setShowStylePreview((prev) => !prev)} onTogglePreview={() => setShowStylePreview((prev) => !prev)}
videoTitle={videoTitle} videoTitle={videoTitle}
onTitleChange={setVideoTitle} onTitleChange={handleTitleChange}
onTitleCompositionStart={handleTitleCompositionStart}
onTitleCompositionEnd={handleTitleCompositionEnd}
titleStyles={titleStyles} titleStyles={titleStyles}
selectedTitleStyleId={selectedTitleStyleId} selectedTitleStyleId={selectedTitleStyleId}
onSelectTitleStyle={setSelectedTitleStyleId} onSelectTitleStyle={setSelectedTitleStyleId}

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo, useRef } from "react";
import useSWR from 'swr'; import useSWR from 'swr';
import Link from "next/link"; import Link from "next/link";
import api from "@/lib/axios"; import api from "@/lib/axios";
@@ -25,6 +25,13 @@ const fetcher = (url: string) => api.get(url).then((res) => res.data);
// 动态获取 API 地址:服务端使用 localhost客户端使用当前域名 // 动态获取 API 地址:服务端使用 localhost客户端使用当前域名
const API_BASE = getApiBaseUrl(); const API_BASE = getApiBaseUrl();
const TITLE_MAX_LENGTH = 15;
const clampTitle = (value: string) => value.slice(0, TITLE_MAX_LENGTH);
const applyTitleLimit = (prev: string, next: string) => {
if (next.length <= TITLE_MAX_LENGTH) return next;
if (prev.length >= TITLE_MAX_LENGTH) return prev;
return next.slice(0, TITLE_MAX_LENGTH);
};
interface Account { interface Account {
platform: string; platform: string;
@@ -38,15 +45,17 @@ interface Video {
path: string; path: string;
} }
export default function PublishPage() { export default function PublishPage() {
const [accounts, setAccounts] = useState<Account[]>([]); const [accounts, setAccounts] = useState<Account[]>([]);
const [videos, setVideos] = useState<Video[]>([]); const [videos, setVideos] = useState<Video[]>([]);
const [selectedVideo, setSelectedVideo] = useState<string>(""); const [selectedVideo, setSelectedVideo] = useState<string>("");
const [videoFilter, setVideoFilter] = useState<string>(""); const [videoFilter, setVideoFilter] = useState<string>("");
const [previewVideoUrl, setPreviewVideoUrl] = useState<string | null>(null); const [previewVideoUrl, setPreviewVideoUrl] = useState<string | null>(null);
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([]); const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([]);
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [tags, setTags] = useState<string>(""); const [tags, setTags] = useState<string>("");
const isTitleComposingRef = useRef(false);
const titleCommittedRef = useRef("");
const [isPublishing, setIsPublishing] = useState(false); const [isPublishing, setIsPublishing] = useState(false);
const [publishResults, setPublishResults] = useState<any[]>([]); const [publishResults, setPublishResults] = useState<any[]>([]);
const [scheduleMode, setScheduleMode] = useState<"now" | "scheduled">("now"); const [scheduleMode, setScheduleMode] = useState<"now" | "scheduled">("now");
@@ -57,8 +66,18 @@ export default function PublishPage() {
// 使用全局认证状态 // 使用全局认证状态
const { userId, isLoading: isAuthLoading } = useAuth(); const { userId, isLoading: isAuthLoading } = useAuth();
// 是否已从 localStorage 恢复完成 // 是否已从 localStorage 恢复完成
const [isRestored, setIsRestored] = useState(false); const [isRestored, setIsRestored] = useState(false);
useEffect(() => {
if (isTitleComposingRef.current) return;
titleCommittedRef.current = title;
}, [title]);
const commitTitle = (value: string) => {
titleCommittedRef.current = value;
setTitle(value);
};
// 加载账号和视频列表 // 加载账号和视频列表
useEffect(() => { useEffect(() => {
@@ -91,7 +110,7 @@ export default function PublishPage() {
console.log("[Publish] localStorage 数据:", { savedTitle, savedTags }); console.log("[Publish] localStorage 数据:", { savedTitle, savedTags });
if (savedTitle) setTitle(savedTitle); if (savedTitle) setTitle(clampTitle(savedTitle));
if (savedTags) { if (savedTags) {
// 兼容 JSON 数组格式AI 生成)和字符串格式(手动输入) // 兼容 JSON 数组格式AI 生成)和字符串格式(手动输入)
try { try {
@@ -288,7 +307,7 @@ export default function PublishPage() {
return videos.filter((v) => v.name.toLowerCase().includes(query)); return videos.filter((v) => v.name.toLowerCase().includes(query));
}, [videos, videoFilter]); }, [videos, videoFilter]);
return ( return (
<div className="min-h-dvh"> <div className="min-h-dvh">
<VideoPreviewModal <VideoPreviewModal
onClose={() => setPreviewVideoUrl(null)} onClose={() => setPreviewVideoUrl(null)}
@@ -515,13 +534,28 @@ export default function PublishPage() {
<label className="block text-gray-400 text-sm mb-2"> <label className="block text-gray-400 text-sm mb-2">
</label> </label>
<input <input
type="text" type="text"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => {
placeholder="输入视频标题..." if (isTitleComposingRef.current) {
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500" setTitle(e.target.value);
/> return;
}
const nextValue = applyTitleLimit(titleCommittedRef.current, e.target.value);
commitTitle(nextValue);
}}
onCompositionStart={() => {
isTitleComposingRef.current = true;
}}
onCompositionEnd={(e) => {
isTitleComposingRef.current = false;
const nextValue = applyTitleLimit(titleCommittedRef.current, e.currentTarget.value);
commitTitle(nextValue);
}}
placeholder="输入视频标题..."
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500"
/>
</div> </div>
<div> <div>
<label className="block text-gray-400 text-sm mb-2"> <label className="block text-gray-400 text-sm mb-2">
@@ -663,5 +697,5 @@ export default function PublishPage() {
</div> </div>
</main> </main>
</div> </div>
); );
} }

View File

@@ -46,7 +46,7 @@ export function MaterialSelector({
<div className="bg-white/5 rounded-2xl p-4 sm:p-6 border border-white/10 backdrop-blur-sm"> <div className="bg-white/5 rounded-2xl p-4 sm:p-6 border border-white/10 backdrop-blur-sm">
<div className="flex justify-between items-center gap-2 mb-4"> <div className="flex justify-between items-center gap-2 mb-4">
<h2 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2 whitespace-nowrap"> <h2 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2 whitespace-nowrap">
📹 📹
<span className="ml-1 text-[11px] sm:text-xs text-gray-400/90 font-normal"> <span className="ml-1 text-[11px] sm:text-xs text-gray-400/90 font-normal">
() ()
</span> </span>
@@ -112,9 +112,9 @@ export function MaterialSelector({
) : materials.length === 0 ? ( ) : materials.length === 0 ? (
<div className="text-center py-8 text-gray-400"> <div className="text-center py-8 text-gray-400">
<div className="text-5xl mb-4">📁</div> <div className="text-5xl mb-4">📁</div>
<p></p> <p></p>
<p className="text-sm mt-2"> <p className="text-sm mt-2">
📤 📤
</p> </p>
</div> </div>
) : ( ) : (

View File

@@ -36,6 +36,8 @@ interface TitleSubtitlePanelProps {
onTogglePreview: () => void; onTogglePreview: () => void;
videoTitle: string; videoTitle: string;
onTitleChange: (value: string) => void; onTitleChange: (value: string) => void;
onTitleCompositionStart?: () => void;
onTitleCompositionEnd?: (value: string) => void;
titleStyles: TitleStyleOption[]; titleStyles: TitleStyleOption[];
selectedTitleStyleId: string; selectedTitleStyleId: string;
onSelectTitleStyle: (id: string) => void; onSelectTitleStyle: (id: string) => void;
@@ -63,6 +65,8 @@ export function TitleSubtitlePanel({
onTogglePreview, onTogglePreview,
videoTitle, videoTitle,
onTitleChange, onTitleChange,
onTitleCompositionStart,
onTitleCompositionEnd,
titleStyles, titleStyles,
selectedTitleStyleId, selectedTitleStyleId,
onSelectTitleStyle, onSelectTitleStyle,
@@ -209,11 +213,13 @@ export function TitleSubtitlePanel({
)} )}
<div className="mb-4"> <div className="mb-4">
<label className="text-sm text-gray-300 mb-2 block"></label> <label className="text-sm text-gray-300 mb-2 block">15</label>
<input <input
type="text" type="text"
value={videoTitle} value={videoTitle}
onChange={(e) => onTitleChange(e.target.value)} onChange={(e) => onTitleChange(e.target.value)}
onCompositionStart={onTitleCompositionStart}
onCompositionEnd={(e) => onTitleCompositionEnd?.(e.currentTarget.value)}
placeholder="输入视频标题,将在片头显示" placeholder="输入视频标题,将在片头显示"
className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 transition-colors" className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 transition-colors"
/> />

View File

@@ -26,7 +26,7 @@ export function VoiceSelector({
return ( return (
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm"> <div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2"> <h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
🎙 🎙
</h2> </h2>
<div className="flex gap-2 mb-4"> <div className="flex gap-2 mb-4">

View File

@@ -1,5 +1,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const TITLE_MAX_LENGTH = 15;
const clampTitle = (value: string) => value.slice(0, TITLE_MAX_LENGTH);
interface RefAudio { interface RefAudio {
id: string; id: string;
name: string; name: string;
@@ -101,7 +104,7 @@ export const useHomePersistence = ({
const savedEnableBgm = localStorage.getItem(`vigent_${storageKey}_enableBgm`); const savedEnableBgm = localStorage.getItem(`vigent_${storageKey}_enableBgm`);
setText(savedText || "大家好,欢迎来到我的频道,今天给大家分享一些有趣的内容。"); setText(savedText || "大家好,欢迎来到我的频道,今天给大家分享一些有趣的内容。");
setVideoTitle(savedTitle || ""); setVideoTitle(savedTitle ? clampTitle(savedTitle) : "");
setEnableSubtitles(savedSubtitles !== null ? savedSubtitles === 'true' : true); setEnableSubtitles(savedSubtitles !== null ? savedSubtitles === 'true' : true);
setTtsMode((savedTtsMode as 'edgetts' | 'voiceclone') || 'edgetts'); setTtsMode((savedTtsMode as 'edgetts' | 'voiceclone') || 'edgetts');
setVoice(savedVoice || "zh-CN-YunxiNeural"); setVoice(savedVoice || "zh-CN-YunxiNeural");