Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8417709f1a | |||
| d161fc95a8 | |||
| 4e807525e9 | |||
| 47bb987e06 | |||
| d7b9c3ac3b | |||
| c46c51170d | |||
| 81de5ab642 | |||
| 1dc7c2d66b | |||
| 534dbd3949 | |||
| ca4a0b1303 | |||
| 3e94013d2b | |||
| 3587559a87 | |||
| af8c483910 | |||
| f109a115d4 | |||
| eb5930e36d | |||
| 5a95987ea0 | |||
| d195bb9f1b | |||
| 6f9e4e3e4e | |||
| e9265049e6 | |||
| 44bda3e67f | |||
| 96c914c321 | |||
| f03a33f8b5 | |||
| f8c3f1b7e5 | |||
| 6175630794 | |||
| 534ffd08b2 | |||
| aa818b75a8 | |||
| 4c2d8404b4 | |||
| 58c1bbc199 | |||
| c5f1098831 | |||
| 11a85bfee7 | |||
| 603650cfb3 | |||
| 15dc5df12c | |||
| 4659f4536e | |||
| 784c4faa55 | |||
| 5b804e9d79 | |||
| 00f0088c2a | |||
| 4a295e6e0d | |||
| 63e0ffeaea | |||
| 2797583d81 |
@@ -31,3 +31,5 @@ tauri-app/src-tauri/binaries/*
|
||||
*test*.key*
|
||||
.atomcode/
|
||||
mixkit_bgm/
|
||||
*.exe
|
||||
*.exe.sig
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
**美家卡智影**是一款面向桌面端的 AI 视频创作应用,采用"Python 后端 API + Tauri 桌面前端"的混合架构。
|
||||
|
||||
- **产品标识**: `cn.meijiaka.ai-video` / `cn.meijiaka.ai-zy`
|
||||
- **版本**: `1.6.5`
|
||||
- **版本**: `1.8.1`
|
||||
- **核心功能**: AI 脚本生成、AI 配音合成(TTS)、声音复刻、视频生成(Vidu)、视频字幕生成、压制成片(FFmpeg)、项目本地持久化
|
||||
|
||||
### 技术栈总览
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
# 用户数据隔离改造方案
|
||||
|
||||
## 背景
|
||||
|
||||
当前所有本地数据存储在全局路径下,切换账号后 A 用户的数据对 B 用户可见。涉及隐私(音色素材)和商业数据(项目文件),需要按 `user_id` 隔离。
|
||||
|
||||
## 目标
|
||||
|
||||
- 按登录用户的 `user_id` 隔离本地数据
|
||||
- 未登录时使用匿名沙盒或禁止写入
|
||||
- 兼容旧数据,首次启动自动迁移
|
||||
|
||||
## 改动范围
|
||||
|
||||
### 1. Rust 层 — 路径改造(`src/storage/paths.rs`)
|
||||
|
||||
新增用户隔离根目录,用户相关数据全部迁移:
|
||||
|
||||
```
|
||||
{app_local_data_dir}/
|
||||
users/
|
||||
{user_id}/
|
||||
voices.json ← 音色素材库
|
||||
cover_avatars.json ← 封面头像库
|
||||
projects/ ← 项目目录
|
||||
config.json ← 用户级配置(可选)
|
||||
bgm_cache/ ← 全局共享(缓存无隐私问题)
|
||||
config.json ← 全局配置(窗口状态、偏好设置等)
|
||||
temp/ ← 临时文件(定时清理)
|
||||
```
|
||||
|
||||
**需要修改的函数:**
|
||||
|
||||
| 当前路径 | 改造后 |
|
||||
|---|---|
|
||||
| `{data}/voices.json` | `{data}/users/{user_id}/voices.json` |
|
||||
| `{data}/cover_avatars.json` | `{data}/users/{user_id}/cover_avatars.json` |
|
||||
| `{data}/cover_avatars/` | `{data}/users/{user_id}/cover_avatars/` |
|
||||
| `{data}/projects/` | `{data}/users/{user_id}/projects/` |
|
||||
| `{data}/config.json` | 保持全局(或拆分为全局+用户级) |
|
||||
| `{data}/bgm_cache/` | 保持全局 |
|
||||
| `{config}/auth.json` | 保持全局(只存当前登录态) |
|
||||
|
||||
**新增:**
|
||||
- `get_current_user_id() -> Option<String>`:从全局状态或调用方获取当前 user_id
|
||||
- `get_user_data_dir(user_id: &str)`:返回用户隔离目录
|
||||
|
||||
### 2. Rust 层 — 存储命令改造
|
||||
|
||||
**受影响的命令文件:**
|
||||
- `src/commands/voice.rs` — load/save/delete voice materials
|
||||
- `src/commands/project.rs` — 项目 CRUD
|
||||
- `src/commands/avatar.rs` — 封面头像
|
||||
- `src/commands/config.rs` — 配置(决定全局 or 用户级)
|
||||
|
||||
**改造方式:**
|
||||
|
||||
方式 A(推荐):命令函数签名增加 `user_id: String` 参数
|
||||
```rust
|
||||
#[tauri::command]
|
||||
pub async fn load_voice_materials(user_id: String) -> ApiResponse<Vec<VoiceMaterial>> {
|
||||
let path = get_user_voices_json_path(&user_id)?;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
方式 B:命令内部从 AppHandle 全局状态读取当前 user_id
|
||||
```rust
|
||||
let user_id = app.state::<CurrentUser>().id.clone();
|
||||
```
|
||||
|
||||
**推荐方式 A**,前端调用时传入 `user.id`,减少 Rust 层对全局状态的依赖,更明确。
|
||||
|
||||
### 3. 前端 — AuthStore 改造(`src/store/authStore.ts`)
|
||||
|
||||
**新增:**
|
||||
- 登录成功后,写入一个全局标记 `CURRENT_USER_ID = user.id`
|
||||
- 登出/被踢时,清除该标记
|
||||
- 所有调用 Rust 存储命令的地方,带上 `userId` 参数
|
||||
|
||||
**受影响的 Store:**
|
||||
- `voiceStore.ts` — `loadVoiceMaterials()`、`addVoiceMaterial()` 等需要传 `userId`
|
||||
- `projectStore.ts` — 项目操作需要传 `userId`
|
||||
- `avatarStore.ts` — 封面头像操作需要传 `userId`
|
||||
|
||||
### 4. 前端 — API 调用改造
|
||||
|
||||
所有 `invoke('xxx', { ... })` 调用用户相关命令时,增加 `userId`:
|
||||
|
||||
```typescript
|
||||
// 改造前
|
||||
await invoke('load_voice_materials')
|
||||
|
||||
// 改造后
|
||||
const userId = useAuthStore.getState().user?.id;
|
||||
await invoke('load_voice_materials', { userId })
|
||||
```
|
||||
|
||||
### 5. 数据迁移策略
|
||||
|
||||
**检测时机:** 应用启动时,`init_app_data_dir` 之后
|
||||
|
||||
**迁移逻辑(Rust):**
|
||||
```rust
|
||||
pub fn migrate_legacy_data(data_dir: &Path) -> Result<(), StorageError> {
|
||||
// 1. 检查是否存在旧数据(全局 voices.json / projects / cover_avatars.json)
|
||||
let legacy_voices = data_dir.join("voices.json");
|
||||
if !legacy_voices.exists() {
|
||||
return Ok(()); // 无旧数据,跳过
|
||||
}
|
||||
|
||||
// 2. 读取 auth.json 获取当前登录的 user_id
|
||||
let auth_path = get_auth_state_path(app)?;
|
||||
let auth: Option<AuthState> = read_json(&auth_path)?;
|
||||
let user_id = match auth.and_then(|a| a.user).map(|u| u.id) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
// 未登录但有旧数据:移到 anonymous/ 目录,提示用户登录后迁移
|
||||
return move_to_anonymous(data_dir);
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 创建用户目录并迁移
|
||||
let user_dir = data_dir.join("users").join(&user_id);
|
||||
ensure_dir(&user_dir)?;
|
||||
|
||||
// 迁移 voices.json
|
||||
if legacy_voices.exists() {
|
||||
fs::rename(&legacy_voices, user_dir.join("voices.json"))?;
|
||||
}
|
||||
|
||||
// 迁移 cover_avatars.json + cover_avatars/
|
||||
let legacy_avatars = data_dir.join("cover_avatars.json");
|
||||
if legacy_avatars.exists() {
|
||||
fs::rename(&legacy_avatars, user_dir.join("cover_avatars.json"))?;
|
||||
}
|
||||
let legacy_avatars_dir = data_dir.join("cover_avatars");
|
||||
if legacy_avatars_dir.exists() {
|
||||
fs::rename(&legacy_avatars_dir, user_dir.join("cover_avatars"))?;
|
||||
}
|
||||
|
||||
// 迁移 projects/
|
||||
let legacy_projects = data_dir.join("projects");
|
||||
if legacy_projects.exists() {
|
||||
fs::rename(&legacy_projects, user_dir.join("projects"))?;
|
||||
}
|
||||
|
||||
// 4. 写迁移标记,避免重复迁移
|
||||
let flag = data_dir.join(".migration_v1_done");
|
||||
fs::write(&flag, "done")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**迁移顺序:**
|
||||
1. 启动应用
|
||||
2. 初始化 `app_local_data_dir`
|
||||
3. 检测 `.migration_v1_done` 标记,不存在则执行迁移
|
||||
4. 迁移完成后继续正常启动
|
||||
|
||||
## 工作量评估
|
||||
|
||||
| 模块 | 文件数 | 预估工作量 |
|
||||
|---|---|---|
|
||||
| paths.rs 改造 | 1 | 2h |
|
||||
| Rust 存储命令加 user_id 参数 | ~8 个文件 | 4h |
|
||||
| lib.rs 注册新签名 | 1 | 0.5h |
|
||||
| 前端 Store 改造 | ~4 个文件 | 3h |
|
||||
| 数据迁移逻辑 | 1 个新文件 | 3h |
|
||||
| 测试验证 | — | 2h |
|
||||
| **总计** | | **~14.5h** |
|
||||
|
||||
## 风险点
|
||||
|
||||
1. **多设备登录同一账号**:数据只在本地隔离,不同设备间数据不互通。如果需要跨设备同步,需要后端支持。
|
||||
2. **匿名数据**:未登录时产生的数据(理论上目前不存在,因为功能都需要登录),需要决定是禁止未登录操作还是存到 `anonymous/` 目录。
|
||||
3. **回滚**:迁移是单向的(移动文件),回滚需要手动恢复。建议迁移前做备份副本而非直接 move。
|
||||
|
||||
## 建议实施时机
|
||||
|
||||
- **v1.8.0** 或 **v1.9.0** 版本中实施
|
||||
- 最好在素材库数据量还不大的早期做,迁移成本低
|
||||
@@ -0,0 +1,259 @@
|
||||
# 用户数据隔离方案评审报告
|
||||
|
||||
## 结论
|
||||
|
||||
**方案大方向正确,但存在 3 个关键设计缺陷必须修正**,修正后可用。
|
||||
|
||||
---
|
||||
|
||||
## 关键缺陷
|
||||
|
||||
### ❌ 缺陷 1:前端传 userId 到 Rust 命令 — 安全漏洞
|
||||
|
||||
**方案原文:**
|
||||
> 方式 A(推荐):命令函数签名增加 `user_id: String` 参数
|
||||
> 前端调用时传入 `user.id`
|
||||
|
||||
**问题:** Tauri `#[tauri::command]` 的参数由前端传入,前端可以伪造任意 `userId`。
|
||||
|
||||
```typescript
|
||||
// 恶意前端代码:A 用户传入 B 的 userId,即可读取 B 的本地数据
|
||||
await invoke('load_voice_materials', { userId: 'B的userId' })
|
||||
```
|
||||
|
||||
**这完全破坏了隔离的安全性。**
|
||||
|
||||
**修正方案(推荐方式 B):**
|
||||
|
||||
Rust 命令从已认证的 `auth.json` 中读取当前 `user.id`,不接收前端传来的 `userId`:
|
||||
|
||||
```rust
|
||||
use tauri::State;
|
||||
|
||||
// 全局状态存储当前用户
|
||||
pub struct CurrentUser {
|
||||
pub id: std::sync::Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_voice_materials(
|
||||
app: tauri::AppHandle,
|
||||
) -> ApiResponse<Vec<VoiceMaterial>> {
|
||||
// 从 auth.json 读取已认证的 user.id
|
||||
let auth: Option<AuthState> = crate::storage::auth::load_auth_state(&app)?;
|
||||
let user_id = match auth.and_then(|a| a.user).map(|u| u.id) {
|
||||
Some(id) => id,
|
||||
None => return ApiResponse::err(401, "未登录"),
|
||||
};
|
||||
|
||||
let path = get_user_voices_json_path(&user_id)?;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
或更简洁:在 `lib.rs` 的 `setup` 中读取 `auth.json`,将 `user_id` 存入 `ManagedState`,所有命令通过 `app.state::<CurrentUser>()` 获取。
|
||||
|
||||
**前端完全不需要传 `userId`,Rust 层自治。**
|
||||
|
||||
---
|
||||
|
||||
### ❌ 缺陷 2:迁移用 `fs::rename`(move)— 不可逆且易崩溃
|
||||
|
||||
**方案原文:**
|
||||
```rust
|
||||
fs::rename(&legacy_voices, user_dir.join("voices.json"))?;
|
||||
```
|
||||
|
||||
**问题:**
|
||||
- `rename` 是原子操作,但**多个文件的迁移不是原子的**
|
||||
- 如果迁移 `voices.json` 后、迁移 `projects/` 前进程崩溃,会留下半迁移状态
|
||||
- 不可逆,回滚困难
|
||||
|
||||
**修正方案:**
|
||||
|
||||
```rust
|
||||
pub fn migrate_legacy_data(data_dir: &Path, app: &AppHandle) -> Result<(), StorageError> {
|
||||
// 1. 检查迁移标记
|
||||
let flag = data_dir.join(".migration_v1_done");
|
||||
if flag.exists() { return Ok(()); }
|
||||
|
||||
// 2. 检查是否有旧数据
|
||||
let legacy_items = vec![
|
||||
("voices.json", false),
|
||||
("cover_avatars.json", false),
|
||||
("cover_avatars/", true),
|
||||
("projects/", true),
|
||||
];
|
||||
let has_legacy = legacy_items.iter().any(|(name, _)| data_dir.join(name).exists());
|
||||
if !has_legacy {
|
||||
// 无旧数据,直接写标记跳过
|
||||
fs::write(&flag, "none")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 3. 获取当前登录用户
|
||||
let auth: Option<AuthState> = crate::storage::auth::load_auth_state(app)?;
|
||||
let user_id = match auth.and_then(|a| a.user).map(|u| u.id) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
// 未登录但有旧数据:复制到 anonymous/,登录后再手动迁移
|
||||
let anon_dir = data_dir.join("anonymous");
|
||||
ensure_dir(&anon_dir)?;
|
||||
for (name, is_dir) in &legacy_items {
|
||||
let src = data_dir.join(name);
|
||||
if src.exists() {
|
||||
let dest = anon_dir.join(name);
|
||||
if *is_dir {
|
||||
copy_dir_all(&src, &dest)?;
|
||||
} else {
|
||||
fs::copy(&src, &dest)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs::write(&flag, "anonymous")?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 创建用户目录,用 copy 而非 move
|
||||
let user_dir = data_dir.join("users").join(&user_id);
|
||||
ensure_dir(&user_dir)?;
|
||||
|
||||
for (name, is_dir) in &legacy_items {
|
||||
let src = data_dir.join(name);
|
||||
let dest = user_dir.join(name);
|
||||
if src.exists() {
|
||||
if *is_dir {
|
||||
copy_dir_all(&src, &dest)?;
|
||||
} else {
|
||||
fs::copy(&src, &dest)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. copy 成功后,再写标记,然后延迟清理旧数据
|
||||
fs::write(&flag, user_id)?;
|
||||
|
||||
// 可选:启动后台线程,延迟 7 天后删除旧数据
|
||||
// 给用户一个回滚窗口
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ 缺陷 3:缺少切换用户时的热切换设计
|
||||
|
||||
**当前问题:**
|
||||
- 切换账号后,前端 Zustand store 中仍残留上一个用户的数据
|
||||
- 需要页面刷新或重启应用才能加载新用户的数据
|
||||
|
||||
**修正方案:**
|
||||
|
||||
在 `authStore.logout()` 和 `authStore.login()` 中增加**数据刷新契约**:
|
||||
|
||||
```typescript
|
||||
// authStore.ts
|
||||
logout: async () => {
|
||||
// ... 原有逻辑 ...
|
||||
|
||||
// 新增:清空所有用户相关的内存状态
|
||||
useVoiceStore.getState().reset();
|
||||
useProjectStore.getState().reset();
|
||||
useAvatarStore.getState().reset();
|
||||
|
||||
// 通知 Rust 层清空缓存
|
||||
await invoke('switch_user', { userId: null });
|
||||
},
|
||||
|
||||
login: async (phone, code) => {
|
||||
// ... 原有登录逻辑 ...
|
||||
|
||||
// 新增:通知 Rust 层切换数据目录
|
||||
await invoke('switch_user', { userId: data.user.id });
|
||||
|
||||
// 重新加载当前用户的数据
|
||||
await useVoiceStore.getState().loadMaterials();
|
||||
await useProjectStore.getState().loadProjects();
|
||||
},
|
||||
```
|
||||
|
||||
Rust 层 `switch_user` 命令:
|
||||
```rust
|
||||
#[tauri::command]
|
||||
pub async fn switch_user(
|
||||
app: tauri::AppHandle,
|
||||
user_id: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
// 更新 ManagedState 中的当前 user_id
|
||||
let state = app.state::<CurrentUser>();
|
||||
let mut guard = state.id.lock().map_err(|e| e.to_string())?;
|
||||
*guard = user_id;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 次要问题
|
||||
|
||||
### ⚠️ `sanitize_id` 对 user_id 的兼容性
|
||||
|
||||
当前 `sanitize_id` 只允许 `a-zA-Z0-9_-`。测试确认:
|
||||
- UUID 格式(`550e8400-e29b-41d4-a716-446655440000`):✅ 通过
|
||||
- 邮箱格式(`user@example.com`):❌ 失败
|
||||
- 含点号(`user.name`):❌ 失败
|
||||
|
||||
**建议:** 确认后端 `user.id` 的格式。如果是 UUID,无需改动;如果是邮箱或其他格式,需要对 `user_id` 单独做路径净化(如用 `sanitize_filename` 或直接 base64/urlencode)。
|
||||
|
||||
### ⚠️ `temp/` 目录也应隔离
|
||||
|
||||
当前 `extract_audio_from_video` 写到 `app_local_data_dir/temp/`,如果多用户并发操作会冲突。建议:
|
||||
```
|
||||
{app_local_data_dir}/temp/{user_id}/
|
||||
```
|
||||
|
||||
### ⚠️ 全局 `config.json` 的拆分
|
||||
|
||||
当前 `config.json` 存的是 API 地址和环境配置(`production`/`debug`),属于设备级配置,保持全局合理。
|
||||
|
||||
但如果以后增加用户偏好(主题、音量、快捷键),需要拆分为:
|
||||
- `config.json` — 设备级(全局)
|
||||
- `users/{user_id}/preferences.json` — 用户级
|
||||
|
||||
---
|
||||
|
||||
## 与主流规范对比
|
||||
|
||||
| 应用 | 隔离方式 | 说明 |
|
||||
|---|---|---|
|
||||
| **微信/QQ 桌面端** | ✅ 按账号隔离目录 | 每个账号独立数据目录,切换账号需重启 |
|
||||
| **Discord/Slack** | ❌ 不隔离 | 完全云端,本地只有缓存 |
|
||||
| **VS Code** | ⚠️ 按 workspace | 不严格按用户,但 settings.json 可放用户目录 |
|
||||
| **Figma** | ❌ 不隔离 | 完全云端 |
|
||||
| **Adobe Creative Cloud** | ✅ 按账号隔离 | 登录后数据按 Creative Cloud 账号隔离 |
|
||||
|
||||
**本项目定位:** 本地创作工具,数据以本地为主,且支持多设备登录(手机号+验证码)。
|
||||
|
||||
**结论:** 按 `user_id` 隔离是**合理且必要的**,尤其在涉及**声音复刻素材**这种高敏感隐私数据时。
|
||||
|
||||
---
|
||||
|
||||
## 修正后的工作量
|
||||
|
||||
| 模块 | 修正后工作量 | 说明 |
|
||||
|---|---|---|
|
||||
| `paths.rs` 改造 | 2h | 加 `users/{user_id}/` 前缀 |
|
||||
| `ManagedState` + `switch_user` | 2h | 新增当前用户状态管理 |
|
||||
| Rust 命令读 auth.json 获取 user_id | 4h | 替代前端传参方案 |
|
||||
| 前端 Store 增加 `reset()` + 登录后重载 | 2h | 热切换体验 |
|
||||
| 数据迁移(copy + 标记) | 3h | 安全迁移 |
|
||||
| 测试验证 | 2h | |
|
||||
| **总计** | **~15h** | 比原方案多 0.5h(主要在 State 管理) |
|
||||
|
||||
---
|
||||
|
||||
## 建议实施时机
|
||||
|
||||
**v1.8.0** 实施,趁数据量小、迁移成本低时做。
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# === 基础配置 ===
|
||||
APP_NAME=美家卡智影 API
|
||||
APP_VERSION=1.6.5
|
||||
APP_VERSION=1.8.1
|
||||
# ⚠️ 生产环境必须设为 false
|
||||
DEBUG=true
|
||||
ENV=development
|
||||
@@ -79,4 +79,4 @@ SMS_BASE_URL=https://bjksmtn.b2m.cn/inter/sendSingleSMS
|
||||
|
||||
# === 日志配置 ===
|
||||
# 生产环境建议 INFO
|
||||
LOG_LEVEL=DEBUG
|
||||
LOG_LEVEL=ERROR
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"http:Pn60lJXcaOGKvMjn5qv-OMr7wR1lp1p8QG7Ul6NK:media-liche": {"upHosts": ["http://upload-z2.qiniup.com", "http://up-z2.qiniup.com"], "ioHosts": ["http://iovip-z2.qbox.me"], "rsHosts": ["http://rs-z2.qbox.me"], "rsfHosts": ["http://rsf-z2.qbox.me"], "apiHosts": ["http://api-z2.qiniu.com"], "deadline": 1778077071}, "http:Pn60lJXcaOGKvMjn5qv-OMr7wR1lp1p8QG7Ul6NK:img-liche": {"upHosts": ["http://upload-z2.qiniup.com", "http://up-z2.qiniup.com"], "ioHosts": ["http://iovip-z2.qbox.me"], "rsHosts": ["http://rs-z2.qbox.me"], "rsfHosts": ["http://rsf-z2.qbox.me"], "apiHosts": ["http://api-z2.qiniu.com"], "deadline": 1776433218}}
|
||||
{"http:Pn60lJXcaOGKvMjn5qv-OMr7wR1lp1p8QG7Ul6NK:media-liche": {"upHosts": ["http://upload-z2.qiniup.com", "http://up-z2.qiniup.com"], "ioHosts": ["http://iovip-z2.qbox.me"], "rsHosts": ["http://rs-z2.qbox.me"], "rsfHosts": ["http://rsf-z2.qbox.me"], "apiHosts": ["http://api-z2.qiniu.com"], "deadline": 1780541956}, "http:Pn60lJXcaOGKvMjn5qv-OMr7wR1lp1p8QG7Ul6NK:img-liche": {"upHosts": ["http://upload-z2.qiniup.com", "http://up-z2.qiniup.com"], "ioHosts": ["http://iovip-z2.qbox.me"], "rsHosts": ["http://rs-z2.qbox.me"], "rsfHosts": ["http://rsf-z2.qbox.me"], "apiHosts": ["http://api-z2.qiniu.com"], "deadline": 1776433218}}
|
||||
@@ -46,4 +46,4 @@ COPY pyproject.toml .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]
|
||||
|
||||
@@ -218,24 +218,35 @@ class ViduAdapter(PlatformAdapter, SyncCapable, TaskCapable, CallbackCapable):
|
||||
callback_url: str | None = None,
|
||||
) -> bool:
|
||||
"""验证 Vidu 回调 HMAC-SHA256 签名"""
|
||||
signature = headers.get("X-HMAC-SIGNATURE")
|
||||
algorithm = headers.get("X-HMAC-ALGORITHM")
|
||||
access_key = headers.get("X-HMAC-ACCESS-KEY")
|
||||
signed_headers_str = headers.get("X-HMAC-SIGNED-HEADERS")
|
||||
date = headers.get("Date")
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# HTTP 头大小写不敏感:建立小写 key 的查找表
|
||||
headers_lower = {k.lower(): v for k, v in headers.items()}
|
||||
|
||||
signature = headers_lower.get("x-hmac-signature")
|
||||
algorithm = headers_lower.get("x-hmac-algorithm")
|
||||
access_key = headers_lower.get("x-hmac-access-key")
|
||||
signed_headers_str = headers_lower.get("x-hmac-signed-headers")
|
||||
date = headers_lower.get("date")
|
||||
|
||||
if not all([signature, algorithm, access_key, signed_headers_str, date]):
|
||||
logger.warning(f"[Vidu] 签名验证失败: 缺少必要头, headers={list(headers.keys())}")
|
||||
return False
|
||||
if algorithm != "hmac-sha256":
|
||||
logger.warning(f"[Vidu] 签名验证失败: 不支持的算法 {algorithm}")
|
||||
return False
|
||||
if access_key != "vidu":
|
||||
logger.warning(f"[Vidu] 签名验证失败: access_key 不匹配 {access_key}")
|
||||
return False
|
||||
|
||||
header_names = [h.strip() for h in signed_headers_str.split(";") if h.strip()]
|
||||
header_values: dict[str, str] = {}
|
||||
for name in header_names:
|
||||
value = headers.get(name)
|
||||
# 签名头名也可能大小写不一致,统一用小写查找
|
||||
value = headers_lower.get(name.lower())
|
||||
if value is None:
|
||||
logger.warning(f"[Vidu] 签名验证失败: 缺少签名头 {name}")
|
||||
return False
|
||||
header_values[name] = value
|
||||
|
||||
@@ -258,7 +269,15 @@ class ViduAdapter(PlatformAdapter, SyncCapable, TaskCapable, CallbackCapable):
|
||||
hmac.new(secret.encode("utf-8"), signing_string.encode("utf-8"), hashlib.sha256).digest()
|
||||
).decode("utf-8")
|
||||
|
||||
return hmac.compare_digest(signature, expected)
|
||||
if not hmac.compare_digest(signature, expected):
|
||||
logger.warning(
|
||||
f"[Vidu] 签名验证失败: callback_url={callback_url}, "
|
||||
f"signing_string={repr(signing_string)}, "
|
||||
f"expected={expected[:20]}..., received={signature[:20]}..."
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def verify_nonce(
|
||||
self,
|
||||
|
||||
@@ -11,8 +11,11 @@ Prompt 模板系统
|
||||
# 获取分类列表
|
||||
categories = list_categories()
|
||||
|
||||
# 加载 System Prompt(大类+小类,随机取一个)
|
||||
system = load_system_prompt("bk", "ht")
|
||||
# 扫描某分类下的所有提示词文件
|
||||
files = list_prompt_files("bk")
|
||||
|
||||
# 加载指定文件的 System Prompt
|
||||
system = load_prompt_file("bk", "水电改造避坑——水电改造的4个坑.txt")
|
||||
|
||||
# 加载并渲染 User Prompt
|
||||
user = load_script_user_prompt(
|
||||
@@ -24,7 +27,9 @@ from .loader import (
|
||||
PolishPromptBuilder,
|
||||
ScriptPromptBuilder,
|
||||
list_categories,
|
||||
list_prompt_files,
|
||||
load_prompt,
|
||||
load_prompt_file,
|
||||
load_script_user_prompt,
|
||||
load_system_prompt,
|
||||
render_template,
|
||||
@@ -36,6 +41,8 @@ __all__ = [
|
||||
"load_system_prompt",
|
||||
"load_script_user_prompt",
|
||||
"list_categories",
|
||||
"list_prompt_files",
|
||||
"load_prompt_file",
|
||||
"ScriptPromptBuilder",
|
||||
"PolishPromptBuilder",
|
||||
]
|
||||
|
||||
@@ -77,18 +77,13 @@ def list_categories() -> list[dict]:
|
||||
"""
|
||||
返回所有分类结构
|
||||
|
||||
以 system/_meta.json 为准,只返回配置中定义的分类。
|
||||
count 动态扫描对应目录统计。
|
||||
以 system/_meta.json 为准,只返回配置中定义的大类。
|
||||
|
||||
Returns:
|
||||
[
|
||||
{
|
||||
"code": "bk",
|
||||
"name": "装修避坑",
|
||||
"subcategories": [
|
||||
{"code": "ht", "name": "装修合同避坑", "count": 2},
|
||||
...
|
||||
]
|
||||
"name": "装修避坑"
|
||||
},
|
||||
...
|
||||
]
|
||||
@@ -99,64 +94,84 @@ def list_categories() -> list[dict]:
|
||||
for cat_meta in meta.get("categories", []):
|
||||
cat_code = cat_meta["code"]
|
||||
cat_name = cat_meta.get("name", cat_code)
|
||||
cat_dir = SYSTEM_PROMPTS_DIR / cat_code
|
||||
|
||||
subcategories = []
|
||||
for sub_meta in cat_meta.get("subcategories", []):
|
||||
sub_code = sub_meta["code"]
|
||||
sub_name = sub_meta.get("name", sub_code)
|
||||
sub_dir = cat_dir / sub_code
|
||||
|
||||
# 统计提示词文件数量
|
||||
count = 0
|
||||
if sub_dir.exists():
|
||||
count = len([
|
||||
f for f in sub_dir.iterdir()
|
||||
if f.is_file() and f.suffix == ".txt"
|
||||
])
|
||||
|
||||
subcategories.append({
|
||||
"code": sub_code,
|
||||
"name": sub_name,
|
||||
"count": count,
|
||||
})
|
||||
|
||||
categories.append({
|
||||
"code": cat_code,
|
||||
"name": cat_name,
|
||||
"subcategories": subcategories,
|
||||
})
|
||||
|
||||
return categories
|
||||
|
||||
|
||||
def load_system_prompt(category: str, subcategory: str) -> str:
|
||||
def list_prompt_files(category: str) -> list[dict]:
|
||||
"""
|
||||
根据大类+小类随机加载一个 System Prompt
|
||||
扫描指定分类目录下的所有提示词文件
|
||||
|
||||
文件名格式: 文案——描述.txt
|
||||
解析为 label + desc 返回给前端展示。
|
||||
|
||||
Args:
|
||||
category: 大类代码,如 "bk"
|
||||
subcategory: 小类代码,如 "ht"
|
||||
|
||||
Returns:
|
||||
随机选中的提示词内容,未找到返回空字符串
|
||||
[
|
||||
{
|
||||
"filename": "水电改造避坑——水电改造的4个坑.txt",
|
||||
"label": "水电改造避坑",
|
||||
"desc": "水电改造的4个坑"
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
sub_dir = SYSTEM_PROMPTS_DIR / category / subcategory
|
||||
if not sub_dir.exists():
|
||||
cat_dir = SYSTEM_PROMPTS_DIR / category
|
||||
if not cat_dir.exists():
|
||||
return []
|
||||
|
||||
files = []
|
||||
for f in sorted(cat_dir.iterdir()):
|
||||
if f.is_file() and f.suffix == ".txt":
|
||||
name = f.stem # 不含 .txt
|
||||
if "——" in name:
|
||||
label, desc = name.split("——", 1)
|
||||
else:
|
||||
label = name
|
||||
desc = ""
|
||||
files.append({
|
||||
"filename": f.name,
|
||||
"label": label.strip(),
|
||||
"desc": desc.strip(),
|
||||
})
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def load_prompt_file(category: str, filename: str) -> str:
|
||||
"""
|
||||
加载指定分类下的指定提示词文件
|
||||
|
||||
Args:
|
||||
category: 大类代码,如 "bk"
|
||||
filename: 文件名,如 "水电改造避坑——水电改造的4个坑.txt"
|
||||
|
||||
Returns:
|
||||
提示词内容,文件不存在返回空字符串
|
||||
"""
|
||||
file_path = SYSTEM_PROMPTS_DIR / category / filename
|
||||
if file_path.exists():
|
||||
return file_path.read_text(encoding="utf-8")
|
||||
return ""
|
||||
|
||||
|
||||
def load_system_prompt(category: str, subcategory: str) -> str:
|
||||
"""
|
||||
【已废弃】根据大类+小类随机加载一个 System Prompt
|
||||
|
||||
保留兼容旧调用,实际行为改为从平铺文件中随机加载。
|
||||
"""
|
||||
files = list_prompt_files(category)
|
||||
if not files:
|
||||
return ""
|
||||
|
||||
# 收集所有提示词文件
|
||||
prompt_files = [
|
||||
f for f in sub_dir.iterdir()
|
||||
if f.is_file() and f.suffix == ".txt"
|
||||
]
|
||||
|
||||
if not prompt_files:
|
||||
return ""
|
||||
|
||||
# 随机取一个提示词模板(非安全场景)
|
||||
chosen = random.choice(prompt_files) # nosec: B311
|
||||
return chosen.read_text(encoding="utf-8")
|
||||
chosen = random.choice(files) # nosec: B311
|
||||
return load_prompt_file(category, chosen["filename"])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,20 +2,7 @@
|
||||
"categories": [
|
||||
{
|
||||
"code": "bk",
|
||||
"name": "装修避坑",
|
||||
"subcategories": [
|
||||
{ "code": "ht", "name": "装修合同" },
|
||||
{ "code": "lc", "name": "装修流程" },
|
||||
{ "code": "bj", "name": "装修报价" },
|
||||
{ "code": "qw", "name": "全屋定制" },
|
||||
{ "code": "sd", "name": "水电改造" },
|
||||
{ "code": "wt", "name": "常见问题" },
|
||||
{ "code": "wg", "name": "瓦工铺贴" },
|
||||
{ "code": "yg", "name": "油工进场" },
|
||||
{ "code": "cl", "name": "装修材料" },
|
||||
{ "code": "jg", "name": "装修监工" },
|
||||
{ "code": "sq", "name": "装修省钱" }
|
||||
]
|
||||
"name": "装修避坑"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
你是一位专业的【口播类短视频】脚本创作专家,专注于家装 / 装修领域的抖音 / 视频号口播内容创作。
|
||||
【核心定位与脚本类型】
|
||||
(一)核心定位
|
||||
精准锁定:准备装修、不懂各品类主材怎么选品牌、怕选错质量差被坑的装修业主,围绕装修 12 大类主材靠谱品牌推荐创作,每次生成随机打乱 12 条品类顺序重新编排,保留原意不变。
|
||||
(二)脚本类型
|
||||
装修口播短视频脚本,无多余开篇引入,直接进入正文主材品牌推荐,正文干货 + 结尾资料领取引导,无多余内容、无重复冗余。
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
无开头范式,去掉所有铺垫引入话术,直接切入各主材品牌推荐正文。
|
||||
中间核心(12 大类主材品牌文案可微调口语化,保持原意不变,每次自动随机打乱重新编排顺序):
|
||||
电线优选:熊猫、远东、德力西三大靠谱品牌。
|
||||
防水选材认准:德高、雨虹、科顺主流大品牌。
|
||||
家装水管优先选:金牛、伟星、日丰口碑款。
|
||||
开关面板推荐:公牛、施耐德、西门子放心选。
|
||||
腻子粉首选:立邦、美巢、德高环保大品牌。
|
||||
家装水泥认准:海螺、红石、中联品质有保障。
|
||||
厨卫五金优选:汉斯格雅、科勒、九牧一线品牌。
|
||||
木地板推荐:圣象、大自然、生活家主流大牌。
|
||||
石膏板选材:龙牌、泰山、可耐福家装常用款。
|
||||
瓷砖胶认准:德高、大禹、神工粘结更牢固。
|
||||
乳胶漆优选:立邦、多乐士、三棵树环保净味。
|
||||
玻璃胶选用:瓦克、西卡、百得防霉耐用款。
|
||||
(备注:完整保留每类主材对应的三个品牌,仅微调句式适配口播;每次生成自动随机打乱 12 个品类排序,不改变品牌名单和推荐原意)
|
||||
中间核心详细分析(贴合口播逻辑,适配业主痛点,不篡改原文核心)
|
||||
排序逻辑:内置 12 大类装修主材固定推荐品牌,每次生成脚本自动随机打乱重新排序,不固定原有顺序,避免内容同质化,适合日常短视频日更。
|
||||
文案调整要求:仅做口语化精简优化,把直白问句改成顺口口播表述,不替换、不删减任何品牌,保持每类主材三个推荐品牌完整不变,原意丝毫不改。
|
||||
字数与时长控制:纯文字 + 数字扣除标点,严格控制在 170-190 字,按每秒 4 个字计算,对应时长 42.5-47.5s,内容精炼、节奏紧凑,适配短平快知识口播。
|
||||
内容适配性:打乱顺序后语句依然衔接自然,每条独立清晰,直接给到可照搬的主材品牌清单,解决业主选材纠结、怕踩坑的核心痛点,实用性拉满。
|
||||
结尾范式:完整保留原文结尾引导原话,仅可轻微优化口语流畅度,不改动评论区扣关键词、领取材料推荐清单的核心引流逻辑。
|
||||
【开篇 & 语言要求】
|
||||
无开篇铺垫,直接切入主材品牌推荐干货;全程短句口语化、接地气,直白罗列品牌,简单好记、业主可直接收藏对照选材。
|
||||
可微调句式语序,严禁替换、删减任意主材品牌,不改变推荐逻辑和原意,语句简短利落,适配短时长口播节奏。
|
||||
【内置固定原文案】
|
||||
电线买谁家?熊猫、远东、德力西。
|
||||
防水买谁家?德高、雨虹、科顺。
|
||||
水管买谁家?金牛、伟星、日丰。
|
||||
开关买谁家?公牛、施耐德、西门子。
|
||||
腻子粉买谁家?立邦、美巢、德高。
|
||||
水泥买谁家?海螺、红石、中联。
|
||||
五金买谁家?汉斯格雅、科勒、九牧。
|
||||
木地板买谁家?圣象、大自然、生活家。
|
||||
石膏板买谁家?龙牌、泰山、可耐福。
|
||||
瓷砖胶买谁家?德高、大禹、神工。
|
||||
乳胶漆买谁家?立邦、多乐士、三棵树。
|
||||
玻璃胶买谁家?瓦克、西卡、百得。
|
||||
记不住的,我这里有材料推荐清单,评论区扣材料,直接拿走。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
原始门窗原貌-毛坯基础
|
||||
厨卫原始毛坯状态-毛坯基础
|
||||
地面原始水泥基层-毛坯基础
|
||||
客厅原始墙面-毛坯基础
|
||||
强弱电箱原始特写-毛坯基础
|
||||
毛坯全屋广角全景-毛坯基础
|
||||
阳台原始结构空镜-毛坯基础
|
||||
墙面点位弹线-现场交底
|
||||
开关插座定位-现场交底
|
||||
开工仪式简单镜头-现场交底
|
||||
施工方案现场讲解-现场交底
|
||||
甲乙工长三方对接-现场交底
|
||||
给排水点位标记-现场交底
|
||||
装修合同核对-现场交底
|
||||
卧室原始状态-翻新基础
|
||||
厨卫原始状态-翻新基础
|
||||
客厅原始状态-翻新基础
|
||||
卷尺实测尺寸-量房勘测
|
||||
手绘户型草图-量房勘测
|
||||
激光水平仪测量-量房勘测
|
||||
电脑户型图制作-量房勘测
|
||||
设计师入户-量房勘测
|
||||
全屋地板铺设施工-主材安装
|
||||
全屋开关面板安装-主材安装
|
||||
卫浴洁具进场安装-主材安装
|
||||
厨卫集成吊顶安装-主材安装
|
||||
室内房门安装固定-主材安装
|
||||
橱柜柜体现场组装-主材安装
|
||||
灯具筒灯射灯安装-主材安装
|
||||
衣柜移门五金安装-主材安装
|
||||
全屋五金调试-收尾细节
|
||||
成品瑕疵修补-收尾细节
|
||||
柜体门缝调整-收尾细节
|
||||
门窗缝隙密封处理-收尾细节
|
||||
全屋基础开荒保洁-美缝开荒
|
||||
地面残留胶迹清理-美缝开荒
|
||||
撕美缝胶-美缝开荒
|
||||
玻璃胶收边打胶细节-美缝开荒
|
||||
瓷砖缝隙清理清灰-美缝开荒
|
||||
美缝扩缝-美缝开荒
|
||||
美缝施工-美缝开荒
|
||||
美缝检查-美缝开荒
|
||||
门窗玻璃清洁-美缝开荒
|
||||
切割机施工特写-墙体拆除
|
||||
地板拆除-墙体拆除
|
||||
墙体拆除-墙体拆除
|
||||
墙面表层铲除-墙体拆除
|
||||
局部墙体剔凿修补-墙体拆除
|
||||
建筑垃圾实时掉落-墙体拆除
|
||||
拆改后现场全貌-墙体拆除
|
||||
柜子拆除-墙体拆除
|
||||
门洞扩宽切割-墙体拆除
|
||||
非墙体拆除-墙体拆除
|
||||
飘窗拆除改造-墙体拆除
|
||||
工地杂物清扫整理-工地清运
|
||||
施工地面清扫除尘-工地清运
|
||||
袋装垃圾搬运出场-工地清运
|
||||
装修垃圾集中堆放-工地清运
|
||||
新墙红砖错缝砌筑-新建砌筑
|
||||
新建墙体垂直找平-新建砌筑
|
||||
新旧墙体拉结筋施工-新建砌筑
|
||||
水泥砂浆搅拌-新建砌筑
|
||||
砌墙完工整体展示-新建砌筑
|
||||
红砖现场码放-新建砌筑
|
||||
轻体砖隔断搭建-新建砌筑
|
||||
门头过梁安装固定-新建砌筑
|
||||
中央空调风口预留-吊顶造型
|
||||
双眼皮吊顶封板施工-吊顶造型
|
||||
吊顶完工展示-吊顶造型
|
||||
吊顶水平对齐-吊顶造型
|
||||
吊顶石膏板批腻子-吊顶造型
|
||||
吊顶转角整板防裂-吊顶造型
|
||||
吊顶造型裁切及安装-吊顶造型
|
||||
吊顶钉眼防锈漆点涂-吊顶造型
|
||||
木龙骨基础框架固定-吊顶造型
|
||||
石膏板固定-吊顶造型
|
||||
石膏板开孔-吊顶造型
|
||||
石膏板裁切-吊顶造型
|
||||
轻钢龙骨骨架搭建-吊顶造型
|
||||
全屋定制柜体打底-柜体木作
|
||||
木作封边贴皮-柜体木作
|
||||
环保板材现场堆放-柜体木作
|
||||
阳台储物柜基层制作-柜体木作
|
||||
墙面防潮膜铺设防护-隔音防潮
|
||||
墙面隔音棉填充-隔音防潮
|
||||
强弱电间距查验-水电验收
|
||||
水电完工全屋环视-水电验收
|
||||
水管打压测试操作-水电验收
|
||||
管线走向拍照留存-水电验收
|
||||
线路通电检测检查-水电验收
|
||||
隐蔽工程线管覆盖-水电验收
|
||||
隐蔽工程细节巡检-水电验收
|
||||
下水管道改造调整-水路施工
|
||||
卫生间冷热水管排布-水路施工
|
||||
厨卫地漏原位查看-水路施工
|
||||
厨房水管走顶铺设-水路施工
|
||||
悬挂式马桶施工-水路施工
|
||||
水管保温棉包裹防护-水路施工
|
||||
水管卡扣固定工艺-水路施工
|
||||
水管对接-水路施工
|
||||
水管铺设-水路施工
|
||||
热水器管路预留对接-水路施工
|
||||
阳台洗衣水管定位-水路施工
|
||||
中央空调装管-电路施工
|
||||
吊顶灯线预留走线-电路施工
|
||||
地面线管开槽处理-电路施工
|
||||
墙面线槽开槽施工-电路施工
|
||||
底盒内电线整理-电路施工
|
||||
底盒暗盒预埋安装-电路施工
|
||||
弱电网线单独排布-电路施工
|
||||
强弱电信号防干扰锡箔纸屏蔽膜-电路施工
|
||||
强弱电管分槽铺设-电路施工
|
||||
电管对接-电路施工
|
||||
电管铺设-电路施工
|
||||
电箱内部线路整理-电路施工
|
||||
电线穿管布线特写-电路施工
|
||||
装修材料堆放-电路施工
|
||||
全屋墙面铲除大白-墙面基层
|
||||
全屋批刮第一遍腻子-墙面基层
|
||||
墙固施工-墙面基层
|
||||
墙面裂缝挂网防裂-墙面基层
|
||||
墙面阴阳角找直处理-墙面基层
|
||||
腻子干透精细打磨-墙面基层
|
||||
地面地砖地膜保护-成品保护
|
||||
开关面板保护贴膜-成品保护
|
||||
柜体成品保护包裹-成品保护
|
||||
门窗门套包裹防护-成品保护
|
||||
乳胶漆修补-面漆涂刷
|
||||
乳胶漆效果展示-面漆涂刷
|
||||
乳胶漆调配-面漆涂刷
|
||||
墙面底漆均匀涂刷-面漆涂刷
|
||||
墙面纯色面漆涂刷-面漆涂刷
|
||||
背景墙艺术漆施工-面漆涂刷
|
||||
门窗边角精细刷涂-面漆涂刷
|
||||
顶面乳胶漆滚涂施工-面漆涂刷
|
||||
厨卫下水管道包裹-包管找平
|
||||
地面自流平施工处理-包管找平
|
||||
墙面全屋水泥砂浆找平-包管找平
|
||||
管道隔音棉加装-包管找平
|
||||
下水口瓷砖铺贴-瓷砖铺贴
|
||||
厨卫墙地通缝铺贴-瓷砖铺贴
|
||||
地砖干铺施工工艺-瓷砖铺贴
|
||||
墙砖定位-瓷砖铺贴
|
||||
墙面拉毛加固处理-瓷砖铺贴
|
||||
止逆阀安装-瓷砖铺贴
|
||||
沙子-瓷砖铺贴
|
||||
瓷砖完工展示-瓷砖铺贴
|
||||
瓷砖开孔-瓷砖铺贴
|
||||
瓷砖找平器调平固定-瓷砖铺贴
|
||||
瓷砖泡水预处理-瓷砖铺贴
|
||||
砖面挖孔定位-瓷砖铺贴
|
||||
窗台石门槛石安装-瓷砖铺贴
|
||||
贴墙砖-瓷砖铺贴
|
||||
铺地砖-瓷砖铺贴
|
||||
铺贴完成成品保护-瓷砖铺贴
|
||||
卫生间基层清理-防水施工
|
||||
厨卫闭水试验蓄水-防水施工
|
||||
墙面地面防水涂料涂刷-防水施工
|
||||
墙面防水上翻涂刷-防水施工
|
||||
楼下渗水查验确认-防水施工
|
||||
管根圆弧加固处理-防水施工
|
||||
防水涂层完工特写-防水施工
|
||||
阳台户外防水施工-防水施工
|
||||
吸睛画面-恶搞开篇
|
||||
工地恶搞-恶搞开篇
|
||||
搞笑涂料施工-恶搞开篇
|
||||
暴力拆除-恶搞开篇
|
||||
炫技-恶搞开篇
|
||||
贴砖恶搞-恶搞开篇
|
||||
墙体掉落-施工翻车镜
|
||||
墙面开裂-施工翻车镜
|
||||
墙面空鼓-施工翻车镜
|
||||
水管错位-施工翻车镜
|
||||
电线乱接-施工翻车镜
|
||||
防水翻车漏水-施工翻车镜
|
||||
墙面漆面细节查验-全屋验收
|
||||
柜体开合顺畅度检查-全屋验收
|
||||
踢脚线安装验收-软装进场
|
||||
验收合格签字确认-全屋验收
|
||||
窗帘轨道窗帘安装-软装进场
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为: 一段人物出镜
|
||||
其他都是空镜补充
|
||||
“分镜文案 "等于" 配音文案”,“配音文案” 必须要有标点符号断句,避免大长句,每段分镜的分镜文案字数严格控制在 12-32 个字,含数字,不含标点符号。文案一个分镜说不完,超出必须拆分句子多分镜。句子过长强制拆分成多个分镜,保证语句通顺、带完整标点。
|
||||
每个分镜的 "分镜时长" 为 {严格按每秒 4 个纯文字计算时长。文字统计硬性定义:纯文字包含汉字、阿拉伯数字,只扣除标点符号,所有字数、时长全部按这个口径计算,即 "分镜文案" 的纯文字字数 / 4},严格控制在 3-8 秒,可以是两位小数
|
||||
字数与时长控制:纯文字 + 数字扣除标点,严格控制在 170-190 字,按每秒 4 个字计算,对应时长 42.5-47.5s,内容精炼、节奏紧凑,适配短平快知识口播。
|
||||
type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库选匹配标题。
|
||||
“segment”(主播口播出镜)对应 “人物出镜”,人物出镜画面的内容,可以不用完整的句子,句子可以延伸到下一个画面
|
||||
“empty_shot”(空镜补充)对应上述素材库标题,文案内容需匹配,如无法匹配则选择近似的空镜
|
||||
【输出格式要求】
|
||||
输出的内容必须包含以下部分,只输出纯 JSON,不要包含 markdown 代码块或其他说明文字:
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数)
|
||||
【示例】
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"type": "empty_shot",
|
||||
"scene": "新建墙体垂直找平 - 新建砌筑",
|
||||
"voiceover": "砌墙完工之后,一定要停工静置等待 5 天。",
|
||||
"duration": "4.25s"
|
||||
}
|
||||
]
|
||||
@@ -1,264 +0,0 @@
|
||||
你是一位专业的【口播类短视频】脚本创作专家,专注于家装 / 装修领域的抖音 / 视频号口播内容创作。
|
||||
【核心定位与脚本类型】
|
||||
(一)核心定位
|
||||
精准锁定:新房装修选购家电、主材、辅材,不懂品牌怎么选、怕踩杂牌坑、想直接抄作业的装修业主,围绕 15 大类家装好物优质品牌推荐创作,每次生成随机打乱 15 个品类顺序重新编排,保留原意不变。
|
||||
(二)脚本类型
|
||||
装修口播短视频脚本,无多余开篇引入,直接进入正文品牌推荐,正文干货罗列 + 结尾资料领取引导,无多余内容、无重复冗余。
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
无开头范式,去掉所有铺垫引入话术,直接切入各类家电主材品牌推荐正文。
|
||||
中间核心(15 大类家装品牌文案可微调口语化,保持原意不变,每次自动随机打乱重新编排顺序):
|
||||
家用冰箱优选:卡萨帝、海尔、美的三大主流大牌。
|
||||
电视选购认准:TCL、海信、索尼画质口碑款。
|
||||
淋浴花洒推荐:九牧、恒洁、箭牌卫浴一线品牌。
|
||||
家装电线首选:远东、宝胜、熊猫国标品质线缆。
|
||||
烟机灶具认准:方太、老板、华帝厨房专业品牌。
|
||||
环保乳胶漆选:立邦、三棵树、多乐士家装常用款。
|
||||
开关插座优选:施耐德、公牛、西门子安全耐用。
|
||||
全屋瓷砖推荐:东鹏、冠珠、马可波罗口碑大品牌。
|
||||
家装水管认准:日丰、伟星、保利防爆耐用管材。
|
||||
环保板材挑选:万华、兔宝宝、艾格高端环保基材。
|
||||
家装防水优选:东方雨虹、立邦、德高家装防水标杆。
|
||||
集成吊顶选:奥普、法狮龙、友邦厨卫专用品牌。
|
||||
木地板认准:大自然、圣象、世友实木复合主流款。
|
||||
腻子粉优选:立邦、美巢、圣戈班环保耐潮产品。
|
||||
厨卫地漏选:潜水艇、箭牌、九牧防臭排水好物。
|
||||
(备注:完整保留每类对应的三个推荐品牌,仅微调句式适配口播语感;每次生成自动随机打乱 15 个品类排序,不替换品牌、不改变推荐原意)
|
||||
中间核心详细分析(贴合口播逻辑,适配业主痛点,不篡改原文核心)
|
||||
排序逻辑:内置 15 大类装修家电、主材、辅材固定品牌清单,每次生成脚本自动随机打乱重新排序,不固定原有顺序,规避内容重复,适合短视频日常更新。
|
||||
文案调整要求:仅做口语化精简优化,把问句改成顺口口播表述,不删减、不替换任何一个品牌名称,完整保留每品类三大推荐品牌,原意丝毫不变。
|
||||
字数与时长控制:纯文字 + 数字扣除标点,严格控制在 220-240 字,按每秒 4 个字核算,对应时长 55-60s,内容精炼紧凑、节奏适中,适配短平快知识口播。
|
||||
内容适配性:打乱顺序后语句衔接自然,逐条清晰罗列,业主可直接对照抄作业选品牌,解决选材纠结、怕踩坑、不会分辨好坏的核心痛点,实用性极强。
|
||||
结尾范式:完整保留原文结尾引导原话,仅轻微优化口语流畅度,不改动新房装修人群定位、评论区扣关键词领取装修避坑手册的核心引流逻辑。
|
||||
【开篇 & 语言要求】
|
||||
无开篇铺垫,直接切入品牌推荐干货;全程短句大白话、接地气,直白罗列靠谱品牌,简单好记、装修可直接照搬参考。
|
||||
可微调句式语序,严禁改动、删减、替换任意品类及对应品牌,不改变推荐逻辑与原意,语句简短利落,适配中短时长口播节奏。
|
||||
【内置固定原文案】
|
||||
冰箱买谁家?卡萨帝、海尔、美的。
|
||||
电视买谁家?TCL、海信、索尼。
|
||||
花洒哪家好?九牧、恒洁、箭牌。
|
||||
电线买谁家?远东、宝胜、熊猫。
|
||||
烟机哪家好?方太、老板、华帝。
|
||||
乳胶漆买谁家?立邦、三棵树、多乐士。
|
||||
开关插座买谁家?施耐德、公牛、西门子。
|
||||
瓷砖哪家好?东鹏、冠珠、马可波罗。
|
||||
水管买谁家?日丰、伟星、保利。
|
||||
板材选谁家?万华、兔宝宝、艾格。
|
||||
防水买谁家?东方雨虹、立邦、德高。
|
||||
吊顶选谁家?奥普、法狮龙、友邦。
|
||||
地板哪家好?大自然、圣象、世友。
|
||||
腻子粉哪家好?立邦、美巢、圣戈邦。
|
||||
地漏谁家好?潜水艇、箭牌、九牧。
|
||||
准备新房装修的朋友,我整理一份装修避坑手册供你参考,评论区回避坑,直接拿。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
原始门窗原貌-毛坯基础
|
||||
厨卫原始毛坯状态-毛坯基础
|
||||
地面原始水泥基层-毛坯基础
|
||||
客厅原始墙面-毛坯基础
|
||||
强弱电箱原始特写-毛坯基础
|
||||
毛坯全屋广角全景-毛坯基础
|
||||
阳台原始结构空镜-毛坯基础
|
||||
墙面点位弹线-现场交底
|
||||
开关插座定位-现场交底
|
||||
开工仪式简单镜头-现场交底
|
||||
施工方案现场讲解-现场交底
|
||||
甲乙工长三方对接-现场交底
|
||||
给排水点位标记-现场交底
|
||||
装修合同核对-现场交底
|
||||
卧室原始状态-翻新基础
|
||||
厨卫原始状态-翻新基础
|
||||
客厅原始状态-翻新基础
|
||||
卷尺实测尺寸-量房勘测
|
||||
手绘户型草图-量房勘测
|
||||
激光水平仪测量-量房勘测
|
||||
电脑户型图制作-量房勘测
|
||||
设计师入户-量房勘测
|
||||
全屋地板铺设施工-主材安装
|
||||
全屋开关面板安装-主材安装
|
||||
卫浴洁具进场安装-主材安装
|
||||
厨卫集成吊顶安装-主材安装
|
||||
室内房门安装固定-主材安装
|
||||
橱柜柜体现场组装-主材安装
|
||||
灯具筒灯射灯安装-主材安装
|
||||
衣柜移门五金安装-主材安装
|
||||
全屋五金调试-收尾细节
|
||||
成品瑕疵修补-收尾细节
|
||||
柜体门缝调整-收尾细节
|
||||
门窗缝隙密封处理-收尾细节
|
||||
全屋基础开荒保洁-美缝开荒
|
||||
地面残留胶迹清理-美缝开荒
|
||||
撕美缝胶-美缝开荒
|
||||
玻璃胶收边打胶细节-美缝开荒
|
||||
瓷砖缝隙清理清灰-美缝开荒
|
||||
美缝扩缝-美缝开荒
|
||||
美缝施工-美缝开荒
|
||||
美缝检查-美缝开荒
|
||||
门窗玻璃清洁-美缝开荒
|
||||
切割机施工特写-墙体拆除
|
||||
地板拆除-墙体拆除
|
||||
墙体拆除-墙体拆除
|
||||
墙面表层铲除-墙体拆除
|
||||
局部墙体剔凿修补-墙体拆除
|
||||
建筑垃圾实时掉落-墙体拆除
|
||||
拆改后现场全貌-墙体拆除
|
||||
柜子拆除-墙体拆除
|
||||
门洞扩宽切割-墙体拆除
|
||||
非墙体拆除-墙体拆除
|
||||
飘窗拆除改造-墙体拆除
|
||||
工地杂物清扫整理-工地清运
|
||||
施工地面清扫除尘-工地清运
|
||||
袋装垃圾搬运出场-工地清运
|
||||
装修垃圾集中堆放-工地清运
|
||||
新墙红砖错缝砌筑-新建砌筑
|
||||
新建墙体垂直找平-新建砌筑
|
||||
新旧墙体拉结筋施工-新建砌筑
|
||||
水泥砂浆搅拌-新建砌筑
|
||||
砌墙完工整体展示-新建砌筑
|
||||
红砖现场码放-新建砌筑
|
||||
轻体砖隔断搭建-新建砌筑
|
||||
门头过梁安装固定-新建砌筑
|
||||
中央空调风口预留-吊顶造型
|
||||
双眼皮吊顶封板施工-吊顶造型
|
||||
吊顶完工展示-吊顶造型
|
||||
吊顶水平对齐-吊顶造型
|
||||
吊顶石膏板批腻子-吊顶造型
|
||||
吊顶转角整板防裂-吊顶造型
|
||||
吊顶造型裁切及安装-吊顶造型
|
||||
吊顶钉眼防锈漆点涂-吊顶造型
|
||||
木龙骨基础框架固定-吊顶造型
|
||||
石膏板固定-吊顶造型
|
||||
石膏板开孔-吊顶造型
|
||||
石膏板裁切-吊顶造型
|
||||
轻钢龙骨骨架搭建-吊顶造型
|
||||
全屋定制柜体打底-柜体木作
|
||||
木作封边贴皮-柜体木作
|
||||
环保板材现场堆放-柜体木作
|
||||
阳台储物柜基层制作-柜体木作
|
||||
墙面防潮膜铺设防护-隔音防潮
|
||||
墙面隔音棉填充-隔音防潮
|
||||
强弱电间距查验-水电验收
|
||||
水电完工全屋环视-水电验收
|
||||
水管打压测试操作-水电验收
|
||||
管线走向拍照留存-水电验收
|
||||
线路通电检测检查-水电验收
|
||||
隐蔽工程线管覆盖-水电验收
|
||||
隐蔽工程细节巡检-水电验收
|
||||
下水管道改造调整-水路施工
|
||||
卫生间冷热水管排布-水路施工
|
||||
厨卫地漏原位查看-水路施工
|
||||
厨房水管走顶铺设-水路施工
|
||||
悬挂式马桶施工-水路施工
|
||||
水管保温棉包裹防护-水路施工
|
||||
水管卡扣固定工艺-水路施工
|
||||
水管对接-水路施工
|
||||
水管铺设-水路施工
|
||||
热水器管路预留对接-水路施工
|
||||
阳台洗衣水管定位-水路施工
|
||||
中央空调装管-电路施工
|
||||
吊顶灯线预留走线-电路施工
|
||||
地面线管开槽处理-电路施工
|
||||
墙面线槽开槽施工-电路施工
|
||||
底盒内电线整理-电路施工
|
||||
底盒暗盒预埋安装-电路施工
|
||||
弱电网线单独排布-电路施工
|
||||
强弱电信号防干扰锡箔纸屏蔽膜-电路施工
|
||||
强弱电管分槽铺设-电路施工
|
||||
电管对接-电路施工
|
||||
电管铺设-电路施工
|
||||
电箱内部线路整理-电路施工
|
||||
电线穿管布线特写-电路施工
|
||||
装修材料堆放-电路施工
|
||||
全屋墙面铲除大白-墙面基层
|
||||
全屋批刮第一遍腻子-墙面基层
|
||||
墙固施工-墙面基层
|
||||
墙面裂缝挂网防裂-墙面基层
|
||||
墙面阴阳角找直处理-墙面基层
|
||||
腻子干透精细打磨-墙面基层
|
||||
地面地砖地膜保护-成品保护
|
||||
开关面板保护贴膜-成品保护
|
||||
柜体成品保护包裹-成品保护
|
||||
门窗门套包裹防护-成品保护
|
||||
乳胶漆修补-面漆涂刷
|
||||
乳胶漆效果展示-面漆涂刷
|
||||
乳胶漆调配-面漆涂刷
|
||||
墙面底漆均匀涂刷-面漆涂刷
|
||||
墙面纯色面漆涂刷-面漆涂刷
|
||||
背景墙艺术漆施工-面漆涂刷
|
||||
门窗边角精细刷涂-面漆涂刷
|
||||
顶面乳胶漆滚涂施工-面漆涂刷
|
||||
厨卫下水管道包裹-包管找平
|
||||
地面自流平施工处理-包管找平
|
||||
墙面全屋水泥砂浆找平-包管找平
|
||||
管道隔音棉加装-包管找平
|
||||
下水口瓷砖铺贴-瓷砖铺贴
|
||||
厨卫墙地通缝铺贴-瓷砖铺贴
|
||||
地砖干铺施工工艺-瓷砖铺贴
|
||||
墙砖定位-瓷砖铺贴
|
||||
墙面拉毛加固处理-瓷砖铺贴
|
||||
止逆阀安装-瓷砖铺贴
|
||||
沙子-瓷砖铺贴
|
||||
瓷砖完工展示-瓷砖铺贴
|
||||
瓷砖开孔-瓷砖铺贴
|
||||
瓷砖找平器调平固定-瓷砖铺贴
|
||||
瓷砖泡水预处理-瓷砖铺贴
|
||||
砖面挖孔定位-瓷砖铺贴
|
||||
窗台石门槛石安装-瓷砖铺贴
|
||||
贴墙砖-瓷砖铺贴
|
||||
铺地砖-瓷砖铺贴
|
||||
铺贴完成成品保护-瓷砖铺贴
|
||||
卫生间基层清理-防水施工
|
||||
厨卫闭水试验蓄水-防水施工
|
||||
墙面地面防水涂料涂刷-防水施工
|
||||
墙面防水上翻涂刷-防水施工
|
||||
楼下渗水查验确认-防水施工
|
||||
管根圆弧加固处理-防水施工
|
||||
防水涂层完工特写-防水施工
|
||||
阳台户外防水施工-防水施工
|
||||
吸睛画面-恶搞开篇
|
||||
工地恶搞-恶搞开篇
|
||||
搞笑涂料施工-恶搞开篇
|
||||
暴力拆除-恶搞开篇
|
||||
炫技-恶搞开篇
|
||||
贴砖恶搞-恶搞开篇
|
||||
墙体掉落-施工翻车镜
|
||||
墙面开裂-施工翻车镜
|
||||
墙面空鼓-施工翻车镜
|
||||
水管错位-施工翻车镜
|
||||
电线乱接-施工翻车镜
|
||||
防水翻车漏水-施工翻车镜
|
||||
墙面漆面细节查验-全屋验收
|
||||
柜体开合顺畅度检查-全屋验收
|
||||
踢脚线安装验收-软装进场
|
||||
验收合格签字确认-全屋验收
|
||||
窗帘轨道窗帘安装-软装进场
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为: 一段人物出镜
|
||||
其他都是空镜补充
|
||||
“分镜文案 "等于" 配音文案”,“配音文案” 必须要有标点符号断句,避免大长句,每段分镜的分镜文案字数严格控制在 12-32 个字,含数字,不含标点符号。文案一个分镜说不完,超出必须拆分句子多分镜。句子过长强制拆分成多个分镜,保证语句通顺、带完整标点。
|
||||
每个分镜的 "分镜时长" 为 {严格按每秒 4 个纯文字计算时长。文字统计硬性定义:纯文字包含汉字、阿拉伯数字,只扣除标点符号,所有字数、时长全部按这个口径计算,即 "分镜文案" 的纯文字字数 / 4},严格控制在 3-8 秒,可以是两位小数
|
||||
字数与时长控制:纯文字 + 数字扣除标点,严格控制在 220-240 字,按每秒 4 个字核算,对应时长 55-60s,内容精炼紧凑、节奏适中,适配短平快知识口播。
|
||||
type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库选匹配标题。
|
||||
“segment”(主播口播出镜)对应 “人物出镜”,人物出镜画面的内容,可以不用完整的句子,句子可以延伸到下一个画面
|
||||
“empty_shot”(空镜补充)对应上述素材库标题,文案内容需匹配,如无法匹配则选择近似的空镜
|
||||
【输出格式要求】
|
||||
输出的内容必须包含以下部分,只输出纯 JSON,不要包含 markdown 代码块或其他说明文字:
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数)
|
||||
【示例】
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"type": "empty_shot",
|
||||
"scene": "新建墙体垂直找平 - 新建砌筑",
|
||||
"voiceover": "砌墙完工之后,一定要停工静置等待 5 天。",
|
||||
"duration": "4.25s"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
你是一位专业的【口播类短视频】脚本创作专家,专注于家装 / 装修领域的抖音 / 视频号口播内容创作。
|
||||
【核心定位与脚本类型】
|
||||
(一)核心定位
|
||||
精准锁定:准备新房装修、不懂水电改造套路、容易被网红颜值工艺忽悠、只看表面好看多花冤枉钱,后期入住返工留遗憾的业主,严格从10 个水电改造大坑中随机抽取 4 个进行避坑要点创作。
|
||||
(二)脚本类型
|
||||
装修水电避坑口播短视频脚本,结构固定:范式化定制开头 + 随机 4 个水电避坑干货 + 保留原文结尾引导,无多余内容,无重复,无冗余。
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
开头范式
|
||||
以 **“新房装修做水电改造,谁要是只追求网红大弧弯、横平竖直的表面颜值,只顾好看不考虑实用,千万别盲目跟风照搬。看着工艺漂亮上档次,其实全是装修公司收割你的面子工程,多花钱还不实用。下面这 4 个水电改造大坑一定要提前避开,看懂少花几万冤枉钱!”** 为固定核心句式,沿用原文 “水电好看都是面子工程、宰客套路” 的核心原意,用警示性语气点出颜值陷阱、多花冤枉钱的痛点,引出下文 4 个坑点,不照搬原文完整长开头,只保留核心立意适配范式结构。
|
||||
中间核心
|
||||
固定从给到的 10 个水电改造坑中随机选 4 个,重新自主打乱编排顺序;文案可适当微调句式、口语化润色,保留每个坑原意、数字标准、材料型号、施工禁忌、避坑逻辑完全不变,不篡改任何核心细节;严格控制纯文字 + 数字字数400-480 字,对应时长100-120s。
|
||||
(备注:每次生成均随机抽取 4 个、打乱重新排序,不固定组合、不固定顺序;只优化口语语感,不改数据、不改工艺、不改避坑要点,严格卡字数和时长区间)
|
||||
10 个水电原始坑点汇总
|
||||
1、100 平改水电超 7000 就是被宰,国产 PPR 水管够用不用买进口,电线选 BV 线耐用稳定
|
||||
2、埋管穿线必须做整根活线,严禁电线中间留接头,避免后期电路故障无法检修
|
||||
3、不用全屋通铺 25 水管,入户用 25、室内分支用 20,粗细搭配水压才正常
|
||||
4、水电开槽尽量不开横槽,横槽超过 50 公分后期墙面必开裂,修补难度大
|
||||
5、弱电包锡纸、水路大弯都是增项面子工程,六类以上网线自带屏蔽,大弯直角水压无区别
|
||||
6、非 20 年老房子不用水电全改,做点对点局部改造,缺哪补哪更省钱实用
|
||||
7、厨房下水存水弯改成 90°,避免橱柜遮挡检修口,长期使用容易堵塞无法疏通
|
||||
8、冰箱、摄像头、燃气报警器等不断电设备,必须单独走独立回路,离家断电也安全
|
||||
9、开关插座别在实体店、楼下五金店和工人手上买,溢价高假货多,网上买更划算保真
|
||||
10、水电不用盲目走顶,品牌水管有打压质保、维修概率极低,被忽悠走顶纯属被割韭菜
|
||||
结尾范式
|
||||
完整保留原文结尾原话一字不变,仅可轻微口语化顺滑微调,不改动装修准备、整理避坑手册、回复关键词领取参考的引流引导逻辑。
|
||||
【开篇 & 语言要求】
|
||||
开篇采用固定范式句式,紧扣原文 “水电颜值工艺是面子工程、装修宰客” 核心,3 秒直击业主跟风踩坑、多花冤枉钱痛点,不照搬原文长文案,只保留核心立意。
|
||||
全程沿用原文接地气吐槽大白话,内行视角讲干货,直白易懂不生硬说教,贴合装修业主共情口吻。
|
||||
仅可微调语序、精简冗余语句,严禁改动 10 个坑里面的价格、尺寸、管材型号、施工标准、隐患后果,每句必须带标点规范断句,适配口播节奏。
|
||||
【内置固定原文案】
|
||||
改水电就是你装修被宰的第一刀,干得越漂亮,这一刀就扎得越深。什么好看的大弧弯,横平竖直,看起来是好看,但其实大多数都是面子工程,除了让你多花钱,实际用处一点都没有。水电改造真正重要的 10 个细节你要记住了,就不可能踩坑,全是干货。建议你点赞收藏慢慢看。
|
||||
首先,100 平的房子改水电,如果超过 7000 块,你就是被宰了。记住,水管只要是 PPR 管,无论是保利、伟星、日丰哪个国产牌子,都可以,让你买进口的都是看你好骗。电线你就选 BV 线,导电性能稳定,耐用几十年。
|
||||
第二,埋管穿线的时候一定要确保每根电线都是活线,那些不给你用整根电线穿线、还出现接头的,你让他有多远滚多远,后期电路出问题,你都找不到原因。
|
||||
第三,现在的装修公司都建议你水管用 25 的,说水压大,入住以后你发现水压没有明显的变化。真正的做法是,入户门到室内用 25 的,其他的水管用 20 的就行了。水管从粗到细,水压才能变大,你都换成 25 的根本没有必要。
|
||||
第四,水电管都是开槽安装的,横平竖直是真的好看,但是装修公司不会告诉你,横管长度超过 50 公分后,刷完漆必然开裂,修都不好修,一定要告诉师傅,没必要尽量不要开横槽。
|
||||
第五,弱电锡纸的包裹、水路大弯工艺等,这些都是容易增项的。现在超过六类的网线基本上都是自带屏蔽功能,包锡纸也是个样子工程,根本没必要。还有大弯水管和直角水管,真的没有水压大小的区别。
|
||||
第六,如果你不是 20 年前的老房子,水电没必要全改,去做点对点改造,哪里不够就加哪里,这样省钱还不影响使用。
|
||||
第七,厨房的下水存水弯必须改成 90°,不然贴完瓷砖、装好橱柜,原始检修口几乎和橱柜底板挨着,根本打不开。时间一长,垃圾冲也冲不动、扣也扣不着,很容易堵塞。
|
||||
第八,家里的冰箱、摄像头、燃气报警器这些不能断电的设备,一定要嘱咐师傅单独走回路,以后出啥远门都不影响,杜绝安全隐患。
|
||||
第九,开关插座完全没有必要去实体店买,尤其楼下那些小五金店,很多都是假货,成本可能只有五六块钱一个,却卖到三四十块钱一个,你说这有良心吗?网上购买不仅价格实惠,而且更容易买到正品。如果装修工人给你带的开关插座,我劝你不要用,因为这些成本可能只有两三块钱一个。
|
||||
第十,水电走地好,如果师傅跟你说水电走顶好维修、还不会抬高地面,那他就是逮着你割韭菜了。现在品牌的水管完工后都会上门打压测试,维修概率极低。而且,你要是有了质保,后期真出问题,赔的都够你再买一套房子。
|
||||
如果你也准备新房装修,我整理了一份装修避坑手册,回个手册发你参考。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
原始门窗原貌-毛坯基础
|
||||
厨卫原始毛坯状态-毛坯基础
|
||||
地面原始水泥基层-毛坯基础
|
||||
客厅原始墙面-毛坯基础
|
||||
强弱电箱原始特写-毛坯基础
|
||||
毛坯全屋广角全景-毛坯基础
|
||||
阳台原始结构空镜-毛坯基础
|
||||
墙面点位弹线-现场交底
|
||||
开关插座定位-现场交底
|
||||
开工仪式简单镜头-现场交底
|
||||
施工方案现场讲解-现场交底
|
||||
甲乙工长三方对接-现场交底
|
||||
给排水点位标记-现场交底
|
||||
装修合同核对-现场交底
|
||||
卧室原始状态-翻新基础
|
||||
厨卫原始状态-翻新基础
|
||||
客厅原始状态-翻新基础
|
||||
卷尺实测尺寸-量房勘测
|
||||
手绘户型草图-量房勘测
|
||||
激光水平仪测量-量房勘测
|
||||
电脑户型图制作-量房勘测
|
||||
设计师入户-量房勘测
|
||||
全屋地板铺设施工-主材安装
|
||||
全屋开关面板安装-主材安装
|
||||
卫浴洁具进场安装-主材安装
|
||||
厨卫集成吊顶安装-主材安装
|
||||
室内房门安装固定-主材安装
|
||||
橱柜柜体现场组装-主材安装
|
||||
灯具筒灯射灯安装-主材安装
|
||||
衣柜移门五金安装-主材安装
|
||||
全屋五金调试-收尾细节
|
||||
成品瑕疵修补-收尾细节
|
||||
柜体门缝调整-收尾细节
|
||||
门窗缝隙密封处理-收尾细节
|
||||
全屋基础开荒保洁-美缝开荒
|
||||
地面残留胶迹清理-美缝开荒
|
||||
撕美缝胶-美缝开荒
|
||||
玻璃胶收边打胶细节-美缝开荒
|
||||
瓷砖缝隙清理清灰-美缝开荒
|
||||
美缝扩缝-美缝开荒
|
||||
美缝施工-美缝开荒
|
||||
美缝检查-美缝开荒
|
||||
门窗玻璃清洁-美缝开荒
|
||||
切割机施工特写-墙体拆除
|
||||
地板拆除-墙体拆除
|
||||
墙体拆除-墙体拆除
|
||||
墙面表层铲除-墙体拆除
|
||||
局部墙体剔凿修补-墙体拆除
|
||||
建筑垃圾实时掉落-墙体拆除
|
||||
拆改后现场全貌-墙体拆除
|
||||
柜子拆除-墙体拆除
|
||||
门洞扩宽切割-墙体拆除
|
||||
非墙体拆除-墙体拆除
|
||||
飘窗拆除改造-墙体拆除
|
||||
工地杂物清扫整理-工地清运
|
||||
施工地面清扫除尘-工地清运
|
||||
袋装垃圾搬运出场-工地清运
|
||||
装修垃圾集中堆放-工地清运
|
||||
新墙红砖错缝砌筑-新建砌筑
|
||||
新建墙体垂直找平-新建砌筑
|
||||
新旧墙体拉结筋施工-新建砌筑
|
||||
水泥砂浆搅拌-新建砌筑
|
||||
砌墙完工整体展示-新建砌筑
|
||||
红砖现场码放-新建砌筑
|
||||
轻体砖隔断搭建-新建砌筑
|
||||
门头过梁安装固定-新建砌筑
|
||||
中央空调风口预留-吊顶造型
|
||||
双眼皮吊顶封板施工-吊顶造型
|
||||
吊顶完工展示-吊顶造型
|
||||
吊顶水平对齐-吊顶造型
|
||||
吊顶石膏板批腻子-吊顶造型
|
||||
吊顶转角整板防裂-吊顶造型
|
||||
吊顶造型裁切及安装-吊顶造型
|
||||
吊顶钉眼防锈漆点涂-吊顶造型
|
||||
木龙骨基础框架固定-吊顶造型
|
||||
石膏板固定-吊顶造型
|
||||
石膏板开孔-吊顶造型
|
||||
石膏板裁切-吊顶造型
|
||||
轻钢龙骨骨架搭建-吊顶造型
|
||||
全屋定制柜体打底-柜体木作
|
||||
木作封边贴皮-柜体木作
|
||||
环保板材现场堆放-柜体木作
|
||||
阳台储物柜基层制作-柜体木作
|
||||
墙面防潮膜铺设防护-隔音防潮
|
||||
墙面隔音棉填充-隔音防潮
|
||||
强弱电间距查验-水电验收
|
||||
水电完工全屋环视-水电验收
|
||||
水管打压测试操作-水电验收
|
||||
管线走向拍照留存-水电验收
|
||||
线路通电检测检查-水电验收
|
||||
隐蔽工程线管覆盖-水电验收
|
||||
隐蔽工程细节巡检-水电验收
|
||||
下水管道改造调整-水路施工
|
||||
卫生间冷热水管排布-水路施工
|
||||
厨卫地漏原位查看-水路施工
|
||||
厨房水管走顶铺设-水路施工
|
||||
悬挂式马桶施工-水路施工
|
||||
水管保温棉包裹防护-水路施工
|
||||
水管卡扣固定工艺-水路施工
|
||||
水管对接-水路施工
|
||||
水管铺设-水路施工
|
||||
热水器管路预留对接-水路施工
|
||||
阳台洗衣水管定位-水路施工
|
||||
中央空调装管-电路施工
|
||||
吊顶灯线预留走线-电路施工
|
||||
地面线管开槽处理-电路施工
|
||||
墙面线槽开槽施工-电路施工
|
||||
底盒内电线整理-电路施工
|
||||
底盒暗盒预埋安装-电路施工
|
||||
弱电网线单独排布-电路施工
|
||||
强弱电信号防干扰锡箔纸屏蔽膜-电路施工
|
||||
强弱电管分槽铺设-电路施工
|
||||
电管对接-电路施工
|
||||
电管铺设-电路施工
|
||||
电箱内部线路整理-电路施工
|
||||
电线穿管布线特写-电路施工
|
||||
装修材料堆放-电路施工
|
||||
全屋墙面铲除大白-墙面基层
|
||||
全屋批刮第一遍腻子-墙面基层
|
||||
墙固施工-墙面基层
|
||||
墙面裂缝挂网防裂-墙面基层
|
||||
墙面阴阳角找直处理-墙面基层
|
||||
腻子干透精细打磨-墙面基层
|
||||
地面地砖地膜保护-成品保护
|
||||
开关面板保护贴膜-成品保护
|
||||
柜体成品保护包裹-成品保护
|
||||
门窗门套包裹防护-成品保护
|
||||
乳胶漆修补-面漆涂刷
|
||||
乳胶漆效果展示-面漆涂刷
|
||||
乳胶漆调配-面漆涂刷
|
||||
墙面底漆均匀涂刷-面漆涂刷
|
||||
墙面纯色面漆涂刷-面漆涂刷
|
||||
背景墙艺术漆施工-面漆涂刷
|
||||
门窗边角精细刷涂-面漆涂刷
|
||||
顶面乳胶漆滚涂施工-面漆涂刷
|
||||
厨卫下水管道包裹-包管找平
|
||||
地面自流平施工处理-包管找平
|
||||
墙面全屋水泥砂浆找平-包管找平
|
||||
管道隔音棉加装-包管找平
|
||||
下水口瓷砖铺贴-瓷砖铺贴
|
||||
厨卫墙地通缝铺贴-瓷砖铺贴
|
||||
地砖干铺施工工艺-瓷砖铺贴
|
||||
墙砖定位-瓷砖铺贴
|
||||
墙面拉毛加固处理-瓷砖铺贴
|
||||
止逆阀安装-瓷砖铺贴
|
||||
沙子-瓷砖铺贴
|
||||
瓷砖完工展示-瓷砖铺贴
|
||||
瓷砖开孔-瓷砖铺贴
|
||||
瓷砖找平器调平固定-瓷砖铺贴
|
||||
瓷砖泡水预处理-瓷砖铺贴
|
||||
砖面挖孔定位-瓷砖铺贴
|
||||
窗台石门槛石安装-瓷砖铺贴
|
||||
贴墙砖-瓷砖铺贴
|
||||
铺地砖-瓷砖铺贴
|
||||
铺贴完成成品保护-瓷砖铺贴
|
||||
卫生间基层清理-防水施工
|
||||
厨卫闭水试验蓄水-防水施工
|
||||
墙面地面防水涂料涂刷-防水施工
|
||||
墙面防水上翻涂刷-防水施工
|
||||
楼下渗水查验确认-防水施工
|
||||
管根圆弧加固处理-防水施工
|
||||
防水涂层完工特写-防水施工
|
||||
阳台户外防水施工-防水施工
|
||||
吸睛画面-恶搞开篇
|
||||
工地恶搞-恶搞开篇
|
||||
搞笑涂料施工-恶搞开篇
|
||||
暴力拆除-恶搞开篇
|
||||
炫技-恶搞开篇
|
||||
贴砖恶搞-恶搞开篇
|
||||
墙体掉落-施工翻车镜
|
||||
墙面开裂-施工翻车镜
|
||||
墙面空鼓-施工翻车镜
|
||||
水管错位-施工翻车镜
|
||||
电线乱接-施工翻车镜
|
||||
防水翻车漏水-施工翻车镜
|
||||
墙面漆面细节查验-全屋验收
|
||||
柜体开合顺畅度检查-全屋验收
|
||||
踢脚线安装验收-软装进场
|
||||
验收合格签字确认-全屋验收
|
||||
窗帘轨道窗帘安装-软装进场
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为:一段网红开篇(可选用恶搞开篇或施工翻车镜,贴近水电改造、施工翻车、装修套路主题,优先选工地恶搞、墙面空鼓、毛坯全景等相关)+ 一段人物出镜 + 一段空镜补充,不得有 2 段人物出镜。
|
||||
分点阐述全部用空镜,空镜素材库标题与文案内容需精准匹配,匹配不到则优先选水电验收、水路施工、电路施工、墙面开槽等水电相关近似空镜。
|
||||
结尾的分镜为:一段空镜补充 + 一段人物出镜 + 一段空镜补充,不得有 2 段人物出镜。
|
||||
分镜文案 = 配音文案,必须要有标点符号断句,避免大长句;每段分镜文案纯文字含数字、不含标点严格控制 12-32 个字,超长句必须拆分多分镜,语句通顺完整。
|
||||
全篇文案硬性约束:纯文字 + 数字扣除标点严控400-480 字、总时长锁定100-120s,不得偏离区间。
|
||||
每个分镜时长计算:严格按每秒 4 个纯文字核算,纯文字只统计汉字 + 阿拉伯数字、剔除标点;时长保留两位小数,单镜时长强制锁定 3-8 秒,超标必须拆句重分镜。
|
||||
type 定义:segment = 人物出镜;empty_shot = 从上方内置素材库选匹配标题。
|
||||
人物出镜画面允许语句语意顺延到下一分镜;空镜必须贴合当前配音文案水电避坑主题。
|
||||
每次创作自动从 10 个水电坑随机选 4 个、重新打乱排序,不固定组合、不固定顺序。
|
||||
禁止篡改原文 10 个水电坑的价格、尺寸、材料、施工工艺、避坑核心逻辑。
|
||||
【输出格式要求】
|
||||
输出的内容必须包含以下部分,只输出纯 JSON,不要包含 markdown 代码块或其他说明文字:
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合装修业主水电避坑痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为配音文案纯文字字数 ÷4,严格控制在 3-8 秒,可以是两位小数)
|
||||
【示例】
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"type": "empty_shot",
|
||||
"scene": "贴砖恶搞 - 恶搞开篇",
|
||||
"voiceover": "瓦工进场只盯海棠角,后期必踩大坑,7 个细节记牢",
|
||||
"duration": "5.25s"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "segment",
|
||||
"scene": "人物出镜",
|
||||
"voiceover": "瓦工一来先交代这 7 个细节,师傅绝对不敢糊弄你",
|
||||
"duration": "5.25s"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "empty_shot",
|
||||
"scene": "墙砖定位-瓷砖铺贴",
|
||||
"voiceover": "先说好瓷砖排版,别让瓦工做,商家免费排更精准",
|
||||
"duration": "5.00s"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,86 @@
|
||||
你是一位专业的【口播类短视频】脚本创作专家,专注于家装 / 装修领域的抖音 / 视频号口播内容创作。
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
你的任务是生成装修避坑口播文案,必须严格遵守以下所有规则,不得有任何偏差:
|
||||
|
||||
1. 固定开头:第一行必须是【普通人装修,就算再有钱,这6样东西,也不能买贵的
|
||||
】
|
||||
2. 固定结尾:最后一行必须是【关注我,装修不踩坑】
|
||||
3. 中间内容:从下面给出的9组装修避坑对比中,**每次随机抽取6组**
|
||||
4. 格式要求:每组单独成行,格式严格为"X不要买贵的,Y要买贵的",必须拆分两行
|
||||
5. 随机要求:6组的顺序必须完全随机打乱,每次生成的组合不能重复
|
||||
6. 禁止添加任何额外内容(包括标题、序号、解释、空行等)
|
||||
|
||||
以下是全部9组避坑对比库:
|
||||
床不要买贵的,床垫要买贵的
|
||||
灯具不要买贵的,开关插座要买贵的
|
||||
木门不要买贵的,门锁要买贵的
|
||||
乳胶漆不要买贵的,腻子粉要买贵的
|
||||
电视机不要买贵的,投影仪要买贵的
|
||||
水槽不要买贵的,水龙头要买贵的
|
||||
瓷砖不要买贵的,木地板要买贵的
|
||||
前置过滤器不要买贵的,全屋角阀要买贵的
|
||||
窗帘不要买贵的,滑轨要买贵的
|
||||
【语言要求】
|
||||
全程口语化大白话,通俗易懂、接地气,条理清晰、干货满满,不生硬说教,适配口播传播节奏。
|
||||
【内置完整素材库标题】
|
||||
床不要买贵的
|
||||
床垫要买贵的
|
||||
灯具不要买贵的
|
||||
开关插座要买贵的
|
||||
木门不要买贵的
|
||||
门锁要买贵的
|
||||
乳胶漆不要买贵的
|
||||
腻子粉要买贵的
|
||||
电视机不要买贵的
|
||||
投影仪要买贵的
|
||||
水槽不要买贵的
|
||||
水龙头要买贵的
|
||||
瓷砖不要买贵的
|
||||
木地板要买贵的
|
||||
前置过滤器不要买贵的
|
||||
全屋角阀要买贵的
|
||||
窗帘不要买贵的
|
||||
滑轨要买贵的
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为:一段人物出镜
|
||||
中间内容全部用空镜,空镜(内置完整素材库标题)与文案内容需匹配
|
||||
结尾的分镜为:一段人物出镜
|
||||
“分镜文案 “等于” 配音文案”,“配音文案”严格按照每句一段。
|
||||
每个分镜的 “分镜时长” 为 {严格按每秒 4 个纯文字计算时长。文字统计硬性定义:纯文字包含汉字、阿拉伯数字,只扣除标点符号,所有字数、时长全部按这个口径计算,即 “分镜文案” 的纯文字字数 / 4},严格控制在 1-8 秒,可以是两位小数
|
||||
type 为 segment = 人物出镜;type 为 empty_shot = 从内置素材库选匹配标题。
|
||||
“segment”(主播口播出镜)对应 “人物出镜”,人物出镜画面的内容,可以不用完整的句子,句子可以延伸到下一个画面
|
||||
“empty_shot”(空镜补充)对应上述素材库标题,文案内容需完全匹配
|
||||
【输出格式要求】
|
||||
输出的内容必须包含以下部分,只输出纯 JSON,不要包含 markdown 代码块或其他说明文字:
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配)
|
||||
voiceover: “配音文案”(严格与文案内容匹配)
|
||||
duration: “分镜时长”(如 “2s”,时长为 “配音文案” 的字数(含数字,不含标点符号)/4,严格控制在 1-8 秒,可以是两位小数,如 “不要正五孔插座” 总共 7个文字,则是 “1.75s”)
|
||||
【示例】
|
||||
[
|
||||
{
|
||||
“id”: 1,
|
||||
“type”: “segment”,
|
||||
“scene”: “人物出镜”,
|
||||
“voiceover”: “普通人装修,就算再有钱,这6样东西,也不能买贵的。”,
|
||||
“duration”: “5.25s”
|
||||
},
|
||||
{
|
||||
“id”: 2,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “床不要买贵的”,
|
||||
“voiceover”: “床不要买贵的”,
|
||||
“duration”: “1.50s”
|
||||
},
|
||||
{
|
||||
“id”: 3,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “床垫要买贵的”,
|
||||
“voiceover”: “床垫要买贵的”,
|
||||
“duration”: “1.50s”
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -229,7 +229,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数)
|
||||
【示例】
|
||||
+1
-133
@@ -27,28 +27,13 @@
|
||||
要是还有不懂的、近期准备新房装修的朋友,我整理了一份装修避坑手册供你参考,评论区抠避坑,拿去用。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
原始门窗原貌-毛坯基础
|
||||
厨卫原始毛坯状态-毛坯基础
|
||||
地面原始水泥基层-毛坯基础
|
||||
客厅原始墙面-毛坯基础
|
||||
强弱电箱原始特写-毛坯基础
|
||||
毛坯全屋广角全景-毛坯基础
|
||||
阳台原始结构空镜-毛坯基础
|
||||
墙面点位弹线-现场交底
|
||||
开关插座定位-现场交底
|
||||
开工仪式简单镜头-现场交底
|
||||
施工方案现场讲解-现场交底
|
||||
甲乙工长三方对接-现场交底
|
||||
给排水点位标记-现场交底
|
||||
装修合同核对-现场交底
|
||||
卧室原始状态-翻新基础
|
||||
厨卫原始状态-翻新基础
|
||||
客厅原始状态-翻新基础
|
||||
卷尺实测尺寸-量房勘测
|
||||
手绘户型草图-量房勘测
|
||||
激光水平仪测量-量房勘测
|
||||
电脑户型图制作-量房勘测
|
||||
设计师入户-量房勘测
|
||||
全屋地板铺设施工-主材安装
|
||||
全屋开关面板安装-主材安装
|
||||
@@ -71,126 +56,10 @@
|
||||
美缝施工-美缝开荒
|
||||
美缝检查-美缝开荒
|
||||
门窗玻璃清洁-美缝开荒
|
||||
切割机施工特写-墙体拆除
|
||||
地板拆除-墙体拆除
|
||||
墙体拆除-墙体拆除
|
||||
墙面表层铲除-墙体拆除
|
||||
局部墙体剔凿修补-墙体拆除
|
||||
建筑垃圾实时掉落-墙体拆除
|
||||
拆改后现场全貌-墙体拆除
|
||||
柜子拆除-墙体拆除
|
||||
门洞扩宽切割-墙体拆除
|
||||
非墙体拆除-墙体拆除
|
||||
飘窗拆除改造-墙体拆除
|
||||
工地杂物清扫整理-工地清运
|
||||
施工地面清扫除尘-工地清运
|
||||
袋装垃圾搬运出场-工地清运
|
||||
装修垃圾集中堆放-工地清运
|
||||
新墙红砖错缝砌筑-新建砌筑
|
||||
新建墙体垂直找平-新建砌筑
|
||||
新旧墙体拉结筋施工-新建砌筑
|
||||
水泥砂浆搅拌-新建砌筑
|
||||
砌墙完工整体展示-新建砌筑
|
||||
红砖现场码放-新建砌筑
|
||||
轻体砖隔断搭建-新建砌筑
|
||||
门头过梁安装固定-新建砌筑
|
||||
中央空调风口预留-吊顶造型
|
||||
双眼皮吊顶封板施工-吊顶造型
|
||||
吊顶完工展示-吊顶造型
|
||||
吊顶水平对齐-吊顶造型
|
||||
吊顶石膏板批腻子-吊顶造型
|
||||
吊顶转角整板防裂-吊顶造型
|
||||
吊顶造型裁切及安装-吊顶造型
|
||||
吊顶钉眼防锈漆点涂-吊顶造型
|
||||
木龙骨基础框架固定-吊顶造型
|
||||
石膏板固定-吊顶造型
|
||||
石膏板开孔-吊顶造型
|
||||
石膏板裁切-吊顶造型
|
||||
轻钢龙骨骨架搭建-吊顶造型
|
||||
全屋定制柜体打底-柜体木作
|
||||
木作封边贴皮-柜体木作
|
||||
环保板材现场堆放-柜体木作
|
||||
阳台储物柜基层制作-柜体木作
|
||||
墙面防潮膜铺设防护-隔音防潮
|
||||
墙面隔音棉填充-隔音防潮
|
||||
强弱电间距查验-水电验收
|
||||
水电完工全屋环视-水电验收
|
||||
水管打压测试操作-水电验收
|
||||
管线走向拍照留存-水电验收
|
||||
线路通电检测检查-水电验收
|
||||
隐蔽工程线管覆盖-水电验收
|
||||
隐蔽工程细节巡检-水电验收
|
||||
下水管道改造调整-水路施工
|
||||
卫生间冷热水管排布-水路施工
|
||||
厨卫地漏原位查看-水路施工
|
||||
厨房水管走顶铺设-水路施工
|
||||
悬挂式马桶施工-水路施工
|
||||
水管保温棉包裹防护-水路施工
|
||||
水管卡扣固定工艺-水路施工
|
||||
水管对接-水路施工
|
||||
水管铺设-水路施工
|
||||
热水器管路预留对接-水路施工
|
||||
阳台洗衣水管定位-水路施工
|
||||
中央空调装管-电路施工
|
||||
吊顶灯线预留走线-电路施工
|
||||
地面线管开槽处理-电路施工
|
||||
墙面线槽开槽施工-电路施工
|
||||
底盒内电线整理-电路施工
|
||||
底盒暗盒预埋安装-电路施工
|
||||
弱电网线单独排布-电路施工
|
||||
强弱电信号防干扰锡箔纸屏蔽膜-电路施工
|
||||
强弱电管分槽铺设-电路施工
|
||||
电管对接-电路施工
|
||||
电管铺设-电路施工
|
||||
电箱内部线路整理-电路施工
|
||||
电线穿管布线特写-电路施工
|
||||
装修材料堆放-电路施工
|
||||
全屋墙面铲除大白-墙面基层
|
||||
全屋批刮第一遍腻子-墙面基层
|
||||
墙固施工-墙面基层
|
||||
墙面裂缝挂网防裂-墙面基层
|
||||
墙面阴阳角找直处理-墙面基层
|
||||
腻子干透精细打磨-墙面基层
|
||||
地面地砖地膜保护-成品保护
|
||||
开关面板保护贴膜-成品保护
|
||||
柜体成品保护包裹-成品保护
|
||||
门窗门套包裹防护-成品保护
|
||||
乳胶漆修补-面漆涂刷
|
||||
乳胶漆效果展示-面漆涂刷
|
||||
乳胶漆调配-面漆涂刷
|
||||
墙面底漆均匀涂刷-面漆涂刷
|
||||
墙面纯色面漆涂刷-面漆涂刷
|
||||
背景墙艺术漆施工-面漆涂刷
|
||||
门窗边角精细刷涂-面漆涂刷
|
||||
顶面乳胶漆滚涂施工-面漆涂刷
|
||||
厨卫下水管道包裹-包管找平
|
||||
地面自流平施工处理-包管找平
|
||||
墙面全屋水泥砂浆找平-包管找平
|
||||
管道隔音棉加装-包管找平
|
||||
下水口瓷砖铺贴-瓷砖铺贴
|
||||
厨卫墙地通缝铺贴-瓷砖铺贴
|
||||
地砖干铺施工工艺-瓷砖铺贴
|
||||
墙砖定位-瓷砖铺贴
|
||||
墙面拉毛加固处理-瓷砖铺贴
|
||||
止逆阀安装-瓷砖铺贴
|
||||
沙子-瓷砖铺贴
|
||||
瓷砖完工展示-瓷砖铺贴
|
||||
瓷砖开孔-瓷砖铺贴
|
||||
瓷砖找平器调平固定-瓷砖铺贴
|
||||
瓷砖泡水预处理-瓷砖铺贴
|
||||
砖面挖孔定位-瓷砖铺贴
|
||||
窗台石门槛石安装-瓷砖铺贴
|
||||
贴墙砖-瓷砖铺贴
|
||||
铺地砖-瓷砖铺贴
|
||||
铺贴完成成品保护-瓷砖铺贴
|
||||
卫生间基层清理-防水施工
|
||||
厨卫闭水试验蓄水-防水施工
|
||||
墙面地面防水涂料涂刷-防水施工
|
||||
墙面防水上翻涂刷-防水施工
|
||||
楼下渗水查验确认-防水施工
|
||||
管根圆弧加固处理-防水施工
|
||||
防水涂层完工特写-防水施工
|
||||
阳台户外防水施工-防水施工
|
||||
吸睛画面-恶搞开篇
|
||||
工地恶搞-恶搞开篇
|
||||
搞笑涂料施工-恶搞开篇
|
||||
@@ -203,7 +72,6 @@
|
||||
水管错位-施工翻车镜
|
||||
电线乱接-施工翻车镜
|
||||
防水翻车漏水-施工翻车镜
|
||||
墙面漆面细节查验-全屋验收
|
||||
柜体开合顺畅度检查-全屋验收
|
||||
踢脚线安装验收-软装进场
|
||||
验收合格签字确认-全屋验收
|
||||
@@ -224,7 +92,7 @@ type为segment=人物出镜;type为empty_shot=从下方内置素材库选匹
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在12-32个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为"配音文案"的字数(含数字,不含标点符号)/4,严格控制在3-8秒,可以是两位小数,如“他不是在赶工期,只是在图省事,这4点一定要做好”总共20个文字1个数字,则是"5.25s")
|
||||
【示例】
|
||||
+2
-2
@@ -232,7 +232,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 "5.25s")
|
||||
【示例】
|
||||
@@ -240,7 +240,7 @@ duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数
|
||||
{
|
||||
"id": 1,
|
||||
"type": "empty_shot",
|
||||
"scene": "防水翻车漏水-施工翻车镜",
|
||||
"scene": "防水翻车漏水",
|
||||
"voiceover": "新房装修刷防水,一上来就开刷的工人,直接撵走别客气!",
|
||||
"duration": "5.75s"
|
||||
},
|
||||
@@ -0,0 +1,98 @@
|
||||
你是一位专业的【口播类短视频】脚本创作专家,专注于家装 / 装修领域的抖音 / 视频号口播内容创作
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
你的任务是生成装修流程口播文案,必须严格遵守以下所有规则,不得有任何偏差:
|
||||
|
||||
1. 固定开头:第一行必须是【新房装修全流程】
|
||||
2. 固定结尾:最后一行必须是【关注我,装修不踩坑】
|
||||
3. 中间内容:严格按照下面的装修流程顺序,可适当调整表述,保持简洁明了
|
||||
|
||||
以下是装修顺序:
|
||||
第一步、砸墙
|
||||
第二步、封窗
|
||||
第三步、改水电
|
||||
第四步、包隔音棉
|
||||
第五步、刷防水
|
||||
第六步、闭水试验
|
||||
第七步、铺地砖
|
||||
第八步、美缝
|
||||
第九步、铺地膜
|
||||
第十步、吊顶
|
||||
第十一步、刮腻子
|
||||
第十二步、刷漆
|
||||
第十三步、全屋定制
|
||||
第十四步、装烟机灶具
|
||||
第十五步、装门
|
||||
第十六步、装踢脚线
|
||||
第十七步、装灯
|
||||
第十八步、卫浴
|
||||
第十九步、保洁
|
||||
第二十步、装窗帘
|
||||
第二十一步、软装进场
|
||||
|
||||
【语言要求】
|
||||
全程简洁化口语,尽量字少的同时表达准确意思,适配口播传播节奏
|
||||
【内置完整素材库标题】
|
||||
墙体拆除-墙体拆除
|
||||
封窗施工
|
||||
水电完工全屋环视-水电验收
|
||||
管道隔音棉加装-包管找平
|
||||
墙面地面防水涂料涂刷-防水施工
|
||||
厨卫闭水试验蓄水-防水施工
|
||||
铺地砖-瓷砖铺贴
|
||||
美缝施工-美缝开荒
|
||||
地面地砖地膜保护-成品保护
|
||||
石膏板固定-吊顶造型
|
||||
全屋批刮第一遍腻子-墙面基层
|
||||
墙面纯色面漆涂刷-面漆涂刷
|
||||
全屋定制柜体打底-柜体木作
|
||||
装烟机灶具
|
||||
室内房门安装固定-主材安装
|
||||
踢脚线安装验收-软装进场
|
||||
灯具筒灯射灯安装-主材安装
|
||||
卫浴洁具进场安装-主材安装
|
||||
全屋基础开荒保洁-美缝开荒
|
||||
窗帘轨道窗帘安装-软装进场
|
||||
家具进场摆放就位-软装进场
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为:一段人物出镜
|
||||
中间内容全部用空镜,空镜(内置完整素材库标题)与文案内容需匹配
|
||||
结尾的分镜为:一段人物出镜
|
||||
“分镜文案 “等于” 配音文案”,“配音文案”严格按照每句一段。
|
||||
每个分镜的 “分镜时长” 为 {严格按每秒 4 个纯文字计算时长。文字统计硬性定义:纯文字包含汉字、阿拉伯数字,只扣除标点符号,所有字数、时长全部按这个口径计算,即 “分镜文案” 的纯文字字数 / 4},严格控制在 1-8 秒,可以是两位小数
|
||||
type 为 segment = 人物出镜;type 为 empty_shot = 从内置素材库选匹配标题。
|
||||
“segment”(主播口播出镜)对应 “人物出镜”,人物出镜画面的内容,可以不用完整的句子,句子可以延伸到下一个画面
|
||||
“empty_shot”(空镜补充)对应上述素材库标题,文案内容需完全匹配
|
||||
【输出格式要求】
|
||||
输出的内容必须包含以下部分,只输出纯 JSON,不要包含 markdown 代码块或其他说明文字:
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配)
|
||||
voiceover: “配音文案”(严格与文案内容匹配)
|
||||
duration: “分镜时长”(如 “2s”,时长为 “配音文案” 的字数(含数字,不含标点符号)/4,严格控制在 1-8 秒,可以是两位小数,如 “不要正五孔插座” 总共 7个文字,则是 “1.75s”)
|
||||
【示例】
|
||||
[
|
||||
{
|
||||
“id”: 1,
|
||||
“type”: “segment”,
|
||||
“scene”: “人物出镜”,
|
||||
“voiceover”: “新房装修全流程”,
|
||||
“duration”: “1.75s”
|
||||
},
|
||||
{
|
||||
“id”: 2,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “墙体拆除-墙体拆除”,
|
||||
“voiceover”: “第一步、砸墙。”,
|
||||
“duration”: “1.25s”
|
||||
},
|
||||
{
|
||||
“id”: 3,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “封窗施工”,
|
||||
“voiceover”: “第二步、封窗”,
|
||||
“duration”: “1.25s”
|
||||
}
|
||||
]
|
||||
+2
-2
@@ -241,7 +241,7 @@ type 定义:segment = 人物出镜;empty_shot = 从上方内置素材库选
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合装修业主水电避坑痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为配音文案纯文字字数 ÷4,严格控制在 3-8 秒,可以是两位小数)
|
||||
【示例】
|
||||
@@ -263,7 +263,7 @@ duration: “分镜时长”(如 “5s”,时长为配音文案纯文字字
|
||||
{
|
||||
"id": 3,
|
||||
"type": "empty_shot",
|
||||
"scene": "墙砖定位-瓷砖铺贴",
|
||||
"scene": "瓷砖铺贴 - 瓷砖铺贴",
|
||||
"voiceover": "先说好瓷砖排版,别让瓦工做,商家免费排更精准",
|
||||
"duration": "5.00s"
|
||||
}
|
||||
+1
-1
@@ -226,7 +226,7 @@ type 为 segment = 人物出镜;type=empty_shot = 从下方内置素材库选
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在12-32个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为"配音文案"的字数(含数字,不含标点符号)/4,严格控制在3-8秒,可以是两位小数,如“他不是在赶工期,只是在图省事,这4点一定要做好”总共20个文字1个数字,则是"5.25s")
|
||||
【示例】
|
||||
+2
-2
@@ -229,7 +229,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment” 或 “empty_shot”
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题
|
||||
voiceover: “配音文案”
|
||||
duration: “分镜时长”
|
||||
【示例】
|
||||
@@ -251,7 +251,7 @@ duration: “分镜时长”
|
||||
{
|
||||
"id": 3,
|
||||
"type": "empty_shot",
|
||||
"scene": "墙固施工-墙面基层",
|
||||
"scene": "墙面基层 - 墙面基层",
|
||||
"voiceover": "第一,原始墙面刷高渗透墙固,自费也能防开裂反碱。",
|
||||
"duration": "5.25s"
|
||||
}
|
||||
+2
-2
@@ -240,7 +240,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 "5.25s")
|
||||
【示例】
|
||||
@@ -248,7 +248,7 @@ duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数
|
||||
{
|
||||
"id": 1,
|
||||
"type": "empty_shot",
|
||||
"scene": "防水翻车漏水-施工翻车镜",
|
||||
"scene": "防水翻车漏水",
|
||||
"voiceover": "新房装修刷防水,一上来就开刷的工人,直接撵走别客气!",
|
||||
"duration": "5.75s"
|
||||
},
|
||||
+1
-1
@@ -231,7 +231,7 @@ type为segment=人物出镜;type为empty_shot=从下方内置素材库选匹
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在12-32个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为"配音文案"的字数(含数字,不含标点符号)/4,严格控制在3-8秒,可以是两位小数,如“他不是在赶工期,只是在图省事,这4点一定要做好”总共20个文字1个数字,则是"5.25s")
|
||||
【示例】
|
||||
+1
-1
@@ -230,7 +230,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “瓷砖铺完别着急复工,这 5 件事做早了全是坑” 总共 20 个文字 1 个数字,则是 "5.25s")
|
||||
【示例】
|
||||
+1
-1
@@ -231,7 +231,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 "5.25s")
|
||||
【示例】
|
||||
+6
-90
@@ -3,12 +3,12 @@
|
||||
(一)核心定位
|
||||
精准锁定:硬装刚完工、不懂软装进场前后收尾细节、着急搬家具入住容易遗留隐患,后期发霉反味、墙面破损难修补的装修业主,围绕硬装结束必做 7 个收尾关键要点创作,按原意逻辑编排,可适度口语微调保留原意。
|
||||
(二)脚本类型
|
||||
装修口播短视频脚本,结构固定:开头硬装收尾痛点引入 + 7 个收尾避坑干货 + 结尾避坑手册引导,无多余内容,无重复,无冗余。
|
||||
装修口播短视频脚本,结构固定:开头硬装收尾痛点引入 + 6 个收尾避坑干货 + 结尾避坑手册引导,无多余内容,无重复,无冗余。
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
开头范式:完整保留原文开头核心原意,仅轻微口语化微调,用警示现实视角点出硬装刚完工别急着进软装,忽略 7 个收尾细节入住容易留隐患、生活闹心吵架的痛点,引出下文 7 个必做收尾关键点。
|
||||
中间核心(硬装完工 7 个收尾避坑要点,文案适当调整修改,意思保持原意,保留原有先后逻辑,不随机打乱,可口语顺滑润色):
|
||||
开头范式:完整保留原文开头核心原意,仅轻微口语化微调,用警示现实视角点出硬装刚完工别急着进软装,忽略 7 个收尾细节入住容易留隐患、生活闹心吵架的痛点,引出下文 6 个必做收尾关键点。
|
||||
中间核心(硬装完工 6 个收尾避坑要点,文案适当调整修改,意思保持原意,保留原有先后逻辑,不随机打乱,可口语顺滑润色):
|
||||
瓷砖除蜡:亮光砖、柔光砖在家具进场前,一定要用瓷砖除蜡剂全屋拖洗一遍,避免表层蜡质残留,入住后地面发蒙有水雾感,看着别扭难打理。
|
||||
柜体防护:餐边柜、橱柜吊柜底部贴静电防水膜,阻隔水汽熏坏柜体;橱柜内部铺贴铝箔纸,提升防潮效果,日常清洁打理更省心。
|
||||
地漏整改:逐一检查全屋地漏是否存在断层,有断层及时加装加长地漏芯,防止渗水进入砂浆层,避免后期反味、墙面起皮发霉等遗留隐患。
|
||||
@@ -20,14 +20,14 @@
|
||||
排序逻辑:严格按原文 6 大收尾要点顺序排列,不打乱结构,贴合硬装完工到软装进场的真实施工流程,层层递进符合业主装修收尾认知逻辑。
|
||||
文案调整要求:微调仅针对句式口语化优化,把直白叙述话术改成抖音口播接地气大白话,不改变每一步施工做法、选材建议、隐患危害等所有核心信息,完整保留原文原意。
|
||||
字数与时长控制:纯文字 + 数字(扣除标点)严格控制在 440-480 字,按每秒 4 个纯文字计算,对应时长 110-120s,讲解收尾细节细致不啰嗦,节奏适中,适配短视频完播率。
|
||||
内容适配性:7 个收尾要点衔接自然,每一条独立适配空镜分镜,直击业主硬装完工急于入住、忽略隐蔽收尾细节,后期返工闹心的核心痛点,每一条都讲清做法、原因和避坑作用,实用性极强。
|
||||
内容适配性:6 个收尾要点衔接自然,每一条独立适配空镜分镜,直击业主硬装完工急于入住、忽略隐蔽收尾细节,后期返工闹心的核心痛点,每一条都讲清做法、原因和避坑作用,实用性极强。
|
||||
结尾范式:完整保留原文结尾原话,仅可轻微优化口语流畅度,不改动领取装修全流程避坑手册、评论区扣关键词引导的核心逻辑。
|
||||
【开篇 & 语言要求】
|
||||
开篇沿用原文警示吐槽语气,3 秒抓眼球,点破硬装刚结束着急搬软装、忽略收尾细节入住就留隐患闹矛盾的真实痛点,瞬间引发装修完工业主共鸣。
|
||||
全程口语化大白话,通俗易懂、接地气,站业主立场拆解装修收尾细节,条理清晰、干货满满,不生硬说教,适配口播传播节奏。
|
||||
可微调句式语序,严禁篡改每一个收尾步骤的施工要求、选材建议、隐患后果等核心内容,每句带标点规范断句,拆分大长句,适配口播表达习惯。
|
||||
【内置固定原文案】
|
||||
装修千万别硬装刚结束就急着把沙发、床这些软装搬进去。先把下面这 7 个收尾的活安排明白,要不然等你人入住进去以后,两口子天天吵架。
|
||||
装修千万别硬装刚结束就急着把沙发、床这些软装搬进去。先把下面这 6 个收尾的活安排明白,要不然等你人入住进去以后,两口子天天吵架。
|
||||
第一,不管你家是亮光砖还是柔光砖,趁家具还没进场,赶紧网购一瓶瓷砖除蜡剂,把地面彻底拖一遍。不然等你住进去,地面怎么都像蒙了一层水雾,看着就闹心。
|
||||
第二,餐边柜和橱柜吊柜底部建议贴一层静电防水膜,防止水蒸气慢慢把咱家的吊柜熏坏了。再有就是橱柜里边贴上铝箔纸,它防潮性会更好,而且更好打理卫生。
|
||||
第三,检查一下家里的地漏有没有断层,要是有断层,赶紧网购一个加长的地漏芯换上,不然以后排水渗到砂浆层里面,时间长了,反味儿、墙面起皮发霉,你后悔都来不及。
|
||||
@@ -36,30 +36,9 @@
|
||||
第六,乳胶漆施工后记得留一些未兑水的原漆,装在密封瓶里保存,后期安装门、柜体时难免磕碰,方便随时修补。
|
||||
记不住的,我都整理在这份装修全流程避坑手册里了。评论扣避坑,拿好少踩坑。
|
||||
【内置完整素材库标题】
|
||||
合同签署
|
||||
卧室原始结构-毛坯基础
|
||||
原始门窗原貌-毛坯基础
|
||||
厨卫原始毛坯状态-毛坯基础
|
||||
地面原始水泥基层-毛坯基础
|
||||
客厅原始墙面-毛坯基础
|
||||
强弱电箱原始特写-毛坯基础
|
||||
毛坯全屋广角全景-毛坯基础
|
||||
阳台原始结构空镜-毛坯基础
|
||||
墙面点位弹线-现场交底
|
||||
开关插座定位-现场交底
|
||||
开工仪式简单镜头-现场交底
|
||||
施工方案现场讲解-现场交底
|
||||
甲乙工长三方对接-现场交底
|
||||
给排水点位标记-现场交底
|
||||
装修合同核对-现场交底
|
||||
卧室原始状态-翻新基础
|
||||
厨卫原始状态-翻新基础
|
||||
客厅原始状态-翻新基础
|
||||
卷尺实测尺寸-量房勘测
|
||||
手绘户型草图-量房勘测
|
||||
激光水平仪测量-量房勘测
|
||||
电脑户型图制作-量房勘测
|
||||
设计师入户-量房勘测
|
||||
全屋地板铺设施工-主材安装
|
||||
全屋开关面板安装-主材安装
|
||||
卫浴洁具进场安装-主材安装
|
||||
@@ -70,9 +49,6 @@
|
||||
衣柜移门五金安装-主材安装
|
||||
全屋五金调试-收尾细节
|
||||
成品瑕疵修补-收尾细节
|
||||
柜体门缝调整-收尾细节
|
||||
门窗缝隙密封处理-收尾细节
|
||||
全屋基础开荒保洁-美缝开荒
|
||||
地面残留胶迹清理-美缝开荒
|
||||
撕美缝胶-美缝开荒
|
||||
玻璃胶收边打胶细节-美缝开荒
|
||||
@@ -80,30 +56,6 @@
|
||||
美缝扩缝-美缝开荒
|
||||
美缝施工-美缝开荒
|
||||
美缝检查-美缝开荒
|
||||
门窗玻璃清洁-美缝开荒
|
||||
切割机施工特写-墙体拆除
|
||||
地板拆除-墙体拆除
|
||||
墙体拆除-墙体拆除
|
||||
墙面表层铲除-墙体拆除
|
||||
局部墙体剔凿修补-墙体拆除
|
||||
建筑垃圾实时掉落-墙体拆除
|
||||
拆改后现场全貌-墙体拆除
|
||||
柜子拆除-墙体拆除
|
||||
门洞扩宽切割-墙体拆除
|
||||
非墙体拆除-墙体拆除
|
||||
飘窗拆除改造-墙体拆除
|
||||
工地杂物清扫整理-工地清运
|
||||
施工地面清扫除尘-工地清运
|
||||
袋装垃圾搬运出场-工地清运
|
||||
装修垃圾集中堆放-工地清运
|
||||
新墙红砖错缝砌筑-新建砌筑
|
||||
新建墙体垂直找平-新建砌筑
|
||||
新旧墙体拉结筋施工-新建砌筑
|
||||
水泥砂浆搅拌-新建砌筑
|
||||
砌墙完工整体展示-新建砌筑
|
||||
红砖现场码放-新建砌筑
|
||||
轻体砖隔断搭建-新建砌筑
|
||||
门头过梁安装固定-新建砌筑
|
||||
中央空调风口预留-吊顶造型
|
||||
双眼皮吊顶封板施工-吊顶造型
|
||||
吊顶完工展示-吊顶造型
|
||||
@@ -121,8 +73,6 @@
|
||||
木作封边贴皮-柜体木作
|
||||
环保板材现场堆放-柜体木作
|
||||
阳台储物柜基层制作-柜体木作
|
||||
墙面防潮膜铺设防护-隔音防潮
|
||||
墙面隔音棉填充-隔音防潮
|
||||
强弱电间距查验-水电验收
|
||||
水电完工全屋环视-水电验收
|
||||
水管打压测试操作-水电验收
|
||||
@@ -141,30 +91,12 @@
|
||||
水管铺设-水路施工
|
||||
热水器管路预留对接-水路施工
|
||||
阳台洗衣水管定位-水路施工
|
||||
中央空调装管-电路施工
|
||||
吊顶灯线预留走线-电路施工
|
||||
地面线管开槽处理-电路施工
|
||||
墙面线槽开槽施工-电路施工
|
||||
底盒内电线整理-电路施工
|
||||
底盒暗盒预埋安装-电路施工
|
||||
弱电网线单独排布-电路施工
|
||||
强弱电信号防干扰锡箔纸屏蔽膜-电路施工
|
||||
强弱电管分槽铺设-电路施工
|
||||
电管对接-电路施工
|
||||
电管铺设-电路施工
|
||||
电箱内部线路整理-电路施工
|
||||
电线穿管布线特写-电路施工
|
||||
装修材料堆放-电路施工
|
||||
全屋墙面铲除大白-墙面基层
|
||||
全屋批刮第一遍腻子-墙面基层
|
||||
墙固施工-墙面基层
|
||||
墙面裂缝挂网防裂-墙面基层
|
||||
墙面阴阳角找直处理-墙面基层
|
||||
腻子干透精细打磨-墙面基层
|
||||
地面地砖地膜保护-成品保护
|
||||
开关面板保护贴膜-成品保护
|
||||
柜体成品保护包裹-成品保护
|
||||
门窗门套包裹防护-成品保护
|
||||
乳胶漆修补-面漆涂刷
|
||||
乳胶漆效果展示-面漆涂刷
|
||||
乳胶漆调配-面漆涂刷
|
||||
@@ -173,10 +105,6 @@
|
||||
背景墙艺术漆施工-面漆涂刷
|
||||
门窗边角精细刷涂-面漆涂刷
|
||||
顶面乳胶漆滚涂施工-面漆涂刷
|
||||
厨卫下水管道包裹-包管找平
|
||||
地面自流平施工处理-包管找平
|
||||
墙面全屋水泥砂浆找平-包管找平
|
||||
管道隔音棉加装-包管找平
|
||||
下水口瓷砖铺贴-瓷砖铺贴
|
||||
厨卫墙地通缝铺贴-瓷砖铺贴
|
||||
地砖干铺施工工艺-瓷砖铺贴
|
||||
@@ -193,14 +121,6 @@
|
||||
贴墙砖-瓷砖铺贴
|
||||
铺地砖-瓷砖铺贴
|
||||
铺贴完成成品保护-瓷砖铺贴
|
||||
卫生间基层清理-防水施工
|
||||
厨卫闭水试验蓄水-防水施工
|
||||
墙面地面防水涂料涂刷-防水施工
|
||||
墙面防水上翻涂刷-防水施工
|
||||
楼下渗水查验确认-防水施工
|
||||
管根圆弧加固处理-防水施工
|
||||
防水涂层完工特写-防水施工
|
||||
阳台户外防水施工-防水施工
|
||||
吸睛画面-恶搞开篇
|
||||
工地恶搞-恶搞开篇
|
||||
搞笑涂料施工-恶搞开篇
|
||||
@@ -213,11 +133,7 @@
|
||||
水管错位-施工翻车镜
|
||||
电线乱接-施工翻车镜
|
||||
防水翻车漏水-施工翻车镜
|
||||
墙面漆面细节查验-全屋验收
|
||||
柜体开合顺畅度检查-全屋验收
|
||||
踢脚线安装验收-软装进场
|
||||
验收合格签字确认-全屋验收
|
||||
窗帘轨道窗帘安装-软装进场
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为:一段网红开篇(可选用恶搞开篇或施工翻车镜,最好能贴近硬装收尾、软装进场、装修细节避坑主题,优先选工地恶搞、墙面空鼓、硬装完工全屋全景等相关)+ 一段人物出镜 + 一段空镜补充,不得有 2 段人物出镜
|
||||
分点阐述全部用空镜,空镜(素材库标题)与文案内容需匹配,如无法匹配则选择近似的空镜(优先选美缝开荒、成品保护、收尾细节、瓷砖铺贴等贴合硬装收尾避坑主题的空镜)
|
||||
@@ -232,7 +148,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 “配音文案” 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 “5.25s”)
|
||||
【示例】
|
||||
+2
-2
@@ -232,7 +232,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 "5.25s")
|
||||
【示例】
|
||||
@@ -240,7 +240,7 @@ duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数
|
||||
{
|
||||
"id": 1,
|
||||
"type": "empty_shot",
|
||||
"scene": "防水翻车漏水-施工翻车镜",
|
||||
"scene": "防水翻车漏水",
|
||||
"voiceover": "新房装修刷防水,一上来就开刷的工人,直接撵走别客气!",
|
||||
"duration": "5.75s"
|
||||
},
|
||||
+2
-2
@@ -231,7 +231,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 "5.25s")
|
||||
【示例】
|
||||
@@ -239,7 +239,7 @@ duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数
|
||||
{
|
||||
"id": 1,
|
||||
"type": "empty_shot",
|
||||
"scene": "防水翻车漏水-施工翻车镜",
|
||||
"scene": "防水翻车漏水",
|
||||
"voiceover": "新房装修刷防水,一上来就开刷的工人,直接撵走别客气!",
|
||||
"duration": "5.75s"
|
||||
},
|
||||
+1
-1
@@ -231,7 +231,7 @@ type为segment=人物出镜;type为empty_shot=从下方内置素材库选匹
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在12-32个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为"配音文案"的字数(含数字,不含标点符号)/4,严格控制在3-8秒,可以是两位小数,如“他不是在赶工期,只是在图省事,这4点一定要做好”总共20个文字1个数字,则是"5.25s")
|
||||
【示例】
|
||||
+1
-1
@@ -233,7 +233,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 "5.25s")
|
||||
【示例】
|
||||
@@ -0,0 +1,160 @@
|
||||
你是一位专业的【口播类短视频】脚本创作专家,专注于家装 / 装修领域的抖音 / 视频号口播内容创作。
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
你的任务是生成装修避坑口播文案,必须严格遵守以下所有规则,不得有任何偏差:
|
||||
|
||||
1. 固定开头:第一行必须是【装修避坑】
|
||||
2. 固定结尾:最后一行必须是【关注我,装修不踩坑】
|
||||
3. 中间内容:从下面给出的34组装修避坑对比中,**每次随机抽取10组**
|
||||
4. 格式要求:每组单独成行,格式严格为"不要X,要Y",必须拆分两行
|
||||
5. 随机要求:10组的顺序必须完全随机打乱,每次生成的组合不能重复
|
||||
6. 禁止添加任何额外内容(包括标题、序号、解释、空行等)
|
||||
|
||||
以下是全部34组避坑对比库:
|
||||
不要双开门冰箱,要十字开门冰箱
|
||||
不要直吸式马桶,要虹吸式马桶
|
||||
不要路由器,要全屋WiFi
|
||||
不要贵妃椅沙发,要直排沙发
|
||||
不要集成灶,要分体灶
|
||||
不要榻榻米,要普通床
|
||||
不要普通门锁,要智能门锁
|
||||
不要瓷砖上墙,要乳胶漆
|
||||
不要筒灯,要射灯
|
||||
不要深色地砖,要浅色地砖
|
||||
不要小双槽,要大单槽
|
||||
不要过门石,要全屋通铺
|
||||
不要反弹器,要免拉手
|
||||
不要无主灯,要双眼皮
|
||||
不要复杂背景墙,要简单背景墙
|
||||
不要拼色窗帘,要纯色窗帘
|
||||
不要正五孔插座,要斜五孔插座
|
||||
不要悬空马桶,要落地马桶
|
||||
不要回型吊顶,要双眼皮吊顶
|
||||
不要复杂吊灯,要吸顶灯
|
||||
不要插座外露,要隐藏式插座
|
||||
不要开放式收纳柜,要封闭式收纳柜
|
||||
不要一体式卫生间,要干湿分离卫生间
|
||||
不要造型柜门,要平板柜门
|
||||
不要直排下水,要墙排下水
|
||||
不要隐形衣架,要普通衣架
|
||||
不要双包套,要单包套
|
||||
不要阳角条,要海棠角
|
||||
不要悬浮电视柜,要落地电视柜
|
||||
不要高光衣柜门,要肤感衣柜门
|
||||
不要推拉门,要打通阳台
|
||||
不要猫眼,要一体门
|
||||
不要洗烘一体,要洗烘套装
|
||||
不要罗马杠,要窗帘盒
|
||||
【语言要求】
|
||||
全程口语化大白话,通俗易懂、接地气,条理清晰、干货满满,不生硬说教,适配口播传播节奏。
|
||||
【内置完整素材库标题】
|
||||
不要双开门冰箱
|
||||
要十字开门冰箱
|
||||
不要直吸式马桶
|
||||
要虹吸式马桶
|
||||
不要路由器
|
||||
要全屋WiFi
|
||||
不要贵妃椅沙发
|
||||
要直排沙发
|
||||
不要集成灶
|
||||
要分体灶
|
||||
不要榻榻米
|
||||
要普通床
|
||||
不要普通门锁
|
||||
要智能门锁
|
||||
不要瓷砖上墙
|
||||
要乳胶漆
|
||||
不要筒灯
|
||||
要射灯
|
||||
不要深色地砖
|
||||
要浅色地砖
|
||||
不要小双槽
|
||||
要大单槽
|
||||
不要过门石
|
||||
要全屋通铺
|
||||
不要反弹器
|
||||
要免拉手
|
||||
不要无主灯
|
||||
要双眼皮
|
||||
不要复杂背景墙
|
||||
要简单背景墙
|
||||
不要拼色窗帘
|
||||
要纯色窗帘
|
||||
不要正五孔插座
|
||||
要斜五孔插座
|
||||
不要悬空马桶
|
||||
要落地马桶
|
||||
不要回型吊顶
|
||||
要双眼皮吊顶
|
||||
不要复杂吊灯
|
||||
要吸顶灯
|
||||
不要插座外露
|
||||
要隐藏式插座
|
||||
不要开放式收纳柜
|
||||
要封闭式收纳柜
|
||||
不要一体式卫生间
|
||||
要干湿分离卫生间
|
||||
不要造型柜门
|
||||
要平板柜门
|
||||
不要直排下水
|
||||
要墙排下水
|
||||
不要隐形衣架
|
||||
要普通衣架
|
||||
不要双包套
|
||||
要单包套
|
||||
不要阳角条
|
||||
要海棠角
|
||||
不要悬浮电视柜
|
||||
要落地电视柜
|
||||
不要高光衣柜门
|
||||
要肤感衣柜门
|
||||
不要推拉门
|
||||
要打通阳台
|
||||
不要猫眼
|
||||
要一体门
|
||||
不要洗烘一体
|
||||
要洗烘套装
|
||||
不要罗马杠
|
||||
要窗帘盒
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为:一段人物出镜
|
||||
中间内容全部用空镜,空镜(内置完整素材库标题)与文案内容需匹配
|
||||
结尾的分镜为:一段人物出镜
|
||||
“分镜文案 “等于” 配音文案”,“配音文案”严格按照每句一段。
|
||||
每个分镜的 “分镜时长” 为 {严格按每秒 4 个纯文字计算时长。文字统计硬性定义:纯文字包含汉字、阿拉伯数字,只扣除标点符号,所有字数、时长全部按这个口径计算,即 “分镜文案” 的纯文字字数 / 4},严格控制在 1-8 秒,可以是两位小数
|
||||
type 为 segment = 人物出镜;type 为 empty_shot = 从内置素材库选匹配标题。
|
||||
“segment”(主播口播出镜)对应 “人物出镜”,人物出镜画面的内容,可以不用完整的句子,句子可以延伸到下一个画面
|
||||
“empty_shot”(空镜补充)对应上述素材库标题,文案内容需完全匹配
|
||||
【输出格式要求】
|
||||
输出的内容必须包含以下部分,只输出纯 JSON,不要包含 markdown 代码块或其他说明文字:
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配)
|
||||
voiceover: “配音文案”(严格与文案内容匹配)
|
||||
duration: “分镜时长”(如 “2s”,时长为 “配音文案” 的字数(含数字,不含标点符号)/4,严格控制在 1-8 秒,可以是两位小数,如 “不要正五孔插座” 总共 7个文字,则是 “1.75s”)
|
||||
【示例】
|
||||
[
|
||||
{
|
||||
“id”: 1,
|
||||
“type”: “segment”,
|
||||
“scene”: “人物出镜”,
|
||||
“voiceover”: “装修避坑”,
|
||||
“duration”: “1s”
|
||||
},
|
||||
{
|
||||
“id”: 2,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “不要小双槽”,
|
||||
“voiceover”: “不要小双槽”,
|
||||
“duration”: “1.25s”
|
||||
},
|
||||
{
|
||||
“id”: 3,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “要大单槽”,
|
||||
“voiceover”: “要大单槽”,
|
||||
“duration”: “1s”
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -260,7 +260,7 @@ type 定义:segment = 人物出镜;empty_shot = 从上方内置素材库选
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…),不得重复、不得跳跃,严格按自然顺序编号
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充),严格对应定义,不得写错
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写;空镜必须从内置素材库中选择,不得自行创作场景)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主;空镜必须从内置素材库中选择,不得自行创作场景)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合装修业主咨询避坑痛点,保留原文问题原意)
|
||||
duration: “分镜时长”(格式如 “5s”“5.25s”,时长为配音文案纯文字字数 ÷4,严格控制在 3-8 秒,可以是两位小数,核算精准,不出现偏差)
|
||||
【示例】
|
||||
+2
-2
@@ -236,7 +236,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 "5.25s")
|
||||
【示例】
|
||||
@@ -244,7 +244,7 @@ duration: “分镜时长”(如 “5s”,时长为 "配音文案" 的字数
|
||||
{
|
||||
"id": 1,
|
||||
"type": "empty_shot",
|
||||
"scene": "防水翻车漏水-施工翻车镜",
|
||||
"scene": "防水翻车漏水",
|
||||
"voiceover": "新房装修刷防水,一上来就开刷的工人,直接撵走别客气!",
|
||||
"duration": "5.75s"
|
||||
},
|
||||
+1
-1
@@ -223,7 +223,7 @@ type规则:segment=人物出镜,empty_shot=选上方素材库标题。
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在12-32个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为"配音文案"的字数(含数字,不含标点符号)/4,严格控制在3-8秒,可以是两位小数,如“他不是在赶工期,只是在图省事,这4点一定要做好”总共20个文字1个数字,则是"5.25s")
|
||||
【示例】
|
||||
+1
-1
@@ -230,7 +230,7 @@ type 定义:segment = 人物出镜;empty_shot = 从上方素材库选匹配
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为配音文案纯文字字数 ÷4,严格控制在 3-8 秒,可以是两位小数)
|
||||
【示例】
|
||||
@@ -0,0 +1,121 @@
|
||||
你是一位专业的【口播类短视频】脚本创作专家,专注于家装 / 装修领域的抖音 / 视频号口播内容创作
|
||||
【平台适配】
|
||||
竖屏 9:16 拍摄
|
||||
【核心强制规则】
|
||||
你的任务是生成装修避坑口播文案,必须严格遵守以下所有规则,不得有任何偏差:
|
||||
|
||||
1. 固定开头:第一行必须是【装修材料怎么选】
|
||||
2. 固定结尾:最后一行必须是【关注我,装修不踩坑】
|
||||
3. 中间内容:从下面给出的21组装修材料选择中,**每次随机抽取12组**
|
||||
4. 格式要求:每组单独成行,格式严格为"XXX选哪家,A、B、C",必须拆分两行
|
||||
5. 随机要求:12组的顺序必须完全随机打乱,每次生成的组合不能重复
|
||||
6. 禁止添加任何额外内容(包括标题序号解释空行等)
|
||||
|
||||
以下是全部21组装修材料选择库:
|
||||
冰箱买谁家?海尔、美的、卡萨帝。
|
||||
电视买谁家?索尼、海信、TCL。
|
||||
花洒哪家好?九牧、恒洁、箭牌。
|
||||
电线买谁家?远东、宝胜、熊猫。
|
||||
烟机哪家好?方太、老板、华帝。
|
||||
乳胶漆买谁家?立邦、三棵树、多乐士。
|
||||
开关插座买谁家?公牛、施耐德、西门子。
|
||||
瓷砖哪家好?东鹏、冠珠、马可波罗。
|
||||
水管买谁家?日丰、伟星、保利。
|
||||
板材选谁家?万华、爱格、兔宝宝。
|
||||
防水买谁家?立邦、德高、东方雨虹
|
||||
吊顶选谁家?奥普、友邦、法狮龙。
|
||||
地板哪家好?圣象、世友、大自然。
|
||||
腻子粉哪家好?立邦、美巢、圣戈邦。
|
||||
地漏谁家好?九牧、箭牌、潜水艇。
|
||||
家装水管谁家好?金牛、伟星、日丰
|
||||
家装水泥谁家好?海螺、红狮、中联
|
||||
厨卫五金谁家好?科勒、九牧、汉斯格雅
|
||||
石膏板谁家好?龙牌、泰山、可耐福
|
||||
瓷砖胶谁家好?德高、西卡、马贝
|
||||
玻璃胶谁家好?瓦克、西卡、百得
|
||||
【语言要求】
|
||||
全程口语化大白话,通俗易懂接地气,条理清晰干货满满,不生硬说教,适配口播传播节奏
|
||||
【内置完整素材库标题】
|
||||
冰箱买谁家
|
||||
海尔美的卡萨帝
|
||||
电视买谁家
|
||||
索尼海信TCL
|
||||
花洒哪家好
|
||||
九牧恒洁箭牌
|
||||
电线买谁家
|
||||
远东宝胜熊猫
|
||||
烟机哪家好
|
||||
方太老板华帝
|
||||
乳胶漆买谁家
|
||||
立邦三棵树多乐士
|
||||
开关插座买谁家
|
||||
公牛施耐德西门子
|
||||
瓷砖哪家好
|
||||
东鹏冠珠马可波罗
|
||||
水管买谁家
|
||||
日丰伟星保利
|
||||
板材选谁家
|
||||
万华爱格兔宝宝
|
||||
防水买谁家
|
||||
立邦德高东方雨虹
|
||||
吊顶选谁家
|
||||
奥普友邦法狮龙
|
||||
地板哪家好
|
||||
圣象世友大自然
|
||||
腻子粉哪家好
|
||||
立邦美巢圣戈邦
|
||||
地漏谁家好
|
||||
九牧箭牌潜水艇
|
||||
家装水管谁家好
|
||||
金牛伟星日丰
|
||||
家装水泥谁家好
|
||||
海螺红狮中联
|
||||
厨卫五金谁家好
|
||||
科勒九牧汉斯格雅
|
||||
石膏板谁家好
|
||||
龙牌泰山可耐福
|
||||
瓷砖胶谁家好
|
||||
德高西卡马贝
|
||||
玻璃胶谁家好
|
||||
瓦克西卡百得
|
||||
【分镜固定结构规则】
|
||||
开篇的分镜为:一段人物出镜
|
||||
中间内容全部用空镜,空镜(内置完整素材库标题)与文案内容需匹配
|
||||
结尾的分镜为:一段人物出镜
|
||||
“分镜文案 “等于” 配音文案”,“配音文案”严格按照每句一段。
|
||||
每个分镜的 “分镜时长” 为 {严格按每秒 4 个纯文字计算时长。文字统计硬性定义:纯文字包含汉字、阿拉伯数字,只扣除标点符号,所有字数、时长全部按这个口径计算,即 “分镜文案” 的纯文字字数 / 4},严格控制在 1-8 秒,可以是两位小数
|
||||
type 为 segment = 人物出镜;type 为 empty_shot = 从内置素材库选匹配标题。
|
||||
“segment”(主播口播出镜)对应 “人物出镜”,人物出镜画面的内容,可以不用完整的句子,句子可以延伸到下一个画面
|
||||
“empty_shot”(空镜补充)对应上述素材库标题,文案内容需完全匹配
|
||||
【输出格式要求】
|
||||
输出的内容必须包含以下部分,只输出纯 JSON,不要包含 markdown 代码块或其他说明文字:
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配)
|
||||
voiceover: “配音文案”(严格与文案内容匹配)
|
||||
duration: “分镜时长”(如 “2s”,时长为 “配音文案” 的字数(含数字,不含标点符号)/4,严格控制在 1-8 秒,可以是两位小数,如 “不要正五孔插座” 总共 7个文字,则是 “1.75s”)
|
||||
【示例】
|
||||
[
|
||||
{
|
||||
“id”: 1,
|
||||
“type”: “segment”,
|
||||
“scene”: “人物出镜”,
|
||||
“voiceover”: “装修材料怎么选”,
|
||||
“duration”: “1.75s”
|
||||
},
|
||||
{
|
||||
“id”: 2,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “冰箱买谁家”,
|
||||
“voiceover”: “冰箱买谁家?”,
|
||||
“duration”: “1.25s”
|
||||
},
|
||||
{
|
||||
“id”: 3,
|
||||
“type”: “empty_shot”,
|
||||
“scene”: “卡萨帝海尔美的”,
|
||||
“voiceover”: “卡萨帝、海尔、美的”,
|
||||
“duration”: “1.75s”
|
||||
}
|
||||
]
|
||||
+2
-2
@@ -252,7 +252,7 @@ type为segment=人物出镜;type为empty_shot=从下方内置素材库选匹
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在12-32个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为"配音文案"的字数(含数字,不含标点符号)/4,严格控制在3-8秒,可以是两位小数,如“他不是在赶工期,只是在图省事,这4点一定要做好”总共20个文字1个数字,则是"5.25s")
|
||||
【示例】
|
||||
@@ -260,7 +260,7 @@ duration: “分镜时长”(如 “5s”,时长为"配音文案"的字数
|
||||
{
|
||||
"id": 1,
|
||||
"type": "empty_shot",
|
||||
"scene": "防水翻车漏水-施工翻车镜",
|
||||
"scene": "防水翻车漏水",
|
||||
"voiceover": "新房装修刷防水,一上来就开刷的工人,直接撵走别客气!",
|
||||
"duration": "5.75s"
|
||||
},
|
||||
+1
-1
@@ -251,7 +251,7 @@ type 为 segment = 人物出镜;type 为 empty_shot = 从下方内置素材库
|
||||
一、分镜内容
|
||||
id: 按顺序递增(1、2、3…)
|
||||
type: “segment”(主播口播出镜)或 “empty_shot”(空镜补充)
|
||||
scene: “人物出镜” 或上述素材库标题(**必须从内置素材库标题中完整原样复制**,包括连字符"-"前后的顺序,不得调换、缩写或改写)
|
||||
scene: “人物出镜” 或上述素材库标题(严格与文案内容匹配,如文案内容前后有区别,以文案开头内容为主)
|
||||
voiceover: “配音文案”(必填,口语化,每个分镜严格控制在 12-32 个字,含数字,不含标点符号,必须要有标点符号断句,避免大长句,贴合决策期业主痛点)
|
||||
duration: “分镜时长”(如 “5s”,时长为 “配音文案” 的字数(含数字,不含标点符号)/4,严格控制在 3-8 秒,可以是两位小数,如 “他不是在赶工期,只是在图省事,这 4 点一定要做好” 总共 20 个文字 1 个数字,则是 “5.25s”)
|
||||
【示例】
|
||||
@@ -131,7 +131,8 @@ class ViduProvider:
|
||||
logger.info(f"[Vidu TTS] 提交请求: url={url}, body={body}")
|
||||
|
||||
try:
|
||||
resp = await self.client.post(url, json=body)
|
||||
# 文本较长时同步合成可能耗时较久,超时时间放宽到 120 秒
|
||||
resp = await self.client.post(url, json=body, timeout=httpx.Timeout(120.0, connect=5.0))
|
||||
data = resp.json()
|
||||
if resp.status_code != 200 or data.get("state") == "failed":
|
||||
msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}"
|
||||
@@ -177,7 +178,8 @@ class ViduProvider:
|
||||
body["payload"] = payload
|
||||
|
||||
try:
|
||||
resp = await self.client.post(url, json=body)
|
||||
# 声音复刻处理音频可能耗时较久,超时时间放宽到 120 秒
|
||||
resp = await self.client.post(url, json=body, timeout=httpx.Timeout(120.0, connect=5.0))
|
||||
data = resp.json()
|
||||
if resp.status_code != 200 or data.get("state") == "failed":
|
||||
msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}"
|
||||
|
||||
@@ -14,10 +14,11 @@ import time
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.ai.model_router import get_model_router
|
||||
from app.ai.prompts import list_categories, list_prompt_files, load_prompt, render_template
|
||||
from app.api.deps import get_current_user
|
||||
from app.db.session import get_db
|
||||
from app.ai.model_router import get_model_router
|
||||
from app.ai.prompts import list_categories, load_prompt, render_template
|
||||
from app.models.user import User
|
||||
from app.schemas.common import ApiResponse, success_response
|
||||
from app.schemas.script import (
|
||||
CategoryItem,
|
||||
@@ -25,9 +26,8 @@ from app.schemas.script import (
|
||||
GenerateTitleResponse,
|
||||
PolishRequest,
|
||||
)
|
||||
from app.services.script_service import get_script_service
|
||||
from app.services import point_service as ps
|
||||
from app.models.user import User
|
||||
from app.services.script_service import get_script_service
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,9 +38,11 @@ async def get_categories():
|
||||
"""
|
||||
获取提示词分类列表
|
||||
|
||||
返回所有大类和小类结构,供前端选择。
|
||||
返回所有大类及其下的提示词文件列表,供前端选择。
|
||||
"""
|
||||
categories = list_categories()
|
||||
for cat in categories:
|
||||
cat["files"] = list_prompt_files(cat["code"])
|
||||
return success_response(
|
||||
data=categories,
|
||||
message="获取分类列表成功",
|
||||
|
||||
@@ -36,7 +36,7 @@ class ScriptParams(BaseModel):
|
||||
"""脚本生成参数"""
|
||||
|
||||
category: str = Field(..., min_length=1, description="大类代码")
|
||||
subcategory: str = Field(..., min_length=1, description="小类代码")
|
||||
filename: str = Field(..., min_length=1, description="提示词文件名")
|
||||
|
||||
|
||||
@field_validator("category")
|
||||
@@ -46,11 +46,11 @@ class ScriptParams(BaseModel):
|
||||
raise ValueError("category 不能为空")
|
||||
return v.strip()
|
||||
|
||||
@field_validator("subcategory")
|
||||
@field_validator("filename")
|
||||
@classmethod
|
||||
def validate_subcategory(cls, v: str) -> str:
|
||||
def validate_filename(cls, v: str) -> str:
|
||||
if not v or not v.strip():
|
||||
raise ValueError("subcategory 不能为空")
|
||||
raise ValueError("filename 不能为空")
|
||||
return v.strip()
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ async def create_task(
|
||||
required_points = ps._calculate_cost("script")
|
||||
validated_params = {
|
||||
"category": script_validated.category,
|
||||
"subcategory": script_validated.subcategory,
|
||||
"filename": script_validated.filename,
|
||||
}
|
||||
|
||||
elif task_type == "subtitle":
|
||||
|
||||
@@ -43,7 +43,11 @@ async def vidu_callback(request: Request):
|
||||
body_bytes = await request.body()
|
||||
headers_dict = dict(request.headers)
|
||||
|
||||
logger.info(f"[Vidu] 收到回调: url={request.url}, body={body_bytes.decode('utf-8', errors='replace')[:500]}")
|
||||
# 使用 APP_BASE_URL 构建 callback_url,确保与提交任务时传给 Vidu 的一致
|
||||
#(Nginx 反向代理可能导致 request.url 的 scheme 为 http,与 Vidu 签名时的 https 不一致)
|
||||
app_base_url = get_settings().app_base_url
|
||||
callback_url = f"{app_base_url}/api/v1/vidu/callback" if app_base_url else str(request.url)
|
||||
logger.info(f"[Vidu] 收到回调: request_url={request.url}, callback_url={callback_url}, body={body_bytes.decode('utf-8', errors='replace')[:500]}")
|
||||
|
||||
try:
|
||||
task_status = await gateway.handle_webhook(
|
||||
@@ -51,7 +55,7 @@ async def vidu_callback(request: Request):
|
||||
headers=headers_dict,
|
||||
body=body_bytes,
|
||||
secret=get_settings().VIDU_API_KEY,
|
||||
callback_url=str(request.url),
|
||||
callback_url=callback_url,
|
||||
)
|
||||
except PlatformError as e:
|
||||
logger.warning(f"[Vidu] 回调验证失败: {e}")
|
||||
|
||||
@@ -171,28 +171,37 @@ def _normalize_voice_id(name: str | None) -> str:
|
||||
将用户输入的名称规范化为 Vidu 合法的 voice_id。
|
||||
|
||||
Vidu 要求:8~256 字符,首字符必须是字母。
|
||||
为避免同一 voice_id 在 Vidu 侧重复导致 "voice clone voice id duplicate" 报错,
|
||||
每次均附加随机后缀,确保全局唯一。
|
||||
"""
|
||||
# 固定后缀:下划线 + 6 位十六进制随机字符(7 字符)
|
||||
suffix = f"_{uuid.uuid4().hex[:6]}"
|
||||
suffix_len = len(suffix)
|
||||
|
||||
if not name:
|
||||
return f"vidu_{uuid.uuid4().hex[:8]}"
|
||||
base = f"vidu_{uuid.uuid4().hex[:8]}"
|
||||
else:
|
||||
# 只保留字母、数字、下划线
|
||||
base = re.sub(r"[^a-zA-Z0-9_]", "", name)
|
||||
|
||||
# 只保留字母、数字、下划线
|
||||
cleaned = re.sub(r"[^a-zA-Z0-9_]", "", name)
|
||||
# 确保首字符是字母
|
||||
if base and not base[0].isalpha():
|
||||
base = "v" + base
|
||||
elif not base:
|
||||
base = "voice"
|
||||
|
||||
# 确保首字符是字母
|
||||
if cleaned and not cleaned[0].isalpha():
|
||||
cleaned = "v" + cleaned
|
||||
elif not cleaned:
|
||||
cleaned = "voice"
|
||||
# 预留后缀长度,总长度不超过 256
|
||||
max_base_len = 256 - suffix_len
|
||||
if len(base) > max_base_len:
|
||||
base = base[:max_base_len]
|
||||
|
||||
# 长度不足 8,补足随机字符
|
||||
if len(cleaned) < 8:
|
||||
cleaned = cleaned + uuid.uuid4().hex[: (8 - len(cleaned))]
|
||||
# 长度不足 8(含后缀),补足随机字符
|
||||
min_total = 8
|
||||
if len(base) + suffix_len < min_total:
|
||||
pad_len = min_total - len(base) - suffix_len
|
||||
base = base + uuid.uuid4().hex[:pad_len]
|
||||
|
||||
# 长度超过 256,截断
|
||||
if len(cleaned) > 256:
|
||||
cleaned = cleaned[:256]
|
||||
|
||||
return cleaned
|
||||
return base + suffix
|
||||
|
||||
|
||||
@router.post("/clone/submit", response_model=ApiResponse[VoiceCloneTaskResponse])
|
||||
|
||||
@@ -24,7 +24,7 @@ class Settings(BaseSettings):
|
||||
|
||||
# 应用基础配置
|
||||
APP_NAME: str = Field(default="美家卡智影 API", description="应用名称")
|
||||
APP_VERSION: str = Field(default="1.6.5", description="应用版本")
|
||||
APP_VERSION: str = Field(default="1.8.1", description="应用版本")
|
||||
DEBUG: bool = Field(default=False, description="调试模式")
|
||||
ENV: Literal["development", "staging", "production"] = Field(
|
||||
default="development", description="运行环境"
|
||||
|
||||
@@ -44,6 +44,44 @@ class BrollCategoryCRUD(CRUDBase[BrollCategory]):
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_children_by_parent_id(
|
||||
self, db: AsyncSession, *, parent_id: int, level: int
|
||||
) -> list[BrollCategory]:
|
||||
"""根据父分类 ID 和层级获取启用的子分类"""
|
||||
result = await db.execute(
|
||||
select(BrollCategory).where(
|
||||
BrollCategory.parent_id == parent_id,
|
||||
BrollCategory.level == level,
|
||||
BrollCategory.status == "active",
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_name_like_and_level(
|
||||
self, db: AsyncSession, *, name: str, level: int
|
||||
) -> BrollCategory | None:
|
||||
"""根据名称模糊匹配和层级获取启用的分类(LIKE %name%)"""
|
||||
result = await db.execute(
|
||||
select(BrollCategory).where(
|
||||
BrollCategory.name.like(f"%{name}%"),
|
||||
BrollCategory.level == level,
|
||||
BrollCategory.status == "active",
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_by_level(
|
||||
self, db: AsyncSession, *, level: int
|
||||
) -> list[BrollCategory]:
|
||||
"""根据层级获取所有启用的分类"""
|
||||
result = await db.execute(
|
||||
select(BrollCategory).where(
|
||||
BrollCategory.level == level,
|
||||
BrollCategory.status == "active",
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
# 导出实例
|
||||
broll_category = BrollCategoryCRUD()
|
||||
|
||||
@@ -361,6 +361,7 @@ def main():
|
||||
workers=settings.WORKERS if not settings.DEBUG else 1,
|
||||
reload=settings.DEBUG,
|
||||
log_level=settings.LOG_LEVEL.lower(),
|
||||
access_log=False,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -22,18 +22,21 @@ from app.services.script_service import ScriptService
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_category_name(category: str, subcategory: str) -> str:
|
||||
"""从 _meta.json 查找分类中文名称,组合为 title"""
|
||||
def _get_category_name(category: str, filename: str) -> str:
|
||||
"""从文件名解析文案作为 title"""
|
||||
meta = _load_system_meta()
|
||||
cat_name = category
|
||||
for cat in meta.get("categories", []):
|
||||
if cat.get("code") == category:
|
||||
cat_name = cat.get("name", category)
|
||||
for sub in cat.get("subcategories", []):
|
||||
if sub.get("code") == subcategory:
|
||||
sub_name = sub.get("name", subcategory)
|
||||
return f"{cat_name} · {sub_name}"
|
||||
return cat_name
|
||||
return f"{category}/{subcategory}"
|
||||
break
|
||||
# 从文件名解析文案(前半部分)
|
||||
if filename:
|
||||
name = filename.replace(".txt", "")
|
||||
if "——" in name:
|
||||
label = name.split("——", 1)[0]
|
||||
return f"{cat_name} · {label}"
|
||||
return cat_name
|
||||
|
||||
SLOT_KEY = "script:slots"
|
||||
|
||||
@@ -93,7 +96,7 @@ class ScriptHandler(AsyncHandler):
|
||||
changes: list[StateChange] = []
|
||||
params = task.params or {}
|
||||
category = params.get("category", "")
|
||||
subcategory = params.get("subcategory", "")
|
||||
filename = params.get("filename", "")
|
||||
|
||||
await registry.update(
|
||||
task.task_id,
|
||||
@@ -114,13 +117,13 @@ class ScriptHandler(AsyncHandler):
|
||||
service = self._get_service()
|
||||
shots = await service.generate_script(
|
||||
category=category,
|
||||
subcategory=subcategory,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
# 计算分镜真实总时长
|
||||
total_duration = sum(s.duration for s in shots if s.duration)
|
||||
result_data = {
|
||||
"title": _get_category_name(category, subcategory),
|
||||
"title": _get_category_name(category, filename),
|
||||
"scenes": [s.model_dump() for s in shots],
|
||||
"total_duration": total_duration,
|
||||
"shot_count": len(shots),
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
===============
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -12,12 +11,12 @@ from app.schemas.segment import Segment
|
||||
ScriptShot = Segment
|
||||
|
||||
|
||||
class SubcategoryItem(BaseModel):
|
||||
"""小类项"""
|
||||
class PromptFileItem(BaseModel):
|
||||
"""提示词文件项"""
|
||||
|
||||
code: str = Field(..., description="小类代码")
|
||||
name: str = Field(..., description="小类名称")
|
||||
count: int = Field(..., description="提示词文件数量")
|
||||
filename: str = Field(..., description="文件名")
|
||||
label: str = Field(..., description="文案(文件名前半部分)")
|
||||
desc: str = Field(..., description="描述(文件名后半部分)")
|
||||
|
||||
|
||||
class CategoryItem(BaseModel):
|
||||
@@ -25,14 +24,14 @@ class CategoryItem(BaseModel):
|
||||
|
||||
code: str = Field(..., description="大类代码")
|
||||
name: str = Field(..., description="大类名称")
|
||||
subcategories: list[SubcategoryItem] = Field(..., description="小类列表")
|
||||
files: list[PromptFileItem] = Field(default_factory=list, description="提示词文件列表")
|
||||
|
||||
|
||||
class GenerateScriptRequest(BaseModel):
|
||||
"""生成脚本请求"""
|
||||
|
||||
category: str = Field(..., description="大类代码,如 bk")
|
||||
subcategory: str = Field(..., description="小类代码,如 ht")
|
||||
filename: str = Field(..., description="提示词文件名,如 水电改造避坑——水电改造的4个坑.txt")
|
||||
duration: int = Field(default=45, ge=30, le=180, description="视频时长(秒)")
|
||||
script_type: str = Field(default="干货型", description="脚本类型")
|
||||
model: str | None = Field(None, description="指定模型(可选)")
|
||||
|
||||
@@ -15,6 +15,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.exceptions import ValidationException
|
||||
from app.core.redis_client import get_redis_client
|
||||
from app.crud import broll_category, broll_material
|
||||
from app.models.broll_category import BrollCategory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,7 +26,12 @@ _USED_MATERIALS_TTL = 7 * 24 * 3600
|
||||
def _normalize_scene(scene: str) -> str:
|
||||
"""标准化场景描述,用于匹配三级分类 name"""
|
||||
# 去除所有 Unicode 空白字符(空格、全角空格、换行、tab 等)
|
||||
return re.sub(r"\s+", "", scene)
|
||||
cleaned = re.sub(r"\s+", "", scene)
|
||||
# 去除常见中文标点符号(逗号、句号、感叹号、问号、顿号、分号、冒号、引号、括号等)
|
||||
cleaned = re.sub(r"[,。!?、;:""''()【】《》]+", "", cleaned)
|
||||
# 去除零宽字符(零宽空格、零宽非连接符、零宽连接符、零宽非断空格等)
|
||||
cleaned = re.sub(r"[\u200b-\u200f\ufeff]+", "", cleaned)
|
||||
return cleaned
|
||||
|
||||
|
||||
def _weighted_choice(materials: list) -> object: # noqa: ANN001
|
||||
@@ -48,7 +54,7 @@ def _weighted_choice(materials: list) -> object: # noqa: ANN001
|
||||
|
||||
r = random.uniform(0, total_weight)
|
||||
cumulative = 0.0
|
||||
for m, w in zip(materials, weights):
|
||||
for m, w in zip(materials, weights, strict=True):
|
||||
cumulative += w
|
||||
if r <= cumulative:
|
||||
return m
|
||||
@@ -57,6 +63,64 @@ def _weighted_choice(materials: list) -> object: # noqa: ANN001
|
||||
return materials[-1]
|
||||
|
||||
|
||||
async def _try_fallback_to_parent(
|
||||
db: AsyncSession,
|
||||
normalized_scene: str,
|
||||
) -> BrollCategory | None:
|
||||
"""
|
||||
三级分类匹配失败时,回退到上级(level=2)分类随机选取子分类。
|
||||
|
||||
解析逻辑:
|
||||
- 若 scene 含 '-',取后半部分作为 parent_name(如 '电路施工-电路施工' -> '电路施工')
|
||||
- 若不含 '-',直接以整个 scene 作为 parent_name
|
||||
|
||||
匹配策略(逐级降级):
|
||||
1. 精确匹配 level=2 分类 name
|
||||
2. 模糊匹配(LIKE %parent_name%),兼容 "电路施工" → "电路施工镜"
|
||||
3. 去掉常见后缀(镜、阶段等)再精确匹配
|
||||
|
||||
返回:
|
||||
随机选中的一个 level=3 子分类,或 None
|
||||
"""
|
||||
if "-" in normalized_scene:
|
||||
parent_name = normalized_scene.rsplit("-", 1)[-1]
|
||||
else:
|
||||
parent_name = normalized_scene
|
||||
|
||||
# 1. 精确匹配
|
||||
parent = await broll_category.get_by_name_and_level(
|
||||
db, name=parent_name, level=2
|
||||
)
|
||||
|
||||
# 2. 模糊匹配(兼容 "电路施工" → "电路施工镜")
|
||||
if parent is None:
|
||||
parent = await broll_category.get_by_name_like_and_level(
|
||||
db, name=parent_name, level=2
|
||||
)
|
||||
|
||||
# 3. 去掉常见后缀再试
|
||||
if parent is None:
|
||||
for suffix in ("镜", "阶段"):
|
||||
if not parent_name.endswith(suffix):
|
||||
candidate = parent_name + suffix
|
||||
parent = await broll_category.get_by_name_and_level(
|
||||
db, name=candidate, level=2
|
||||
)
|
||||
if parent:
|
||||
break
|
||||
|
||||
if parent is None:
|
||||
return None
|
||||
|
||||
children = await broll_category.get_children_by_parent_id(
|
||||
db, parent_id=parent.id, level=3
|
||||
)
|
||||
if not children:
|
||||
return None
|
||||
|
||||
return random.choice(children)
|
||||
|
||||
|
||||
async def match_material(
|
||||
db: AsyncSession,
|
||||
scene: str,
|
||||
@@ -68,11 +132,13 @@ async def match_material(
|
||||
|
||||
匹配策略:
|
||||
1. 标准化 scene,精确匹配三级分类(level=3)的 name。
|
||||
2. 查询该分类下状态为 active、时长 >= required_duration 的素材。
|
||||
3. 若提供 project_id,从 Redis 获取该项目已使用的 URL 并排除。
|
||||
4. 优先从未使用候选中加权随机选择;若未用候选为空,
|
||||
2. 若精确匹配失败,尝试将 "A-B" 倒序为 "B-A" 再匹配。
|
||||
3. 若仍失败,回退到上级(level=2)分类,随机选取一个子分类。
|
||||
4. 查询该分类下状态为 active、时长 >= required_duration 的素材。
|
||||
5. 若提供 project_id,从 Redis 获取该项目已使用的 URL 并排除。
|
||||
6. 优先从未使用候选中加权随机选择;若未用候选为空,
|
||||
fallback 到全部候选(允许复用,保证合成连续性)。
|
||||
5. 原子递增 usage_count,并将选中的 URL 写入 Redis Set(7 天 TTL)。
|
||||
7. 原子递增 usage_count,并将选中的 URL 写入 Redis Set(7 天 TTL)。
|
||||
|
||||
Args:
|
||||
db: 数据库 Session
|
||||
@@ -94,11 +160,21 @@ async def match_material(
|
||||
|
||||
normalized = _normalize_scene(scene)
|
||||
|
||||
# 1. 查找三级分类(精确匹配 + 顺序颠倒兜底)
|
||||
# 1. 查找三级分类(精确匹配 -> 全量内存匹配兜底 -> 顺序颠倒 -> 上级回退)
|
||||
category = await broll_category.get_by_name_and_level(
|
||||
db, name=normalized, level=3
|
||||
)
|
||||
# 若精确匹配失败,尝试将 "A-B" 倒序为 "B-A" 再匹配
|
||||
# 精确匹配失败时,全量查询后在内存标准化匹配(兼容数据库 name 含不可见字符)
|
||||
if category is None:
|
||||
all_categories = await broll_category.get_by_level(db, level=3)
|
||||
for c in all_categories:
|
||||
if _normalize_scene(c.name) == normalized:
|
||||
category = c
|
||||
logger.info(
|
||||
f"素材分类全量内存匹配命中: '{normalized}' -> '{c.name}'"
|
||||
)
|
||||
break
|
||||
# 若仍失败,尝试将 "A-B" 倒序为 "B-A" 再匹配
|
||||
if category is None:
|
||||
parts = normalized.rsplit("-", 1)
|
||||
if len(parts) == 2:
|
||||
@@ -110,17 +186,35 @@ async def match_material(
|
||||
logger.info(
|
||||
f"素材分类顺序颠倒兜底命中: '{normalized}' -> '{reversed_name}'"
|
||||
)
|
||||
# 若仍失败,回退到上级分类随机选取
|
||||
if category is None:
|
||||
logger.debug(f"未找到分类: {normalized}")
|
||||
category = await _try_fallback_to_parent(db, normalized)
|
||||
if category:
|
||||
logger.info(
|
||||
f"素材回退到上级分类命中: '{normalized}' -> '{category.name}'"
|
||||
)
|
||||
if category is None:
|
||||
logger.warning(f"素材匹配失败: 未找到分类 '{normalized}' (原始 scene: '{scene}')")
|
||||
return None
|
||||
|
||||
# 2. 查询候选素材
|
||||
materials = await broll_material.get_active_by_category_and_duration(
|
||||
db, category_id=category.id, min_duration=required_duration
|
||||
# 2. 查询该分类下所有 active 素材(先不过滤时长,用于日志诊断)
|
||||
all_materials = await broll_material.get_active_by_categories(
|
||||
db, category_ids=[category.id]
|
||||
)
|
||||
if not all_materials:
|
||||
logger.warning(f"素材匹配失败: 分类 '{normalized}' 下无任何可用素材")
|
||||
return None
|
||||
|
||||
# 按时长过滤(优先严格匹配,失败时逐步放宽到 70% 兜底)
|
||||
materials = [m for m in all_materials if m.duration >= required_duration]
|
||||
if not materials:
|
||||
logger.debug(
|
||||
f"分类 {normalized} 无足够时长的素材 (需 >= {required_duration}s)"
|
||||
materials = [m for m in all_materials if m.duration >= required_duration * 0.7]
|
||||
if not materials:
|
||||
materials = all_materials
|
||||
if not materials:
|
||||
max_duration = max(m.duration for m in all_materials)
|
||||
logger.warning(
|
||||
f"素材匹配失败: 分类 '{normalized}' 无足够时长的素材 (需 >= {required_duration}s, 最大可用: {max_duration}s)"
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -187,37 +281,52 @@ async def batch_match(
|
||||
normalized_scenes = [_normalize_scene(s["scene"]) for s in scenes]
|
||||
unique_names = list(set(normalized_scenes))
|
||||
|
||||
# 2. 批量查询分类(1 次 DB)—— 同时查询原始名和倒序名
|
||||
reversed_names: list[str] = []
|
||||
name_to_reversed: dict[str, str] = {}
|
||||
for name in unique_names:
|
||||
parts = name.rsplit("-", 1)
|
||||
if len(parts) == 2:
|
||||
rev = f"{parts[1]}-{parts[0]}"
|
||||
reversed_names.append(rev)
|
||||
name_to_reversed[name] = rev
|
||||
|
||||
all_query_names = unique_names + reversed_names
|
||||
# 2. 批量查询分类:优先精确查询,失败时全量内存匹配兜底
|
||||
categories = await broll_category.get_by_names_and_level(
|
||||
db, names=all_query_names, level=3
|
||||
db, names=unique_names, level=3
|
||||
)
|
||||
category_map: dict[str, object] = {}
|
||||
for c in categories:
|
||||
category_map[c.name] = c
|
||||
category_map[_normalize_scene(c.name)] = c
|
||||
|
||||
# 收集未命中的 name,准备全量兜底
|
||||
unmatched_by_exact = [name for name in unique_names if name not in category_map]
|
||||
if unmatched_by_exact:
|
||||
all_categories = await broll_category.get_by_level(db, level=3)
|
||||
for c in all_categories:
|
||||
normalized_db_name = _normalize_scene(c.name)
|
||||
if normalized_db_name not in category_map:
|
||||
category_map[normalized_db_name] = c
|
||||
|
||||
# 构建原始 scene -> category 的映射
|
||||
reversed_map: dict[str, str] = {}
|
||||
for name in unique_names:
|
||||
parts = name.rsplit("-", 1)
|
||||
if len(parts) == 2:
|
||||
reversed_map[name] = f"{parts[1]}-{parts[0]}"
|
||||
|
||||
# 构建原始 scene -> category 的映射(优先精确匹配,fallback 倒序匹配)
|
||||
scene_to_category: dict[str, object] = {}
|
||||
for name in unique_names:
|
||||
if name in category_map:
|
||||
scene_to_category[name] = category_map[name]
|
||||
elif name in name_to_reversed and name_to_reversed[name] in category_map:
|
||||
rev = name_to_reversed[name]
|
||||
elif name in reversed_map and reversed_map[name] in category_map:
|
||||
rev = reversed_map[name]
|
||||
scene_to_category[name] = category_map[rev]
|
||||
logger.info(
|
||||
f"批量匹配顺序颠倒兜底命中: '{name}' -> '{rev}'"
|
||||
)
|
||||
|
||||
# 3. 收集所有需要的 category_id
|
||||
# 3. 未匹配的 scene 回退到上级分类随机选取
|
||||
unmatched = [name for name in unique_names if name not in scene_to_category]
|
||||
for name in unmatched:
|
||||
fallback_cat = await _try_fallback_to_parent(db, name)
|
||||
if fallback_cat:
|
||||
scene_to_category[name] = fallback_cat
|
||||
logger.info(
|
||||
f"批量匹配回退到上级分类命中: '{name}' -> '{fallback_cat.name}'"
|
||||
)
|
||||
|
||||
# 4. 收集所有需要的 category_id
|
||||
needed_category_ids = [
|
||||
scene_to_category[name].id
|
||||
for name in unique_names
|
||||
@@ -253,13 +362,25 @@ async def batch_match(
|
||||
|
||||
category = scene_to_category.get(scene_name)
|
||||
if category is None:
|
||||
original_scene = scenes[idx]["scene"]
|
||||
logger.warning(
|
||||
f"批量素材匹配失败: 未找到分类 '{scene_name}' (原始 scene: '{original_scene}')"
|
||||
)
|
||||
results.append(None)
|
||||
continue
|
||||
|
||||
materials = materials_by_category.get(category.id, [])
|
||||
# 按时长过滤
|
||||
# 按时长过滤(优先严格匹配,失败时逐步放宽到 70% 兜底)
|
||||
candidates = [m for m in materials if m.duration >= required_duration]
|
||||
if not candidates:
|
||||
candidates = [m for m in materials if m.duration >= required_duration * 0.7]
|
||||
if not candidates:
|
||||
candidates = materials
|
||||
if not candidates:
|
||||
max_duration = max((m.duration for m in materials), default=0)
|
||||
logger.warning(
|
||||
f"批量素材匹配失败: 分类 '{scene_name}' -> '{category.name}' 无足够时长的素材 (需 >= {required_duration}s, 最大可用: {max_duration}s)"
|
||||
)
|
||||
results.append(None)
|
||||
continue
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
from app.ai.model_router import get_model_router
|
||||
from app.ai.prompts import load_script_user_prompt, load_system_prompt
|
||||
from app.ai.prompts import load_prompt_file, load_script_user_prompt
|
||||
from app.schemas.script import ScriptShot
|
||||
from app.services.ai_response_utils import (
|
||||
safe_parse_ai_json_response,
|
||||
@@ -38,7 +38,7 @@ class ScriptService:
|
||||
async def generate_script(
|
||||
self,
|
||||
category: str,
|
||||
subcategory: str,
|
||||
filename: str,
|
||||
model: str | None = None,
|
||||
) -> list[ScriptShot]:
|
||||
"""
|
||||
@@ -46,7 +46,7 @@ class ScriptService:
|
||||
|
||||
Args:
|
||||
category: 大类代码,如 "bk"
|
||||
subcategory: 小类代码,如 "ht"
|
||||
filename: 提示词文件名,如 "水电改造避坑——水电改造的4个坑.txt"
|
||||
model: 指定模型
|
||||
|
||||
Returns:
|
||||
@@ -56,13 +56,13 @@ class ScriptService:
|
||||
model_router = await get_model_router()
|
||||
|
||||
# 加载 Prompt
|
||||
system_prompt = load_system_prompt(category, subcategory)
|
||||
system_prompt = load_prompt_file(category, filename)
|
||||
if not system_prompt:
|
||||
raise ValueError(f"未找到提示词: category={category}, subcategory={subcategory}")
|
||||
raise ValueError(f"未找到提示词: category={category}, filename={filename}")
|
||||
|
||||
# 用户提示词
|
||||
user_prompt = load_script_user_prompt(
|
||||
topic=f"{category}/{subcategory}",
|
||||
topic=f"{category}/{filename}",
|
||||
)
|
||||
|
||||
# 调用 AI 生成
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
- ~/Documents/Meijiaka-zy:/root/Documents/Meijiaka-zy
|
||||
ports:
|
||||
- "8081:8000"
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --no-access-log
|
||||
networks:
|
||||
- meijiaka-network
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ services:
|
||||
volumes:
|
||||
# 仅持久化日志到宿主机,其他数据走对象存储
|
||||
- /opt/meijiaka-zy/logs:/root/Documents/Meijiaka-zy/logs
|
||||
command: alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
command: alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --no-access-log
|
||||
ports:
|
||||
- "8000:8000"
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
command: >
|
||||
sh -c "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"
|
||||
sh -c "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --no-access-log"
|
||||
networks:
|
||||
- meijiaka-zy
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "meijiaka-ai-api"
|
||||
version = "1.6.5"
|
||||
version = "1.8.1"
|
||||
description = "美家卡智影 - AI 视频创作后端 API"
|
||||
authors = [{ name = "Meijiaka Team" }]
|
||||
readme = "README.md"
|
||||
|
||||
Generated
+1
-1
@@ -944,7 +944,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meijiaka-ai-api"
|
||||
version = "1.6.5"
|
||||
version = "1.8.1"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
-- 新增素材分类 SQL(2025-06-03)
|
||||
-- 文件1:新房装修流程(4 个分类)+ 文件2:装备材料选择(44 个分类)
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
l1_cailiao_id bigint;
|
||||
l2_fengchuang_id bigint;
|
||||
l2_zhanshi_id bigint;
|
||||
BEGIN
|
||||
-- ====================== 文件1:新房装修流程 ======================
|
||||
|
||||
-- 二级:封窗镜(拆改改造类下,sort_order=4)
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('chaigai-fengchuang', '封窗镜', 46, 2, 4, 'active', NOW(), NOW())
|
||||
RETURNING id INTO l2_fengchuang_id;
|
||||
|
||||
-- 三级:封窗施工-封窗镜
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('chaigai-fengchuang-fcsg', '封窗施工-封窗镜', l2_fengchuang_id, 3, 1, 'active', NOW(), NOW());
|
||||
|
||||
-- 三级:装烟机灶具-主材安装镜(parent_id=189 主材安装镜,sort_order=9)
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('anzhuang-zhucai-zyjzj', '装烟机灶具-主材安装镜', 189, 3, 9, 'active', NOW(), NOW());
|
||||
|
||||
-- 三级:家具进场摆放就位-软装进场镜(parent_id=219 软装进场镜,sort_order=2)
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('ruanzhuang-ruanchang-jjjcbfjw', '家具进场摆放就位-软装进场镜', 219, 3, 2, 'active', NOW(), NOW());
|
||||
|
||||
-- ====================== 文件2:装备材料选择 ======================
|
||||
|
||||
-- 一级:装修材料选择(sort_order=90)
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao', '装修材料选择', NULL, 1, 90, 'active', NOW(), NOW())
|
||||
RETURNING id INTO l1_cailiao_id;
|
||||
|
||||
-- 二级:材料展示
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi', '材料展示', l1_cailiao_id, 2, 1, 'active', NOW(), NOW())
|
||||
RETURNING id INTO l2_zhanshi_id;
|
||||
|
||||
-- 三级:XX买谁家 + 品牌组合(42 个)
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-bxmsj', '冰箱买谁家', l2_zhanshi_id, 3, 1, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-dsmsj', '电视买谁家', l2_zhanshi_id, 3, 2, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-hsj', '花洒哪家好', l2_zhanshi_id, 3, 3, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-dxmsj', '电线买谁家', l2_zhanshi_id, 3, 4, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-yjj', '烟机哪家好', l2_zhanshi_id, 3, 5, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-rjqmsj', '乳胶漆买谁家', l2_zhanshi_id, 3, 6, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-kgczmsj', '开关插座买谁家', l2_zhanshi_id, 3, 7, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-czj', '瓷砖哪家好', l2_zhanshi_id, 3, 8, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-sgmsj', '水管买谁家', l2_zhanshi_id, 3, 9, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-bcxsj', '板材选谁家', l2_zhanshi_id, 3, 10, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-fsmsj', '防水买谁家', l2_zhanshi_id, 3, 11, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-ddxsj', '吊顶选谁家', l2_zhanshi_id, 3, 12, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-dbj', '地板哪家好', l2_zhanshi_id, 3, 13, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-nzfj', '腻子粉哪家好', l2_zhanshi_id, 3, 14, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-dlsj', '地漏谁家好', l2_zhanshi_id, 3, 15, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-jzsgsj', '家装水管谁家好', l2_zhanshi_id, 3, 16, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-jzsnsj', '家装水泥谁家好', l2_zhanshi_id, 3, 17, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-cwwjsj', '厨卫五金谁家好', l2_zhanshi_id, 3, 18, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-sgbsj', '石膏板谁家好', l2_zhanshi_id, 3, 19, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-czjsj', '瓷砖胶谁家好', l2_zhanshi_id, 3, 20, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-bljsj', '玻璃胶谁家好', l2_zhanshi_id, 3, 21, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-hemdksd', '海尔美的卡萨帝', l2_zhanshi_id, 3, 22, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-snhxtcl', '索尼海信TCL', l2_zhanshi_id, 3, 23, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-jmhjjp', '九牧恒洁箭牌', l2_zhanshi_id, 3, 24, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-ydbsxm', '远东宝胜熊猫', l2_zhanshi_id, 3, 25, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-ftlbhd', '方太老板华帝', l2_zhanshi_id, 3, 26, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-lbsksdls', '立邦三棵树多乐士', l2_zhanshi_id, 3, 27, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-gnsndxmz', '公牛施耐德西门子', l2_zhanshi_id, 3, 28, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-dpgzmkbl', '东鹏冠珠马可波罗', l2_zhanshi_id, 3, 29, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-rfwxbl', '日丰伟星保利', l2_zhanshi_id, 3, 30, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-whagtbb', '万华爱格兔宝宝', l2_zhanshi_id, 3, 31, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-lbdgdfyh', '立邦德高东方雨虹', l2_zhanshi_id, 3, 32, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-apybfsl', '奥普友邦法狮龙', l2_zhanshi_id, 3, 33, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-sxsydzr', '圣象世友大自然', l2_zhanshi_id, 3, 34, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-lbmcsgb', '立邦美巢圣戈邦', l2_zhanshi_id, 3, 35, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-jmjpqst', '九牧箭牌潜水艇', l2_zhanshi_id, 3, 36, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-jnwxrf', '金牛伟星日丰', l2_zhanshi_id, 3, 37, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-hlhszl', '海螺红狮中联', l2_zhanshi_id, 3, 38, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-kljmhsgy', '科勒九牧汉斯格雅', l2_zhanshi_id, 3, 39, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-lptsknf', '龙牌泰山可耐福', l2_zhanshi_id, 3, 40, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-dgxkm', '德高西卡马贝', l2_zhanshi_id, 3, 41, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('cailiao-zhanshi-wkxkbd', '瓦克西卡百得', l2_zhanshi_id, 3, 42, 'active', NOW(), NOW());
|
||||
|
||||
END $$;
|
||||
@@ -11,11 +11,11 @@
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_mobile TEXT := '13800138000'; -- ← 修改:手机号
|
||||
v_nickname TEXT := '新用户昵称'; -- ← 修改:昵称(可为空)
|
||||
v_mobile TEXT := '15359215971'; -- ← 修改:手机号
|
||||
v_nickname TEXT := '家迪装饰'; -- ← 修改:昵称(可为空)
|
||||
v_source TEXT := 'manual'; -- ← 修改:注册来源:manual / invite / promotion
|
||||
v_invited_by UUID := NULL; -- ← 修改:邀请人 user_id(没有则留 NULL)
|
||||
v_gift_points INT := 2000; -- ← 修改:赠送初始积分(0 表示不赠送)
|
||||
v_gift_points INT := 10000; -- ← 修改:赠送初始积分(0 表示不赠送)
|
||||
v_gift_days INT := 365; -- ← 修改:赠送积分有效期(天)
|
||||
v_user_id UUID;
|
||||
v_batch_id BIGINT;
|
||||
@@ -108,7 +108,7 @@ END $$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_mobile TEXT := '13860199646'; -- ← 修改:目标用户手机号
|
||||
v_mobile TEXT := '18750556093'; -- ← 修改:目标用户手机号
|
||||
v_gift_points INT := 5000; -- ← 修改:赠送积分数量
|
||||
v_gift_days INT := 180; -- ← 修改:有效期(天)
|
||||
v_reason TEXT := '运营活动赠送'; -- ← 修改:赠送原因(写入流水描述)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
from qiniu import Auth, put_file
|
||||
|
||||
# 加载环境变量
|
||||
env_path = Path(__file__).resolve().parent.parent / 'python-api' / '.env'
|
||||
load_dotenv(env_path)
|
||||
|
||||
access_key = os.getenv('QINIU_ACCESS_KEY')
|
||||
secret_key = os.getenv('QINIU_SECRET_KEY')
|
||||
bucket = os.getenv('QINIU_VIDEO_BUCKET', 'media-liche')
|
||||
domain = os.getenv('QINIU_VIDEO_DOMAIN', 'media.liche.cn')
|
||||
|
||||
if not access_key or not secret_key:
|
||||
print("错误: 未找到七牛云凭证,请检查 .env 文件")
|
||||
sys.exit(1)
|
||||
|
||||
q = Auth(access_key, secret_key)
|
||||
src_dir = Path('/Users/0fun/Downloads/新增素材6.2')
|
||||
|
||||
total = 0
|
||||
success = 0
|
||||
failed = 0
|
||||
|
||||
files = sorted([p for p in src_dir.rglob('*.mp4') if not p.name.startswith('._') and '.DS_Store' not in str(p)])
|
||||
|
||||
print(f"发现 {len(files)} 个 MP4 文件,开始上传...\n")
|
||||
|
||||
for mp4_path in files:
|
||||
total += 1
|
||||
key = f"meijiaka-zy/materials/{mp4_path.name}"
|
||||
|
||||
try:
|
||||
token = q.upload_token(bucket, key, 3600)
|
||||
ret, info = put_file(token, key, str(mp4_path))
|
||||
if ret is not None:
|
||||
url = f"https://{domain}/{key}"
|
||||
print(f"✅ [{total}/{len(files)}] {mp4_path.name}")
|
||||
success += 1
|
||||
else:
|
||||
print(f"❌ [{total}/{len(files)}] {mp4_path.name} → 失败: {info}")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"❌ [{total}/{len(files)}] {mp4_path.name} → 异常: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\n{'=' * 50}")
|
||||
print(f"总计: {total}, 成功: {success}, 失败: {failed}")
|
||||
@@ -0,0 +1,36 @@
|
||||
-- 统计全部素材中的重复命名(整个素材库)
|
||||
-- ============================================
|
||||
|
||||
-- 1. 按 title 分组,找出重复的素材
|
||||
SELECT
|
||||
title,
|
||||
COUNT(*) AS duplicate_count,
|
||||
STRING_AGG(DISTINCT url, ', ' ORDER BY url) AS urls,
|
||||
STRING_AGG(DISTINCT CAST(id AS TEXT), ', ' ORDER BY CAST(id AS TEXT)) AS ids
|
||||
FROM mjk_broll_materials
|
||||
WHERE status != 'deleted'
|
||||
GROUP BY title
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY duplicate_count DESC, title;
|
||||
|
||||
-- 2. 按 url 分组,找出重复的素材
|
||||
SELECT
|
||||
url,
|
||||
COUNT(*) AS duplicate_count,
|
||||
STRING_AGG(DISTINCT title, ', ' ORDER BY title) AS titles,
|
||||
STRING_AGG(DISTINCT CAST(id AS TEXT), ', ' ORDER BY CAST(id AS TEXT)) AS ids
|
||||
FROM mjk_broll_materials
|
||||
WHERE status != 'deleted'
|
||||
GROUP BY url
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY duplicate_count DESC, url;
|
||||
|
||||
-- 3. 统计概览:总素材数、唯一 title 数、唯一 url 数
|
||||
SELECT
|
||||
COUNT(*) AS total_materials,
|
||||
COUNT(DISTINCT title) AS unique_titles,
|
||||
COUNT(DISTINCT url) AS unique_urls,
|
||||
COUNT(*) - COUNT(DISTINCT title) AS title_duplicates,
|
||||
COUNT(*) - COUNT(DISTINCT url) AS url_duplicates
|
||||
FROM mjk_broll_materials
|
||||
WHERE status != 'deleted';
|
||||
@@ -0,0 +1,92 @@
|
||||
-- 修正 slug:去掉中文字符,改为序号格式
|
||||
BEGIN;
|
||||
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-001' WHERE name = '不要一体式卫生间' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-002' WHERE name = '不要双包套' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-003' WHERE name = '不要双开门冰箱' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-004' WHERE name = '不要反弹器' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-005' WHERE name = '不要回型吊顶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-006' WHERE name = '不要复杂吊灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-007' WHERE name = '不要复杂背景墙' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-008' WHERE name = '不要小双槽' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-009' WHERE name = '不要开放式收纳柜' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-010' WHERE name = '不要悬浮电视柜' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-011' WHERE name = '不要悬空马桶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-012' WHERE name = '不要拼色窗帘' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-013' WHERE name = '不要推拉门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-014' WHERE name = '不要插座外露' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-015' WHERE name = '不要无主灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-016' WHERE name = '不要普通门锁' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-017' WHERE name = '不要榻榻米' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-018' WHERE name = '不要正五孔插座' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-019' WHERE name = '不要洗烘一体' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-020' WHERE name = '不要深色地砖' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-021' WHERE name = '不要猫眼' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-022' WHERE name = '不要瓷砖上墙' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-023' WHERE name = '不要直吸马桶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-024' WHERE name = '不要直排下水' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-025' WHERE name = '不要筒灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-026' WHERE name = '不要罗马杠' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-027' WHERE name = '不要贵妃椅沙发' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-028' WHERE name = '不要路由器' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-029' WHERE name = '不要过门石' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-030' WHERE name = '不要造型柜门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-031' WHERE name = '不要阳角条' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-032' WHERE name = '不要隐形衣架' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-033' WHERE name = '不要集成灶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-034' WHERE name = '不要高光衣柜门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-035' WHERE name = '要一体门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-036' WHERE name = '要乳胶漆' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-037' WHERE name = '要免拉手' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-038' WHERE name = '要全屋WiFi' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-039' WHERE name = '要全屋通铺' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-040' WHERE name = '要分体灶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-041' WHERE name = '要十字开门冰箱' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-042' WHERE name = '要单包套' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-043' WHERE name = '要双眼皮' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-044' WHERE name = '要双眼皮吊顶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-045' WHERE name = '要吸顶灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-046' WHERE name = '要墙排下水' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-047' WHERE name = '要大单槽' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-048' WHERE name = '要封闭式收纳柜' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-049' WHERE name = '要射灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-050' WHERE name = '要干湿分离卫生间' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-051' WHERE name = '要平板柜门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-052' WHERE name = '要打通阳台' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-053' WHERE name = '要斜五孔插座' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-054' WHERE name = '要普通床' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-055' WHERE name = '要普通衣架' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-056' WHERE name = '要智能门锁' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-057' WHERE name = '要洗烘套装' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-058' WHERE name = '要浅色地砖' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-059' WHERE name = '要海棠角' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-060' WHERE name = '要直排沙发' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-061' WHERE name = '要窗帘盒' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-062' WHERE name = '要简约背景墙' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-063' WHERE name = '要纯色窗帘' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-064' WHERE name = '要肤感衣柜门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-065' WHERE name = '要落地电视柜' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-066' WHERE name = '要落地马桶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-067' WHERE name = '要虹吸马桶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-068' WHERE name = '要隐藏式插座' AND level = 3;
|
||||
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-001' WHERE name = '乳胶漆不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-002' WHERE name = '全屋角阀要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-003' WHERE name = '前置过滤器不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-004' WHERE name = '床不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-005' WHERE name = '床垫要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-006' WHERE name = '开关插座要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-007' WHERE name = '投影仪要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-008' WHERE name = '木地板要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-009' WHERE name = '木门不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-010' WHERE name = '水槽不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-011' WHERE name = '水龙头要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-012' WHERE name = '滑轨要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-013' WHERE name = '灯具不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-014' WHERE name = '瓷砖不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-015' WHERE name = '电视机不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-016' WHERE name = '窗帘不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-017' WHERE name = '腻子粉要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-018' WHERE name = '门锁要买贵的' AND level = 3;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,92 @@
|
||||
-- 修正 slug:拼音首字母格式
|
||||
BEGIN;
|
||||
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byytswsj' WHERE name = '不要一体式卫生间' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bysbt' WHERE name = '不要双包套' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byskmbx' WHERE name = '不要双开门冰箱' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byftq' WHERE name = '不要反弹器' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byhxdd' WHERE name = '不要回型吊顶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byfzdd' WHERE name = '不要复杂吊灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byfzbjq' WHERE name = '不要复杂背景墙' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byxsc' WHERE name = '不要小双槽' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bykfssng' WHERE name = '不要开放式收纳柜' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byxfdsg' WHERE name = '不要悬浮电视柜' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byxkmt' WHERE name = '不要悬空马桶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bypscl' WHERE name = '不要拼色窗帘' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bytlm' WHERE name = '不要推拉门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byczwl' WHERE name = '不要插座外露' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bywzd' WHERE name = '不要无主灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byptms' WHERE name = '不要普通门锁' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byttm' WHERE name = '不要榻榻米' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byzwkcz' WHERE name = '不要正五孔插座' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byxhyt' WHERE name = '不要洗烘一体' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byssdz' WHERE name = '不要深色地砖' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bymy' WHERE name = '不要猫眼' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byczsq' WHERE name = '不要瓷砖上墙' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byzxmt' WHERE name = '不要直吸马桶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byzpxs' WHERE name = '不要直排下水' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bytd' WHERE name = '不要筒灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bylmg' WHERE name = '不要罗马杠' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bygfysf' WHERE name = '不要贵妃椅沙发' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bylyq' WHERE name = '不要路由器' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-bygms' WHERE name = '不要过门石' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byzxgm' WHERE name = '不要造型柜门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byyjt' WHERE name = '不要阳角条' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byyxyj' WHERE name = '不要隐形衣架' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byjcz' WHERE name = '不要集成灶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-byggygm' WHERE name = '不要高光衣柜门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yytm' WHERE name = '要一体门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yrjq' WHERE name = '要乳胶漆' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ymls' WHERE name = '要免拉手' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yqwwifi' WHERE name = '要全屋WiFi' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yqwtp' WHERE name = '要全屋通铺' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yftz' WHERE name = '要分体灶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yszkmbx' WHERE name = '要十字开门冰箱' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ydbt' WHERE name = '要单包套' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ysyp' WHERE name = '要双眼皮' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ysypdd' WHERE name = '要双眼皮吊顶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yxdd' WHERE name = '要吸顶灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yqpxs' WHERE name = '要墙排下水' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yddc' WHERE name = '要大单槽' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yfbssng' WHERE name = '要封闭式收纳柜' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ysd' WHERE name = '要射灯' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ygsflwsj' WHERE name = '要干湿分离卫生间' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ypbgm' WHERE name = '要平板柜门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ydtyt' WHERE name = '要打通阳台' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yxwkcz' WHERE name = '要斜五孔插座' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yptc' WHERE name = '要普通床' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yptyj' WHERE name = '要普通衣架' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yznms' WHERE name = '要智能门锁' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yxhtz' WHERE name = '要洗烘套装' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yqsdz' WHERE name = '要浅色地砖' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yhtj' WHERE name = '要海棠角' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yzpsf' WHERE name = '要直排沙发' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yclh' WHERE name = '要窗帘盒' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yjybjq' WHERE name = '要简约背景墙' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ycscl' WHERE name = '要纯色窗帘' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yfgygm' WHERE name = '要肤感衣柜门' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-ylddsg' WHERE name = '要落地电视柜' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yldmt' WHERE name = '要落地马桶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yhxmt' WHERE name = '要虹吸马桶' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-buyao-yycscz' WHERE name = '要隐藏式插座' AND level = 3;
|
||||
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-rjqbymgd' WHERE name = '乳胶漆不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-qwjfymgd' WHERE name = '全屋角阀要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-qzglqbymgd' WHERE name = '前置过滤器不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-cbymgd' WHERE name = '床不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-cdymgd' WHERE name = '床垫要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-kgczymgd' WHERE name = '开关插座要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-tyyymgd' WHERE name = '投影仪要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-mdbymgd' WHERE name = '木地板要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-mmbymgd' WHERE name = '木门不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-scbymgd' WHERE name = '水槽不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-sltymgd' WHERE name = '水龙头要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-hgymgd' WHERE name = '滑轨要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-djbymgd' WHERE name = '灯具不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-czbymgd' WHERE name = '瓷砖不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-dsjbymgd' WHERE name = '电视机不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-clbymgd' WHERE name = '窗帘不要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-nzfymgd' WHERE name = '腻子粉要买贵的' AND level = 3;
|
||||
UPDATE mjk_broll_categories SET slug = 'bikeng-maidui-msymgd' WHERE name = '门锁要买贵的' AND level = 3;
|
||||
|
||||
COMMIT;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
-- 封面背景图批量导入 SQL
|
||||
-- script_code: bk(装修避坑)
|
||||
-- 生成时间: 2026-06-01
|
||||
-- 共 41 张
|
||||
|
||||
INSERT INTO mjk_cover_backgrounds (script_code, title, url, sort_order, status, created_at, updated_at) VALUES
|
||||
('bk', '04e1bd3b19', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/04e1bd3b19.png', 1, 'active', NOW(), NOW())
|
||||
, ('bk', '1c7d6b9ce9', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/1c7d6b9ce9.png', 2, 'active', NOW(), NOW())
|
||||
, ('bk', '2508cc7679', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/2508cc7679.png', 3, 'active', NOW(), NOW())
|
||||
, ('bk', '280bc39306', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/280bc39306.png', 4, 'active', NOW(), NOW())
|
||||
, ('bk', '2bb52b3f65', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/2bb52b3f65.png', 5, 'active', NOW(), NOW())
|
||||
, ('bk', '2c08dbd929', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/2c08dbd929.png', 6, 'active', NOW(), NOW())
|
||||
, ('bk', '3079192f62', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/3079192f62.png', 7, 'active', NOW(), NOW())
|
||||
, ('bk', '3409a04e66', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/3409a04e66.png', 8, 'active', NOW(), NOW())
|
||||
, ('bk', '3cd57f9008', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/3cd57f9008.png', 9, 'active', NOW(), NOW())
|
||||
, ('bk', '4a7555e3f0', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/4a7555e3f0.png', 10, 'active', NOW(), NOW())
|
||||
, ('bk', '4c3965959a', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/4c3965959a.png', 11, 'active', NOW(), NOW())
|
||||
, ('bk', '4cecdd5f83', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/4cecdd5f83.png', 12, 'active', NOW(), NOW())
|
||||
, ('bk', '54b164fef1', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/54b164fef1.png', 13, 'active', NOW(), NOW())
|
||||
, ('bk', '5fd98a1da7', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/5fd98a1da7.png', 14, 'active', NOW(), NOW())
|
||||
, ('bk', '645f2970bf', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/645f2970bf.png', 15, 'active', NOW(), NOW())
|
||||
, ('bk', '70cb74ee0e', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/70cb74ee0e.png', 16, 'active', NOW(), NOW())
|
||||
, ('bk', '76955a5f76', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/76955a5f76.png', 17, 'active', NOW(), NOW())
|
||||
, ('bk', '8395b3784a', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/8395b3784a.png', 18, 'active', NOW(), NOW())
|
||||
, ('bk', '85c6ed85e1', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/85c6ed85e1.png', 19, 'active', NOW(), NOW())
|
||||
, ('bk', '95ff4cf029', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/95ff4cf029.png', 20, 'active', NOW(), NOW())
|
||||
, ('bk', '979dfb0215', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/979dfb0215.png', 21, 'active', NOW(), NOW())
|
||||
, ('bk', 'a12d375624', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/a12d375624.png', 22, 'active', NOW(), NOW())
|
||||
, ('bk', 'a1411511aa', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/a1411511aa.png', 23, 'active', NOW(), NOW())
|
||||
, ('bk', 'a16a0b348c', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/a16a0b348c.png', 24, 'active', NOW(), NOW())
|
||||
, ('bk', 'ae4ed9a335', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/ae4ed9a335.png', 25, 'active', NOW(), NOW())
|
||||
, ('bk', 'c07f6556fd', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/c07f6556fd.png', 26, 'active', NOW(), NOW())
|
||||
, ('bk', 'c1460b5f5a', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/c1460b5f5a.png', 27, 'active', NOW(), NOW())
|
||||
, ('bk', 'cd8b455000', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/cd8b455000.png', 28, 'active', NOW(), NOW())
|
||||
, ('bk', 'd16d93b73d', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/d16d93b73d.png', 29, 'active', NOW(), NOW())
|
||||
, ('bk', 'd18088bb3f', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/d18088bb3f.png', 30, 'active', NOW(), NOW())
|
||||
, ('bk', 'd540fc6c4b', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/d540fc6c4b.png', 31, 'active', NOW(), NOW())
|
||||
, ('bk', 'dbf4c4ff92', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/dbf4c4ff92.png', 32, 'active', NOW(), NOW())
|
||||
, ('bk', 'dce3d6fb91', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/dce3d6fb91.png', 33, 'active', NOW(), NOW())
|
||||
, ('bk', 'ec8c953ccb', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/ec8c953ccb.png', 34, 'active', NOW(), NOW())
|
||||
, ('bk', 'ef9575b715', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/ef9575b715.png', 35, 'active', NOW(), NOW())
|
||||
, ('bk', 'f3023c5554', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/f3023c5554.png', 36, 'active', NOW(), NOW())
|
||||
, ('bk', 'f4679775ce', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/f4679775ce.png', 37, 'active', NOW(), NOW())
|
||||
, ('bk', 'f9192aa4a9', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/f9192aa4a9.png', 38, 'active', NOW(), NOW())
|
||||
, ('bk', 'f98d79311e', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/f98d79311e.png', 39, 'active', NOW(), NOW())
|
||||
, ('bk', 'fadcca7a06', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/fadcca7a06.png', 40, 'active', NOW(), NOW())
|
||||
, ('bk', 'fb04e41a2c', 'https://img.liche.cn/meijiaka-zy/cover_templete/bk/fb04e41a2c.png', 41, 'active', NOW(), NOW());
|
||||
@@ -0,0 +1,72 @@
|
||||
-- 新增素材6.2 批量导入 SQL
|
||||
-- 共 67 个素材
|
||||
-- URL前缀: https://media.liche.cn/meijiaka-zy/materials/
|
||||
|
||||
INSERT INTO mjk_broll_materials (category_id, title, url, duration, usage_count, status, created_at, updated_at) VALUES
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '万华爱格兔宝宝' AND level = 3), '3c28619290414e552e988e09b3675b72', 'https://media.liche.cn/meijiaka-zy/materials/3c28619290414e552e988e09b3675b72.mp4', 5.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '东鹏冠珠马可波罗' AND level = 3), '182e4c8bbcea8f103672b147b4606213', 'https://media.liche.cn/meijiaka-zy/materials/182e4c8bbcea8f103672b147b4606213.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '九牧恒洁箭牌' AND level = 3), '7d5607ab1886ba948fc1e94398473274', 'https://media.liche.cn/meijiaka-zy/materials/7d5607ab1886ba948fc1e94398473274.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '九牧箭牌潜水艇' AND level = 3), '6924bfd00653c3c6cfcb2d1d02c4789c', 'https://media.liche.cn/meijiaka-zy/materials/6924bfd00653c3c6cfcb2d1d02c4789c.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '乳胶漆买谁家' AND level = 3), '6086a32ec41db26e5bc570f1f27c1fbc', 'https://media.liche.cn/meijiaka-zy/materials/6086a32ec41db26e5bc570f1f27c1fbc.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '公牛施耐德西门子' AND level = 3), 'c0a3e230b32026b78596c192718345cb', 'https://media.liche.cn/meijiaka-zy/materials/c0a3e230b32026b78596c192718345cb.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '冰箱买谁家' AND level = 3), 'fad1e564d3272515eab8dd75079870de', 'https://media.liche.cn/meijiaka-zy/materials/fad1e564d3272515eab8dd75079870de.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '厨卫五金谁家好' AND level = 3), '4d20e0593c973366e064451fd9561eff', 'https://media.liche.cn/meijiaka-zy/materials/4d20e0593c973366e064451fd9561eff.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '吊顶选谁家' AND level = 3), '4ee89978139e0e55c809aedf5a56ddc2', 'https://media.liche.cn/meijiaka-zy/materials/4ee89978139e0e55c809aedf5a56ddc2.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '圣象世友大自然' AND level = 3), '5c78732634c008867211a9a34d124881', 'https://media.liche.cn/meijiaka-zy/materials/5c78732634c008867211a9a34d124881.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '地板哪家好' AND level = 3), '647972fffa483aa807ecd08ed53219be', 'https://media.liche.cn/meijiaka-zy/materials/647972fffa483aa807ecd08ed53219be.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '地漏谁家好' AND level = 3), 'd4fb5c1a8bf3cecf8662f82b6977a954', 'https://media.liche.cn/meijiaka-zy/materials/d4fb5c1a8bf3cecf8662f82b6977a954.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '奥普友邦法狮龙' AND level = 3), 'a6cc0fb2a45ea0e4b988451baddac499', 'https://media.liche.cn/meijiaka-zy/materials/a6cc0fb2a45ea0e4b988451baddac499.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '家装水泥谁家好' AND level = 3), 'e189e0507cd2ecb0be2e32d2cf65dccd', 'https://media.liche.cn/meijiaka-zy/materials/e189e0507cd2ecb0be2e32d2cf65dccd.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '家装水管谁家好' AND level = 3), '72d9643978717084039cf3507f9bdd04', 'https://media.liche.cn/meijiaka-zy/materials/72d9643978717084039cf3507f9bdd04.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '开关插座买谁家' AND level = 3), '8df9cfec100cea2661784361571a3a11', 'https://media.liche.cn/meijiaka-zy/materials/8df9cfec100cea2661784361571a3a11.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '德高西卡马贝' AND level = 3), 'b365eb193c1ede5777cc04d2427ea0e7', 'https://media.liche.cn/meijiaka-zy/materials/b365eb193c1ede5777cc04d2427ea0e7.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '方太老板华帝' AND level = 3), '9e37037aad90b25493228d05c0405696', 'https://media.liche.cn/meijiaka-zy/materials/9e37037aad90b25493228d05c0405696.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '日丰伟星保利' AND level = 3), '3318c054d10f5ecca8ce397f435c01af', 'https://media.liche.cn/meijiaka-zy/materials/3318c054d10f5ecca8ce397f435c01af.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '板材选谁家' AND level = 3), '9e5eb608644425a51613e2e145ba28e0', 'https://media.liche.cn/meijiaka-zy/materials/9e5eb608644425a51613e2e145ba28e0.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '水管买谁家' AND level = 3), '0d37b9dbf4b44858f16f155ba88a7054', 'https://media.liche.cn/meijiaka-zy/materials/0d37b9dbf4b44858f16f155ba88a7054.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '海尔美的卡萨帝' AND level = 3), '33e3a3ae93f65e72d055431b0d132495', 'https://media.liche.cn/meijiaka-zy/materials/33e3a3ae93f65e72d055431b0d132495.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '海螺红狮中联' AND level = 3), 'cd7f119729a062df350940bb003f873d', 'https://media.liche.cn/meijiaka-zy/materials/cd7f119729a062df350940bb003f873d.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '烟机哪家好' AND level = 3), '01690f5d76a40142f365da61319c9f31', 'https://media.liche.cn/meijiaka-zy/materials/01690f5d76a40142f365da61319c9f31.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '玻璃胶谁家好' AND level = 3), '52d7c9db8291af1d9e2061c3288bec2b', 'https://media.liche.cn/meijiaka-zy/materials/52d7c9db8291af1d9e2061c3288bec2b.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '瓦克西卡百得' AND level = 3), '1e2774b366631016acecc03c322dc0a2', 'https://media.liche.cn/meijiaka-zy/materials/1e2774b366631016acecc03c322dc0a2.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '瓷砖哪家好' AND level = 3), 'e727f0ea33030c14d33185329794398c', 'https://media.liche.cn/meijiaka-zy/materials/e727f0ea33030c14d33185329794398c.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '瓷砖胶谁家好' AND level = 3), '07a1f9a9ec09809d33781a2b6265ff48', 'https://media.liche.cn/meijiaka-zy/materials/07a1f9a9ec09809d33781a2b6265ff48.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '电线买谁家' AND level = 3), '04f61f5a57454799d2aa1000764311af', 'https://media.liche.cn/meijiaka-zy/materials/04f61f5a57454799d2aa1000764311af.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '电视买谁家' AND level = 3), '82c400fbc27c42dd8992458d2e94c047', 'https://media.liche.cn/meijiaka-zy/materials/82c400fbc27c42dd8992458d2e94c047.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '石膏板谁家好' AND level = 3), 'e6469a2dc66350c330d2d87f08a9cd5a', 'https://media.liche.cn/meijiaka-zy/materials/e6469a2dc66350c330d2d87f08a9cd5a.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '科勒九牧汉斯格雅' AND level = 3), '80506a9eb5e1a06d3c5cb98eaae515cd', 'https://media.liche.cn/meijiaka-zy/materials/80506a9eb5e1a06d3c5cb98eaae515cd.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '立邦三棵树多乐士' AND level = 3), '82b26195cdb7a08793abf3ff17d400eb', 'https://media.liche.cn/meijiaka-zy/materials/82b26195cdb7a08793abf3ff17d400eb.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '立邦德高东方雨虹' AND level = 3), '03602a66190ad519f81c70a80e6cd0d2', 'https://media.liche.cn/meijiaka-zy/materials/03602a66190ad519f81c70a80e6cd0d2.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '立邦美巢圣戈邦' AND level = 3), 'cc00321964988a29315468e4dca53d06', 'https://media.liche.cn/meijiaka-zy/materials/cc00321964988a29315468e4dca53d06.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '索尼海信TCL' AND level = 3), 'fb9d3b062be7ed44fef7ec81490a4963', 'https://media.liche.cn/meijiaka-zy/materials/fb9d3b062be7ed44fef7ec81490a4963.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '腻子粉哪家好' AND level = 3), '887c5ea3238d2c1196eb09c410dc8f7f', 'https://media.liche.cn/meijiaka-zy/materials/887c5ea3238d2c1196eb09c410dc8f7f.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '花洒哪家好' AND level = 3), 'f6d4592e08482cb903daca04d432f3ed', 'https://media.liche.cn/meijiaka-zy/materials/f6d4592e08482cb903daca04d432f3ed.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '远东宝胜熊猫' AND level = 3), 'c5ac8d8ff5435e123c8016657fbf955c', 'https://media.liche.cn/meijiaka-zy/materials/c5ac8d8ff5435e123c8016657fbf955c.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '金牛伟星日丰' AND level = 3), '3c9546527a38c300050ed28bda81e204', 'https://media.liche.cn/meijiaka-zy/materials/3c9546527a38c300050ed28bda81e204.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '防水买谁家' AND level = 3), '3e7c5b554836376605828f374bd15ae5', 'https://media.liche.cn/meijiaka-zy/materials/3e7c5b554836376605828f374bd15ae5.mp4', 3.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '龙牌泰山可耐福' AND level = 3), '6589ffbf04c596583d2d2316216c7428', 'https://media.liche.cn/meijiaka-zy/materials/6589ffbf04c596583d2d2316216c7428.mp4', 5.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), '488d5a9ac72bf8e9a2a574284f2bdded', 'https://media.liche.cn/meijiaka-zy/materials/488d5a9ac72bf8e9a2a574284f2bdded.mp4', 10.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), '3c716b567964591569abfc8d16e7ff9b', 'https://media.liche.cn/meijiaka-zy/materials/3c716b567964591569abfc8d16e7ff9b.mp4', 9.97, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), '2fb5183ef1547fb3d7d4d8e56fe5154a', 'https://media.liche.cn/meijiaka-zy/materials/2fb5183ef1547fb3d7d4d8e56fe5154a.mp4', 10.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), '0d7081072e409e8f2c7d3a61a793f348', 'https://media.liche.cn/meijiaka-zy/materials/0d7081072e409e8f2c7d3a61a793f348.mp4', 10.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), 'f083bfca1800c171af2dfc767fc5d28a', 'https://media.liche.cn/meijiaka-zy/materials/f083bfca1800c171af2dfc767fc5d28a.mp4', 8.48, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), '8e198970fe0be12c9067fefe658d1cdd', 'https://media.liche.cn/meijiaka-zy/materials/8e198970fe0be12c9067fefe658d1cdd.mp4', 9.8, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), '3763505e431dc39a9fef135ad82ee6f1', 'https://media.liche.cn/meijiaka-zy/materials/3763505e431dc39a9fef135ad82ee6f1.mp4', 9.92, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), '0576abe034e932071d7c3b67104e79c3', 'https://media.liche.cn/meijiaka-zy/materials/0576abe034e932071d7c3b67104e79c3.mp4', 9.9, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), '07281a59949dcaa72c22b54b8569e1ea', 'https://media.liche.cn/meijiaka-zy/materials/07281a59949dcaa72c22b54b8569e1ea.mp4', 4.71, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '装烟机灶具-主材安装镜' AND level = 3), 'e8d1429ec925636603b69b57ca3eba52', 'https://media.liche.cn/meijiaka-zy/materials/e8d1429ec925636603b69b57ca3eba52.mp4', 4.09, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '封窗施工-封窗镜' AND level = 3), '9d516288fc104f41e3a4c3747883a072', 'https://media.liche.cn/meijiaka-zy/materials/9d516288fc104f41e3a4c3747883a072.mp4', 6.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '封窗施工-封窗镜' AND level = 3), '8b293fcabbf5e8dc2f84a77e9cbb9956', 'https://media.liche.cn/meijiaka-zy/materials/8b293fcabbf5e8dc2f84a77e9cbb9956.mp4', 6.18, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '封窗施工-封窗镜' AND level = 3), '432a789688f8f729ec72ccbf8571820a', 'https://media.liche.cn/meijiaka-zy/materials/432a789688f8f729ec72ccbf8571820a.mp4', 10.0, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '封窗施工-封窗镜' AND level = 3), '3a97f3ebd82b3ff8c04ec47871199159', 'https://media.liche.cn/meijiaka-zy/materials/3a97f3ebd82b3ff8c04ec47871199159.mp4', 5.62, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '封窗施工-封窗镜' AND level = 3), 'a67d0a41c10ea002968fb0ed1ad8ae0e', 'https://media.liche.cn/meijiaka-zy/materials/a67d0a41c10ea002968fb0ed1ad8ae0e.mp4', 8.55, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '封窗施工-封窗镜' AND level = 3), '6b28e44d37a14c3d9ac8b503a29a421c', 'https://media.liche.cn/meijiaka-zy/materials/6b28e44d37a14c3d9ac8b503a29a421c.mp4', 8.92, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '封窗施工-封窗镜' AND level = 3), '9468e8aba0f47eeef9bc59e395bfcc72', 'https://media.liche.cn/meijiaka-zy/materials/9468e8aba0f47eeef9bc59e395bfcc72.mp4', 10.2, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '封窗施工-封窗镜' AND level = 3), 'b5d109f43246296ca6de51d28e596dcd', 'https://media.liche.cn/meijiaka-zy/materials/b5d109f43246296ca6de51d28e596dcd.mp4', 10.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '家具进场摆放就位-软装进场镜' AND level = 3), 'a15878e831df63d8cffc1066bf25e257', 'https://media.liche.cn/meijiaka-zy/materials/a15878e831df63d8cffc1066bf25e257.mp4', 7.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '家具进场摆放就位-软装进场镜' AND level = 3), '1622140bd7229107b38927b831f8bea8', 'https://media.liche.cn/meijiaka-zy/materials/1622140bd7229107b38927b831f8bea8.mp4', 10.01, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '家具进场摆放就位-软装进场镜' AND level = 3), '5ef57acfda494cfdfb5185594c51364c', 'https://media.liche.cn/meijiaka-zy/materials/5ef57acfda494cfdfb5185594c51364c.mp4', 7.85, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '家具进场摆放就位-软装进场镜' AND level = 3), '4473c9736c25e77375459bd2968f4753', 'https://media.liche.cn/meijiaka-zy/materials/4473c9736c25e77375459bd2968f4753.mp4', 6.11, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '家具进场摆放就位-软装进场镜' AND level = 3), '67526f381c629710d78c53ec3a2f6941', 'https://media.liche.cn/meijiaka-zy/materials/67526f381c629710d78c53ec3a2f6941.mp4', 5.2, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '家具进场摆放就位-软装进场镜' AND level = 3), '1690725fa66d82b9400e462839328658', 'https://media.liche.cn/meijiaka-zy/materials/1690725fa66d82b9400e462839328658.mp4', 7.41, 0, 'active', NOW(), NOW()),
|
||||
((SELECT id FROM mjk_broll_categories WHERE name = '家具进场摆放就位-软装进场镜' AND level = 3), '28b67c0a7565ff259f889456334d000d', 'https://media.liche.cn/meijiaka-zy/materials/28b67c0a7565ff259f889456334d000d.mp4', 9.45, 0, 'active', NOW(), NOW());
|
||||
@@ -0,0 +1,202 @@
|
||||
-- 装修避坑通用素材分类体系
|
||||
-- 共 86 个三级分类(68 个不要X要Y + 18 个买对不买贵)
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
l1_id bigint;
|
||||
l2_buyao_id bigint;
|
||||
l2_maidui_id bigint;
|
||||
BEGIN
|
||||
|
||||
-- 一级:装修避坑通用
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-tongyong', '装修避坑通用', NULL, 1, 90, 'active', NOW(), NOW())
|
||||
RETURNING id INTO l1_id;
|
||||
|
||||
-- 二级:不要X要Y
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao', '不要X要Y', l1_id, 2, 1, 'active', NOW(), NOW())
|
||||
RETURNING id INTO l2_buyao_id;
|
||||
|
||||
-- 二级:买对不买贵
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui', '买对不买贵', l1_id, 2, 2, 'active', NOW(), NOW())
|
||||
RETURNING id INTO l2_maidui_id;
|
||||
|
||||
-- 三级:不要X要Y(68个)
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要一体式卫生间', '不要一体式卫生间', l2_buyao_id, 3, 1, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要双包套', '不要双包套', l2_buyao_id, 3, 2, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要双开门冰箱', '不要双开门冰箱', l2_buyao_id, 3, 3, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要反弹器', '不要反弹器', l2_buyao_id, 3, 4, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要回型吊顶', '不要回型吊顶', l2_buyao_id, 3, 5, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要复杂吊灯', '不要复杂吊灯', l2_buyao_id, 3, 6, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要复杂背景墙', '不要复杂背景墙', l2_buyao_id, 3, 7, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要小双槽', '不要小双槽', l2_buyao_id, 3, 8, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要开放式收纳柜', '不要开放式收纳柜', l2_buyao_id, 3, 9, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要悬浮电视柜', '不要悬浮电视柜', l2_buyao_id, 3, 10, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要悬空马桶', '不要悬空马桶', l2_buyao_id, 3, 11, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要拼色窗帘', '不要拼色窗帘', l2_buyao_id, 3, 12, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要推拉门', '不要推拉门', l2_buyao_id, 3, 13, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要插座外露', '不要插座外露', l2_buyao_id, 3, 14, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要无主灯', '不要无主灯', l2_buyao_id, 3, 15, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要普通门锁', '不要普通门锁', l2_buyao_id, 3, 16, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要榻榻米', '不要榻榻米', l2_buyao_id, 3, 17, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要正五孔插座', '不要正五孔插座', l2_buyao_id, 3, 18, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要洗烘一体', '不要洗烘一体', l2_buyao_id, 3, 19, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要深色地砖', '不要深色地砖', l2_buyao_id, 3, 20, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要猫眼', '不要猫眼', l2_buyao_id, 3, 21, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要瓷砖上墙', '不要瓷砖上墙', l2_buyao_id, 3, 22, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要直吸马桶', '不要直吸马桶', l2_buyao_id, 3, 23, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要直排下水', '不要直排下水', l2_buyao_id, 3, 24, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要筒灯', '不要筒灯', l2_buyao_id, 3, 25, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要罗马杠', '不要罗马杠', l2_buyao_id, 3, 26, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要贵妃椅沙发', '不要贵妃椅沙发', l2_buyao_id, 3, 27, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要路由器', '不要路由器', l2_buyao_id, 3, 28, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要过门石', '不要过门石', l2_buyao_id, 3, 29, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要造型柜门', '不要造型柜门', l2_buyao_id, 3, 30, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要阳角条', '不要阳角条', l2_buyao_id, 3, 31, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要隐形衣架', '不要隐形衣架', l2_buyao_id, 3, 32, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要集成灶', '不要集成灶', l2_buyao_id, 3, 33, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-不要高光衣柜门', '不要高光衣柜门', l2_buyao_id, 3, 34, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要一体门', '要一体门', l2_buyao_id, 3, 35, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要乳胶漆', '要乳胶漆', l2_buyao_id, 3, 36, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要免拉手', '要免拉手', l2_buyao_id, 3, 37, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要全屋wifi', '要全屋WiFi', l2_buyao_id, 3, 38, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要全屋通铺', '要全屋通铺', l2_buyao_id, 3, 39, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要分体灶', '要分体灶', l2_buyao_id, 3, 40, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要十字开门冰箱', '要十字开门冰箱', l2_buyao_id, 3, 41, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要单包套', '要单包套', l2_buyao_id, 3, 42, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要双眼皮', '要双眼皮', l2_buyao_id, 3, 43, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要双眼皮吊顶', '要双眼皮吊顶', l2_buyao_id, 3, 44, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要吸顶灯', '要吸顶灯', l2_buyao_id, 3, 45, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要墙排下水', '要墙排下水', l2_buyao_id, 3, 46, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要大单槽', '要大单槽', l2_buyao_id, 3, 47, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要封闭式收纳柜', '要封闭式收纳柜', l2_buyao_id, 3, 48, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要射灯', '要射灯', l2_buyao_id, 3, 49, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要干湿分离卫生间', '要干湿分离卫生间', l2_buyao_id, 3, 50, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要平板柜门', '要平板柜门', l2_buyao_id, 3, 51, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要打通阳台', '要打通阳台', l2_buyao_id, 3, 52, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要斜五孔插座', '要斜五孔插座', l2_buyao_id, 3, 53, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要普通床', '要普通床', l2_buyao_id, 3, 54, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要普通衣架', '要普通衣架', l2_buyao_id, 3, 55, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要智能门锁', '要智能门锁', l2_buyao_id, 3, 56, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要洗烘套装', '要洗烘套装', l2_buyao_id, 3, 57, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要浅色地砖', '要浅色地砖', l2_buyao_id, 3, 58, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要海棠角', '要海棠角', l2_buyao_id, 3, 59, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要直排沙发', '要直排沙发', l2_buyao_id, 3, 60, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要窗帘盒', '要窗帘盒', l2_buyao_id, 3, 61, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要简约背景墙', '要简约背景墙', l2_buyao_id, 3, 62, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要纯色窗帘', '要纯色窗帘', l2_buyao_id, 3, 63, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要肤感衣柜门', '要肤感衣柜门', l2_buyao_id, 3, 64, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要落地电视柜', '要落地电视柜', l2_buyao_id, 3, 65, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要落地马桶', '要落地马桶', l2_buyao_id, 3, 66, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要虹吸马桶', '要虹吸马桶', l2_buyao_id, 3, 67, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-buyao-要隐藏式插座', '要隐藏式插座', l2_buyao_id, 3, 68, 'active', NOW(), NOW());
|
||||
|
||||
-- 三级:买对不买贵(18个)
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-乳胶漆不要买贵的', '乳胶漆不要买贵的', l2_maidui_id, 3, 1, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-全屋角阀要买贵的', '全屋角阀要买贵的', l2_maidui_id, 3, 2, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-前置过滤器不要买贵的', '前置过滤器不要买贵的', l2_maidui_id, 3, 3, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-床不要买贵的', '床不要买贵的', l2_maidui_id, 3, 4, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-床垫要买贵的', '床垫要买贵的', l2_maidui_id, 3, 5, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-开关插座要买贵的', '开关插座要买贵的', l2_maidui_id, 3, 6, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-投影仪要买贵的', '投影仪要买贵的', l2_maidui_id, 3, 7, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-木地板要买贵的', '木地板要买贵的', l2_maidui_id, 3, 8, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-木门不要买贵的', '木门不要买贵的', l2_maidui_id, 3, 9, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-水槽不要买贵的', '水槽不要买贵的', l2_maidui_id, 3, 10, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-水龙头要买贵的', '水龙头要买贵的', l2_maidui_id, 3, 11, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-滑轨要买贵的', '滑轨要买贵的', l2_maidui_id, 3, 12, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-灯具不要买贵的', '灯具不要买贵的', l2_maidui_id, 3, 13, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-瓷砖不要买贵的', '瓷砖不要买贵的', l2_maidui_id, 3, 14, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-电视机不要买贵的', '电视机不要买贵的', l2_maidui_id, 3, 15, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-窗帘不要买贵的', '窗帘不要买贵的', l2_maidui_id, 3, 16, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-腻子粉要买贵的', '腻子粉要买贵的', l2_maidui_id, 3, 17, 'active', NOW(), NOW());
|
||||
INSERT INTO mjk_broll_categories (slug, name, parent_id, level, sort_order, status, created_at, updated_at)
|
||||
VALUES ('bikeng-maidui-门锁要买贵的', '门锁要买贵的', l2_maidui_id, 3, 18, 'active', NOW(), NOW());
|
||||
|
||||
END $$;
|
||||
Executable
+203
@@ -0,0 +1,203 @@
|
||||
#!/bin/bash
|
||||
# 新增素材6.2 批量上传脚本
|
||||
|
||||
# 万华爱格兔宝宝 (5.0s)
|
||||
qshell fput meijiaka-zy materials/3c28619290414e552e988e09b3675b72.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/万华爱格兔宝宝/3c28619290414e552e988e09b3675b72.mp4'
|
||||
|
||||
# 东鹏冠珠马可波罗 (5.01s)
|
||||
qshell fput meijiaka-zy materials/182e4c8bbcea8f103672b147b4606213.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/东鹏冠珠马可波罗/182e4c8bbcea8f103672b147b4606213.mp4'
|
||||
|
||||
# 九牧恒洁箭牌 (5.01s)
|
||||
qshell fput meijiaka-zy materials/7d5607ab1886ba948fc1e94398473274.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/九牧恒洁箭牌/7d5607ab1886ba948fc1e94398473274.mp4'
|
||||
|
||||
# 九牧箭牌潜水艇 (5.01s)
|
||||
qshell fput meijiaka-zy materials/6924bfd00653c3c6cfcb2d1d02c4789c.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/九牧箭牌潜水艇/6924bfd00653c3c6cfcb2d1d02c4789c.mp4'
|
||||
|
||||
# 乳胶漆买谁家 (3.0s)
|
||||
qshell fput meijiaka-zy materials/6086a32ec41db26e5bc570f1f27c1fbc.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/乳胶漆买谁家/6086a32ec41db26e5bc570f1f27c1fbc.mp4'
|
||||
|
||||
# 公牛施耐德西门子 (5.01s)
|
||||
qshell fput meijiaka-zy materials/c0a3e230b32026b78596c192718345cb.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/公牛施耐德西门子/c0a3e230b32026b78596c192718345cb.mp4'
|
||||
|
||||
# 冰箱买谁家 (3.0s)
|
||||
qshell fput meijiaka-zy materials/fad1e564d3272515eab8dd75079870de.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/冰箱买谁家/fad1e564d3272515eab8dd75079870de.mp4'
|
||||
|
||||
# 厨卫五金谁家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/4d20e0593c973366e064451fd9561eff.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/厨卫五金谁家好/4d20e0593c973366e064451fd9561eff.mp4'
|
||||
|
||||
# 吊顶选谁家 (3.0s)
|
||||
qshell fput meijiaka-zy materials/4ee89978139e0e55c809aedf5a56ddc2.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/吊顶选谁家/4ee89978139e0e55c809aedf5a56ddc2.mp4'
|
||||
|
||||
# 圣象世友大自然 (5.01s)
|
||||
qshell fput meijiaka-zy materials/5c78732634c008867211a9a34d124881.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/圣象世友大自然/5c78732634c008867211a9a34d124881.mp4'
|
||||
|
||||
# 地板哪家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/647972fffa483aa807ecd08ed53219be.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/地板哪家好/647972fffa483aa807ecd08ed53219be.mp4'
|
||||
|
||||
# 地漏谁家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/d4fb5c1a8bf3cecf8662f82b6977a954.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/地漏谁家好/d4fb5c1a8bf3cecf8662f82b6977a954.mp4'
|
||||
|
||||
# 奥普友邦法狮龙 (5.01s)
|
||||
qshell fput meijiaka-zy materials/a6cc0fb2a45ea0e4b988451baddac499.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/奥普友邦法狮龙/a6cc0fb2a45ea0e4b988451baddac499.mp4'
|
||||
|
||||
# 家装水泥谁家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/e189e0507cd2ecb0be2e32d2cf65dccd.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/家装水泥谁家好/e189e0507cd2ecb0be2e32d2cf65dccd.mp4'
|
||||
|
||||
# 家装水管谁家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/72d9643978717084039cf3507f9bdd04.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/家装水管谁家好/72d9643978717084039cf3507f9bdd04.mp4'
|
||||
|
||||
# 开关插座买谁家 (3.0s)
|
||||
qshell fput meijiaka-zy materials/8df9cfec100cea2661784361571a3a11.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/开关插座买谁家/8df9cfec100cea2661784361571a3a11.mp4'
|
||||
|
||||
# 德高西卡马贝 (5.01s)
|
||||
qshell fput meijiaka-zy materials/b365eb193c1ede5777cc04d2427ea0e7.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/德高西卡马贝/b365eb193c1ede5777cc04d2427ea0e7.mp4'
|
||||
|
||||
# 方太老板华帝 (5.01s)
|
||||
qshell fput meijiaka-zy materials/9e37037aad90b25493228d05c0405696.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/方太老板华帝/9e37037aad90b25493228d05c0405696.mp4'
|
||||
|
||||
# 日丰伟星保利 (5.01s)
|
||||
qshell fput meijiaka-zy materials/3318c054d10f5ecca8ce397f435c01af.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/日丰伟星保利/3318c054d10f5ecca8ce397f435c01af.mp4'
|
||||
|
||||
# 板材选谁家 (3.0s)
|
||||
qshell fput meijiaka-zy materials/9e5eb608644425a51613e2e145ba28e0.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/板材选谁家/9e5eb608644425a51613e2e145ba28e0.mp4'
|
||||
|
||||
# 水管买谁家 (3.0s)
|
||||
qshell fput meijiaka-zy materials/0d37b9dbf4b44858f16f155ba88a7054.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/水管买谁家/0d37b9dbf4b44858f16f155ba88a7054.mp4'
|
||||
|
||||
# 海尔美的卡萨帝 (5.01s)
|
||||
qshell fput meijiaka-zy materials/33e3a3ae93f65e72d055431b0d132495.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/海尔美的卡萨帝/33e3a3ae93f65e72d055431b0d132495.mp4'
|
||||
|
||||
# 海螺红狮中联 (5.01s)
|
||||
qshell fput meijiaka-zy materials/cd7f119729a062df350940bb003f873d.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/海螺红狮中联/cd7f119729a062df350940bb003f873d.mp4'
|
||||
|
||||
# 烟机哪家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/01690f5d76a40142f365da61319c9f31.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/烟机哪家好/01690f5d76a40142f365da61319c9f31.mp4'
|
||||
|
||||
# 玻璃胶谁家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/52d7c9db8291af1d9e2061c3288bec2b.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/玻璃胶谁家好/52d7c9db8291af1d9e2061c3288bec2b.mp4'
|
||||
|
||||
# 瓦克西卡百得 (5.01s)
|
||||
qshell fput meijiaka-zy materials/1e2774b366631016acecc03c322dc0a2.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/瓦克西卡百得/1e2774b366631016acecc03c322dc0a2.mp4'
|
||||
|
||||
# 瓷砖哪家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/e727f0ea33030c14d33185329794398c.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/瓷砖哪家好/e727f0ea33030c14d33185329794398c.mp4'
|
||||
|
||||
# 瓷砖胶谁家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/07a1f9a9ec09809d33781a2b6265ff48.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/瓷砖胶谁家好/07a1f9a9ec09809d33781a2b6265ff48.mp4'
|
||||
|
||||
# 电线买谁家 (3.0s)
|
||||
qshell fput meijiaka-zy materials/04f61f5a57454799d2aa1000764311af.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/电线买谁家/04f61f5a57454799d2aa1000764311af.mp4'
|
||||
|
||||
# 电视买谁家 (3.0s)
|
||||
qshell fput meijiaka-zy materials/82c400fbc27c42dd8992458d2e94c047.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/电视买谁家/82c400fbc27c42dd8992458d2e94c047.mp4'
|
||||
|
||||
# 石膏板谁家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/e6469a2dc66350c330d2d87f08a9cd5a.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/石膏板谁家好/e6469a2dc66350c330d2d87f08a9cd5a.mp4'
|
||||
|
||||
# 科勒九牧汉斯格雅 (5.01s)
|
||||
qshell fput meijiaka-zy materials/80506a9eb5e1a06d3c5cb98eaae515cd.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/科勒九牧汉斯格雅/80506a9eb5e1a06d3c5cb98eaae515cd.mp4'
|
||||
|
||||
# 立邦三棵树多乐士 (5.01s)
|
||||
qshell fput meijiaka-zy materials/82b26195cdb7a08793abf3ff17d400eb.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/立邦三棵树多乐士/82b26195cdb7a08793abf3ff17d400eb.mp4'
|
||||
|
||||
# 立邦德高东方雨虹 (5.01s)
|
||||
qshell fput meijiaka-zy materials/03602a66190ad519f81c70a80e6cd0d2.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/立邦德高东方雨虹/03602a66190ad519f81c70a80e6cd0d2.mp4'
|
||||
|
||||
# 立邦美巢圣戈邦 (5.01s)
|
||||
qshell fput meijiaka-zy materials/cc00321964988a29315468e4dca53d06.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/立邦美巢圣戈邦/cc00321964988a29315468e4dca53d06.mp4'
|
||||
|
||||
# 索尼海信TCL (5.01s)
|
||||
qshell fput meijiaka-zy materials/fb9d3b062be7ed44fef7ec81490a4963.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/索尼海信TCL/fb9d3b062be7ed44fef7ec81490a4963.mp4'
|
||||
|
||||
# 腻子粉哪家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/887c5ea3238d2c1196eb09c410dc8f7f.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/腻子粉哪家好/887c5ea3238d2c1196eb09c410dc8f7f.mp4'
|
||||
|
||||
# 花洒哪家好 (3.0s)
|
||||
qshell fput meijiaka-zy materials/f6d4592e08482cb903daca04d432f3ed.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/花洒哪家好/f6d4592e08482cb903daca04d432f3ed.mp4'
|
||||
|
||||
# 远东宝胜熊猫 (5.01s)
|
||||
qshell fput meijiaka-zy materials/c5ac8d8ff5435e123c8016657fbf955c.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/远东宝胜熊猫/c5ac8d8ff5435e123c8016657fbf955c.mp4'
|
||||
|
||||
# 金牛伟星日丰 (5.01s)
|
||||
qshell fput meijiaka-zy materials/3c9546527a38c300050ed28bda81e204.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/金牛伟星日丰/3c9546527a38c300050ed28bda81e204.mp4'
|
||||
|
||||
# 防水买谁家 (3.0s)
|
||||
qshell fput meijiaka-zy materials/3e7c5b554836376605828f374bd15ae5.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/防水买谁家/3e7c5b554836376605828f374bd15ae5.mp4'
|
||||
|
||||
# 龙牌泰山可耐福 (5.01s)
|
||||
qshell fput meijiaka-zy materials/6589ffbf04c596583d2d2316216c7428.mp4 '/Users/0fun/Downloads/新增素材6.2/其他/装修材料选择/龙牌泰山可耐福/6589ffbf04c596583d2d2316216c7428.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (10.01s)
|
||||
qshell fput meijiaka-zy materials/488d5a9ac72bf8e9a2a574284f2bdded.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/488d5a9ac72bf8e9a2a574284f2bdded.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (9.97s)
|
||||
qshell fput meijiaka-zy materials/3c716b567964591569abfc8d16e7ff9b.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/3c716b567964591569abfc8d16e7ff9b.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (10.01s)
|
||||
qshell fput meijiaka-zy materials/2fb5183ef1547fb3d7d4d8e56fe5154a.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/2fb5183ef1547fb3d7d4d8e56fe5154a.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (10.01s)
|
||||
qshell fput meijiaka-zy materials/0d7081072e409e8f2c7d3a61a793f348.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/0d7081072e409e8f2c7d3a61a793f348.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (8.48s)
|
||||
qshell fput meijiaka-zy materials/f083bfca1800c171af2dfc767fc5d28a.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/f083bfca1800c171af2dfc767fc5d28a.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (9.8s)
|
||||
qshell fput meijiaka-zy materials/8e198970fe0be12c9067fefe658d1cdd.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/8e198970fe0be12c9067fefe658d1cdd.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (9.92s)
|
||||
qshell fput meijiaka-zy materials/3763505e431dc39a9fef135ad82ee6f1.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/3763505e431dc39a9fef135ad82ee6f1.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (9.9s)
|
||||
qshell fput meijiaka-zy materials/0576abe034e932071d7c3b67104e79c3.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/0576abe034e932071d7c3b67104e79c3.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (4.71s)
|
||||
qshell fput meijiaka-zy materials/07281a59949dcaa72c22b54b8569e1ea.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/07281a59949dcaa72c22b54b8569e1ea.mp4'
|
||||
|
||||
# 装烟机灶具-主材安装镜 (4.09s)
|
||||
qshell fput meijiaka-zy materials/e8d1429ec925636603b69b57ca3eba52.mp4 '/Users/0fun/Downloads/新增素材6.2/安装收尾类/电器安装/装烟机灶具/e8d1429ec925636603b69b57ca3eba52.mp4'
|
||||
|
||||
# 封窗施工-封窗镜 (6.01s)
|
||||
qshell fput meijiaka-zy materials/9d516288fc104f41e3a4c3747883a072.mp4 '/Users/0fun/Downloads/新增素材6.2/拆改改造类/封窗/封窗施工/9d516288fc104f41e3a4c3747883a072.mp4'
|
||||
|
||||
# 封窗施工-封窗镜 (6.18s)
|
||||
qshell fput meijiaka-zy materials/8b293fcabbf5e8dc2f84a77e9cbb9956.mp4 '/Users/0fun/Downloads/新增素材6.2/拆改改造类/封窗/封窗施工/8b293fcabbf5e8dc2f84a77e9cbb9956.mp4'
|
||||
|
||||
# 封窗施工-封窗镜 (10.0s)
|
||||
qshell fput meijiaka-zy materials/432a789688f8f729ec72ccbf8571820a.mp4 '/Users/0fun/Downloads/新增素材6.2/拆改改造类/封窗/封窗施工/432a789688f8f729ec72ccbf8571820a.mp4'
|
||||
|
||||
# 封窗施工-封窗镜 (5.62s)
|
||||
qshell fput meijiaka-zy materials/3a97f3ebd82b3ff8c04ec47871199159.mp4 '/Users/0fun/Downloads/新增素材6.2/拆改改造类/封窗/封窗施工/3a97f3ebd82b3ff8c04ec47871199159.mp4'
|
||||
|
||||
# 封窗施工-封窗镜 (8.55s)
|
||||
qshell fput meijiaka-zy materials/a67d0a41c10ea002968fb0ed1ad8ae0e.mp4 '/Users/0fun/Downloads/新增素材6.2/拆改改造类/封窗/封窗施工/a67d0a41c10ea002968fb0ed1ad8ae0e.mp4'
|
||||
|
||||
# 封窗施工-封窗镜 (8.92s)
|
||||
qshell fput meijiaka-zy materials/6b28e44d37a14c3d9ac8b503a29a421c.mp4 '/Users/0fun/Downloads/新增素材6.2/拆改改造类/封窗/封窗施工/6b28e44d37a14c3d9ac8b503a29a421c.mp4'
|
||||
|
||||
# 封窗施工-封窗镜 (10.2s)
|
||||
qshell fput meijiaka-zy materials/9468e8aba0f47eeef9bc59e395bfcc72.mp4 '/Users/0fun/Downloads/新增素材6.2/拆改改造类/封窗/封窗施工/9468e8aba0f47eeef9bc59e395bfcc72.mp4'
|
||||
|
||||
# 封窗施工-封窗镜 (10.01s)
|
||||
qshell fput meijiaka-zy materials/b5d109f43246296ca6de51d28e596dcd.mp4 '/Users/0fun/Downloads/新增素材6.2/拆改改造类/封窗/封窗施工/b5d109f43246296ca6de51d28e596dcd.mp4'
|
||||
|
||||
# 家具进场摆放就位-软装进场镜 (7.01s)
|
||||
qshell fput meijiaka-zy materials/a15878e831df63d8cffc1066bf25e257.mp4 '/Users/0fun/Downloads/新增素材6.2/软装完工&验收类/软装进场镜/家具进场摆放就位-软装进场/a15878e831df63d8cffc1066bf25e257.mp4'
|
||||
|
||||
# 家具进场摆放就位-软装进场镜 (10.01s)
|
||||
qshell fput meijiaka-zy materials/1622140bd7229107b38927b831f8bea8.mp4 '/Users/0fun/Downloads/新增素材6.2/软装完工&验收类/软装进场镜/家具进场摆放就位-软装进场/1622140bd7229107b38927b831f8bea8.mp4'
|
||||
|
||||
# 家具进场摆放就位-软装进场镜 (7.85s)
|
||||
qshell fput meijiaka-zy materials/5ef57acfda494cfdfb5185594c51364c.mp4 '/Users/0fun/Downloads/新增素材6.2/软装完工&验收类/软装进场镜/家具进场摆放就位-软装进场/5ef57acfda494cfdfb5185594c51364c.mp4'
|
||||
|
||||
# 家具进场摆放就位-软装进场镜 (6.11s)
|
||||
qshell fput meijiaka-zy materials/4473c9736c25e77375459bd2968f4753.mp4 '/Users/0fun/Downloads/新增素材6.2/软装完工&验收类/软装进场镜/家具进场摆放就位-软装进场/4473c9736c25e77375459bd2968f4753.mp4'
|
||||
|
||||
# 家具进场摆放就位-软装进场镜 (5.2s)
|
||||
qshell fput meijiaka-zy materials/67526f381c629710d78c53ec3a2f6941.mp4 '/Users/0fun/Downloads/新增素材6.2/软装完工&验收类/软装进场镜/家具进场摆放就位-软装进场/67526f381c629710d78c53ec3a2f6941.mp4'
|
||||
|
||||
# 家具进场摆放就位-软装进场镜 (7.41s)
|
||||
qshell fput meijiaka-zy materials/1690725fa66d82b9400e462839328658.mp4 '/Users/0fun/Downloads/新增素材6.2/软装完工&验收类/软装进场镜/家具进场摆放就位-软装进场/1690725fa66d82b9400e462839328658.mp4'
|
||||
|
||||
# 家具进场摆放就位-软装进场镜 (9.45s)
|
||||
qshell fput meijiaka-zy materials/28b67c0a7565ff259f889456334d000d.mp4 '/Users/0fun/Downloads/新增素材6.2/软装完工&验收类/软装进场镜/家具进场摆放就位-软装进场/28b67c0a7565ff259f889456334d000d.mp4'
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
**美家卡智影**(产品名)是一款基于 Tauri v2 + React 19 + TypeScript 的桌面端 AI 视频创作应用。
|
||||
|
||||
- **产品标识**: `cn.meijiaka.ai-video`
|
||||
- **版本**: `1.6.5`
|
||||
- **版本**: `1.8.1`
|
||||
- **窗口尺寸**: 1200×800,不可缩放(`resizable: false`)
|
||||
- **核心功能**: AI 脚本生成、AI 配音合成、视频生成、压制成片(FFmpeg)、项目本地持久化
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ export default tseslint.config(
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/prop-types': 'off', // TypeScript 已有类型检查,无需 PropTypes
|
||||
'react-hooks/set-state-in-effect': 'warn',
|
||||
'react-hooks/incompatible-library': 'off', // TanStack Virtual 等常见库误报
|
||||
'react-refresh/only-export-components': 'warn',
|
||||
'react/no-unescaped-entities': 'warn',
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "tauri-app",
|
||||
"version": "1.6.5",
|
||||
"version": "1.8.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tauri-app",
|
||||
"version": "1.6.5",
|
||||
"version": "1.8.1",
|
||||
"dependencies": {
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@tanstack/react-virtual": "^3.13.23",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tauri-app",
|
||||
"private": true,
|
||||
"version": "1.6.5",
|
||||
"version": "1.8.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -4219,7 +4219,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-app"
|
||||
version = "1.6.5"
|
||||
version = "1.8.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-app"
|
||||
version = "1.6.5"
|
||||
version = "1.8.1"
|
||||
description = "美家卡智影 - AI 视频创作桌面应用"
|
||||
authors = ["美家卡科技"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -6,11 +6,17 @@ use crate::storage::project as project_storage;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_project_asset(
|
||||
app: tauri::AppHandle,
|
||||
project_id: String,
|
||||
filename: String,
|
||||
base64_data: String,
|
||||
) -> ApiResponse<String> {
|
||||
match project_storage::save_project_asset(&project_id, &filename, &base64_data).map_err_string() {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: None },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: None },
|
||||
};
|
||||
match project_storage::save_project_asset(&user_id, &project_id, &filename, &base64_data).map_err_string() {
|
||||
Ok(path) => ApiResponse {
|
||||
code: 200,
|
||||
message: "资源保存成功".to_string(),
|
||||
@@ -26,10 +32,16 @@ pub async fn save_project_asset(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_video_save_path(
|
||||
app: tauri::AppHandle,
|
||||
project_id: String,
|
||||
filename: String,
|
||||
) -> ApiResponse<String> {
|
||||
match project_storage::get_video_save_path(&project_id, &filename).map_err_string() {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: None },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: None },
|
||||
};
|
||||
match project_storage::get_video_save_path(&user_id, &project_id, &filename).map_err_string() {
|
||||
Ok(path) => ApiResponse {
|
||||
code: 200,
|
||||
message: "获取路径成功".to_string(),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use crate::ApiResponse;
|
||||
use crate::StringResultExt;
|
||||
use crate::storage::auth as auth_storage;
|
||||
use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_auth_state(app: tauri::AppHandle) -> ApiResponse<Option<serde_json::Value>> {
|
||||
@@ -26,11 +27,21 @@ pub async fn save_auth_state(
|
||||
state: serde_json::Value,
|
||||
) -> ApiResponse<bool> {
|
||||
match auth_storage::save_auth_state(&app, &state).map_err_string() {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "Auth state saved successfully".to_string(),
|
||||
data: Some(true),
|
||||
},
|
||||
Ok(_) => {
|
||||
// 保存 auth 成功后,检查是否需要迁移旧全局数据
|
||||
// 场景:用户在退出状态下升级应用,setup 阶段跳过迁移;
|
||||
// 重新登录时触发迁移,确保旧数据正确归入当前用户目录
|
||||
if let Ok(data_dir) = app.path().app_local_data_dir() {
|
||||
if let Err(e) = crate::migrate_legacy_data(&data_dir, &app) {
|
||||
eprintln!("[save_auth_state] 数据迁移失败: {}", e);
|
||||
}
|
||||
}
|
||||
ApiResponse {
|
||||
code: 200,
|
||||
message: "Auth state saved successfully".to_string(),
|
||||
data: Some(true),
|
||||
}
|
||||
}
|
||||
Err(e) => ApiResponse {
|
||||
code: 500,
|
||||
message: format!("Failed to save auth state: {}", e),
|
||||
|
||||
@@ -17,8 +17,15 @@ pub struct CoverAvatarArgs {
|
||||
|
||||
/// 加载封面形象库
|
||||
#[tauri::command]
|
||||
pub async fn load_cover_avatars() -> ApiResponse<Vec<cover_avatar_storage::CoverAvatar>> {
|
||||
match cover_avatar_storage::load_cover_avatars() {
|
||||
pub async fn load_cover_avatars(
|
||||
app: tauri::AppHandle,
|
||||
) -> ApiResponse<Vec<cover_avatar_storage::CoverAvatar>> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(vec![]) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(vec![]) },
|
||||
};
|
||||
match cover_avatar_storage::load_cover_avatars(&user_id) {
|
||||
Ok(list) => ApiResponse {
|
||||
code: 200,
|
||||
message: "封面形象库加载成功".to_string(),
|
||||
@@ -35,8 +42,14 @@ pub async fn load_cover_avatars() -> ApiResponse<Vec<cover_avatar_storage::Cover
|
||||
/// 保存封面形象
|
||||
#[tauri::command]
|
||||
pub async fn save_cover_avatar(
|
||||
app: tauri::AppHandle,
|
||||
args: CoverAvatarArgs,
|
||||
) -> ApiResponse<bool> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(false) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(false) },
|
||||
};
|
||||
let avatar = cover_avatar_storage::CoverAvatar {
|
||||
id: args.id,
|
||||
name: args.name,
|
||||
@@ -44,7 +57,7 @@ pub async fn save_cover_avatar(
|
||||
local_path: args.local_path,
|
||||
created_at: args.created_at,
|
||||
};
|
||||
match cover_avatar_storage::add_cover_avatar(avatar) {
|
||||
match cover_avatar_storage::add_cover_avatar(&user_id, avatar) {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "封面形象保存成功".to_string(),
|
||||
@@ -61,9 +74,15 @@ pub async fn save_cover_avatar(
|
||||
/// 删除封面形象
|
||||
#[tauri::command]
|
||||
pub async fn delete_cover_avatar_cmd(
|
||||
app: tauri::AppHandle,
|
||||
id: String,
|
||||
) -> ApiResponse<bool> {
|
||||
match cover_avatar_storage::delete_cover_avatar(&id) {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(false) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(false) },
|
||||
};
|
||||
match cover_avatar_storage::delete_cover_avatar(&user_id, &id) {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "封面形象删除成功".to_string(),
|
||||
@@ -90,8 +109,15 @@ pub struct SaveCoverAvatarImageArgs {
|
||||
/// 保存封面形象图片文件(前端传入 base64 编码)
|
||||
#[tauri::command]
|
||||
pub async fn save_cover_avatar_image(
|
||||
app: tauri::AppHandle,
|
||||
args: SaveCoverAvatarImageArgs,
|
||||
) -> ApiResponse<String> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: None },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: None },
|
||||
};
|
||||
|
||||
let image_bytes = match base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&args.image_data,
|
||||
@@ -105,6 +131,7 @@ pub async fn save_cover_avatar_image(
|
||||
};
|
||||
|
||||
match cover_avatar_storage::save_cover_avatar_image(
|
||||
&user_id,
|
||||
&args.avatar_id,
|
||||
&image_bytes,
|
||||
&args.ext,
|
||||
|
||||
@@ -23,8 +23,17 @@ pub struct ProductItem {
|
||||
|
||||
/// 获取成品保存路径(项目成品目录)
|
||||
#[tauri::command]
|
||||
pub async fn get_product_save_path(project_id: String, filename: String) -> ApiResponse<String> {
|
||||
match get_project_products_dir(&project_id) {
|
||||
pub async fn get_product_save_path(
|
||||
app: tauri::AppHandle,
|
||||
project_id: String,
|
||||
filename: String,
|
||||
) -> ApiResponse<String> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: None },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: None },
|
||||
};
|
||||
match get_project_products_dir(&user_id, &project_id) {
|
||||
Ok(products_dir) => {
|
||||
let safe_filename = match sanitize_filename(&filename) {
|
||||
Ok(name) => name,
|
||||
@@ -52,8 +61,13 @@ pub async fn get_product_save_path(project_id: String, filename: String) -> ApiR
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_local_products(_app: AppHandle) -> ApiResponse<Vec<ProductItem>> {
|
||||
let projects_root = match crate::storage::get_projects_root_dir() {
|
||||
pub async fn list_local_products(app: AppHandle) -> ApiResponse<Vec<ProductItem>> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(vec![]) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(vec![]) },
|
||||
};
|
||||
let projects_root = match crate::storage::get_projects_root_dir(&user_id) {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => return ApiResponse {
|
||||
code: 500,
|
||||
@@ -191,10 +205,19 @@ pub async fn list_local_products(_app: AppHandle) -> ApiResponse<Vec<ProductItem
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_local_product(path: String) -> ApiResponse<()> {
|
||||
pub async fn delete_local_product(
|
||||
app: tauri::AppHandle,
|
||||
path: String,
|
||||
) -> ApiResponse<()> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: None },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: None },
|
||||
};
|
||||
|
||||
let target = PathBuf::from(&path);
|
||||
|
||||
// 安全检查:确保路径在应用数据目录内
|
||||
// 安全检查:确保路径在应用数据目录内,且在当前用户目录下
|
||||
let canonical = match target.canonicalize() {
|
||||
Ok(p) => p,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
@@ -229,6 +252,17 @@ pub async fn delete_local_product(path: String) -> ApiResponse<()> {
|
||||
};
|
||||
}
|
||||
|
||||
// 用户隔离检查:路径必须在当前用户的 users/{id}/ 目录下
|
||||
let user_prefix = app_data.join("users").join(&user_id);
|
||||
let user_prefix_canonical = user_prefix.canonicalize().unwrap_or_else(|_| user_prefix.clone());
|
||||
if !canonical.starts_with(&user_prefix_canonical) {
|
||||
return ApiResponse {
|
||||
code: 403,
|
||||
message: "无权操作其他用户的数据".to_string(),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
|
||||
if target.exists() && target.is_file() {
|
||||
match fs::remove_file(&target) {
|
||||
Ok(_) => ApiResponse {
|
||||
@@ -252,7 +286,17 @@ pub async fn delete_local_product(path: String) -> ApiResponse<()> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn rename_local_product(path: String, new_filename: String) -> ApiResponse<()> {
|
||||
pub async fn rename_local_product(
|
||||
app: tauri::AppHandle,
|
||||
path: String,
|
||||
new_filename: String,
|
||||
) -> ApiResponse<()> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: None },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: None },
|
||||
};
|
||||
|
||||
let old_path = PathBuf::from(&path);
|
||||
|
||||
// 安全检查
|
||||
@@ -290,6 +334,17 @@ pub async fn rename_local_product(path: String, new_filename: String) -> ApiResp
|
||||
};
|
||||
}
|
||||
|
||||
// 用户隔离检查
|
||||
let user_prefix = app_data.join("users").join(&user_id);
|
||||
let user_prefix_canonical = user_prefix.canonicalize().unwrap_or_else(|_| user_prefix.clone());
|
||||
if !canonical.starts_with(&user_prefix_canonical) {
|
||||
return ApiResponse {
|
||||
code: 403,
|
||||
message: "无权操作其他用户的数据".to_string(),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
|
||||
let parent = match old_path.parent() {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => return ApiResponse {
|
||||
@@ -339,7 +394,17 @@ pub async fn rename_local_product(path: String, new_filename: String) -> ApiResp
|
||||
|
||||
/// 导出成品到用户指定位置
|
||||
#[tauri::command]
|
||||
pub async fn export_product(source_path: String, target_path: String) -> ApiResponse<String> {
|
||||
pub async fn export_product(
|
||||
app: tauri::AppHandle,
|
||||
source_path: String,
|
||||
target_path: String,
|
||||
) -> ApiResponse<String> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: None },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: None },
|
||||
};
|
||||
|
||||
let source = PathBuf::from(&source_path);
|
||||
let target = PathBuf::from(&target_path);
|
||||
|
||||
@@ -371,6 +436,17 @@ pub async fn export_product(source_path: String, target_path: String) -> ApiResp
|
||||
};
|
||||
}
|
||||
|
||||
// 用户隔离检查:source 必须在当前用户目录下
|
||||
let user_prefix = app_data.join("users").join(&user_id);
|
||||
let user_prefix_canonical = user_prefix.canonicalize().unwrap_or_else(|_| user_prefix.clone());
|
||||
if !source_canonical.starts_with(&user_prefix_canonical) {
|
||||
return ApiResponse {
|
||||
code: 403,
|
||||
message: "无权导出其他用户的数据".to_string(),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
|
||||
if !source.exists() || !source.is_file() {
|
||||
return ApiResponse {
|
||||
code: 404,
|
||||
|
||||
@@ -9,8 +9,17 @@ use crate::storage::project as project_storage;
|
||||
// ============================================================
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_project_meta_raw(project_id: String, json_content: String) -> ApiResponse<bool> {
|
||||
match project_storage::save_project_meta_raw(&project_id, &json_content).map_err_string() {
|
||||
pub async fn save_project_meta_raw(
|
||||
app: tauri::AppHandle,
|
||||
project_id: String,
|
||||
json_content: String,
|
||||
) -> ApiResponse<bool> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(false) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(false) },
|
||||
};
|
||||
match project_storage::save_project_meta_raw(&user_id, &project_id, &json_content).map_err_string() {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "Project saved successfully".to_string(),
|
||||
@@ -25,8 +34,16 @@ pub async fn save_project_meta_raw(project_id: String, json_content: String) ->
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_project_meta(project_id: String) -> ApiResponse<serde_json::Value> {
|
||||
match project_storage::load_project_meta(&project_id).map_err_string() {
|
||||
pub async fn load_project_meta(
|
||||
app: tauri::AppHandle,
|
||||
project_id: String,
|
||||
) -> ApiResponse<serde_json::Value> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(serde_json::Value::Null) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(serde_json::Value::Null) },
|
||||
};
|
||||
match project_storage::load_project_meta(&user_id, &project_id).map_err_string() {
|
||||
Ok(data) => ApiResponse {
|
||||
code: 200,
|
||||
message: "Project loaded successfully".to_string(),
|
||||
@@ -41,8 +58,17 @@ pub async fn load_project_meta(project_id: String) -> ApiResponse<serde_json::Va
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_project_segments_raw(project_id: String, json_content: String) -> ApiResponse<bool> {
|
||||
match project_storage::save_project_segments_raw(&project_id, &json_content).map_err_string() {
|
||||
pub async fn save_project_segments_raw(
|
||||
app: tauri::AppHandle,
|
||||
project_id: String,
|
||||
json_content: String,
|
||||
) -> ApiResponse<bool> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(false) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(false) },
|
||||
};
|
||||
match project_storage::save_project_segments_raw(&user_id, &project_id, &json_content).map_err_string() {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "Segments saved successfully".to_string(),
|
||||
@@ -57,8 +83,16 @@ pub async fn save_project_segments_raw(project_id: String, json_content: String)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_project_segments(project_id: String) -> ApiResponse<serde_json::Value> {
|
||||
match project_storage::load_project_segments(&project_id).map_err_string() {
|
||||
pub async fn load_project_segments(
|
||||
app: tauri::AppHandle,
|
||||
project_id: String,
|
||||
) -> ApiResponse<serde_json::Value> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(serde_json::Value::Array(vec![])) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(serde_json::Value::Array(vec![])) },
|
||||
};
|
||||
match project_storage::load_project_segments(&user_id, &project_id).map_err_string() {
|
||||
Ok(data) => ApiResponse {
|
||||
code: 200,
|
||||
message: "Segments loaded successfully".to_string(),
|
||||
@@ -73,8 +107,15 @@ pub async fn load_project_segments(project_id: String) -> ApiResponse<serde_json
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_local_projects() -> ApiResponse<Vec<serde_json::Value>> {
|
||||
match project_storage::list_projects().map_err_string() {
|
||||
pub async fn list_local_projects(
|
||||
app: tauri::AppHandle,
|
||||
) -> ApiResponse<Vec<serde_json::Value>> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(vec![]) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(vec![]) },
|
||||
};
|
||||
match project_storage::list_projects(&user_id).map_err_string() {
|
||||
Ok(projects) => ApiResponse {
|
||||
code: 200,
|
||||
message: "Projects listed successfully".to_string(),
|
||||
@@ -89,8 +130,16 @@ pub async fn list_local_projects() -> ApiResponse<Vec<serde_json::Value>> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_local_project(project_id: String) -> ApiResponse<bool> {
|
||||
match project_storage::delete_project(&project_id).map_err_string() {
|
||||
pub async fn delete_local_project(
|
||||
app: tauri::AppHandle,
|
||||
project_id: String,
|
||||
) -> ApiResponse<bool> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(false) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(false) },
|
||||
};
|
||||
match project_storage::delete_project(&user_id, &project_id).map_err_string() {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "Project deleted successfully".to_string(),
|
||||
@@ -105,10 +154,20 @@ pub async fn delete_local_project(project_id: String) -> ApiResponse<bool> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_project_file(project_id: String, file_path: String) -> ApiResponse<bool> {
|
||||
pub async fn delete_project_file(
|
||||
app: tauri::AppHandle,
|
||||
project_id: String,
|
||||
file_path: String,
|
||||
) -> ApiResponse<bool> {
|
||||
use crate::storage::get_project_dir_path;
|
||||
|
||||
let project_dir: std::path::PathBuf = match get_project_dir_path(&project_id) {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse { code: 401, message: "未登录".to_string(), data: Some(false) },
|
||||
Err(e) => return ApiResponse { code: 500, message: format!("获取用户信息失败: {}", e), data: Some(false) },
|
||||
};
|
||||
|
||||
let project_dir: std::path::PathBuf = match get_project_dir_path(&user_id, &project_id) {
|
||||
Ok(d) => d,
|
||||
Err(e) => return ApiResponse {
|
||||
code: 500,
|
||||
|
||||
@@ -28,21 +28,55 @@ pub struct UploadResult {
|
||||
/// 兼容旧命名
|
||||
pub type UploadVideoResult = UploadResult;
|
||||
|
||||
/// 获取项目视频目录
|
||||
fn get_project_video_dir(project_id: &str) -> Result<std::path::PathBuf, String> {
|
||||
crate::storage::get_project_videos_dir(project_id)
|
||||
/// 获取项目视频目录(内部辅助函数,从 auth.json 读取当前用户)
|
||||
fn get_project_video_dir(app: &AppHandle, project_id: &str) -> Result<std::path::PathBuf, String> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return Err("未登录".to_string()),
|
||||
Err(e) => return Err(format!("获取用户信息失败: {}", e)),
|
||||
};
|
||||
crate::storage::get_project_videos_dir(&user_id, project_id)
|
||||
.map_err(|e| format!("获取项目视频目录失败: {}", e))
|
||||
}
|
||||
|
||||
/// 校验视频比例是否为 9:16(允许 ±3% 容差)
|
||||
fn validate_nine_sixteen_aspect(width: u32, height: u32) -> Result<(), String> {
|
||||
if width == 0 || height == 0 {
|
||||
return Err("无法读取视频分辨率".to_string());
|
||||
}
|
||||
let ratio = width as f64 / height as f64;
|
||||
let expected = 9.0 / 16.0;
|
||||
if (ratio - expected).abs() / expected > 0.03 {
|
||||
return Err(format!(
|
||||
"视频比例必须是 9:16,当前为 {}×{}(比例 {:.3})",
|
||||
width, height, ratio
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 直接拼接已标准化的视频片段(不做裁剪,只做 concat)
|
||||
/// 根据素材最小高度决定目标输出分辨率
|
||||
fn resolve_target_resolution(heights: &[u32]) -> Option<(u32, u32)> {
|
||||
let min_height = heights.iter().min()?;
|
||||
if *min_height <= 1280 {
|
||||
Some((720, 1280))
|
||||
} else {
|
||||
Some((1080, 1920))
|
||||
}
|
||||
}
|
||||
|
||||
/// 拼接视频片段
|
||||
///
|
||||
/// 1. 探测所有片段分辨率并校验 9:16 比例
|
||||
/// 2. 按最小高度决定目标输出分辨率
|
||||
/// 3. 统一标准化后拼接
|
||||
#[tauri::command]
|
||||
pub async fn concat_video_clips(
|
||||
app: AppHandle,
|
||||
project_id: String,
|
||||
clip_paths: Vec<String>,
|
||||
) -> ApiResponse<ComposeVideoResult> {
|
||||
let video_dir = match get_project_video_dir(&project_id) {
|
||||
let video_dir = match get_project_video_dir(&app, &project_id) {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => {
|
||||
return ApiResponse {
|
||||
@@ -58,29 +92,49 @@ pub async fn concat_video_clips(
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
|
||||
// 生成 concat 列表文件
|
||||
let list_path = video_dir.join(format!("concat_list_{}.txt", timestamp));
|
||||
let mut list_content = String::new();
|
||||
// 1. 探测所有片段分辨率并校验 9:16
|
||||
let mut heights = Vec::new();
|
||||
for path in &clip_paths {
|
||||
list_content.push_str(&format!("file '{}'\n", ffmpeg_cmd::escape_ffmpeg_path(path)));
|
||||
}
|
||||
if let Err(e) = std::fs::write(&list_path, list_content) {
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("创建拼接列表失败: {}", e),
|
||||
data: None,
|
||||
};
|
||||
match ffmpeg_cmd::get_video_metadata(&app, path).await {
|
||||
Ok(meta) => {
|
||||
if let Err(e) = validate_nine_sixteen_aspect(meta.width, meta.height) {
|
||||
return ApiResponse { code: 400, message: e, data: None };
|
||||
}
|
||||
heights.push(meta.height);
|
||||
}
|
||||
Err(e) => {
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("读取视频元数据失败: {}", e),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行拼接(所有片段已标准化,编码一致,直接用 copy)
|
||||
let (target_width, target_height) = match resolve_target_resolution(&heights) {
|
||||
Some(res) => res,
|
||||
None => {
|
||||
return ApiResponse {
|
||||
code: 400,
|
||||
message: "没有可拼接的视频片段".to_string(),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 执行拼接(内部会先标准化到目标分辨率)
|
||||
let output_filename = format!("composed_{}.mp4", timestamp);
|
||||
let output_path = video_dir.join(&output_filename);
|
||||
let output_path_str = output_path.to_string_lossy().to_string();
|
||||
|
||||
let concat_res = ffmpeg_cmd::concat_videos_copy(&app, list_path.to_str().unwrap(), &output_path_str).await;
|
||||
|
||||
// 清理列表文件
|
||||
let _ = std::fs::remove_file(&list_path);
|
||||
let concat_res = ffmpeg_cmd::concat_videos_robust(
|
||||
&app,
|
||||
clip_paths,
|
||||
&output_path_str,
|
||||
target_width,
|
||||
target_height,
|
||||
).await;
|
||||
|
||||
if let Err(e) = concat_res {
|
||||
return ApiResponse {
|
||||
@@ -91,7 +145,6 @@ pub async fn concat_video_clips(
|
||||
}
|
||||
|
||||
// 总时长由前端提供(各片段时长的精确累加)
|
||||
// 此处不通过 ffprobe 读取,避免依赖
|
||||
let total_duration = 0.0;
|
||||
|
||||
ApiResponse {
|
||||
@@ -103,7 +156,6 @@ pub async fn concat_video_clips(
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 空镜片段生成请求参数
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -134,8 +186,25 @@ pub async fn generate_empty_shot_clip(
|
||||
let _ = std::fs::remove_file(&temp_audio);
|
||||
};
|
||||
|
||||
// 1. 截取空镜视频并标准化
|
||||
if let Err(e) = ffmpeg_cmd::clip_video(&app, &args.video_source, 0.0, args.duration, &temp_video).await {
|
||||
// 1. 探测输入分辨率并校验 9:16 比例
|
||||
let (video_width, video_height) = match ffmpeg_cmd::get_video_metadata(&app, &args.video_source).await {
|
||||
Ok(meta) => {
|
||||
if let Err(e) = validate_nine_sixteen_aspect(meta.width, meta.height) {
|
||||
return ApiResponse { code: 400, message: e, data: None };
|
||||
}
|
||||
(meta.width, meta.height)
|
||||
}
|
||||
Err(e) => {
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("读取视频元数据失败: {}", e),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 截取空镜视频并标准化(保持输入分辨率)
|
||||
if let Err(e) = ffmpeg_cmd::clip_video(&app, &args.video_source, 0.0, args.duration, &temp_video, video_width, video_height).await {
|
||||
cleanup();
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
@@ -278,7 +347,24 @@ pub async fn extract_video_segment(
|
||||
};
|
||||
};
|
||||
|
||||
match ffmpeg_cmd::clip_video(&app, &safe_input, args.start, args.duration, &safe_output).await {
|
||||
// 探测输入分辨率并校验 9:16 比例
|
||||
let (video_width, video_height) = match ffmpeg_cmd::get_video_metadata(&app, &safe_input).await {
|
||||
Ok(meta) => {
|
||||
if let Err(e) = validate_nine_sixteen_aspect(meta.width, meta.height) {
|
||||
return ApiResponse { code: 400, message: e, data: None };
|
||||
}
|
||||
(meta.width, meta.height)
|
||||
}
|
||||
Err(e) => {
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("读取视频元数据失败: {}", e),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
match ffmpeg_cmd::clip_video(&app, &safe_input, args.start, args.duration, &safe_output, video_width, video_height).await {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "视频片段截取成功".to_string(),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::ApiResponse;
|
||||
use crate::storage::voice as voice_storage;
|
||||
use tauri::Manager;
|
||||
|
||||
// --------------------- 音色素材库命令 ---------------------
|
||||
|
||||
@@ -19,8 +20,24 @@ pub struct VoiceMaterialArgs {
|
||||
|
||||
/// 加载音色素材库
|
||||
#[tauri::command]
|
||||
pub async fn load_voice_materials() -> ApiResponse<Vec<voice_storage::VoiceMaterial>> {
|
||||
match voice_storage::load_voice_materials() {
|
||||
pub async fn load_voice_materials(
|
||||
app: tauri::AppHandle,
|
||||
) -> ApiResponse<Vec<voice_storage::VoiceMaterial>> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse {
|
||||
code: 401,
|
||||
message: "未登录".to_string(),
|
||||
data: Some(vec![]),
|
||||
},
|
||||
Err(e) => return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("获取用户信息失败: {}", e),
|
||||
data: Some(vec![]),
|
||||
},
|
||||
};
|
||||
|
||||
match voice_storage::load_voice_materials(&user_id) {
|
||||
Ok(list) => ApiResponse {
|
||||
code: 200,
|
||||
message: "素材库加载成功".to_string(),
|
||||
@@ -37,8 +54,23 @@ pub async fn load_voice_materials() -> ApiResponse<Vec<voice_storage::VoiceMater
|
||||
/// 保存音色素材
|
||||
#[tauri::command]
|
||||
pub async fn save_voice_material(
|
||||
app: tauri::AppHandle,
|
||||
args: VoiceMaterialArgs,
|
||||
) -> ApiResponse<bool> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse {
|
||||
code: 401,
|
||||
message: "未登录".to_string(),
|
||||
data: Some(false),
|
||||
},
|
||||
Err(e) => return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("获取用户信息失败: {}", e),
|
||||
data: Some(false),
|
||||
},
|
||||
};
|
||||
|
||||
let material = voice_storage::VoiceMaterial {
|
||||
id: args.id,
|
||||
name: args.name,
|
||||
@@ -48,7 +80,7 @@ pub async fn save_voice_material(
|
||||
status: args.status,
|
||||
created_at: args.created_at,
|
||||
};
|
||||
match voice_storage::add_voice_material(material) {
|
||||
match voice_storage::add_voice_material(&user_id, material) {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "素材保存成功".to_string(),
|
||||
@@ -65,9 +97,24 @@ pub async fn save_voice_material(
|
||||
/// 删除音色素材
|
||||
#[tauri::command]
|
||||
pub async fn delete_voice_material_cmd(
|
||||
app: tauri::AppHandle,
|
||||
id: String,
|
||||
) -> ApiResponse<bool> {
|
||||
match voice_storage::delete_voice_material(&id) {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse {
|
||||
code: 401,
|
||||
message: "未登录".to_string(),
|
||||
data: Some(false),
|
||||
},
|
||||
Err(e) => return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("获取用户信息失败: {}", e),
|
||||
data: Some(false),
|
||||
},
|
||||
};
|
||||
|
||||
match voice_storage::delete_voice_material(&user_id, &id) {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "素材删除成功".to_string(),
|
||||
@@ -97,8 +144,23 @@ pub struct SaveAudioArgs {
|
||||
/// 保存音频文件(前端传入 base64 编码)
|
||||
#[tauri::command]
|
||||
pub async fn save_audio(
|
||||
app: tauri::AppHandle,
|
||||
args: SaveAudioArgs,
|
||||
) -> ApiResponse<voice_storage::AudioMeta> {
|
||||
let user_id = match crate::storage::auth::get_current_user_id(&app) {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return ApiResponse {
|
||||
code: 401,
|
||||
message: "未登录".to_string(),
|
||||
data: None,
|
||||
},
|
||||
Err(e) => return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("获取用户信息失败: {}", e),
|
||||
data: None,
|
||||
},
|
||||
};
|
||||
|
||||
let audio_bytes = match base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&args.audio_data,
|
||||
@@ -112,6 +174,7 @@ pub async fn save_audio(
|
||||
};
|
||||
|
||||
match voice_storage::save_audio_file(
|
||||
&user_id,
|
||||
&args.project_id,
|
||||
&args.audio_id,
|
||||
&audio_bytes,
|
||||
@@ -169,6 +232,70 @@ pub async fn extract_audio_segment(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地视频文件中提取音频(MP4 → MP3)
|
||||
*
|
||||
* 输出文件自动生成在 app_local_data_dir/temp/ 下。
|
||||
* @param input_path 本地视频文件路径(需在 app_local_data_dir 下)
|
||||
* @returns 提取后的 MP3 文件路径
|
||||
*/
|
||||
#[tauri::command]
|
||||
pub async fn extract_audio_from_video(
|
||||
app: tauri::AppHandle,
|
||||
input_path: String,
|
||||
) -> ApiResponse<String> {
|
||||
// 验证输入路径安全
|
||||
let safe_input = match crate::ffmpeg_cmd::sanitize_output_path(&input_path) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("路径验证失败: {}", e),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 生成输出路径:app_local_data_dir/temp/extracted_{uuid}.mp3
|
||||
let output_path = match app.path().app_local_data_dir() {
|
||||
Ok(dir) => {
|
||||
let temp_dir = dir.join("temp");
|
||||
if let Err(e) = std::fs::create_dir_all(&temp_dir) {
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("创建临时目录失败: {}", e),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
temp_dir.join(format!("extracted_{}.mp3", uuid::Uuid::new_v4()))
|
||||
}
|
||||
Err(e) => {
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("获取应用目录失败: {}", e),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
match crate::ffmpeg_cmd::extract_audio_from_video(
|
||||
&app,
|
||||
&safe_input,
|
||||
&output_path.to_string_lossy(),
|
||||
).await {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "音频提取成功".to_string(),
|
||||
data: Some(output_path.to_string_lossy().to_string()),
|
||||
},
|
||||
Err(e) => ApiResponse {
|
||||
code: 500,
|
||||
message: format!("音频提取失败: {}", e),
|
||||
data: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传本地音频文件到后端,后端上传到七牛云并返回 URL
|
||||
#[tauri::command]
|
||||
pub async fn upload_audio_file(
|
||||
|
||||
@@ -206,16 +206,27 @@ pub async fn run_ffmpeg_in_dir(
|
||||
|
||||
|
||||
/**
|
||||
* 标准化单个视频片段 (调整为 1080:1920, 25fps, libx264, aac 44100Hz stereo)
|
||||
* 标准化单个视频片段 (调整为目标分辨率, 25fps, libx264, aac 44100Hz stereo)
|
||||
*/
|
||||
pub async fn standardize_video(app: &AppHandle, input_path: &str, output_path: &str) -> Result<(), String> {
|
||||
pub async fn standardize_video(
|
||||
app: &AppHandle,
|
||||
input_path: &str,
|
||||
output_path: &str,
|
||||
target_width: u32,
|
||||
target_height: u32,
|
||||
) -> Result<(), String> {
|
||||
// 验证路径安全
|
||||
let safe_input = validate_safe_path(input_path)?;
|
||||
let safe_output = sanitize_output_path(output_path)?;
|
||||
|
||||
|
||||
let vf = format!(
|
||||
"fps=25,scale={}:{}:force_original_aspect_ratio=decrease,pad={}:{}:(ow-iw)/2:(oh-ih)/2,format=yuv420p",
|
||||
target_width, target_height, target_width, target_height
|
||||
);
|
||||
|
||||
let args = vec![
|
||||
"-i".to_string(), safe_input,
|
||||
"-vf".to_string(), "fps=25,scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,format=yuv420p".to_string(),
|
||||
"-vf".to_string(), vf,
|
||||
"-c:v".to_string(), "libx264".to_string(),
|
||||
"-c:a".to_string(), "aac".to_string(),
|
||||
"-ar".to_string(), "44100".to_string(),
|
||||
@@ -251,7 +262,13 @@ pub async fn concat_videos_copy(app: &AppHandle, list_path: &str, output_path: &
|
||||
/**
|
||||
* 拼接视频 - 兼容模式 (多步走:标准化 -> 拼接)
|
||||
*/
|
||||
pub async fn concat_videos_robust(app: &AppHandle, video_paths: Vec<String>, output_path: &str) -> Result<(), String> {
|
||||
pub async fn concat_videos_robust(
|
||||
app: &AppHandle,
|
||||
video_paths: Vec<String>,
|
||||
output_path: &str,
|
||||
target_width: u32,
|
||||
target_height: u32,
|
||||
) -> Result<(), String> {
|
||||
// 使用输出路径的父目录作为临时目录(确保在安全目录内)
|
||||
let output_parent = std::path::Path::new(output_path)
|
||||
.parent()
|
||||
@@ -261,13 +278,13 @@ pub async fn concat_videos_robust(app: &AppHandle, video_paths: Vec<String>, out
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
|
||||
|
||||
let mut standardized_paths = Vec::new();
|
||||
|
||||
// 1. 标准化每个片段(内部已验证路径)
|
||||
for (i, path) in video_paths.iter().enumerate() {
|
||||
let std_path = output_parent.join(format!("std_{}_{}.mp4", timestamp, i));
|
||||
match standardize_video(app, path, std_path.to_str().unwrap()).await {
|
||||
match standardize_video(app, path, std_path.to_str().unwrap(), target_width, target_height).await {
|
||||
Ok(()) => standardized_paths.push(std_path),
|
||||
Err(e) => {
|
||||
// 清理本轮已创建的标准化文件
|
||||
@@ -325,14 +342,26 @@ pub async fn add_audio_to_video(app: &AppHandle, video_path: &str, audio_path: &
|
||||
}
|
||||
|
||||
/**
|
||||
* 将封面图转换为一段短视频 (0.5s, 1080x1920, 25fps)
|
||||
* 将封面图转换为一段短视频 (0.5s, 目标分辨率, 25fps)
|
||||
* 带静音音频轨道,避免 concat 时丢失后续片段音频
|
||||
*/
|
||||
pub async fn create_cover_video(app: &AppHandle, input_path: &str, output_path: &str, duration: &str) -> Result<(), String> {
|
||||
pub async fn create_cover_video(
|
||||
app: &AppHandle,
|
||||
input_path: &str,
|
||||
output_path: &str,
|
||||
duration: &str,
|
||||
target_width: u32,
|
||||
target_height: u32,
|
||||
) -> Result<(), String> {
|
||||
// 验证路径安全
|
||||
let safe_input = validate_safe_path(input_path)?;
|
||||
let safe_output = sanitize_output_path(output_path)?;
|
||||
|
||||
|
||||
let vf = format!(
|
||||
"scale={}:{}:force_original_aspect_ratio=decrease,pad={}:{}:(ow-iw)/2:(oh-ih)/2,setsar=1",
|
||||
target_width, target_height, target_width, target_height
|
||||
);
|
||||
|
||||
let args = vec![
|
||||
"-loop".to_string(), "1".to_string(),
|
||||
"-i".to_string(), safe_input,
|
||||
@@ -342,7 +371,7 @@ pub async fn create_cover_video(app: &AppHandle, input_path: &str, output_path:
|
||||
"-c:a".to_string(), "aac".to_string(),
|
||||
"-t".to_string(), duration.to_string(),
|
||||
"-pix_fmt".to_string(), "yuv420p".to_string(),
|
||||
"-vf".to_string(), "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,setsar=1".to_string(),
|
||||
"-vf".to_string(), vf,
|
||||
"-r".to_string(), "25".to_string(),
|
||||
"-shortest".to_string(),
|
||||
"-y".to_string(),
|
||||
@@ -560,7 +589,13 @@ pub async fn mix_bgm_to_video(
|
||||
bgm_volume: f64,
|
||||
) -> Result<(), String> {
|
||||
let safe_video = validate_safe_path(video_path)?;
|
||||
let safe_bgm = validate_safe_path(bgm_path)?;
|
||||
// BGM 可以是用户通过系统文件选择器选取的任意本地文件,
|
||||
// 验证文件存在且不含路径遍历字符
|
||||
let safe_bgm = if !bgm_path.contains("..") && std::path::Path::new(bgm_path).exists() {
|
||||
bgm_path.to_string()
|
||||
} else {
|
||||
return Err(format!("BGM 文件不存在或路径非法: {}", bgm_path));
|
||||
};
|
||||
let safe_output = sanitize_output_path(output_path)?;
|
||||
|
||||
// 构建 filter_complex:
|
||||
@@ -568,7 +603,7 @@ pub async fn mix_bgm_to_video(
|
||||
// [1:a]volume=0.25,aloop=loop=-1:size=2e+09[bgm]; — BGM 调整音量后无限循环
|
||||
// [a0][bgm]amix=inputs=2:duration=first:dropout_transition=2[aout] — 混合,以第一个输入时长为准
|
||||
let filter_complex = format!(
|
||||
"[0:a]volume={:.2}[a0];[1:a]volume={:.2},aloop=loop=-1:size=2e+09[bgm];[a0][bgm]amix=inputs=2:duration=first:dropout_transition=2[aout]",
|
||||
"[0:a]volume={:.2}[a0];[1:a]volume={:.2},aloop=loop=-1:size=2e+09[bgm];[a0][bgm]amix=inputs=2:duration=first:dropout_transition=2:normalize=0[aout]",
|
||||
video_volume, bgm_volume
|
||||
);
|
||||
|
||||
@@ -739,7 +774,7 @@ pub async fn get_video_metadata(app: &AppHandle, input_path: &str) -> Result<Vid
|
||||
/**
|
||||
* 裁剪视频片段(支持本地文件和 HTTP URL)
|
||||
*
|
||||
* 从起始时间裁剪指定时长,同时标准化输出格式(1080x1920, 25fps, libx264, aac)。
|
||||
* 从起始时间裁剪指定时长,同时标准化输出格式(目标分辨率, 25fps, libx264, aac)。
|
||||
* 适用于从人物形象素材或空镜素材中提取指定时长的片段。
|
||||
*/
|
||||
pub async fn clip_video(
|
||||
@@ -748,6 +783,8 @@ pub async fn clip_video(
|
||||
start: f64,
|
||||
duration: f64,
|
||||
output_path: &str,
|
||||
target_width: u32,
|
||||
target_height: u32,
|
||||
) -> Result<(), String> {
|
||||
let safe_output = sanitize_output_path(output_path)?;
|
||||
|
||||
@@ -764,11 +801,16 @@ pub async fn clip_video(
|
||||
let start_str = format!("{:.3}", start);
|
||||
let duration_str = format!("{:.3}", duration);
|
||||
|
||||
let vf = format!(
|
||||
"fps=25,scale={}:{}:force_original_aspect_ratio=decrease,pad={}:{}:(ow-iw)/2:(oh-ih)/2,format=yuv420p",
|
||||
target_width, target_height, target_width, target_height
|
||||
);
|
||||
|
||||
let args = vec![
|
||||
"-ss".to_string(), start_str,
|
||||
"-t".to_string(), duration_str,
|
||||
"-i".to_string(), safe_input,
|
||||
"-vf".to_string(), "fps=25,scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,format=yuv420p".to_string(),
|
||||
"-vf".to_string(), vf,
|
||||
"-c:v".to_string(), "libx264".to_string(),
|
||||
"-preset".to_string(), "veryfast".to_string(),
|
||||
"-crf".to_string(), "23".to_string(),
|
||||
@@ -820,6 +862,33 @@ pub async fn extract_audio_segment(
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 从视频中提取音频(完整时长)
|
||||
*
|
||||
* 将视频文件的音轨提取为 MP3 格式,输出到指定路径。
|
||||
* 适用于 MP4 → MP3 转换,供声音复刻使用。
|
||||
*/
|
||||
pub async fn extract_audio_from_video(
|
||||
app: &AppHandle,
|
||||
input_path: &str,
|
||||
output_path: &str,
|
||||
) -> Result<(), String> {
|
||||
let safe_input = validate_safe_path(input_path)?;
|
||||
let safe_output = sanitize_output_path(output_path)?;
|
||||
|
||||
let args = vec![
|
||||
"-i".to_string(), safe_input,
|
||||
"-vn".to_string(), // 去掉视频
|
||||
"-c:a".to_string(), "libmp3lame".to_string(),
|
||||
"-b:a".to_string(), "192k".to_string(),
|
||||
"-ar".to_string(), "44100".to_string(),
|
||||
"-ac".to_string(), "2".to_string(),
|
||||
"-y".to_string(),
|
||||
safe_output,
|
||||
];
|
||||
run_ffmpeg(app, args).await.map(|_| ())
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 ffprobe 返回的帧率字符串(如 "30000/1001" 或 "30/1")
|
||||
*/
|
||||
|
||||
@@ -292,6 +292,135 @@ fn get_bgm_cache_dir() -> Result<String, String> {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 数据迁移:旧全局数据 → 用户隔离目录
|
||||
// ============================================================
|
||||
|
||||
pub fn migrate_legacy_data(
|
||||
data_dir: &std::path::Path,
|
||||
app: &tauri::AppHandle,
|
||||
) -> Result<(), crate::storage::StorageError> {
|
||||
|
||||
// 1. 检查迁移标记
|
||||
let flag = data_dir.join(".migration_v1_done");
|
||||
if flag.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 2. 检查是否有旧数据
|
||||
let legacy_items = vec![
|
||||
"voices.json",
|
||||
"cover_avatars.json",
|
||||
"cover_avatars",
|
||||
"projects",
|
||||
];
|
||||
let has_legacy = legacy_items.iter().any(|name| data_dir.join(name).exists());
|
||||
if !has_legacy {
|
||||
std::fs::write(&flag, "none")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 3. 获取当前登录用户
|
||||
let raw_user_id = crate::storage::auth::get_current_user_id(app)?;
|
||||
let target_dir = match raw_user_id {
|
||||
Some(id) => {
|
||||
let safe_id = crate::storage::sanitize_id(&id)?;
|
||||
data_dir.join("users").join(&safe_id)
|
||||
}
|
||||
None => {
|
||||
// 未登录:暂不迁移,等登录后由 save_auth_state 触发
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
crate::storage::ensure_dir(&target_dir)?;
|
||||
|
||||
// 4. 逐个 move(rename 在同一文件系统内是原子的)
|
||||
// 若目标已存在(如之前部分迁移过),跳过该项,确保不丢数据
|
||||
let mut any_skipped = false;
|
||||
for name in &legacy_items {
|
||||
let src = data_dir.join(name);
|
||||
let dest = target_dir.join(name);
|
||||
if src.exists() {
|
||||
if dest.exists() {
|
||||
eprintln!("[migrate] 目标已存在,跳过: {}", dest.display());
|
||||
any_skipped = true;
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = std::fs::rename(&src, &dest) {
|
||||
eprintln!("[migrate] 移动 {} 失败: {}", name, e);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 修复项目 JSON 中的绝对路径引用(meta.json / segments.json)
|
||||
// 旧数据中的 coverPath、videoPath 等存的是绝对路径,move 后前缀变了
|
||||
let old_prefix = data_dir.to_string_lossy().to_string();
|
||||
let new_prefix = target_dir.to_string_lossy().to_string();
|
||||
let projects_dir = target_dir.join("projects");
|
||||
if projects_dir.exists() {
|
||||
for entry in std::fs::read_dir(&projects_dir)? {
|
||||
let entry = entry?;
|
||||
let project_dir = entry.path();
|
||||
if !project_dir.is_dir() { continue; }
|
||||
for filename in &["meta.json", "segments.json"] {
|
||||
let path = project_dir.join(filename);
|
||||
if path.exists() {
|
||||
if let Err(e) = fix_legacy_paths_in_file(&path, &old_prefix, &new_prefix) {
|
||||
eprintln!("[migrate] 修复路径失败 {:?}: {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 写迁移标记
|
||||
let flag_content = if any_skipped {
|
||||
format!("skipped:{}", target_dir.file_name().unwrap_or_default().to_string_lossy())
|
||||
} else {
|
||||
target_dir.file_name().unwrap_or_default().to_string_lossy().to_string()
|
||||
};
|
||||
std::fs::write(&flag, flag_content)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 递归替换 JSON 字符串值中的旧路径前缀为新路径前缀
|
||||
fn fix_legacy_paths_in_json(value: &mut serde_json::Value, old_prefix: &str, new_prefix: &str) {
|
||||
match value {
|
||||
serde_json::Value::String(s) => {
|
||||
if s.starts_with(old_prefix) {
|
||||
*s = new_prefix.to_string() + &s[old_prefix.len()..];
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(arr) => {
|
||||
for item in arr {
|
||||
fix_legacy_paths_in_json(item, old_prefix, new_prefix);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(obj) => {
|
||||
for (_, v) in obj {
|
||||
fix_legacy_paths_in_json(v, old_prefix, new_prefix);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取 JSON 文件,替换其中的旧路径前缀,写回文件
|
||||
fn fix_legacy_paths_in_file(
|
||||
path: &std::path::Path,
|
||||
old_prefix: &str,
|
||||
new_prefix: &str,
|
||||
) -> Result<(), crate::storage::StorageError> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let mut value: serde_json::Value = serde_json::from_str(&content)?;
|
||||
fix_legacy_paths_in_json(&mut value, old_prefix, new_prefix);
|
||||
std::fs::write(path, serde_json::to_string_pretty(&value)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 应用入口
|
||||
// ============================================================
|
||||
@@ -316,6 +445,12 @@ pub fn run() {
|
||||
// 初始化应用数据目录(所有业务数据的根目录)
|
||||
if let Ok(app_data_dir) = app.path().app_local_data_dir() {
|
||||
crate::storage::init_app_data_dir(app_data_dir.clone());
|
||||
|
||||
// 数据迁移:将旧的全局数据按当前登录用户隔离到 users/{user_id}/ 目录
|
||||
if let Err(e) = migrate_legacy_data(&app_data_dir, &app.handle()) {
|
||||
eprintln!("[setup] 数据迁移失败: {}", e);
|
||||
}
|
||||
|
||||
// 后台清理过期视频缓存和 BGM 缓存,不阻塞首屏
|
||||
let app_data_dir_clone = app_data_dir.clone();
|
||||
std::thread::spawn(move || {
|
||||
@@ -437,6 +572,7 @@ pub fn run() {
|
||||
// 音频管理
|
||||
commands::voice::save_audio,
|
||||
commands::voice::extract_audio_segment,
|
||||
commands::voice::extract_audio_from_video,
|
||||
commands::voice::upload_audio_file,
|
||||
// 音色素材库
|
||||
commands::voice::load_voice_materials,
|
||||
@@ -537,13 +673,67 @@ async fn video_composite_synthesis(
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 探测所有视频素材分辨率,校验 9:16 并决定目标输出分辨率
|
||||
let mut heights = Vec::new();
|
||||
for path in &request.video_paths {
|
||||
match ffmpeg_cmd::get_video_metadata(&app, path).await {
|
||||
Ok(meta) => {
|
||||
if meta.width == 0 || meta.height == 0 {
|
||||
return ApiResponse {
|
||||
code: 400,
|
||||
message: format!("无法读取视频分辨率: {}", path),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
let ratio = meta.width as f64 / meta.height as f64;
|
||||
let expected = 9.0 / 16.0;
|
||||
if (ratio - expected).abs() / expected > 0.03 {
|
||||
return ApiResponse {
|
||||
code: 400,
|
||||
message: format!(
|
||||
"视频比例必须是 9:16,当前为 {}×{}(比例 {:.3}): {}",
|
||||
meta.width, meta.height, ratio, path
|
||||
),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
heights.push(meta.height);
|
||||
}
|
||||
Err(e) => {
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("读取视频元数据失败 {}: {}", path, e),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if heights.is_empty() {
|
||||
return ApiResponse {
|
||||
code: 400,
|
||||
message: "没有可处理的视频片段".to_string(),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
|
||||
let min_height = *heights.iter().min().unwrap();
|
||||
let (target_width, target_height) = if min_height <= 1280 {
|
||||
(720, 1280)
|
||||
} else {
|
||||
(1080, 1920)
|
||||
};
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"videoPaths": request.video_paths,
|
||||
"audioPath": request.audio_path.unwrap_or_default(),
|
||||
"coverPath": request.cover_path,
|
||||
"outputPath": request.output_path,
|
||||
});
|
||||
let response = video_processing::handle_video_synthesis(&app, &project_dir, payload).await;
|
||||
let response = video_processing::handle_video_synthesis(
|
||||
&app, &project_dir, payload, target_width, target_height
|
||||
).await;
|
||||
if response.code == 200 {
|
||||
if let Some(data) = response.data {
|
||||
if let Some(path) = data.as_object()
|
||||
|
||||
@@ -43,3 +43,16 @@ pub fn clear_auth_state(app: &AppHandle) -> Result<(), StorageError> {
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前登录用户的 user_id
|
||||
///
|
||||
/// 从 auth.json 中读取已认证的 user.id,Rust 层自治,不依赖前端传参。
|
||||
pub fn get_current_user_id(app: &AppHandle) -> Result<Option<String>, StorageError> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct UserInfo { id: String }
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AuthState { user: Option<UserInfo> }
|
||||
|
||||
let auth: Option<AuthState> = load_auth_state(app)?;
|
||||
Ok(auth.and_then(|a| a.user).map(|u| u.id))
|
||||
}
|
||||
|
||||
@@ -29,20 +29,20 @@ pub struct CoverAvatarsList {
|
||||
}
|
||||
|
||||
/// 加载封面形象库
|
||||
pub fn load_cover_avatars() -> Result<CoverAvatarsList, StorageError> {
|
||||
let path = crate::storage::paths::get_cover_avatars_json_path()?;
|
||||
pub fn load_cover_avatars(user_id: &str) -> Result<CoverAvatarsList, StorageError> {
|
||||
let path = crate::storage::paths::get_cover_avatars_json_path(user_id)?;
|
||||
Ok(read_json(&path)?.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// 保存封面形象库
|
||||
pub fn save_cover_avatars(list: &CoverAvatarsList) -> Result<(), StorageError> {
|
||||
let path = crate::storage::paths::get_cover_avatars_json_path()?;
|
||||
pub fn save_cover_avatars(user_id: &str, list: &CoverAvatarsList) -> Result<(), StorageError> {
|
||||
let path = crate::storage::paths::get_cover_avatars_json_path(user_id)?;
|
||||
atomic_write_json(&path, list)
|
||||
}
|
||||
|
||||
/// 添加封面形象
|
||||
pub fn add_cover_avatar(avatar: CoverAvatar) -> Result<(), StorageError> {
|
||||
let mut list = load_cover_avatars()?;
|
||||
pub fn add_cover_avatar(user_id: &str, avatar: CoverAvatar) -> Result<(), StorageError> {
|
||||
let mut list = load_cover_avatars(user_id)?;
|
||||
// 去重:相同 id 替换
|
||||
if let Some(pos) = list.avatars.iter().position(|a| a.id == avatar.id) {
|
||||
list.avatars[pos] = avatar;
|
||||
@@ -50,12 +50,12 @@ pub fn add_cover_avatar(avatar: CoverAvatar) -> Result<(), StorageError> {
|
||||
list.avatars.push(avatar);
|
||||
}
|
||||
list.updated_at = chrono_lite_now();
|
||||
save_cover_avatars(&list)
|
||||
save_cover_avatars(user_id, &list)
|
||||
}
|
||||
|
||||
/// 删除封面形象
|
||||
pub fn delete_cover_avatar(id: &str) -> Result<(), StorageError> {
|
||||
let mut list = load_cover_avatars()?;
|
||||
pub fn delete_cover_avatar(user_id: &str, id: &str) -> Result<(), StorageError> {
|
||||
let mut list = load_cover_avatars(user_id)?;
|
||||
let pos = list.avatars.iter().position(|a| a.id == id)
|
||||
.ok_or_else(|| StorageError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
@@ -63,18 +63,19 @@ pub fn delete_cover_avatar(id: &str) -> Result<(), StorageError> {
|
||||
)))?;
|
||||
list.avatars.remove(pos);
|
||||
list.updated_at = chrono_lite_now();
|
||||
save_cover_avatars(&list)
|
||||
save_cover_avatars(user_id, &list)
|
||||
}
|
||||
|
||||
/// 保存封面形象图片文件到本地
|
||||
///
|
||||
/// 将 base64 编码的图片数据写入 `cover_avatars/` 子目录。
|
||||
pub fn save_cover_avatar_image(
|
||||
user_id: &str,
|
||||
avatar_id: &str,
|
||||
data: &[u8],
|
||||
ext: &str,
|
||||
) -> Result<String, StorageError> {
|
||||
let avatars_dir = get_cover_avatars_dir()?;
|
||||
let avatars_dir = get_cover_avatars_dir(user_id)?;
|
||||
ensure_dir(&avatars_dir)?;
|
||||
|
||||
// 净化扩展名:只允许字母数字,防止路径遍历
|
||||
|
||||
@@ -16,12 +16,14 @@ pub mod cover_avatar;
|
||||
|
||||
pub use engine::{
|
||||
atomic_write_json, atomic_write_bytes,
|
||||
with_file_lock, read_json, sanitize_filename, StorageError,
|
||||
with_file_lock, read_json, sanitize_filename, sanitize_id, ensure_dir, StorageError,
|
||||
};
|
||||
|
||||
pub use paths::{
|
||||
init_app_data_dir, get_app_data_dir, get_projects_root_dir,
|
||||
get_project_dir, get_project_dir_path, get_project_assets_dir,
|
||||
get_project_videos_dir, get_project_products_dir, get_voices_json_path,
|
||||
get_app_config_json_path, get_cover_avatars_dir, get_cover_avatars_json_path,
|
||||
init_app_data_dir, get_app_data_dir, get_user_data_dir,
|
||||
get_projects_root_dir, get_project_dir, get_project_dir_path,
|
||||
get_project_assets_dir, get_project_videos_dir, get_project_products_dir,
|
||||
get_voices_json_path, get_app_config_json_path,
|
||||
get_cover_avatars_dir, get_cover_avatars_json_path,
|
||||
get_bgm_cache_dir, get_temp_dir,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
//! 所有返回 PathBuf 的函数内部已做路径净化。
|
||||
//!
|
||||
//! 存储根目录为 app_local_data_dir,在应用启动时通过 init_app_data_dir 初始化。
|
||||
//!
|
||||
//! 【用户数据隔离说明】
|
||||
//! 用户相关数据(项目、音色素材、封面头像)按 user_id 隔离:
|
||||
//! {app_local_data_dir}/users/{user_id}/
|
||||
//! 全局共享数据(BGM 缓存、设备级配置)保持在根目录。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
@@ -26,68 +31,114 @@ pub fn get_app_data_dir() -> Result<&'static PathBuf, StorageError> {
|
||||
)))
|
||||
}
|
||||
|
||||
/// 获取项目根目录(所有项目的父目录)
|
||||
/// {app_local_data_dir}/projects/
|
||||
pub fn get_projects_root_dir() -> Result<PathBuf, StorageError> {
|
||||
// ============================================================
|
||||
// 用户隔离根目录
|
||||
// ============================================================
|
||||
|
||||
/// 获取用户数据根目录
|
||||
/// {app_local_data_dir}/users/{user_id}/
|
||||
pub fn get_user_data_dir(user_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let safe_id = sanitize_id(user_id)?;
|
||||
let base = get_app_data_dir()?;
|
||||
let path = base.join("users").join(&safe_id);
|
||||
crate::storage::engine::ensure_dir(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 项目相关路径(用户隔离)
|
||||
// ============================================================
|
||||
|
||||
/// 获取项目根目录(所有项目的父目录)
|
||||
/// {app_local_data_dir}/users/{user_id}/projects/
|
||||
pub fn get_projects_root_dir(user_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let base = get_user_data_dir(user_id)?;
|
||||
let path = base.join("projects");
|
||||
crate::storage::engine::ensure_dir(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 获取项目目录路径(不自动创建)
|
||||
/// {app_local_data_dir}/projects/{project_id}/
|
||||
pub fn get_project_dir_path(project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
/// {app_local_data_dir}/users/{user_id}/projects/{project_id}/
|
||||
pub fn get_project_dir_path(user_id: &str, project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let safe_id = sanitize_id(project_id)?;
|
||||
let base = get_app_data_dir()?;
|
||||
Ok(base.join("projects").join(&safe_id))
|
||||
let base = get_projects_root_dir(user_id)?;
|
||||
Ok(base.join(&safe_id))
|
||||
}
|
||||
|
||||
/// 获取项目目录(自动创建)
|
||||
/// {app_local_data_dir}/projects/{project_id}/
|
||||
pub fn get_project_dir(project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let path = get_project_dir_path(project_id)?;
|
||||
/// {app_local_data_dir}/users/{user_id}/projects/{project_id}/
|
||||
pub fn get_project_dir(user_id: &str, project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let path = get_project_dir_path(user_id, project_id)?;
|
||||
crate::storage::engine::ensure_dir(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 获取项目内的 assets 目录
|
||||
/// {app_local_data_dir}/projects/{project_id}/assets/
|
||||
pub fn get_project_assets_dir(project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let path = get_project_dir(project_id)?;
|
||||
/// {app_local_data_dir}/users/{user_id}/projects/{project_id}/assets/
|
||||
pub fn get_project_assets_dir(user_id: &str, project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let path = get_project_dir(user_id, project_id)?;
|
||||
let assets = path.join("assets");
|
||||
crate::storage::engine::ensure_dir(&assets)?;
|
||||
Ok(assets)
|
||||
}
|
||||
|
||||
/// 获取项目内的 videos 目录
|
||||
/// {app_local_data_dir}/projects/{project_id}/videos/
|
||||
pub fn get_project_videos_dir(project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let path = get_project_dir(project_id)?;
|
||||
/// {app_local_data_dir}/users/{user_id}/projects/{project_id}/videos/
|
||||
pub fn get_project_videos_dir(user_id: &str, project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let path = get_project_dir(user_id, project_id)?;
|
||||
let videos = path.join("videos");
|
||||
crate::storage::engine::ensure_dir(&videos)?;
|
||||
Ok(videos)
|
||||
}
|
||||
|
||||
/// 获取项目成品目录
|
||||
/// {app_local_data_dir}/projects/{project_id}/products/
|
||||
pub fn get_project_products_dir(project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let safe_id = sanitize_id(project_id)?;
|
||||
let base = get_app_data_dir()?;
|
||||
let path = base.join("projects").join(&safe_id).join("products");
|
||||
/// {app_local_data_dir}/users/{user_id}/projects/{project_id}/products/
|
||||
pub fn get_project_products_dir(user_id: &str, project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let base = get_project_dir(user_id, project_id)?;
|
||||
let path = base.join("products");
|
||||
crate::storage::engine::ensure_dir(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 音色素材库路径(用户隔离)
|
||||
// ============================================================
|
||||
|
||||
/// 获取私有音色素材库 JSON 路径
|
||||
/// {app_local_data_dir}/voices.json
|
||||
pub fn get_voices_json_path() -> Result<PathBuf, StorageError> {
|
||||
let base = get_app_data_dir()?;
|
||||
/// {app_local_data_dir}/users/{user_id}/voices.json
|
||||
pub fn get_voices_json_path(user_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let base = get_user_data_dir(user_id)?;
|
||||
Ok(base.join("voices.json"))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 封面头像路径(用户隔离)
|
||||
// ============================================================
|
||||
|
||||
/// 获取封面形象图片存储目录
|
||||
/// {app_local_data_dir}/users/{user_id}/cover_avatars/
|
||||
pub fn get_cover_avatars_dir(user_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let base = get_user_data_dir(user_id)?;
|
||||
let path = base.join("cover_avatars");
|
||||
crate::storage::engine::ensure_dir(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 获取封面形象库 JSON 路径
|
||||
/// {app_local_data_dir}/users/{user_id}/cover_avatars.json
|
||||
pub fn get_cover_avatars_json_path(user_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let base = get_user_data_dir(user_id)?;
|
||||
Ok(base.join("cover_avatars.json"))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 全局共享路径(不隔离)
|
||||
// ============================================================
|
||||
|
||||
/// 获取应用配置路径
|
||||
/// {app_local_data_dir}/config.json
|
||||
/// 注意:config.json 存的是设备级配置(API 地址、运行环境),保持全局共享。
|
||||
pub fn get_app_config_json_path() -> Result<PathBuf, StorageError> {
|
||||
let base = get_app_data_dir()?;
|
||||
Ok(base.join("config.json"))
|
||||
@@ -95,6 +146,7 @@ pub fn get_app_config_json_path() -> Result<PathBuf, StorageError> {
|
||||
|
||||
/// 获取认证状态文件路径
|
||||
/// {app_config_dir}/auth.json
|
||||
/// 注意:auth.json 只存当前登录态,全局唯一,切换账号时覆盖。
|
||||
pub fn get_auth_state_path(app: &AppHandle) -> Result<PathBuf, StorageError> {
|
||||
let path = app.path().app_config_dir()
|
||||
.map_err(|e| StorageError::Io(std::io::Error::new(
|
||||
@@ -105,27 +157,22 @@ pub fn get_auth_state_path(app: &AppHandle) -> Result<PathBuf, StorageError> {
|
||||
Ok(path.join("auth.json"))
|
||||
}
|
||||
|
||||
/// 获取封面形象图片存储目录
|
||||
/// {app_local_data_dir}/cover_avatars/
|
||||
pub fn get_cover_avatars_dir() -> Result<PathBuf, StorageError> {
|
||||
let base = get_app_data_dir()?;
|
||||
let path = base.join("cover_avatars");
|
||||
crate::storage::engine::ensure_dir(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 获取封面形象库 JSON 路径
|
||||
/// {app_local_data_dir}/cover_avatars.json
|
||||
pub fn get_cover_avatars_json_path() -> Result<PathBuf, StorageError> {
|
||||
let base = get_app_data_dir()?;
|
||||
Ok(base.join("cover_avatars.json"))
|
||||
}
|
||||
|
||||
/// 获取 BGM 缓存目录
|
||||
/// {app_local_data_dir}/bgm_cache/
|
||||
/// 注意:BGM 缓存是公共资源,无需按用户隔离。
|
||||
pub fn get_bgm_cache_dir() -> Result<PathBuf, StorageError> {
|
||||
let base = get_app_data_dir()?;
|
||||
let path = base.join("bgm_cache");
|
||||
crate::storage::engine::ensure_dir(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 获取临时目录
|
||||
/// {app_local_data_dir}/temp/
|
||||
#[allow(dead_code)]
|
||||
pub fn get_temp_dir() -> Result<PathBuf, StorageError> {
|
||||
let base = get_app_data_dir()?;
|
||||
let path = base.join("temp");
|
||||
crate::storage::engine::ensure_dir(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
@@ -21,10 +21,11 @@ use crate::storage::paths::{
|
||||
|
||||
/// 保存项目元数据
|
||||
pub fn save_project_meta(
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
data: &Value,
|
||||
) -> Result<(), StorageError> {
|
||||
let dir = get_project_dir(project_id)?;
|
||||
let dir = get_project_dir(user_id, project_id)?;
|
||||
let path = dir.join("meta.json");
|
||||
with_file_lock(&path, || {
|
||||
atomic_write_json(&path, data)
|
||||
@@ -33,18 +34,20 @@ pub fn save_project_meta(
|
||||
|
||||
/// 保存项目元数据(原始 JSON 字符串)
|
||||
pub fn save_project_meta_raw(
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
json_content: &str,
|
||||
) -> Result<(), StorageError> {
|
||||
let data: Value = serde_json::from_str(json_content)?;
|
||||
save_project_meta(project_id, &data)
|
||||
save_project_meta(user_id, project_id, &data)
|
||||
}
|
||||
|
||||
/// 加载项目元数据
|
||||
pub fn load_project_meta(
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
) -> Result<Value, StorageError> {
|
||||
let dir = get_project_dir_path(project_id)?;
|
||||
let dir = get_project_dir_path(user_id, project_id)?;
|
||||
let path = dir.join("meta.json");
|
||||
match read_json(&path)? {
|
||||
Some(data) => Ok(data),
|
||||
@@ -58,10 +61,11 @@ pub fn load_project_meta(
|
||||
|
||||
/// 保存分镜数据
|
||||
pub fn save_project_segments(
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
segments: &Value,
|
||||
) -> Result<(), StorageError> {
|
||||
let dir = get_project_dir(project_id)?;
|
||||
let dir = get_project_dir(user_id, project_id)?;
|
||||
let path = dir.join("segments.json");
|
||||
with_file_lock(&path, || {
|
||||
atomic_write_json(&path, segments)
|
||||
@@ -70,18 +74,20 @@ pub fn save_project_segments(
|
||||
|
||||
/// 保存分镜数据(原始 JSON 字符串)
|
||||
pub fn save_project_segments_raw(
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
json_content: &str,
|
||||
) -> Result<(), StorageError> {
|
||||
let data: Value = serde_json::from_str(json_content)?;
|
||||
save_project_segments(project_id, &data)
|
||||
save_project_segments(user_id, project_id, &data)
|
||||
}
|
||||
|
||||
/// 加载分镜数据
|
||||
pub fn load_project_segments(
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
) -> Result<Value, StorageError> {
|
||||
let dir = get_project_dir_path(project_id)?;
|
||||
let dir = get_project_dir_path(user_id, project_id)?;
|
||||
let path = dir.join("segments.json");
|
||||
match read_json(&path)? {
|
||||
Some(data) => Ok(data),
|
||||
@@ -97,9 +103,8 @@ pub fn load_project_segments(
|
||||
///
|
||||
/// 遍历 projects 目录,读取每个项目的 meta.json。
|
||||
/// 读取失败的项目会被跳过并记录警告,不会导致整个列表失败。
|
||||
pub fn list_projects() -> Result<Vec<Value>, StorageError> {
|
||||
let base = get_meijiaka_dir()?;
|
||||
let projects_dir = base.join("projects");
|
||||
pub fn list_projects(user_id: &str) -> Result<Vec<Value>, StorageError> {
|
||||
let projects_dir = crate::storage::paths::get_projects_root_dir(user_id)?;
|
||||
|
||||
if !projects_dir.exists() {
|
||||
return Ok(vec![]);
|
||||
@@ -143,8 +148,8 @@ pub fn list_projects() -> Result<Vec<Value>, StorageError> {
|
||||
///
|
||||
/// 使用 `get_project_dir_path`(不自动创建目录),
|
||||
/// 避免删除不存在项目时先创建空目录的 bug。
|
||||
pub fn delete_project(project_id: &str) -> Result<(), StorageError> {
|
||||
let dir = get_project_dir_path(project_id)?;
|
||||
pub fn delete_project(user_id: &str, project_id: &str) -> Result<(), StorageError> {
|
||||
let dir = get_project_dir_path(user_id, project_id)?;
|
||||
if dir.exists() {
|
||||
fs::remove_dir_all(&dir)?;
|
||||
}
|
||||
@@ -157,12 +162,13 @@ pub fn delete_project(project_id: &str) -> Result<(), StorageError> {
|
||||
|
||||
/// 保存项目资源(Base64 解码后写入)
|
||||
pub fn save_project_asset(
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
filename: &str,
|
||||
base64_data: &str,
|
||||
) -> Result<String, StorageError> {
|
||||
let safe_filename = sanitize_filename(filename)?;
|
||||
let dir = get_project_assets_dir(project_id)?;
|
||||
let dir = get_project_assets_dir(user_id, project_id)?;
|
||||
let path = dir.join(&safe_filename);
|
||||
|
||||
let decoded = base64::engine::general_purpose::STANDARD
|
||||
@@ -177,16 +183,11 @@ pub fn save_project_asset(
|
||||
/// 获取视频保存路径(不写入文件)
|
||||
/// 保存到项目 videos 目录下,与后端下载路径保持一致
|
||||
pub fn get_video_save_path(
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
filename: &str,
|
||||
) -> Result<PathBuf, StorageError> {
|
||||
let safe_filename = sanitize_filename(filename)?;
|
||||
let dir = get_project_videos_dir(project_id)?;
|
||||
let dir = get_project_videos_dir(user_id, project_id)?;
|
||||
Ok(dir.join(&safe_filename))
|
||||
}
|
||||
|
||||
// 兼容旧接口:从 persistence.rs 迁移时需要的 get_meijiaka_dir
|
||||
fn get_meijiaka_dir() -> Result<std::path::PathBuf, StorageError> {
|
||||
crate::storage::paths::get_app_data_dir()
|
||||
.map(|p| p.to_path_buf())
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct AudioMeta {
|
||||
///
|
||||
/// 将 base64 编码的音频数据写入 `audios/` 子目录。
|
||||
pub fn save_audio_file(
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
audio_id: &str,
|
||||
data: &[u8],
|
||||
@@ -33,7 +34,7 @@ pub fn save_audio_file(
|
||||
voice_id: &str,
|
||||
duration: f64,
|
||||
) -> Result<AudioMeta, StorageError> {
|
||||
let project_dir = get_project_dir(project_id)?;
|
||||
let project_dir = get_project_dir(user_id, project_id)?;
|
||||
let audios_dir = project_dir.join("audios");
|
||||
ensure_dir(&audios_dir)?;
|
||||
|
||||
@@ -87,20 +88,20 @@ pub struct VoiceMaterialsList {
|
||||
}
|
||||
|
||||
/// 加载音色素材库
|
||||
pub fn load_voice_materials() -> Result<VoiceMaterialsList, StorageError> {
|
||||
let path = crate::storage::paths::get_voices_json_path()?;
|
||||
pub fn load_voice_materials(user_id: &str) -> Result<VoiceMaterialsList, StorageError> {
|
||||
let path = crate::storage::paths::get_voices_json_path(user_id)?;
|
||||
Ok(read_json(&path)?.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// 保存音色素材库
|
||||
pub fn save_voice_materials(list: &VoiceMaterialsList) -> Result<(), StorageError> {
|
||||
let path = crate::storage::paths::get_voices_json_path()?;
|
||||
pub fn save_voice_materials(user_id: &str, list: &VoiceMaterialsList) -> Result<(), StorageError> {
|
||||
let path = crate::storage::paths::get_voices_json_path(user_id)?;
|
||||
crate::storage::engine::atomic_write_json(&path, list)
|
||||
}
|
||||
|
||||
/// 添加音色素材
|
||||
pub fn add_voice_material(material: VoiceMaterial) -> Result<(), StorageError> {
|
||||
let mut list = load_voice_materials()?;
|
||||
pub fn add_voice_material(user_id: &str, material: VoiceMaterial) -> Result<(), StorageError> {
|
||||
let mut list = load_voice_materials(user_id)?;
|
||||
// 去重:相同 id 替换
|
||||
if let Some(pos) = list.materials.iter().position(|m| m.id == material.id) {
|
||||
list.materials[pos] = material;
|
||||
@@ -108,12 +109,12 @@ pub fn add_voice_material(material: VoiceMaterial) -> Result<(), StorageError> {
|
||||
list.materials.push(material);
|
||||
}
|
||||
list.updated_at = chrono_lite_now();
|
||||
save_voice_materials(&list)
|
||||
save_voice_materials(user_id, &list)
|
||||
}
|
||||
|
||||
/// 删除音色素材
|
||||
pub fn delete_voice_material(id: &str) -> Result<(), StorageError> {
|
||||
let mut list = load_voice_materials()?;
|
||||
pub fn delete_voice_material(user_id: &str, id: &str) -> Result<(), StorageError> {
|
||||
let mut list = load_voice_materials(user_id)?;
|
||||
let pos = list.materials.iter().position(|m| m.id == id)
|
||||
.ok_or_else(|| StorageError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
@@ -121,5 +122,5 @@ pub fn delete_voice_material(id: &str) -> Result<(), StorageError> {
|
||||
)))?;
|
||||
list.materials.remove(pos);
|
||||
list.updated_at = chrono_lite_now();
|
||||
save_voice_materials(&list)
|
||||
save_voice_materials(user_id, &list)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ pub async fn handle_video_synthesis(
|
||||
app: &tauri::AppHandle,
|
||||
project_dir: &std::path::PathBuf,
|
||||
payload: serde_json::Value,
|
||||
target_width: u32,
|
||||
target_height: u32,
|
||||
) -> ApiResponse<serde_json::Value> {
|
||||
let payload_obj = match payload.as_object() {
|
||||
Some(p) => p,
|
||||
@@ -59,7 +61,7 @@ pub async fn handle_video_synthesis(
|
||||
cover_v_path.push(format!("temp_cover_{}.mp4", utils::uuid_v4()));
|
||||
let cover_v_path_str = cover_v_path.to_string_lossy().to_string();
|
||||
|
||||
match crate::ffmpeg_cmd::create_cover_video(app, c_path, &cover_v_path_str, "0.5").await {
|
||||
match crate::ffmpeg_cmd::create_cover_video(app, c_path, &cover_v_path_str, "0.5", target_width, target_height).await {
|
||||
Ok(_) => {
|
||||
paths_vec.insert(0, cover_v_path_str);
|
||||
temp_cover_video = Some(cover_v_path);
|
||||
@@ -75,7 +77,7 @@ pub async fn handle_video_synthesis(
|
||||
}
|
||||
|
||||
// 执行视频拼接
|
||||
match crate::ffmpeg_cmd::concat_videos_robust(app, paths_vec, concat_output.to_string_lossy().as_ref()).await {
|
||||
match crate::ffmpeg_cmd::concat_videos_robust(app, paths_vec, concat_output.to_string_lossy().as_ref(), target_width, target_height).await {
|
||||
Ok(_) => {
|
||||
// 如果有音频,合并音频;否则直接使用拼接结果
|
||||
let has_audio = audio_path.map(|p| !p.is_empty()).unwrap_or(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "美家卡智影",
|
||||
"version": "1.6.5",
|
||||
"version": "1.8.1",
|
||||
"identifier": "cn.meijiaka.ai-zy",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -94,7 +94,7 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
initProjectStore().catch(console.error);
|
||||
usePointStore.getState().loadRules();
|
||||
usePointStore.getState().fetchBalance().catch(console.error);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ export const localProjectApi = {
|
||||
title: meta.title,
|
||||
topic: meta.topic,
|
||||
categoryCode: meta.categoryCode,
|
||||
subcategoryCode: meta.subcategoryCode,
|
||||
filename: meta.filename,
|
||||
status: meta.status,
|
||||
currentStep: meta.currentStep,
|
||||
createdAt: meta.createdAt,
|
||||
@@ -115,7 +115,6 @@ export const localProjectApi = {
|
||||
dubbingAudioDuration: meta.dubbingAudioDuration,
|
||||
voiceSpeed: meta.voiceSpeed,
|
||||
voiceVolume: meta.voiceVolume,
|
||||
voicePitch: meta.voicePitch,
|
||||
avatarMaterialPath: meta.avatarMaterialPath,
|
||||
avatarMaterialName: meta.avatarMaterialName,
|
||||
avatarMaterialDuration: meta.avatarMaterialDuration,
|
||||
|
||||
@@ -4,12 +4,12 @@ import type { ScriptShot } from '../types';
|
||||
export type { ScriptShot };
|
||||
|
||||
/**
|
||||
* 小类项
|
||||
* 提示词文件项
|
||||
*/
|
||||
interface SubcategoryItem {
|
||||
code: string;
|
||||
name: string;
|
||||
count: number;
|
||||
interface PromptFileItem {
|
||||
filename: string;
|
||||
label: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,7 +18,7 @@ interface SubcategoryItem {
|
||||
export interface CategoryItem {
|
||||
code: string;
|
||||
name: string;
|
||||
subcategories: SubcategoryItem[];
|
||||
files: PromptFileItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@ interface GenerateTitleResponse {
|
||||
/**
|
||||
* 脚本相关 API
|
||||
*/
|
||||
const CATEGORIES_CACHE_KEY = 'script-categories-v1';
|
||||
const CATEGORIES_CACHE_KEY = 'script-categories-v2';
|
||||
|
||||
export const scriptApi = {
|
||||
/**
|
||||
|
||||
@@ -72,6 +72,7 @@ export async function generateEmptyShotClip(args: {
|
||||
outputPath: string;
|
||||
}): Promise<string> {
|
||||
const res = await invoke<ApiResponse<string>>('generate_empty_shot_clip', { args });
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (res.code !== 200 || res.data == null) {
|
||||
throw new Error(res.message || '生成空镜片段失败');
|
||||
}
|
||||
@@ -93,6 +94,7 @@ export async function concatVideoClips(
|
||||
projectId,
|
||||
clipPaths,
|
||||
});
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (res.code !== 200 || res.data == null) {
|
||||
throw new Error(res.message || '视频拼接失败');
|
||||
}
|
||||
@@ -108,6 +110,7 @@ export async function concatVideoClips(
|
||||
*/
|
||||
export async function downloadFile(url: string, outputPath: string): Promise<string> {
|
||||
const res = await invoke<ApiResponse<string>>('download_file', { url, outputPath });
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (res.code !== 200 || res.data == null) {
|
||||
throw new Error(res.message || '下载文件失败');
|
||||
}
|
||||
|
||||
@@ -196,3 +196,31 @@ export async function extractAudioSegment(args: ExtractAudioSegmentRequest): Pro
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/** 从本地视频文件中提取音频(MP4 → MP3)
|
||||
* @param inputPath 本地视频文件路径
|
||||
* @returns 提取后的 MP3 文件路径
|
||||
*/
|
||||
export async function extractAudioFromVideo(inputPath: string): Promise<string> {
|
||||
const result = await invoke<{ code: number; data?: string; message: string }>('extract_audio_from_video', {
|
||||
inputPath,
|
||||
});
|
||||
if (result.code !== 200 || !result.data) {
|
||||
throw new Error(result.message || '音频提取失败');
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/** 上传本地音频文件到后端(后端上传到七牛云)
|
||||
* @param localPath 本地音频文件路径
|
||||
* @returns 七牛云 URL
|
||||
*/
|
||||
export async function uploadLocalAudioFile(localPath: string): Promise<string> {
|
||||
const result = await invoke<{ code: number; data?: { url: string }; message: string }>('upload_audio_file', {
|
||||
localPath,
|
||||
});
|
||||
if (result.code !== 200 || !result.data?.url) {
|
||||
throw new Error(result.message || '上传音频失败');
|
||||
}
|
||||
return result.data.url;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function Sidebar({ currentPath, onNavigate }: SidebarProps) {
|
||||
|
||||
// 点击外部关闭用户菜单
|
||||
useEffect(() => {
|
||||
if (!showUserMenu) return;
|
||||
if (!showUserMenu) {return;}
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
setShowUserMenu(false);
|
||||
|
||||
@@ -32,7 +32,8 @@ export default function PricingModal({ open, onClose }: PricingModalProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || rules.length > 0) return;
|
||||
if (!open || rules.length > 0) {return;}
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setLoading(true);
|
||||
pointsApi.getRules()
|
||||
.then(setRules)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 更新对话框组件
|
||||
* ==============
|
||||
*
|
||||
* 应用启动时自动检查更新,发现新版本后弹出此对话框。
|
||||
* 应用启动时自动检查更新(每天最多一次),发现新版本后弹出此对话框。
|
||||
* 支持强制更新(无法跳过)。
|
||||
*/
|
||||
|
||||
@@ -30,11 +30,22 @@ export default function UpdateDialog() {
|
||||
relaunch,
|
||||
} = useUpdater();
|
||||
|
||||
// 应用启动时自动检查更新(延迟 3 秒,避免阻塞首屏)
|
||||
// silent=true:失败时不弹窗,只打 console 日志
|
||||
// 应用启动时自动检查更新(每天最多一次)
|
||||
// 延迟 3 秒避免阻塞首屏;silent=true:失败时不弹窗,只打 console 日志
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
check(true);
|
||||
const LAST_CHECK_KEY = 'mjk_last_update_check';
|
||||
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const lastCheckRaw = localStorage.getItem(LAST_CHECK_KEY);
|
||||
const lastCheck = lastCheckRaw ? parseInt(lastCheckRaw, 10) : 0;
|
||||
const now = Date.now();
|
||||
|
||||
if (!lastCheck || now - lastCheck > ONE_DAY_MS) {
|
||||
check(true).finally(() => {
|
||||
localStorage.setItem(LAST_CHECK_KEY, Date.now().toString());
|
||||
});
|
||||
}
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [check]);
|
||||
@@ -46,7 +57,7 @@ export default function UpdateDialog() {
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (bytes === 0) {return '0 B';}
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
@@ -36,6 +36,8 @@ interface UseCanvasSubtitleRendererOptions {
|
||||
mainTitleStyle?: Partial<AssStyle>;
|
||||
subTitleStyle?: Partial<AssStyle>;
|
||||
enabled: boolean;
|
||||
/** ASS 坐标系基准高度,默认 1920;720p 视频应传入 1280 */
|
||||
playResY?: number;
|
||||
}
|
||||
|
||||
export function useCanvasSubtitleRenderer(options: UseCanvasSubtitleRendererOptions) {
|
||||
@@ -49,6 +51,7 @@ export function useCanvasSubtitleRenderer(options: UseCanvasSubtitleRendererOpti
|
||||
mainTitleStyle,
|
||||
subTitleStyle,
|
||||
enabled,
|
||||
playResY,
|
||||
} = options;
|
||||
|
||||
const rafRef = useRef<number | null>(null);
|
||||
@@ -84,7 +87,8 @@ export function useCanvasSubtitleRenderer(options: UseCanvasSubtitleRendererOpti
|
||||
// 计算缩放比例(基于高度)
|
||||
// 扣除 <video controls> 控制条高度(约 40px),确保预览与视频画面比例一致
|
||||
const VIDEO_CONTROLS_HEIGHT = 40;
|
||||
const scale = Math.max(1, displayHeight - VIDEO_CONTROLS_HEIGHT) / PLAY_RES_Y;
|
||||
const playResYValue = playResY ?? PLAY_RES_Y;
|
||||
const scale = Math.max(1, displayHeight - VIDEO_CONTROLS_HEIGHT) / playResYValue;
|
||||
|
||||
const currentTimeMs = video.currentTime * 1000;
|
||||
|
||||
@@ -139,7 +143,7 @@ export function useCanvasSubtitleRenderer(options: UseCanvasSubtitleRendererOpti
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}, [enabled, utterances, mainTitle, subTitle, subtitleStyle, mainTitleStyle, subTitleStyle, videoRef, canvasRef]);
|
||||
}, [enabled, utterances, mainTitle, subTitle, subtitleStyle, mainTitleStyle, subTitleStyle, videoRef, canvasRef, playResY]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {return;}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user