diff --git a/01_卡资(金)/金盾_数据安全/存客宝/SKILL.md b/01_卡资(金)/金盾_数据安全/存客宝/SKILL.md index 6258d893..4df6acdb 100644 --- a/01_卡资(金)/金盾_数据安全/存客宝/SKILL.md +++ b/01_卡资(金)/金盾_数据安全/存客宝/SKILL.md @@ -27,6 +27,10 @@ updated: "2026-02-16" 2. **部署**:Vercel、宝塔、GitHub Webhook(见 Vercel与v0部署流水线) 3. **运维**:数据库清理(见 数据库管理)、服务器管理 +## 抖音发布 + +存客宝当前**无抖音登录/发布 SDK**。若需将视频发布到抖音,使用「抖音发布」Skill(抖音开放平台 OAuth + 上传/创建视频);若后续存客宝或腕推提供抖音发布能力,可在 `03_卡木(木)/木叶_视频内容/抖音发布/SKILL.md` 中补充对接方式。 + ## 参考 - `开发文档/00_汇总索引.md`:开发文档、功能迭代、需求 diff --git a/02_卡人(水)/水桥_平台对接/Soul创业实验/SKILL.md b/02_卡人(水)/水桥_平台对接/Soul创业实验/SKILL.md index 41149314..98d6d955 100644 --- a/02_卡人(水)/水桥_平台对接/Soul创业实验/SKILL.md +++ b/02_卡人(水)/水桥_平台对接/Soul创业实验/SKILL.md @@ -1,16 +1,16 @@ --- name: Soul创业实验 -description: 《一场soul的创业实验》内容与运营统一入口——写作、上传、运营报表,子类聚合 +description: 《一场soul的创业实验》内容与运营统一入口——写作、上传、推送(飞书群)、运营报表,子类聚合 triggers: Soul创业实验、写Soul文章、写授文章、Soul派对写文章、第9章写文章、写soul场次、soul文章规则、Soul文章上传、Soul派对文章、第9章上传、soul上传、写soul文章、运营报表、派对填表、派对纪要 owner: 水桥 group: 水 -version: "1.0" -updated: "2026-02-26" +version: "1.1" +updated: "2026-03-01" --- # Soul创业实验 Skill -> 《一场soul的创业实验》相关:**写作**(第9章单场文章)、**上传**(文章到小程序)、**运营报表**(派对数据→飞书)。按需求进入对应子类执行。 +> 《一场soul的创业实验》相关:**写作**(第9章单场文章)、**上传**(文章到小程序)、**推送**(上传后同步固定飞书群:前 6% 正文「一句一行、行间空一行」+ 海报图/二维码,不发小程序链接)、**运营报表**(派对数据→飞书)。按需求进入对应子类执行。 **📌 交接备注(2026-02-26)**:**一场创业实验**(Soul 创业实验场 / Mycontent 项目)**后续操作交由永平**。代码与进度以 GitHub 为准:**https://github.com/fnvtk/Mycontent/tree/yongpxu-soul**。卡若侧仅保留写作/上传/运营报表等 Skill 入口,具体开发与迭代以永平分支为准。 @@ -20,8 +20,8 @@ updated: "2026-02-26" | 子类 | 触发词示例 | 说明 | |:---|:---|:---| -| **写作** | 写Soul文章、写授文章、Soul派对写文章、第9章写文章、写soul场次、soul文章规则 | 按派对 TXT 写第9章单场文章,人称「我」整篇最多一次、联系管理/切片/副业隐晦植入 | -| **上传** | Soul文章上传、Soul派对文章、第9章上传、soul上传、写soul文章、文章写好上传 | 文章写好后上传到小程序,id 已存在则更新 | +| **写作** | 写Soul文章、写授文章、Soul派对写文章、第9章写文章、写soul场次、soul文章规则 | 按派对 TXT 写第9章单场文章,**先读** `写作/写作规范.md`:人称「我」整篇最多三次、每句空一行、大白话;**数值与场景必须具体**(金额、人数、时长、曝光、成本等写清具体数字;涉及 Soul 投流/派对价值算法时写清 75 曝光进 1 人、3 万曝光/天、投流 1000 曝光 6~10 块等);**约 20% 处 + 结尾各一句分享句(≤50 字),不用「干货」二字及「干货:」等格式**;联系管理/切片/副业隐晦植入 | +| **上传** | Soul文章上传、Soul派对文章、第9章上传、soul上传、写soul文章、文章写好上传 | 文章写好后上传到小程序;**上传后同步固定飞书群**:发前 6% 正文(**一句一行、行间空一行**)+ 章节海报图(含小程序码),**不发小程序链接**,见 `上传/README.md` 与 `上传/推送逻辑.md` | | **运营报表** | 运营报表、派对填表、派对截图填表发群、派对纪要、106场、107场、本月运营数据 | 派对效果数据→飞书表格→智能纪要→飞书群,见飞书管理下运营报表 Skill | 执行时:根据用户说的关键词判断是**写作 / 上传 / 运营报表**,再进入对应子类(读本目录下 `写作/` 或 `上传/` 或引用运营报表)。 @@ -32,13 +32,15 @@ updated: "2026-02-26" - **入口**:`写作/写作规范.md`(人称、结构、格式、隐晦植入等,唯一规范来源)。 - **何时用**:写第 X 场、写Soul文章、写授文章。写完后输出到书稿第9章目录 `9.xx 第X场|主题.md`,再走子类二上传。 +- **数值与场景**:文中**有数值、有场景必须写具体**;涉及 Soul 投流、派对价值算法时,写清具体数值(如约 75~80 曝光进 1 人、每天约 3 万曝光、进房 300~600、1000 曝光 6~10 块、进一人约 4 毛 2、获客到微信约 20 块/人等),见写作规范「数值与场景」一条。 --- -## 子类二:上传 +## 子类二:上传与推送 -- **入口**:`上传/README.md`(路径、content_upload 命令、飞书群)。 +- **入口**:`上传/README.md`(路径、content_upload 命令)、`上传/推送逻辑.md`(飞书群推送规则)。 - **何时用**:文章写好上传小程序、发到小程序。id 已存在→更新,否则创建。 +- **推送**:上传到小程序后,**同步发固定飞书群**:先发文章前 6% 正文(**一句一行、行间空一行**),再发章节海报图(含该章节小程序码);**不在群里发小程序链接**,只发二维码(海报)。 --- @@ -54,14 +56,29 @@ updated: "2026-02-26" |:---|:---| | `SKILL.md` | 本文件,仅触发与子类导航 | | `写作/写作规范.md` | 写作唯一规范(人称、结构、格式、隐晦植入) | -| `上传/README.md` | 上传唯一说明(路径、命令、飞书) | +| `上传/README.md` | 上传唯一说明(路径、命令、推送步骤) | +| `上传/推送逻辑.md` | 飞书群推送:前 6% + 海报图、不发链接、脚本与链路 | +| `上传/飞书妙记转文章并发布.md` | 飞书妙记链接 → 下载文本/视频 → 写成第 X 场文章 → 发飞书群 + 小程序(一句话指令 + 步骤) | Soul 相关仅保留本 Skill 一个目录,原 Soul文章上传、Soul文章写作 已删除并合并至此。 --- +## 发布与推送链路(完整) + +1. **写文章**:按 `写作/写作规范.md` 写第 X 场,输出到书稿第9章目录 `第X场|主题.md`。 +2. **上传到小程序**:在永平项目执行 `content_upload.py --id 9.xx --title "…" --content-file "" --part part-4 --chapter chapter-9 --price 1.0`。 +3. **同步飞书群**(上传后必做):在永平项目执行 `scripts/send_chapter_poster_to_feishu.py <章节id> "<章节标题>" --md "<同文章md路径>"`。效果:向 Soul 彩民团队飞书群**先发一条文本**(标题 + 文章前 6% 正文),**再发一张海报图**(含该章节小程序码);**不发送小程序链接**,仅通过海报中的二维码引导阅读。 + +海报规则:摘要区用文章前 6% 字数、每句空一行、不超出边框;无手指图标;字体使用 PingFang 优化可读性。详见 `上传/推送逻辑.md`。 + +固定推送群:**Soul 彩民团队** 飞书群(webhook 已写在脚本默认值中,无需复制;运行推送命令即发到该群)。 + +--- + ## 版本记录 | 版本 | 日期 | 说明 | |:---|:---|:---| | 1.0 | 2026-02-26 | 初版;写作、上传、运营报表统一为子类,原 Soul文章写作 / Soul文章上传 合并至此 | +| 1.1 | 2026-03-01 | 上传后推送飞书群:前 6% + 海报图,不发链接;新增推送逻辑文档与链路说明 | diff --git a/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/README.md b/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/README.md index bacbd7b7..a29e49b5 100644 --- a/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/README.md +++ b/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/README.md @@ -43,10 +43,19 @@ python3 content_upload.py --id 9.23 --title "9.23 第110场|Soul变现逻辑 --- -## 发飞书群(可选) +## 同步飞书群(上传后必做) -- 永平项目下:`python3 scripts/post_to_feishu.py --release 9.xx --title "9.xx 第X场|标题"` 可发新发布通知到卡若日志飞书群。 -- 海报到飞书:需配置 `scripts/.env.feishu`(FEISHU_APP_ID / FEISHU_APP_SECRET),调用 `send_poster_to_feishu.py`(若存在)。 +上传到小程序后,**同步**推送到固定飞书群:发**前 6% 正文**(一句一行、行间空一行)+ **章节海报图**(含该章节小程序码),**不发小程序链接**。详见 `上传/推送逻辑.md`。 + +在永平项目下执行(`--md` 为**本篇文章**的 md 路径): + +```bash +python3 scripts/send_chapter_poster_to_feishu.py 9.xx "第X场|标题" --md "<文章.md 完整路径>" +``` + +- 需配置 `scripts/.env.feishu`(FEISHU_APP_ID、FEISHU_APP_SECRET)。 +- 依赖:`pip install requests Pillow`。 +- 默认发到 **Soul 彩民团队** 飞书群(webhook 已写在脚本内),无需复制链接,直接运行上述命令即可。 --- diff --git a/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/推送逻辑.md b/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/推送逻辑.md new file mode 100644 index 00000000..ec012582 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/推送逻辑.md @@ -0,0 +1,48 @@ +# Soul 文章推送飞书群 · 逻辑说明 + +> 上传到小程序后,**同步**把该章节推送到固定飞书群:发**前 6% 正文**(一句一行、行间空一行)+ **章节海报图**(含小程序码),**不发小程序链接**。 + +--- + +## 原则 + +- **不发链接**:群里不出现「小程序:https://...」等链接,只通过海报中的**二维码**引导扫码阅读。 +- **文本格式**:**一句一行**,句子之间**空一行**(也就是用 `\n\n` 分隔句子)。 +- **内容**:一条**文本消息**(标题 + 文章前 6% 正文)+ 一条**图片消息**(章节海报,内含该章节小程序码)。 +- **顺序**:先发前 6% 正文,再发海报图。 + +--- + +## 脚本与命令 + +- **脚本**:永平项目下 `scripts/send_chapter_poster_to_feishu.py` +- **依赖**:`pip install requests Pillow`;飞书应用凭证写在 `scripts/.env.feishu`(FEISHU_APP_ID、FEISHU_APP_SECRET)。 +- **固定群 webhook**:脚本内置默认发到 **Soul 彩民团队** 飞书群,webhook 为 `https://open.feishu.cn/open-apis/bot/v2/hook/14a7e0d3-864d-4709-ad40-0def6edba566`。无需复制链接,直接运行命令即可。 +- **命令示例**(上传完成后执行): + +```bash +cd "/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平" +python3 scripts/send_chapter_poster_to_feishu.py 9.24 "第112场|一个人起头,维权挣了大半套房" \ + --md "/Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/第112场|一个人起头,维权挣了大半套房.md" +``` + +- **参数**:`<章节id>`、`<章节标题>`、`--md <文章 .md 路径>`。摘要自动取该文章正文前 6% 字数。 + +--- + +## 海报规则 + +- 标题:章节标题(如「第112场|…」),不再用「Soul创业派对」或「精彩内容」做主标题。 +- 摘要区:文章前 6% 字数,每句空一行,严格限制在摘要框内,超出则截断并加省略号。 +- **无手指图标**;底部为「长按识别小程序码」+ 章节小程序码。 +- 字体:PingFang(标题加粗、正文常规),便于阅读。 + +--- + +## 完整链路(写文章 → 上传 → 推送) + +1. 写文章 → 保存到书稿第9章目录(如 `第112场|…md`)。 +2. 上传到小程序:`content_upload.py --id 9.xx --title "…" --content-file "" --part part-4 --chapter chapter-9 --price 1.0`。 +3. 推送飞书群:`scripts/send_chapter_poster_to_feishu.py <章节id> "<章节标题>" --md "<同一 md 路径>"`。 + +推送后,飞书群收到:① 标题 + 前 6% 正文;② 海报图(含该章节小程序码)。不收到任何小程序链接。 diff --git a/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/飞书妙记转文章并发布.md b/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/飞书妙记转文章并发布.md new file mode 100644 index 00000000..56ac6ef2 --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/Soul创业实验/上传/飞书妙记转文章并发布.md @@ -0,0 +1,84 @@ +# 飞书妙记 → 第9章文章 → 飞书群 + 小程序 + +> 把飞书妙记链接丢给执行人时,用下面这一句 + 本页步骤即可。 + +--- + +## 一句话指令(复制给对方) + +``` +请帮我: +1. 打开这个飞书妙记,把里面的**文本**和**视频**都下载下来。 +2. 根据文本内容,按《一场soul的创业实验》第9章写作规范,写成一篇「第 X 场|标题」的文章(和 112 场同风格)。 +3. 文章写完后:发布到「一场创业实验」飞书群(Soul 彩民团队),并发布到「一场创业实验」小程序。 + +飞书妙记链接:https://cunkebao.feishu.cn/minutes/obcn5muad6wexv91g65v1883 +``` + +--- + +## 执行步骤(给执行人用) + +### 第一步:下载妙记的文本和视频 + +- 打开链接:https://cunkebao.feishu.cn/minutes/obcn5muad6wexv91g65v1883 +- 在妙记页面里:导出/复制**全文文本**,并下载**视频**(如有)。文本用于写文章,视频可留作素材或切片。 + +### 第二步:按规范写文章 + +- **写作规范**:必须先读 `Soul创业实验/写作/写作规范.md`(卡若AI 或永平项目里可查)。 +- **要点**:人称「我」整篇最多 3 次;每句空一行;大白话;数值与场景写具体;约 20% 处和结尾各一句分享句(≤50 字,不用「干货」二字);涉及 Soul 投流/派对价值时写清具体数值(75 曝光进 1 人、3 万曝光/天等)。 +- **输出**:保存为第9章下的一篇 md,例如 `第113场|本场主题.md`,放在书稿目录: + `《一场soul的创业实验》/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/` + 章节 id 与 112 场类似,用 9.25、9.26 等(与现有不重复即可)。 + +### 第三步:发布到飞书群 + 小程序 + +在**永平项目**根目录执行下面三件事(顺序不要改): + +**1)上传到小程序** + +```bash +cd "/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平" +python3 content_upload.py --id 9.xx --title "第X场|标题" \ + --content-file "<刚写好的文章.md 完整路径>" \ + --part part-4 --chapter chapter-9 --price 1.0 +``` + +(把 `9.xx` 和 `第X场|标题`、`<刚写好的文章.md 完整路径>` 换成实际值。) + +**2)推送到飞书群(Soul 彩民团队)** + +```bash +python3 scripts/send_chapter_poster_to_feishu.py 9.xx "第X场|标题" --md "<同一篇文章.md 的完整路径>" +``` + +- 会先发「标题 + 文章前 6% 正文(一句一行、行间空一行)」,再发章节海报图(含该章节小程序码),不发小程序链接。 +- 默认已发到 Soul 彩民团队飞书群,无需再填 webhook。 + +**3)可选:同步到飞书知识库** + +若需要这篇文章也出现在「创业实验」飞书知识库里,在永平项目执行: + +```bash +python3 scripts/feishu_wiki_upload.py --full +``` + +(会按书稿目录同步全书;若只加单篇,需在脚本中增加对应 `--only 113场` 等逻辑。) + +--- + +## 相关文档 + +| 文档 | 说明 | +|:---|:---| +| `Soul创业实验/写作/写作规范.md` | 写作唯一规范(人称、数值、分享句、格式等) | +| `Soul创业实验/上传/README.md` | 上传命令与路径说明 | +| `Soul创业实验/上传/推送逻辑.md` | 飞书群推送规则(6% 正文 + 海报图、不发链接) | + +--- + +## 当前妙记链接(可替换) + +- 本次任务链接:https://cunkebao.feishu.cn/minutes/obcn5muad6wexv91g65v1883 +- 以后换别的妙记,只需把「一句话指令」里的链接换成新链接,步骤不变。 diff --git a/02_卡人(水)/水桥_平台对接/Soul创业实验/写作/写作规范.md b/02_卡人(水)/水桥_平台对接/Soul创业实验/写作/写作规范.md index 530b3144..6a8d86ba 100644 --- a/02_卡人(水)/水桥_平台对接/Soul创业实验/写作/写作规范.md +++ b/02_卡人(水)/水桥_平台对接/Soul创业实验/写作/写作规范.md @@ -8,7 +8,7 @@ | 项目 | 规范 | |:---|:---| -| **「我」** | **整篇文章最多出现一次**。建议不用或仅一处;用「这边」「直接」「就」等替代。成稿后全文搜索「我」,超过 1 处必须改写。 | +| **「我」** | **整篇文章最多出现三次**。多用「这边」「直接」「就」「场上」等替代;成稿后全文搜索「我」,超过 3 处必须改写。 | | **「卡若」** | 每篇最多提一次;不需要时可完全不出现。 | --- @@ -19,7 +19,8 @@ |:---|:---| | 字数 | 每小节 2000~5000 字 | | 来源 | 以真实聊天内容为基础,不改原意 | -| 数据 | 以聊天内提到的数值为主,不编造(场观、人数、时长、营收等) | +| 数据 | 以聊天内提到的数值为主,不编造(场观、人数、时长、营收等);**有数值就必须写具体**,不写「大概很多」「不少」等模糊说法 | +| **数值与场景** | **数值跟场景要具体写进去**:金额、人数、时长、比例、曝光量、进房数、成本、获客价等,一律写清具体数字;涉及 Soul 投流、派对价值时,要写清算法口径(如:约 75~80 曝光进 1 人、每天约 3 万曝光、进房 300~600 人、1000 曝光 6~10 块、进一人约 4 毛 2、获客到微信约 20 块/人等) | | 配图 | 不需要 | --- @@ -27,7 +28,7 @@ ## 三、主题与结构 - **一个观点**:每节只表达一个核心观点,主题清晰 -- **有数据**:用具体数值验证(场观、人数、时长、营收等) +- **有数据**:用具体数值验证(场观、人数、时长、营收等);**数值与场景必须具体**,不写笼统表述,有提到的算法、成本、比例都要带具体数字写进去(如 Soul 推流 75 曝光进 1 人、3 万曝光/天、投流 1000 曝光 6~10 块等) - **与目录一致**:小节名称、内容与全书结构、其他小节风格统一 - **时间**:以文档/聊天记录时间为准 @@ -47,14 +48,15 @@ - **分段**:每段一个主题,小主题隐于叙述中,不列段头小标题 - **穿插**:细节、对话、观点分析 - **多用对话**:增强真实感(「X 号问」「有人问」「直接回答」「这边说」等) +- **分享句(两处,强制)**:约 20% 处一句、结尾一句,各不超过 50 字,围绕本节主题、紧扣内容,留余味或可执行。**不要出现「干货」二字**,不要用「干货:」或「**干货**:」等格式,直接写一句金句即可,可单独成段。 --- ## 六、写作技巧 -- 第一人称叙述时少用「我」,见第一节 -- 短句,大白话,口语化 -- 每句话后空一行,段间空行 +- 第一人称叙述时少用「我」,见第一节(整篇最多三次) +- **短句,大白话,口语化**:像平时说话一样写,不书面、不拽词 +- **每句空一行**:一句写完就换行、再空一行再写下一句,不要好几句挤成一段 - 善用对比、反转;适当自嘲或幽默 --- @@ -84,7 +86,9 @@ ## 十、格式规范(统一) -- 每句话后空一行 -- 段间空行 +- **每句空一行**:每一句话后面换行 + 空一行,再写下一句;不要多句挤在一段 +- 段与段之间空行 - 对话、细节、观点分行,避免大段堆砌 - 用 `---` 做段落分隔(与全书一致) +- **分享句**:全文约 20% 处一句(≤50 字)、结尾一句(≤50 字、围绕主题);**不用「干货」二字及「干货:」等格式**,直接一句金句 +- 写作与改写第9章文章时,**必须先读本规范**;以后写 Soul 派对场次文章都用这套 diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/卡猫复盘/SKILL.md b/02_卡人(水)/水桥_平台对接/飞书管理/卡猫复盘/SKILL.md new file mode 100644 index 00000000..5e19eedf --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/飞书管理/卡猫复盘/SKILL.md @@ -0,0 +1,79 @@ +--- +name: 卡猫复盘 +description: 以婼瑄目录为唯一素材的卡猫复盘,目标·结果=今年总目标+完成%,人/事/数具体,输出飞书+卡猫群 +triggers: 卡猫复盘、婼瑄复盘、卡猫今日复盘、婼瑄今日、复盘到卡猫、发卡猫群 +owner: 水桥 +group: 水 +version: "1.0" +updated: "2026-02-28" +--- + +# 卡猫复盘 Skill + +> **唯一素材**:婼瑄目录。**目标·结果**:卡猫今年总目标 + 完成百分比。**人/事/数**:必须具体。 + +--- + +## 一、格式规范(强制) + +执行前必读:**`运营中枢/参考资料/卡猫复盘格式_固定规则.md`** + +要点: +- **🎯 目标·结果·达成率** = **卡猫当年总目标**(一句或 1~3 句,每句 ≤30 字)+ **当前达成情况**(一句)+ **完成百分比 XX%**。不得写「本次任务目标」。 +- **人**:写真实称呼(叶倩如、郑朝阳、郑清土、财务、腾讯侧等),不写「某人」。 +- **事**:写具体事件(如「2 月 28 日叶倩如侧聊流量开票与下游消化」),不写「讨论了问题」。 +- **数**:能写则写(如 10 亿流水/票、15%、6 点专票、20% 换 500 万、端口 18789),不写「很多」。 + +--- + +## 二、素材目录与文件选择 + +| 项目 | 规定 | +|:---|:---| +| **素材目录** | **仅婼瑄**:`/Users/karuo/Library/Mobile Documents/com~apple~CloudDocs/Documents/婼瑄`(含所有子目录) | +| **文件选择** | 当次复盘 = 该目录下**当日或用户指定日期**新增/修改的 txt、md 等;或用户指定文件/日期范围 | +| **输出目录** | 复盘 MD 存 **婼瑄/复盘/**,文件名 `YYYY-MM-DD_卡猫复盘_主题.md` | + +--- + +## 三、执行步骤 + +1. **读格式**:读 `运营中枢/参考资料/卡猫复盘格式_固定规则.md`。 +2. **取当年总目标**:从 记忆.md 或婼瑄目录内已有复盘/文档中提取「卡猫 [年份] 年总目标」;若无则写「待补充」并注明完成约 X%。 +3. **选文件**:在婼瑄目录下按日期或用户指定,列出本次依据的 txt/md 文件。 +4. **读内容**:读取上述文件,提取**具体的人、具体的事、具体的数字**。 +5. **写复盘**:按卡猫复盘格式五块(🎯📌💡📝▶)撰写,目标·结果=今年总目标+完成%,过程/反思/总结中人/事/数具体。 +6. **落盘**:保存到 `婼瑄/复盘/YYYY-MM-DD_卡猫复盘_主题.md`。 +7. **发布**:用飞书统一发布脚本上传到卡猫知识库节点,并推送卡猫群 webhook。 + +--- + +## 四、飞书与群配置 + +| 项目 | 值 | +|:---|:---| +| 飞书 Wiki 父节点 | `KCVWwPfJvi1d6ZkTfMCcCkOYnVc`(卡猫知识库) | +| 卡猫群 webhook | `https://open.feishu.cn/open-apis/bot/v2/hook/4458f8d6-4bb5-492d-8ca2-e5cf06c0bd22` | +| 发布脚本 | `02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_article_unified_publish.py` | + +发布命令示例: + +```bash +python3 卡若AI/02_卡人(水)/水桥_平台对接/飞书管理/脚本/feishu_article_unified_publish.py \ + --parent KCVWwPfJvi1d6ZkTfMCcCkOYnVc \ + --title "YYYY-MM-DD 卡猫复盘:主题" \ + --md "婼瑄/复盘/YYYY-MM-DD_卡猫复盘_主题.md" \ + --json "婼瑄/复盘/YYYY-MM-DD_卡猫复盘_主题_feishu_blocks.json" \ + --webhook "https://open.feishu.cn/open-apis/bot/v2/hook/4458f8d6-4bb5-492d-8ca2-e5cf06c0bd22" +``` + +--- + +## 五、与通用复盘的区别 + +| 项目 | 通用卡若复盘 | 卡猫复盘 | +|:---|:---|:---| +| 目标·结果 | 当次任务目标+结果+达成率 | **卡猫今年总目标**+当前达成+**完成%** | +| 人/事/数 | 无强制 | **必须具体**(真人、具体事、具体数) | +| 素材来源 | 任意 | **仅婼瑄目录** | +| 输出 | 对话内复盘块 | 婼瑄/复盘/*.md + 飞书 + 卡猫群 | diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json index fb8fe9ce..c86ee980 100644 --- a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/.feishu_tokens.json @@ -1,6 +1,6 @@ { - "access_token": "u-4NFObvRHB9baG_6zHLv28Al5kgW5k1ipp8aaZww00BOn", - "refresh_token": "ur-7XjMc1PjB1NoflqWI30rQbl5kqMBk1UhVEaaUwQ00wD6", + "access_token": "u-7VelB5t1J6xrwEh5fd0Ftwl5mWoBk1iPMUaaYRw00BPi", + "refresh_token": "ur-6MS.YzNGd5KoE3SUs_5r3ul5kqU5k1WrWEaaEBM00wSj", "name": "飞书用户", - "auth_time": "2026-02-27T16:16:01.288755" + "auth_time": "2026-02-28T19:27:39.322083" } \ No newline at end of file diff --git a/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_feishu_log.py b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_feishu_log.py new file mode 100644 index 00000000..6e93867b --- /dev/null +++ b/02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_feishu_log.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +今日飞书日志:从聊天记录+今日文档统一整理,与本月/最终目标百分比,今日核心一条 +- 今日核心目标:每天20条Soul视频 + 20:00发1条朋友圈 +""" +import os +import sys +from datetime import datetime + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +if SCRIPT_DIR not in sys.path: + sys.path.insert(0, SCRIPT_DIR) + +from auto_log import get_token_silent, write_log, open_result, resolve_wiki_token_for_date + + +def build_tasks_today(): + """今日任务:昨日完成度+本月未完成并入+本月/最终目标%+今日核心(20条Soul+8点朋友圈)""" + today = datetime.now() + date_str = f"{today.month}月{today.day}日" + # 昨日2月27日完成度(与 write_today_0227 / 本月其他日对齐) + yesterday_done = "昨日2月27日:一人公司5%、玩值电竞25%、飞书日志100%" + # 本月未完成项(看板 T003/T004/T005 + 今日核心),并入今日 + month_unfinished = "本月未完成并入今日:一人公司、玩值电竞、卡若AI 4项优化、20条Soul+8点朋友圈" + # 本月与最终目标 + month_goal_pct = 12 + gap_pct = 88 + return [ + { + "person": "卡若", + "events": ["昨日完成度", "本月未完成并入", "今日核心"], + "quadrant": "重要紧急", + "t_targets": [ + yesterday_done, + month_unfinished, + f"本月目标约 {month_goal_pct}%,距最终目标差 {gap_pct}%", + "今日核心:每天20条Soul视频 + 20:00发1条朋友圈", + ], + "n_process": [ + "【昨日】2月27日完成度已更新至上;本月其他日未完成项一并写入", + "【复盘】从聊天记录与今日文档统一整理", + "【2月突破执行】未完成项并入今日,持续迭代至100%", + ], + "t_thoughts": [ + "昨日与本月完成度、未完成项均更新到当日日志;今日核心 20条Soul+8点朋友圈", + ], + "w_work": ["一人公司", "玩值电竞", "卡若AI优化", "20条Soul视频", "20:00发1条朋友圈", "飞书日志"], + "f_feedback": [ + "昨日完成度已写入 ✅", + "本月未完成已并入今日 🔄", + f"本月/最终 {month_goal_pct}% / 100%,差 {gap_pct}%", + "今日核心→20条Soul+8点朋友圈 🔄", + ], + } + ] + + +def main(): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("--overwrite", action="store_true", help="覆盖已有当日日志") + args = parser.parse_args() + + today = datetime.now() + date_str = f"{today.month}月{today.day}日" + print("=" * 50) + print(f"📝 写入今日飞书日志:{date_str}" + (" [覆盖]" if args.overwrite else "")) + print("=" * 50) + + token = get_token_silent() + if not token: + print("❌ 无法获取飞书 Token") + sys.exit(1) + + tasks = build_tasks_today() + target_wiki_token = resolve_wiki_token_for_date(date_str) + ok = write_log(token, date_str, tasks, target_wiki_token, overwrite=getattr(args, "overwrite", False)) + if ok: + open_result(target_wiki_token) + print(f"✅ {date_str} 飞书日志已写入") + sys.exit(0) + print("❌ 写入失败") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/02_卡人(水)/水溪_整理归档/项目调研/SKILL.md b/02_卡人(水)/水溪_整理归档/项目调研/SKILL.md new file mode 100644 index 00000000..2ecbf333 --- /dev/null +++ b/02_卡人(水)/水溪_整理归档/项目调研/SKILL.md @@ -0,0 +1,59 @@ +--- +name: 项目调研 +description: 平台分析、项目调研、各 APP 资料、A 群等聊天记录与对话分类统一归档到 开发/7.项目调研,按项目分子目录。 +triggers: 项目调研、平台分析、A群、A群聊天记录、聊天记录清理、对话分类、对话分类号、按项目归档、调研归档、APP资料、其他APP、各APP +owner: 水溪 +group: 水 +--- + +# 项目调研 + +将**平台分析**、**项目调研**、各 **APP 资料**、**A 群等群聊记录**、**对话分类**(含对话分类号)及**其他相关资料**,统一收纳到 `开发/7.项目调研`,**按项目分子目录**。凡与某 APP 或某项目相关的调研、聊天记录、对话分类,均放入对应项目子目录。 + +--- + +## 根目录(固定) + +| 路径 | 说明 | +|------|------| +| **根目录** | `/Users/karuo/Documents/开发/7.项目调研` | +| 规范说明 | `7.项目调研/README.md` | + +--- + +## 执行规则 + +1. **识别意图**:用户提到「项目调研」「平台分析」「把 XX 聊天记录/对话清理到这里」「A 群」「按项目归档」「对话分类」等 → 使用本 Skill,产出与归档一律进入 `7.项目调研`。 +2. **按项目归档**: + - 若已有对应项目子目录 → 放入该项目下的 `平台分析/`、`聊天记录/`、`对话分类/` 或 `资料/`。 + - 若无对应项目 → 先放入 `_待归档/`,或在用户指定项目名后新建 `项目名/` 及四类子目录。 +3. **目录结构**(与根目录 README 一致): + - 每个项目下:`平台分析/`、`聊天记录/`、`对话分类/`、`资料/`。 + - 群聊记录、对话分类可再按群名、日期或主题分子目录。 + +--- + +## 流程摘要 + +| 步骤 | 动作 | +|------|------| +| 1 | 确认内容类型:平台分析 / 聊天记录 / 对话分类 / 资料 | +| 2 | 确认或新建项目子目录(无则用 `_待归档` 或新建项目名) | +| 3 | 写入对应子目录,命名清晰(含日期或主题) | +| 4 | 必要时更新 `7.项目调研/README.md` 的「当前项目列表」 | + +--- + +## 常用路径速查 + +- 根目录:`/Users/karuo/Documents/开发/7.项目调研` +- 待归档:`7.项目调研/_待归档/` +- 项目 X 的聊天记录:`7.项目调研/项目X/聊天记录/` +- 项目 X 的平台分析:`7.项目调研/项目X/平台分析/` + +--- + +## 相关 + +- 对话归档(日常 AI 对话):`水溪_整理归档/对话归档/SKILL.md`,路径为 `个人/3、工作台/AI的每日对话/`,与本文不同。 +- 本 Skill 仅负责「项目调研」相关:平台分析、项目调研、群聊与对话分类、APP 资料,统一进 `开发/7.项目调研` 并按项目存放。 diff --git a/03_卡木(木)/木叶_视频内容/抖音发布/SKILL.md b/03_卡木(木)/木叶_视频内容/抖音发布/SKILL.md new file mode 100644 index 00000000..939b4baa --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/抖音发布/SKILL.md @@ -0,0 +1,87 @@ +--- +name: 抖音发布 +description: 通过抖音开放平台实现抖音登录(OAuth)与视频发布。与 Soul 竖屏成片、视频切片联动,将成片一键发布到抖音。若使用存客宝/腕推等矩阵工具发布抖音,可在此 Skill 补充对接方式。 +triggers: 抖音发布、发布到抖音、抖音登录、抖音上传、腕推抖音 +owner: 木叶 +group: 木 +version: "1.0" +updated: "2026-02-28" +--- + +# 抖音发布 + +> **登录与发布**:使用**抖音开放平台** OAuth 获取用户授权(access_token、open_id),再调用「上传视频」+「创建视频」接口发布。 +> **存客宝**:当前存客宝 Skill 与文档中无抖音发布 SDK;若后续存客宝或腕推等工具提供抖音发布能力,可在此 Skill 补充脚本与配置。 + +--- + +## 一、流程概览 + +``` +抖音开放平台应用(申请「代替用户发布内容到抖音」) + → 用户 OAuth 登录授权 → 获得 access_token、open_id + → 上传视频(upload_video)→ 获得 video_id + → 创建视频(create_video)→ 发布到抖音 +``` + +--- + +## 二、前置条件 + +1. **抖音开放平台**: [https://partner.open-douyin.com](https://partner.open-douyin.com) 注册开发者、创建应用。 +2. **能力申请**:应用详情 → 能力管理 → 能力实验室 → **代替用户发布内容到抖音**。 +3. **用户授权**:用户通过 OAuth 授权后,获得 `access_token`(约 15 天)、`open_id`;可存于本地或存客宝等系统供脚本使用。 + +--- + +## 三、一键命令(发布单条成片) + +```bash +cd /Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/抖音发布/脚本 + +# 使用已保存的 token 发布(需先跑一次登录并保存) +python3 douyin_publish.py --video "/path/to/成片/标题.mp4" --title "视频标题 #话题" + +# 指定 token 文件(默认读 脚本/.env 或 config.json 中的 access_token、open_id) +python3 douyin_publish.py --video "/path/to/xxx.mp4" --title "标题" --token-file ./tokens.json +``` + +--- + +## 四、登录形式(获取 access_token / open_id) + +抖音开放平台采用 **OAuth 2.0**: + +1. **授权码模式**:引导用户打开授权页 → 用户同意后回调带 `code` → 用 `code` 换 `access_token`、`open_id`。 +2. **脚本用法**:首次需在浏览器完成授权,或使用开放平台「获取 access_token」接口(需 client_key、client_secret、code)。将得到的 `access_token`、`open_id` 写入 `脚本/tokens.json` 或环境变量,供 `douyin_publish.py` 读取。 + +详见:`参考资料/抖音开放平台_登录与发布流程.md`。 + +--- + +## 五、与视频切片 / Soul 竖屏的联动 + +- **成片目录**:Soul 竖屏成片输出在 `xxx_output/成片/`,文件名为标题(如 `没人来就一个人站站到最后钱才来.mp4`)。 +- **批量发布**:可对 `成片/` 目录遍历,逐条调用 `douyin_publish.py --video --title <文件名或 highlights 标题>`;标题可来自 `成片/目录索引.md` 或 `highlights.json`。 +- **腕推 / 存客宝**:若使用腕推或存客宝的抖音发布能力,可将对接方式(API 文档、SDK 路径)补充到本 Skill 的「参考资料」或脚本说明中,脚本可改为调其接口。 + +--- + +## 六、相关文件 + +| 文件 | 说明 | +|------|------| +| `脚本/douyin_publish.py` | 发布脚本:读 token → 上传视频 → 创建视频 | +| `参考资料/抖音开放平台_登录与发布流程.md` | 开放平台 OAuth、上传、创建视频接口说明与链接 | + +--- + +## 七、API 摘要(抖音开放平台) + +| 步骤 | 接口 | 说明 | +|------|------|------| +| 登录 | OAuth 授权 → access_token、open_id | 用户授权后获得 | +| 上传 | POST `/api/douyin/v1/video/upload_video/` | form: video=@文件;返回加密 video_id | +| 发布 | POST `/api/douyin/v1/video/create_video/` | body: video_id、text(标题,可带话题) | + +视频时长不超过 15 分钟;标题不超过 1000 字;每日发布上限 75 条(同一应用下)。 diff --git a/03_卡木(木)/木叶_视频内容/抖音发布/参考资料/抖音开放平台_登录与发布流程.md b/03_卡木(木)/木叶_视频内容/抖音发布/参考资料/抖音开放平台_登录与发布流程.md new file mode 100644 index 00000000..aae893b0 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/抖音发布/参考资料/抖音开放平台_登录与发布流程.md @@ -0,0 +1,46 @@ +# 抖音开放平台:登录与发布流程 + +> 用于「抖音发布」Skill:获取抖音登录态(access_token、open_id)并发布视频。 + +## 一、登录(OAuth 获取 access_token / open_id) + +1. **开放平台**:https://partner.open-douyin.com + 创建应用,获取 `client_key`、`client_secret`。 + +2. **能力申请**:应用详情 → 能力管理 → 能力实验室 → **代替用户发布内容到抖音**。 + +3. **用户授权**: + - 授权页:`https://open.douyin.com/platform/oauth/connect/?client_key={client_key}&response_type=code&scope=user_info,video.create&redirect_uri={redirect_uri}&state={state}` + - 用户同意后跳转 `redirect_uri?code=xxx&state=xxx`。 + +4. **用 code 换 token**: + POST `https://open.douyin.com/oauth/access_token/` + 参数:`client_key`、`client_secret`、`code`、`grant_type=authorization_code`。 + 返回:`access_token`、`open_id`、`expires_in` 等。 + 文档:https://partner.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/account-permission/get-access-token + +5. **保存**:将 `access_token`、`open_id` 写入 `脚本/tokens.json` 或环境变量,供发布脚本使用。 + access_token 有效期约 15 天,过期需重新授权或刷新。 + +## 二、上传视频 + +- **接口**:POST `https://open.douyin.com/api/douyin/v1/video/upload_video/?open_id={open_id}` +- **请求头**:`access-token: {access_token}`,`Content-Type: multipart/form-data` +- **Body**:form 字段 `video` = 视频文件(本地文件) +- **返回**:`data.video.video_id`(加密 ID,用于创建视频) + +文档:https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/video-management/douyin/create-video/upload-video + +## 三、创建视频(发布) + +- **接口**:POST `https://open.douyin.com/api/douyin/v1/video/create_video/?open_id={open_id}` +- **请求头**:`access-token: {access_token}`,`Content-Type: application/json` +- **Body**:`{"video_id": "{上一步的 video_id}", "text": "标题 #话题1 #话题2"}` +- **限制**:视频时长 ≤15 分钟;标题 ≤1000 字;同一应用下每日 ≤75 条 + +文档:https://partner.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/video-management/douyin/create-video/video-create + +## 四、存客宝 / 腕推 + +- 当前**存客宝** Skill 与文档中无抖音登录或发布 SDK。 +- 若使用**腕推**或其它矩阵工具发布抖音,请将对接方式(API 或 SDK 文档路径)补充到「抖音发布」Skill 或本参考资料,脚本可改为调用对应接口。 diff --git a/03_卡木(木)/木叶_视频内容/抖音发布/脚本/douyin_publish.py b/03_卡木(木)/木叶_视频内容/抖音发布/脚本/douyin_publish.py new file mode 100644 index 00000000..068ae7bc --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/抖音发布/脚本/douyin_publish.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +抖音发布:使用抖音开放平台 API 上传视频并发布。 +依赖:用户已完成 OAuth 授权,access_token、open_id 存于环境变量或 tokens.json。 +与「抖音发布」Skill 配套;存客宝/腕推若提供抖音发布接口可替换本脚本内的 API 调用。 +""" + +import argparse +import json +import os +import sys +from pathlib import Path + +try: + import requests +except ImportError: + print("请安装 requests: pip install requests", file=sys.stderr) + sys.exit(1) + +# 抖音开放平台 API 基址 +BASE = "https://open.douyin.com" +UPLOAD_URL = f"{BASE}/api/douyin/v1/video/upload_video/" +CREATE_URL = f"{BASE}/api/douyin/v1/video/create_video/" + + +def load_tokens(token_file: Path | None) -> tuple[str, str]: + """从环境变量或 token 文件读取 access_token、open_id。""" + token = os.environ.get("DOUYIN_ACCESS_TOKEN") or os.environ.get("ACCESS_TOKEN") + open_id = os.environ.get("DOUYIN_OPEN_ID") or os.environ.get("OPEN_ID") + if token_file and token_file.exists(): + with open(token_file, "r", encoding="utf-8") as f: + data = json.load(f) + token = token or data.get("access_token") + open_id = open_id or data.get("open_id") + if not token or not open_id: + print( + "未配置 access_token / open_id。请先完成抖音 OAuth 登录,将 access_token、open_id 写入 tokens.json 或设置环境变量 DOUYIN_ACCESS_TOKEN、DOUYIN_OPEN_ID。", + file=sys.stderr, + ) + print("参见:参考资料/抖音开放平台_登录与发布流程.md", file=sys.stderr) + sys.exit(1) + return token.strip(), open_id.strip() + + +def upload_video(access_token: str, open_id: str, video_path: str) -> str: + """上传视频,返回加密 video_id。""" + path = Path(video_path) + if not path.exists() or not path.is_file(): + raise FileNotFoundError(f"视频文件不存在: {video_path}") + url = f"{UPLOAD_URL}?open_id={open_id}" + headers = {"access-token": access_token} + with open(path, "rb") as f: + files = {"video": (path.name, f, "video/mp4")} + r = requests.post(url, headers=headers, files=files, timeout=120) + r.raise_for_status() + data = r.json() + if data.get("extra", {}).get("error_code") != 0: + raise RuntimeError(data.get("extra", {}).get("description", "上传失败")) + video_id = data.get("data", {}).get("video", {}).get("video_id") + if not video_id: + raise RuntimeError("响应中无 video_id") + return video_id + + +def create_video(access_token: str, open_id: str, video_id: str, text: str) -> dict: + """创建视频(发布)。""" + url = f"{CREATE_URL}?open_id={open_id}" + headers = {"access-token": access_token, "Content-Type": "application/json"} + body = {"video_id": video_id, "text": text[:1000]} + r = requests.post(url, headers=headers, json=body, timeout=30) + r.raise_for_status() + data = r.json() + if data.get("extra", {}).get("error_code") != 0: + raise RuntimeError(data.get("extra", {}).get("description", "发布失败")) + return data.get("data", {}) + + +def main(): + parser = argparse.ArgumentParser(description="抖音发布:上传视频并发布到抖音(开放平台 API)") + parser.add_argument("--video", "-v", required=True, help="本地视频路径(竖屏成片)") + parser.add_argument("--title", "-t", required=True, help="发布标题,可带 #话题") + parser.add_argument("--token-file", "-f", type=Path, default=Path(__file__).parent / "tokens.json", help="存 access_token、open_id 的 JSON 文件") + args = parser.parse_args() + + access_token, open_id = load_tokens(args.token_file) + print("上传视频...") + video_id = upload_video(access_token, open_id, args.video) + print("发布中...") + result = create_video(access_token, open_id, video_id, args.title) + print("发布成功:", result.get("item_id", "OK")) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/03_卡木(木)/木叶_视频内容/抖音发布/脚本/tokens.json.example b/03_卡木(木)/木叶_视频内容/抖音发布/脚本/tokens.json.example new file mode 100644 index 00000000..1626363b --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/抖音发布/脚本/tokens.json.example @@ -0,0 +1,4 @@ +{ + "access_token": "抖音开放平台 OAuth 获取的 access_token", + "open_id": "用户 open_id" +} diff --git a/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md b/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md index 2e1638fc..dde03097 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md +++ b/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md @@ -19,11 +19,17 @@ updated: "2026-02-17" ## ⭐ Soul派对切片流程(默认) ``` -原始视频 → MLX转录 → 字幕转简体 → 高光识别(Ollama→规则) → 批量切片 → soul_enhance → 输出成片 +原始视频 → MLX转录 → 字幕转简体 → 高光识别(当前模型/AI) → 批量切片 → soul_enhance → 输出成片 ↑ ↓ 提取后立即繁转简+修正错误 封面+字幕(已简体)+加速10%+去语气词 ``` +**切片时长**:每段为**完整的一个片段**,时长 **30 秒~300 秒**,由该完整片段起止时间决定。**标题**用一句**刺激性观点**(见 `Soul竖屏切片_SKILL.md`)。 + +**提问→回答 结构**:若片段内有人提问,前3秒优先展示**提问问题**,再播回答;高光识别填 `question` 且 `hook_3sec` 与之一致,成片整条去语助词。详见 `参考资料/视频结构_提问回答与高光.md`、`参考资料/高光识别提示词.md`。 + +**Soul 竖屏专用**:抖音/首页用竖屏成片、完整参数与流程见 → **`Soul竖屏切片_SKILL.md`**(竖屏 498×1080、crop 参数、批量命令)。 + ### 一键命令(Soul派对专用) #### 一体化流水线(推荐) @@ -57,6 +63,46 @@ python3 batch_clip.py -i 视频.mp4 -l highlights.json -o clips/ -p soul python3 soul_enhance.py -c clips/ -l highlights.json -t transcript.srt -o clips_enhanced/ ``` +#### 按章节主题提取(推荐:第9章单场成片) + +以**章节 .md 正文**为来源提取核心主题,再在转录稿中匹配时间,不限于 5 分钟、片段数与章节结构一致。详见 `参考资料/主题片段提取规则.md`。 + +```bash +# 从章节生成 highlights,再走 batch_clip + soul_enhance +python3 chapter_themes_to_highlights.py -c "第112场.md" -t transcript.srt -o highlights_from_chapter.json +python3 batch_clip.py -i 视频.mp4 -l highlights_from_chapter.json -o clips/ -p soul112 +python3 soul_enhance.py -c clips/ -l highlights_from_chapter.json -t transcript.srt -o clips_enhanced/ +``` + +- **主题来源**:章节 .md 按 `---` 分块,每块一个主题;文件名由 batch_clip 按 `前缀_序号_标题` 生成(标题仅保留中文与安全字符)。 + +### Soul 竖屏成片(横版源 → 竖屏中段去白边) + +**约定**:以后剪辑 Soul 视频,成片统一做「竖屏中段」裁剪:横版 1920×1080 只保留中间竖条并去掉左右白边,输出 498×1080 竖屏。 + +| 步骤 | 说明 | +|------|------| +| 源 | 横版 1920×1080(soul_enhance 输出) | +| 1 | 取竖条 608×1080,起点 **x=483**(相对画面左) | +| 2 | 裁掉左侧白边 60px、右侧白边 50px → 内容区宽 498 | +| 输出 | **498×1080** 竖屏,仅内容窗口 | + +**FFmpeg 一条命令(固定参数):** + +```bash +# 单文件。输入为 1920×1080 的 enhanced 成片 +ffmpeg -y -i "输入_enhanced.mp4" -vf "crop=608:1080:483:0,crop=498:1080:60:0" -c:a copy "输出_竖屏中段.mp4" +``` + +**批量对某目录下所有 \*_enhanced.mp4 做竖屏中段:** + +```bash +# 脚本目录下执行,或直接调用 +python3 脚本/soul_vertical_crop.py --dir "/path/to/clips_enhanced" --suffix "_竖屏中段" +``` + +参数说明见:`参考资料/竖屏中段裁剪参数说明.md`。 + ### 增强功能说明 | 功能 | 说明 | @@ -213,6 +259,8 @@ 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 去白边) | ⭐⭐⭐ | +| chapter_themes_to_highlights.py | 按章节 .md 主题提取片段(本地模型→highlights.json) | ⭐⭐⭐ | | identify_highlights.py | 高光识别(Ollama→规则) | ⭐⭐ | | batch_clip.py | 批量切片 | ⭐⭐ | | one_video.py | 单视频一键成片 | ⭐⭐ | diff --git a/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md b/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md new file mode 100644 index 00000000..0fb1f154 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md @@ -0,0 +1,108 @@ +# Soul 竖屏切片 · 专用 Skill + +> 专门切 Soul 派对视频为**竖屏成片**,用于抖音/首页。**只保留两个文件夹**:剪辑 → 成片。 + +--- + +## 一、两文件夹结构(无 clips_enhanced / clips_竖屏) + +| 文件夹 | 含义 | 内容 | +|--------|------|------| +| **clips/** | 剪辑 | batch_clip 输出的横版切片(soul112_01_标题.mp4) | +| **成片/** | 成片 | 竖屏 498×1080 + 封面 + 字幕 + 去语助词,文件名为**纯标题**(无序号、无 _enhanced) | + +不再单独生成 `clips_enhanced`、`clips_竖屏`;成片由 `soul_enhance` 一步直出到 `成片/`。 + +--- + +## 二、视频结构:提问→回答 + 前3秒高光 + 去语助词 + +- **前3秒**:先看片段有没有人提问;**有提问**则把**提问的问题**放到前3秒(封面/前贴),先展示问题再播回答;无提问则用金句/悬念作 hook。 +- **成片链路**:前3秒展示问题(或金句)→ 正片回答 → **整片去除语助词**(提问与回答部分均由 soul_enhance 清理)。 +- **高光**:按「3秒高光亮点」剪,每段 30~300 秒完整语义单元;高光识别若有提问须填 `question`,且 `hook_3sec` 与之一致。 + +详见:`参考资料/视频结构_提问回答与高光.md`、`参考资料/高光识别提示词.md`。 + +--- + +## 三、流程总览 + +``` +原视频 → 转录(MLX) → 高光识别(含 question/hook_3sec,见高光识别提示词) → batch_clip → soul_enhance(成片竖屏直出到 成片/) +``` + +- **batch_clip**:输出到 `clips/` +- **soul_enhance -o 成片/ --vertical --title-only**:封面(优先用 question 作前3秒)+ 字幕 + **完整去语助词** + 竖屏裁剪,直接输出到 `成片/`,文件名为标题 + +--- + +## 四、高光与切片(30 秒~300 秒) + +| 项 | 规则 | +|----|------| +| **单段时长** | **30~300 秒**,由完整片段起止决定 | +| **完整性** | 每段是一个完整话题/情节,有头有尾 | +| **标题** | **一句刺激性观点**(金句、反常识、结论句) | +| **数量** | 建议 ≤10 段/场 | +| **语助词** | 识别与剪辑须符合 `参考资料/高光识别提示词.md`,成片由 soul_enhance 统一去语助词 | + +--- + +## 五、成片:封面 + 字幕 + 竖屏 + +- **封面**:竖屏 498×1080 内**不超出界面**;深色渐变背景(墨绿→绿)、左上角 Soul logo、标题文字**严格居中**且左右留白 44px,多行自动换行不裁切。 +- **字幕**:封面结束后才显示,**居中**在竖屏内;语助词由 soul_enhance 统一清理。重新加字幕时加 `--force-burn-subs`。 +- **竖屏**:498×1080,crop 参数与 `参考资料/竖屏中段裁剪参数说明.md` 一致 + +--- + +## 六、竖屏裁剪参数(成片内嵌) + +| 步骤 | 滤镜 | +|------|------| +| 1 | crop=608:1080:483:0 | +| 2 | crop=498:1080:60:0 | + +**输出**:498×1080 竖屏。 + +--- + +## 七、完整命令示例(112 场) + +**1. 高光**(当前模型生成 highlights.json,标题用刺激性观点,30~300 秒完整段;语助词与节奏感见 `参考资料/高光识别提示词.md`) + +**2. 剪辑(clips)** +```bash +python3 batch_clip.py -i "原视频.mp4" -l highlights.json -o clips/ -p soul112 +``` + +**3. 成片(竖屏+封面+字幕+去语助词,直出到 成片/)** +```bash +python3 soul_enhance.py -c clips/ -l highlights.json -t transcript.srt -o 成片/ --vertical --title-only +``` + +输出目录结构示例: +``` +xxx_output/ + clips/ # 横版切片 + 成片/ # 竖屏成片,文件名为标题.mp4 + 成片/目录索引.md + highlights.json + transcript.srt +``` + +--- + +## 八、参数速查 + +| 项 | 值 | +|----|-----| +| 文件夹 | 仅 **clips/**、**成片/** | +| 成片尺寸 | 498×1080 竖屏 | +| 成片文件名 | 纯标题(无 01、无 _enhanced) | +| 单段时长 | 30~300 秒 | +| 高光/语助词 | 见 `参考资料/高光识别提示词.md` | + +详细 crop 说明见:`参考资料/竖屏中段裁剪参数说明.md`。 + +**发布到抖音**:成片生成后,可用「抖音发布」Skill(开放平台 OAuth 登录 + 上传/创建视频)或腕推等工具发布;见 `03_卡木(木)/木叶_视频内容/抖音发布/SKILL.md`。 diff --git a/03_卡木(木)/木叶_视频内容/视频切片/参考资料/主题片段提取规则.md b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/主题片段提取规则.md new file mode 100644 index 00000000..84b1629e --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/主题片段提取规则.md @@ -0,0 +1,124 @@ +# 主题片段提取规则 + +> 按「章节正文」提取核心主题,再在视频转录稿中匹配时间,产出切片方案。 +> 与《视频切片》SKILL 配套,文件名与流程与 batch_clip / soul_enhance 一致。 + +--- + +## 一、数据来源 + +| 来源 | 用途 | +|------|------| +| **章节 .md** | 第9章单场文章(如 `第112场|一个人起头,维权挣了大半套房.md`),作为**主题与优质片段内容**的唯一依据 | +| **transcript.srt** | 该场派对视频的 MLX Whisper 转录稿(带时间戳),用于定位每段主题在视频中的起止时间 | + +--- + +## 二、流程(步骤顺序) + +``` +1. 章节正文拆主题 + → 脚本 chapter_themes_to_highlights.py 按 --- 或段落拆成若干主题 + +2. 转录稿 + 本地模型匹配时间 + → 用本地最佳模型(Ollama qwen2.5:7b,无则 1.5b)在 transcript.srt 中为每个主题找一段连续内容,输出 start_time / end_time + +3. 输出 highlights.json + → 格式与 identify_highlights.py 一致,供 batch_clip 与 soul_enhance 使用 + +4. 批量切片 + → batch_clip.py -i 视频 -l highlights.json -o clips/ -p soul112 + +5. 增强(封面 + 字幕) + → soul_enhance.py -c clips/ -l highlights.json -t transcript.srt -o clips_enhanced/ +``` + +--- + +## 三、时长与数量 + +- **视频长度由完整片段决定**:每段是一个**完整的话题/情节**,有头有尾,不从中途截断。 +- **单段时长**:**30 秒~300 秒**(0.5~5 分钟),由该完整片段实际时长决定。 +- **片段数量**:由章节主题或高光数量决定(如 10 段)。 + +--- + +## 四、highlights.json 字段(与视频切片规则一致) + +每个片段必须包含(供 batch_clip + soul_enhance 使用): + +| 字段 | 说明 | +|------|------| +| `title` | 简短标题,15 字内,用于**文件名**与封面/列表展示 | +| `start_time` | 开始时间,格式 `HH:MM:SS` | +| `end_time` | 结束时间,格式 `HH:MM:SS` | +| `hook_3sec` | 前 3 秒 Hook 文案,15 字内,封面/前贴用 | +| `cta_ending` | 结尾 CTA,统一用「关注我,每天学一招私域干货」 | +| `transcript_excerpt` | 该段内容 50 字内摘要 | +| `reason` | 为何该段时间对应该主题(可选) | + +--- + +## 五、文件名规则(与 batch_clip 一致) + +- **格式**:`{前缀}_{序号:02d}_{标题}.mp4` +- **前缀**:由 batch_clip 的 `-p` 指定,如 `soul112` 表示第 112 场。 +- **标题**:由 highlights 中该条的 `title` 经 **sanitize_filename** 得到: + - 仅保留中文、空格、下划线、连字符; + - 过长截断(默认 50 字); + - 若为空则用「片段」。 + +**示例(第 112 场,前缀 soul112)** +- `soul112_01_起头难跑通就能变成付费服务.mp4` +- `soul112_02_没人起头就起头一个人站着.mp4` +- `soul112_03_干货起头难跑通更难.mp4` + +标题来自 highlights 中每条条目的 `title` 字段,经 `batch_clip.sanitize_filename` 后仅保留中文、空格、`_`、`-`,过长截断至 50 字。 + +--- + +## 六、命令示例(112 场) + +```bash +# 1. 转录(若尚未有 transcript.srt) +cd 03_卡木(木)/木叶_视频内容/视频切片/脚本 +conda activate mlx-whisper +# 先跑 soul_slice_pipeline 到「转录+转简体」完成,或单独用 mlx_whisper 生成 transcript.srt + +# 2. 按章节主题生成 highlights.json +python3 chapter_themes_to_highlights.py \ + --chapter "/Users/karuo/Documents/个人/2、我写的书/《一场soul的创业实验》/第四篇|真实的赚钱/第9章|我在Soul上亲访的赚钱案例/第112场|一个人起头,维权挣了大半套房.md" \ + --transcript "/Users/karuo/Movies/soul视频/soul 派对 112场 20260228_output/transcript.srt" \ + --output "/Users/karuo/Movies/soul视频/soul 派对 112场 20260228_output/highlights_from_chapter.json" + +# 3. 批量切片(按 highlights 时间点切) +python3 batch_clip.py -i "/Users/karuo/Movies/soul视频/原视频/soul 派对 112场 20260228.mp4" \ + -l "/Users/karuo/Movies/soul视频/soul 派对 112场 20260228_output/highlights_from_chapter.json" \ + -o "/Users/karuo/Movies/soul视频/soul 派对 112场 20260228_output/clips" \ + -p soul112 + +# 4. 增强(封面 + 字幕居中) +python3 soul_enhance.py -c "/Users/karuo/Movies/soul视频/soul 派对 112场 20260228_output/clips" \ + -l "/Users/karuo/Movies/soul视频/soul 派对 112场 20260228_output/highlights_from_chapter.json" \ + -t "/Users/karuo/Movies/soul视频/soul 派对 112场 20260228_output/transcript.srt" \ + -o "/Users/karuo/Movies/soul视频/soul 派对 112场 20260228_output/clips_enhanced" +``` + +--- + +## 七、本地模型 + +- **优先**:Ollama `qwen2.5:7b`(脚本默认) +- **备选**:`qwen2.5:1.5b`(`--model qwen2.5:1.5b`) +- 若 7b 未安装,脚本可改为自动回退 1.5b(需在脚本内加 try/except 换模型)。 + +--- + +## 八、与「高光识别」的区别 + +| 方式 | 主题来源 | 时长 | +|------|----------|------| +| **identify_highlights.py** | 仅从转录稿由 AI 识别「干货/金句」 | 默认 1~5 分钟 | +| **chapter_themes_to_highlights.py** | 从章节 .md 拆主题,再在转录中匹配 | 按内容完整度,不限于 5 分钟 | + +二者输出的 highlights.json 格式相同,均可直接用于 batch_clip 与 soul_enhance。 diff --git a/03_卡木(木)/木叶_视频内容/视频切片/参考资料/竖屏中段裁剪参数说明.md b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/竖屏中段裁剪参数说明.md new file mode 100644 index 00000000..35d08bb8 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/竖屏中段裁剪参数说明.md @@ -0,0 +1,33 @@ +# Soul 竖屏中段裁剪参数说明 + +> 与 **视频切片** Skill「Soul 竖屏成片」一致,以后剪辑 Soul 视频统一用此参数。 + +## 源与输出 + +- **源视频**:横版 1920×1080(16:9,soul_enhance 输出) +- **需求**:保留画面中间竖条(手机内容区),去左右白边 +- **输出**:**498×1080** 竖屏 + +## 当前固定参数(已微调) + +| 步骤 | 滤镜 | 说明 | +|------|------|------| +| 1 | crop=608:1080:483:0 | 从横版取竖条 608 宽,起点 x=483 | +| 2 | crop=498:1080:60:0 | 裁掉左侧白边 60px、右侧 50px,内容宽 498 | +| 输出 | 498×1080 | 仅内容窗口 | + +**一条命令:** + +```bash +ffmpeg -y -i "输入_enhanced.mp4" -vf "crop=608:1080:483:0,crop=498:1080:60:0" -c:a copy "输出_竖屏中段.mp4" +``` + +**批量:** 使用 `脚本/soul_vertical_crop.py --dir clips_enhanced目录`。加 `--title-only` 时输出文件名为纯标题(无序号、无「竖屏中段」)。 + +## 若源为竖版 1080×1920 + +保留中间宽度示例: + +```bash +ffmpeg -y -i "输入.mp4" -vf "crop=918:1920:81:0" -c:a copy "输出.mp4" +``` diff --git a/03_卡木(木)/木叶_视频内容/视频切片/参考资料/视频结构_提问回答与高光.md b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/视频结构_提问回答与高光.md new file mode 100644 index 00000000..c00b58e5 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/视频结构_提问回答与高光.md @@ -0,0 +1,58 @@ +# 视频结构:提问→回答 + 前3秒高光 + 去语助词 + +> Soul 派对切片成片的统一结构:先看片段有没有人提问,有则前3秒展示**提问**,再播**回答**;全片去除语助词;高光按「3秒亮点」剪。 + +--- + +## 一、成片链路(一句话) + +**前3秒展示「问题」 → 正片播「回答」 → 整片去语助词,按3秒高光剪出完整片段。** + +--- + +## 二、提问→回答 结构 + +| 步骤 | 动作 | 说明 | +|------|------|------| +| 1 | 看片段是否有人提问 | 高光识别时判断:该段时间内是否有观众/连麦者提出问题 | +| 2 | 若有提问 | 提取**提问原文**(可去语助词、精简一句),填 `question`,`hook_3sec` 与之一致 | +| 3 | 前3秒 | 封面/前贴展示「提问的问题」,让用户先看到问题 | +| 4 | 正片 | 从回答开始,或「先问后答」完整保留,形成完整提问→回答链路 | +| 5 | 若无提问 | 前3秒用金句/悬念/数据等 hook_3sec,无 question 字段 | + +**目的**:用户先被「问题」抓住,再听回答,完播率更高;提问与回答中的语助词在成片阶段**完整去除**。 + +--- + +## 三、前3秒高光 + +- **有提问**:前3秒 = 提问问题(question / hook_3sec),先问后答。 +- **无提问**:前3秒 = 金句/悬念/数据/问题型 hook(见 `高光识别提示词.md`)。 +- 高光识别按「3秒能抓人」的标准选段,每段 30~300 秒完整语义单元;输出里若有提问则必填 `question` 且 `hook_3sec` 与之一致。 + +--- + +## 四、去语助词(完整) + +- **范围**:整条成片(含前3秒展示的提问部分与后面回答部分)。 +- **执行**:soul_enhance 对字幕/文字统一做语助词清理(嗯、啊、呃、那个、就是、然后等),见 `高光识别提示词.md` 语助词列表。 +- **结果**:提问与回答的文案在成片中均为去语助词后的干净版。 + +--- + +## 五、剪辑与成片流程(与 Skill 一致) + +``` +原视频 → 转录(MLX) → 高光识别(含 question/hook_3sec) → batch_clip → soul_enhance(封面=提问/金句 + 字幕 + 去语助词 + 竖屏) → 成片/ +``` + +- **高光识别**:按 `高光识别提示词.md`,有提问则填 question,hook_3sec 优先用提问。 +- **batch_clip**:按 highlights.json 切出 clips/。 +- **soul_enhance**:封面/前3秒用 `question ?? hook_3sec ?? title`,字幕与整片去语助词,竖屏直出到 成片/。 + +--- + +## 六、与高光识别提示词的关系 + +- 本结构中的「提问→回答」「前3秒=提问」「去语助词」均与 `高光识别提示词.md` 一致。 +- 高光识别输出 JSON 需含:有提问时 `question` + `hook_3sec`(与 question 一致);成片阶段用 question 优先做前3秒展示,并做完整去语助词。 diff --git a/03_卡木(木)/木叶_视频内容/视频切片/参考资料/高光识别提示词.md b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/高光识别提示词.md index 73d9c459..859cd9a7 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/参考资料/高光识别提示词.md +++ b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/高光识别提示词.md @@ -48,9 +48,21 @@ - 吊人胃口的开场 - "接下来这段更精彩" +# 提问→回答 结构(前3秒优先用提问) + +**成片结构**:先展示**提问**(前3秒),再进入**回答**(正片)。若片段里有人提问,必须把**提问的问题**放到前3秒。 + +1. **判断**:看该高光片段文字稿中是否有人提问(观众/连麦者问的问题)。 +2. **若有提问**: + - 提取**提问的原文**(可去语助词、精简为一句),填入 `question` 字段; + - `hook_3sec` 优先用该提问内容(与 question 一致),成片前3秒封面/贴片展示「问题」,正片从回答开始或先问后答。 +3. **若无提问**:`hook_3sec` 用金句/悬念/数据等抓眼球开场,`question` 可省略。 + +**目的**:前3秒让用户看到「问题」,再听回答,形成完整**提问→回答**链路;提问与回答部分的语助词均在成片时完整去除。 + # 前3秒Hook设计原则 -Hook的目的是让用户在前3秒决定看完整个视频。 +Hook的目的是让用户在前3秒决定看完整个视频。**有提问时 Hook = 提问问题。** ## Hook类型选择: @@ -96,9 +108,13 @@ CTA的目的是引导用户完成下一步动作。 2. **完整性**:必须是完整的语义单元,不能话说一半 3. **独立性**:单独播放能理解,不依赖上下文 4. **开场**:尽量以金句或问题开头,3秒内抓住注意力 +5. **语助词与空格**:识别时标注或剔除语助词(嗯、啊、呃、那个、就是、然后等)及中间无意义空排/停顿,与主题片段提取规则一致,成片剪辑时由 soul_enhance 统一清理 +6. **节奏感**:优先选讲话有步骤、有节奏的片段(起承转合清晰、不拖沓、少重复),避免大段碎碎念或断句混乱 # 输出格式(严格JSON) +每个片段若为「有人提问+回答」结构,必须填 `question`,且 `hook_3sec` 优先用提问内容(前3秒先展示提问再回答)。 + ```json [ { @@ -108,9 +124,10 @@ CTA的目的是引导用户完成下一步动作。 "end_time": "00:13:56", "duration_sec": 82, "transcript_excerpt": "这段话的前50字...", - "hook_3sec": "99%的人都不知道这个方法", - "hook_type": "悬念型", - "hook_reason": "用数据+悬念制造好奇心", + "question": "为什么你做私域总是不赚钱?", + "hook_3sec": "为什么你做私域总是不赚钱?", + "hook_type": "问题型", + "hook_reason": "片段内有人提问,前3秒用提问抓注意力", "cta_ending": "想学更多?评论区扣1,拉你进群", "cta_type": "group", "reason": "为什么这个片段有传播力(20字内)", @@ -121,6 +138,9 @@ CTA的目的是引导用户完成下一步动作。 ] ``` +- **question**(可选但推荐):片段中**观众/连麦者的提问原文**(可精简、去语助词)。有则成片前3秒展示该问题,再播回答。 +- **hook_3sec**:有提问时与 question 一致;无提问时用金句/悬念等。 + # 评分说明 - **9-10分**:爆款潜力,必切 @@ -136,6 +156,8 @@ CTA的目的是引导用户完成下一步动作。 4. 每个片段必须包含 hook_3sec 和 cta_ending 5. Hook和CTA必须与片段内容强相关 6. 只输出JSON,不要其他解释 +7. **与主题片段提取一致**:Soul 剪辑全流程(高光识别 → batch_clip → soul_enhance → 竖屏裁剪)继承本提示词;主题片段由章节拆解时,判断标准、语助词与节奏感要求与本提示词保持一致。 +8. **有提问时**:片段内有人提问则必须填 `question`,且 `hook_3sec` 与 question 一致,成片前3秒先展示提问再回答;去语助词覆盖整条成片(含提问与回答)。 # 视频文字稿(带时间戳) @@ -156,6 +178,19 @@ CTA的目的是引导用户完成下一步动作。 | `{{TRANSCRIPT}}` | 完整文字稿 | Whisper输出 | | `{{DEFAULT_CTA}}` | 默认CTA文案 | "关注我,获取更多干货" | +## 语助词与空排(成片剪辑时去除) + +高光识别与主题片段提取时,应避免选「语助词密集、空排多」的段落;成片由 soul_enhance 统一清理: + +- **语助词**:嗯、啊、呃、额、哦、噢、唉、哎、诶、喔、那个、就是、然后、这个、所以说、怎么说、怎么说呢、对吧、是吧、好吧、行吧、那、就、就是那个、其实、那么、然后呢、还有就是、以及、另外、等等、你知道吗、我跟你说、好、对、OK +- **中间空排**:长时间静音、无意义停顿在剪辑时由 soul_enhance 做静音检测并压缩,高光识别时优先选节奏紧凑的片段 + +## 节奏感要求 + +- 优先:有步骤、有起伏、起承转合清晰的片段 +- 避免:大段碎碎念、断句混乱、同一句话重复多遍、长时间无信息量停顿 +- 与主题片段提取规则一致:每段为完整语义单元,时长 30~300 秒,标题为一句刺激性观点 + ## 文字稿格式要求 输入给AI的文字稿应该是这样的格式: diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/chapter_themes_to_highlights.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/chapter_themes_to_highlights.py new file mode 100644 index 00000000..ecd22e8b --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/chapter_themes_to_highlights.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +按章节正文提取主题片段 +======================== +以「第9章单场文章」的 .md 正文为来源,提取核心主题,再用本地最佳模型(Ollama) +在转录稿中匹配每段主题的起止时间,输出 highlights.json,供 batch_clip + soul_enhance 使用。 + +- 主题来源:章节 .md,按 --- 或段落拆成若干核心主题 +- 不限 5 分钟:每段按内容完整度定时长,可 1~5 分钟或更长 +- 文件名:由 batch_clip 按「前缀_序号_标题」生成,标题来自 title,仅保留中文与安全字符 +""" + +import argparse +import json +import re +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +OLLAMA_URL = "http://localhost:11434" +DEFAULT_CTA = "关注我,每天学一招私域干货" +# 本地优先用 7b,失败则回退 1.5b +OLLAMA_MODELS = ["qwen2.5:7b", "qwen2.5:1.5b"] + + +def parse_chapter_themes(md_path: Path) -> list[dict]: + """ + 从章节 .md 拆出主题列表。 + 每个主题:{"title": "简短标题", "text": "该段正文摘要或全文"} + 按 --- 分块,第一块视为开篇/场次信息可跳过,其余每块为一个主题。 + """ + text = md_path.read_text(encoding="utf-8") + # 按 --- 分块(兼容 --- 前后换行) + blocks = re.split(r"\n---+\s*\n", text.strip()) + themes = [] + for idx, block in enumerate(blocks): + block = block.strip() + # 跳过过短或纯元信息的块(如仅「第112场,135分钟」) + if not block or len(block) < 25: + continue + # 去掉首行的 # 标题、日期、场次 + lines = [ln.strip() for ln in block.split("\n") if ln.strip()] + first_line = lines[0] if lines else "" + if idx == 0 and (re.match(r"^#\s+", first_line) or re.match(r"^\d{4}年", first_line) or re.match(r"^第\s*\d+\s*场", first_line)): + # 第一块若只是标题/日期/场次,取第二行起或整块 + content_lines = lines[1:] if len(lines) > 1 else lines + if not content_lines: + continue + first_line = content_lines[0] + # 主题标题:首句或前 22 字 + if len(first_line) > 25: + title = first_line[:22] + "…" + else: + title = first_line or "片段" + excerpt = re.sub(r"\*\*[^*]+\*\*:?", "", block)[:500] + themes.append({"title": title, "text": excerpt}) + return themes + + +def parse_srt_segments(srt_path: Path) -> list[dict]: + """解析 SRT 为 [{start_sec, end_sec, text, start_time, end_time}, ...]""" + content = srt_path.read_text(encoding="utf-8") + segments = [] + pattern = r"(\d+)\n(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})\n(.*?)(?=\n\n|\Z)" + for m in re.findall(pattern, content, re.DOTALL): + sh, sm, ss = int(m[1]), int(m[2]), int(m[3]) + eh, em, es = int(m[5]), int(m[6]), int(m[7]) + start_sec = sh * 3600 + sm * 60 + ss + end_sec = eh * 3600 + em * 60 + es + text = m[9].strip().replace("\n", " ") + if len(text) > 2: + segments.append({ + "start_sec": start_sec, "end_sec": end_sec, + "start_time": f"{sh:02d}:{sm:02d}:{ss:02d}", + "end_time": f"{eh:02d}:{em:02d}:{es:02d}", + "text": text, + }) + return segments + + +def fallback_highlights_from_themes(transcript_path: Path, themes: list[dict], min_sec: int = 90, max_sec: int = 300) -> list[dict]: + """ + 规则兜底:用主题关键词在转录稿中定位每段起止时间。 + 每个主题取标题/正文前 80 字中的关键词,在 segments 中找首次出现位置,再扩展为 min_sec~max_sec 的完整段。 + """ + segments = parse_srt_segments(transcript_path) + if not segments: + return [] + total_sec = segments[-1]["end_sec"] + used_ranges: list[tuple[float, float]] = [] + result = [] + + def _keywords(t: dict) -> list[str]: + raw = (t.get("title", "") + " " + t.get("text", ""))[:200] + # 去掉标点,取 2 字以上的词 + words = re.findall(r"[\u4e00-\u9fff]{2,}", raw) + return list(dict.fromkeys(words))[:8] + + for theme in themes: + title = theme.get("title", "片段")[:20] + kws = _keywords(theme) + if not kws: + result.append({"title": title, "start_time": "00:00:00", "end_time": "00:01:30", "hook_3sec": title, "cta_ending": DEFAULT_CTA, "transcript_excerpt": title, "reason": "无关键词"}) + continue + # 找第一个包含任一关键词的 segment 索引 + best_idx = None + for i, seg in enumerate(segments): + t = seg["text"] + if any(kw in t for kw in kws): + best_idx = i + break + if best_idx is None: + continue + seg = segments[best_idx] + start_sec = max(0, seg["start_sec"] - 15) + end_sec = min(total_sec, seg["end_sec"] + max_sec) + # 向后扩展至至少 min_sec,最多 max_sec + end_sec = min(end_sec, start_sec + max_sec) + if end_sec - start_sec < min_sec: + end_sec = min(total_sec, start_sec + min_sec) + overlap = any(not (end_sec <= a or start_sec >= b) for a, b in used_ranges) + if overlap: + continue + used_ranges.append((start_sec, end_sec)) + texts = [s["text"] for s in segments if s["end_sec"] >= start_sec and s["start_sec"] <= end_sec] + excerpt = (" ".join(texts)[:50] + "…") if texts else title + result.append({ + "title": title, + "start_time": _sec_to_hhmmss(start_sec), + "end_time": _sec_to_hhmmss(end_sec), + "hook_3sec": (title[:15] + "…") if len(title) > 15 else title, + "cta_ending": DEFAULT_CTA, + "transcript_excerpt": excerpt, + "reason": "按章节主题关键词定位", + }) + return result + + +def srt_to_timestamped_text(srt_path: Path) -> str: + """SRT 转为带时间戳的纯文本,供模型阅读""" + content = srt_path.read_text(encoding="utf-8") + lines = [] + pattern = r"(\d+)\n(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\n(.*?)(?=\n\n|\Z)" + for m in re.findall(pattern, content, re.DOTALL): + start = m[1].replace(",", ".") + text = m[3].strip().replace("\n", " ") + lines.append(f"[{start}] {text}") + return "\n".join(lines) + + +def _sec_to_hhmmss(sec: float) -> str: + s = int(sec) + h, m = s // 3600, (s % 3600) // 60 + ss = s % 60 + return f"{h:02d}:{m:02d}:{ss:02d}" + + +def _parse_time_to_sec(t: str) -> float: + """解析 HH:MM:SS 或秒数为秒""" + t = str(t).strip().replace(",", ".") + parts = re.split(r"[:.]", t) + if len(parts) >= 3: + try: + return int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2]) + except (ValueError, TypeError): + pass + try: + return float(t) + except ValueError: + return 0 + + +def _filter_short_clips(data: list[dict], min_sec: float = 60) -> list[dict]: + """过滤时长小于 min_sec 的片段""" + result = [] + for item in data: + if not isinstance(item, dict): + continue + st = item.get("start_time") or item.get("start") or "00:00:00" + et = item.get("end_time") or item.get("end") or "00:01:00" + dur = _parse_time_to_sec(et) - _parse_time_to_sec(st) + if dur >= min_sec: + result.append(item) + else: + print(f" 过滤短片段: {item.get('title','?')} (仅{dur:.0f}秒)", file=sys.stderr) + return result + + +def call_ollama_chapter_themes(transcript_text: str, themes: list[dict], model: str, max_tokens: int = 8192) -> list[dict]: + """ + 用 Ollama 根据「章节主题」在转录稿中定位每段起止时间。 + 返回 list[dict],每项含 start_time, end_time, title, hook_3sec, cta_ending, transcript_excerpt, reason。 + """ + import requests + + themes_desc = "\n".join([f"- {t['title']}: {t['text'][:200]}…" for t in themes]) + transcript_trim = transcript_text[:22000] if len(transcript_text) > 22000 else transcript_text + + prompt = f"""你是短视频策划。下面是一篇「第9章单场文章」拆出的核心主题,以及该场派对视频的带时间戳文字稿。 +请为【每一个主题】在文字稿中找出最匹配的【一段连续内容】,给出精确的 start_time 和 end_time。 +每段时长 60 秒~5 分钟均可,以「主题完整、有头有尾」为准,不限于 5 分钟。 + +【章节主题】 +{themes_desc} + +【输出要求】 +- 只输出一个 JSON 数组,不要 ``` 或其它说明 +- 每个元素必须包含:title, start_time, end_time, hook_3sec, cta_ending, transcript_excerpt, reason +- start_time / end_time 格式为 HH:MM:SS,且必须来自下面文字稿中的时间戳 +- title 用该主题的简短标题(15 字内),hook_3sec 用该段前 3 秒可用的金句(15 字内) +- cta_ending 统一用:「{DEFAULT_CTA}」 +- transcript_excerpt:该段内容 50 字内摘要 +- reason:为何这段对应该主题(一句话) + +【视频文字稿】 +--- +{transcript_trim} +---""" + + r = requests.post( + f"{OLLAMA_URL}/api/generate", + json={ + "model": model, + "prompt": prompt, + "stream": False, + "options": {"temperature": 0.2, "num_predict": max_tokens}, + }, + timeout=180, + ) + if r.status_code != 200: + raise RuntimeError(f"Ollama {r.status_code}") + raw = r.json().get("response", "").strip() + + # 解析 JSON 数组 + m = re.search(r"\[[\s\S]*\]", raw) + if not m: + raise ValueError("模型未返回合法 JSON 数组") + data = json.loads(m.group()) + + out = [] + for item in data: + if not isinstance(item, dict): + continue + st = item.get("start_time") or item.get("start") + et = item.get("end_time") or item.get("end") + if isinstance(st, (int, float)): + item["start_time"] = _sec_to_hhmmss(st) + if isinstance(et, (int, float)): + item["end_time"] = _sec_to_hhmmss(et) + item.setdefault("cta_ending", DEFAULT_CTA) + item.setdefault("title", item.get("theme", "片段")) + out.append(item) + return out + + +def main(): + parser = argparse.ArgumentParser(description="按章节正文提取主题片段 → highlights.json") + parser.add_argument("--chapter", "-c", required=True, help="章节 .md 路径(第9章单场文章)") + parser.add_argument("--transcript", "-t", required=True, help="transcript.srt 路径") + parser.add_argument("--output", "-o", required=True, help="highlights.json 输出路径") + parser.add_argument("--model", "-m", default="", help="Ollama 模型,默认优先 7b 再 1.5b") + args = parser.parse_args() + + models_to_try = [args.model] if args.model else OLLAMA_MODELS + + chapter_path = Path(args.chapter).resolve() + transcript_path = Path(args.transcript).resolve() + if not chapter_path.exists(): + print(f"❌ 章节不存在: {chapter_path}", file=sys.stderr) + sys.exit(1) + if not transcript_path.exists(): + print(f"❌ 转录稿不存在: {transcript_path}", file=sys.stderr) + sys.exit(1) + + print("1. 从章节正文提取核心主题...") + themes = parse_chapter_themes(chapter_path) + print(f" 共 {len(themes)} 个主题") + for i, t in enumerate(themes[:10], 1): + print(f" - {i}. {t['title']}") + if len(themes) > 10: + print(f" ... 等共 {len(themes)} 个") + + print("2. 转录稿转带时间戳文本...") + transcript_text = srt_to_timestamped_text(transcript_path) + + print("3. 调用本地模型匹配主题与时间...") + highlights = None + for model in models_to_try: + try: + print(f" 尝试 {model} ...") + highlights = call_ollama_chapter_themes(transcript_text, themes, model) + break + except Exception as e: + print(f" {model} 失败: {e}", file=sys.stderr) + if not highlights: + print(" 使用规则兜底:按主题关键词在转录稿中定位时间段...", file=sys.stderr) + highlights = fallback_highlights_from_themes(transcript_path, themes) + if not highlights: + print("❌ 无法生成 highlights(模型失败且规则兜底无匹配)", file=sys.stderr) + sys.exit(1) + highlights = _filter_short_clips(highlights) + + out_path = Path(args.output).resolve() + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path, "w", encoding="utf-8") as f: + json.dump(highlights, f, ensure_ascii=False, indent=2) + print(f"✅ 已输出 {len(highlights)} 个主题片段: {out_path}") + print(" 后续:batch_clip -i 视频 -l 本文件 -o clips/ -p soul112 → soul_enhance 带封面与字幕") + + +if __name__ == "__main__": + main() diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py index 8f6429e7..897ec480 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py @@ -73,8 +73,8 @@ def fallback_highlights(transcript_path: str, clip_count: int) -> list: return result -def srt_to_timestamped_text(srt_path: str) -> str: - """将 SRT 转为带时间戳的纯文本""" +def srt_to_timestamped_text(srt_path: str, skip_repetitive_head: int = 150) -> str: + """将 SRT 转为带时间戳的纯文本。跳过开头重复段落(如「我看你不太好」循环)""" with open(srt_path, "r", encoding="utf-8") as f: content = f.read() lines = [] @@ -82,8 +82,36 @@ def srt_to_timestamped_text(srt_path: str) -> str: for m in re.findall(pattern, content, re.DOTALL): start = m[1].replace(",", ".") text = m[3].strip().replace("\n", " ") - lines.append(f"[{start}] {text}") - return "\n".join(lines) + lines.append((start, text)) + # 跳过开头重复段,直到出现连续 3 段不同且长度>=5 的内容 + varied_start = 0 + recent = [] + for i, (s, t) in enumerate(lines): + if len(t) >= 5: + recent = (recent + [t])[-3:] + if len(recent) == 3 and len(set(recent)) >= 2: + varied_start = max(0, i - 2) + break + else: + recent = [] + if varied_start > 0: + lines = lines[varied_start:] + # 过滤单字/短句重复段(如「你」循环),连续 5 次以上则整块跳过 + out = [] + prev, cnt = None, 0 + skip_until = -1 + for i, (s, t) in enumerate(lines): + if len(t) <= 2 and t == prev: + cnt += 1 + if cnt >= 5 and skip_until < 0: + skip_until = i # 开始跳过 + if skip_until >= 0: + continue + else: + prev, cnt = t, 1 + skip_until = -1 + out.append((s, t)) + return "\n".join(f"[{s}] {t}" for s, t in out) def _sec_to_hhmmss(sec: float) -> str: @@ -127,44 +155,46 @@ def _filter_short_clips(data: list) -> list: def _build_prompt(transcript: str, clip_count: int) -> str: - """构建高光识别 prompt(1-5分钟,主题与内容必须一致,全中文)""" - txt = transcript[:25000] if len(transcript) > 25000 else transcript - return f"""你是资深短视频策划师。请从视频文字稿中识别尽可能多的**完整干货片段**,目标 {clip_count} 个以上。 + """构建高光识别 prompt(提问→回答:有提问时 question/hook_3sec 用提问问题)""" + txt = transcript[:5000] if len(transcript) > 5000 else transcript + return f"""识别视频文字稿中的 {clip_count} 个高光片段,直接输出 JSON 数组,第一个字符必须是 [。 -【时长要求】每个片段 **60-300 秒(1-5 分钟)**,少于 60 秒的不要输出。 -【主题一致】title、hook_3sec、transcript_excerpt 必须**完全对应**该时间段内的实际内容,禁止泛泛而谈。 - - title:从该段时间内的核心观点提炼,15字内 - - hook_3sec:从该段第一句或核心金句提炼,15字内 - - transcript_excerpt:该段内容的中文摘要,50字内 +重要:若某片段里有人提问(观众/连麦者问的问题),必须提取提问内容填 question,且 hook_3sec 用该提问。成片前3秒先展示提问,再播回答。 -【切片原则】 -- 每段必须是完整话题,有头有尾 -- 优先:金句、故事、方法论、反常识、情绪高点 -- 相邻片段间隔至少 60 秒 -- 从文字稿时间戳精确提取 start_time、end_time +示例(有提问): +[{{"title":"普通人怎么敢跟ZF搞","start_time":"01:12:30","end_time":"01:15:30","question":"普通人怎么敢跟ZF搞?","hook_3sec":"普通人怎么敢跟ZF搞?","cta_ending":"{DEFAULT_CTA}","transcript_excerpt":"维权起头跑通就成生意","reason":"提问+回答完整"}}] +示例(无提问): +[{{"title":"起头难","start_time":"00:05:55","end_time":"00:08:00","hook_3sec":"没人起头就起头","cta_ending":"{DEFAULT_CTA}","transcript_excerpt":"起头难跑通就能变成付费服务","reason":"核心观点"}}] -【输出字段】全部**简体中文**,英文原文需翻译: -- title、start_time、end_time、hook_3sec、cta_ending(用"{DEFAULT_CTA}")、transcript_excerpt、reason - -只输出 JSON 数组,不要 ``` 或其他文字。 - -视频文字稿: ---- +文字稿(从时间戳提取 start_time、end_time,每段 60-300 秒): {txt} ----""" + +直接输出 JSON 数组,以 [ 开头。有提问的片段必须带 question 且 hook_3sec 与 question 一致。""" def _parse_ai_json(text: str) -> list: """从 AI 输出中提取 JSON 数组""" + if not text or not text.strip(): + raise ValueError("AI 返回为空") text = text.strip() if text.startswith("```"): text = re.sub(r"^```(?:json)?\s*", "", text) text = re.sub(r"\s*```\s*$", "", text) - # 尝试找到 [...] + # 优先匹配完整 [...],若模型只返回单个对象则包成数组 m = re.search(r"\[[\s\S]*\]", text) if m: - return json.loads(m.group()) - return json.loads(text) + try: + return json.loads(m.group()) + except json.JSONDecodeError: + pass + # 尝试解析为单个对象后包成数组 + m = re.search(r"\{[\s\S]*\}", text) + if m: + try: + return [json.loads(m.group())] + except json.JSONDecodeError: + pass + raise ValueError("未找到合法 JSON 数组或对象") def _is_mostly_chinese(text: str) -> bool: @@ -205,7 +235,7 @@ def _ensure_chinese_highlights(data: list) -> list: for i, item in enumerate(data): if not isinstance(item, dict): continue - for key in ["title", "hook_3sec", "transcript_excerpt"]: + for key in ["title", "hook_3sec", "question", "transcript_excerpt"]: val = item.get(key) if val and not _is_mostly_chinese(str(val)): translated = _translate_to_chinese(str(val)) @@ -217,24 +247,41 @@ def _ensure_chinese_highlights(data: list) -> list: return data -def call_ollama(transcript: str, clip_count: int = CLIP_COUNT) -> str: - """调用卡若AI本地模型(Ollama)""" +OLLAMA_MODELS = ["qwen2.5:3b", "qwen2.5:1.5b"] # 优先 3b,能力更强 + + +def call_ollama(transcript: str, clip_count: int = CLIP_COUNT, model: str = "qwen2.5:3b") -> str: + """调用卡若AI本地模型(Ollama),使用 chat 接口避免对话式误判""" import requests prompt = _build_prompt(transcript, clip_count) + system = ( + "你是短视频策划师。用户会提供视频文字稿,你只输出一个 JSON 数组。" + "若某片段内有人提问(观众/连麦者问的问题),必须提取提问原文填 question,且 hook_3sec 用该提问(前3秒先展示提问再回答);无提问则 hook_3sec 用金句/悬念。" + "格式含 title, start_time, end_time, hook_3sec, cta_ending, transcript_excerpt, reason;有提问时加 question。" + "禁止输出任何非 JSON 内容。" + ) try: r = requests.post( - f"{OLLAMA_URL}/api/generate", + f"{OLLAMA_URL}/api/chat", json={ - "model": "qwen2.5:1.5b", - "prompt": prompt, + "model": model, + "messages": [ + {"role": "system", "content": system}, + {"role": "user", "content": prompt}, + ], "stream": False, - "options": {"temperature": 0.3, "num_predict": 4096}, + "options": {"temperature": 0.2, "num_predict": 8192}, }, - timeout=90, + timeout=300, ) if r.status_code != 200: raise RuntimeError(f"Ollama {r.status_code}") - return r.json().get("response", "").strip() + body = r.json() + msg = body.get("message", {}) + resp = (msg.get("content") or "").strip() + if not resp: + raise RuntimeError("Ollama 返回空响应") + return resp except Exception as e: raise RuntimeError(f"Ollama 调用失败: {e}") from e @@ -244,6 +291,7 @@ def main(): parser.add_argument("--transcript", "-t", required=True, help="transcript.srt 路径") parser.add_argument("--output", "-o", required=True, help="highlights.json 输出路径") parser.add_argument("--clips", "-n", type=int, default=CLIP_COUNT, help="切片数量") + parser.add_argument("--require-ai", action="store_true", help="必须用 AI 识别,失败则退出不兜底") args = parser.parse_args() transcript_path = Path(args.transcript) if not transcript_path.exists(): @@ -253,20 +301,27 @@ def main(): if len(text) < 100: print("❌ 文字稿过短,请检查 SRT 格式", file=sys.stderr) sys.exit(1) - # 级联:Ollama(卡若AI本地) → 规则备用 + # 级联:Ollama 3b → 1.5b → 规则备用(--require-ai 时不用规则) data = None - for name, fn in [ - ("Ollama (卡若AI本地)", call_ollama), - ]: + raw = "" + for model in OLLAMA_MODELS: try: - print(f"正在调用 {name} 分析高光片段...") - raw = fn(text, args.clips) + print(f"正在调用 Ollama {model} 分析高光片段...") + raw = call_ollama(text, args.clips, model) + if not raw: + raise ValueError("模型返回空") data = _parse_ai_json(raw) if data and isinstance(data, list) and len(data) > 0: + print(f" ✓ {model} 成功,识别 {len(data)} 段") break except Exception as e: - print(f"{name} 调用失败 ({e})", file=sys.stderr) + print(f" {model} 失败: {e}", file=sys.stderr) + if raw: + print(f" 返回预览: {str(raw)[:400]}...", file=sys.stderr) if not data or not isinstance(data, list): + if getattr(args, "require_ai", False): + print("❌ 必须用 AI 识别,当前无可用模型或解析失败", file=sys.stderr) + sys.exit(1) print("使用规则备用切分", file=sys.stderr) data = fallback_highlights(str(transcript_path), args.clips) if not data: diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py index d2c352f1..3346621c 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py @@ -10,12 +10,13 @@ Soul切片增强脚本 v2.0 """ import argparse -import subprocess +import json import os import re -import json -import tempfile import shutil +import subprocess +import sys +import tempfile from pathlib import Path from PIL import Image, ImageDraw, ImageFont, ImageFilter @@ -39,6 +40,12 @@ SPEED_FACTOR = 1.10 # 加速10% SILENCE_THRESHOLD = -40 # 静音阈值(dB) SILENCE_MIN_DURATION = 0.5 # 最短静音时长(秒) +# Soul 竖屏裁剪(与 soul_vertical_crop 一致,成片直出用) +CROP_VF = "crop=608:1080:483:0,crop=498:1080:60:0" +# 竖屏成片时封面/字幕用此尺寸,叠在横版上的 x 位置(与 crop 后保留区域对齐) +VERTICAL_W, VERTICAL_H = 498, 1080 +OVERLAY_X = 543 # 1920 下保留区域左缘:483+60 + # 繁转简(OpenCC 优先,否则用映射) _OPENCC = None def _get_opencc(): @@ -104,6 +111,14 @@ COVER_FONT_PRIORITY = [ "/System/Library/Fonts/Supplemental/Songti.ttc", ] +# Soul 品牌绿(绿点/绿色社交) +SOUL_GREEN = (0, 210, 106) # #00D26A +SOUL_GREEN_DARK = (0, 160, 80) +# 竖屏封面高级背景:深色渐变(不超出界面) +VERTICAL_COVER_TOP = (12, 32, 24) # 深墨绿 +VERTICAL_COVER_BOTTOM = (8, 48, 36) # 略亮绿 +VERTICAL_COVER_PADDING = 44 # 左右留白,保证文字不贴边、不超出 + # 样式配置 STYLE = { 'cover': { @@ -159,6 +174,19 @@ def draw_text_with_outline(draw, pos, text, font, color, outline_color, outline_ # 主体 draw.text((x, y), text, font=font, fill=color) +def sanitize_filename(name: str, max_length: int = 50) -> str: + """成片文件名:仅保留中文、空格、_-,与 batch_clip 一致""" + name = _to_simplified(str(name)) + safe = [] + for c in name: + if c in " _-" or "\u4e00" <= c <= "\u9fff": + safe.append(c) + result = "".join(safe).strip() + if len(result) > max_length: + result = result[:max_length] + return result.strip(" _-") or "片段" + + def clean_filler_words(text): """清理语助词 + 去除多余空格""" result = text @@ -322,83 +350,142 @@ def get_cover_font(size): return ImageFont.load_default() +def _draw_vertical_gradient(draw, width, height, top_rgb, bottom_rgb): + """绘制竖屏封面用深色渐变背景,高级感""" + for y in range(height): + t = y / max(height - 1, 1) + r = int(top_rgb[0] + (bottom_rgb[0] - top_rgb[0]) * t) + g = int(top_rgb[1] + (bottom_rgb[1] - top_rgb[1]) * t) + b = int(top_rgb[2] + (bottom_rgb[2] - top_rgb[2]) * t) + draw.rectangle([0, y, width, y + 1], fill=(r, g, b)) + + def create_cover_image(hook_text, width, height, output_path, video_path=None): - """创建封面贴片(简体中文,字体优化)""" + """创建封面贴片。竖屏 498x1080 时:高级渐变背景、文字严格在界面内居中不超出、左上角 Soul logo。""" hook_text = _to_simplified(str(hook_text or "").strip()) if not hook_text: hook_text = "精彩切片" style = STYLE['cover'] hook_style = STYLE['hook'] + is_vertical = (width, height) == (VERTICAL_W, VERTICAL_H) - # 从视频提取背景帧 - if video_path and os.path.exists(video_path): - temp_frame = output_path.replace('.png', '_frame.jpg') - subprocess.run([ - 'ffmpeg', '-y', '-ss', '1', '-i', video_path, - '-vframes', '1', '-q:v', '2', temp_frame - ], capture_output=True) - - if os.path.exists(temp_frame): - bg = Image.open(temp_frame).resize((width, height)) - bg = bg.filter(ImageFilter.GaussianBlur(radius=style['bg_blur'])) - os.remove(temp_frame) - else: - bg = Image.new('RGB', (width, height), (30, 30, 50)) + if is_vertical: + # 竖屏成片:高级深色渐变背景,不依赖视频帧,保证不超出界面 + img = Image.new('RGB', (width, height), VERTICAL_COVER_TOP) + draw = ImageDraw.Draw(img) + _draw_vertical_gradient(draw, width, height, VERTICAL_COVER_TOP, VERTICAL_COVER_BOTTOM) + # 轻微半透明暗角,让文字更突出 + overlay = Image.new('RGBA', (width, height), (0, 0, 0, 100)) + img = img.convert('RGBA') + img = Image.alpha_composite(img, overlay) + draw = ImageDraw.Draw(img) else: - bg = Image.new('RGB', (width, height), (30, 30, 50)) - - # 叠加暗层 - overlay = Image.new('RGBA', (width, height), (0, 0, 0, style['overlay_alpha'])) - bg = bg.convert('RGBA') - img = Image.alpha_composite(bg, overlay) - - draw = ImageDraw.Draw(img) - - # 顶部装饰线 - for i in range(3): - alpha = 150 - i * 40 - draw.rectangle([0, i*3, width, i*3+2], fill=(255, 215, 0, alpha)) - - # 底部装饰线 - for i in range(3): - alpha = 150 - i * 40 - draw.rectangle([0, height - i*3 - 2, width, height - i*3], fill=(255, 215, 0, alpha)) - - # Hook 文字(封面用更好看的字体) - font = get_cover_font(hook_style['font_size']) - - # 计算换行 - max_width = width - 80 - lines = [] - current_line = "" - - for char in hook_text: - test_line = current_line + char - test_w, _ = get_text_size(draw, test_line, font) - if test_w <= max_width: - current_line = test_line + # 横版:沿用视频帧模糊背景 + if video_path and os.path.exists(video_path): + temp_frame = output_path.replace('.png', '_frame.jpg') + subprocess.run([ + 'ffmpeg', '-y', '-ss', '1', '-i', video_path, + '-vframes', '1', '-q:v', '2', temp_frame + ], capture_output=True) + if os.path.exists(temp_frame): + bg = Image.open(temp_frame).resize((width, height)) + bg = bg.filter(ImageFilter.GaussianBlur(radius=style['bg_blur'])) + os.remove(temp_frame) + else: + bg = Image.new('RGB', (width, height), (25, 35, 30)) else: + bg = Image.new('RGB', (width, height), (25, 35, 30)) + overlay = Image.new('RGBA', (width, height), (0, 25, 15, style['overlay_alpha'])) + img = bg.convert('RGBA') + img = Image.alpha_composite(img, overlay) + draw = ImageDraw.Draw(img) + + # Soul 绿装饰线(顶部、底部) + for i in range(3): + alpha = 180 - i * 50 + draw.rectangle([0, i * 3, width, i * 3 + 2], fill=(*SOUL_GREEN, alpha)) + for i in range(3): + alpha = 180 - i * 50 + draw.rectangle([0, height - i * 3 - 2, width, height - i * 3], fill=(*SOUL_GREEN, alpha)) + + # 左上角 Soul logo 小图标(绿圆 + 白字 S),保证在界面内 + logo_x, logo_y = 28, 28 + logo_r = 20 + draw.ellipse([logo_x - logo_r, logo_y - logo_r, logo_x + logo_r, logo_y + logo_r], fill=SOUL_GREEN, outline=(255, 255, 255)) + try: + logo_font = get_cover_font(26) + draw.text((logo_x - 5, logo_y - 12), "S", font=logo_font, fill=(255, 255, 255)) + except Exception: + pass + + # 标题文字:竖屏时严格限制在 padding 内,多行居中,绝不超出界面 + if is_vertical: + max_text_width = width - 2 * VERTICAL_COVER_PADDING # 498 - 88 = 410 + cover_font_size = 48 + font = get_cover_font(cover_font_size) + lines = [] + for _ in range(20): + current_line = "" + lines = [] # 本轮换行结果 + for char in hook_text: + test_line = current_line + char + test_w, _ = get_text_size(draw, test_line, font) + if test_w <= max_text_width: + current_line = test_line + else: + if current_line: + lines.append(current_line) + current_line = char if current_line: lines.append(current_line) - current_line = char - if current_line: - lines.append(current_line) - - # 绘制文字(完全居中) - line_height = hook_style['font_size'] + 15 - total_height = len(lines) * line_height - start_y = (height - total_height) // 2 - for i, line in enumerate(lines): - line_w, line_h = get_text_size(draw, line, font) - x = (width - line_w) // 2 - y = start_y + i * line_height - - draw_text_with_outline( - draw, (x, y), line, font, - hook_style['color'], - hook_style['outline_color'], - hook_style['outline_width'] - ) + if cover_font_size <= 28 or len(lines) <= 6: + break + cover_font_size -= 2 + font = get_cover_font(cover_font_size) + line_height = cover_font_size + 14 + total_height = len(lines) * line_height + start_y = (height - total_height) // 2 + for i, line in enumerate(lines): + line_w, line_h = get_text_size(draw, line, font) + x = (width - line_w) // 2 + x = max(VERTICAL_COVER_PADDING, min(width - VERTICAL_COVER_PADDING - line_w, x)) + y = start_y + i * line_height + draw_text_with_outline( + draw, (x, y), line, font, + hook_style['color'], + hook_style['outline_color'], + min(hook_style['outline_width'], 3) + ) + else: + cover_font_size = hook_style['font_size'] + font = get_cover_font(cover_font_size) + max_width = width - 80 + lines = [] + current_line = "" + for char in hook_text: + test_line = current_line + char + test_w, _ = get_text_size(draw, test_line, font) + if test_w <= max_width: + current_line = test_line + else: + if current_line: + lines.append(current_line) + current_line = char + if current_line: + lines.append(current_line) + line_height = cover_font_size + 12 + total_height = len(lines) * line_height + start_y = (height - total_height) // 2 + for i, line in enumerate(lines): + line_w, line_h = get_text_size(draw, line, font) + x = (width - line_w) // 2 + y = start_y + i * line_height + draw_text_with_outline( + draw, (x, y), line, font, + hook_style['color'], + hook_style['outline_color'], + hook_style['outline_width'] + ) img.save(output_path, 'PNG') return output_path @@ -406,29 +493,39 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None): # ============ 字幕图片生成 ============ def create_subtitle_image(text, width, height, output_path): - """创建字幕图片(关键词加粗加大突出)""" + """创建字幕图片(关键词加粗加大突出)。竖屏 498 宽时字号略小、保证居中且不溢出。""" style = STYLE['subtitle'] img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) + # 竖屏窄幅时缩小字号,保证整行在画面内且居中 base_size = style['font_size'] - kw_size = base_size + style.get('keyword_size_add', 4) + if (width, height) == (VERTICAL_W, VERTICAL_H): + base_size = min(base_size, 38) font = get_font(FONT_BOLD, base_size) - kw_font = get_font(FONT_HEAVY, kw_size) # 关键词用粗体+大字 text_w, text_h = get_text_size(draw, text, font) + while text_w > width - 80 and base_size > 24: + base_size -= 2 + font = get_font(FONT_BOLD, base_size) + text_w, text_h = get_text_size(draw, text, font) + kw_size = base_size + style.get('keyword_size_add', 4) + kw_font = get_font(FONT_HEAVY, kw_size) - # 字幕完全居中 + # 字幕完全居中(水平+垂直正中间);竖屏时限制在界面内不超出 base_x = (width - text_w) // 2 - base_y = height - text_h - style['margin_bottom'] + if (width, height) == (VERTICAL_W, VERTICAL_H): + pad = 24 + base_x = max(pad, min(width - pad - text_w, base_x)) + base_y = (height - text_h) // 2 - # 背景条 + # 背景条(不超出画布) padding = 15 bg_rect = [ - base_x - padding - 10, - base_y - padding, - base_x + text_w + padding + 10, - base_y + text_h + padding + max(0, base_x - padding - 10), + max(0, base_y - padding), + min(width, base_x + text_w + padding + 10), + min(height, base_y + text_h + padding) ] # 绘制圆角背景 @@ -562,8 +659,8 @@ def _parse_clip_index(filename: str) -> int: def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_path, - force_burn_subs=False, skip_subs=False): - """增强单个切片。检测原片是否已有字幕,有则跳过烧录,无则烧录中文""" + force_burn_subs=False, skip_subs=False, vertical=False): + """增强单个切片。vertical=True 时最后裁成竖屏 498x1080 直出成片。""" print(f"\n处理: {os.path.basename(clip_path)}") @@ -573,16 +670,21 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa print(f" 分辨率: {width}x{height}, 时长: {duration:.1f}秒") - hook_text = highlight_info.get('hook_3sec') or highlight_info.get('title') or '' + # 前3秒优先用「提问问题」:有 question 则封面/前贴先展示提问,再播回答 + hook_text = highlight_info.get('question') or highlight_info.get('hook_3sec') or highlight_info.get('title') or '' if not hook_text and clip_path: m = re.search(r'\d+[_\s]+(.+?)(?:_enhanced)?\.mp4$', os.path.basename(clip_path)) if m: hook_text = m.group(1).strip() cover_duration = STYLE['cover']['duration'] + # 竖屏成片:封面/字幕按 498x1080 做,叠在裁切区域,文字与字幕在竖屏上完整且居中 + out_w, out_h = (VERTICAL_W, VERTICAL_H) if vertical else (width, height) + overlay_pos = f"{OVERLAY_X}:0" if vertical else "0:0" + # 1. 生成封面 cover_img = os.path.join(temp_dir, 'cover.png') - create_cover_image(hook_text, width, height, cover_img, clip_path) + create_cover_image(hook_text, out_w, out_h, cover_img, clip_path) print(f" ✓ 封面生成") # 2. 字幕逻辑:有字幕/图片则跳过,无则烧录中文 @@ -610,7 +712,7 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa print(f" ✓ 字幕解析 ({len(subtitles)}条),已转中文") for i, sub in enumerate(subtitles[:50]): img_path = os.path.join(temp_dir, f'sub_{i:04d}.png') - create_subtitle_image(sub['text'], width, height, img_path) + create_subtitle_image(sub['text'], out_w, out_h, img_path) sub_images.append({'path': img_path, 'start': sub['start'], 'end': sub['end']}) if sub_images: print(f" ✓ 字幕图片 ({len(sub_images)}张)") @@ -622,12 +724,12 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa # 5. 构建FFmpeg命令 current_video = clip_path - # 5.1 添加封面 + # 5.1 添加封面(竖屏时叠在 x=543,与最终裁切区域对齐) cover_output = os.path.join(temp_dir, 'with_cover.mp4') cmd = [ 'ffmpeg', '-y', '-i', current_video, '-i', cover_img, - '-filter_complex', f"[0:v][1:v]overlay=0:0:enable='lt(t,{cover_duration})'[v]", + '-filter_complex', f"[0:v][1:v]overlay={overlay_pos}:enable='lt(t,{cover_duration})'[v]", '-map', '[v]', '-map', '0:a', '-c:v', 'libx264', '-preset', 'fast', '-crf', '22', '-c:a', 'copy', cover_output @@ -638,7 +740,7 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa current_video = cover_output print(f" ✓ 封面烧录") - # 5.2 分批烧录字幕 + # 5.2 分批烧录字幕(封面结束后才显示,不盖住封面) if sub_images: batch_size = 8 for batch_idx in range(0, len(sub_images), batch_size): @@ -650,12 +752,16 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa filters = [] last_output = '0:v' - for i, img in enumerate(batch): input_idx = i + 1 output_name = f'v{i}' - enable = f"between(t,{img['start']:.3f},{img['end']:.3f})" - filters.append(f"[{last_output}][{input_idx}:v]overlay=0:0:enable='{enable}'[{output_name}]") + # 封面结束后才显示字幕,不盖住封面 + sub_start = max(img['start'], cover_duration) + if sub_start < img['end']: + enable = f"between(t,{sub_start:.3f},{img['end']:.3f})" + filters.append(f"[{last_output}][{input_idx}:v]overlay={overlay_pos}:enable='{enable}'[{output_name}]") + else: + filters.append(f"[{last_output}]copy[{output_name}]") last_output = output_name filter_complex = ';'.join(filters) @@ -693,8 +799,17 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa current_video = speed_output print(f" ✓ 加速10%") - # 5.4 复制到输出 - shutil.copy(current_video, output_path) + # 5.4 输出:竖屏则裁成 498x1080 直出,否则直接复制 + if vertical: + r = subprocess.run([ + 'ffmpeg', '-y', '-i', current_video, + '-vf', CROP_VF, '-c:a', 'copy', output_path + ], capture_output=True, text=True) + if r.returncode != 0: + print(f" ❌ 竖屏裁剪失败: {r.stderr[:200]}", file=sys.stderr) + shutil.copy(current_video, output_path) + else: + shutil.copy(current_video, output_path) if os.path.exists(output_path): size_mb = os.path.getsize(output_path) / (1024 * 1024) @@ -709,7 +824,9 @@ def main(): parser.add_argument("--clips", "-c", help="切片目录") parser.add_argument("--highlights", "-l", help="highlights.json 路径") parser.add_argument("--transcript", "-t", help="transcript.srt 路径") - parser.add_argument("--output", "-o", help="输出目录") + parser.add_argument("--output", "-o", help="输出目录(成片时填 成片 文件夹路径)") + parser.add_argument("--vertical", action="store_true", help="成片直出竖屏 498x1080,与封面+字幕一起输出到 -o 目录") + parser.add_argument("--title-only", action="store_true", help="输出文件名为纯标题(无序号、无_enhanced),与 --vertical 搭配用于成片") parser.add_argument("--skip-subs", action="store_true", help="跳过字幕烧录(原片已有字幕时用)") parser.add_argument("--force-burn-subs", action="store_true", help="强制烧录字幕(忽略检测)") args = parser.parse_args() @@ -729,12 +846,14 @@ def main(): print(f"❌ transcript 不存在: {transcript_path}") return + vertical = getattr(args, 'vertical', False) + title_only = getattr(args, 'title_only', False) print("="*60) - print("🎬 Soul切片增强处理(Pillow,无需 drawtext)") + print("🎬 Soul切片增强" + ("(成片竖屏直出)" if vertical else "")) print("="*60) - print(f"功能: 封面+字幕+加速10%+去语气词") + print(f"功能: 封面+字幕+加速10%+去语气词" + ("+竖屏498x1080" if vertical else "")) print(f"输入: {clips_dir}") - print(f"输出: {output_dir}") + print(f"输出: {output_dir}" + ("(成片,文件名=标题)" if title_only else "")) print("="*60) output_dir.mkdir(parents=True, exist_ok=True) @@ -754,13 +873,19 @@ def main(): clip_num = _parse_clip_index(clip_path.name) or (i + 1) highlight_info = highlights[clip_num - 1] if 0 < clip_num <= len(highlights) else {} - output_path = output_dir / clip_path.name.replace('.mp4', '_enhanced.mp4') + if getattr(args, 'title_only', False): + title = (highlight_info.get('title') or highlight_info.get('hook_3sec') or clip_path.stem) + name = sanitize_filename(title) + '.mp4' + output_path = output_dir / name + else: + output_path = output_dir / clip_path.name.replace('.mp4', '_enhanced.mp4') temp_dir = tempfile.mkdtemp(prefix='enhance_') try: if enhance_clip(str(clip_path), str(output_path), highlight_info, temp_dir, str(transcript_path), force_burn_subs=getattr(args, 'force_burn_subs', False), - skip_subs=getattr(args, 'skip_subs', False)): + skip_subs=getattr(args, 'skip_subs', False), + vertical=getattr(args, 'vertical', False)): success_count += 1 finally: shutil.rmtree(temp_dir, ignore_errors=True) @@ -773,12 +898,12 @@ def main(): generate_index(highlights, output_dir) def generate_index(highlights, output_dir): - """生成目录索引(标题/Hook/CTA 统一简体中文)""" - index_path = output_dir.parent / "目录索引_enhanced.md" + """生成目录索引(标题/Hook/CTA 统一简体中文),索引写在输出目录内""" + index_path = output_dir / "目录索引.md" with open(index_path, 'w', encoding='utf-8') as f: - f.write("# Soul派对 - 增强版切片目录\n\n") - f.write(f"**优化**: 封面+字幕+加速10%+去语气词\n\n") + f.write("# Soul派对 - 成片目录\n\n") + f.write("**优化**: 封面+字幕+加速10%+去语气词(成片含竖屏时已裁为498×1080)\n\n") f.write("## 切片列表\n\n") f.write("| 序号 | 标题 | Hook | CTA |\n") f.write("|------|------|------|-----|\n") diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_vertical_crop.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_vertical_crop.py new file mode 100644 index 00000000..cc551a82 --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_vertical_crop.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Soul 竖屏中段裁剪(批量) +横版 1920×1080 → 竖屏 498×1080,去左右白边。 +参数与 SKILL「Soul 竖屏成片」一致,以后剪辑 Soul 视频统一用此脚本。 +""" +import argparse +import re +import subprocess +import sys +from pathlib import Path + +# 固定参数(与 SKILL 一致) +CROP_VF = "crop=608:1080:483:0,crop=498:1080:60:0" +OUT_SUFFIX = "_竖屏中段" + + +def main(): + parser = argparse.ArgumentParser(description="Soul 竖屏中段批量裁剪") + parser.add_argument("--dir", "-d", required=True, help="切片目录(clips 或 clips_enhanced)") + parser.add_argument("--suffix", "-s", default=OUT_SUFFIX, help="输出文件名后缀") + parser.add_argument("--pattern", "-p", default="*_enhanced.mp4", help="匹配文件,如 *.mp4 表示所有 mp4") + parser.add_argument("--out-dir", "-o", default="", help="输出目录(默认同 --dir)") + parser.add_argument("--title-only", action="store_true", help="输出文件名仅用标题(去掉序号、竖屏中段等)") + parser.add_argument("--dry-run", action="store_true", help="只列文件不执行") + args = parser.parse_args() + + base = Path(args.dir).resolve() + if not base.is_dir(): + print(f"❌ 目录不存在: {base}", file=sys.stderr) + sys.exit(1) + + out_dir = Path(args.out_dir).resolve() if args.out_dir else base + out_dir.mkdir(parents=True, exist_ok=True) + + files = sorted(base.glob(args.pattern)) + files = [f for f in files if OUT_SUFFIX not in f.stem and "_竖屏中段" not in f.stem] + if not files: + print(f" 未找到 {args.pattern}(已排除竖屏中段): {base}") + return + + print(f"📁 {base} → {out_dir}") + print(f" 共 {len(files)} 个将做竖屏中段裁剪") + if args.dry_run: + for f in files: + print(f" - {f.name}") + return + + for f in files: + if getattr(args, "title_only", False): + # 仅标题:去掉 soul112_01_、_enhanced、_竖屏中段 等,只保留标题 + stem = re.sub(r"^soul\d+_\d+_", "", f.stem) + stem = re.sub(r"_enhanced$", "", stem) + stem = re.sub(r"_竖屏中段$", "", stem) + out_name = stem + f.suffix + else: + out_name = f.stem + args.suffix + f.suffix + out_path = out_dir / out_name + cmd = [ + "ffmpeg", "-y", "-i", str(f), + "-vf", CROP_VF, + "-c:a", "copy", + str(out_path), + ] + print(f" {f.name} → {out_name}") + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode != 0: + print(f" ❌ 失败: {r.stderr[:200]}", file=sys.stderr) + else: + print(f" ✅ {out_path.name}") + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/SKILL_REGISTRY.md b/SKILL_REGISTRY.md index 8297ea1e..8d05b903 100644 --- a/SKILL_REGISTRY.md +++ b/SKILL_REGISTRY.md @@ -77,6 +77,7 @@ | W01 | 文件整理 | 水溪 | 整理文件、外置硬盘 | `02_卡人(水)/水溪_整理归档/文件整理/SKILL.md` | 文件分类、去重、归档 | | W02 | 文档清洗 | 水溪 | PDF转Markdown | `02_卡人(水)/水溪_整理归档/文档清洗/SKILL.md` | PDF/Word 转结构化 Markdown | | W03 | 对话归档 | 水溪 | 归档今日对话 | `02_卡人(水)/水溪_整理归档/对话归档/SKILL.md` | AI 对话记录收集与归类 | +| W03a | **项目调研** | 水溪 | **项目调研、平台分析、A群、A群聊天记录、聊天记录清理、对话分类、对话分类号、按项目归档、调研归档、APP资料、其他APP、各APP** | `02_卡人(水)/水溪_整理归档/项目调研/SKILL.md` | 平台分析/项目调研/各APP资料/群聊/对话分类统一归档到 开发/7.项目调研,按项目分子目录 | | W04 | 自动记忆管理 | 水溪 | 记忆、存入记忆 | `02_卡人(水)/水溪_整理归档/自动记忆管理/SKILL.md` | 长期记忆写入与检索 | | W05 | 需求拆解与计划制定 | 水泉 | 需求拆解、任务分析 | `02_卡人(水)/水泉_规划拆解/需求拆解与计划制定/SKILL.md` | 大需求拆成可执行步骤 | | W06 | 任务规划 | 水泉 | 任务规划、制定计划 | `02_卡人(水)/水泉_规划拆解/任务规划/SKILL.md` | 制定执行计划与排期 | @@ -87,6 +88,7 @@ | W11 | Soul派对运营报表 | 水桥 | **运营报表、派对填表、派对截图填表发群、派对纪要、智能纪要、106场、107场、本月运营数据** | `02_卡人(水)/水桥_平台对接/飞书管理/运营报表_SKILL.md` | 派对截图+TXT→飞书运营报表→智能纪要→飞书群推送,含Token自刷新与写入校验 | | W12 | MCP 搜索与连接 | 水桥 | **MCP、找MCP、连接MCP、MCP搜索、发现MCP、添加MCP、需要MCP、MCP安装、MCP发现、查MCP、装MCP** | `02_卡人(水)/水桥_平台对接/MCP管理/SKILL.md` | 搜索 5000+ MCP 服务器→生成安装配置→写入 Cursor/Claude 等 | | W13 | Excel表格与日报 | 水桥 | **Excel写飞书、Excel导入飞书、批量写飞书表格、飞书表格导入、CSV写飞书、日报图表发飞书、表格日报** | `02_卡人(水)/水桥_平台对接/飞书管理/Excel表格与日报_SKILL.md` | 本地 Excel/CSV→飞书表格→自动日报图表→发飞书群 | +| W14 | **卡猫复盘** | 水桥 | **卡猫复盘、婼瑄复盘、卡猫今日复盘、婼瑄今日、复盘到卡猫、发卡猫群** | `02_卡人(水)/水桥_平台对接/飞书管理/卡猫复盘/SKILL.md` | 婼瑄目录→目标=今年总目标+完成%+人/事/数具体→飞书+卡猫群 | ## 木组 · 卡木(产品内容创造) @@ -94,6 +96,7 @@ |:--|:---|:---|:---|:---|:---| | M01 | 视频切片 | 木叶 | **视频剪辑、切片发布、切片动效包装、程序化包装、片头片尾、批量封面、视频包装** | `03_卡木(木)/木叶_视频内容/视频切片/SKILL.md` | 长视频切片+字幕+发布;联动切片动效包装(片头/片尾/程序化) | | M01b | 抖音视频解析 | 木叶 | **抖音视频、抖音链接、抖音解析、抖音下载、提取抖音文案、抖音无水印** | `03_卡木(木)/木叶_视频内容/抖音视频解析/SKILL.md` | 链接→解析ID→提取文案→下载无水印视频 | +| M01c | 抖音发布 | 木叶 | **抖音发布、发布到抖音、抖音登录、抖音上传、腕推抖音** | `03_卡木(木)/木叶_视频内容/抖音发布/SKILL.md` | 开放平台 OAuth 登录 + 上传/创建视频发布;可对接腕推/存客宝 | | M02 | 网站逆向分析 | 木根 | 逆向分析、模拟登录 | `03_卡木(木)/木根_逆向分析/网站逆向分析/SKILL.md` | 网站 API 分析、SDK 生成 | | M03 | 项目生成 | 木果 | 生成项目、五行模板 | `03_卡木(木)/木果_项目模板/项目生成/SKILL.md` | 按五行模板生成新项目 | | M04 | 开发模板 | 木果 | 创建项目、初始化模板 | `03_卡木(木)/木果_项目模板/开发模板/SKILL.md` | 前后端项目模板库 | @@ -155,8 +158,8 @@ | 组 | 负责人 | 成员数 | 技能数 | |:--|:---|:--|:--| | 金 | 卡资 | 2 | 21 | -| 水 | 卡人 | 3 | 12 | +| 水 | 卡人 | 3 | 13 | | 木 | 卡木 | 3 | 8 | | 火 | 卡火 | 4 | 15 | | 土 | 卡土 | 4 | 7 | -| **合计** | **5** | **14** | **63** | +| **合计** | **5** | **14** | **64** | diff --git a/运营中枢/参考资料/giffgaff发短信收短信_流程史记.md b/运营中枢/参考资料/giffgaff发短信收短信_流程史记.md new file mode 100644 index 00000000..3f6c040d --- /dev/null +++ b/运营中枢/参考资料/giffgaff发短信收短信_流程史记.md @@ -0,0 +1,81 @@ +# giffgaff 发短信 / 收短信 · 流程史记 + +> **用途**:一页查清 giffgaff 英国卡「发短信、收短信、保号」全流程;搜索「发短信」「收短信」「giffgaff」即可定位本文。 +> 更新:2026-03-01 + +--- + +## 一、总览 + +| 动作 | 入口 | 说明 | +|:---|:---|:---| +| **发短信** | 手机自带「信息」App | 官网/My giffgaff **无网页发短信**,只能用手机发 | +| **收短信** | 手机「信息」App | 验证码、通知等均在手机端查看 | +| **登录/查余额** | giffgaff.com → Log in / My giffgaff | 网页可查余额、套餐、订单,不能看/发短信 | + +--- + +## 二、激活与登录(前置) + +1. **插卡**:收到 giffgaff SIM 后插入手机,约 **30 分钟内**会收到欢迎短信。 +2. **登录官网**:打开 [giffgaff.com](https://www.giffgaff.com) → 右上角 **Log in** → 用手机号 + 收短信验证码登录。 +3. **建议**:登录后改好邮箱、密码,方便以后找回与保号提醒。 + +--- + +## 三、发短信(完整流程) + +### 3.1 在哪里发 + +- **唯一入口**:手机自带「信息 / Messages」App,用 giffgaff 号码发送。 +- **官网/My giffgaff**:仅支持查余额、套餐、订单,**不支持**在网页上发短信或看短信记录。 + +### 3.2 号码格式 + +- **发给英国号码**:`+44 7XXX XXXXXX`(或 07XXX XXXXXX)。 +- **发给中国号码**:`+86138XXXXXXXX` 或 `0086138XXXXXXXX`(带国家区号)。 + +### 3.3 常见问题与排查 + +| 现象 | 处理办法 | +|:---|:---| +| **新卡发不出短信** | 手机设置 → 蜂窝网络 → 运营商:先选「中国移动」,仍不行再试「中国联通」。 | +| **eSIM 或换机后发不出** | 打开「信息」→ 检查 **短信中心号码** 是否为 `+447802002606`,不对则手动改为该号码。 | +| **无信号/无服务** | 查 [giffgaff 覆盖](https://www.giffgaff.com/coverage);新卡可等最多约 24 小时再试;必要时手动选网/重启。 | + +--- + +## 四、收短信 + +- 所有短信(验证码、通知、普通短信)都在手机「信息」里收,与普通 SIM 一致。 +- 可接收英国及国际号码发来的短信;收短信一般不单独扣费(以套餐/资费说明为准)。 + +--- + +## 五、保号(避免号码被回收) + +- **规则**:**180 天内** 账户余额必须有 **至少一次变动**(充值或消费),否则号码可能被回收。 +- **最低成本保号**:发一条短信即可,约 **0.3 英镑/条**(以官网当前资费为准)。 +- **可操作**: + - 给任意号码发一条短信(如给自己另一个号),或 + - 发短信到 giffgaff 客服:**+447973000186**。 +- **建议**:在日历里设「每 5~6 个月提醒」发一条短信或做一次充值,避免忘掉 180 天。 + +--- + +## 六、其他注意 + +- **关闭语音信箱**:在拨号盘输入 **##002#** 可关闭语音信箱,避免漏接来电被转语音信箱产生扣费。 +- **资费与帮助**:以 [help.giffgaff.com](https://help.giffgaff.com) 及 My giffgaff 当前说明为准;发不出短信时可看 [Why can't I send a text?](https://help.giffgaff.com/en/articles/243850-why-can-t-i-send-a-text)。 + +--- + +## 七、快速检索关键词 + +本文档覆盖并可直接搜索: + +- **发短信**、**收短信**、**giffgaff**、**英国卡**、**保号**、**短信中心号码**、**eSIM**、**无法发短信**、**My giffgaff**、**登录**、**180天**、**+447802002606**、**+447973000186**、**##002#** + +--- + +*史记归位:运营中枢/参考资料 · 卡若AI* diff --git a/运营中枢/参考资料/卡猫复盘格式_固定规则.md b/运营中枢/参考资料/卡猫复盘格式_固定规则.md new file mode 100644 index 00000000..d7a6e02a --- /dev/null +++ b/运营中枢/参考资料/卡猫复盘格式_固定规则.md @@ -0,0 +1,73 @@ +# 卡猫复盘格式(固定规则) + +> **专用规则。** 卡猫复盘是**以婼瑄目录为唯一素材**的复盘,目标·结果**仅指卡猫今年总目标及完成百分比**;正文中**人、事、数必须具体**,不得泛写。 + +--- + +## 一、目标·结果·达成率的含义(强制) + +- **🎯 目标·结果·达成率** 在本格式下**仅指**: + - **目标** = **卡猫当年(如 2026 年)总目标**(一句说清年度主目标,可多句拆为 1 2 3,每句 ≤30 字); + - **结果** = 截至本复盘日,相对该总目标的**当前达成情况**(一句); + - **达成率** = **完成百分比 XX%**(可估算,需有数字)。 +- 不得用「本次任务目标」「本日复盘目标」等替代;若当年总目标尚未成文,可写「卡猫 20XX 年总目标:XXX(待补充);当前完成约 X%」。 + +**示例(仅作格式参考):** + +``` +🎯 目标·结果·达成率 +卡猫 2026 年总目标:融 2000 万 + 电竞实体落地 + 流水并表。当前:流水/开票线在对接,实体与融资节奏在推进;整体完成约 5%。 +``` + +--- + +## 二、人、事、数必须具体(强制) + +- **人**:出现即写**真实称呼/角色**(如叶倩如、郑朝阳、郑清土、财务、腾讯侧),不写「某人」「有人」「说话人」。 +- **事**:写**具体事件/动作**(如「2 月 28 日叶倩如侧聊流量开票与下游消化」「与财务对平台可开品名与票点」),不写「讨论了问题」「做了沟通」。 +- **数**:能写数字的必须写(如年 10 亿流水/票、广告费流水 15%、6 点专票 vs 1 点普票、第一轮 20% 换 500 万、Gateway 端口 18789),不写「很多」「若干」。 + +--- + +## 三、完整格式(与卡若复盘五块一致) + +卡猫复盘仍用**完整复盘五块**,仅对「目标·结果·达成率」和「内容具体度」做上述约束: + +``` +[卡若复盘](YYYY-MM-DD HH:mm) + +🎯 目标·结果·达成率 +卡猫 [年份] 年总目标:[一句或 1 2 3 句,每句 ≤30 字]。当前:[一句]。整体完成约 XX%。 + +📌 过程 +1. [具体人] [具体事];[具体数字]。(一句) +2. … +3. … + +💡 反思 +1. … +2. … + +📝 总结 +… + +▶ 下一步执行 +… +``` + +- 过程/反思/总结/下一步中涉及的人、事、数均须**具体**,见第二节。 + +--- + +## 四、素材目录与输出(强制) + +- **素材目录**:**仅婼瑄目录**(`/Users/karuo/Library/Mobile Documents/com~apple~CloudDocs/Documents/婼瑄`),含其下所有子目录(如 叶倩如、复盘、天恩 等)。不得以其他目录作为卡猫复盘素材来源。 +- **文件选择**:当次复盘所依据的文件 = 婼瑄目录下**当日或指定日期新增/修改**的 txt、md 等可读文件;或用户指定的文件/日期范围。 +- **输出**:复盘 Markdown 存于 **婼瑄/复盘/**,命名 `YYYY-MM-DD_卡猫复盘_主题.md`;发布到飞书(卡猫知识库节点)并推送到卡猫群(webhook)。 + +--- + +## 五、引用关系 + +- 本文件为**卡猫复盘**专用格式;通用复盘格式仍见 `卡若复盘格式_固定规则.md`。 +- 执行入口:`02_卡人(水)/水桥_平台对接/飞书管理/卡猫复盘/SKILL.md`;触发词含「卡猫复盘」「婼瑄复盘」等。 diff --git a/运营中枢/工作台/00_卡若AI总索引.md b/运营中枢/工作台/00_卡若AI总索引.md index 3a129475..280d3674 100644 --- a/运营中枢/工作台/00_卡若AI总索引.md +++ b/运营中枢/工作台/00_卡若AI总索引.md @@ -59,6 +59,16 @@ 查报表、对账、同步 → 上表;做财务分析、报表生成 → 用土簿/财务管理 Skill。 +### 项目调研(统一归档) + +| 路径 | 说明 | +|:---|:---| +| **根目录** | `/Users/karuo/Documents/开发/7.项目调研` | +| 规范与项目列表 | `开发/7.项目调研/README.md` | +| Skill | 水溪 · 项目调研(平台分析、聊天记录、对话分类、APP 资料 → 按项目归档) | + +凡「项目调研」「平台分析」「A 群/聊天记录清理」「对话分类」「按项目归档」→ 产出与归档均进 `7.项目调研`,由水溪 项目调研 Skill 处理。 + ### 开发文档(已整合到 运营中枢) | 路径/文档 | 说明 | diff --git a/运营中枢/工作台/00_能力总索引.md b/运营中枢/工作台/00_能力总索引.md index 9ad1e65c..8fa65116 100644 --- a/运营中枢/工作台/00_能力总索引.md +++ b/运营中枢/工作台/00_能力总索引.md @@ -13,7 +13,7 @@ ──────────────────────────────────────────────────────────────────────── 服务器/磁盘/NAS/备份/数据库/局域网/iPhone -> 卡资 → 金仓 -> 检查、清理、备份、管理 存客宝/远程部署/数据库/微信解析 -> 卡资 → 金盾 -> 存客宝、部署、数据 -整理/归档/飞书/纪要/需求拆解/任务规划/小程序 -> 卡人 → 水溪/水泉/水桥 -> 整理、规划、同步 +整理/归档/飞书/纪要/需求拆解/任务规划/小程序/项目调研/平台分析/聊天记录清理 -> 卡人 → 水溪/水泉/水桥 -> 整理、规划、同步、调研归档 视频切片/逆向/项目/模板/档案/前端 -> 卡木 → 木叶/木根/木果 -> 视频、逆向、项目 全栈/消息/读书/文档清洗/代码修复/追问/本地模型 -> 卡火 → 火炬/火锤/火眼/火种 -> 开发、修复、学习 商业/技能工厂/流量/招商/财务/报表 -> 卡土 → 土基/土砖/土渠/土簿 -> 算账、复制、流量、财务 @@ -33,7 +33,7 @@ | **设备管理** | iPhone管理、局域网控制、iCloud管理 | 金仓 | | **数据安全** | 容灾备份、微信管理、照片分类 | 金仓 | | **开发辅助** | 代码修复、智能追问、开发模板、项目生成、v0模型集成、Vercel部署、存客宝、个人档案生成器、任务规划 | 金盾 | -| **信息流程** | 飞书管理、智能纪要、小程序管理、需求拆解与计划制定、对话归档 | 水桥 | +| **信息流程** | 飞书管理、智能纪要、小程序管理、需求拆解与计划制定、对话归档、**项目调研** | 水桥/水溪 | | **知识管理** | 全栈开发、读书笔记、文档清洗 | 火炬 | | **效率工具** | 上帝之眼、前端生成、技能工厂、流量自动化、视频切片、网站逆向分析 | 火眸 | | **商业运营** | 财务管理、商业工具集、手机流量自动操作 | 土簿 | diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index 456335ef..70525dda 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -170,3 +170,4 @@ | 2026-02-27 10:53:48 | 🔄 卡若AI 同步 2026-02-27 10:53 | 更新:Cursor规则、水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 | | 2026-02-28 06:25:45 | 🔄 卡若AI 同步 2026-02-28 06:25 | 更新:水桥平台对接、卡木、火炬、运营中枢工作台 | 排除 >20MB: 14 个 | | 2026-02-28 13:25:18 | 🔄 卡若AI 同步 2026-02-28 13:25 | 更新:Cursor规则、金仓、火炬、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 14 个 | +| 2026-02-28 13:27:29 | 🔄 卡若AI 同步 2026-02-28 13:27 | 更新:火炬、运营中枢工作台 | 排除 >20MB: 14 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index fca232aa..855e339e 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -173,3 +173,4 @@ | 2026-02-27 10:53:48 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-27 10:53 | 更新:Cursor规则、水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-28 06:25:45 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-28 06:25 | 更新:水桥平台对接、卡木、火炬、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-28 13:25:18 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-28 13:25 | 更新:Cursor规则、金仓、火炬、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | +| 2026-02-28 13:27:29 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-28 13:27 | 更新:火炬、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | diff --git a/运营中枢/技能路由/SKILL_INDEX.md b/运营中枢/技能路由/SKILL_INDEX.md index d92e7c4f..9d6ec2c8 100644 --- a/运营中枢/技能路由/SKILL_INDEX.md +++ b/运营中枢/技能路由/SKILL_INDEX.md @@ -64,6 +64,7 @@ | 小程序管理 | 水桥/小程序管理/SKILL.md | | 需求拆解与计划制定 | 水泉/需求拆解与计划制定/SKILL.md | | 对话归档 | 水溪/对话归档/SKILL.md | +| **项目调研** | 水溪_整理归档/项目调研/SKILL.md | ---