Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a5912c517 | ||
|
|
74516dbcdb | ||
|
|
5357d97012 | ||
|
|
33d8e52802 | ||
|
|
9af50a9066 | ||
|
|
6c6fbae13a |
@@ -24,6 +24,7 @@ backend/
|
|||||||
│ │ └── voice_clone_service.py# Qwen3-TTS 声音克隆
|
│ │ └── voice_clone_service.py# Qwen3-TTS 声音克隆
|
||||||
│ └── tests/ # 单元测试与集成测试
|
│ └── tests/ # 单元测试与集成测试
|
||||||
├── scripts/ # 运维脚本 (watchdog.py, init_db.py)
|
├── scripts/ # 运维脚本 (watchdog.py, init_db.py)
|
||||||
|
├── assets/ # 资源库 (fonts, bgm, styles)
|
||||||
└── requirements.txt # 依赖清单
|
└── requirements.txt # 依赖清单
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,8 +46,11 @@ backend/
|
|||||||
|
|
||||||
2. **视频生成 (Videos)**
|
2. **视频生成 (Videos)**
|
||||||
* `POST /api/videos/generate`: 提交生成任务
|
* `POST /api/videos/generate`: 提交生成任务
|
||||||
* `GET /api/videos/{task_id}`: 查询任务状态
|
* `GET /api/videos/tasks/{task_id}`: 查询任务状态
|
||||||
* `GET /api/videos/history`: 获取历史视频列表
|
* `GET /api/videos/generated`: 获取历史视频列表
|
||||||
|
* `DELETE /api/videos/generated/{video_id}`: 删除历史视频
|
||||||
|
|
||||||
|
> **修正 (16:20)**:任务查询与历史列表接口已更新为 `/api/videos/tasks/{task_id}` 与 `/api/videos/generated`。
|
||||||
|
|
||||||
3. **素材管理 (Materials)**
|
3. **素材管理 (Materials)**
|
||||||
* `POST /api/materials/upload`: 上传素材 (Direct Upload to Supabase)
|
* `POST /api/materials/upload`: 上传素材 (Direct Upload to Supabase)
|
||||||
@@ -55,8 +59,34 @@ backend/
|
|||||||
4. **社交发布 (Publish)**
|
4. **社交发布 (Publish)**
|
||||||
* `POST /api/publish`: 发布视频到 B站/抖音/小红书
|
* `POST /api/publish`: 发布视频到 B站/抖音/小红书
|
||||||
|
|
||||||
|
5. **资源库 (Assets)**
|
||||||
|
* `GET /api/assets/subtitle-styles`: 字幕样式列表
|
||||||
|
* `GET /api/assets/title-styles`: 标题样式列表
|
||||||
|
* `GET /api/assets/bgm`: 背景音乐列表
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🎛️ 视频生成扩展参数
|
||||||
|
|
||||||
|
`POST /api/videos/generate` 支持以下可选字段:
|
||||||
|
|
||||||
|
- `subtitle_style_id`: 字幕样式 ID
|
||||||
|
- `title_style_id`: 标题样式 ID
|
||||||
|
- `subtitle_font_size`: 字幕字号(覆盖样式默认值)
|
||||||
|
- `title_font_size`: 标题字号(覆盖样式默认值)
|
||||||
|
- `bgm_id`: 背景音乐 ID
|
||||||
|
- `bgm_volume`: 背景音乐音量(0-1,默认 0.2)
|
||||||
|
|
||||||
|
## 📦 资源库与静态资源
|
||||||
|
|
||||||
|
- 本地资源目录:`backend/assets/{fonts,bgm,styles}`
|
||||||
|
- 静态访问路径:`/assets`(用于前端样式预览与背景音乐试听)
|
||||||
|
|
||||||
|
## 🎵 背景音乐混音策略
|
||||||
|
|
||||||
|
- 混音发生在 **唇形对齐之后**,避免影响字幕/口型时间轴。
|
||||||
|
- 使用 FFmpeg `amix`,禁用归一化以保持配音音量稳定。
|
||||||
|
|
||||||
## 🛠️ 开发环境搭建
|
## 🛠️ 开发环境搭建
|
||||||
|
|
||||||
### 1. 虚拟环境
|
### 1. 虚拟环境
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
---
|
|
||||||
|
|
||||||
## 🔧 Qwen-TTS Flash Attention 优化 (10:00)
|
## 🔧 Qwen-TTS Flash Attention 优化 (10:00)
|
||||||
|
|
||||||
### 优化背景
|
### 优化背景
|
||||||
@@ -18,8 +16,6 @@ pip install -U flash-attn --no-build-isolation
|
|||||||
- **显存占用**: 显著降低,消除 OOM 风险
|
- **显存占用**: 显著降低,消除 OOM 风险
|
||||||
- **代码变动**: 无代码变动,仅环境优化 (自动检测)
|
- **代码变动**: 无代码变动,仅环境优化 (自动检测)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛡️ 服务看门狗 Watchdog (10:30)
|
## 🛡️ 服务看门狗 Watchdog (10:30)
|
||||||
|
|
||||||
### 问题描述
|
### 问题描述
|
||||||
@@ -53,6 +49,89 @@ if service["failures"] >= service['threshold']:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🎨 交互体验与视图优化 (14:20)
|
||||||
|
|
||||||
|
### 主页优化
|
||||||
|
- 视频生成完成后,预览优先选中最新输出
|
||||||
|
- 选择项持久化:素材 / 背景音乐 / 历史作品
|
||||||
|
- 列表内滚动定位选中项,避免页面跳动
|
||||||
|
- 刷新回到顶部(首页)
|
||||||
|
- 标题/字幕样式预览面板
|
||||||
|
- 背景音乐试听即选中并自动开启,音量滑块实时影响试听
|
||||||
|
|
||||||
|
### 发布页优化
|
||||||
|
- 刷新回到顶部(发布页)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎵 背景音乐链路修复 (15:00)
|
||||||
|
|
||||||
|
### 修复点
|
||||||
|
- FFmpeg 混音改为 `shell=False`,避免 `filter_complex` 被 shell 误解析
|
||||||
|
- `amix` 禁用归一化,避免配音音量被压低
|
||||||
|
|
||||||
|
### 关键修改
|
||||||
|
`backend/app/services/video_service.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗣️ 字幕断句修复 (15:20)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- 字幕切分逻辑保留英文单词整体,避免中英混合被硬切
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `backend/app/services/whisper_service.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧱 资源库与样式能力接入 (15:40)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- 字体库 / BGM 资源接入本地 assets
|
||||||
|
- 新增样式配置文件(字幕/标题)
|
||||||
|
- 新增资源 API 与静态挂载 `/assets`
|
||||||
|
- Remotion 支持样式参数与字体加载
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `backend/assets/fonts/`
|
||||||
|
- `backend/assets/bgm/`
|
||||||
|
- `backend/assets/styles/subtitle.json`
|
||||||
|
- `backend/assets/styles/title.json`
|
||||||
|
- `backend/app/services/assets_service.py`
|
||||||
|
- `backend/app/api/assets.py`
|
||||||
|
- `backend/app/main.py`
|
||||||
|
- `backend/app/api/videos.py`
|
||||||
|
- `backend/app/services/remotion_service.py`
|
||||||
|
- `remotion/src/components/Subtitles.tsx`
|
||||||
|
- `remotion/src/components/Title.tsx`
|
||||||
|
- `remotion/src/Video.tsx`
|
||||||
|
- `remotion/render.ts`
|
||||||
|
- `frontend/src/app/page.tsx`
|
||||||
|
- `frontend/next.config.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 运维调整 (16:10)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- Watchdog 移除 LatentSync 监控,避免长推理误杀
|
||||||
|
- LatentSync PM2 增加内存重启阈值(运行时配置)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 前端按钮图标统一 (16:40)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- 首页与发布页按钮图标统一替换为 Lucide SVG
|
||||||
|
- 交互按钮保持一致尺寸与对齐
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `frontend/src/components/home/`
|
||||||
|
- `frontend/src/app/publish/page.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📝 文档更新
|
## 📝 文档更新
|
||||||
|
|
||||||
- [x] `Docs/QWEN3_TTS_DEPLOY.md`: 添加 Flash Attention 安装指南
|
- [x] `Docs/QWEN3_TTS_DEPLOY.md`: 添加 Flash Attention 安装指南
|
||||||
|
|||||||
141
Docs/DevLogs/Day17.md
Normal file
141
Docs/DevLogs/Day17.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
## 🧩 发布预览与播放修复 (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`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 首页持久化修复 (12:20)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- 接入 `useHomePersistence`,补齐 `isRestored` 恢复/保存逻辑
|
||||||
|
- 修复首页刷新后选择项恢复链路,`npm run build` 通过
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `frontend/src/app/page.tsx`
|
||||||
|
- `frontend/src/hooks/useHomePersistence.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 前端 UI 拆分 (11:00)
|
||||||
|
## 🧩 前端 UI 拆分 (09:10)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- 首页 `page.tsx` 拆分为独立 UI 组件,状态与逻辑仍集中在页面
|
||||||
|
- 新增首页组件目录 `frontend/src/components/home/`
|
||||||
|
|
||||||
|
### 组件列表
|
||||||
|
- `HomeHeader`
|
||||||
|
- `MaterialSelector`
|
||||||
|
- `ScriptEditor`
|
||||||
|
- `TitleSubtitlePanel`
|
||||||
|
- `VoiceSelector`
|
||||||
|
- `RefAudioPanel`
|
||||||
|
- `BgmPanel`
|
||||||
|
- `GenerateActionBar`
|
||||||
|
- `PreviewPanel`
|
||||||
|
- `HistoryList`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧰 前端通用工具抽取 (09:30)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- 抽取 API Base / 资源 URL / 日期格式化等通用工具
|
||||||
|
- 首页与发布页统一调用,消除重复逻辑
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `frontend/src/lib/media.ts`
|
||||||
|
- `frontend/src/app/page.tsx`
|
||||||
|
- `frontend/src/app/publish/page.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 前端规范更新 (09:40)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- 更新 `FRONTEND_DEV.md` 以匹配最新目录结构
|
||||||
|
- 新增 `media.ts` 使用规范与示例
|
||||||
|
- 增加组件拆分规范与页面 checklist
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `Docs/FRONTEND_DEV.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 交互体验与视图优化 (10:00)
|
||||||
|
|
||||||
|
### 标题/字幕预览
|
||||||
|
- 标题/字幕预览按素材分辨率缩放,字号更接近成片
|
||||||
|
- 标题/字幕样式选择持久化,刷新不回默认
|
||||||
|
- 默认样式更新:标题 90px 站酷快乐体,字幕 60px 经典黄字 + DingTalkJinBuTi
|
||||||
|
|
||||||
|
### 发布页优化
|
||||||
|
- 选择作品改为卡片列表 + 搜索 + 预览弹窗
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ 性能微优化 (10:30)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- 列表渲染启用 `content-visibility`(素材/历史/参考音频/发布作品),BGM 列表保留滚动定位
|
||||||
|
- 首屏数据请求并行化(`Promise.allSettled`)
|
||||||
|
- localStorage 写入防抖(文本/标题/BGM 音量/发布表单)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧱 Phase 2 Hook 抽取 (11:45)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- `useTitleSubtitleStyles`:标题/字幕样式获取与默认选择逻辑
|
||||||
|
- `useMaterials`:素材列表/上传/删除逻辑抽取
|
||||||
|
- `useRefAudios`:参考音频列表/上传/删除逻辑抽取
|
||||||
|
- `useBgm`:背景音乐列表与加载状态抽取
|
||||||
|
- `useMediaPlayers`:音频试听逻辑集中管理(参考音频/背景音乐)
|
||||||
|
- `useGeneratedVideos`:历史作品列表获取 + 选择逻辑抽取
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `frontend/src/hooks/useTitleSubtitleStyles.ts`
|
||||||
|
- `frontend/src/hooks/useMaterials.ts`
|
||||||
|
- `frontend/src/hooks/useRefAudios.ts`
|
||||||
|
- `frontend/src/hooks/useBgm.ts`
|
||||||
|
- `frontend/src/hooks/useMediaPlayers.ts`
|
||||||
|
- `frontend/src/hooks/useGeneratedVideos.ts`
|
||||||
|
- `frontend/src/app/page.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖼️ 预览弹窗增强 (11:10)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- 预览弹窗统一为可复用组件,支持标题与提示
|
||||||
|
- 发布页预览与素材预览共享弹窗样式
|
||||||
|
- 弹窗头部样式统一(图标 + 标题 + 关闭按钮)
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- `frontend/src/components/VideoPreviewModal.tsx`
|
||||||
|
- `frontend/src/app/page.tsx`
|
||||||
|
- `frontend/src/app/publish/page.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧭 术语统一 (11:20)
|
||||||
|
|
||||||
|
### 内容
|
||||||
|
- “视频预览” → “作品预览”
|
||||||
|
- “历史视频” → “历史作品”
|
||||||
|
- “选择要发布的视频” → “选择要发布的作品”
|
||||||
|
|
||||||
|
---
|
||||||
@@ -24,11 +24,12 @@
|
|||||||
| :---: | :--- | :--- |
|
| :---: | :--- | :--- |
|
||||||
| 🔥 **High** | `Docs/DevLogs/DayN.md` | **(最新日志)** 详细记录变更、修复、代码片段 |
|
| 🔥 **High** | `Docs/DevLogs/DayN.md` | **(最新日志)** 详细记录变更、修复、代码片段 |
|
||||||
| 🔥 **High** | `Docs/task_complete.md` | **(任务总览)** 更新 `[x]`、进度条、时间线 |
|
| 🔥 **High** | `Docs/task_complete.md` | **(任务总览)** 更新 `[x]`、进度条、时间线 |
|
||||||
| ⚡ **Med** | `README.md` | **(项目主页)** 功能特性、技术栈、最新截图 |
|
| ⚡ **Med** | `README.md` | **(项目主页)** 功能特性、技术栈、最新截图 |
|
||||||
| ⚡ **Med** | `Docs/DEPLOY_MANUAL.md` | **(部署手册)** 环境变量、依赖包、启动命令变更 |
|
| ⚡ **Med** | `Docs/DEPLOY_MANUAL.md` | **(部署手册)** 环境变量、依赖包、启动命令变更 |
|
||||||
| ⚡ **Med** | `Docs/FRONTEND_DEV.md` | **(前端规范)** API封装、日期格式化、新页面规范 |
|
| ⚡ **Med** | `Docs/FRONTEND_DEV.md` | **(前端规范)** API封装、日期格式化、新页面规范 |
|
||||||
| 🧊 **Low** | `Docs/implementation_plan.md` | **(实施计划)** 核对计划与实际实现的差异 |
|
| ⚡ **Med** | `Docs/FRONTEND_README.md` | **(前端文档)** 功能说明、页面变更 |
|
||||||
| 🧊 **Low** | `frontend/README.md` | **(前端文档)** 新页面路由、组件用法、UI变更 |
|
| 🧊 **Low** | `Docs/implementation_plan.md` | **(实施计划)** 核对计划与实际实现的差异 |
|
||||||
|
| 🧊 **Low** | `Docs/architecture_plan.md` | **(前端架构)** 拆分计划与阶段目标 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -140,20 +141,20 @@
|
|||||||
|
|
||||||
> **核心原则**:使用正确的工具,避免字符编码问题
|
> **核心原则**:使用正确的工具,避免字符编码问题
|
||||||
|
|
||||||
### ✅ 推荐工具:replace_file_content
|
### ✅ 推荐工具:apply_patch
|
||||||
|
|
||||||
**使用场景**:
|
**使用场景**:
|
||||||
- 追加新章节到文件末尾
|
- 追加新章节到文件末尾
|
||||||
- 修改/替换现有章节内容
|
- 修改/替换现有章节内容
|
||||||
- 更新状态标记(🔄 → ✅)
|
- 更新状态标记(🔄 → ✅)
|
||||||
- 修正错误内容
|
- 修正错误内容
|
||||||
|
|
||||||
**优势**:
|
**优势**:
|
||||||
- ✅ 自动处理字符编码(Windows CRLF)
|
- ✅ 自动处理字符编码(Windows CRLF)
|
||||||
- ✅ 精确替换,不会误删其他内容
|
- ✅ 精确替换,不会误删其他内容
|
||||||
- ✅ 有错误提示,方便调试
|
- ✅ 有错误提示,方便调试
|
||||||
|
|
||||||
**注意事项**:
|
**注意事项**:
|
||||||
```markdown
|
```markdown
|
||||||
1. **必须精确匹配**:TargetContent 必须与文件完全一致
|
1. **必须精确匹配**:TargetContent 必须与文件完全一致
|
||||||
2. **处理换行符**:文件使用 \r\n,不要漏掉 \r
|
2. **处理换行符**:文件使用 \r\n,不要漏掉 \r
|
||||||
@@ -177,39 +178,45 @@
|
|||||||
|
|
||||||
### 📝 最佳实践示例
|
### 📝 最佳实践示例
|
||||||
|
|
||||||
**追加新章节**:
|
**追加新章节**:
|
||||||
```python
|
```diff
|
||||||
replace_file_content(
|
*** Begin Patch
|
||||||
TargetFile="path/to/DayN.md",
|
*** Update File: Docs/DevLogs/DayN.md
|
||||||
TargetContent="## 🔗 相关文档\n\n...\n\n", # 文件末尾的内容
|
@@
|
||||||
ReplacementContent="## 🔗 相关文档\n\n...\n\n---\n\n## 🆕 新章节\n内容...",
|
## 🔗 相关文档
|
||||||
StartLine=280,
|
|
||||||
EndLine=284
|
...
|
||||||
)
|
---
|
||||||
```
|
|
||||||
|
## 🆕 新章节
|
||||||
|
内容...
|
||||||
|
*** End Patch
|
||||||
|
```
|
||||||
|
|
||||||
**修改现有内容**:
|
**修改现有内容**:
|
||||||
```python
|
```diff
|
||||||
replace_file_content(
|
*** Begin Patch
|
||||||
TargetContent="**状态**:🔄 待修复",
|
*** Update File: Docs/DevLogs/DayN.md
|
||||||
ReplacementContent="**状态**:✅ 已修复",
|
@@
|
||||||
StartLine=310,
|
-**状态**:🔄 待修复
|
||||||
EndLine=310
|
+**状态**:✅ 已修复
|
||||||
)
|
*** End Patch
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📁 文件结构
|
## 📁 文件结构
|
||||||
|
|
||||||
```
|
```
|
||||||
ViGent/Docs/
|
ViGent2/Docs/
|
||||||
├── task_complete.md # 任务总览(仅按需更新)
|
├── task_complete.md # 任务总览(仅按需更新)
|
||||||
├── Doc_Rules.md # 本文件
|
├── Doc_Rules.md # 本文件
|
||||||
├── FRONTEND_DEV.md # 前端开发规范
|
├── FRONTEND_DEV.md # 前端开发规范
|
||||||
├── DEPLOY_MANUAL.md # 部署手册
|
├── FRONTEND_README.md # 前端功能文档
|
||||||
├── SUPABASE_DEPLOY.md # Supabase 部署文档
|
├── architecture_plan.md # 前端拆分计划
|
||||||
|
├── DEPLOY_MANUAL.md # 部署手册
|
||||||
|
├── SUPABASE_DEPLOY.md # Supabase 部署文档
|
||||||
└── DevLogs/
|
└── DevLogs/
|
||||||
├── Day1.md # 开发日志
|
├── Day1.md # 开发日志
|
||||||
└── ...
|
└── ...
|
||||||
@@ -217,7 +224,7 @@ ViGent/Docs/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📅 DayN.md 更新规则(日常更新)
|
## 📅 DayN.md 更新规则(日常更新)
|
||||||
|
|
||||||
### 新建判断 (对话开始前)
|
### 新建判断 (对话开始前)
|
||||||
1. **回顾进度**:查看 `task_complete.md` 了解当前状态
|
1. **回顾进度**:查看 `task_complete.md` 了解当前状态
|
||||||
@@ -225,9 +232,9 @@ ViGent/Docs/
|
|||||||
- **今天 (与当前日期相同)** → 🚨 **绝对禁止创建新文件**,必须**追加**到现有 `DayN.md` 末尾!即使是完全不同的功能模块。
|
- **今天 (与当前日期相同)** → 🚨 **绝对禁止创建新文件**,必须**追加**到现有 `DayN.md` 末尾!即使是完全不同的功能模块。
|
||||||
- **之前 (昨天或更早)** → 创建 `Day{N+1}.md`
|
- **之前 (昨天或更早)** → 创建 `Day{N+1}.md`
|
||||||
|
|
||||||
### 追加格式
|
### 追加格式
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 [章节标题]
|
## 🔧 [章节标题]
|
||||||
|
|
||||||
@@ -243,14 +250,18 @@ ViGent/Docs/
|
|||||||
- ✅ 修复了 xxx
|
- ✅ 修复了 xxx
|
||||||
```
|
```
|
||||||
|
|
||||||
### 快速修复格式
|
### 快速修复格式
|
||||||
```markdown
|
```markdown
|
||||||
## 🐛 [Bug 简述] (HH:MM)
|
## 🐛 [Bug 简述] (HH:MM)
|
||||||
|
|
||||||
**问题**:一句话描述
|
**问题**:一句话描述
|
||||||
**修复**:修改了 `文件名` 中的 xxx
|
**修复**:修改了 `文件名` 中的 xxx
|
||||||
**状态**:✅ 已修复 / 🔄 待验证
|
**状态**:✅ 已修复 / 🔄 待验证
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ⚠️ 注意
|
||||||
|
- **DayN.md 文件开头禁止使用 `---`**,避免被解析为 Front Matter。
|
||||||
|
- 分隔线只用于章节之间,不作为文件第一行。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -305,4 +316,4 @@ ViGent/Docs/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**最后更新**:2026-01-23
|
**最后更新**:2026-02-04
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ frontend/src/
|
|||||||
│ ├── admin/ # 管理员页面
|
│ ├── admin/ # 管理员页面
|
||||||
│ ├── login/ # 登录页面
|
│ ├── login/ # 登录页面
|
||||||
│ └── register/ # 注册页面
|
│ └── register/ # 注册页面
|
||||||
|
├── components/ # 可复用组件
|
||||||
|
│ ├── home/ # 首页拆分组件
|
||||||
|
│ └── ...
|
||||||
├── lib/ # 公共工具函数
|
├── lib/ # 公共工具函数
|
||||||
│ ├── axios.ts # Axios 实例(含 401/403 拦截器)
|
│ ├── axios.ts # Axios 实例(含 401/403 拦截器)
|
||||||
│ └── auth.ts # 认证相关函数
|
│ ├── auth.ts # 认证相关函数
|
||||||
|
│ └── media.ts # API Base / URL / 日期等通用工具
|
||||||
└── proxy.ts # 路由代理(原 middleware)
|
└── proxy.ts # 路由代理(原 middleware)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -146,6 +150,27 @@ const { data } = useSWR('/api/xxx', fetcher, { refreshInterval: 2000 });
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 通用工具函数 (media.ts)
|
||||||
|
|
||||||
|
### 统一 API Base / URL 解析
|
||||||
|
使用 `@/lib/media` 统一处理服务端/客户端 API Base 与资源地址,避免硬编码:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getApiBaseUrl, resolveMediaUrl, resolveAssetUrl, formatDate } from '@/lib/media';
|
||||||
|
|
||||||
|
const apiBase = getApiBaseUrl(); // SSR: http://localhost:8006 / Client: ''
|
||||||
|
const playableUrl = resolveMediaUrl(video.path); // 兼容签名 URL 与相对路径
|
||||||
|
const fontUrl = resolveAssetUrl(`fonts/${fontFile}`);
|
||||||
|
const timeText = formatDate(video.created_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 资源路径规则
|
||||||
|
- 视频/音频:优先用 `resolveMediaUrl()`
|
||||||
|
- 字体/BGM:使用 `resolveAssetUrl()`(自动编码中文路径)
|
||||||
|
- 预览前若已有签名 URL,先用 `isAbsoluteUrl()` 判定,避免再次拼接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 日期格式化规范
|
## 日期格式化规范
|
||||||
|
|
||||||
### 禁止使用 `toLocaleString()`
|
### 禁止使用 `toLocaleString()`
|
||||||
@@ -161,25 +186,47 @@ new Date(timestamp * 1000).toLocaleString('zh-CN')
|
|||||||
**正确做法:**
|
**正确做法:**
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ 使用固定格式
|
// ✅ 使用固定格式
|
||||||
const formatDate = (timestamp: number) => {
|
import { formatDate } from '@/lib/media';
|
||||||
const d = new Date(timestamp * 1000);
|
|
||||||
const year = d.getFullYear();
|
|
||||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(d.getDate()).padStart(2, '0');
|
|
||||||
const hour = String(d.getHours()).padStart(2, '0');
|
|
||||||
const minute = String(d.getMinutes()).padStart(2, '0');
|
|
||||||
return `${year}/${month}/${day} ${hour}:${minute}`;
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 组件拆分规范
|
||||||
|
|
||||||
|
当页面组件超过 300-500 行,建议拆分到 `components/`:
|
||||||
|
|
||||||
|
- `page.tsx` 负责状态与业务逻辑
|
||||||
|
- 组件只接受 props 与回调,尽量不直接发 API
|
||||||
|
- 首页拆分组件统一放在 `components/home/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 用户偏好持久化
|
||||||
|
|
||||||
|
首页涉及样式与字号等用户偏好时,需持久化并在刷新后恢复:
|
||||||
|
|
||||||
|
- **必须持久化**:
|
||||||
|
- 标题样式 ID / 字幕样式 ID
|
||||||
|
- 标题字号 / 字幕字号
|
||||||
|
- 背景音乐选择 / 音量 / 开关状态
|
||||||
|
- 素材选择 / 历史作品选择
|
||||||
|
|
||||||
|
### 实施规范
|
||||||
|
- 使用 `storageKey = userId || 'guest'`,按用户隔离。
|
||||||
|
- **恢复先于保存**:恢复完成前禁止写入(`isRestored` 保护)。
|
||||||
|
- 避免默认值覆盖用户选择(优先读取已保存值)。
|
||||||
|
- 优先使用 `useHomePersistence` 集中管理恢复/保存,页面内避免分散的 localStorage 读写。
|
||||||
|
- 如需新增持久化字段,必须加入恢复与保存逻辑,并更新本节。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 新增页面 Checklist
|
## 新增页面 Checklist
|
||||||
|
|
||||||
1. [ ] 导入 `import api from '@/lib/axios'`
|
1. [ ] 导入 `import api from '@/lib/axios'`
|
||||||
2. [ ] 所有 API 请求使用 `api.get/post/delete()` 而非原生 `fetch`
|
2. [ ] 所有 API 请求使用 `api.get/post/delete()` 而非原生 `fetch`
|
||||||
3. [ ] 日期格式化使用固定格式函数,不用 `toLocaleString()`
|
3. [ ] 日期格式化使用 `@/lib/media` 的 `formatDate`
|
||||||
4. [ ] 添加 `'use client'` 指令(如需客户端交互)
|
4. [ ] 资源 URL 使用 `resolveMediaUrl`/`resolveAssetUrl`
|
||||||
|
5. [ ] 添加 `'use client'` 指令(如需客户端交互)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ViGent2 Frontend
|
# ViGent2 Frontend
|
||||||
|
|
||||||
ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。
|
ViGent2 的前端界面,采用 Next.js 16 + TailwindCSS 构建。
|
||||||
|
|
||||||
## ✨ 核心功能
|
## ✨ 核心功能
|
||||||
|
|
||||||
@@ -8,9 +8,13 @@ ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。
|
|||||||
- **素材管理**: 拖拽上传人物视频,实时预览。
|
- **素材管理**: 拖拽上传人物视频,实时预览。
|
||||||
- **文案配音**: 集成 EdgeTTS,支持多音色选择 (云溪 / 晓晓)。
|
- **文案配音**: 集成 EdgeTTS,支持多音色选择 (云溪 / 晓晓)。
|
||||||
- **AI 标题/标签**: 一键生成视频标题与标签 (Day 14)。
|
- **AI 标题/标签**: 一键生成视频标题与标签 (Day 14)。
|
||||||
|
- **标题/字幕样式**: 样式选择 + 预览 + 字号调节 (Day 16)。
|
||||||
|
- **背景音乐**: 试听 + 音量控制 + 选择持久化 (Day 16)。
|
||||||
|
- **交互优化**: 选择项持久化、列表内定位、刷新回顶部 (Day 16)。
|
||||||
|
- **预览一致性**: 标题/字幕预览按素材分辨率缩放,效果更接近成片 (Day 17)。
|
||||||
- **进度追踪**: 实时显示视频生成进度 (10% -> 100%)。
|
- **进度追踪**: 实时显示视频生成进度 (10% -> 100%)。
|
||||||
- **结果预览**: 生成完成后直接播放下载。
|
- **作品预览**: 生成完成后直接播放下载(作品预览 + 历史作品)。
|
||||||
- **本地保存**: 文案/标题自动保存,刷新后恢复 (Day 14)。
|
- **本地保存**: 文案/标题/偏好由 `useHomePersistence` 统一持久化,刷新后恢复 (Day 14/17)。
|
||||||
|
|
||||||
### 2. 全自动发布 (`/publish`) [Day 7 新增]
|
### 2. 全自动发布 (`/publish`) [Day 7 新增]
|
||||||
- **多平台管理**: 统一管理 B站、抖音、小红书账号状态。
|
- **多平台管理**: 统一管理 B站、抖音、小红书账号状态。
|
||||||
@@ -19,6 +23,8 @@ ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。
|
|||||||
- 实时检测扫码状态 (Wait/Success)。
|
- 实时检测扫码状态 (Wait/Success)。
|
||||||
- Cookie 自动保存与状态同步。
|
- Cookie 自动保存与状态同步。
|
||||||
- **发布配置**: 设置视频标题、标签、简介。
|
- **发布配置**: 设置视频标题、标签、简介。
|
||||||
|
- **作品选择**: 卡片列表 + 搜索 + 预览弹窗。
|
||||||
|
- **预览兼容**: 签名 URL / 相对路径均可直接预览。
|
||||||
- **定时任务**: 支持 "立即发布" 或 "定时发布"。
|
- **定时任务**: 支持 "立即发布" 或 "定时发布"。
|
||||||
|
|
||||||
### 3. 声音克隆 [Day 13 新增]
|
### 3. 声音克隆 [Day 13 新增]
|
||||||
@@ -30,13 +36,20 @@ ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。
|
|||||||
- **片头标题**: 可选输入,视频开头显示 3 秒淡入淡出标题。
|
- **片头标题**: 可选输入,视频开头显示 3 秒淡入淡出标题。
|
||||||
- **逐字高亮字幕**: 卡拉OK效果,默认开启,可关闭。
|
- **逐字高亮字幕**: 卡拉OK效果,默认开启,可关闭。
|
||||||
- **自动对齐**: 基于 faster-whisper 生成字级别时间戳。
|
- **自动对齐**: 基于 faster-whisper 生成字级别时间戳。
|
||||||
|
- **样式预设**: 标题/字幕样式选择 + 预览 + 字号调节 (Day 16)。
|
||||||
|
- **默认样式**: 标题 90px 站酷快乐体;字幕 60px 经典黄字 + DingTalkJinBuTi (Day 17)。
|
||||||
|
- **样式持久化**: 标题/字幕样式与字号刷新保留 (Day 17)。
|
||||||
|
|
||||||
### 5. 账户设置 [Day 15 新增]
|
### 5. 背景音乐 [Day 16 新增]
|
||||||
|
- **试听预览**: 点击试听即选中,音量滑块实时生效。
|
||||||
|
- **混音控制**: 仅影响 BGM,配音保持原音量。
|
||||||
|
|
||||||
|
### 6. 账户设置 [Day 15 新增]
|
||||||
- **手机号登录**: 11位中国手机号验证登录。
|
- **手机号登录**: 11位中国手机号验证登录。
|
||||||
- **账户下拉菜单**: 显示有效期 + 修改密码 + 安全退出。
|
- **账户下拉菜单**: 显示有效期 + 修改密码 + 安全退出。
|
||||||
- **修改密码**: 弹窗输入当前密码与新密码,修改后强制重新登录。
|
- **修改密码**: 弹窗输入当前密码与新密码,修改后强制重新登录。
|
||||||
|
|
||||||
### 6. 文案提取助手 (`ScriptExtractionModal`) [Day 15 新增]
|
### 7. 文案提取助手 (`ScriptExtractionModal`) [Day 15 新增]
|
||||||
- **多源提取**: 支持文件拖拽上传与 URL 粘贴 (B站/抖音/TikTok)。
|
- **多源提取**: 支持文件拖拽上传与 URL 粘贴 (B站/抖音/TikTok)。
|
||||||
- **AI 洗稿**: 集成 GLM-4.7-Flash,自动改写为口播文案。
|
- **AI 洗稿**: 集成 GLM-4.7-Flash,自动改写为口播文案。
|
||||||
- **一键填入**: 提取结果直接填充至视频生成输入框。
|
- **一键填入**: 提取结果直接填充至视频生成输入框。
|
||||||
@@ -44,7 +57,7 @@ ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。
|
|||||||
|
|
||||||
## 🛠️ 技术栈
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
- **框架**: Next.js 14 (App Router)
|
- **框架**: Next.js 16 (App Router)
|
||||||
- **样式**: TailwindCSS
|
- **样式**: TailwindCSS
|
||||||
- **图标**: Lucide React
|
- **图标**: Lucide React
|
||||||
- **组件**: 自定义现代化组件 (Glassmorphism 风格)
|
- **组件**: 自定义现代化组件 (Glassmorphism 风格)
|
||||||
@@ -77,15 +90,16 @@ src/
|
|||||||
│ │ └── page.tsx
|
│ │ └── page.tsx
|
||||||
│ └── layout.tsx # 全局布局 (导航栏)
|
│ └── layout.tsx # 全局布局 (导航栏)
|
||||||
├── components/ # UI 组件
|
├── components/ # UI 组件
|
||||||
│ ├── VideoUploader.tsx # 视频上传
|
│ ├── home/ # 首页拆分组件
|
||||||
│ ├── StatusBadge.tsx # 状态徽章
|
|
||||||
│ └── ...
|
│ └── ...
|
||||||
└── lib/ # 工具函数
|
└── lib/ # 工具函数
|
||||||
|
└── media.ts # API Base / URL / 日期等通用工具
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔌 后端对接
|
## 🔌 后端对接
|
||||||
|
|
||||||
- **Base URL**: `http://localhost:8006`
|
- **Base URL**: `http://localhost:8006` (SSR) / 相对路径 (Client)
|
||||||
|
- **URL 统一工具**: `@/lib/media` 提供 `resolveMediaUrl` / `resolveAssetUrl`
|
||||||
- **代理配置**: Next.js Rewrites (如需) 或直接 CORS。
|
- **代理配置**: Next.js Rewrites (如需) 或直接 CORS。
|
||||||
|
|
||||||
## 🎨 设计规范
|
## 🎨 设计规范
|
||||||
|
|||||||
333
Docs/Logs.md
333
Docs/Logs.md
@@ -1,333 +0,0 @@
|
|||||||
rongye@r730-ubuntu:~$ pm2 logs vigent2-qwen-tts
|
|
||||||
[TAILING] Tailing last 15 lines for [vigent2-qwen-tts] process (change the value with --lines option)
|
|
||||||
/home/rongye/.pm2/logs/vigent2-qwen-tts-error.log last 15 lines:
|
|
||||||
13|vigent2 | Setting `pad_token_id` to `eos_token_id`:2150 for open-end generation.
|
|
||||||
|
|
||||||
/home/rongye/.pm2/logs/vigent2-qwen-tts-out.log last 15 lines:
|
|
||||||
13|vigent2 | 🔄 Loading Qwen3-TTS model...
|
|
||||||
13|vigent2 |
|
|
||||||
13|vigent2 | ********
|
|
||||||
13|vigent2 | Warning: flash-attn is not installed. Will only run the manual PyTorch version. Please install flash-attn for faster inference.
|
|
||||||
13|vigent2 | ********
|
|
||||||
13|vigent2 |
|
|
||||||
13|vigent2 | ✅ Qwen3-TTS model loaded in 8.6s
|
|
||||||
13|vigent2 | INFO: 127.0.0.1:56814 - "GET /health HTTP/1.1" 200 OK
|
|
||||||
13|vigent2 | 🎤 Generating: 大家好,欢迎来到我的频道,今天给大家分享一些有趣的内容。...
|
|
||||||
13|vigent2 | 📝 Ref text: 其实生活中有许多美好的瞬间,比如清晨的阳光,或者一杯温热的清茶。希望这次生成的音色能够自然、流畅,完...
|
|
||||||
13|vigent2 | [WARNING] Min value of input waveform signal is -1.006709337234497
|
|
||||||
13|vigent2 | [WARNING] Max value of input waveform signal is 1.0008893013000488
|
|
||||||
13|vigent2 | ✅ Generated in 15.0s, duration: 4.6s
|
|
||||||
13|vigent2 | INFO: 127.0.0.1:36556 - "POST /generate HTTP/1.1" 200 OK
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
rongye@r730-ubuntu:~$ pm2 logs vigent2-backend --lines 400
|
|
||||||
[TAILING] Tailing last 400 lines for [vigent2-backend] process (change the value with --lines option)
|
|
||||||
/home/rongye/.pm2/logs/vigent2-backend-out.log last 400 lines:
|
|
||||||
11|vigent2 | Storage endpoint URL should have a trailing slash.
|
|
||||||
11|vigent2 | Storage endpoint URL should have a trailing slash.
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769651820268 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769651825016 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/publish/accounts HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769651828852 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769654501430 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769654987404 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "POST /api/ref-audios HTTP/1.1" 500 Internal Server Error
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769655093628 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "POST /api/ref-audios HTTP/1.1" 500 Internal Server Error
|
|
||||||
11|vigent2 | Storage endpoint URL should have a trailing slash.
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769655569331 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "POST /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "POST /api/videos/generate HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | [Pipeline] TTS completed in 17.7s
|
|
||||||
11|vigent2 | [LipSync] Health check: ready=True
|
|
||||||
11|vigent2 | [LipSync] Starting LatentSync inference...
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | [Pipeline] LipSync completed in 122.8s
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | [Pipeline] Total generation time: 143.1s
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/tasks/bf4760b8-e338-49ee-9777-828c1ef0c855 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/publish/accounts HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769655769762 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/publish/accounts HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769655923194 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769655960629 HTTP/1.1" 403 Forbidden
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 403 Forbidden
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 403 Forbidden
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "POST /api/auth/logout HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "POST /api/auth/login HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769655964287 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/publish/accounts HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 403 Forbidden
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "POST /api/auth/logout HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/publish/accounts HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "POST /api/auth/login HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769656015718 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769656233290 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "POST /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/publish/accounts HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769656987465 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/materials?t=1769657141569 HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/videos/generated HTTP/1.1" 200 OK
|
|
||||||
11|vigent2 | INFO: 27.17.161.128:0 - "GET /api/ref-audios HTTP/1.1" 200 OK
|
|
||||||
|
|
||||||
/home/rongye/.pm2/logs/vigent2-backend-error.log last 400 lines:
|
|
||||||
11|vigent2 | rnPaKCW6D20R7A4QpeumwXIRUkzHtaFASP40bWfE6KL05g4rq6VbZFQ9X4FVBZ2lbwW%2Faa32knjuye8aa1ejtZEGmyfXpfcryezEIy0gmYYjjT7lKB6HAupj9%2FCezQ%3D%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX1%2BpEG8yoPZBpac674EsBSuEU0HhlEZGMLIKNfviY6GGLzzbk6%2BbdfmzJ5s1nr16B0NNWzywMDwDD00Cktdf8N50BWw0Pp7Xuy2cOM6L15tjqobzRZyayXyVA1o%2B5kHPODaa3yg4cjWjee8OqG1qRaX4EwOXc0YzPZI%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX1%2BRY4oSsXW07D0HdYfXZhDpJS%2Fl%2F%2Bysns8Xand%2BMI7%2FJBIRw1RV%2FIJPzbTSpW8kmvwLCsUosyNPtsZbl3lGRDOM4YJIL%2BaFVjvjAWDo0WA89ezEeTVY9hzd9rwV3A6dbv5vJhrEdjolAkub50ItC47iV1fIGb%2FN3vI%3D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19r9Vr3ov%2BP30OgbarNSaCn6bebg11iU%2B8UV7b%2F116JurvSpJ77d%2FdZ62kjIP%2BMF3h3R9RathLKFQ%3D%3D; rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX19CE%2F3GmyTsZHCQhdFWdzYnJYPdvCMBFbM%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX18t2%2FxrnN2HPEqTssR572nq%2FgCim9EQN7E%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:00.756 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/videos/generated
|
|
||||||
11|vigent2 | 2026-01-29 11:06:00.757 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-fetch-dest': 'empty', 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1', 'accept': 'application/json, text/plain, */*', 'referer': 'https://vigent.hbyrkj.top/', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'accept-language': 'en-US,en;q=0.9', 'priority': 'u=3, i', 'accept-encoding': 'gzip, deflate, br', 'cookie': 'access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiN2JlNWIzNWYtMzQ0Ni00ZTIyLWEzMTktZTc5M2NlNDBmYTRiIiwiZXhwIjoxNzcwMTc4MTE0fQ.pk4sCAkd9hcN6fE5_8RXH42zfMl7YPSV5i1R9QeER4s; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1760758482696%2C%220199f562-16c1-7fcb-9bef-fdc33838b6a8%22%2C1760758470336%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_session=RudderEncrypt%3AU2FsdGVkX19ssBuVw9hBTDRVrnPaKCW6D20R7A4QpeumwXIRUkzHtaFASP40bWfE6KL05g4rq6VbZFQ9X4FVBZ2lbwW%2Faa32knjuye8aa1ejtZEGmyfXpfcryezEIy0gmYYjjT7lKB6HAupj9%2FCezQ%3D%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX1%2BpEG8yoPZBpac674EsBSuEU0HhlEZGMLIKNfviY6GGLzzbk6%2BbdfmzJ5s1nr16B0NNWzywMDwDD00Cktdf8N50BWw0Pp7Xuy2cOM6L15tjqobzRZyayXyVA1o%2B5kHPODaa3yg4cjWjee8OqG1qRaX4EwOXc0YzPZI%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX1%2BRY4oSsXW07D0HdYfXZhDpJS%2Fl%2F%2Bysns8Xand%2BMI7%2FJBIRw1RV%2FIJPzbTSpW8kmvwLCsUosyNPtsZbl3lGRDOM4YJIL%2BaFVjvjAWDo0WA89ezEeTVY9hzd9rwV3A6dbv5vJhrEdjolAkub50ItC47iV1fIGb%2FN3vI%3D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19r9Vr3ov%2BP30OgbarNSaCn6bebg11iU%2B8UV7b%2F116JurvSpJ77d%2FdZ62kjIP%2BMF3h3R9RathLKFQ%3D%3D; rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX19CE%2F3GmyTsZHCQhdFWdzYnJYPdvCMBFbM%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX18t2%2FxrnN2HPEqTssR572nq%2FgCim9EQN7E%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:00.765 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/ref-audios - Status: 403 - Duration: 0.02s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:00.766 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/videos/generated - Status: 403 - Duration: 0.01s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:00.812 | INFO | app.main:dispatch:21 - START Request: POST https://vigent.hbyrkj.top/api/auth/logout
|
|
||||||
11|vigent2 | 2026-01-29 11:06:00.812 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'content-length': '0', 'accept': '*/*', 'sec-fetch-site': 'same-origin', 'origin': 'https://vigent.hbyrkj.top', 'sec-fetch-mode': 'cors', 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1', 'referer': 'https://vigent.hbyrkj.top/', 'sec-fetch-dest': 'empty', 'accept-language': 'en-US,en;q=0.9', 'priority': 'u=3, i', 'accept-encoding': 'gzip, deflate, br', 'cookie': 'access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiN2JlNWIzNWYtMzQ0Ni00ZTIyLWEzMTktZTc5M2NlNDBmYTRiIiwiZXhwIjoxNzcwMTc4MTE0fQ.pk4sCAkd9hcN6fE5_8RXH42zfMl7YPSV5i1R9QeER4s; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1760758482696%2C%220199f562-16c1-7fcb-9bef-fdc33838b6a8%22%2C1760758470336%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_session=RudderEncrypt%3AU2FsdGVkX19ssBuVw9hBTDRVrnPaKCW6D20R7A4QpeumwXIRUkzHtaFASP40bWfE6KL05g4rq6VbZFQ9X4FVBZ2lbwW%2Faa32knjuye8aa1ejtZEGmyfXpfcryezEIy0gmYYjjT7lKB6HAupj9%2FCezQ%3D%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX1%2BpEG8yoPZBpac674EsBSuEU0HhlEZGMLIKNfviY6GGLzzbk6%2BbdfmzJ5s1nr16B0NNWzywMDwDD00Cktdf8N50BWw0Pp7Xuy2cOM6L15tjqobzRZyayXyVA1o%2B5kHPODaa3yg4cjWjee8OqG1qRaX4EwOXc0YzPZI%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX1%2BRY4oSsXW07D0HdYfXZhDpJS%2Fl%2F%2Bysns8Xand%2BMI7%2FJBIRw1RV%2FIJPzbTSpW8kmvwLCsUosyNPtsZbl3lGRDOM4YJIL%2BaFVjvjAWDo0WA89ezEeTVY9hzd9rwV3A6dbv5vJhrEdjolAkub50ItC47iV1fIGb%2FN3vI%3D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19r9Vr3ov%2BP30OgbarNSaCn6bebg11iU%2B8UV7b%2F116JurvSpJ77d%2FdZ62kjIP%2BMF3h3R9RathLKFQ%3D%3D; rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX19CE%2F3GmyTsZHCQhdFWdzYnJYPdvCMBFbM%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX18t2%2FxrnN2HPEqTssR572nq%2FgCim9EQN7E%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:00.815 | INFO | app.main:dispatch:26 - END Request: POST https://vigent.hbyrkj.top/api/auth/logout - Status: 200 - Duration: 0.00s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:03.694 | INFO | app.main:dispatch:21 - START Request: POST https://vigent.hbyrkj.top/api/auth/login
|
|
||||||
11|vigent2 | 2026-01-29 11:06:03.695 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'content-length': '58', 'accept': '*/*', 'content-type': 'application/json', 'sec-fetch-site': 'same-origin', 'origin': 'https://vigent.hbyrkj.top', 'sec-fetch-mode': 'cors', 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1', 'referer': 'https://vigent.hbyrkj.top/login', 'sec-fetch-dest': 'empty', 'accept-language': 'en-US,en;q=0.9', 'priority': 'u=3, i', 'accept-encoding': 'gzip, deflate, br', 'cookie': 'ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1760758482696%2C%220199f562-16c1-7fcb-9bef-fdc33838b6a8%22%2C1760758470336%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_session=RudderEncrypt%3AU2FsdGVkX19ssBuVw9hBTDRVrnPaKCW6D20R7A4QpeumwXIRUkzHtaFASP40bWfE6KL05g4rq6VbZFQ9X4FVBZ2lbwW%2Faa32knjuye8aa1ejtZEGmyfXpfcryezEIy0gmYYjjT7lKB6HAupj9%2FCezQ%3D%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX1%2BpEG8yoPZBpac674EsBSuEU0HhlEZGMLIKNfviY6GGLzzbk6%2BbdfmzJ5s1nr16B0NNWzywMDwDD00Cktdf8N50BWw0Pp7Xuy2cOM6L15tjqobzRZyayXyVA1o%2B5kHPODaa3yg4cjWjee8OqG1qRaX4EwOXc0YzPZI%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX1%2BRY4oSsXW07D0HdYfXZhDpJS%2Fl%2F%2Bysns8Xand%2BMI7%2FJBIRw1RV%2FIJPzbTSpW8kmvwLCsUosyNPtsZbl3lGRDOM4YJIL%2BaFVjvjAWDo0WA89ezEeTVY9hzd9rwV3A6dbv5vJhrEdjolAkub50ItC47iV1fIGb%2FN3vI%3D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19r9Vr3ov%2BP30OgbarNSaCn6bebg11iU%2B8UV7b%2F116JurvSpJ77d%2FdZ62kjIP%2BMF3h3R9RathLKFQ%3D%3D; rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX19CE%2F3GmyTsZHCQhdFWdzYnJYPdvCMBFbM%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX18t2%2FxrnN2HPEqTssR572nq%2FgCim9EQN7E%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.185 | INFO | app.api.auth:login:157 - 用户登录: lamnickdavid@gmail.com
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.185 | INFO | app.main:dispatch:26 - END Request: POST https://vigent.hbyrkj.top/api/auth/login - Status: 200 - Duration: 0.49s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.359 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/materials?t=1769655964287
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.359 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-fetch-dest': 'empty', 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1', 'accept': 'application/json, text/plain, */*', 'referer': 'https://vigent.hbyrkj.top/', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'accept-language': 'en-US,en;q=0.9', 'priority': 'u=3, i', 'accept-encoding': 'gzip, deflate, br', 'cookie': 'access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMWE0NzczNTktZmMwZS00MjVhLTk3MGUtODc1ZTcyNjFjYWJiIiwiZXhwIjoxNzcwMjYwNzY0fQ.X-nGjaX_gwaJw995Zuw_fnj2oY_K-oM6tgwMDR4pDQk; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1760758482696%2C%220199f562-16c1-7fcb-9bef-fdc33838b6a8%22%2C1760758470336%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_session=RudderEncrypt%3AU2FsdGVkX19ssBuVw9hBTDRVrnPaKCW6D20R7A4QpeumwXIRUkzHtaFASP40bWfE6KL05g4rq6VbZFQ9X4FVBZ2lbwW%2Faa32knjuye8aa1ejtZEGmyfXpfcryezEIy0gmYYjjT7lKB6HAupj9%2FCezQ%3D%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX1%2BpEG8yoPZBpac674EsBSuEU0HhlEZGMLIKNfviY6GGLzzbk6%2BbdfmzJ5s1nr16B0NNWzywMDwDD00Cktdf8N50BWw0Pp7Xuy2cOM6L15tjqobzRZyayXyVA1o%2B5kHPODaa3yg4cjWjee8OqG1qRaX4EwOXc0YzPZI%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX1%2BRY4oSsXW07D0HdYfXZhDpJS%2Fl%2F%2Bysns8Xand%2BMI7%2FJBIRw1RV%2FIJPzbTSpW8kmvwLCsUosyNPtsZbl3lGRDOM4YJIL%2BaFVjvjAWDo0WA89ezEeTVY9hzd9rwV3A6dbv5vJhrEdjolAkub50ItC47iV1fIGb%2FN3vI%3D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19r9Vr3ov%2BP30OgbarNSaCn6bebg11iU%2B8UV7b%2F116JurvSpJ77d%2FdZ62kjIP%2BMF3h3R9RathLKFQ%3D%3D; rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX19CE%2F3GmyTsZHCQhdFWdzYnJYPdvCMBFbM%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX18t2%2FxrnN2HPEqTssR572nq%2FgCim9EQN7E%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.377 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/ref-audios
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.377 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-fetch-dest': 'empty', 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1', 'accept': 'application/json, text/plain, */*', 'referer': 'https://vigent.hbyrkj.top/', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'accept-language': 'en-US,en;q=0.9', 'priority': 'u=3, i', 'accept-encoding': 'gzip, deflate, br', 'cookie': 'access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMWE0NzczNTktZmMwZS00MjVhLTk3MGUtODc1ZTcyNjFjYWJiIiwiZXhwIjoxNzcwMjYwNzY0fQ.X-nGjaX_gwaJw995Zuw_fnj2oY_K-oM6tgwMDR4pDQk; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1760758482696%2C%220199f562-16c1-7fcb-9bef-fdc33838b6a8%22%2C1760758470336%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_session=RudderEncrypt%3AU2FsdGVkX19ssBuVw9hBTDRVrnPaKCW6D20R7A4QpeumwXIRUkzHtaFASP40bWfE6KL05g4rq6VbZFQ9X4FVBZ2lbwW%2Faa32knjuye8aa1ejtZEGmyfXpfcryezEIy0gmYYjjT7lKB6HAupj9%2FCezQ%3D%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX1%2BpEG8yoPZBpac674EsBSuEU0HhlEZGMLIKNfviY6GGLzzbk6%2BbdfmzJ5s1nr16B0NNWzywMDwDD00Cktdf8N50BWw0Pp7Xuy2cOM6L15tjqobzRZyayXyVA1o%2B5kHPODaa3yg4cjWjee8OqG1qRaX4EwOXc0YzPZI%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX1%2BRY4oSsXW07D0HdYfXZhDpJS%2Fl%2F%2Bysns8Xand%2BMI7%2FJBIRw1RV%2FIJPzbTSpW8kmvwLCsUosyNPtsZbl3lGRDOM4YJIL%2BaFVjvjAWDo0WA89ezEeTVY9hzd9rwV3A6dbv5vJhrEdjolAkub50ItC47iV1fIGb%2FN3vI%3D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19r9Vr3ov%2BP30OgbarNSaCn6bebg11iU%2B8UV7b%2F116JurvSpJ77d%2FdZ62kjIP%2BMF3h3R9RathLKFQ%3D%3D; rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX19CE%2F3GmyTsZHCQhdFWdzYnJYPdvCMBFbM%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX18t2%2FxrnN2HPEqTssR572nq%2FgCim9EQN7E%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.392 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/videos/generated
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.392 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-fetch-dest': 'empty', 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1', 'accept': 'application/json, text/plain, */*', 'referer': 'https://vigent.hbyrkj.top/', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'accept-language': 'en-US,en;q=0.9', 'priority': 'u=3, i', 'accept-encoding': 'gzip, deflate, br', 'cookie': 'access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMWE0NzczNTktZmMwZS00MjVhLTk3MGUtODc1ZTcyNjFjYWJiIiwiZXhwIjoxNzcwMjYwNzY0fQ.X-nGjaX_gwaJw995Zuw_fnj2oY_K-oM6tgwMDR4pDQk; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1760758482696%2C%220199f562-16c1-7fcb-9bef-fdc33838b6a8%22%2C1760758470336%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_session=RudderEncrypt%3AU2FsdGVkX19ssBuVw9hBTDRVrnPaKCW6D20R7A4QpeumwXIRUkzHtaFASP40bWfE6KL05g4rq6VbZFQ9X4FVBZ2lbwW%2Faa32knjuye8aa1ejtZEGmyfXpfcryezEIy0gmYYjjT7lKB6HAupj9%2FCezQ%3D%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX1%2BpEG8yoPZBpac674EsBSuEU0HhlEZGMLIKNfviY6GGLzzbk6%2BbdfmzJ5s1nr16B0NNWzywMDwDD00Cktdf8N50BWw0Pp7Xuy2cOM6L15tjqobzRZyayXyVA1o%2B5kHPODaa3yg4cjWjee8OqG1qRaX4EwOXc0YzPZI%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX1%2BRY4oSsXW07D0HdYfXZhDpJS%2Fl%2F%2Bysns8Xand%2BMI7%2FJBIRw1RV%2FIJPzbTSpW8kmvwLCsUosyNPtsZbl3lGRDOM4YJIL%2BaFVjvjAWDo0WA89ezEeTVY9hzd9rwV3A6dbv5vJhrEdjolAkub50ItC47iV1fIGb%2FN3vI%3D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19r9Vr3ov%2BP30OgbarNSaCn6bebg11iU%2B8UV7b%2F116JurvSpJ77d%2FdZ62kjIP%2BMF3h3R9RathLKFQ%3D%3D; rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX19CE%2F3GmyTsZHCQhdFWdzYnJYPdvCMBFbM%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX18t2%2FxrnN2HPEqTssR572nq%2FgCim9EQN7E%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.478 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/materials?t=1769655964287 - Status: 200 - Duration: 0.12s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.491 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/videos/generated - Status: 200 - Duration: 0.10s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:04.614 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/ref-audios - Status: 200 - Duration: 0.24s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:16.329 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/publish/accounts
|
|
||||||
11|vigent2 | 2026-01-29 11:06:16.329 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/publish', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiNDk2N2QwNjMtNjhhZC00NzFkLThhMWQtOGE1MmJhODAxZjBjIiwiZXhwIjoxNzcwMTc4MzM4fQ.k9JOPKwqHrNTTNOsUQlMuA63rOETStl7uWXAIIDLGtA'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:16.333 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/videos/generated
|
|
||||||
11|vigent2 | 2026-01-29 11:06:16.333 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/publish', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiNDk2N2QwNjMtNjhhZC00NzFkLThhMWQtOGE1MmJhODAxZjBjIiwiZXhwIjoxNzcwMTc4MzM4fQ.k9JOPKwqHrNTTNOsUQlMuA63rOETStl7uWXAIIDLGtA'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:16.342 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/publish/accounts - Status: 200 - Duration: 0.01s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:16.343 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/videos/generated - Status: 403 - Duration: 0.01s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:16.397 | INFO | app.main:dispatch:21 - START Request: POST https://vigent.hbyrkj.top/api/auth/logout
|
|
||||||
11|vigent2 | 2026-01-29 11:06:16.397 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'content-length': '0', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'accept': '*/*', 'origin': 'https://vigent.hbyrkj.top', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/publish', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiNDk2N2QwNjMtNjhhZC00NzFkLThhMWQtOGE1MmJhODAxZjBjIiwiZXhwIjoxNzcwMTc4MzM4fQ.k9JOPKwqHrNTTNOsUQlMuA63rOETStl7uWXAIIDLGtA'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:16.398 | INFO | app.main:dispatch:26 - END Request: POST https://vigent.hbyrkj.top/api/auth/logout - Status: 200 - Duration: 0.00s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:28.685 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/videos/generated
|
|
||||||
11|vigent2 | 2026-01-29 11:06:28.686 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-fetch-dest': 'empty', 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1', 'accept': 'application/json, text/plain, */*', 'referer': 'https://vigent.hbyrkj.top/publish', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'accept-language': 'en-US,en;q=0.9', 'priority': 'u=3, i', 'accept-encoding': 'gzip, deflate, br', 'cookie': 'access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMWE0NzczNTktZmMwZS00MjVhLTk3MGUtODc1ZTcyNjFjYWJiIiwiZXhwIjoxNzcwMjYwNzY0fQ.X-nGjaX_gwaJw995Zuw_fnj2oY_K-oM6tgwMDR4pDQk; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1760758482696%2C%220199f562-16c1-7fcb-9bef-fdc33838b6a8%22%2C1760758470336%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_session=RudderEncrypt%3AU2FsdGVkX19ssBuVw9hBTDRVrnPaKCW6D20R7A4QpeumwXIRUkzHtaFASP40bWfE6KL05g4rq6VbZFQ9X4FVBZ2lbwW%2Faa32knjuye8aa1ejtZEGmyfXpfcryezEIy0gmYYjjT7lKB6HAupj9%2FCezQ%3D%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX1%2BpEG8yoPZBpac674EsBSuEU0HhlEZGMLIKNfviY6GGLzzbk6%2BbdfmzJ5s1nr16B0NNWzywMDwDD00Cktdf8N50BWw0Pp7Xuy2cOM6L15tjqobzRZyayXyVA1o%2B5kHPODaa3yg4cjWjee8OqG1qRaX4EwOXc0YzPZI%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX1%2BRY4oSsXW07D0HdYfXZhDpJS%2Fl%2F%2Bysns8Xand%2BMI7%2FJBIRw1RV%2FIJPzbTSpW8kmvwLCsUosyNPtsZbl3lGRDOM4YJIL%2BaFVjvjAWDo0WA89ezEeTVY9hzd9rwV3A6dbv5vJhrEdjolAkub50ItC47iV1fIGb%2FN3vI%3D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19r9Vr3ov%2BP30OgbarNSaCn6bebg11iU%2B8UV7b%2F116JurvSpJ77d%2FdZ62kjIP%2BMF3h3R9RathLKFQ%3D%3D; rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX19CE%2F3GmyTsZHCQhdFWdzYnJYPdvCMBFbM%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX18t2%2FxrnN2HPEqTssR572nq%2FgCim9EQN7E%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:28.704 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/publish/accounts
|
|
||||||
11|vigent2 | 2026-01-29 11:06:28.705 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-fetch-dest': 'empty', 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Mobile/15E148 Safari/604.1', 'accept': 'application/json, text/plain, */*', 'referer': 'https://vigent.hbyrkj.top/publish', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'accept-language': 'en-US,en;q=0.9', 'priority': 'u=3, i', 'accept-encoding': 'gzip, deflate, br', 'cookie': 'access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMWE0NzczNTktZmMwZS00MjVhLTk3MGUtODc1ZTcyNjFjYWJiIiwiZXhwIjoxNzcwMjYwNzY0fQ.X-nGjaX_gwaJw995Zuw_fnj2oY_K-oM6tgwMDR4pDQk; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1760758482696%2C%220199f562-16c1-7fcb-9bef-fdc33838b6a8%22%2C1760758470336%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_session=RudderEncrypt%3AU2FsdGVkX19ssBuVw9hBTDRVrnPaKCW6D20R7A4QpeumwXIRUkzHtaFASP40bWfE6KL05g4rq6VbZFQ9X4FVBZ2lbwW%2Faa32knjuye8aa1ejtZEGmyfXpfcryezEIy0gmYYjjT7lKB6HAupj9%2FCezQ%3D%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX1%2BpEG8yoPZBpac674EsBSuEU0HhlEZGMLIKNfviY6GGLzzbk6%2BbdfmzJ5s1nr16B0NNWzywMDwDD00Cktdf8N50BWw0Pp7Xuy2cOM6L15tjqobzRZyayXyVA1o%2B5kHPODaa3yg4cjWjee8OqG1qRaX4EwOXc0YzPZI%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX1%2BRY4oSsXW07D0HdYfXZhDpJS%2Fl%2F%2Bysns8Xand%2BMI7%2FJBIRw1RV%2FIJPzbTSpW8kmvwLCsUosyNPtsZbl3lGRDOM4YJIL%2BaFVjvjAWDo0WA89ezEeTVY9hzd9rwV3A6dbv5vJhrEdjolAkub50ItC47iV1fIGb%2FN3vI%3D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19r9Vr3ov%2BP30OgbarNSaCn6bebg11iU%2B8UV7b%2F116JurvSpJ77d%2FdZ62kjIP%2BMF3h3R9RathLKFQ%3D%3D; rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX19CE%2F3GmyTsZHCQhdFWdzYnJYPdvCMBFbM%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX18t2%2FxrnN2HPEqTssR572nq%2FgCim9EQN7E%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:28.710 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/publish/accounts - Status: 200 - Duration: 0.01s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:28.745 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/videos/generated - Status: 200 - Duration: 0.06s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.021 | INFO | app.main:dispatch:21 - START Request: POST https://vigent.hbyrkj.top/api/auth/login
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.021 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'content-length': '58', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'content-type': 'application/json', 'sec-ch-ua-mobile': '?0', 'accept': '*/*', 'origin': 'https://vigent.hbyrkj.top', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/login', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.624 | INFO | app.api.auth:login:157 - 用户登录: lamnickdavid@gmail.com
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.625 | INFO | app.main:dispatch:26 - END Request: POST https://vigent.hbyrkj.top/api/auth/login - Status: 200 - Duration: 0.60s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.806 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/materials?t=1769656015718
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.806 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.820 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/videos/generated
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.821 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.834 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/ref-audios
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.834 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.865 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/materials?t=1769656015718 - Status: 200 - Duration: 0.06s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:51.941 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/videos/generated - Status: 200 - Duration: 0.12s
|
|
||||||
11|vigent2 | 2026-01-29 11:06:52.076 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/ref-audios - Status: 200 - Duration: 0.24s
|
|
||||||
11|vigent2 | 2026-01-29 11:10:29.354 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/materials?t=1769656233290
|
|
||||||
11|vigent2 | 2026-01-29 11:10:29.354 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:10:29.405 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/ref-audios
|
|
||||||
11|vigent2 | 2026-01-29 11:10:29.406 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:10:29.423 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/videos/generated
|
|
||||||
11|vigent2 | 2026-01-29 11:10:29.423 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:10:29.474 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/materials?t=1769656233290 - Status: 200 - Duration: 0.12s
|
|
||||||
11|vigent2 | 2026-01-29 11:10:29.535 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/videos/generated - Status: 200 - Duration: 0.11s
|
|
||||||
11|vigent2 | 2026-01-29 11:10:29.653 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/ref-audios - Status: 200 - Duration: 0.25s
|
|
||||||
11|vigent2 | 2026-01-29 11:11:20.032 | INFO | app.main:dispatch:21 - START Request: POST https://vigent.hbyrkj.top/api/ref-audios
|
|
||||||
11|vigent2 | 2026-01-29 11:11:20.032 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'content-length': '204514', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'content-type': 'multipart/form-data; boundary=----WebKitFormBoundaryBGjf99CuOGlQdB7a', 'sec-ch-ua-mobile': '?0', 'origin': 'https://vigent.hbyrkj.top', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:11:20.536 | INFO | app.services.storage:upload_file:97 - Storage upload success: 94cd91e3-7d89-45e8-9d85-e8ba0660d74c/1769656280_myvoice.wav
|
|
||||||
11|vigent2 | 2026-01-29 11:11:20.576 | INFO | app.services.storage:upload_file:97 - Storage upload success: 94cd91e3-7d89-45e8-9d85-e8ba0660d74c/1769656280_myvoice.json
|
|
||||||
11|vigent2 | 2026-01-29 11:11:20.584 | INFO | app.main:dispatch:26 - END Request: POST https://vigent.hbyrkj.top/api/ref-audios - Status: 200 - Duration: 0.55s
|
|
||||||
11|vigent2 | 2026-01-29 11:11:20.638 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/ref-audios
|
|
||||||
11|vigent2 | 2026-01-29 11:11:20.638 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:11:21.086 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/ref-audios - Status: 200 - Duration: 0.45s
|
|
||||||
11|vigent2 | 2026-01-29 11:22:58.683 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/videos/generated
|
|
||||||
11|vigent2 | 2026-01-29 11:22:58.684 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/publish', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:22:58.742 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/publish/accounts
|
|
||||||
11|vigent2 | 2026-01-29 11:22:58.743 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/publish', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:22:58.747 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/publish/accounts - Status: 200 - Duration: 0.01s
|
|
||||||
11|vigent2 | 2026-01-29 11:22:58.798 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/videos/generated - Status: 200 - Duration: 0.11s
|
|
||||||
11|vigent2 | 2026-01-29 11:23:03.509 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/materials?t=1769656987465
|
|
||||||
11|vigent2 | 2026-01-29 11:23:03.510 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:23:03.535 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/videos/generated
|
|
||||||
11|vigent2 | 2026-01-29 11:23:03.535 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:23:03.551 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/ref-audios
|
|
||||||
11|vigent2 | 2026-01-29 11:23:03.552 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:23:03.569 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/materials?t=1769656987465 - Status: 200 - Duration: 0.06s
|
|
||||||
11|vigent2 | 2026-01-29 11:23:03.658 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/videos/generated - Status: 200 - Duration: 0.12s
|
|
||||||
11|vigent2 | 2026-01-29 11:23:03.996 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/ref-audios - Status: 200 - Duration: 0.44s
|
|
||||||
11|vigent2 | 2026-01-29 11:25:37.605 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/ref-audios
|
|
||||||
11|vigent2 | 2026-01-29 11:25:37.605 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:25:37.653 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/materials?t=1769657141569
|
|
||||||
11|vigent2 | 2026-01-29 11:25:37.653 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:25:37.673 | INFO | app.main:dispatch:21 - START Request: GET https://vigent.hbyrkj.top/api/videos/generated
|
|
||||||
11|vigent2 | 2026-01-29 11:25:37.674 | INFO | app.main:dispatch:22 - HEADERS: {'connection': 'upgrade', 'host': 'vigent.hbyrkj.top', 'x-real-ip': '27.17.161.128', 'x-forwarded-for': '27.17.161.128', 'x-forwarded-proto': 'https', 'sec-ch-ua-platform': '"Windows"', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36', 'accept': 'application/json, text/plain, */*', 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"', 'sec-ch-ua-mobile': '?0', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://vigent.hbyrkj.top/', 'accept-encoding': 'gzip, deflate, br, zstd', 'accept-language': 'en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7', 'priority': 'u=1, i', 'cookie': 'rl_page_init_referrer=RudderEncrypt%3AU2FsdGVkX1%2FRWtsIwIaguDp15em58SDrIwOvRJVXeK4%3D; rl_page_init_referring_domain=RudderEncrypt%3AU2FsdGVkX1%2BVZN6tniQmiO5L2fGVdcrYOkqG%2BRHkNFw%3D; ph_phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo_posthog=%7B%22distinct_id%22%3A%22f6a1ba3602218bc1551bb81b48167bf7484eeb86ed8ee9484fa83f1267023264%230d2437ec-d81b-491c-991f-0b6559daa00d%22%2C%22%24sesid%22%3A%5B1762504332341%2C%22019a5d6c-ae21-7d75-8919-11e9621a135f%22%2C1762503994906%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fn8n.hbyrkj.top%2Fsignin%3Fredirect%3D%25252F%22%7D%7D; rl_anonymous_id=RudderEncrypt%3AU2FsdGVkX19aTpWYcHFt3zhITFSLk1XAMr9V2jBWQwsuLDNLXh93pTlQ%2FUpvwmv6h%2Fl1bW4xH83hrkWPCTkSYg%3D%3D; rl_group_id=RudderEncrypt%3AU2FsdGVkX1%2BhywVlN3t3ypqwAMgqlh7ZRNLMKnFMhxA%3D; rl_group_trait=RudderEncrypt%3AU2FsdGVkX19jAKNOmR%2FnSngsWGVcmYB2qyvsbh3wQc0%3D; rl_user_id=RudderEncrypt%3AU2FsdGVkX180hU8QtHwPe3dPd1o7rEP7efRzFgCIvuIPRwbE3dWE0aEQCCMpQTN%2B7AGEtH6mjRvEuqcbfOdaX4TtJGL2jHbdcZUuA7Mpjf0uvsZ15LToi0zM1NWR7i6wE2z4vcYyFaBdB1uTJq3SxhX2WsqWe4YiT12vld0E%2F5w%3D; rl_trait=RudderEncrypt%3AU2FsdGVkX19tgs8QN46oDejOzAvFyTx%2FIVRu7LAGDzh2eg%2FAhV8eY%2FyjW12D%2BtSOVq6NLF2lSZcY40rlQ%2B1fUc3DAe2euuWhIECOtlxtY5Hho11ZdHGB8lZ4CSLo%2BWmSIjzmkQ33RgkeNF9eYV4AV1PpdZZ%2Fjyl%2BVjCQtaNVV5c%3D; rl_session=RudderEncrypt%3AU2FsdGVkX19jm82pV3xfWHI%2FE6QaUo5xFQZuXuYh%2FkUCBhyJGY7TqzAK3YDkYppIpUipS7LtUSxm6iWAAp3vGhbB58MN7hrVa8imlwsuL7ceFNN%2BR1uTEvKTR8wWKaii2Xzs%2FYnhG3X8kmImIfYZgg%3D%3D; access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGNkOTFlMy03ZDg5LTQ1ZTgtOWQ4NS1lOGJhMDY2MGQ3NGMiLCJzZXNzaW9uX3Rva2VuIjoiMjFjMmEwMmItYjY5Ny00MGVjLWIwMmItMjI1YzJjOWUyZGMzIiwiZXhwIjoxNzcwMjYwODExfQ.MpOjnbwllAzarfaoTk1SzYVMqAEXBEMyRt5UyiJ90Qw'}
|
|
||||||
11|vigent2 | 2026-01-29 11:25:37.781 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/materials?t=1769657141569 - Status: 200 - Duration: 0.13s
|
|
||||||
11|vigent2 | 2026-01-29 11:25:37.792 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/videos/generated - Status: 200 - Duration: 0.12s
|
|
||||||
11|vigent2 | 2026-01-29 11:25:38.087 | INFO | app.main:dispatch:26 - END Request: GET https://vigent.hbyrkj.top/api/ref-audios - Status: 200 - Duration: 0.48s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
rongye@r730-ubuntu:~$ pm2 logs vigent2-latentsync
|
|
||||||
[TAILING] Tailing last 15 lines for [vigent2-latentsync] process (change the value with --lines option)
|
|
||||||
/home/rongye/.pm2/logs/vigent2-latentsync-out.log last 15 lines:
|
|
||||||
/home/rongye/.pm2/logs/vigent2-latentsync-error.log last 15 lines:
|
|
||||||
@@ -42,17 +42,28 @@
|
|||||||
|
|
||||||
| 模块 | 技术选择 | 备选方案 |
|
| 模块 | 技术选择 | 备选方案 |
|
||||||
|------|----------|----------|
|
|------|----------|----------|
|
||||||
| **前端框架** | Next.js 14 | Vue 3 + Vite |
|
| **前端框架** | Next.js 16 | Vue 3 + Vite |
|
||||||
| **UI 组件库** | Tailwind + shadcn/ui | Ant Design |
|
| **UI 组件库** | TailwindCSS (自定义组件) | Ant Design |
|
||||||
| **后端框架** | FastAPI | Flask |
|
| **后端框架** | FastAPI | Flask |
|
||||||
| **任务队列** | Celery + Redis | RQ / Dramatiq |
|
| **任务队列** | FastAPI BackgroundTasks (asyncio) | Celery + Redis |
|
||||||
| **唇形同步** | **LatentSync 1.6** | MuseTalk / Wav2Lip |
|
| **唇形同步** | **LatentSync 1.6** | MuseTalk / Wav2Lip |
|
||||||
| **TTS 配音** | EdgeTTS | CosyVoice |
|
| **TTS 配音** | EdgeTTS | CosyVoice |
|
||||||
| **声音克隆** | **Qwen3-TTS 1.7B** ✅ | GPT-SoVITS |
|
| **声音克隆** | **Qwen3-TTS 1.7B** ✅ | GPT-SoVITS |
|
||||||
| **视频处理** | FFmpeg | MoviePy |
|
| **视频处理** | FFmpeg | MoviePy |
|
||||||
| **自动发布** | social-auto-upload | 自行实现 |
|
| **自动发布** | Playwright | 自行实现 |
|
||||||
| **数据库** | SQLite → PostgreSQL | MySQL |
|
| **数据库** | Supabase (PostgreSQL) | MySQL |
|
||||||
| **文件存储** | 本地 / MinIO | 阿里云 OSS |
|
| **文件存储** | Supabase Storage | 阿里云 OSS |
|
||||||
|
|
||||||
|
> **修正 (18:10)**:当前实现采用 Next.js 16、FastAPI BackgroundTasks 与 Supabase Storage/Auth,自动发布基于 Playwright。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 现状补充 (Day 17)
|
||||||
|
|
||||||
|
- 前端已拆分为组件化结构(`components/home/`),主页面逻辑集中。
|
||||||
|
- 通用工具 `media.ts` 统一处理 API Base / 资源 URL / 日期格式化。
|
||||||
|
- 作品预览弹窗统一样式,并支持素材/发布预览复用。
|
||||||
|
- 标题/字幕预览按素材分辨率缩放,效果更接近成片。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -60,24 +71,11 @@
|
|||||||
|
|
||||||
### 阶段一:核心功能验证 (MVP)
|
### 阶段一:核心功能验证 (MVP)
|
||||||
|
|
||||||
> **目标**:验证 MuseTalk + EdgeTTS 效果,跑通端到端流程
|
> **目标**:验证 LatentSync + EdgeTTS 效果,跑通端到端流程
|
||||||
|
|
||||||
#### 1.1 环境搭建
|
#### 1.1 环境搭建
|
||||||
|
|
||||||
```bash
|
参考 `models/LatentSync/DEPLOY.md` 完成 LatentSync 环境与权重部署。
|
||||||
# 创建项目目录
|
|
||||||
mkdir TalkingHeadAgent
|
|
||||||
cd TalkingHeadAgent
|
|
||||||
|
|
||||||
# 克隆 MuseTalk
|
|
||||||
git clone https://github.com/TMElyralab/MuseTalk.git
|
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
cd MuseTalk
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# 下载模型权重 (按官方文档)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.2 集成 EdgeTTS
|
#### 1.2 集成 EdgeTTS
|
||||||
|
|
||||||
@@ -98,13 +96,13 @@ async def text_to_speech(text: str, voice: str = "zh-CN-YunxiNeural", output_pat
|
|||||||
# test_pipeline.py
|
# test_pipeline.py
|
||||||
"""
|
"""
|
||||||
1. 文案 → EdgeTTS → 音频
|
1. 文案 → EdgeTTS → 音频
|
||||||
2. 静态视频 + 音频 → MuseTalk → 口播视频
|
2. 静态视频 + 音频 → LatentSync → 口播视频
|
||||||
3. 添加字幕 → FFmpeg → 最终视频
|
3. 添加字幕 → FFmpeg → 最终视频
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 1.4 验证标准
|
#### 1.4 验证标准
|
||||||
- [ ] MuseTalk 能正常推理
|
- [ ] LatentSync 能正常推理
|
||||||
- [ ] 唇形与音频同步率 > 90%
|
- [ ] 唇形与音频同步率 > 90%
|
||||||
- [ ] 单个视频生成时间 < 2 分钟
|
- [ ] 单个视频生成时间 < 2 分钟
|
||||||
|
|
||||||
@@ -145,22 +143,16 @@ backend/
|
|||||||
| `/api/materials` | POST | 上传素材视频 | ✅ |
|
| `/api/materials` | POST | 上传素材视频 | ✅ |
|
||||||
| `/api/materials` | GET | 获取素材列表 | ✅ |
|
| `/api/materials` | GET | 获取素材列表 | ✅ |
|
||||||
| `/api/videos/generate` | POST | 创建视频生成任务 | ✅ |
|
| `/api/videos/generate` | POST | 创建视频生成任务 | ✅ |
|
||||||
| `/api/tasks/{id}` | GET | 查询任务状态 | ✅ |
|
| `/api/videos/tasks/{id}` | GET | 查询任务状态 | ✅ |
|
||||||
| `/api/videos/{id}/download` | GET | 下载生成的视频 | ✅ |
|
| `/api/videos/generated` | GET | 获取历史作品列表 | ✅ |
|
||||||
| `/api/publish` | POST | 发布到社交平台 | ✅ |
|
| `/api/publish` | POST | 发布到社交平台 | ✅ |
|
||||||
|
|
||||||
#### 2.3 Celery 任务定义
|
#### 2.3 BackgroundTasks 任务定义
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# tasks/celery_tasks.py
|
# app/api/videos.py
|
||||||
@celery.task
|
background_tasks.add_task(_process_video_generation, task_id, req, user_id)
|
||||||
def generate_video_task(material_id: str, text: str, voice: str):
|
```
|
||||||
# 1. TTS 生成音频
|
|
||||||
# 2. MuseTalk 唇形同步
|
|
||||||
# 3. FFmpeg 添加字幕
|
|
||||||
# 4. 保存并返回视频 URL
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -183,9 +175,9 @@ def generate_video_task(material_id: str, text: str, voice: str):
|
|||||||
# 创建 Next.js 项目
|
# 创建 Next.js 项目
|
||||||
npx create-next-app@latest frontend --typescript --tailwind --app
|
npx create-next-app@latest frontend --typescript --tailwind --app
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install @tanstack/react-query axios
|
npm install axios swr
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,220 +0,0 @@
|
|||||||
# Qwen3-TTS 声音克隆集成到 ViGent2
|
|
||||||
|
|
||||||
## 需求概述
|
|
||||||
1. 前端支持上传/在线录制参考音频(wav, mp3, m4a 等)
|
|
||||||
2. EdgeTTS 音色保留,增加 Qwen3-TTS 声音克隆界面
|
|
||||||
3. 两种 TTS 方式做成统一界面(Tab 切换)
|
|
||||||
4. 声音克隆使用相同的口播文案输入
|
|
||||||
|
|
||||||
## 架构设计
|
|
||||||
|
|
||||||
### GPU 分配
|
|
||||||
| GPU | 服务 | 模型 |
|
|
||||||
|-----|------|------|
|
|
||||||
| GPU0 | Qwen3-TTS | 0.6B-Base (声音克隆) |
|
|
||||||
| GPU1 | LatentSync | 1.6 (唇形同步) |
|
|
||||||
|
|
||||||
### 存储
|
|
||||||
- 新增 Supabase bucket: `ref_audios`
|
|
||||||
- 路径格式: `{user_id}/{timestamp}_{filename}.wav`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实现步骤
|
|
||||||
|
|
||||||
### 1. 后端:新建声音克隆服务
|
|
||||||
**文件**: `backend/app/services/voice_clone_service.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
class VoiceCloneService:
|
|
||||||
def __init__(self):
|
|
||||||
self.gpu_id = 0
|
|
||||||
self.model_path = "models/Qwen3-TTS/checkpoints/0.6B-Base"
|
|
||||||
self._model = None
|
|
||||||
self._lock = asyncio.Lock()
|
|
||||||
|
|
||||||
async def generate_audio(self, text, ref_audio_path, ref_text, output_path, language="Chinese"):
|
|
||||||
# 使用 Qwen3TTSModel.generate_voice_clone()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 后端:新建参考音频 API
|
|
||||||
**文件**: `backend/app/api/ref_audios.py`
|
|
||||||
|
|
||||||
| 接口 | 方法 | 功能 |
|
|
||||||
|------|------|------|
|
|
||||||
| `/api/ref-audios` | POST | 上传参考音频 + ref_text |
|
|
||||||
| `/api/ref-audios` | GET | 列出用户的参考音频 |
|
|
||||||
| `/api/ref-audios/{id}` | DELETE | 删除参考音频 |
|
|
||||||
|
|
||||||
上传时自动转换为 wav (16kHz mono),存储 ref_text 元数据。
|
|
||||||
|
|
||||||
### 3. 后端:修改视频生成 API
|
|
||||||
**文件**: `backend/app/api/videos.py`
|
|
||||||
|
|
||||||
扩展 GenerateRequest:
|
|
||||||
```python
|
|
||||||
class GenerateRequest(BaseModel):
|
|
||||||
text: str
|
|
||||||
voice: str = "zh-CN-YunxiNeural"
|
|
||||||
material_path: str
|
|
||||||
# 新增
|
|
||||||
tts_mode: str = "edgetts" # "edgetts" | "voiceclone"
|
|
||||||
ref_audio_id: Optional[str] = None
|
|
||||||
ref_text: Optional[str] = None
|
|
||||||
```
|
|
||||||
|
|
||||||
修改 `_process_video_generation()`:
|
|
||||||
```python
|
|
||||||
if req.tts_mode == "voiceclone":
|
|
||||||
await voice_clone_service.generate_audio(...)
|
|
||||||
else:
|
|
||||||
await tts_service.generate_audio(...)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 后端:注册路由
|
|
||||||
**文件**: `backend/app/main.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
from app.api import ref_audios
|
|
||||||
app.include_router(ref_audios.router, prefix="/api/ref-audios", tags=["ref-audios"])
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 前端:改造音色选择区域
|
|
||||||
**文件**: `frontend/src/app/page.tsx`
|
|
||||||
|
|
||||||
**新增状态**:
|
|
||||||
```typescript
|
|
||||||
const [ttsMode, setTtsMode] = useState<'edgetts' | 'voiceclone'>('edgetts');
|
|
||||||
const [refAudios, setRefAudios] = useState<RefAudio[]>([]);
|
|
||||||
const [selectedRefAudio, setSelectedRefAudio] = useState<RefAudio | null>(null);
|
|
||||||
const [refText, setRefText] = useState('');
|
|
||||||
|
|
||||||
// 在线录音相关
|
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
|
||||||
const [recordedBlob, setRecordedBlob] = useState<Blob | null>(null);
|
|
||||||
const [recordingTime, setRecordingTime] = useState(0);
|
|
||||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
||||||
```
|
|
||||||
|
|
||||||
**UI 结构**:
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ 🎙️ 选择配音方式 │
|
|
||||||
├─────────────────────────────────────┤
|
|
||||||
│ [EdgeTTS 音色] [声音克隆] ← Tab │
|
|
||||||
├─────────────────────────────────────┤
|
|
||||||
│ Tab 1: 现有音色 2x3 网格 │
|
|
||||||
│ │
|
|
||||||
│ Tab 2: 声音克隆 │
|
|
||||||
│ ┌───────────────────────────────┐ │
|
|
||||||
│ │ 📁 我的参考音频 │ │
|
|
||||||
│ │ [ref1] [ref2] [+上传] │ │
|
|
||||||
│ └───────────────────────────────┘ │
|
|
||||||
│ ┌───────────────────────────────┐ │
|
|
||||||
│ │ 🎤 或在线录音 │ │
|
|
||||||
│ │ [开始录音] [停止] 时长: 0:05 │ │
|
|
||||||
│ │ (录音完成后显示试听和使用按钮) │ │
|
|
||||||
│ └───────────────────────────────┘ │
|
|
||||||
│ ┌───────────────────────────────┐ │
|
|
||||||
│ │ 📝 参考音频文字 (必填) │ │
|
|
||||||
│ │ [textarea] │ │
|
|
||||||
│ └───────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**在线录音逻辑**:
|
|
||||||
```typescript
|
|
||||||
const startRecording = async () => {
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
|
|
||||||
const chunks: BlobPart[] = [];
|
|
||||||
|
|
||||||
mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
|
|
||||||
mediaRecorder.onstop = () => {
|
|
||||||
const blob = new Blob(chunks, { type: 'audio/webm' });
|
|
||||||
setRecordedBlob(blob);
|
|
||||||
stream.getTracks().forEach(track => track.stop());
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaRecorder.start();
|
|
||||||
setIsRecording(true);
|
|
||||||
mediaRecorderRef.current = mediaRecorder;
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopRecording = () => {
|
|
||||||
mediaRecorderRef.current?.stop();
|
|
||||||
setIsRecording(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useRecording = async () => {
|
|
||||||
// 将录音 Blob 上传到后端
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', recordedBlob, 'recording.webm');
|
|
||||||
formData.append('ref_text', refText);
|
|
||||||
const { data } = await api.post('/api/ref-audios', formData);
|
|
||||||
// 上传成功后刷新列表并选中
|
|
||||||
fetchRefAudios();
|
|
||||||
setSelectedRefAudio(data);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 前端:修改生成请求
|
|
||||||
```typescript
|
|
||||||
const handleGenerate = async () => {
|
|
||||||
const payload = {
|
|
||||||
material_path: materialObj.path,
|
|
||||||
text: text,
|
|
||||||
tts_mode: ttsMode,
|
|
||||||
...(ttsMode === 'edgetts'
|
|
||||||
? { voice }
|
|
||||||
: { ref_audio_id: selectedRefAudio.id, ref_text: refText })
|
|
||||||
};
|
|
||||||
await api.post('/api/videos/generate', payload);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 文件清单
|
|
||||||
|
|
||||||
### 新建
|
|
||||||
| 文件 | 描述 |
|
|
||||||
|------|------|
|
|
||||||
| `backend/app/services/voice_clone_service.py` | 声音克隆服务 |
|
|
||||||
| `backend/app/api/ref_audios.py` | 参考音频管理 API |
|
|
||||||
|
|
||||||
### 修改
|
|
||||||
| 文件 | 修改内容 |
|
|
||||||
|------|----------|
|
|
||||||
| `backend/app/api/videos.py` | 扩展 GenerateRequest,修改 TTS 调用逻辑 |
|
|
||||||
| `backend/app/main.py` | 注册 ref_audios 路由 |
|
|
||||||
| `backend/app/services/storage.py` | 添加 BUCKET_REF_AUDIOS |
|
|
||||||
| `frontend/src/app/page.tsx` | Tab 切换 UI、参考音频选择、refText 输入 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 验证方法
|
|
||||||
|
|
||||||
1. **后端测试**:
|
|
||||||
```bash
|
|
||||||
# 启动后端
|
|
||||||
cd backend && uvicorn app.main:app --port 8006
|
|
||||||
|
|
||||||
# 测试参考音频上传
|
|
||||||
curl -X POST http://localhost:8006/api/ref-audios \
|
|
||||||
-F "file=@test.wav" -F "ref_text=测试文字"
|
|
||||||
|
|
||||||
# 测试声音克隆生成
|
|
||||||
curl -X POST http://localhost:8006/api/videos/generate \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"text":"测试文案","tts_mode":"voiceclone","ref_audio_id":"xxx","ref_text":"参考文字","material_path":"..."}'
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **前端测试**:
|
|
||||||
- 打开首页,确认 Tab 切换正常
|
|
||||||
- 上传参考音频,确认列表显示
|
|
||||||
- 选择声音克隆模式,填写参考文字,点击生成
|
|
||||||
- 确认生成的视频使用克隆的声音
|
|
||||||
|
|
||||||
3. **端到端测试**:
|
|
||||||
- 上传参考音频 → 选择声音克隆 → 输入口播文案 → 生成视频 → 播放验证声音
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# ViGent2 开发任务清单 (Task Log)
|
# ViGent2 开发任务清单 (Task Log)
|
||||||
|
|
||||||
**项目**: ViGent2 数字人口播视频生成系统
|
**项目**: ViGent2 数字人口播视频生成系统
|
||||||
**进度**: 100% (Day 16 - 深度优化完成)
|
**进度**: 100% (Day 17 - 前端重构与体验优化)
|
||||||
**更新时间**: 2026-02-03
|
**更新时间**: 2026-02-04
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,11 +10,26 @@
|
|||||||
|
|
||||||
> 这里记录了每一天的核心开发内容与 milestone。
|
> 这里记录了每一天的核心开发内容与 milestone。
|
||||||
|
|
||||||
### Day 16: 深度性能优化 (Current) 🚀
|
### Day 17: 前端重构与体验优化 (Current) 🚀
|
||||||
- [x] **Qwen-TTS 加速**: 集成 Flash Attention 2,模型加载速度提升至 8.9s。
|
- [x] **UI 组件拆分**: 首页拆分为独立组件,降低 `page.tsx` 复杂度。
|
||||||
- [x] **服务守护**: 开发 `Watchdog` 看门狗机制,自动监控并重启僵死服务。
|
- [x] **通用工具抽取**: `media.ts` 统一 API Base / URL / 日期格式化。
|
||||||
- [x] **LatentSync 性能确认**: 验证 DeepCache + 原生 Flash Attn 生效。
|
- [x] **交互优化**: 选择项持久化、列表内定位、刷新回顶部、最新作品优先预览。
|
||||||
- [x] **文档重构**: 全面更新 README、部署手册及后端文档。
|
- [x] **发布页改造**: 作品列表卡片化 + 搜索 + 预览弹窗。
|
||||||
|
- [x] **预览体验**: 预览弹窗统一头部样式与提示文案。
|
||||||
|
- [x] **预览一致性**: 标题/字幕预览按素材分辨率缩放。
|
||||||
|
- [x] **样式默认与持久化**: 默认样式与字号调整,刷新保留用户选择。
|
||||||
|
- [x] **性能微优化**: 列表渲染优化 + 并行请求 + localStorage 防抖。
|
||||||
|
- [x] **资源能力**: 字体/BGM 资源库 + `/api/assets` 接入。
|
||||||
|
- [x] **音频与字幕修复**: BGM 混音稳定性与字幕断句优化。
|
||||||
|
- [x] **持久化修复**: 接入 `useHomePersistence`,恢复 `isRestored` 逻辑并通过构建。
|
||||||
|
- [x] **预览与选择修复**: 发布预览兼容签名 URL,音频试听路径解析,素材/BGM 回退有效项。
|
||||||
|
- [x] **体验细节优化**: 录音预览 URL 回收,预览弹窗滚动恢复,全局任务提示挂载。
|
||||||
|
|
||||||
|
### Day 16: 深度性能优化
|
||||||
|
- [x] **Qwen-TTS 加速**: 集成 Flash Attention 2,模型加载速度提升至 8.9s。
|
||||||
|
- [x] **服务守护**: 开发 `Watchdog` 看门狗机制,自动监控并重启僵死服务。
|
||||||
|
- [x] **LatentSync 性能确认**: 验证 DeepCache + 原生 Flash Attn 生效。
|
||||||
|
- [x] **文档重构**: 全面更新 README、部署手册及后端文档。
|
||||||
|
|
||||||
### Day 15: 手机号认证迁移
|
### Day 15: 手机号认证迁移
|
||||||
- [x] **认证系统升级**: 从邮箱迁移至 11 位手机号注册/登录。
|
- [x] **认证系统升级**: 从邮箱迁移至 11 位手机号注册/登录。
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -15,17 +15,22 @@
|
|||||||
|
|
||||||
## ✨ 功能特性
|
## ✨ 功能特性
|
||||||
|
|
||||||
### 核心能力
|
### 核心能力
|
||||||
- 🎬 **高清唇形同步** - LatentSync 1.6 驱动,512×512 高分辨率 Latent Diffusion 模型。
|
- 🎬 **高清唇形同步** - LatentSync 1.6 驱动,512×512 高分辨率 Latent Diffusion 模型。
|
||||||
- 🎙️ **多模态配音** - 支持 **EdgeTTS** (微软超自然语音) 和 **Qwen3-TTS** (3秒极速声音克隆)。
|
- 🎙️ **多模态配音** - 支持 **EdgeTTS** (微软超自然语音) 和 **Qwen3-TTS** (3秒极速声音克隆)。
|
||||||
- 📝 **智能字幕** - 集成 faster-whisper + Remotion,自动生成逐字高亮 (卡拉OK效果) 字幕。
|
- 📝 **智能字幕** - 集成 faster-whisper + Remotion,自动生成逐字高亮 (卡拉OK效果) 字幕。
|
||||||
- 🤖 **AI 辅助创作** - 内置 GLM-4.7-Flash,支持 B站/抖音链接文案提取、AI 洗稿、标题/标签自动生成。
|
- 🎨 **样式预设** - 标题/字幕样式选择 + 预览 + 字号调节,支持自定义字体库。
|
||||||
|
- 🖼️ **作品预览一致性** - 标题/字幕预览按素材分辨率缩放,效果更接近成片。
|
||||||
|
- 💾 **用户偏好持久化** - 首页状态统一恢复/保存,刷新后延续上次配置。
|
||||||
|
- 🎵 **背景音乐** - 试听 + 音量控制 + 混音,保持配音音量稳定。
|
||||||
|
- 🤖 **AI 辅助创作** - 内置 GLM-4.7-Flash,支持 B站/抖音链接文案提取、AI 洗稿、标题/标签自动生成。
|
||||||
|
|
||||||
### 平台化功能
|
### 平台化功能
|
||||||
- 📱 **全自动发布** - 支持 B站、抖音、小红书定时发布,扫码登录 + Cookie 持久化。
|
- 📱 **全自动发布** - 支持 B站、抖音、小红书定时发布,扫码登录 + Cookie 持久化。
|
||||||
- 🔐 **企业级认证** - 完善的用户隔离系统 (Supabase),支持手机号注册/登录、密码管理。
|
- 🖥️ **发布管理预览** - 支持签名 URL / 相对路径作品预览,确保可直接播放。
|
||||||
|
- 🔐 **认证与隔离** - 基于 Supabase 的用户隔离,支持手机号注册/登录、密码管理。
|
||||||
- 🛡️ **服务守护** - 内置 Watchdog 看门狗机制,自动监控并重启僵死服务,确保 7x24h 稳定运行。
|
- 🛡️ **服务守护** - 内置 Watchdog 看门狗机制,自动监控并重启僵死服务,确保 7x24h 稳定运行。
|
||||||
- 🚀 **极致性能** - 视频预压缩、模型常驻服务 (0s加载)、双 GPU 流水线并发。
|
- 🚀 **性能优化** - 视频预压缩、模型常驻服务(近实时加载)、双 GPU 流水线并发。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -33,7 +38,7 @@
|
|||||||
|
|
||||||
| 领域 | 核心技术 | 说明 |
|
| 领域 | 核心技术 | 说明 |
|
||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| **前端** | Next.js 14 | TypeScript, TailwindCSS, SWR |
|
| **前端** | Next.js 16 | TypeScript, TailwindCSS, SWR |
|
||||||
| **后端** | FastAPI | Python 3.10, AsyncIO, PM2 |
|
| **后端** | FastAPI | Python 3.10, AsyncIO, PM2 |
|
||||||
| **数据库** | Supabase | PostgreSQL, Storage (本地/S3), Auth |
|
| **数据库** | Supabase | PostgreSQL, Storage (本地/S3), Auth |
|
||||||
| **唇形同步** | LatentSync 1.6 | PyTorch 2.5, Diffusers, DeepCache |
|
| **唇形同步** | LatentSync 1.6 | PyTorch 2.5, Diffusers, DeepCache |
|
||||||
|
|||||||
22
backend/app/api/assets.py
Normal file
22
backend/app/api/assets.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app.core.deps import get_current_user
|
||||||
|
from app.services.assets_service import list_styles, list_bgm
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/subtitle-styles")
|
||||||
|
async def list_subtitle_styles(current_user: dict = Depends(get_current_user)):
|
||||||
|
return {"styles": list_styles("subtitle")}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/title-styles")
|
||||||
|
async def list_title_styles(current_user: dict = Depends(get_current_user)):
|
||||||
|
return {"styles": list_styles("title")}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/bgm")
|
||||||
|
async def list_bgm_items(current_user: dict = Depends(get_current_user)):
|
||||||
|
return {"bgm": list_bgm()}
|
||||||
@@ -8,13 +8,19 @@ import traceback
|
|||||||
import time
|
import time
|
||||||
import httpx
|
import httpx
|
||||||
import os
|
import os
|
||||||
from app.services.tts_service import TTSService
|
from app.services.tts_service import TTSService
|
||||||
from app.services.video_service import VideoService
|
from app.services.video_service import VideoService
|
||||||
from app.services.lipsync_service import LipSyncService
|
from app.services.lipsync_service import LipSyncService
|
||||||
from app.services.voice_clone_service import voice_clone_service
|
from app.services.voice_clone_service import voice_clone_service
|
||||||
from app.services.storage import storage_service
|
from app.services.assets_service import (
|
||||||
from app.services.whisper_service import whisper_service
|
get_style,
|
||||||
from app.services.remotion_service import remotion_service
|
get_default_style,
|
||||||
|
resolve_bgm_path,
|
||||||
|
prepare_style_for_remotion,
|
||||||
|
)
|
||||||
|
from app.services.storage import storage_service
|
||||||
|
from app.services.whisper_service import whisper_service
|
||||||
|
from app.services.remotion_service import remotion_service
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.deps import get_current_user
|
from app.core.deps import get_current_user
|
||||||
|
|
||||||
@@ -28,9 +34,15 @@ class GenerateRequest(BaseModel):
|
|||||||
tts_mode: str = "edgetts" # "edgetts" | "voiceclone"
|
tts_mode: str = "edgetts" # "edgetts" | "voiceclone"
|
||||||
ref_audio_id: Optional[str] = None # 参考音频 storage path
|
ref_audio_id: Optional[str] = None # 参考音频 storage path
|
||||||
ref_text: Optional[str] = None # 参考音频的转写文字
|
ref_text: Optional[str] = None # 参考音频的转写文字
|
||||||
# 字幕和标题功能
|
# 字幕和标题功能
|
||||||
title: Optional[str] = None # 视频标题(片头显示)
|
title: Optional[str] = None # 视频标题(片头显示)
|
||||||
enable_subtitles: bool = True # 是否启用逐字高亮字幕
|
enable_subtitles: bool = True # 是否启用逐字高亮字幕
|
||||||
|
subtitle_style_id: Optional[str] = None # 字幕样式 ID
|
||||||
|
title_style_id: Optional[str] = None # 标题样式 ID
|
||||||
|
subtitle_font_size: Optional[int] = None # 字幕字号(覆盖样式)
|
||||||
|
title_font_size: Optional[int] = None # 标题字号(覆盖样式)
|
||||||
|
bgm_id: Optional[str] = None # 背景音乐 ID
|
||||||
|
bgm_volume: Optional[float] = 0.2 # 背景音乐音量 (0-1)
|
||||||
|
|
||||||
tasks = {} # In-memory task store
|
tasks = {} # In-memory task store
|
||||||
|
|
||||||
@@ -52,15 +64,15 @@ async def _check_lipsync_ready(force: bool = False) -> bool:
|
|||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
# 5分钟缓存
|
# 5分钟缓存
|
||||||
if not force and _lipsync_ready is not None and (now - _lipsync_last_check) < 300:
|
if not force and _lipsync_ready is not None and (now - _lipsync_last_check) < 300:
|
||||||
return _lipsync_ready
|
return bool(_lipsync_ready)
|
||||||
|
|
||||||
lipsync = _get_lipsync_service()
|
lipsync = _get_lipsync_service()
|
||||||
health = await lipsync.check_health()
|
health = await lipsync.check_health()
|
||||||
_lipsync_ready = health.get("ready", False)
|
_lipsync_ready = health.get("ready", False)
|
||||||
_lipsync_last_check = now
|
_lipsync_last_check = now
|
||||||
print(f"[LipSync] Health check: ready={_lipsync_ready}")
|
print(f"[LipSync] Health check: ready={_lipsync_ready}")
|
||||||
return _lipsync_ready
|
return bool(_lipsync_ready)
|
||||||
|
|
||||||
async def _download_material(path_or_url: str, temp_path: Path):
|
async def _download_material(path_or_url: str, temp_path: Path):
|
||||||
"""下载素材到临时文件 (流式下载,节省内存)"""
|
"""下载素材到临时文件 (流式下载,节省内存)"""
|
||||||
@@ -194,25 +206,79 @@ async def _process_video_generation(task_id: str, req: GenerateRequest, user_id:
|
|||||||
logger.warning(f"Whisper alignment failed, skipping subtitles: {e}")
|
logger.warning(f"Whisper alignment failed, skipping subtitles: {e}")
|
||||||
captions_path = None
|
captions_path = None
|
||||||
|
|
||||||
tasks[task_id]["progress"] = 85
|
tasks[task_id]["progress"] = 85
|
||||||
|
|
||||||
|
# 3.5 背景音乐混音(不影响唇形与字幕对齐)
|
||||||
|
video = VideoService()
|
||||||
|
final_audio_path = audio_path
|
||||||
|
if req.bgm_id:
|
||||||
|
tasks[task_id]["message"] = "正在合成背景音乐..."
|
||||||
|
tasks[task_id]["progress"] = 86
|
||||||
|
|
||||||
|
bgm_path = resolve_bgm_path(req.bgm_id)
|
||||||
|
if bgm_path:
|
||||||
|
mix_output_path = temp_dir / f"{task_id}_audio_mix.wav"
|
||||||
|
temp_files.append(mix_output_path)
|
||||||
|
volume = req.bgm_volume if req.bgm_volume is not None else 0.2
|
||||||
|
volume = max(0.0, min(float(volume), 1.0))
|
||||||
|
try:
|
||||||
|
video.mix_audio(
|
||||||
|
voice_path=str(audio_path),
|
||||||
|
bgm_path=str(bgm_path),
|
||||||
|
output_path=str(mix_output_path),
|
||||||
|
bgm_volume=volume
|
||||||
|
)
|
||||||
|
final_audio_path = mix_output_path
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"BGM mix failed, fallback to voice only: {e}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"BGM not found: {req.bgm_id}")
|
||||||
|
|
||||||
# 4. Remotion 视频合成(字幕 + 标题)- 进度 85% -> 95%
|
# 4. Remotion 视频合成(字幕 + 标题)- 进度 85% -> 95%
|
||||||
# 判断是否需要使用 Remotion(有字幕或标题时使用)
|
# 判断是否需要使用 Remotion(有字幕或标题时使用)
|
||||||
use_remotion = (captions_path and captions_path.exists()) or req.title
|
use_remotion = (captions_path and captions_path.exists()) or req.title
|
||||||
|
|
||||||
|
subtitle_style = None
|
||||||
|
title_style = None
|
||||||
|
if req.enable_subtitles:
|
||||||
|
subtitle_style = get_style("subtitle", req.subtitle_style_id) or get_default_style("subtitle")
|
||||||
|
if req.title:
|
||||||
|
title_style = get_style("title", req.title_style_id) or get_default_style("title")
|
||||||
|
|
||||||
|
if req.subtitle_font_size and req.enable_subtitles:
|
||||||
|
if subtitle_style is None:
|
||||||
|
subtitle_style = {}
|
||||||
|
subtitle_style["font_size"] = int(req.subtitle_font_size)
|
||||||
|
|
||||||
|
if req.title_font_size and req.title:
|
||||||
|
if title_style is None:
|
||||||
|
title_style = {}
|
||||||
|
title_style["font_size"] = int(req.title_font_size)
|
||||||
|
|
||||||
|
if use_remotion:
|
||||||
|
subtitle_style = prepare_style_for_remotion(
|
||||||
|
subtitle_style,
|
||||||
|
temp_dir,
|
||||||
|
f"{task_id}_subtitle_font"
|
||||||
|
)
|
||||||
|
title_style = prepare_style_for_remotion(
|
||||||
|
title_style,
|
||||||
|
temp_dir,
|
||||||
|
f"{task_id}_title_font"
|
||||||
|
)
|
||||||
|
|
||||||
final_output_local_path = temp_dir / f"{task_id}_output.mp4"
|
final_output_local_path = temp_dir / f"{task_id}_output.mp4"
|
||||||
temp_files.append(final_output_local_path)
|
temp_files.append(final_output_local_path)
|
||||||
|
|
||||||
if use_remotion:
|
if use_remotion:
|
||||||
tasks[task_id]["message"] = "正在合成视频 (Remotion)..."
|
tasks[task_id]["message"] = "正在合成视频 (Remotion)..."
|
||||||
tasks[task_id]["progress"] = 87
|
tasks[task_id]["progress"] = 87
|
||||||
|
|
||||||
# 先用 FFmpeg 合成音视频(Remotion 需要带音频的视频)
|
# 先用 FFmpeg 合成音视频(Remotion 需要带音频的视频)
|
||||||
composed_video_path = temp_dir / f"{task_id}_composed.mp4"
|
composed_video_path = temp_dir / f"{task_id}_composed.mp4"
|
||||||
temp_files.append(composed_video_path)
|
temp_files.append(composed_video_path)
|
||||||
|
|
||||||
video = VideoService()
|
await video.compose(str(lipsync_video_path), str(final_audio_path), str(composed_video_path))
|
||||||
await video.compose(str(lipsync_video_path), str(audio_path), str(composed_video_path))
|
|
||||||
|
|
||||||
# 检查 Remotion 是否可用
|
# 检查 Remotion 是否可用
|
||||||
remotion_health = await remotion_service.check_health()
|
remotion_health = await remotion_service.check_health()
|
||||||
@@ -223,16 +289,18 @@ async def _process_video_generation(task_id: str, req: GenerateRequest, user_id:
|
|||||||
mapped = 87 + int(percent * 0.08)
|
mapped = 87 + int(percent * 0.08)
|
||||||
tasks[task_id]["progress"] = mapped
|
tasks[task_id]["progress"] = mapped
|
||||||
|
|
||||||
await remotion_service.render(
|
await remotion_service.render(
|
||||||
video_path=str(composed_video_path),
|
video_path=str(composed_video_path),
|
||||||
output_path=str(final_output_local_path),
|
output_path=str(final_output_local_path),
|
||||||
captions_path=str(captions_path) if captions_path else None,
|
captions_path=str(captions_path) if captions_path else None,
|
||||||
title=req.title,
|
title=req.title,
|
||||||
title_duration=3.0,
|
title_duration=3.0,
|
||||||
fps=25,
|
fps=25,
|
||||||
enable_subtitles=req.enable_subtitles,
|
enable_subtitles=req.enable_subtitles,
|
||||||
on_progress=on_remotion_progress
|
subtitle_style=subtitle_style,
|
||||||
)
|
title_style=title_style,
|
||||||
|
on_progress=on_remotion_progress
|
||||||
|
)
|
||||||
print(f"[Pipeline] Remotion render completed")
|
print(f"[Pipeline] Remotion render completed")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Remotion render failed, using FFmpeg fallback: {e}")
|
logger.warning(f"Remotion render failed, using FFmpeg fallback: {e}")
|
||||||
@@ -248,8 +316,7 @@ async def _process_video_generation(task_id: str, req: GenerateRequest, user_id:
|
|||||||
tasks[task_id]["message"] = "正在合成最终视频..."
|
tasks[task_id]["message"] = "正在合成最终视频..."
|
||||||
tasks[task_id]["progress"] = 90
|
tasks[task_id]["progress"] = 90
|
||||||
|
|
||||||
video = VideoService()
|
await video.compose(str(lipsync_video_path), str(final_audio_path), str(final_output_local_path))
|
||||||
await video.compose(str(lipsync_video_path), str(audio_path), str(final_output_local_path))
|
|
||||||
|
|
||||||
total_time = time.time() - start_time
|
total_time = time.time() - start_time
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
# 基础路径配置
|
# 基础路径配置
|
||||||
BASE_DIR: Path = Path(__file__).resolve().parent.parent
|
BASE_DIR: Path = Path(__file__).resolve().parent.parent
|
||||||
UPLOAD_DIR: Path = BASE_DIR.parent / "uploads"
|
UPLOAD_DIR: Path = BASE_DIR.parent / "uploads"
|
||||||
OUTPUT_DIR: Path = BASE_DIR.parent / "outputs"
|
OUTPUT_DIR: Path = BASE_DIR.parent / "outputs"
|
||||||
|
ASSETS_DIR: Path = BASE_DIR.parent / "assets"
|
||||||
|
|
||||||
# 数据库/缓存
|
# 数据库/缓存
|
||||||
REDIS_URL: str = "redis://localhost:6379/0"
|
REDIS_URL: str = "redis://localhost:6379/0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from app.core import config
|
from app.core import config
|
||||||
from app.api import materials, videos, publish, login_helper, auth, admin, ref_audios, ai, tools
|
from app.api import materials, videos, publish, login_helper, auth, admin, ref_audios, ai, tools, assets
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -41,12 +41,14 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create dirs
|
# Create dirs
|
||||||
settings.UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
settings.UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
settings.OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
settings.OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
(settings.UPLOAD_DIR / "materials").mkdir(exist_ok=True)
|
(settings.UPLOAD_DIR / "materials").mkdir(exist_ok=True)
|
||||||
|
settings.ASSETS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
app.mount("/outputs", StaticFiles(directory=str(settings.OUTPUT_DIR)), name="outputs")
|
app.mount("/outputs", StaticFiles(directory=str(settings.OUTPUT_DIR)), name="outputs")
|
||||||
app.mount("/uploads", StaticFiles(directory=str(settings.UPLOAD_DIR)), name="uploads")
|
app.mount("/uploads", StaticFiles(directory=str(settings.UPLOAD_DIR)), name="uploads")
|
||||||
|
app.mount("/assets", StaticFiles(directory=str(settings.ASSETS_DIR)), name="assets")
|
||||||
|
|
||||||
# 注册路由
|
# 注册路由
|
||||||
app.include_router(materials.router, prefix="/api/materials", tags=["Materials"])
|
app.include_router(materials.router, prefix="/api/materials", tags=["Materials"])
|
||||||
@@ -55,9 +57,10 @@ app.include_router(publish.router, prefix="/api/publish", tags=["Publish"])
|
|||||||
app.include_router(login_helper.router, prefix="/api", tags=["LoginHelper"])
|
app.include_router(login_helper.router, prefix="/api", tags=["LoginHelper"])
|
||||||
app.include_router(auth.router) # /api/auth
|
app.include_router(auth.router) # /api/auth
|
||||||
app.include_router(admin.router) # /api/admin
|
app.include_router(admin.router) # /api/admin
|
||||||
app.include_router(ref_audios.router, prefix="/api/ref-audios", tags=["RefAudios"])
|
app.include_router(ref_audios.router, prefix="/api/ref-audios", tags=["RefAudios"])
|
||||||
app.include_router(ai.router) # /api/ai
|
app.include_router(ai.router) # /api/ai
|
||||||
app.include_router(tools.router, prefix="/api/tools", tags=["Tools"])
|
app.include_router(tools.router, prefix="/api/tools", tags=["Tools"])
|
||||||
|
app.include_router(assets.router, prefix="/api/assets", tags=["Assets"])
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
128
backend/app/services/assets_service.py
Normal file
128
backend/app/services/assets_service.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
BGM_EXTENSIONS = {".wav", ".mp3", ".m4a", ".aac", ".flac", ".ogg", ".webm"}
|
||||||
|
|
||||||
|
|
||||||
|
def _style_file_path(style_type: str) -> Path:
|
||||||
|
return settings.ASSETS_DIR / "styles" / f"{style_type}.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_style_file(style_type: str) -> List[Dict[str, Any]]:
|
||||||
|
style_path = _style_file_path(style_type)
|
||||||
|
if not style_path.exists():
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
with open(style_path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load style file {style_path}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def list_styles(style_type: str) -> List[Dict[str, Any]]:
|
||||||
|
return _load_style_file(style_type)
|
||||||
|
|
||||||
|
|
||||||
|
def get_style(style_type: str, style_id: Optional[str]) -> Optional[Dict[str, Any]]:
|
||||||
|
if not style_id:
|
||||||
|
return None
|
||||||
|
for item in _load_style_file(style_type):
|
||||||
|
if item.get("id") == style_id:
|
||||||
|
return item
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_style(style_type: str) -> Optional[Dict[str, Any]]:
|
||||||
|
styles = _load_style_file(style_type)
|
||||||
|
if not styles:
|
||||||
|
return None
|
||||||
|
for item in styles:
|
||||||
|
if item.get("is_default"):
|
||||||
|
return item
|
||||||
|
return styles[0]
|
||||||
|
|
||||||
|
|
||||||
|
def list_bgm() -> List[Dict[str, Any]]:
|
||||||
|
bgm_root = settings.ASSETS_DIR / "bgm"
|
||||||
|
if not bgm_root.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
items: List[Dict[str, Any]] = []
|
||||||
|
for path in bgm_root.rglob("*"):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
if path.suffix.lower() not in BGM_EXTENSIONS:
|
||||||
|
continue
|
||||||
|
rel = path.relative_to(bgm_root).as_posix()
|
||||||
|
items.append({
|
||||||
|
"id": rel,
|
||||||
|
"name": path.stem,
|
||||||
|
"ext": path.suffix.lower().lstrip(".")
|
||||||
|
})
|
||||||
|
|
||||||
|
items.sort(key=lambda x: x.get("name", ""))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_bgm_path(bgm_id: str) -> Optional[Path]:
|
||||||
|
if not bgm_id:
|
||||||
|
return None
|
||||||
|
bgm_root = settings.ASSETS_DIR / "bgm"
|
||||||
|
candidate = (bgm_root / bgm_id).resolve()
|
||||||
|
try:
|
||||||
|
candidate.relative_to(bgm_root.resolve())
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if candidate.exists() and candidate.is_file():
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_style_for_remotion(
|
||||||
|
style: Optional[Dict[str, Any]],
|
||||||
|
temp_dir: Path,
|
||||||
|
prefix: str
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
if not style:
|
||||||
|
return None
|
||||||
|
|
||||||
|
prepared = dict(style)
|
||||||
|
font_file = prepared.get("font_file")
|
||||||
|
if not font_file:
|
||||||
|
return prepared
|
||||||
|
|
||||||
|
source_font = (settings.ASSETS_DIR / "fonts" / font_file).resolve()
|
||||||
|
try:
|
||||||
|
source_font.relative_to((settings.ASSETS_DIR / "fonts").resolve())
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Font path outside assets: {font_file}")
|
||||||
|
return prepared
|
||||||
|
|
||||||
|
if not source_font.exists():
|
||||||
|
logger.warning(f"Font file missing: {source_font}")
|
||||||
|
return prepared
|
||||||
|
|
||||||
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
ext = source_font.suffix.lower()
|
||||||
|
target_name = f"{prefix}{ext}"
|
||||||
|
target_path = temp_dir / target_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.copy(source_font, target_path)
|
||||||
|
prepared["font_file"] = target_name
|
||||||
|
if not prepared.get("font_family"):
|
||||||
|
prepared["font_family"] = prefix
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to copy font {source_font} -> {target_path}: {e}")
|
||||||
|
|
||||||
|
return prepared
|
||||||
@@ -4,6 +4,7 @@ Remotion 视频渲染服务
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -30,6 +31,8 @@ class RemotionService:
|
|||||||
title_duration: float = 3.0,
|
title_duration: float = 3.0,
|
||||||
fps: int = 25,
|
fps: int = 25,
|
||||||
enable_subtitles: bool = True,
|
enable_subtitles: bool = True,
|
||||||
|
subtitle_style: Optional[dict] = None,
|
||||||
|
title_style: Optional[dict] = None,
|
||||||
on_progress: Optional[callable] = None
|
on_progress: Optional[callable] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -64,6 +67,12 @@ class RemotionService:
|
|||||||
cmd.extend(["--title", title])
|
cmd.extend(["--title", title])
|
||||||
cmd.extend(["--titleDuration", str(title_duration)])
|
cmd.extend(["--titleDuration", str(title_duration)])
|
||||||
|
|
||||||
|
if subtitle_style:
|
||||||
|
cmd.extend(["--subtitleStyle", json.dumps(subtitle_style, ensure_ascii=False)])
|
||||||
|
|
||||||
|
if title_style:
|
||||||
|
cmd.extend(["--titleStyle", json.dumps(title_style, ensure_ascii=False)])
|
||||||
|
|
||||||
logger.info(f"Running Remotion render: {' '.join(cmd)}")
|
logger.info(f"Running Remotion render: {' '.join(cmd)}")
|
||||||
|
|
||||||
# 在线程池中运行子进程
|
# 在线程池中运行子进程
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
视频合成服务
|
视频合成服务
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
|
import shlex
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -12,18 +13,18 @@ class VideoService:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _run_ffmpeg(self, cmd: list) -> bool:
|
def _run_ffmpeg(self, cmd: list) -> bool:
|
||||||
cmd_str = ' '.join(f'"{c}"' if ' ' in c or '\\' in c else c for c in cmd)
|
cmd_str = ' '.join(shlex.quote(str(c)) for c in cmd)
|
||||||
logger.debug(f"FFmpeg CMD: {cmd_str}")
|
logger.debug(f"FFmpeg CMD: {cmd_str}")
|
||||||
try:
|
try:
|
||||||
# Synchronous call for BackgroundTasks compatibility
|
# Synchronous call for BackgroundTasks compatibility
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd_str,
|
cmd,
|
||||||
shell=True,
|
shell=False,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding='utf-8',
|
encoding='utf-8',
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logger.error(f"FFmpeg Error: {result.stderr}")
|
logger.error(f"FFmpeg Error: {result.stderr}")
|
||||||
return False
|
return False
|
||||||
@@ -32,9 +33,9 @@ class VideoService:
|
|||||||
logger.error(f"FFmpeg Exception: {e}")
|
logger.error(f"FFmpeg Exception: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _get_duration(self, file_path: str) -> float:
|
def _get_duration(self, file_path: str) -> float:
|
||||||
# Synchronous call for BackgroundTasks compatibility
|
# Synchronous call for BackgroundTasks compatibility
|
||||||
cmd = f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "{file_path}"'
|
cmd = f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "{file_path}"'
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
@@ -44,7 +45,39 @@ class VideoService:
|
|||||||
)
|
)
|
||||||
return float(result.stdout.strip())
|
return float(result.stdout.strip())
|
||||||
except Exception:
|
except Exception:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
def mix_audio(
|
||||||
|
self,
|
||||||
|
voice_path: str,
|
||||||
|
bgm_path: str,
|
||||||
|
output_path: str,
|
||||||
|
bgm_volume: float = 0.2
|
||||||
|
) -> str:
|
||||||
|
"""混合人声与背景音乐"""
|
||||||
|
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
volume = max(0.0, min(float(bgm_volume), 1.0))
|
||||||
|
filter_complex = (
|
||||||
|
f"[0:a]volume=1.0[a0];"
|
||||||
|
f"[1:a]volume={volume}[a1];"
|
||||||
|
f"[a0][a1]amix=inputs=2:duration=first:dropout_transition=2:normalize=0[aout]"
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y",
|
||||||
|
"-i", voice_path,
|
||||||
|
"-stream_loop", "-1", "-i", bgm_path,
|
||||||
|
"-filter_complex", filter_complex,
|
||||||
|
"-map", "[aout]",
|
||||||
|
"-c:a", "pcm_s16le",
|
||||||
|
"-shortest",
|
||||||
|
output_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
if self._run_ffmpeg(cmd):
|
||||||
|
return output_path
|
||||||
|
raise RuntimeError("FFmpeg audio mix failed")
|
||||||
|
|
||||||
async def compose(
|
async def compose(
|
||||||
self,
|
self,
|
||||||
|
|||||||
58
backend/assets/styles/subtitle.json
Normal file
58
backend/assets/styles/subtitle.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "subtitle_classic_yellow",
|
||||||
|
"label": "经典黄字",
|
||||||
|
"font_file": "title/思源黑体/SourceHanSansCN-Bold思源黑体免费.otf",
|
||||||
|
"font_family": "SourceHanSansCN-Bold",
|
||||||
|
"font_size": 52,
|
||||||
|
"highlight_color": "#FFE600",
|
||||||
|
"normal_color": "#FFFFFF",
|
||||||
|
"stroke_color": "#000000",
|
||||||
|
"stroke_size": 3,
|
||||||
|
"letter_spacing": 2,
|
||||||
|
"bottom_margin": 80,
|
||||||
|
"is_default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "subtitle_cyan",
|
||||||
|
"label": "清爽青蓝",
|
||||||
|
"font_file": "DingTalk Sans.ttf",
|
||||||
|
"font_family": "DingTalkSans",
|
||||||
|
"font_size": 48,
|
||||||
|
"highlight_color": "#00E5FF",
|
||||||
|
"normal_color": "#FFFFFF",
|
||||||
|
"stroke_color": "#000000",
|
||||||
|
"stroke_size": 3,
|
||||||
|
"letter_spacing": 1,
|
||||||
|
"bottom_margin": 76,
|
||||||
|
"is_default": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "subtitle_orange",
|
||||||
|
"label": "活力橙",
|
||||||
|
"font_file": "simhei.ttf",
|
||||||
|
"font_family": "SimHei",
|
||||||
|
"font_size": 50,
|
||||||
|
"highlight_color": "#FF8A00",
|
||||||
|
"normal_color": "#FFFFFF",
|
||||||
|
"stroke_color": "#000000",
|
||||||
|
"stroke_size": 3,
|
||||||
|
"letter_spacing": 2,
|
||||||
|
"bottom_margin": 80,
|
||||||
|
"is_default": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "subtitle_clean_white",
|
||||||
|
"label": "纯白轻描",
|
||||||
|
"font_file": "DingTalk JinBuTi.ttf",
|
||||||
|
"font_family": "DingTalkJinBuTi",
|
||||||
|
"font_size": 46,
|
||||||
|
"highlight_color": "#FFFFFF",
|
||||||
|
"normal_color": "#FFFFFF",
|
||||||
|
"stroke_color": "#111111",
|
||||||
|
"stroke_size": 2,
|
||||||
|
"letter_spacing": 1,
|
||||||
|
"bottom_margin": 72,
|
||||||
|
"is_default": false
|
||||||
|
}
|
||||||
|
]
|
||||||
58
backend/assets/styles/title.json
Normal file
58
backend/assets/styles/title.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "title_bold_white",
|
||||||
|
"label": "黑体大标题",
|
||||||
|
"font_file": "title/思源黑体/SourceHanSansCN-Heavy思源黑体免费.otf",
|
||||||
|
"font_family": "SourceHanSansCN-Heavy",
|
||||||
|
"font_size": 72,
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"stroke_color": "#000000",
|
||||||
|
"stroke_size": 8,
|
||||||
|
"letter_spacing": 4,
|
||||||
|
"top_margin": 60,
|
||||||
|
"font_weight": 900,
|
||||||
|
"is_default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "title_serif_gold",
|
||||||
|
"label": "宋体金色",
|
||||||
|
"font_file": "title/思源宋体/SourceHanSerifCN-SemiBold思源宋体免费.otf",
|
||||||
|
"font_family": "SourceHanSerifCN-SemiBold",
|
||||||
|
"font_size": 70,
|
||||||
|
"color": "#FDE68A",
|
||||||
|
"stroke_color": "#2B1B00",
|
||||||
|
"stroke_size": 8,
|
||||||
|
"letter_spacing": 3,
|
||||||
|
"top_margin": 58,
|
||||||
|
"font_weight": 800,
|
||||||
|
"is_default": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "title_douyin",
|
||||||
|
"label": "抖音活力",
|
||||||
|
"font_file": "title/抖音美好体开源.otf",
|
||||||
|
"font_family": "DouyinMeiHao",
|
||||||
|
"font_size": 72,
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"stroke_color": "#1F0A00",
|
||||||
|
"stroke_size": 8,
|
||||||
|
"letter_spacing": 4,
|
||||||
|
"top_margin": 60,
|
||||||
|
"font_weight": 900,
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# ViGent2 Frontend
|
|
||||||
|
|
||||||
ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。
|
|
||||||
|
|
||||||
## ✨ 核心功能
|
|
||||||
|
|
||||||
### 1. 视频生成 (`/`)
|
|
||||||
- **素材管理**: 拖拽上传人物视频,实时预览。
|
|
||||||
- **文案配音**: 集成 EdgeTTS,支持多音色选择 (云溪 / 晓晓)。
|
|
||||||
- **AI 标题/标签**: 一键生成视频标题与标签 (Day 14)。
|
|
||||||
- **进度追踪**: 实时显示视频生成进度 (10% -> 100%)。
|
|
||||||
- **结果预览**: 生成完成后直接播放下载。
|
|
||||||
- **本地保存**: 文案/标题自动保存,刷新后恢复 (Day 14)。
|
|
||||||
|
|
||||||
### 2. 全自动发布 (`/publish`) [Day 7 新增]
|
|
||||||
- **多平台管理**: 统一管理 B站、抖音、小红书账号状态。
|
|
||||||
- **扫码登录**:
|
|
||||||
- 集成后端 Playwright 生成的 QR Code。
|
|
||||||
- 实时检测扫码状态 (Wait/Success)。
|
|
||||||
- Cookie 自动保存与状态同步。
|
|
||||||
- **发布配置**: 设置视频标题、标签、简介。
|
|
||||||
- **定时任务**: 支持 "立即发布" 或 "定时发布"。
|
|
||||||
|
|
||||||
### 3. 声音克隆 [Day 13 新增]
|
|
||||||
- **TTS 模式选择**: EdgeTTS (预设音色) / 声音克隆 (自定义音色) 切换。
|
|
||||||
- **参考音频管理**: 上传/列表/删除参考音频 (3-20秒 WAV)。
|
|
||||||
- **一键克隆**: 选择参考音频后自动调用 Qwen3-TTS 服务。
|
|
||||||
|
|
||||||
### 4. 字幕与标题 [Day 13 新增]
|
|
||||||
- **片头标题**: 可选输入,视频开头显示 3 秒淡入淡出标题。
|
|
||||||
- **逐字高亮字幕**: 卡拉OK效果,默认开启,可关闭。
|
|
||||||
- **自动对齐**: 基于 faster-whisper 生成字级别时间戳。
|
|
||||||
|
|
||||||
### 5. 账户设置 [Day 15 新增]
|
|
||||||
- **手机号登录**: 11位中国手机号验证登录。
|
|
||||||
- **账户下拉菜单**: 显示有效期 + 修改密码 + 安全退出。
|
|
||||||
- **修改密码**: 弹窗输入当前密码与新密码,修改后强制重新登录。
|
|
||||||
|
|
||||||
### 6. 文案提取助手 (`ScriptExtractionModal`) [Day 15 新增]
|
|
||||||
- **多源提取**: 支持文件拖拽上传与 URL 粘贴 (B站/抖音/TikTok)。
|
|
||||||
- **AI 洗稿**: 集成 GLM-4.7-Flash,自动改写为口播文案。
|
|
||||||
- **一键填入**: 提取结果直接填充至视频生成输入框。
|
|
||||||
- **智能交互**: 实时进度展示,防误触设计。
|
|
||||||
|
|
||||||
## 🛠️ 技术栈
|
|
||||||
|
|
||||||
- **框架**: Next.js 14 (App Router)
|
|
||||||
- **样式**: TailwindCSS
|
|
||||||
- **图标**: Lucide React
|
|
||||||
- **组件**: 自定义现代化组件 (Glassmorphism 风格)
|
|
||||||
- **API**: Axios 实例 `@/lib/axios` (对接后端 FastAPI :8006)
|
|
||||||
|
|
||||||
## 🚀 开发指南
|
|
||||||
|
|
||||||
### 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 启动开发服务器
|
|
||||||
|
|
||||||
默认运行在 **3002** 端口 (通过 `package.json` 配置):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# 访问: http://localhost:3002
|
|
||||||
```
|
|
||||||
|
|
||||||
### 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── app/
|
|
||||||
│ ├── page.tsx # 视频生成主页
|
|
||||||
│ ├── publish/ # 发布管理页
|
|
||||||
│ │ └── page.tsx
|
|
||||||
│ └── layout.tsx # 全局布局 (导航栏)
|
|
||||||
├── components/ # UI 组件
|
|
||||||
│ ├── VideoUploader.tsx # 视频上传
|
|
||||||
│ ├── StatusBadge.tsx # 状态徽章
|
|
||||||
│ └── ...
|
|
||||||
└── lib/ # 工具函数
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔌 后端对接
|
|
||||||
|
|
||||||
- **Base URL**: `http://localhost:8006`
|
|
||||||
- **代理配置**: Next.js Rewrites (如需) 或直接 CORS。
|
|
||||||
|
|
||||||
## 🎨 设计规范
|
|
||||||
|
|
||||||
- **主色调**: 深紫/黑色系 (Dark Mode)
|
|
||||||
- **交互**: 悬停微动画 (Hover Effects)
|
|
||||||
- **响应式**: 适配桌面端大屏操作
|
|
||||||
@@ -16,6 +16,10 @@ const nextConfig: NextConfig = {
|
|||||||
source: '/outputs/:path*',
|
source: '/outputs/:path*',
|
||||||
destination: 'http://localhost:8006/outputs/:path*', // 转发生成的视频
|
destination: 'http://localhost:8006/outputs/:path*', // 转发生成的视频
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: '/assets/:path*',
|
||||||
|
destination: 'http://localhost:8006/assets/:path*', // 转发静态资源(字体/音乐)
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export default function RootLayout({
|
|||||||
>
|
>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<TaskProvider>
|
<TaskProvider>
|
||||||
|
<GlobalTaskIndicator />
|
||||||
{children}
|
{children}
|
||||||
</TaskProvider>
|
</TaskProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,30 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } 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";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { getApiBaseUrl, formatDate, resolveMediaUrl, isAbsoluteUrl } from "@/lib/media";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import AccountSettingsDropdown from "@/components/AccountSettingsDropdown";
|
||||||
|
import VideoPreviewModal from "@/components/VideoPreviewModal";
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
RotateCcw,
|
||||||
|
LogOut,
|
||||||
|
QrCode,
|
||||||
|
Rocket,
|
||||||
|
Clock,
|
||||||
|
RefreshCw,
|
||||||
|
Search,
|
||||||
|
Eye,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
// SWR fetcher 使用 axios(自动处理 401/403)
|
// SWR fetcher 使用 axios(自动处理 401/403)
|
||||||
const fetcher = (url: string) => api.get(url).then((res) => res.data);
|
const fetcher = (url: string) => api.get(url).then((res) => res.data);
|
||||||
|
|
||||||
// 动态获取 API 地址:服务端使用 localhost,客户端使用当前域名
|
// 动态获取 API 地址:服务端使用 localhost,客户端使用当前域名
|
||||||
const API_BASE = typeof window === 'undefined'
|
const API_BASE = getApiBaseUrl();
|
||||||
? 'http://localhost:8006'
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// 格式化日期(避免 Hydration 错误)
|
|
||||||
const formatDate = (timestamp: number) => {
|
|
||||||
const d = new Date(timestamp * 1000);
|
|
||||||
const year = d.getFullYear();
|
|
||||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(d.getDate()).padStart(2, '0');
|
|
||||||
const hour = String(d.getHours()).padStart(2, '0');
|
|
||||||
const minute = String(d.getMinutes()).padStart(2, '0');
|
|
||||||
return `${year}/${month}/${day} ${hour}:${minute}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
platform: string;
|
platform: string;
|
||||||
@@ -38,9 +39,11 @@ interface Video {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 [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>("");
|
||||||
@@ -57,11 +60,21 @@ export default function PublishPage() {
|
|||||||
// 是否已从 localStorage 恢复完成
|
// 是否已从 localStorage 恢复完成
|
||||||
const [isRestored, setIsRestored] = useState(false);
|
const [isRestored, setIsRestored] = useState(false);
|
||||||
|
|
||||||
// 加载账号和视频列表
|
// 加载账号和视频列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAccounts();
|
void Promise.allSettled([
|
||||||
fetchVideos();
|
fetchAccounts(),
|
||||||
}, []);
|
fetchVideos(),
|
||||||
|
]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
if ('scrollRestoration' in window.history) {
|
||||||
|
window.history.scrollRestoration = 'manual';
|
||||||
|
}
|
||||||
|
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 获取存储 key 的前缀(登录用户使用 userId,未登录使用 guest)
|
// 获取存储 key 的前缀(登录用户使用 userId,未登录使用 guest)
|
||||||
const storageKey = userId || 'guest';
|
const storageKey = userId || 'guest';
|
||||||
@@ -99,13 +112,21 @@ export default function PublishPage() {
|
|||||||
}, [storageKey, isAuthLoading]);
|
}, [storageKey, isAuthLoading]);
|
||||||
|
|
||||||
// 保存用户输入到 localStorage(恢复完成后才保存,未登录用户也可保存)
|
// 保存用户输入到 localStorage(恢复完成后才保存,未登录用户也可保存)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRestored) localStorage.setItem(`vigent_${storageKey}_publish_title`, title);
|
if (!isRestored) return;
|
||||||
}, [title, storageKey, isRestored]);
|
const timeout = setTimeout(() => {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_publish_title`, title);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [title, storageKey, isRestored]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRestored) localStorage.setItem(`vigent_${storageKey}_publish_tags`, tags);
|
if (!isRestored) return;
|
||||||
}, [tags, storageKey, isRestored]);
|
const timeout = setTimeout(() => {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_publish_tags`, tags);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [tags, storageKey, isRestored]);
|
||||||
|
|
||||||
const fetchAccounts = async () => {
|
const fetchAccounts = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -253,17 +274,28 @@ export default function PublishPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const platformIcons: Record<string, string> = {
|
const platformIcons: Record<string, string> = {
|
||||||
douyin: "🎵",
|
douyin: "🎵",
|
||||||
xiaohongshu: "📕",
|
xiaohongshu: "📕",
|
||||||
weixin: "💬",
|
weixin: "💬",
|
||||||
kuaishou: "⚡",
|
kuaishou: "⚡",
|
||||||
bilibili: "📺",
|
bilibili: "📺",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredVideos = useMemo(() => {
|
||||||
|
const query = videoFilter.trim().toLowerCase();
|
||||||
|
if (!query) return videos;
|
||||||
|
return videos.filter((v) => v.name.toLowerCase().includes(query));
|
||||||
|
}, [videos, videoFilter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh">
|
<div className="min-h-dvh">
|
||||||
{/* QR码弹窗 */}
|
<VideoPreviewModal
|
||||||
|
onClose={() => setPreviewVideoUrl(null)}
|
||||||
|
videoUrl={previewVideoUrl}
|
||||||
|
title="发布视频预览"
|
||||||
|
/>
|
||||||
|
{/* QR码弹窗 */}
|
||||||
{qrPlatform && (
|
{qrPlatform && (
|
||||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-2xl p-8 max-w-md min-w-[320px]">
|
<div className="bg-white rounded-2xl p-8 max-w-md min-w-[320px]">
|
||||||
@@ -296,38 +328,27 @@ export default function PublishPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Header - 统一样式 */}
|
{/* Header - 统一样式 */}
|
||||||
<header className="border-b border-white/10 bg-black/20 backdrop-blur-sm">
|
<header className="border-b border-white/10 bg-black/20 backdrop-blur-sm relative z-[100]">
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center justify-between">
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center justify-between">
|
||||||
<Link href="/" className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2 sm:gap-3 hover:opacity-80 transition-opacity">
|
<Link href="/" className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2 sm:gap-3 hover:opacity-80 transition-opacity">
|
||||||
<span className="text-3xl sm:text-4xl">🎬</span>
|
<span className="text-3xl sm:text-4xl">🎬</span>
|
||||||
IPAgent
|
IPAgent
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex items-center gap-1 sm:gap-4">
|
<div className="flex items-center gap-1 sm:gap-4">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
|
className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors flex items-center gap-1"
|
||||||
>
|
>
|
||||||
返回创作
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Link>
|
返回创作
|
||||||
<span className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-gradient-to-r from-purple-600 to-pink-600 text-white rounded-lg font-semibold">
|
</Link>
|
||||||
发布管理
|
<span className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-gradient-to-r from-purple-600 to-pink-600 text-white rounded-lg font-semibold">
|
||||||
</span>
|
发布管理
|
||||||
<button
|
</span>
|
||||||
onClick={async () => {
|
<AccountSettingsDropdown />
|
||||||
if (confirm('确定要退出登录吗?')) {
|
</div>
|
||||||
try {
|
</div>
|
||||||
await api.post('/api/auth/logout');
|
</header>
|
||||||
} catch (e) { }
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-red-500/10 hover:bg-red-500/20 text-red-200 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
退出
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="max-w-6xl mx-auto px-6 py-8">
|
<main className="max-w-6xl mx-auto px-6 py-8">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
@@ -365,26 +386,29 @@ export default function PublishPage() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{account.logged_in ? (
|
{account.logged_in ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleLogin(account.platform)}
|
onClick={() => handleLogin(account.platform)}
|
||||||
className="px-3 py-1 bg-white/10 hover:bg-white/20 text-white text-sm rounded-lg transition-colors"
|
className="px-3 py-1 bg-white/10 hover:bg-white/20 text-white text-sm rounded-lg transition-colors flex items-center gap-1"
|
||||||
>
|
>
|
||||||
↻ 重新登录
|
<RotateCcw className="h-3.5 w-3.5" />
|
||||||
</button>
|
重新登录
|
||||||
<button
|
</button>
|
||||||
onClick={() => handleLogout(account.platform)}
|
<button
|
||||||
className="px-3 py-1 bg-red-500/80 hover:bg-red-600 text-white text-sm rounded-lg transition-colors"
|
onClick={() => handleLogout(account.platform)}
|
||||||
>
|
className="px-3 py-1 bg-red-500/80 hover:bg-red-600 text-white text-sm rounded-lg transition-colors flex items-center gap-1"
|
||||||
注销
|
>
|
||||||
</button>
|
<LogOut className="h-3.5 w-3.5" />
|
||||||
|
注销
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleLogin(account.platform)}
|
onClick={() => handleLogin(account.platform)}
|
||||||
className="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-colors"
|
className="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg transition-colors flex items-center gap-1"
|
||||||
>
|
>
|
||||||
🔐 扫码登录
|
<QrCode className="h-3.5 w-3.5" />
|
||||||
</button>
|
扫码登录
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -395,33 +419,92 @@ export default function PublishPage() {
|
|||||||
|
|
||||||
{/* 右侧: 发布表单 */}
|
{/* 右侧: 发布表单 */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 选择视频 */}
|
{/* 选择视频 */}
|
||||||
<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">
|
<h2 className="text-lg font-semibold text-white mb-4">
|
||||||
🎥 选择要发布的视频
|
🎥 选择要发布的作品
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{videos.length === 0 ? (
|
{videos.length === 0 ? (
|
||||||
<p className="text-gray-400">
|
<p className="text-gray-400">
|
||||||
暂无已生成的视频,请先
|
暂无已生成的视频,请先
|
||||||
<Link href="/" className="text-purple-400 hover:underline">
|
<Link href="/" className="text-purple-400 hover:underline">
|
||||||
生成视频
|
生成视频
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<select
|
<>
|
||||||
value={selectedVideo}
|
<div className="flex items-center gap-2 mb-3">
|
||||||
onChange={(e) => setSelectedVideo(e.target.value)}
|
<div className="relative flex-1">
|
||||||
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white custom-select cursor-pointer hover:border-purple-500/50 transition-colors"
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500" />
|
||||||
>
|
<input
|
||||||
{videos.map((v) => (
|
value={videoFilter}
|
||||||
<option key={v.path} value={v.path}>
|
onChange={(e) => setVideoFilter(e.target.value)}
|
||||||
{v.name}
|
placeholder="搜索视频..."
|
||||||
</option>
|
className="w-full pl-9 pr-3 py-2 bg-black/30 border border-white/10 rounded-lg text-white text-sm placeholder-gray-500 focus:outline-none focus:border-purple-500 transition-colors"
|
||||||
))}
|
/>
|
||||||
</select>
|
</div>
|
||||||
)}
|
<button
|
||||||
</div>
|
onClick={fetchVideos}
|
||||||
|
className="px-2 py-2 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredVideos.length === 0 ? (
|
||||||
|
<div className="text-center py-4 text-gray-500 text-sm">
|
||||||
|
没有匹配的视频
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="space-y-2 max-h-64 overflow-y-auto hide-scrollbar"
|
||||||
|
style={{ contentVisibility: 'auto' }}
|
||||||
|
>
|
||||||
|
{filteredVideos.map((v) => (
|
||||||
|
<div
|
||||||
|
key={v.path}
|
||||||
|
className={`p-3 rounded-lg border transition-all flex items-center justify-between group ${selectedVideo === v.path
|
||||||
|
? "border-purple-500 bg-purple-500/20"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedVideo(v.path)}
|
||||||
|
className="flex-1 text-left"
|
||||||
|
>
|
||||||
|
<div className="text-white text-sm truncate">
|
||||||
|
{v.name}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-2 pl-2">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const previewPath = isAbsoluteUrl(v.path)
|
||||||
|
? v.path
|
||||||
|
: v.path.startsWith('/')
|
||||||
|
? v.path
|
||||||
|
: `/${v.path}`;
|
||||||
|
setPreviewVideoUrl(resolveMediaUrl(previewPath) || previewPath);
|
||||||
|
}}
|
||||||
|
className="p-1 text-gray-500 hover:text-purple-400 transition-colors"
|
||||||
|
title="预览"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
{selectedVideo === v.path && (
|
||||||
|
<span className="text-xs text-purple-300">已选</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 填写信息 */}
|
{/* 填写信息 */}
|
||||||
<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">
|
||||||
@@ -490,32 +573,40 @@ export default function PublishPage() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{/* 立即发布 - 占 3/4 */}
|
{/* 立即发布 - 占 3/4 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setScheduleMode("now");
|
setScheduleMode("now");
|
||||||
handlePublish();
|
handlePublish();
|
||||||
}}
|
}}
|
||||||
disabled={isPublishing || selectedPlatforms.length === 0}
|
disabled={isPublishing || selectedPlatforms.length === 0}
|
||||||
className={`flex-[3] py-4 rounded-xl font-bold text-lg transition-all ${isPublishing || selectedPlatforms.length === 0
|
className={`flex-[3] py-4 rounded-xl font-bold text-lg transition-all flex items-center justify-center gap-2 ${isPublishing || selectedPlatforms.length === 0
|
||||||
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
||||||
: "bg-gradient-to-r from-green-600 to-teal-600 hover:from-green-700 hover:to-teal-700 text-white"
|
: "bg-gradient-to-r from-green-600 to-teal-600 hover:from-green-700 hover:to-teal-700 text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isPublishing && scheduleMode === "now" ? "发布中..." : "🚀 立即发布"}
|
{isPublishing && scheduleMode === "now" ? (
|
||||||
</button>
|
"发布中..."
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Rocket className="h-5 w-5" />
|
||||||
|
立即发布
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
{/* 定时发布 - 占 1/4 */}
|
{/* 定时发布 - 占 1/4 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setScheduleMode(scheduleMode === "scheduled" ? "now" : "scheduled")}
|
onClick={() => setScheduleMode(scheduleMode === "scheduled" ? "now" : "scheduled")}
|
||||||
disabled={isPublishing || selectedPlatforms.length === 0}
|
disabled={isPublishing || selectedPlatforms.length === 0}
|
||||||
className={`flex-1 py-4 rounded-xl font-bold text-base transition-all ${isPublishing || selectedPlatforms.length === 0
|
className={`flex-1 py-4 rounded-xl font-bold text-base transition-all flex items-center justify-center gap-2 ${isPublishing || selectedPlatforms.length === 0
|
||||||
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
||||||
: scheduleMode === "scheduled"
|
: scheduleMode === "scheduled"
|
||||||
? "bg-purple-600 text-white"
|
? "bg-purple-600 text-white"
|
||||||
: "bg-white/10 hover:bg-white/20 text-white"
|
: "bg-white/10 hover:bg-white/20 text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
⏰ 定时
|
<Clock className="h-5 w-5" />
|
||||||
</button>
|
定时
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 定时发布时间选择器 */}
|
{/* 定时发布时间选择器 */}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import api from "@/lib/axios";
|
import api from "@/lib/axios";
|
||||||
|
|
||||||
interface ScriptExtractionModalProps {
|
interface ScriptExtractionModalProps {
|
||||||
|
|||||||
@@ -1,50 +1,71 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { X, Video } from "lucide-react";
|
||||||
|
|
||||||
interface VideoPreviewModalProps {
|
interface VideoPreviewModalProps {
|
||||||
videoUrl: string | null;
|
videoUrl: string | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VideoPreviewModal({ videoUrl, onClose }: VideoPreviewModalProps) {
|
export default function VideoPreviewModal({
|
||||||
useEffect(() => {
|
videoUrl,
|
||||||
// 按 ESC 关闭
|
onClose,
|
||||||
const handleEsc = (e: KeyboardEvent) => {
|
title = "视频预览",
|
||||||
if (e.key === 'Escape') onClose();
|
subtitle = "ESC 关闭 · 点击空白关闭",
|
||||||
};
|
}: VideoPreviewModalProps) {
|
||||||
if (videoUrl) {
|
useEffect(() => {
|
||||||
document.addEventListener('keydown', handleEsc);
|
if (!videoUrl) return;
|
||||||
// 禁止背景滚动
|
// 按 ESC 关闭
|
||||||
document.body.style.overflow = 'hidden';
|
const handleEsc = (e: KeyboardEvent) => {
|
||||||
}
|
if (e.key === 'Escape') onClose();
|
||||||
return () => {
|
};
|
||||||
document.removeEventListener('keydown', handleEsc);
|
const prevOverflow = document.body.style.overflow;
|
||||||
document.body.style.overflow = 'unset';
|
document.addEventListener('keydown', handleEsc);
|
||||||
};
|
// 禁止背景滚动
|
||||||
}, [videoUrl, onClose]);
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleEsc);
|
||||||
|
document.body.style.overflow = prevOverflow;
|
||||||
|
};
|
||||||
|
}, [videoUrl, onClose]);
|
||||||
|
|
||||||
if (!videoUrl) return null;
|
if (!videoUrl) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
<div
|
||||||
<div className="relative w-full max-w-4xl bg-gray-900 border border-white/10 rounded-2xl shadow-2xl overflow-hidden flex flex-col">
|
className="fixed inset-0 z-[200] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 animate-in fade-in duration-200"
|
||||||
{/* Header */}
|
onClick={onClose}
|
||||||
<div className="flex items-center justify-between px-6 py-2 border-b border-white/10 bg-white/5">
|
>
|
||||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
<div
|
||||||
🎥 视频预览
|
className="relative w-full max-w-4xl bg-gray-900 border border-white/10 rounded-2xl shadow-2xl overflow-hidden flex flex-col"
|
||||||
</h3>
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-6 py-3 border-b border-white/10 bg-gradient-to-r from-white/5 via-white/0 to-white/5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="h-9 w-9 rounded-lg bg-white/10 flex items-center justify-center text-white">
|
||||||
|
<Video className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-2 text-gray-400 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
className="p-2 text-gray-400 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<X className="h-5 w-5" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Video Player */}
|
|
||||||
<div className="bg-black flex items-center justify-center min-h-[50vh] max-h-[80vh]">
|
<div className="bg-black flex items-center justify-center min-h-[50vh] max-h-[80vh]">
|
||||||
<video
|
<video
|
||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
@@ -53,12 +74,7 @@ export default function VideoPreviewModal({ videoUrl, onClose }: VideoPreviewMod
|
|||||||
className="w-full h-full max-h-[80vh] object-contain"
|
className="w-full h-full max-h-[80vh] object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Click outside to close */}
|
|
||||||
<div className="absolute inset-0 -z-10" onClick={onClose}></div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
137
frontend/src/components/home/BgmPanel.tsx
Normal file
137
frontend/src/components/home/BgmPanel.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import type { RefObject, MouseEvent } from "react";
|
||||||
|
import { RefreshCw, Play, Pause } from "lucide-react";
|
||||||
|
|
||||||
|
interface BgmItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ext?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BgmPanelProps {
|
||||||
|
bgmList: BgmItem[];
|
||||||
|
bgmLoading: boolean;
|
||||||
|
bgmError: string;
|
||||||
|
enableBgm: boolean;
|
||||||
|
onToggleEnable: (value: boolean) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
selectedBgmId: string;
|
||||||
|
onSelectBgm: (id: string) => void;
|
||||||
|
playingBgmId: string | null;
|
||||||
|
onTogglePreview: (bgm: BgmItem, event: MouseEvent) => void;
|
||||||
|
bgmVolume: number;
|
||||||
|
onVolumeChange: (value: number) => void;
|
||||||
|
bgmListContainerRef: RefObject<HTMLDivElement | null>;
|
||||||
|
registerBgmItemRef: (id: string, element: HTMLDivElement | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BgmPanel({
|
||||||
|
bgmList,
|
||||||
|
bgmLoading,
|
||||||
|
bgmError,
|
||||||
|
enableBgm,
|
||||||
|
onToggleEnable,
|
||||||
|
onRefresh,
|
||||||
|
selectedBgmId,
|
||||||
|
onSelectBgm,
|
||||||
|
playingBgmId,
|
||||||
|
onTogglePreview,
|
||||||
|
bgmVolume,
|
||||||
|
onVolumeChange,
|
||||||
|
bgmListContainerRef,
|
||||||
|
registerBgmItemRef,
|
||||||
|
}: BgmPanelProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2">🎵 背景音乐</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={onRefresh}
|
||||||
|
className="px-2 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enableBgm}
|
||||||
|
onChange={(e) => onToggleEnable(e.target.checked)}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{bgmLoading ? (
|
||||||
|
<div className="text-center py-4 text-gray-400 text-sm">正在加载背景音乐...</div>
|
||||||
|
) : bgmError ? (
|
||||||
|
<div className="text-center py-4 text-red-300 text-sm">
|
||||||
|
加载失败:{bgmError}
|
||||||
|
<button
|
||||||
|
onClick={onRefresh}
|
||||||
|
className="ml-2 px-2 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : bgmList.length === 0 ? (
|
||||||
|
<div className="text-center py-4 text-gray-500 text-sm">暂无背景音乐,请先导入素材</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
ref={bgmListContainerRef}
|
||||||
|
className={`space-y-2 max-h-64 overflow-y-auto hide-scrollbar ${enableBgm ? '' : 'opacity-70'}`}
|
||||||
|
>
|
||||||
|
{bgmList.map((bgm) => (
|
||||||
|
<div
|
||||||
|
key={bgm.id}
|
||||||
|
ref={(el) => registerBgmItemRef(bgm.id, el)}
|
||||||
|
className={`p-3 rounded-lg border transition-all flex items-center justify-between group ${selectedBgmId === bgm.id
|
||||||
|
? "border-purple-500 bg-purple-500/20"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button onClick={() => onSelectBgm(bgm.id)} className="flex-1 text-left">
|
||||||
|
<div className="text-white text-sm truncate">{bgm.name}</div>
|
||||||
|
<div className="text-xs text-gray-400">.{bgm.ext || 'audio'}</div>
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-2 pl-2">
|
||||||
|
<button
|
||||||
|
onClick={(e) => onTogglePreview(bgm, e)}
|
||||||
|
className="p-1 text-gray-500 hover:text-purple-400 transition-colors"
|
||||||
|
title="试听"
|
||||||
|
>
|
||||||
|
{playingBgmId === bgm.id ? (
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{selectedBgmId === bgm.id && (
|
||||||
|
<span className="text-xs text-purple-300">已选</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableBgm && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="text-sm text-gray-300 mb-2 block">音量</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.05"
|
||||||
|
value={bgmVolume}
|
||||||
|
onChange={(e) => onVolumeChange(parseFloat(e.target.value))}
|
||||||
|
className="w-full accent-purple-500"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-gray-400 mt-1">当前: {Math.round(bgmVolume * 100)}%</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
frontend/src/components/home/GenerateActionBar.tsx
Normal file
53
frontend/src/components/home/GenerateActionBar.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Rocket } from "lucide-react";
|
||||||
|
|
||||||
|
interface GenerateActionBarProps {
|
||||||
|
isGenerating: boolean;
|
||||||
|
progress: number;
|
||||||
|
disabled: boolean;
|
||||||
|
onGenerate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenerateActionBar({
|
||||||
|
isGenerating,
|
||||||
|
progress,
|
||||||
|
disabled,
|
||||||
|
onGenerate,
|
||||||
|
}: GenerateActionBarProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onGenerate}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`w-full py-4 rounded-xl font-bold text-lg transition-all ${disabled
|
||||||
|
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
||||||
|
: "bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white shadow-lg hover:shadow-purple-500/25"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isGenerating ? (
|
||||||
|
<span className="flex items-center justify-center gap-3">
|
||||||
|
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
生成中... {progress}%
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<Rocket className="h-5 w-5" />
|
||||||
|
生成视频
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
frontend/src/components/home/HistoryList.tsx
Normal file
80
frontend/src/components/home/HistoryList.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { RefreshCw, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface GeneratedVideo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size_mb: number;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryListProps {
|
||||||
|
generatedVideos: GeneratedVideo[];
|
||||||
|
selectedVideoId: string | null;
|
||||||
|
onSelectVideo: (video: GeneratedVideo) => void;
|
||||||
|
onDeleteVideo: (id: string) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
registerVideoRef: (id: string, element: HTMLDivElement | null) => void;
|
||||||
|
formatDate: (timestamp: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HistoryList({
|
||||||
|
generatedVideos,
|
||||||
|
selectedVideoId,
|
||||||
|
onSelectVideo,
|
||||||
|
onDeleteVideo,
|
||||||
|
onRefresh,
|
||||||
|
registerVideoRef,
|
||||||
|
formatDate,
|
||||||
|
}: HistoryListProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2">📂 历史作品</h2>
|
||||||
|
<button
|
||||||
|
onClick={onRefresh}
|
||||||
|
className="px-3 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{generatedVideos.length === 0 ? (
|
||||||
|
<div className="text-center py-4 text-gray-500">
|
||||||
|
<p>暂无生成的作品</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="space-y-2 max-h-64 overflow-y-auto hide-scrollbar"
|
||||||
|
style={{ contentVisibility: 'auto' }}
|
||||||
|
>
|
||||||
|
{generatedVideos.map((v) => (
|
||||||
|
<div
|
||||||
|
key={v.id}
|
||||||
|
ref={(el) => registerVideoRef(v.id, el)}
|
||||||
|
className={`p-3 rounded-lg border transition-all flex items-center justify-between group ${selectedVideoId === v.id
|
||||||
|
? "border-purple-500 bg-purple-500/20"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button onClick={() => onSelectVideo(v)} className="flex-1 text-left">
|
||||||
|
<div className="text-white text-sm truncate">{formatDate(v.created_at)}</div>
|
||||||
|
<div className="text-gray-400 text-xs">{v.size_mb.toFixed(1)} MB</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteVideo(v.id);
|
||||||
|
}}
|
||||||
|
className="p-1 text-gray-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="删除视频"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
frontend/src/components/home/HomeHeader.tsx
Normal file
30
frontend/src/components/home/HomeHeader.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import AccountSettingsDropdown from "@/components/AccountSettingsDropdown";
|
||||||
|
|
||||||
|
export function HomeHeader() {
|
||||||
|
return (
|
||||||
|
<header className="border-b border-white/10 bg-black/20 backdrop-blur-sm relative z-[100]">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center justify-between">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-xl sm:text-2xl font-bold text-white flex items-center gap-2 sm:gap-3 hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<span className="text-3xl sm:text-4xl">🎬</span>
|
||||||
|
IPAgent
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-1 sm:gap-4">
|
||||||
|
<span className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-gradient-to-r from-purple-600 to-pink-600 text-white rounded-lg font-semibold">
|
||||||
|
视频生成
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
href="/publish"
|
||||||
|
className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
发布管理
|
||||||
|
</Link>
|
||||||
|
<AccountSettingsDropdown />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
168
frontend/src/components/home/MaterialSelector.tsx
Normal file
168
frontend/src/components/home/MaterialSelector.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import type { ChangeEvent } from "react";
|
||||||
|
import { Upload, RefreshCw, Eye, Trash2, X } from "lucide-react";
|
||||||
|
|
||||||
|
interface Material {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
scene: string;
|
||||||
|
size_mb: number;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MaterialSelectorProps {
|
||||||
|
materials: Material[];
|
||||||
|
selectedMaterial: string;
|
||||||
|
isUploading: boolean;
|
||||||
|
uploadProgress: number;
|
||||||
|
uploadError: string | null;
|
||||||
|
fetchError: string | null;
|
||||||
|
apiBase: string;
|
||||||
|
onUploadChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
onSelectMaterial: (id: string) => void;
|
||||||
|
onPreviewMaterial: (path: string) => void;
|
||||||
|
onDeleteMaterial: (id: string) => void;
|
||||||
|
onClearUploadError: () => void;
|
||||||
|
registerMaterialRef: (id: string, element: HTMLDivElement | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MaterialSelector({
|
||||||
|
materials,
|
||||||
|
selectedMaterial,
|
||||||
|
isUploading,
|
||||||
|
uploadProgress,
|
||||||
|
uploadError,
|
||||||
|
fetchError,
|
||||||
|
apiBase,
|
||||||
|
onUploadChange,
|
||||||
|
onRefresh,
|
||||||
|
onSelectMaterial,
|
||||||
|
onPreviewMaterial,
|
||||||
|
onDeleteMaterial,
|
||||||
|
onClearUploadError,
|
||||||
|
registerMaterialRef,
|
||||||
|
}: MaterialSelectorProps) {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</h2>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="video-upload"
|
||||||
|
accept=".mp4,.mov,.avi"
|
||||||
|
onChange={onUploadChange}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="video-upload"
|
||||||
|
className={`px-2 py-1 text-xs rounded cursor-pointer transition-all whitespace-nowrap flex items-center gap-1 ${isUploading
|
||||||
|
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
||||||
|
: "bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Upload className="h-3.5 w-3.5" />
|
||||||
|
上传
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={onRefresh}
|
||||||
|
className="px-2 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300 whitespace-nowrap flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isUploading && (
|
||||||
|
<div className="mb-4 p-4 bg-purple-500/10 rounded-xl border border-purple-500/30">
|
||||||
|
<div className="flex justify-between text-sm text-purple-300 mb-2">
|
||||||
|
<span>📤 上传中...</span>
|
||||||
|
<span>{uploadProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-black/30 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-purple-500 to-pink-500 transition-all duration-300"
|
||||||
|
style={{ width: `${uploadProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{uploadError && (
|
||||||
|
<div className="mb-4 p-4 bg-red-500/20 text-red-200 rounded-xl text-sm flex justify-between items-center">
|
||||||
|
<span>❌ {uploadError}</span>
|
||||||
|
<button onClick={onClearUploadError} className="text-red-300 hover:text-white">
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fetchError ? (
|
||||||
|
<div className="p-4 bg-red-500/20 text-red-200 rounded-xl text-sm mb-4">
|
||||||
|
获取素材失败: {fetchError}
|
||||||
|
<br />
|
||||||
|
API: {apiBase}/api/materials/
|
||||||
|
</div>
|
||||||
|
) : materials.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
<div className="text-5xl mb-4">📁</div>
|
||||||
|
<p>暂无素材视频</p>
|
||||||
|
<p className="text-sm mt-2">
|
||||||
|
点击上方「📤 上传视频」按钮添加素材
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="space-y-2 max-h-64 overflow-y-auto hide-scrollbar"
|
||||||
|
style={{ contentVisibility: 'auto' }}
|
||||||
|
>
|
||||||
|
{materials.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
ref={(el) => registerMaterialRef(m.id, el)}
|
||||||
|
className={`p-3 rounded-lg border transition-all flex items-center justify-between group ${selectedMaterial === m.id
|
||||||
|
? "border-purple-500 bg-purple-500/20"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button onClick={() => onSelectMaterial(m.id)} className="flex-1 text-left">
|
||||||
|
<div className="text-white text-sm truncate">{m.scene || m.name}</div>
|
||||||
|
<div className="text-gray-400 text-xs">{m.size_mb.toFixed(1)} MB</div>
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-2 pl-2">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (m.path) {
|
||||||
|
onPreviewMaterial(m.path);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-1 text-gray-500 hover:text-white opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="预览视频"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteMaterial(m.id);
|
||||||
|
}}
|
||||||
|
className="p-1 text-gray-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="删除素材"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
frontend/src/components/home/PreviewPanel.tsx
Normal file
74
frontend/src/components/home/PreviewPanel.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Download, Send } from "lucide-react";
|
||||||
|
|
||||||
|
interface Task {
|
||||||
|
task_id: string;
|
||||||
|
status: string;
|
||||||
|
progress: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PreviewPanelProps {
|
||||||
|
currentTask: Task | null;
|
||||||
|
isGenerating: boolean;
|
||||||
|
generatedVideo: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PreviewPanel({
|
||||||
|
currentTask,
|
||||||
|
isGenerating,
|
||||||
|
generatedVideo,
|
||||||
|
}: PreviewPanelProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{currentTask && isGenerating && (
|
||||||
|
<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">⏳ 生成进度</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="h-3 bg-black/30 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-purple-500 to-pink-500 transition-all duration-300"
|
||||||
|
style={{ width: `${currentTask.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-300">正在AI生成中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<div className="aspect-video bg-black/50 rounded-xl overflow-hidden flex items-center justify-center">
|
||||||
|
{generatedVideo ? (
|
||||||
|
<video src={generatedVideo} controls className="w-full h-full object-contain" />
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-500 text-center">
|
||||||
|
<div className="text-5xl mb-4">📹</div>
|
||||||
|
<p>生成的作品将在这里预览</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{generatedVideo && (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href={generatedVideo}
|
||||||
|
download
|
||||||
|
className="mt-4 w-full py-3 rounded-xl bg-green-600 hover:bg-green-700 text-white font-medium flex items-center justify-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
下载视频
|
||||||
|
</a>
|
||||||
|
<Link
|
||||||
|
href="/publish"
|
||||||
|
className="mt-3 w-full py-3 rounded-xl bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-medium flex items-center justify-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<Send className="h-4 w-4" />
|
||||||
|
发布到社交平台
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
277
frontend/src/components/home/RefAudioPanel.tsx
Normal file
277
frontend/src/components/home/RefAudioPanel.tsx
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
|
import { Upload, RefreshCw, Play, Pause, Pencil, Trash2, Check, X, Mic, Square } from "lucide-react";
|
||||||
|
|
||||||
|
interface RefAudio {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
ref_text: string;
|
||||||
|
duration_sec: number;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RefAudioPanelProps {
|
||||||
|
refAudios: RefAudio[];
|
||||||
|
selectedRefAudio: RefAudio | null;
|
||||||
|
onSelectRefAudio: (audio: RefAudio) => void;
|
||||||
|
isUploadingRef: boolean;
|
||||||
|
uploadRefError: string | null;
|
||||||
|
onClearUploadRefError: () => void;
|
||||||
|
onUploadRefAudio: (file: File) => void;
|
||||||
|
onFetchRefAudios: () => void;
|
||||||
|
playingAudioId: string | null;
|
||||||
|
onTogglePlayPreview: (audio: RefAudio, event: MouseEvent) => void;
|
||||||
|
editingAudioId: string | null;
|
||||||
|
editName: string;
|
||||||
|
onEditNameChange: (value: string) => void;
|
||||||
|
onStartEditing: (audio: RefAudio, event: MouseEvent) => void;
|
||||||
|
onSaveEditing: (id: string, event: MouseEvent) => void;
|
||||||
|
onCancelEditing: (event: MouseEvent) => void;
|
||||||
|
onDeleteRefAudio: (id: string) => void;
|
||||||
|
recordedBlob: Blob | null;
|
||||||
|
isRecording: boolean;
|
||||||
|
recordingTime: number;
|
||||||
|
onStartRecording: () => void;
|
||||||
|
onStopRecording: () => void;
|
||||||
|
onUseRecording: () => void;
|
||||||
|
formatRecordingTime: (seconds: number) => string;
|
||||||
|
fixedRefText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RefAudioPanel({
|
||||||
|
refAudios,
|
||||||
|
selectedRefAudio,
|
||||||
|
onSelectRefAudio,
|
||||||
|
isUploadingRef,
|
||||||
|
uploadRefError,
|
||||||
|
onClearUploadRefError,
|
||||||
|
onUploadRefAudio,
|
||||||
|
onFetchRefAudios,
|
||||||
|
playingAudioId,
|
||||||
|
onTogglePlayPreview,
|
||||||
|
editingAudioId,
|
||||||
|
editName,
|
||||||
|
onEditNameChange,
|
||||||
|
onStartEditing,
|
||||||
|
onSaveEditing,
|
||||||
|
onCancelEditing,
|
||||||
|
onDeleteRefAudio,
|
||||||
|
recordedBlob,
|
||||||
|
isRecording,
|
||||||
|
recordingTime,
|
||||||
|
onStartRecording,
|
||||||
|
onStopRecording,
|
||||||
|
onUseRecording,
|
||||||
|
formatRecordingTime,
|
||||||
|
fixedRefText,
|
||||||
|
}: RefAudioPanelProps) {
|
||||||
|
const [recordedUrl, setRecordedUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!recordedBlob) {
|
||||||
|
setRecordedUrl(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = URL.createObjectURL(recordedBlob);
|
||||||
|
setRecordedUrl(url);
|
||||||
|
return () => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
}, [recordedBlob]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm text-gray-300">📁 我的参考音频</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="ref-audio-upload"
|
||||||
|
accept=".wav,.mp3,.m4a,.webm,.ogg,.flac,.aac"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
onUploadRefAudio(file);
|
||||||
|
}
|
||||||
|
e.target.value = '';
|
||||||
|
}}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="ref-audio-upload"
|
||||||
|
className={`px-2 py-1 text-xs rounded cursor-pointer transition-all flex items-center gap-1 ${isUploadingRef
|
||||||
|
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
||||||
|
: "bg-purple-600 hover:bg-purple-700 text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Upload className="h-3.5 w-3.5" />
|
||||||
|
上传
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={onFetchRefAudios}
|
||||||
|
className="px-2 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isUploadingRef && (
|
||||||
|
<div className="mb-2 p-2 bg-purple-500/10 rounded text-sm text-purple-300">
|
||||||
|
⏳ 上传中...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{uploadRefError && (
|
||||||
|
<div className="mb-2 p-2 bg-red-500/20 text-red-200 rounded text-xs flex justify-between">
|
||||||
|
<span>❌ {uploadRefError}</span>
|
||||||
|
<button onClick={onClearUploadRefError} className="text-red-300 hover:text-white">
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{refAudios.length === 0 ? (
|
||||||
|
<div className="text-center py-4 text-gray-500 text-sm">
|
||||||
|
暂无参考音频,请上传或录制
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 gap-2" style={{ contentVisibility: 'auto' }}>
|
||||||
|
{refAudios.map((audio) => (
|
||||||
|
<div
|
||||||
|
key={audio.id}
|
||||||
|
className={`p-2 rounded-lg border transition-all relative group cursor-pointer ${selectedRefAudio?.id === audio.id
|
||||||
|
? "border-purple-500 bg-purple-500/20"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (editingAudioId !== audio.id) {
|
||||||
|
onSelectRefAudio(audio);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{editingAudioId === audio.id ? (
|
||||||
|
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editName}
|
||||||
|
onChange={(e) => onEditNameChange(e.target.value)}
|
||||||
|
className="w-full bg-black/50 text-white text-xs px-1 py-0.5 rounded border border-purple-500 focus:outline-none"
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') onSaveEditing(audio.id, e as any);
|
||||||
|
if (e.key === 'Escape') onCancelEditing(e as any);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button onClick={(e) => onSaveEditing(audio.id, e)} className="text-green-400 hover:text-green-300 text-xs">
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
<button onClick={(e) => onCancelEditing(e)} className="text-gray-400 hover:text-gray-300 text-xs">
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between items-start mb-1">
|
||||||
|
<div className="text-white text-xs truncate pr-1 flex-1" title={audio.name}>
|
||||||
|
{audio.name}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
onClick={(e) => onTogglePlayPreview(audio, e)}
|
||||||
|
className="text-gray-400 hover:text-purple-400 text-xs"
|
||||||
|
title="试听"
|
||||||
|
>
|
||||||
|
{playingAudioId === audio.id ? (
|
||||||
|
<Pause className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<Play className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => onStartEditing(audio, e)}
|
||||||
|
className="text-gray-400 hover:text-blue-400 text-xs"
|
||||||
|
title="重命名"
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteRefAudio(audio.id);
|
||||||
|
}}
|
||||||
|
className="text-gray-400 hover:text-red-400 text-xs"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-xs">{audio.duration_sec.toFixed(1)}s</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-white/10 pt-4">
|
||||||
|
<span className="text-sm text-gray-300 mb-2 block">🎤 或在线录音</span>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{!isRecording ? (
|
||||||
|
<button
|
||||||
|
onClick={onStartRecording}
|
||||||
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Mic className="h-4 w-4" />
|
||||||
|
开始录音
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={onStopRecording}
|
||||||
|
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
停止
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isRecording && (
|
||||||
|
<span className="text-red-400 text-sm animate-pulse">
|
||||||
|
🔴 录音中 {formatRecordingTime(recordingTime)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{recordedBlob && !isRecording && (
|
||||||
|
<div className="mt-3 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="text-green-300 text-sm">✅ 录音完成 ({formatRecordingTime(recordingTime)})</span>
|
||||||
|
<audio src={recordedUrl || ''} controls className="h-8" />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onUseRecording}
|
||||||
|
disabled={isUploadingRef}
|
||||||
|
className="px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded text-sm disabled:bg-gray-600"
|
||||||
|
>
|
||||||
|
使用此录音
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-white/10 pt-4">
|
||||||
|
<label className="text-sm text-gray-300 mb-2 block">📝 录音/上传时请朗读以下内容:</label>
|
||||||
|
<div className="w-full bg-black/30 border border-white/10 rounded-lg p-3 text-white text-sm">
|
||||||
|
{fixedRefText}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
请清晰朗读上述内容完成录音,系统将以此为参考克隆您的声音
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
frontend/src/components/home/ScriptEditor.tsx
Normal file
66
frontend/src/components/home/ScriptEditor.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { FileText, Loader2, Sparkles } from "lucide-react";
|
||||||
|
|
||||||
|
interface ScriptEditorProps {
|
||||||
|
text: string;
|
||||||
|
onChangeText: (value: string) => void;
|
||||||
|
onOpenExtractModal: () => void;
|
||||||
|
onGenerateMeta: () => void;
|
||||||
|
isGeneratingMeta: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScriptEditor({
|
||||||
|
text,
|
||||||
|
onChangeText,
|
||||||
|
onOpenExtractModal,
|
||||||
|
onGenerateMeta,
|
||||||
|
isGeneratingMeta,
|
||||||
|
}: ScriptEditorProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
|
||||||
|
<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>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={onOpenExtractModal}
|
||||||
|
className="px-2 py-1 text-xs rounded transition-all whitespace-nowrap bg-purple-600 hover:bg-purple-700 text-white flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<FileText className="h-3.5 w-3.5" />
|
||||||
|
文案提取助手
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onGenerateMeta}
|
||||||
|
disabled={isGeneratingMeta || !text.trim()}
|
||||||
|
className={`px-2 py-1 text-xs rounded transition-all whitespace-nowrap ${isGeneratingMeta || !text.trim()
|
||||||
|
? "bg-gray-600 cursor-not-allowed text-gray-400"
|
||||||
|
: "bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isGeneratingMeta ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
生成中...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Sparkles className="h-3.5 w-3.5" />
|
||||||
|
AI生成标题标签
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => onChangeText(e.target.value)}
|
||||||
|
placeholder="请输入你想说的话..."
|
||||||
|
className="w-full h-40 bg-black/30 border border-white/10 rounded-xl p-4 text-white placeholder-gray-500 resize-none focus:outline-none focus:border-purple-500 transition-colors hide-scrollbar"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between mt-2 text-sm text-gray-400">
|
||||||
|
<span>{text.length} 字</span>
|
||||||
|
<span>预计时长: ~{Math.ceil(text.length / 4)} 秒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
309
frontend/src/components/home/TitleSubtitlePanel.tsx
Normal file
309
frontend/src/components/home/TitleSubtitlePanel.tsx
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
import type { RefObject } from "react";
|
||||||
|
import { Eye } from "lucide-react";
|
||||||
|
|
||||||
|
interface SubtitleStyleOption {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
font_family?: string;
|
||||||
|
font_file?: string;
|
||||||
|
font_size?: number;
|
||||||
|
highlight_color?: string;
|
||||||
|
normal_color?: string;
|
||||||
|
stroke_color?: string;
|
||||||
|
stroke_size?: number;
|
||||||
|
letter_spacing?: number;
|
||||||
|
bottom_margin?: number;
|
||||||
|
is_default?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TitleStyleOption {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
font_family?: string;
|
||||||
|
font_file?: string;
|
||||||
|
font_size?: number;
|
||||||
|
color?: string;
|
||||||
|
stroke_color?: string;
|
||||||
|
stroke_size?: number;
|
||||||
|
letter_spacing?: number;
|
||||||
|
font_weight?: number;
|
||||||
|
top_margin?: number;
|
||||||
|
is_default?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TitleSubtitlePanelProps {
|
||||||
|
showStylePreview: boolean;
|
||||||
|
onTogglePreview: () => void;
|
||||||
|
videoTitle: string;
|
||||||
|
onTitleChange: (value: string) => void;
|
||||||
|
titleStyles: TitleStyleOption[];
|
||||||
|
selectedTitleStyleId: string;
|
||||||
|
onSelectTitleStyle: (id: string) => void;
|
||||||
|
titleFontSize: number;
|
||||||
|
onTitleFontSizeChange: (value: number) => void;
|
||||||
|
subtitleStyles: SubtitleStyleOption[];
|
||||||
|
selectedSubtitleStyleId: string;
|
||||||
|
onSelectSubtitleStyle: (id: string) => void;
|
||||||
|
subtitleFontSize: number;
|
||||||
|
onSubtitleFontSizeChange: (value: number) => void;
|
||||||
|
enableSubtitles: boolean;
|
||||||
|
onToggleSubtitles: (value: boolean) => void;
|
||||||
|
resolveAssetUrl: (path?: string | null) => string | null;
|
||||||
|
getFontFormat: (fontFile?: string) => string;
|
||||||
|
buildTextShadow: (color: string, size: number) => string;
|
||||||
|
previewScale?: number;
|
||||||
|
previewAspectRatio?: string;
|
||||||
|
previewBaseWidth?: number;
|
||||||
|
previewBaseHeight?: number;
|
||||||
|
previewContainerRef?: RefObject<HTMLDivElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TitleSubtitlePanel({
|
||||||
|
showStylePreview,
|
||||||
|
onTogglePreview,
|
||||||
|
videoTitle,
|
||||||
|
onTitleChange,
|
||||||
|
titleStyles,
|
||||||
|
selectedTitleStyleId,
|
||||||
|
onSelectTitleStyle,
|
||||||
|
titleFontSize,
|
||||||
|
onTitleFontSizeChange,
|
||||||
|
subtitleStyles,
|
||||||
|
selectedSubtitleStyleId,
|
||||||
|
onSelectSubtitleStyle,
|
||||||
|
subtitleFontSize,
|
||||||
|
onSubtitleFontSizeChange,
|
||||||
|
enableSubtitles,
|
||||||
|
onToggleSubtitles,
|
||||||
|
resolveAssetUrl,
|
||||||
|
getFontFormat,
|
||||||
|
buildTextShadow,
|
||||||
|
previewScale = 1,
|
||||||
|
previewAspectRatio = '16 / 9',
|
||||||
|
previewBaseWidth = 1280,
|
||||||
|
previewBaseHeight = 720,
|
||||||
|
previewContainerRef,
|
||||||
|
}: TitleSubtitlePanelProps) {
|
||||||
|
const activeSubtitleStyle = subtitleStyles.find((s) => s.id === selectedSubtitleStyleId)
|
||||||
|
|| subtitleStyles.find((s) => s.is_default)
|
||||||
|
|| subtitleStyles[0];
|
||||||
|
|
||||||
|
const activeTitleStyle = titleStyles.find((s) => s.id === selectedTitleStyleId)
|
||||||
|
|| titleStyles.find((s) => s.is_default)
|
||||||
|
|| titleStyles[0];
|
||||||
|
|
||||||
|
const previewTitleText = videoTitle.trim() || "这里是标题预览";
|
||||||
|
const subtitleHighlightText = "最近,一个叫Cloudbot";
|
||||||
|
const subtitleNormalText = "的开源项目在GitHub上彻底火了";
|
||||||
|
|
||||||
|
const subtitleHighlightColor = activeSubtitleStyle?.highlight_color || "#FFE600";
|
||||||
|
const subtitleNormalColor = activeSubtitleStyle?.normal_color || "#FFFFFF";
|
||||||
|
const subtitleStrokeColor = activeSubtitleStyle?.stroke_color || "#000000";
|
||||||
|
const subtitleStrokeSize = activeSubtitleStyle?.stroke_size ?? 3;
|
||||||
|
const subtitleLetterSpacing = activeSubtitleStyle?.letter_spacing ?? 2;
|
||||||
|
const subtitleBottomMargin = activeSubtitleStyle?.bottom_margin ?? 0;
|
||||||
|
const subtitleFontFamilyName = `SubtitlePreview-${activeSubtitleStyle?.id || "default"}`;
|
||||||
|
const subtitleFontUrl = activeSubtitleStyle?.font_file
|
||||||
|
? resolveAssetUrl(`fonts/${activeSubtitleStyle.font_file}`)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const titleColor = activeTitleStyle?.color || "#FFFFFF";
|
||||||
|
const titleStrokeColor = activeTitleStyle?.stroke_color || "#000000";
|
||||||
|
const titleStrokeSize = activeTitleStyle?.stroke_size ?? 8;
|
||||||
|
const titleLetterSpacing = activeTitleStyle?.letter_spacing ?? 4;
|
||||||
|
const titleTopMargin = activeTitleStyle?.top_margin ?? 0;
|
||||||
|
const titleFontWeight = activeTitleStyle?.font_weight ?? 900;
|
||||||
|
const titleFontFamilyName = `TitlePreview-${activeTitleStyle?.id || "default"}`;
|
||||||
|
const titleFontUrl = activeTitleStyle?.font_file
|
||||||
|
? resolveAssetUrl(`fonts/${activeTitleStyle.font_file}`)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white/5 rounded-2xl p-4 sm:p-6 border border-white/10 backdrop-blur-sm">
|
||||||
|
<div className="flex items-center justify-between mb-4 gap-2">
|
||||||
|
<h2 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2">
|
||||||
|
🎬 标题与字幕
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onTogglePreview}
|
||||||
|
className="px-2 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Eye className="h-3.5 w-3.5" />
|
||||||
|
{showStylePreview ? "收起预览" : "预览样式"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showStylePreview && (
|
||||||
|
<div
|
||||||
|
ref={previewContainerRef}
|
||||||
|
className="mb-4 rounded-xl border border-white/10 bg-black/40 relative overflow-hidden"
|
||||||
|
style={{ aspectRatio: previewAspectRatio, minHeight: '180px' }}
|
||||||
|
>
|
||||||
|
{(titleFontUrl || subtitleFontUrl) && (
|
||||||
|
<style>{`
|
||||||
|
${titleFontUrl ? `@font-face { font-family: '${titleFontFamilyName}'; src: url('${titleFontUrl}') format('${getFontFormat(activeTitleStyle?.font_file)}'); font-weight: 400; font-style: normal; }` : ''}
|
||||||
|
${subtitleFontUrl ? `@font-face { font-family: '${subtitleFontFamilyName}'; src: url('${subtitleFontUrl}') format('${getFontFormat(activeSubtitleStyle?.font_file)}'); font-weight: 400; font-style: normal; }` : ''}
|
||||||
|
`}</style>
|
||||||
|
)}
|
||||||
|
<div className="absolute inset-0 opacity-20 bg-gradient-to-br from-purple-500/40 via-transparent to-pink-500/30" />
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0"
|
||||||
|
style={{
|
||||||
|
width: `${previewBaseWidth}px`,
|
||||||
|
height: `${previewBaseHeight}px`,
|
||||||
|
transform: `scale(${previewScale})`,
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full text-center"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: `${titleTopMargin}px`,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
color: titleColor,
|
||||||
|
fontSize: `${titleFontSize}px`,
|
||||||
|
fontWeight: titleFontWeight,
|
||||||
|
fontFamily: titleFontUrl
|
||||||
|
? `'${titleFontFamilyName}', "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif`
|
||||||
|
: '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif',
|
||||||
|
textShadow: buildTextShadow(titleStrokeColor, titleStrokeSize),
|
||||||
|
letterSpacing: `${titleLetterSpacing}px`,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
opacity: videoTitle.trim() ? 1 : 0.7,
|
||||||
|
padding: '0 5%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{previewTitleText}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="w-full text-center"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: `${subtitleBottomMargin}px`,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
fontSize: `${subtitleFontSize}px`,
|
||||||
|
fontFamily: subtitleFontUrl
|
||||||
|
? `'${subtitleFontFamilyName}', "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif`
|
||||||
|
: '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif',
|
||||||
|
textShadow: buildTextShadow(subtitleStrokeColor, subtitleStrokeSize),
|
||||||
|
letterSpacing: `${subtitleLetterSpacing}px`,
|
||||||
|
lineHeight: 1.35,
|
||||||
|
padding: '0 6%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{enableSubtitles ? (
|
||||||
|
<>
|
||||||
|
<span style={{ color: subtitleHighlightColor }}>{subtitleHighlightText}</span>
|
||||||
|
<span style={{ color: subtitleNormalColor }}>{subtitleNormalText}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 text-sm">字幕已关闭</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="text-sm text-gray-300 mb-2 block">片头标题(可选)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={videoTitle}
|
||||||
|
onChange={(e) => onTitleChange(e.target.value)}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{titleStyles.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="text-sm text-gray-300 mb-2 block">标题样式</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{titleStyles.map((style) => (
|
||||||
|
<button
|
||||||
|
key={style.id}
|
||||||
|
onClick={() => onSelectTitleStyle(style.id)}
|
||||||
|
className={`p-2 rounded-lg border transition-all text-left ${selectedTitleStyleId === style.id
|
||||||
|
? "border-purple-500 bg-purple-500/20"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-white text-sm truncate">{style.label}</div>
|
||||||
|
<div className="text-xs text-gray-400 truncate">
|
||||||
|
{style.font_family || style.font_file || ""}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="text-xs text-gray-400 mb-2 block">标题字号: {titleFontSize}px</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="48"
|
||||||
|
max="110"
|
||||||
|
step="1"
|
||||||
|
value={titleFontSize}
|
||||||
|
onChange={(e) => onTitleFontSizeChange(parseInt(e.target.value, 10))}
|
||||||
|
className="w-full accent-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableSubtitles && subtitleStyles.length > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="text-sm text-gray-300 mb-2 block">字幕样式</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{subtitleStyles.map((style) => (
|
||||||
|
<button
|
||||||
|
key={style.id}
|
||||||
|
onClick={() => onSelectSubtitleStyle(style.id)}
|
||||||
|
className={`p-2 rounded-lg border transition-all text-left ${selectedSubtitleStyleId === style.id
|
||||||
|
? "border-purple-500 bg-purple-500/20"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-white text-sm truncate">{style.label}</div>
|
||||||
|
<div className="text-xs text-gray-400 truncate">
|
||||||
|
{style.font_family || style.font_file || ""}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="text-xs text-gray-400 mb-2 block">字幕字号: {subtitleFontSize}px</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="32"
|
||||||
|
max="90"
|
||||||
|
step="1"
|
||||||
|
value={subtitleFontSize}
|
||||||
|
onChange={(e) => onSubtitleFontSizeChange(parseInt(e.target.value, 10))}
|
||||||
|
className="w-full accent-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-gray-300">逐字高亮字幕</span>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">自动生成卡拉OK效果字幕</p>
|
||||||
|
</div>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enableSubtitles}
|
||||||
|
onChange={(e) => onToggleSubtitles(e.target.checked)}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
frontend/src/components/home/VoiceSelector.tsx
Normal file
75
frontend/src/components/home/VoiceSelector.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { Mic, Volume2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface VoiceOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VoiceSelectorProps {
|
||||||
|
ttsMode: "edgetts" | "voiceclone";
|
||||||
|
onSelectTtsMode: (mode: "edgetts" | "voiceclone") => void;
|
||||||
|
voices: VoiceOption[];
|
||||||
|
voice: string;
|
||||||
|
onSelectVoice: (id: string) => void;
|
||||||
|
voiceCloneSlot: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VoiceSelector({
|
||||||
|
ttsMode,
|
||||||
|
onSelectTtsMode,
|
||||||
|
voices,
|
||||||
|
voice,
|
||||||
|
onSelectVoice,
|
||||||
|
voiceCloneSlot,
|
||||||
|
}: VoiceSelectorProps) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectTtsMode("edgetts")}
|
||||||
|
className={`flex-1 py-2 px-4 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${ttsMode === "edgetts"
|
||||||
|
? "bg-purple-600 text-white"
|
||||||
|
: "bg-white/10 text-gray-300 hover:bg-white/20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Volume2 className="h-4 w-4" />
|
||||||
|
选择声音
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectTtsMode("voiceclone")}
|
||||||
|
className={`flex-1 py-2 px-4 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${ttsMode === "voiceclone"
|
||||||
|
? "bg-purple-600 text-white"
|
||||||
|
: "bg-white/10 text-gray-300 hover:bg-white/20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Mic className="h-4 w-4" />
|
||||||
|
克隆声音
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ttsMode === "edgetts" && (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{voices.map((v) => (
|
||||||
|
<button
|
||||||
|
key={v.id}
|
||||||
|
onClick={() => onSelectVoice(v.id)}
|
||||||
|
className={`p-3 rounded-xl border-2 transition-all text-left ${voice === v.id
|
||||||
|
? "border-purple-500 bg-purple-500/20"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-white text-sm">{v.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ttsMode === "voiceclone" && voiceCloneSlot}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
frontend/src/hooks/useBgm.ts
Normal file
55
frontend/src/hooks/useBgm.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import api from "@/lib/axios";
|
||||||
|
|
||||||
|
export interface BgmItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ext?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseBgmOptions {
|
||||||
|
storageKey: string;
|
||||||
|
selectedBgmId: string;
|
||||||
|
setSelectedBgmId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBgm = ({
|
||||||
|
storageKey,
|
||||||
|
selectedBgmId,
|
||||||
|
setSelectedBgmId,
|
||||||
|
}: UseBgmOptions) => {
|
||||||
|
const [bgmList, setBgmList] = useState<BgmItem[]>([]);
|
||||||
|
const [bgmLoading, setBgmLoading] = useState(false);
|
||||||
|
const [bgmError, setBgmError] = useState<string>("");
|
||||||
|
|
||||||
|
const fetchBgmList = useCallback(async () => {
|
||||||
|
setBgmLoading(true);
|
||||||
|
setBgmError("");
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/api/assets/bgm');
|
||||||
|
const items: BgmItem[] = Array.isArray(data.bgm) ? data.bgm : [];
|
||||||
|
setBgmList(items);
|
||||||
|
|
||||||
|
const savedBgmId = localStorage.getItem(`vigent_${storageKey}_bgmId`);
|
||||||
|
setSelectedBgmId((prev) => {
|
||||||
|
if (prev && items.some((item) => item.id === prev)) return prev;
|
||||||
|
if (savedBgmId && items.some((item) => item.id === savedBgmId)) return savedBgmId;
|
||||||
|
return items[0]?.id || "";
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error?.response?.data?.detail || error?.message || '加载失败';
|
||||||
|
setBgmError(message);
|
||||||
|
setBgmList([]);
|
||||||
|
console.error("获取背景音乐失败:", error);
|
||||||
|
} finally {
|
||||||
|
setBgmLoading(false);
|
||||||
|
}
|
||||||
|
}, [setSelectedBgmId, storageKey]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bgmList,
|
||||||
|
bgmLoading,
|
||||||
|
bgmError,
|
||||||
|
fetchBgmList,
|
||||||
|
};
|
||||||
|
};
|
||||||
81
frontend/src/hooks/useGeneratedVideos.ts
Normal file
81
frontend/src/hooks/useGeneratedVideos.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import api from "@/lib/axios";
|
||||||
|
|
||||||
|
interface GeneratedVideo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size_mb: number;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseGeneratedVideosOptions {
|
||||||
|
storageKey: string;
|
||||||
|
selectedVideoId: string | null;
|
||||||
|
setSelectedVideoId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
setGeneratedVideo: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
resolveMediaUrl: (url?: string | null) => string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGeneratedVideos = ({
|
||||||
|
storageKey,
|
||||||
|
selectedVideoId,
|
||||||
|
setSelectedVideoId,
|
||||||
|
setGeneratedVideo,
|
||||||
|
resolveMediaUrl,
|
||||||
|
}: UseGeneratedVideosOptions) => {
|
||||||
|
const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([]);
|
||||||
|
|
||||||
|
const fetchGeneratedVideos = useCallback(async (preferVideoId?: string) => {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/api/videos/generated');
|
||||||
|
const videos: GeneratedVideo[] = data.videos || [];
|
||||||
|
setGeneratedVideos(videos);
|
||||||
|
|
||||||
|
const savedSelectedVideoId = localStorage.getItem(`vigent_${storageKey}_selectedVideoId`);
|
||||||
|
const currentId = preferVideoId || selectedVideoId || savedSelectedVideoId || null;
|
||||||
|
let nextId: string | null = null;
|
||||||
|
let nextUrl: string | null = null;
|
||||||
|
|
||||||
|
if (currentId) {
|
||||||
|
const found = videos.find(v => v.id === currentId);
|
||||||
|
if (found) {
|
||||||
|
nextId = found.id;
|
||||||
|
nextUrl = resolveMediaUrl(found.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextId && videos.length > 0) {
|
||||||
|
nextId = videos[0].id;
|
||||||
|
nextUrl = resolveMediaUrl(videos[0].path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextId) {
|
||||||
|
setSelectedVideoId(nextId);
|
||||||
|
setGeneratedVideo(nextUrl);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取历史视频失败:", error);
|
||||||
|
}
|
||||||
|
}, [resolveMediaUrl, selectedVideoId, setGeneratedVideo, setSelectedVideoId, storageKey]);
|
||||||
|
|
||||||
|
const deleteVideo = useCallback(async (videoId: string) => {
|
||||||
|
if (!confirm("确定要删除这个视频吗?")) return;
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/videos/generated/${videoId}`);
|
||||||
|
if (selectedVideoId === videoId) {
|
||||||
|
setSelectedVideoId(null);
|
||||||
|
setGeneratedVideo(null);
|
||||||
|
}
|
||||||
|
fetchGeneratedVideos();
|
||||||
|
} catch (error) {
|
||||||
|
alert("删除失败: " + error);
|
||||||
|
}
|
||||||
|
}, [fetchGeneratedVideos, selectedVideoId, setGeneratedVideo, setSelectedVideoId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
generatedVideos,
|
||||||
|
fetchGeneratedVideos,
|
||||||
|
deleteVideo,
|
||||||
|
};
|
||||||
|
};
|
||||||
250
frontend/src/hooks/useHomePersistence.ts
Normal file
250
frontend/src/hooks/useHomePersistence.ts
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface RefAudio {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
ref_text: string;
|
||||||
|
duration_sec: number;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseHomePersistenceOptions {
|
||||||
|
isAuthLoading: boolean;
|
||||||
|
storageKey: string;
|
||||||
|
text: string;
|
||||||
|
setText: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
videoTitle: string;
|
||||||
|
setVideoTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
enableSubtitles: boolean;
|
||||||
|
setEnableSubtitles: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
ttsMode: 'edgetts' | 'voiceclone';
|
||||||
|
setTtsMode: React.Dispatch<React.SetStateAction<'edgetts' | 'voiceclone'>>;
|
||||||
|
voice: string;
|
||||||
|
setVoice: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
selectedMaterial: string;
|
||||||
|
setSelectedMaterial: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
selectedSubtitleStyleId: string;
|
||||||
|
setSelectedSubtitleStyleId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
selectedTitleStyleId: string;
|
||||||
|
setSelectedTitleStyleId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
subtitleFontSize: number;
|
||||||
|
setSubtitleFontSize: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
titleFontSize: number;
|
||||||
|
setTitleFontSize: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
setSubtitleSizeLocked: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setTitleSizeLocked: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
selectedBgmId: string;
|
||||||
|
setSelectedBgmId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
bgmVolume: number;
|
||||||
|
setBgmVolume: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
enableBgm: boolean;
|
||||||
|
setEnableBgm: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
selectedVideoId: string | null;
|
||||||
|
setSelectedVideoId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
selectedRefAudio: RefAudio | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHomePersistence = ({
|
||||||
|
isAuthLoading,
|
||||||
|
storageKey,
|
||||||
|
text,
|
||||||
|
setText,
|
||||||
|
videoTitle,
|
||||||
|
setVideoTitle,
|
||||||
|
enableSubtitles,
|
||||||
|
setEnableSubtitles,
|
||||||
|
ttsMode,
|
||||||
|
setTtsMode,
|
||||||
|
voice,
|
||||||
|
setVoice,
|
||||||
|
selectedMaterial,
|
||||||
|
setSelectedMaterial,
|
||||||
|
selectedSubtitleStyleId,
|
||||||
|
setSelectedSubtitleStyleId,
|
||||||
|
selectedTitleStyleId,
|
||||||
|
setSelectedTitleStyleId,
|
||||||
|
subtitleFontSize,
|
||||||
|
setSubtitleFontSize,
|
||||||
|
titleFontSize,
|
||||||
|
setTitleFontSize,
|
||||||
|
setSubtitleSizeLocked,
|
||||||
|
setTitleSizeLocked,
|
||||||
|
selectedBgmId,
|
||||||
|
setSelectedBgmId,
|
||||||
|
bgmVolume,
|
||||||
|
setBgmVolume,
|
||||||
|
enableBgm,
|
||||||
|
setEnableBgm,
|
||||||
|
selectedVideoId,
|
||||||
|
setSelectedVideoId,
|
||||||
|
selectedRefAudio,
|
||||||
|
}: UseHomePersistenceOptions) => {
|
||||||
|
const [isRestored, setIsRestored] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthLoading) return;
|
||||||
|
|
||||||
|
const savedText = localStorage.getItem(`vigent_${storageKey}_text`);
|
||||||
|
const savedTitle = localStorage.getItem(`vigent_${storageKey}_title`);
|
||||||
|
const savedSubtitles = localStorage.getItem(`vigent_${storageKey}_subtitles`);
|
||||||
|
const savedTtsMode = localStorage.getItem(`vigent_${storageKey}_ttsMode`);
|
||||||
|
const savedVoice = localStorage.getItem(`vigent_${storageKey}_voice`);
|
||||||
|
const savedMaterial = localStorage.getItem(`vigent_${storageKey}_material`);
|
||||||
|
const savedSubtitleStyle = localStorage.getItem(`vigent_${storageKey}_subtitleStyle`);
|
||||||
|
const savedTitleStyle = localStorage.getItem(`vigent_${storageKey}_titleStyle`);
|
||||||
|
const savedSubtitleFontSize = localStorage.getItem(`vigent_${storageKey}_subtitleFontSize`);
|
||||||
|
const savedTitleFontSize = localStorage.getItem(`vigent_${storageKey}_titleFontSize`);
|
||||||
|
const savedBgmId = localStorage.getItem(`vigent_${storageKey}_bgmId`);
|
||||||
|
const savedSelectedVideoId = localStorage.getItem(`vigent_${storageKey}_selectedVideoId`);
|
||||||
|
const savedBgmVolume = localStorage.getItem(`vigent_${storageKey}_bgmVolume`);
|
||||||
|
const savedEnableBgm = localStorage.getItem(`vigent_${storageKey}_enableBgm`);
|
||||||
|
|
||||||
|
setText(savedText || "大家好,欢迎来到我的频道,今天给大家分享一些有趣的内容。");
|
||||||
|
setVideoTitle(savedTitle || "");
|
||||||
|
setEnableSubtitles(savedSubtitles !== null ? savedSubtitles === 'true' : true);
|
||||||
|
setTtsMode((savedTtsMode as 'edgetts' | 'voiceclone') || 'edgetts');
|
||||||
|
setVoice(savedVoice || "zh-CN-YunxiNeural");
|
||||||
|
|
||||||
|
if (savedMaterial) setSelectedMaterial(savedMaterial);
|
||||||
|
if (savedSubtitleStyle) setSelectedSubtitleStyleId(savedSubtitleStyle);
|
||||||
|
if (savedTitleStyle) setSelectedTitleStyleId(savedTitleStyle);
|
||||||
|
|
||||||
|
if (savedSubtitleFontSize) {
|
||||||
|
const parsed = parseInt(savedSubtitleFontSize, 10);
|
||||||
|
if (!Number.isNaN(parsed)) {
|
||||||
|
setSubtitleFontSize(parsed);
|
||||||
|
setSubtitleSizeLocked(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedTitleFontSize) {
|
||||||
|
const parsed = parseInt(savedTitleFontSize, 10);
|
||||||
|
if (!Number.isNaN(parsed)) {
|
||||||
|
setTitleFontSize(parsed);
|
||||||
|
setTitleSizeLocked(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedBgmId) setSelectedBgmId(savedBgmId);
|
||||||
|
if (savedBgmVolume) setBgmVolume(parseFloat(savedBgmVolume));
|
||||||
|
if (savedEnableBgm !== null) setEnableBgm(savedEnableBgm === 'true');
|
||||||
|
if (savedSelectedVideoId) setSelectedVideoId(savedSelectedVideoId);
|
||||||
|
|
||||||
|
setIsRestored(true);
|
||||||
|
}, [
|
||||||
|
isAuthLoading,
|
||||||
|
setBgmVolume,
|
||||||
|
setEnableBgm,
|
||||||
|
setEnableSubtitles,
|
||||||
|
setSelectedBgmId,
|
||||||
|
setSelectedMaterial,
|
||||||
|
setSelectedSubtitleStyleId,
|
||||||
|
setSelectedTitleStyleId,
|
||||||
|
setSelectedVideoId,
|
||||||
|
setSubtitleFontSize,
|
||||||
|
setSubtitleSizeLocked,
|
||||||
|
setText,
|
||||||
|
setTitleFontSize,
|
||||||
|
setTitleSizeLocked,
|
||||||
|
setTtsMode,
|
||||||
|
setVideoTitle,
|
||||||
|
setVoice,
|
||||||
|
storageKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRestored) return;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_text`, text);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [text, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRestored) return;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_title`, videoTitle);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [videoTitle, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored) localStorage.setItem(`vigent_${storageKey}_subtitles`, String(enableSubtitles));
|
||||||
|
}, [enableSubtitles, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored) localStorage.setItem(`vigent_${storageKey}_ttsMode`, ttsMode);
|
||||||
|
}, [ttsMode, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored) localStorage.setItem(`vigent_${storageKey}_voice`, voice);
|
||||||
|
}, [voice, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored && selectedMaterial) {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_material`, selectedMaterial);
|
||||||
|
}
|
||||||
|
}, [selectedMaterial, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored && selectedSubtitleStyleId) {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_subtitleStyle`, selectedSubtitleStyleId);
|
||||||
|
}
|
||||||
|
}, [selectedSubtitleStyleId, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored && selectedTitleStyleId) {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_titleStyle`, selectedTitleStyleId);
|
||||||
|
}
|
||||||
|
}, [selectedTitleStyleId, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored) {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_subtitleFontSize`, String(subtitleFontSize));
|
||||||
|
}
|
||||||
|
}, [subtitleFontSize, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored) {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_titleFontSize`, String(titleFontSize));
|
||||||
|
}
|
||||||
|
}, [titleFontSize, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored) {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_bgmId`, selectedBgmId);
|
||||||
|
}
|
||||||
|
}, [selectedBgmId, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRestored) return;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_bgmVolume`, String(bgmVolume));
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [bgmVolume, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored) {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_enableBgm`, String(enableBgm));
|
||||||
|
}
|
||||||
|
}, [enableBgm, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRestored) return;
|
||||||
|
if (selectedVideoId) {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_selectedVideoId`, selectedVideoId);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(`vigent_${storageKey}_selectedVideoId`);
|
||||||
|
}
|
||||||
|
}, [selectedVideoId, storageKey, isRestored]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRestored && selectedRefAudio) {
|
||||||
|
localStorage.setItem(`vigent_${storageKey}_refAudioId`, selectedRefAudio.id);
|
||||||
|
}
|
||||||
|
}, [selectedRefAudio, storageKey, isRestored]);
|
||||||
|
|
||||||
|
return { isRestored };
|
||||||
|
};
|
||||||
121
frontend/src/hooks/useMaterials.ts
Normal file
121
frontend/src/hooks/useMaterials.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import api from "@/lib/axios";
|
||||||
|
|
||||||
|
interface Material {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
scene: string;
|
||||||
|
size_mb: number;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseMaterialsOptions {
|
||||||
|
selectedMaterial: string;
|
||||||
|
setSelectedMaterial: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMaterials = ({
|
||||||
|
selectedMaterial,
|
||||||
|
setSelectedMaterial,
|
||||||
|
}: UseMaterialsOptions) => {
|
||||||
|
const [materials, setMaterials] = useState<Material[]>([]);
|
||||||
|
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||||
|
const [debugData, setDebugData] = useState<string>("");
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
|
const [uploadData, setUploadData] = useState<string>("");
|
||||||
|
|
||||||
|
const fetchMaterials = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setFetchError(null);
|
||||||
|
setDebugData("Loading...");
|
||||||
|
|
||||||
|
const { data } = await api.get(`/api/materials?t=${new Date().getTime()}`);
|
||||||
|
setDebugData(JSON.stringify(data).substring(0, 200));
|
||||||
|
const nextMaterials = data.materials || [];
|
||||||
|
setMaterials(nextMaterials);
|
||||||
|
|
||||||
|
const nextSelected = nextMaterials.find((item: Material) => item.id === selectedMaterial)?.id
|
||||||
|
|| nextMaterials[0]?.id
|
||||||
|
|| "";
|
||||||
|
if (nextSelected !== selectedMaterial) {
|
||||||
|
setSelectedMaterial(nextSelected);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取素材失败:", error);
|
||||||
|
setFetchError(String(error));
|
||||||
|
setDebugData(`Error: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}, [selectedMaterial, setSelectedMaterial]);
|
||||||
|
|
||||||
|
const deleteMaterial = useCallback(async (materialId: string) => {
|
||||||
|
if (!confirm("确定要删除这个素材吗?")) return;
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/materials/${materialId}`);
|
||||||
|
fetchMaterials();
|
||||||
|
if (selectedMaterial === materialId) {
|
||||||
|
setSelectedMaterial("");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("删除失败: " + error);
|
||||||
|
}
|
||||||
|
}, [fetchMaterials, selectedMaterial, setSelectedMaterial]);
|
||||||
|
|
||||||
|
const handleUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const validTypes = ['.mp4', '.mov', '.avi'];
|
||||||
|
const ext = file.name.toLowerCase().slice(file.name.lastIndexOf('.'));
|
||||||
|
if (!validTypes.includes(ext)) {
|
||||||
|
setUploadError('仅支持 MP4、MOV、AVI 格式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploading(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
setUploadError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
await api.post('/api/materials', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
if (progressEvent.total) {
|
||||||
|
const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
||||||
|
setUploadProgress(progress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setUploadProgress(100);
|
||||||
|
setIsUploading(false);
|
||||||
|
fetchMaterials();
|
||||||
|
setUploadData("");
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Upload failed:", err);
|
||||||
|
setIsUploading(false);
|
||||||
|
const errorMsg = err.response?.data?.detail || err.message || String(err);
|
||||||
|
setUploadError(`上传失败: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.target.value = '';
|
||||||
|
}, [fetchMaterials]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
materials,
|
||||||
|
fetchError,
|
||||||
|
debugData,
|
||||||
|
isUploading,
|
||||||
|
uploadProgress,
|
||||||
|
uploadError,
|
||||||
|
uploadData,
|
||||||
|
setUploadError,
|
||||||
|
fetchMaterials,
|
||||||
|
deleteMaterial,
|
||||||
|
handleUpload,
|
||||||
|
};
|
||||||
|
};
|
||||||
116
frontend/src/hooks/useMediaPlayers.ts
Normal file
116
frontend/src/hooks/useMediaPlayers.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import type { BgmItem } from "@/hooks/useBgm";
|
||||||
|
|
||||||
|
interface RefAudio {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
ref_text: string;
|
||||||
|
duration_sec: number;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseMediaPlayersOptions {
|
||||||
|
bgmVolume: number;
|
||||||
|
resolveBgmUrl: (bgmId?: string | null) => string | null;
|
||||||
|
resolveMediaUrl: (url?: string | null) => string | null;
|
||||||
|
setSelectedBgmId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setEnableBgm: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMediaPlayers = ({
|
||||||
|
bgmVolume,
|
||||||
|
resolveBgmUrl,
|
||||||
|
resolveMediaUrl,
|
||||||
|
setSelectedBgmId,
|
||||||
|
setEnableBgm,
|
||||||
|
}: UseMediaPlayersOptions) => {
|
||||||
|
const [playingAudioId, setPlayingAudioId] = useState<string | null>(null);
|
||||||
|
const [playingBgmId, setPlayingBgmId] = useState<string | null>(null);
|
||||||
|
const audioPlayerRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const bgmPlayerRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
|
const stopAudio = useCallback(() => {
|
||||||
|
if (audioPlayerRef.current) {
|
||||||
|
audioPlayerRef.current.pause();
|
||||||
|
audioPlayerRef.current.currentTime = 0;
|
||||||
|
audioPlayerRef.current = null;
|
||||||
|
}
|
||||||
|
setPlayingAudioId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stopBgm = useCallback(() => {
|
||||||
|
if (bgmPlayerRef.current) {
|
||||||
|
bgmPlayerRef.current.pause();
|
||||||
|
bgmPlayerRef.current.currentTime = 0;
|
||||||
|
bgmPlayerRef.current = null;
|
||||||
|
}
|
||||||
|
setPlayingBgmId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const togglePlayPreview = useCallback((audio: RefAudio, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (bgmPlayerRef.current) {
|
||||||
|
stopBgm();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playingAudioId === audio.id) {
|
||||||
|
stopAudio();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAudio();
|
||||||
|
|
||||||
|
const audioUrl = resolveMediaUrl(audio.path) || audio.path;
|
||||||
|
if (!audioUrl) {
|
||||||
|
alert("无法播放该参考音频");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const player = new Audio(audioUrl);
|
||||||
|
player.onended = () => setPlayingAudioId(null);
|
||||||
|
player.play().catch((err) => alert("播放失败: " + err));
|
||||||
|
audioPlayerRef.current = player;
|
||||||
|
setPlayingAudioId(audio.id);
|
||||||
|
}, [playingAudioId, resolveMediaUrl, stopAudio, stopBgm]);
|
||||||
|
|
||||||
|
const toggleBgmPreview = useCallback((bgm: BgmItem, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedBgmId(bgm.id);
|
||||||
|
setEnableBgm(true);
|
||||||
|
|
||||||
|
const bgmUrl = resolveBgmUrl(bgm.id);
|
||||||
|
if (!bgmUrl) {
|
||||||
|
alert("无法播放该背景音乐");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playingBgmId === bgm.id) {
|
||||||
|
stopBgm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAudio();
|
||||||
|
stopBgm();
|
||||||
|
|
||||||
|
const player = new Audio(bgmUrl);
|
||||||
|
player.volume = Math.max(0, Math.min(bgmVolume, 1));
|
||||||
|
player.onended = () => setPlayingBgmId(null);
|
||||||
|
player.play().catch((err) => alert("播放失败: " + err));
|
||||||
|
bgmPlayerRef.current = player;
|
||||||
|
setPlayingBgmId(bgm.id);
|
||||||
|
}, [bgmVolume, playingBgmId, resolveBgmUrl, setEnableBgm, setSelectedBgmId, stopAudio, stopBgm]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bgmPlayerRef.current) {
|
||||||
|
bgmPlayerRef.current.volume = Math.max(0, Math.min(bgmVolume, 1));
|
||||||
|
}
|
||||||
|
}, [bgmVolume]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
playingAudioId,
|
||||||
|
playingBgmId,
|
||||||
|
togglePlayPreview,
|
||||||
|
toggleBgmPreview,
|
||||||
|
};
|
||||||
|
};
|
||||||
91
frontend/src/hooks/useRefAudios.ts
Normal file
91
frontend/src/hooks/useRefAudios.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import api from "@/lib/axios";
|
||||||
|
|
||||||
|
interface RefAudio {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
ref_text: string;
|
||||||
|
duration_sec: number;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseRefAudiosOptions {
|
||||||
|
fixedRefText: string;
|
||||||
|
selectedRefAudio: RefAudio | null;
|
||||||
|
setSelectedRefAudio: React.Dispatch<React.SetStateAction<RefAudio | null>>;
|
||||||
|
setRefText: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRefAudios = ({
|
||||||
|
fixedRefText,
|
||||||
|
selectedRefAudio,
|
||||||
|
setSelectedRefAudio,
|
||||||
|
setRefText,
|
||||||
|
}: UseRefAudiosOptions) => {
|
||||||
|
const [refAudios, setRefAudios] = useState<RefAudio[]>([]);
|
||||||
|
const [isUploadingRef, setIsUploadingRef] = useState(false);
|
||||||
|
const [uploadRefError, setUploadRefError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchRefAudios = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/api/ref-audios');
|
||||||
|
const items: RefAudio[] = data.items || [];
|
||||||
|
items.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
setRefAudios(items);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取参考音频失败:", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const uploadRefAudio = useCallback(async (file: File) => {
|
||||||
|
const refTextInput = fixedRefText;
|
||||||
|
|
||||||
|
setIsUploadingRef(true);
|
||||||
|
setUploadRefError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('ref_text', refTextInput);
|
||||||
|
|
||||||
|
const { data } = await api.post('/api/ref-audios', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchRefAudios();
|
||||||
|
setSelectedRefAudio(data);
|
||||||
|
setRefText(data.ref_text);
|
||||||
|
setIsUploadingRef(false);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Upload ref audio failed:", err);
|
||||||
|
setIsUploadingRef(false);
|
||||||
|
const errorMsg = err.response?.data?.detail || err.message || String(err);
|
||||||
|
setUploadRefError(`上传失败: ${errorMsg}`);
|
||||||
|
}
|
||||||
|
}, [fetchRefAudios, fixedRefText, setRefText, setSelectedRefAudio]);
|
||||||
|
|
||||||
|
const deleteRefAudio = useCallback(async (audioId: string) => {
|
||||||
|
if (!confirm("确定要删除这个参考音频吗?")) return;
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/ref-audios/${encodeURIComponent(audioId)}`);
|
||||||
|
fetchRefAudios();
|
||||||
|
if (selectedRefAudio?.id === audioId) {
|
||||||
|
setSelectedRefAudio(null);
|
||||||
|
setRefText('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("删除失败: " + error);
|
||||||
|
}
|
||||||
|
}, [fetchRefAudios, selectedRefAudio, setRefText, setSelectedRefAudio]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
refAudios,
|
||||||
|
isUploadingRef,
|
||||||
|
uploadRefError,
|
||||||
|
setUploadRefError,
|
||||||
|
fetchRefAudios,
|
||||||
|
uploadRefAudio,
|
||||||
|
deleteRefAudio,
|
||||||
|
};
|
||||||
|
};
|
||||||
98
frontend/src/hooks/useTitleSubtitleStyles.ts
Normal file
98
frontend/src/hooks/useTitleSubtitleStyles.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import api from "@/lib/axios";
|
||||||
|
|
||||||
|
export interface SubtitleStyleOption {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
font_family?: string;
|
||||||
|
font_file?: string;
|
||||||
|
font_size?: number;
|
||||||
|
highlight_color?: string;
|
||||||
|
normal_color?: string;
|
||||||
|
stroke_color?: string;
|
||||||
|
stroke_size?: number;
|
||||||
|
letter_spacing?: number;
|
||||||
|
bottom_margin?: number;
|
||||||
|
is_default?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TitleStyleOption {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
font_family?: string;
|
||||||
|
font_file?: string;
|
||||||
|
font_size?: number;
|
||||||
|
color?: string;
|
||||||
|
stroke_color?: string;
|
||||||
|
stroke_size?: number;
|
||||||
|
letter_spacing?: number;
|
||||||
|
font_weight?: number;
|
||||||
|
top_margin?: number;
|
||||||
|
is_default?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseTitleSubtitleStylesOptions {
|
||||||
|
isAuthLoading: boolean;
|
||||||
|
storageKey: string;
|
||||||
|
setSelectedSubtitleStyleId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setSelectedTitleStyleId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTitleSubtitleStyles = ({
|
||||||
|
isAuthLoading,
|
||||||
|
storageKey,
|
||||||
|
setSelectedSubtitleStyleId,
|
||||||
|
setSelectedTitleStyleId,
|
||||||
|
}: UseTitleSubtitleStylesOptions) => {
|
||||||
|
const [subtitleStyles, setSubtitleStyles] = useState<SubtitleStyleOption[]>([]);
|
||||||
|
const [titleStyles, setTitleStyles] = useState<TitleStyleOption[]>([]);
|
||||||
|
|
||||||
|
const refreshSubtitleStyles = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/api/assets/subtitle-styles');
|
||||||
|
const styles: SubtitleStyleOption[] = data.styles || [];
|
||||||
|
setSubtitleStyles(styles);
|
||||||
|
|
||||||
|
const savedStyleId = localStorage.getItem(`vigent_${storageKey}_subtitleStyle`);
|
||||||
|
setSelectedSubtitleStyleId((prev) => {
|
||||||
|
if (prev && styles.some((s) => s.id === prev)) return prev;
|
||||||
|
if (savedStyleId && styles.some((s) => s.id === savedStyleId)) return savedStyleId;
|
||||||
|
const defaultStyle = styles.find((s) => s.is_default) || styles[0];
|
||||||
|
return defaultStyle?.id || "";
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取字幕样式失败:", error);
|
||||||
|
}
|
||||||
|
}, [setSelectedSubtitleStyleId, storageKey]);
|
||||||
|
|
||||||
|
const refreshTitleStyles = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/api/assets/title-styles');
|
||||||
|
const styles: TitleStyleOption[] = data.styles || [];
|
||||||
|
setTitleStyles(styles);
|
||||||
|
|
||||||
|
const savedStyleId = localStorage.getItem(`vigent_${storageKey}_titleStyle`);
|
||||||
|
setSelectedTitleStyleId((prev) => {
|
||||||
|
if (prev && styles.some((s) => s.id === prev)) return prev;
|
||||||
|
if (savedStyleId && styles.some((s) => s.id === savedStyleId)) return savedStyleId;
|
||||||
|
const defaultStyle = styles.find((s) => s.is_default) || styles[0];
|
||||||
|
return defaultStyle?.id || "";
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取标题样式失败:", error);
|
||||||
|
}
|
||||||
|
}, [setSelectedTitleStyleId, storageKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthLoading) return;
|
||||||
|
refreshSubtitleStyles();
|
||||||
|
refreshTitleStyles();
|
||||||
|
}, [isAuthLoading, refreshSubtitleStyles, refreshTitleStyles]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subtitleStyles,
|
||||||
|
titleStyles,
|
||||||
|
refreshSubtitleStyles,
|
||||||
|
refreshTitleStyles,
|
||||||
|
};
|
||||||
|
};
|
||||||
61
frontend/src/lib/media.ts
Normal file
61
frontend/src/lib/media.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const DEFAULT_API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8006';
|
||||||
|
|
||||||
|
export const getApiBaseUrl = () => {
|
||||||
|
return typeof window === 'undefined' ? DEFAULT_API_BASE : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAbsoluteUrl = (url: string) => /^https?:\/\//i.test(url);
|
||||||
|
|
||||||
|
export const joinBaseUrl = (base: string, path: string) => {
|
||||||
|
if (!base) return path;
|
||||||
|
if (!path.startsWith('/')) return `${base}/${path}`;
|
||||||
|
return `${base}${path}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveMediaUrl = (url?: string | null) => {
|
||||||
|
if (!url) return null;
|
||||||
|
if (isAbsoluteUrl(url)) return url;
|
||||||
|
return joinBaseUrl(getApiBaseUrl(), url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encodePathSegments = (value: string) =>
|
||||||
|
value.split('/').map(encodeURIComponent).join('/');
|
||||||
|
|
||||||
|
export const resolveAssetUrl = (assetPath?: string | null) => {
|
||||||
|
if (!assetPath) return null;
|
||||||
|
const encoded = encodePathSegments(assetPath);
|
||||||
|
return joinBaseUrl(getApiBaseUrl(), `/assets/${encoded}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveBgmUrl = (bgmId?: string | null) => {
|
||||||
|
if (!bgmId) return null;
|
||||||
|
return resolveAssetUrl(`bgm/${bgmId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFontFormat = (fontFile?: string) => {
|
||||||
|
if (!fontFile) return 'truetype';
|
||||||
|
const ext = fontFile.split('.').pop()?.toLowerCase();
|
||||||
|
if (ext === 'otf') return 'opentype';
|
||||||
|
return 'truetype';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildTextShadow = (color: string, size: number) => {
|
||||||
|
return [
|
||||||
|
`-${size}px -${size}px 0 ${color}`,
|
||||||
|
`${size}px -${size}px 0 ${color}`,
|
||||||
|
`-${size}px ${size}px 0 ${color}`,
|
||||||
|
`${size}px ${size}px 0 ${color}`,
|
||||||
|
`0 0 ${size * 4}px rgba(0,0,0,0.9)`,
|
||||||
|
`0 4px 8px rgba(0,0,0,0.6)`
|
||||||
|
].join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDate = (timestamp: number) => {
|
||||||
|
const d = new Date(timestamp * 1000);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(d.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}/${month}/${day} ${hour}:${minute}`;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user