🔄 卡若AI 同步 2026-03-03 12:01 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个
This commit is contained in:
@@ -15,7 +15,9 @@ from urllib.parse import quote
|
||||
FEISHU_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
TOKEN_FILE = os.path.join(FEISHU_SCRIPT_DIR, '.feishu_tokens.json')
|
||||
WIKI_NODE_OR_SPREADSHEET_TOKEN = os.environ.get('FEISHU_SPREADSHEET_TOKEN', 'wikcnIgAGSNHo0t36idHJ668Gfd')
|
||||
SHEET_ID = os.environ.get('FEISHU_SHEET_ID', '7A3Cy9')
|
||||
SHEET_ID = os.environ.get('FEISHU_SHEET_ID', '7A3Cy9') # 2月默认 sheet
|
||||
# 月份 → 工作表 sheetId(2月=7A3Cy9;3月=bJR5sA,与飞书「3月」标签一致)
|
||||
SHEET_ID_BY_MONTH = {2: '7A3Cy9', 3: 'bJR5sA'}
|
||||
# 飞书群机器人 webhook(推送运营报表链接与场次数据)
|
||||
FEISHU_GROUP_WEBHOOK = os.environ.get('FEISHU_GROUP_WEBHOOK', 'https://open.feishu.cn/open-apis/bot/v2/hook/34b762fc-5b9b-4abb-a05a-96c8fb9599f1')
|
||||
OPERATION_REPORT_LINK = 'https://cunkebao.feishu.cn/wiki/wikcnIgAGSNHo0t36idHJ668Gfd?sheet=7A3Cy9'
|
||||
@@ -39,9 +41,13 @@ ROWS = {
|
||||
'106': [ '退伍军人低空经济 贴息8800', 135, 33312, 395, 7, 88, 3, 24, 9, 42 ],
|
||||
# 107场 2026-02-23:关闭页 137min/398进房/60最高/36关注/2礼物/16灵魂力/33820曝光,小助手 10人均/85互动/34关注
|
||||
'107': [ '职场情绪价值 核心团队管理', 137, 33820, 398, 10, 85, 2, 16, 36, 60 ],
|
||||
# 113场 2026-03-02:关闭页 163min/445成员/54最高/19新增粉丝/1礼物/29灵魂力/42360曝光,小助手 8人均/139互动/16关注
|
||||
'113': [ '钱一月Ai创业私域', 163, 42360, 445, 8, 139, 1, 29, 19, 54 ],
|
||||
}
|
||||
# 场次→按日期列填写时的日期(表头为当月日期 1~31)
|
||||
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23'}
|
||||
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23', '113': '2'}
|
||||
# 场次→月份(用于选择 2月/3月 等工作表标签,避免写入错月)
|
||||
SESSION_MONTH = {'105': 2, '106': 2, '107': 2, '113': 3}
|
||||
|
||||
# 小程序当日运营数据:日期号 → {访问次数, 访客, 交易金额},填表时自动写入对应日期列
|
||||
# 数据来源:微信公众平台 → 小程序 → 统计 → 实时访问/概况
|
||||
@@ -154,6 +160,32 @@ def get_sheet_meta(access_token, spreadsheet_token):
|
||||
return sheets[0].get('sheetId') or sheets[0].get('title') or SHEET_ID
|
||||
|
||||
|
||||
def get_sheet_id_by_month(access_token, spreadsheet_token, month):
|
||||
"""按月份选工作表标签:标题含「X月」的 sheet(如 3月)→ 返回其 sheetId,避免写入错月。"""
|
||||
if month in SHEET_ID_BY_MONTH:
|
||||
return SHEET_ID_BY_MONTH[month]
|
||||
url = f'https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/metainfo'
|
||||
r = requests.get(
|
||||
url,
|
||||
headers={'Authorization': f'Bearer {access_token}'},
|
||||
timeout=15,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return SHEET_ID
|
||||
body = r.json()
|
||||
if body.get('code') != 0:
|
||||
return SHEET_ID
|
||||
sheets = (body.get('data') or {}).get('sheets') or []
|
||||
month_label = f'{month}月'
|
||||
for s in sheets:
|
||||
title = (s.get('title') or '').strip()
|
||||
if month_label in title:
|
||||
sid = s.get('sheetId') or s.get('title')
|
||||
if sid:
|
||||
return sid
|
||||
return SHEET_ID
|
||||
|
||||
|
||||
def read_sheet_range(access_token, spreadsheet_token, range_str):
|
||||
"""读取表格范围,返回 values 或 None"""
|
||||
url = f'https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values/{quote(range_str, safe="")}'
|
||||
@@ -258,7 +290,7 @@ def main():
|
||||
session = (sys.argv[1] if len(sys.argv) > 1 else '104').strip()
|
||||
row = ROWS.get(session)
|
||||
if not row:
|
||||
print('❌ 未知场次,可用: 96, 97, 98, 99, 100, 103, 104, 105, 106')
|
||||
print('❌ 未知场次,可用: 96, 97, 98, 99, 100, 103, 104, 105, 106, 107, 113')
|
||||
sys.exit(1)
|
||||
token = load_token() or refresh_and_load_token()
|
||||
if not token:
|
||||
@@ -267,7 +299,10 @@ def main():
|
||||
raw = (row + [None] * EFFECT_COLS)[:EFFECT_COLS]
|
||||
values = [_to_cell_value(raw[0])] + [_to_cell_value(raw[i]) for i in range(1, EFFECT_COLS)]
|
||||
spreadsheet_token = WIKI_NODE_OR_SPREADSHEET_TOKEN
|
||||
sheet_id = SHEET_ID
|
||||
month = SESSION_MONTH.get(session, 2)
|
||||
sheet_id = get_sheet_id_by_month(token, spreadsheet_token, month)
|
||||
if month != 2:
|
||||
print(f'✅ 已选 {month}月 工作表(sheet_id={sheet_id})')
|
||||
range_read = f"{sheet_id}!A1:AG35"
|
||||
vals, read_code, read_body = read_sheet_range(token, spreadsheet_token, range_read)
|
||||
# 401 时刷新 token 并重试读取,确保能定位到日期列
|
||||
@@ -300,19 +335,20 @@ def main():
|
||||
LABELS_GROUP = ['主题', '时长(分钟)', 'Soul推流人数', '进房人数', '人均时长(分钟)', '互动数量', '礼物', '灵魂力', '增加关注', '最高在线']
|
||||
|
||||
def _maybe_send_group(sess, raw_vals):
|
||||
if sess not in ('105', '106', '107'):
|
||||
if sess not in ('105', '106', '107', '113'):
|
||||
return
|
||||
date_label = {'105': '2月20日', '106': '2月21日', '107': '2月23日'}.get(sess, sess + '场')
|
||||
date_label = {'105': '2月20日', '106': '2月21日', '107': '2月23日', '113': '3月2日'}.get(sess, sess + '场')
|
||||
report_link = OPERATION_REPORT_LINK if sheet_id == SHEET_ID else f'https://cunkebao.feishu.cn/wiki/wikcnIgAGSNHo0t36idHJ668Gfd?sheet={sheet_id}'
|
||||
lines = [
|
||||
'【Soul 派对运营报表】',
|
||||
f'链接:{OPERATION_REPORT_LINK}',
|
||||
f'链接:{report_link}',
|
||||
'',
|
||||
f'{sess}场({date_label})已登记:',
|
||||
]
|
||||
for i, label in enumerate(LABELS_GROUP):
|
||||
val = raw_vals[i] if i < len(raw_vals) else ''
|
||||
lines.append(f'{label}:{val}')
|
||||
src_date = {'105': '20260220', '106': '20260221', '107': '20260223'}.get(sess, '20260220')
|
||||
src_date = {'105': '20260220', '106': '20260221', '107': '20260223', '113': '20260302'}.get(sess, '20260220')
|
||||
lines.append(f'数据来源:soul 派对 {sess}场 {src_date}.txt')
|
||||
msg = '\n'.join(lines)
|
||||
ok, _ = send_feishu_group_message(FEISHU_GROUP_WEBHOOK, msg)
|
||||
|
||||
@@ -8,7 +8,8 @@ cd "$SCRIPT_DIR"
|
||||
TOKEN_FILE="$SCRIPT_DIR/.feishu_tokens.json"
|
||||
NOTE_DIR="/Users/karuo/Documents/个人/2、我写的日记/读书笔记"
|
||||
JSON_DIR="/Users/karuo/Documents/卡若Ai的文件夹/导出/读书笔记_feishu_json"
|
||||
PARENT="KNf7wA8Rki1NSdkkSIqcdFtTnWb"
|
||||
# 读书笔记目录链接为快捷方式(KY7ewL21Ki5YRqkuDbecQuCTnTc),需在其父节点下创建
|
||||
PARENT="QPyPwwUmtiweUOk6aTmcZLBxnIg"
|
||||
PUBLISH="python3 $SCRIPT_DIR/feishu_article_unified_publish.py"
|
||||
|
||||
# 若本地无 token,尝试从本地 API 拉取
|
||||
@@ -53,4 +54,4 @@ $PUBLISH --parent "$PARENT" --title "卡若读书笔记:曾仕强《易经》"
|
||||
$PUBLISH --parent "$PARENT" --title "卡若读书笔记:5000天后的世界 - 凯文凯利" --md "$NOTE_DIR/卡若读书笔记:5000天后的世界 - 凯文凯利.md" --json "$JSON_DIR/凯文凯利.json" && echo " ✅ 凯文凯利"
|
||||
$PUBLISH --parent "$PARENT" --title "卡若读书笔记:盐铁之辩与AI之道" --md "$NOTE_DIR/卡若读书笔记:盐铁之辩与AI之道.md" --json "$JSON_DIR/盐铁之辩.json" && echo " ✅ 盐铁之辩"
|
||||
|
||||
echo "📌 全部上传完成。飞书节点: https://cunkebao.feishu.cn/wiki/$PARENT"
|
||||
echo "📌 全部上传完成。读书笔记目录(你给的链接): https://cunkebao.feishu.cn/wiki/KY7ewL21Ki5YRqkuDbecQuCTnTc"
|
||||
|
||||
@@ -150,10 +150,11 @@ MINIPROGRAM_EXTRA = {
|
||||
'107': ['主题关键词 ≤12字', 140, 35000, 400, 8, 90, 3, 25, 10, 45],
|
||||
```
|
||||
|
||||
在 `SESSION_DATE_COLUMN` 中添加日期映射:
|
||||
在 `SESSION_DATE_COLUMN` 和 `SESSION_MONTH` 中添加映射(**按月份选工作表标签**,3 月填 3 月表):
|
||||
|
||||
```python
|
||||
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '22'}
|
||||
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23', '113': '2'}
|
||||
SESSION_MONTH = {'105': 2, '106': 2, '107': 2, '113': 3} # 113场=3月→选「3月」标签
|
||||
```
|
||||
|
||||
#### Step 3:执行写入 + 校验
|
||||
@@ -324,9 +325,12 @@ export FEISHU_APP_SECRET=dhjU0qWd5AzicGWTf4cTqhCWJOrnuCk4
|
||||
# 2. SESSION_DATE_COLUMN 加日期映射
|
||||
SESSION_DATE_COLUMN = {..., 'NEW': '日期号'}
|
||||
|
||||
# 3. _maybe_send_group 内 date_label 和 src_date 可选加映射(可选,不加则不发群)
|
||||
# 3. SESSION_MONTH 加月份(跨月时必填:3 月场次填 3,写入「3月」标签而非 2 月)
|
||||
SESSION_MONTH = {..., 'NEW': 3}
|
||||
|
||||
# 4. 若当日有小程序数据,在 MINIPROGRAM_EXTRA 中加:
|
||||
# 4. _maybe_send_group 内 date_label 和 src_date 可选加映射(可选,不加则不发群)
|
||||
|
||||
# 5. 若当日有小程序数据,在 MINIPROGRAM_EXTRA 中加:
|
||||
# MINIPROGRAM_EXTRA = {..., '23': {'访问次数': 55, '访客': 55, '交易金额': 0}}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: 视频切片
|
||||
description: Soul派对视频切片 + 切片动效包装(片头/片尾/程序化)。触发词含视频剪辑、切片发布、切片动效包装、程序化包装、片头片尾。
|
||||
description: Soul派对视频切片 + 切片动效包装(片头/片尾/程序化)+ 剪映思路借鉴(智能剪口播/镜头分割)。触发词含视频剪辑、切片发布、切片动效包装、程序化包装、片头片尾。
|
||||
group: 木
|
||||
triggers: 视频剪辑、切片发布、字幕烧录、**切片动效包装、程序化包装、片头片尾、批量封面、视频包装**
|
||||
triggers: 视频剪辑、切片发布、字幕烧录、**切片动效包装、程序化包装、片头片尾、批量封面、视频包装**、镜头切分、场景检测
|
||||
owner: 木叶
|
||||
version: "1.2"
|
||||
updated: "2026-02-17"
|
||||
version: "1.3"
|
||||
updated: "2026-03-03"
|
||||
---
|
||||
|
||||
# 视频切片
|
||||
@@ -260,6 +260,7 @@ python3 scripts/burn_subtitles_clean.py -i enhanced.mp4 -s clean.srt -o 成片.m
|
||||
| **soul_slice_pipeline.py** | Soul 切片一体化流水线 | ⭐⭐⭐ 最常用 |
|
||||
| **soul_enhance.py** | 封面+字幕(简体)+加速+去语气词 | ⭐⭐⭐ |
|
||||
| **soul_vertical_crop.py** | Soul 竖屏中段批量裁剪(横版→498×1080 去白边) | ⭐⭐⭐ |
|
||||
| **scene_detect_to_highlights.py** | 镜头/场景检测 → highlights.json(PySceneDetect,可接 batch_clip) | ⭐⭐ |
|
||||
| chapter_themes_to_highlights.py | 按章节 .md 主题提取片段(本地模型→highlights.json) | ⭐⭐⭐ |
|
||||
| identify_highlights.py | 高光识别(Ollama→规则) | ⭐⭐ |
|
||||
| batch_clip.py | 批量切片 | ⭐⭐ |
|
||||
@@ -309,6 +310,9 @@ pip3 list | grep -E "moviepy|Pillow|opencc"
|
||||
|
||||
```bash
|
||||
pip3 install --break-system-packages moviepy Pillow opencc-python-reimplemented
|
||||
|
||||
# 镜头切分(可选):PySceneDetect
|
||||
pip3 install 'scenedetect[opencv]'
|
||||
```
|
||||
|
||||
---
|
||||
@@ -385,6 +389,66 @@ ffmpeg 合成:片头 + 切片 + 片尾
|
||||
|
||||
---
|
||||
|
||||
## 🎞 剪映思路借鉴与自实现(可选能力)
|
||||
|
||||
> 参考 **剪映专业版**(`/Applications/VideoFusion-macOS.app`)内可读配置与流程,用开源方案自实现「智能剪口播」与「智能镜头分割」,不依赖剪映二进制。详见:`参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md`。
|
||||
|
||||
### 智能剪口播(口播稿 → 按文案/时间轴切片段)
|
||||
|
||||
| 剪映逻辑 | 本技能对应实现 |
|
||||
|----------|----------------|
|
||||
| 语音→文字 + 时间戳 | **MLX Whisper** 转录 → `transcript.srt` |
|
||||
| 按文案智能剪、口播稿↔时间轴对齐 | **高光识别**(`identify_highlights` / `chapter_themes_to_highlights`)→ `highlights.json` → `batch_clip` |
|
||||
| 前端配置键 | `script_ai_cut_config`、`transcript_options`(仅作对照,不读写剪映) |
|
||||
|
||||
**结论**:现有流程「转录 → 字幕转简 → 高光识别 → 批量切片 → soul_enhance」已覆盖「智能剪口播」能力;按句/按段细切可与 `transcript.srt` 时间戳结合,在 `highlights.json` 中按句生成条目即可。
|
||||
|
||||
### 智能镜头分割(按镜头/场景切分)
|
||||
|
||||
剪映 **SceneEditDetection** 思路(仅借鉴思路与参数,算法用开源实现):
|
||||
|
||||
- **输入**:帧序列;剪映内部为 96×96 小图 + 数组缓冲。
|
||||
- **算法思路**:图像特征 + 滑动窗口 + 后处理阈值 → 输出镜头边界。
|
||||
- **剪映可读参数**(`SceneEditDetection/config.json`):
|
||||
`sliding_window_size: 7`、`img_feat_dims: 128`、`post_process_threshold: 0.35`、backbone/predhead 模型名(内部用,不引用)。
|
||||
|
||||
**自实现方案**:使用 **PySceneDetect**(ContentDetector/AdaptiveDetector),按阈值与最小场景长度得到切点,再转为与 `batch_clip` 兼容的 `highlights.json`。
|
||||
|
||||
**一键:镜头检测 → highlights → 批量切片 → 增强**
|
||||
|
||||
```bash
|
||||
cd 03_卡木(木)/木叶_视频内容/视频切片/脚本
|
||||
pip install 'scenedetect[opencv]' # 仅首次
|
||||
|
||||
# 镜头检测 → 生成 highlights.json
|
||||
python3 scene_detect_to_highlights.py -i "原视频.mp4" -o "输出目录/highlights_from_scenes.json" -t 27 --min-scene-len 15
|
||||
|
||||
# 用生成的 highlights 做切片 + 增强(与现有流水线一致)
|
||||
python3 batch_clip.py -i "原视频.mp4" -l "输出目录/highlights_from_scenes.json" -o "输出目录/clips/" -p scene
|
||||
python3 soul_enhance.py -c "输出目录/clips/" -l "输出目录/highlights_from_scenes.json" -t "输出目录/transcript.srt" -o "输出目录/clips_enhanced/"
|
||||
```
|
||||
|
||||
**参数速查**:
|
||||
|
||||
| 参数 | 说明 | 建议 |
|
||||
|------|------|------|
|
||||
| `--threshold` / `-t` | 内容变化阈值,越大切点越少 | 27(可试 20~35) |
|
||||
| `--min-scene-len` | 最小场景长度(帧) | 15 |
|
||||
| `--min-duration` | 过滤短于 N 秒的片段 | 按需 |
|
||||
| `--max-clips` / `-n` | 最多保留片段数 | 0=不限制 |
|
||||
|
||||
**与「高光切片」二选一**:
|
||||
- **高光切片**:按话题/金句/提问(需转录 + 高光识别),适合口播、访谈。
|
||||
- **镜头切片**:按画面切换切分,适合多机位、快剪、无稿素材;可先跑 `scene_detect_to_highlights` 再走同一套 `batch_clip` + `soul_enhance`。
|
||||
|
||||
### 参考资料(剪映与流程)
|
||||
|
||||
- **剪映逆向分析**:`03_卡木(木)/木叶_视频内容/视频切片/参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md`
|
||||
- 智能剪口播 H5 路径、智能片段分割 config 与参数、自实现建议与合规说明。
|
||||
- **热点切片标准流程**:`参考资料/热点切片_标准流程.md`(五步、两目录、命令速查)。
|
||||
|
||||
---
|
||||
|
||||
## 📊 输出示例
|
||||
|
||||
```
|
||||
@@ -403,11 +467,16 @@ ffmpeg 合成:片头 + 切片 + 片尾
|
||||
|
||||
```
|
||||
03_卡木(木)/木叶_视频内容/视频切片/
|
||||
├── scripts/
|
||||
├── 脚本/
|
||||
│ ├── soul_slice_pipeline.py # ⭐ Soul 一体化
|
||||
│ ├── soul_enhance.py # ⭐ 封面+字幕+加速
|
||||
│ ├── scene_detect_to_highlights.py # 镜头检测→highlights(剪映思路自实现)
|
||||
│ ├── one_video.py # 单视频成片
|
||||
│ └── ...
|
||||
├── 参考资料/
|
||||
│ ├── 剪映_智能剪口播与智能片段分割_逆向分析.md # 剪映思路与参数参考
|
||||
│ ├── 热点切片_标准流程.md
|
||||
│ └── 竖屏中段裁剪参数说明.md
|
||||
├── 切片动效包装/ # 联动能力:片头/片尾/程序化
|
||||
│ ├── 10秒视频/ # React 程序化模板
|
||||
│ └── 参考资料/切片动效包装速查.md
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
|
||||
## 三、流程总览
|
||||
|
||||
**标准五步**(每步完成再走下一步):① 分析视频→识别话题→导出话题时间 ② 按高光时刻结构整理(前 3 秒/提问) ③ 按时间节点切片→**切片/** ④ 去语助词(合并到⑤) ⑤ 封面+字幕→**成片/**。详见 `参考资料/热点切片_标准流程.md`。
|
||||
|
||||
```
|
||||
原视频 → 转录(MLX) → 高光识别(含 question/hook_3sec,见高光识别提示词) → batch_clip → soul_enhance(成片竖屏直出到 成片/)
|
||||
```
|
||||
|
||||
124
03_卡木(木)/木叶_视频内容/视频切片/参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md
Normal file
124
03_卡木(木)/木叶_视频内容/视频切片/参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 剪映专业版:智能剪口播 & 智能片段分割 · 逆向分析摘要
|
||||
|
||||
> 分析对象:`/Applications/VideoFusion-macOS.app`(剪映专业版,ByteDance,Qt/QML 架构)
|
||||
> 分析范围:**智能剪口播**、**智能片段分割** 两功能在应用内的实现位置与可读配置,不涉及二进制反编译。
|
||||
|
||||
---
|
||||
|
||||
## 一、应用身份与结构
|
||||
|
||||
- **Bundle ID**:`com.lemon.lvpro`
|
||||
- **显示名**:剪映专业版(Info.plist 中麦克风/摄像头描述为「剪映专业版」)
|
||||
- **技术栈**:Qt5、QML、React(部分 H5 模块)、NodeHub 算法管线(JSON 配置 + 原生执行)
|
||||
|
||||
---
|
||||
|
||||
## 二、智能剪口播(口播稿 → 按文案/时间轴切片段)
|
||||
|
||||
### 2.1 前端与入口
|
||||
|
||||
- **资源路径**:`Contents/Resources/image_h5_smart_voiceover_script/`
|
||||
- **入口页**:`voiceover-script.html`,加载 `voiceover-script.*.js`(React 打包)
|
||||
- **相关 H5**:`smart-lyrics.html`、`smart-text.html`(智能歌词/智能文案同套前端体系)
|
||||
|
||||
### 2.2 可读逻辑(从 JS 字符串与调用链推断)
|
||||
|
||||
- **配置键**:`script_ai_cut_config`、`transcript_options`(通过 `getSettingsV2({ nodeKey, keys })` 从宿主获取)
|
||||
- **与宿主通信**:通过 `callJSB` 调用原生:
|
||||
- **开始转录**:`j.startTranscript(this.state.$value.selected)`
|
||||
参数含:`duration`、`theme`、`input` 等,用于「语音 → 文字」并可能带时间戳
|
||||
- **按文案智能剪**:`j.startScriptAiCutEdit({ text: tg.state.currentContent.$value })`
|
||||
把当前编辑区文案传给原生,由原生做「口播稿 ↔ 时间轴」对齐并切片段
|
||||
- **事件回调**:`onTranscriptUpdate`,收到 `state`、`transcript_text`、`progress_title`、`error_code`、`task_id`、`expected_time` 等
|
||||
- **埋点/能力名**:`ai_rough_cut_*`(AI 粗剪)、`smart_voiceover_script_api_metrics`、`ai_rough_cut_ai_write_*`
|
||||
|
||||
### 2.3 实现要点归纳
|
||||
|
||||
- **输入**:口播稿正文(或先通过「转录」得到带时间戳的文本)。
|
||||
- **输出**:由原生侧在时间轴上生成/切分片段(具体切分、对齐算法在 **native 二进制** 中,前端只传文案与收结果)。
|
||||
- **流程**:H5 编辑文案 → 选时长/主题等 → `startTranscript` 或 `startScriptAiCutEdit` → 原生执行 → 通过 `onTranscriptUpdate` 等回传状态与结果。
|
||||
|
||||
---
|
||||
|
||||
## 三、智能片段分割(按镜头/场景切分)
|
||||
|
||||
### 3.1 算法配置位置
|
||||
|
||||
- **资源路径**:`Contents/Resources/SceneEditDetection/`
|
||||
- **配置文件**:`config.json`(当前)、`config_prev.json`(旧版)
|
||||
|
||||
### 3.2 算法管线(从 config.json 逆向)
|
||||
|
||||
- **节点**:
|
||||
- `input_0`:`blit`,尺寸 **96×96**(当前版;旧版 128×128)
|
||||
- `input_1`:`array_buffer_producer`(提供数组/缓冲,如帧序列或特征)
|
||||
- `compress_shot_detect_0`:**compress_shot_detect**(压缩镜头检测)
|
||||
|
||||
- **关键参数**(当前版):
|
||||
- `compress_shot_detect_sliding_window_size`: **7**
|
||||
- `compress_shot_detect_img_feat_dims`: **128**
|
||||
- `compress_shot_detect_post_process_threshold`: **0.35**
|
||||
- `compress_shot_detect_backbone_model_name`: **"jy_compressShotDetectBackbone_new"**
|
||||
- `compress_shot_detect_predhead_model_name`: **"jy_compressShotDetectPredHead_new"**
|
||||
- `compress_shot_detect_debug_data_save_path`: **""**
|
||||
|
||||
- **数据流**:
|
||||
`input_0`(图像) + `input_1`(数组缓冲) → **compress_shot_detect** → 输出应为镜头边界/切分点(具体输出格式在原生实现中)。
|
||||
|
||||
### 3.3 实现要点归纳
|
||||
|
||||
- **类型**:基于**图像特征 + 滑动窗口**的镜头/场景切分(非纯音频)。
|
||||
- **思路**:小图 96×96 输入 → backbone 提特征 → predhead 在滑动窗口内做切点预测 → 后处理阈值 0.35。
|
||||
- **模型**:双模型结构(backbone + predhead),名称带 `jy_`、`_new`,为剪映内部命名;模型权重在应用其他目录或运行时下载,未在 SceneEditDetection 下明文暴露。
|
||||
|
||||
---
|
||||
|
||||
## 四、其他相关模块(便于对照)
|
||||
|
||||
| 功能 | 资源路径 | 说明 |
|
||||
|----------------|----------------------------------|------|
|
||||
| 智能裁剪 | `lvop_intelligent_crop/` | reframe:人脸 + 显著性(nodehub_image_saliency) + video_reframe,输出裁剪框 |
|
||||
| 智能文案/歌词 | `image_h5_smart_text/`、`smart_lyrics` | 与口播稿共用一套「文案→时间轴」体系 |
|
||||
| 算法通用配置 | `agicbach/algorithmConfig.json` | 人脸/表情等,与 AIGC 充电脚本(jianying_aigc_charge) 关联 |
|
||||
| 片段/轨道逻辑 | `aigc_text_template/js/main.js` | ScriptSegment、SetWidgetTimeRange、canvasSplitThreshold 等,用于模板内片段与画布分割 |
|
||||
|
||||
---
|
||||
|
||||
## 五、自实现建议(不依赖逆向二进制)
|
||||
|
||||
1. **智能剪口播**
|
||||
- 语音→文字:用 **Whisper**(如 MLX Whisper)得到带时间戳的 SRT/文稿。
|
||||
- 按文案切:用文稿时间戳对应到时间轴,在 FFmpeg/OpenCV 或时间线 API 上按句/段切分并生成片段。
|
||||
- 你现有流程(`transcript.srt` + `highlights.json` + `batch_clip`)已覆盖「话题/高光时间 → 切片」逻辑,可与「按句切」结合做细粒度口播剪。
|
||||
|
||||
2. **智能片段分割**
|
||||
- 思路与剪映一致:**帧级特征 → 切点检测 → 后处理**。
|
||||
- 可用:**PySceneDetect**、**scenedetect**(阈值或基于内容)、或自训轻量 backbone+二分类头,用 96×96 或 128×128 输入、滑动窗口 + 阈值做切点。
|
||||
- 数据:用公开镜头切分数据集(如 RAI、其他 scene detection 数据集)训练/微调。
|
||||
|
||||
3. **合规**
|
||||
- 本分析仅基于应用内**可读资源**(JSON、前端 JS 字符串、目录与命名)。
|
||||
- 未对原生二进制做反编译或提取;自实现时使用开源模型与公开算法,不依赖剪映内部模型或代码。
|
||||
|
||||
---
|
||||
|
||||
## 六、文件与路径速查
|
||||
|
||||
```
|
||||
VideoFusion-macOS.app/
|
||||
├── Contents/Info.plist # 应用信息、剪映专业版描述
|
||||
├── Contents/Resources/
|
||||
│ ├── image_h5_smart_voiceover_script/ # 智能剪口播 H5
|
||||
│ │ ├── voiceover-script.html
|
||||
│ │ └── static/js/voiceover-script.*.js
|
||||
│ ├── SceneEditDetection/ # 智能片段分割
|
||||
│ │ ├── config.json # compress_shot_detect 参数
|
||||
│ │ └── config_prev.json
|
||||
│ ├── lvop_intelligent_crop/ # 智能裁剪(reframe)
|
||||
│ │ └── algo/reframe_plugin.json
|
||||
│ └── aigc_text_template/js/main.js # ScriptSegment、时间范围、canvas 分割
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*文档生成后可用于:对照现有热点切片流程、设计自研「按口播切」与「按镜头切」管线,不涉及对剪映二进制的进一步逆向。*
|
||||
81
03_卡木(木)/木叶_视频内容/视频切片/参考资料/热点切片_标准流程.md
Normal file
81
03_卡木(木)/木叶_视频内容/视频切片/参考资料/热点切片_标准流程.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 热点切片 · 标准流程(五步、两目录)
|
||||
|
||||
> 只保留**两个目录**:**切片**、**成片**。单段时长 **30 秒~8 分钟**,按高光时刻时间节点切,前 3 秒抓眼球/提问。
|
||||
|
||||
---
|
||||
|
||||
## 第一步:分析视频 → 识别话题 → 导出话题时间
|
||||
|
||||
1. **找到视频**,确定源文件。
|
||||
2. **转录**:MLX Whisper 导出 `transcript.srt`(带时间戳)。
|
||||
3. **识别话题**:从转录/文档中识别主要话题与结构。
|
||||
4. **导出话题时间**:得到每个话题的**起止时间节点**(如 00:00:00-00:04:00),整理成可裁剪的时间表。
|
||||
|
||||
**产出**:`transcript.srt`、话题与时间节点列表(或 `highlights.json` 雏形)。
|
||||
|
||||
---
|
||||
|
||||
## 第二步:按高光时刻结构整理(前 3 秒规则)
|
||||
|
||||
1. **高光时刻与话术**:为每段确定「高光时刻」文案(前 3 秒展示的提问或金句)。
|
||||
2. **时间节点与视频结构**:与第一步的时间节点一一对应。
|
||||
3. **前 3 秒规则**:有**提问**的片段,前 3 秒用**提问话术**;无提问用金句/悬念。格式见 `高光识别提示词.md`。
|
||||
|
||||
**产出**:`highlights.json`(含 `title`、`start_time`、`end_time`、`hook_3sec`、`question` 等),作为后续切片与成片的结构依据。
|
||||
|
||||
---
|
||||
|
||||
## 第三步:按时间节点做切片提取
|
||||
|
||||
1. **按 highlights 时间**:用 `batch_clip` 根据 `start_time`、`end_time` 裁剪视频。
|
||||
2. **单段时长**:控制在 **30 秒~8 分钟** 一个话题。
|
||||
3. **输出**:全部写入 **切片/** 目录,文件名含前缀与标题。
|
||||
|
||||
**产出**:**切片/** 目录下的横版切片 mp4。
|
||||
|
||||
---
|
||||
|
||||
## 第四步:去语助词并整理为新切片
|
||||
|
||||
1. **语助词去除**:在成片环节由 `soul_enhance` 统一处理(转录稿清理 + 烧录字幕时已去语助词)。
|
||||
2. **不单独生成「去语助词版」目录**:去语助词与封面、字幕在同一链路完成,输出到 **成片/**。
|
||||
|
||||
**说明**:当前流程将第四步合并到第五步,由 soul_enhance 一步完成(去语助词 + 封面 + 字幕)。
|
||||
|
||||
---
|
||||
|
||||
## 第五步:加封面 + 烧录字幕 → 成片
|
||||
|
||||
1. **封面**:前 3 秒使用高光时刻/提问文案,半透明质感,Soul 绿风格。
|
||||
2. **字幕**:烧录到画面,封面结束后显示,居中去语助词。
|
||||
3. **输出**:竖屏 498×1080(可选)、文件名为纯标题,全部写入 **成片/**。
|
||||
|
||||
**产出**:**成片/** 目录下的成片 mp4(封面+字幕+去语助词)。
|
||||
|
||||
---
|
||||
|
||||
## 流程小结(顺序执行)
|
||||
|
||||
| 步骤 | 内容 | 产出 |
|
||||
|------|------|------|
|
||||
| 1 | 分析视频 → 识别话题 → 导出话题时间 | transcript.srt、时间节点 |
|
||||
| 2 | 按高光时刻结构整理(前 3 秒、提问/金句) | highlights.json |
|
||||
| 3 | 按时间节点切片提取(30 秒~8 分钟/段) | **切片/** |
|
||||
| 4 | 去语助词(合并在步骤 5) | — |
|
||||
| 5 | 加封面 + 烧录字幕 | **成片/** |
|
||||
|
||||
**只保留两个目录**:**切片**、**成片**。其他中间目录不保留。
|
||||
|
||||
---
|
||||
|
||||
## 命令速查(112 场示例)
|
||||
|
||||
```bash
|
||||
# 1~2. 已有 transcript.srt、highlights.json(按热点切片时间节点)
|
||||
|
||||
# 3. 切片
|
||||
python3 batch_clip.py -i "原视频.mp4" -l highlights.json -o 切片/ -p soul112
|
||||
|
||||
# 4~5. 成片(去语助词+封面+字幕)
|
||||
python3 soul_enhance.py -c 切片/ -l highlights.json -t transcript.srt -o 成片/ --vertical --title-only --force-burn-subs
|
||||
```
|
||||
97
03_卡木(木)/木叶_视频内容/视频切片/脚本/scene_detect_to_highlights.py
Normal file
97
03_卡木(木)/木叶_视频内容/视频切片/脚本/scene_detect_to_highlights.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
镜头/场景切分 → highlights.json
|
||||
基于 PySceneDetect 做镜头边界检测,输出与 batch_clip / soul_slice_pipeline 兼容的 highlights 格式。
|
||||
参考剪映专业版智能片段分割思路:帧级特征 + 切点检测 + 后处理阈值(见 参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md)。
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def format_timestamp(seconds: float) -> str:
|
||||
"""秒 → HH:MM:SS"""
|
||||
h = int(seconds // 3600)
|
||||
m = int((seconds % 3600) // 60)
|
||||
s = seconds % 60
|
||||
return f"{h:02d}:{m:02d}:{int(s):02d}.{int((s % 1) * 100):02d}"
|
||||
|
||||
|
||||
def detect_scenes(video_path: str, threshold: float = 27.0, min_scene_len: int = 15) -> list:
|
||||
"""
|
||||
使用 PySceneDetect 检测场景切点。
|
||||
threshold: 内容变化阈值,越大切点越少(剪映参考约 0.35 后处理阈值,此处为 ContentDetector 的 0-255 档位)。
|
||||
min_scene_len: 最小场景长度(帧数),避免过碎。
|
||||
"""
|
||||
try:
|
||||
from scenedetect import detect, ContentDetector
|
||||
except ImportError:
|
||||
print("请安装: pip install scenedetect[opencv]")
|
||||
sys.exit(1)
|
||||
|
||||
detector = ContentDetector(threshold=threshold, min_scene_len=min_scene_len)
|
||||
scene_list = detect(video_path, detector)
|
||||
if not scene_list:
|
||||
return []
|
||||
# scene_list: list of (start FrameTimecode, end FrameTimecode)
|
||||
out = []
|
||||
for start_tc, end_tc in scene_list:
|
||||
start_sec = start_tc.get_seconds()
|
||||
end_sec = end_tc.get_seconds()
|
||||
out.append((start_sec, end_sec))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="镜头检测 → highlights.json(供 batch_clip 使用)")
|
||||
parser.add_argument("--video", "-i", required=True, help="输入视频路径")
|
||||
parser.add_argument("--output", "-o", required=True, help="输出 highlights.json 路径")
|
||||
parser.add_argument("--threshold", "-t", type=float, default=27.0,
|
||||
help="ContentDetector 阈值,越大切点越少(默认 27)")
|
||||
parser.add_argument("--min-scene-len", type=int, default=15,
|
||||
help="最小场景长度(帧),默认 15")
|
||||
parser.add_argument("--max-clips", "-n", type=int, default=0,
|
||||
help="最多保留片段数,0 表示不限制")
|
||||
parser.add_argument("--min-duration", type=float, default=0,
|
||||
help="过滤掉时长小于此值的片段(秒),默认 0")
|
||||
args = parser.parse_args()
|
||||
|
||||
video_path = Path(args.video)
|
||||
if not video_path.exists():
|
||||
print(f"❌ 视频不存在: {video_path}")
|
||||
sys.exit(1)
|
||||
|
||||
scene_list = detect_scenes(
|
||||
str(video_path),
|
||||
threshold=args.threshold,
|
||||
min_scene_len=args.min_scene_len,
|
||||
)
|
||||
if not scene_list:
|
||||
print("未检测到场景切点,可尝试降低 --threshold")
|
||||
sys.exit(1)
|
||||
|
||||
clips = []
|
||||
for i, (start_sec, end_sec) in enumerate(scene_list, 1):
|
||||
duration = end_sec - start_sec
|
||||
if args.min_duration and duration < args.min_duration:
|
||||
continue
|
||||
clips.append({
|
||||
"start_time": format_timestamp(start_sec),
|
||||
"end_time": format_timestamp(end_sec),
|
||||
"title": f"镜头{i}",
|
||||
})
|
||||
if args.max_clips and len(clips) >= args.max_clips:
|
||||
break
|
||||
|
||||
out_path = Path(args.output)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump({"clips": clips}, f, ensure_ascii=False, indent=2)
|
||||
print(f"✓ 已写入 {len(clips)} 个镜头 → {out_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -96,6 +96,9 @@ def main():
|
||||
parser.add_argument("--skip-subs", action="store_true", help="跳过字幕烧录(原片已有字幕时用)")
|
||||
parser.add_argument("--force-burn-subs", action="store_true", help="强制烧录字幕(忽略检测)")
|
||||
parser.add_argument("--force-transcribe", action="store_true", help="强制重新转录(删除旧 transcript 并重跑)")
|
||||
parser.add_argument("--two-folders", action="store_true", help="仅用两文件夹:切片、成片(默认 clips、clips_enhanced)")
|
||||
parser.add_argument("--slices-only", action="store_true", help="只做到切片(MLX 转录→高光→批量切片),不跑成片增强")
|
||||
parser.add_argument("--prefix", default="", help="切片文件名前缀,如 soul112")
|
||||
args = parser.parse_args()
|
||||
|
||||
video_path = Path(args.video).resolve()
|
||||
@@ -109,11 +112,15 @@ def main():
|
||||
base_dir = video_path.parent / (video_path.stem + "_output")
|
||||
base_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
use_two_folders = getattr(args, "two_folders", False)
|
||||
clips_dir_name = "切片" if use_two_folders else "clips"
|
||||
enhanced_dir_name = "成片" if use_two_folders else "clips_enhanced"
|
||||
|
||||
audio_path = base_dir / "audio.wav"
|
||||
transcript_path = base_dir / "transcript.srt"
|
||||
highlights_path = base_dir / "highlights.json"
|
||||
clips_dir = base_dir / "clips"
|
||||
enhanced_dir = base_dir / "clips_enhanced"
|
||||
clips_dir = base_dir / clips_dir_name
|
||||
enhanced_dir = base_dir / enhanced_dir_name
|
||||
|
||||
print("=" * 60)
|
||||
print("🎬 Soul 切片流水线:视频制作 + 视频切片")
|
||||
@@ -202,6 +209,7 @@ def main():
|
||||
|
||||
# 3. 批量切片
|
||||
clips_dir.mkdir(parents=True, exist_ok=True)
|
||||
clip_prefix = getattr(args, "prefix", None) or "soul"
|
||||
if not args.skip_clips:
|
||||
run(
|
||||
[
|
||||
@@ -210,15 +218,25 @@ def main():
|
||||
"--input", str(video_path),
|
||||
"--highlights", str(highlights_path),
|
||||
"--output", str(clips_dir),
|
||||
"--prefix", "soul",
|
||||
"--prefix", clip_prefix,
|
||||
],
|
||||
"批量切片",
|
||||
timeout=300,
|
||||
)
|
||||
elif not list(clips_dir.glob("*.mp4")):
|
||||
print("❌ clips/ 为空,请去掉 --skip-clips 或先完成切片")
|
||||
print(f"❌ {clips_dir_name}/ 为空,请去掉 --skip-clips 或先完成切片")
|
||||
sys.exit(1)
|
||||
|
||||
if getattr(args, "slices_only", False):
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("✅ 切片阶段完成(--slices-only)")
|
||||
print("=" * 60)
|
||||
print(f" 切片: {clips_dir}")
|
||||
print(f" 转录: {transcript_path}")
|
||||
print(f" 高光: {highlights_path}")
|
||||
return
|
||||
|
||||
# 4. 增强(封面+字幕+加速):soul_enhance(Pillow,无需 drawtext)
|
||||
enhanced_dir.mkdir(parents=True, exist_ok=True)
|
||||
enhance_cmd = [
|
||||
@@ -238,7 +256,7 @@ def main():
|
||||
import shutil
|
||||
enhanced_count = len(list(enhanced_dir.glob("*.mp4")))
|
||||
if enhanced_count == 0 and clips_list:
|
||||
print(" (soul_enhance 失败,复制原始切片到 clips_enhanced)")
|
||||
print(f" (soul_enhance 失败,复制原始切片到 {enhanced_dir_name}/)")
|
||||
for f in sorted(clips_dir.glob("*.mp4")):
|
||||
shutil.copy(f, enhanced_dir / f.name)
|
||||
|
||||
@@ -247,7 +265,7 @@ def main():
|
||||
print("✅ 流水线完成")
|
||||
print("=" * 60)
|
||||
print(f" 切片: {clips_dir}")
|
||||
print(f" 增强: {enhanced_dir}")
|
||||
print(f" 成片: {enhanced_dir}")
|
||||
print(f" 清单: {base_dir / 'clips_manifest.json'}")
|
||||
|
||||
|
||||
|
||||
@@ -214,3 +214,4 @@
|
||||
| 2026-03-03 04:58:10 | 🔄 卡若AI 同步 2026-03-03 04:58 | 更新:Cursor规则、总索引与入口、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-03 05:02:46 | 🔄 卡若AI 同步 2026-03-03 05:02 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-03 10:15:48 | 🔄 卡若AI 同步 2026-03-03 10:15 | 更新:水桥平台对接、卡木、火炬、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-03 10:20:17 | 🔄 卡若AI 同步 2026-03-03 10:20 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
|
||||
@@ -217,3 +217,4 @@
|
||||
| 2026-03-03 04:58:10 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-03 04:58 | 更新:Cursor规则、总索引与入口、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-03 05:02:46 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-03 05:02 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-03 10:15:48 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-03 10:15 | 更新:水桥平台对接、卡木、火炬、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-03 10:20:17 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-03 10:20 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
Reference in New Issue
Block a user