diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py index d99c3f1b..023c4f38 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/soul_party_to_feishu_sheet.py @@ -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) diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/读书笔记_上传到飞书.sh b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/读书笔记_上传到飞书.sh index 25504ddd..62ac4505 100755 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/读书笔记_上传到飞书.sh +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/读书笔记_上传到飞书.sh @@ -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" diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/运营报表_SKILL.md b/02_卡人(水)/水桥_平台对接/飞书管理/运营报表_SKILL.md index 5ad96c12..6f2a910d 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/运营报表_SKILL.md +++ b/02_卡人(水)/水桥_平台对接/飞书管理/运营报表_SKILL.md @@ -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}} ``` diff --git a/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md b/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md index dde03097..3a38745e 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md +++ b/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md @@ -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,13 +467,18 @@ ffmpeg 合成:片头 + 切片 + 片尾 ``` 03_卡木(木)/木叶_视频内容/视频切片/ -├── scripts/ -│ ├── soul_slice_pipeline.py # ⭐ Soul 一体化 -│ ├── soul_enhance.py # ⭐ 封面+字幕+加速 -│ ├── one_video.py # 单视频成片 +├── 脚本/ +│ ├── soul_slice_pipeline.py # ⭐ Soul 一体化 +│ ├── soul_enhance.py # ⭐ 封面+字幕+加速 +│ ├── scene_detect_to_highlights.py # 镜头检测→highlights(剪映思路自实现) +│ ├── one_video.py # 单视频成片 │ └── ... -├── 切片动效包装/ # 联动能力:片头/片尾/程序化 -│ ├── 10秒视频/ # React 程序化模板 +├── 参考资料/ +│ ├── 剪映_智能剪口播与智能片段分割_逆向分析.md # 剪映思路与参数参考 +│ ├── 热点切片_标准流程.md +│ └── 竖屏中段裁剪参数说明.md +├── 切片动效包装/ # 联动能力:片头/片尾/程序化 +│ ├── 10秒视频/ # React 程序化模板 │ └── 参考资料/切片动效包装速查.md ├── fonts/ └── SKILL.md diff --git a/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md b/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md index 2722a39f..84486039 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md +++ b/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md @@ -27,6 +27,8 @@ ## 三、流程总览 +**标准五步**(每步完成再走下一步):① 分析视频→识别话题→导出话题时间 ② 按高光时刻结构整理(前 3 秒/提问) ③ 按时间节点切片→**切片/** ④ 去语助词(合并到⑤) ⑤ 封面+字幕→**成片/**。详见 `参考资料/热点切片_标准流程.md`。 + ``` 原视频 → 转录(MLX) → 高光识别(含 question/hook_3sec,见高光识别提示词) → batch_clip → soul_enhance(成片竖屏直出到 成片/) ``` diff --git a/03_卡木(木)/木叶_视频内容/视频切片/参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md new file mode 100644 index 00000000..87024bc7 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/剪映_智能剪口播与智能片段分割_逆向分析.md @@ -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 分割 +``` + +--- + +*文档生成后可用于:对照现有热点切片流程、设计自研「按口播切」与「按镜头切」管线,不涉及对剪映二进制的进一步逆向。* diff --git a/03_卡木(木)/木叶_视频内容/视频切片/参考资料/热点切片_标准流程.md b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/热点切片_标准流程.md new file mode 100644 index 00000000..3ed7ba12 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/热点切片_标准流程.md @@ -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 +``` diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/scene_detect_to_highlights.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/scene_detect_to_highlights.py new file mode 100644 index 00000000..37801822 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/scene_detect_to_highlights.py @@ -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()) diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py index 143f2ea3..b5ca19e6 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py @@ -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'}") diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index c56e6912..ca1d6108 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -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 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 99ffe048..39750722 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -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) |