279 lines
8.7 KiB
Markdown
279 lines
8.7 KiB
Markdown
|
||
## 🔧 上传架构重构 (Direct Upload)
|
||
|
||
### 🚨 问题描述 (10:30)
|
||
**现象**:上传大于 7MB 的文件时,后端返回 500 Internal Server Error,实际为 `ClientDisconnect`。
|
||
**ROOT CAUSE (关键原因)**:
|
||
- **Aliyun Nginx 网关限制**:`api.hbyrkj.top` 域名的 Nginx 配置缺少 `client_max_body_size 0;`。
|
||
- **默认限制**:Nginx 默认限制请求体为 1MB (或少量),导致大文件上传时连接被网关强制截断。
|
||
- **误判**:初期待查方向集中在 FRP 和 Backend Proxy 超时,实际是网关层的硬限制。
|
||
|
||
### ✅ 解决方案:前端直传 Supabase + 网关配置 (14:00)
|
||
|
||
**核心变更**:
|
||
1. **网关配置**:在 Aliyun Nginx 的 `api.hbyrkj.top` 配置块中添加 `client_max_body_size 0;` (解除大小限制)。
|
||
2. **架构优化**:移除后端文件转发逻辑,改由前端直接上传到 Supabase Storage (减少链路节点)。
|
||
|
||
#### 1. 前端改造 (`frontend/src/app/page.tsx`)
|
||
- 引入 `@supabase/supabase-js` 客户端。
|
||
- 使用 `supabase.storage.from('materials').upload()` 直接上传。
|
||
- 移除旧的 `XMLHttpRequest` 代理上传逻辑。
|
||
- 添加文件重命名策略:`{timestamp}_{sanitized_filename}`。
|
||
|
||
```typescript
|
||
// V2: Direct Upload (Bypass Backend)
|
||
const { data, error } = await supabase.storage
|
||
.from('materials')
|
||
.upload(path, file, {
|
||
cacheControl: '3600',
|
||
upsert: false
|
||
});
|
||
```
|
||
|
||
#### 2. 后端适配 (`backend/app/api/materials.py`)
|
||
- **上传接口**:(已废弃/保留用于极小文件) 主要流量走直传。
|
||
- **列表接口**:更新为返回 **签名 URL (Signed URL)**,而非本地路径。
|
||
- **兼容性**:前端直接接收 `path` 字段为完整 URL,无需再次拼接。
|
||
|
||
#### 3. 权限控制 (RLS)
|
||
- Supabase 默认禁止匿名写入。
|
||
- 执行 SQL 策略允许 `anon` 角色对 `materials` 桶的 `INSERT` 和 `SELECT` 权限。
|
||
|
||
```sql
|
||
-- Allow anonymous uploads
|
||
CREATE POLICY "Allow public uploads"
|
||
ON storage.objects FOR INSERT
|
||
TO anon WITH CHECK (bucket_id = 'materials');
|
||
```
|
||
|
||
### 结果
|
||
- ✅ **彻底解决超时**:上传不再经过 Nginx/FRP,直接走 Supabase CDN。
|
||
- ✅ **解除大小限制**:不再受限于后端服务的 `client_max_body_size`。
|
||
- ✅ **用户体验提升**:上传速度更快,进度条更准确。
|
||
|
||
|
||
|
||
## 🔧 Supabase 部署与 RLS 配置
|
||
|
||
### 相关文件
|
||
- `supabase_rls.sql`: 定义存储桶权限的 SQL 脚本。
|
||
- `docker-compose.yml`: 确认 Storage 服务配置正常。
|
||
|
||
### 操作步骤
|
||
1. 将 `supabase_rls.sql` 上传至服务器。
|
||
2. 通过 Docker 执行 SQL:
|
||
```bash
|
||
cat supabase_rls.sql | docker exec -i supabase-db psql -U postgres
|
||
```
|
||
3. 验证前端上传成功。
|
||
|
||
---
|
||
|
||
## 🔐 用户隔离实现 (15:00)
|
||
|
||
### 问题描述
|
||
不同账户登录后能看到其他用户上传的素材和生成的视频,缺乏数据隔离。
|
||
|
||
### 解决方案:存储路径前缀隔离
|
||
|
||
#### 1. 素材模块 (`backend/app/api/materials.py`)
|
||
|
||
```python
|
||
# 上传时添加用户ID前缀
|
||
storage_path = f"{user_id}/{timestamp}_{safe_name}"
|
||
|
||
# 列表时只查询当前用户目录
|
||
files_obj = await storage_service.list_files(
|
||
bucket=storage_service.BUCKET_MATERIALS,
|
||
path=user_id # 只列出用户目录下的文件
|
||
)
|
||
|
||
# 删除时验证权限
|
||
if not material_id.startswith(f"{user_id}/"):
|
||
raise HTTPException(403, "无权删除此素材")
|
||
```
|
||
|
||
#### 2. 视频模块 (`backend/app/api/videos.py`)
|
||
|
||
```python
|
||
# 生成视频时使用用户ID目录
|
||
storage_path = f"{user_id}/{task_id}_output.mp4"
|
||
|
||
# 列表/删除同样基于用户目录隔离
|
||
```
|
||
|
||
#### 3. 发布模块 (`backend/app/services/publish_service.py`)
|
||
- Cookie 存储支持用户隔离:`cookies/{user_id}/{platform}.json`
|
||
|
||
### 存储结构
|
||
```
|
||
Supabase Storage/
|
||
├── materials/
|
||
│ ├── {user_id_1}/
|
||
│ │ ├── 1737000001_video1.mp4
|
||
│ │ └── 1737000002_video2.mp4
|
||
│ └── {user_id_2}/
|
||
│ └── 1737000003_video3.mp4
|
||
└── outputs/
|
||
├── {user_id_1}/
|
||
│ └── {task_id}_output.mp4
|
||
└── {user_id_2}/
|
||
└── ...
|
||
```
|
||
|
||
### 结果
|
||
- ✅ 不同用户数据完全隔离
|
||
- ✅ Cookie 和登录状态按用户存储
|
||
- ✅ 删除操作验证所有权
|
||
|
||
---
|
||
|
||
## 🌐 Storage URL 修复 (16:00)
|
||
|
||
### 问题描述
|
||
生成的视频 URL 为 `http://localhost:8008/...`,前端无法访问。
|
||
|
||
### 解决方案
|
||
|
||
#### 1. 后端配置 (`backend/.env`)
|
||
```ini
|
||
SUPABASE_URL=http://localhost:8008 # 内部访问
|
||
SUPABASE_PUBLIC_URL=https://api.hbyrkj.top # 公网访问
|
||
```
|
||
|
||
#### 2. URL 转换 (`backend/app/services/storage.py`)
|
||
```python
|
||
def _convert_to_public_url(self, url: str) -> str:
|
||
"""将内部 URL 转换为公网可访问的 URL"""
|
||
if settings.SUPABASE_PUBLIC_URL and settings.SUPABASE_URL:
|
||
internal_url = settings.SUPABASE_URL.rstrip('/')
|
||
public_url = settings.SUPABASE_PUBLIC_URL.rstrip('/')
|
||
return url.replace(internal_url, public_url)
|
||
return url
|
||
```
|
||
|
||
### 结果
|
||
- ✅ 前端获取的 URL 可正常访问
|
||
- ✅ 视频预览和下载功能正常
|
||
|
||
---
|
||
|
||
## ⚡ 发布服务优化 - 本地文件直读 (16:30)
|
||
|
||
### 问题描述
|
||
发布视频时需要先通过 HTTP 下载 Supabase Storage 文件到临时目录,效率低且浪费资源。
|
||
|
||
### 发现
|
||
Supabase Storage 文件实际存储在本地磁盘:
|
||
```
|
||
/home/rongye/ProgramFiles/Supabase/volumes/storage/stub/stub/{bucket}/{path}/{internal_uuid}
|
||
```
|
||
|
||
### 解决方案
|
||
|
||
#### 1. 添加本地路径获取方法 (`storage.py`)
|
||
```python
|
||
SUPABASE_STORAGE_LOCAL_PATH = Path("/home/rongye/ProgramFiles/Supabase/volumes/storage/stub/stub")
|
||
|
||
def get_local_file_path(self, bucket: str, path: str) -> Optional[str]:
|
||
"""获取 Storage 文件的本地磁盘路径"""
|
||
dir_path = SUPABASE_STORAGE_LOCAL_PATH / bucket / path
|
||
if not dir_path.exists():
|
||
return None
|
||
files = list(dir_path.iterdir())
|
||
return str(files[0]) if files else None
|
||
```
|
||
|
||
#### 2. 发布服务优先使用本地文件 (`publish_service.py`)
|
||
```python
|
||
# 解析 URL 获取 bucket 和 path
|
||
match = re.search(r'/storage/v1/object/sign/([^/]+)/(.+?)\?', video_path)
|
||
if match:
|
||
bucket, storage_path = match.group(1), match.group(2)
|
||
local_video_path = storage_service.get_local_file_path(bucket, storage_path)
|
||
|
||
if local_video_path and os.path.exists(local_video_path):
|
||
logger.info(f"[发布] 直接使用本地文件: {local_video_path}")
|
||
else:
|
||
# Fallback: HTTP 下载
|
||
```
|
||
|
||
### 结果
|
||
- ✅ 发布速度显著提升(跳过下载步骤)
|
||
- ✅ 减少临时文件占用
|
||
- ✅ 保留 HTTP 下载作为 Fallback
|
||
|
||
---
|
||
|
||
## 🔧 Supabase Studio 配置 (17:00)
|
||
|
||
### 修改内容
|
||
更新 `/home/rongye/ProgramFiles/Supabase/.env`:
|
||
```ini
|
||
# 修改前
|
||
SUPABASE_PUBLIC_URL=http://localhost:8000
|
||
|
||
# 修改后
|
||
SUPABASE_PUBLIC_URL=https://api.hbyrkj.top
|
||
```
|
||
|
||
### 原因
|
||
通过 `supabase.hbyrkj.top` 公网访问 Studio 时,需要正确的 API 公网地址。
|
||
|
||
### 操作
|
||
```bash
|
||
docker compose restart studio
|
||
```
|
||
|
||
### 待解决
|
||
- 🔄 Studio Settings 页面加载问题(401 Unauthorized)- 可能与 Nginx Basic Auth 配置冲突
|
||
|
||
---
|
||
|
||
## 📁 今日修改文件清单
|
||
|
||
| 文件 | 变更类型 | 说明 |
|
||
|------|----------|------|
|
||
| `backend/app/api/materials.py` | 修改 | 添加用户隔离 |
|
||
| `backend/app/api/videos.py` | 修改 | 添加用户隔离 |
|
||
| `backend/app/services/storage.py` | 修改 | URL转换 + 本地路径获取 |
|
||
| `backend/app/services/publish_service.py` | 修改 | 本地文件直读优化 |
|
||
| `backend/.env` | 修改 | 添加 SUPABASE_PUBLIC_URL |
|
||
| `Supabase/.env` | 修改 | SUPABASE_PUBLIC_URL |
|
||
| `frontend/src/app/page.tsx` | 修改 | 改用后端API上传 |
|
||
|
||
---
|
||
|
||
## 📅 明日任务规划 (Day 12)
|
||
|
||
### 🎯 目标:部署 Qwen3-TTS 0.6B 声音克隆系统
|
||
|
||
**任务背景**:
|
||
- 当前使用 EdgeTTS(微软云端 TTS),音色固定,无法自定义
|
||
- Qwen3-TTS 支持**零样本声音克隆**,可用少量音频克隆任意人声
|
||
|
||
**核心任务**:
|
||
1. **模型部署**
|
||
- 创建独立 Conda 环境 (`qwen-tts`)
|
||
- 下载 Qwen3-TTS 0.6B 模型权重
|
||
- 配置 GPU 推理环境
|
||
|
||
2. **后端集成**
|
||
- 新增 `qwen_tts_service.py` 服务
|
||
- 支持声音克隆:上传参考音频 → 生成克隆语音
|
||
- 兼容现有 `tts_service.py` 接口
|
||
|
||
3. **前端适配**
|
||
- 添加"声音克隆"选项
|
||
- 支持上传参考音频(3-10秒)
|
||
- 音色预览功能
|
||
|
||
**预期成果**:
|
||
- ✅ 用户可上传自己的声音样本
|
||
- ✅ 生成的口播视频使用克隆后的声音
|
||
- ✅ 保留 EdgeTTS 作为备选方案
|
||
|
||
**参考资源**:
|
||
- 模型:[Qwen/Qwen3-TTS-0.6B](https://huggingface.co/Qwen/Qwen3-TTS-0.6B)
|
||
- 显存需求:~4GB (0.6B 参数量)
|