🔄 卡若AI 同步 2026-03-01 06:14 | 更新:金盾、水桥平台对接、水溪整理归档、卡木、总索引与入口、运营中枢参考资料、运营中枢工作台、运营中枢技能路由 | 排除 >20MB: 14 个
This commit is contained in:
@@ -27,6 +27,10 @@ updated: "2026-02-16"
|
||||
2. **部署**:Vercel、宝塔、GitHub Webhook(见 Vercel与v0部署流水线)
|
||||
3. **运维**:数据库清理(见 数据库管理)、服务器管理
|
||||
|
||||
## 抖音发布
|
||||
|
||||
存客宝当前**无抖音登录/发布 SDK**。若需将视频发布到抖音,使用「抖音发布」Skill(抖音开放平台 OAuth + 上传/创建视频);若后续存客宝或腕推提供抖音发布能力,可在 `03_卡木(木)/木叶_视频内容/抖音发布/SKILL.md` 中补充对接方式。
|
||||
|
||||
## 参考
|
||||
|
||||
- `开发文档/00_汇总索引.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 "<md路径>" --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% + 海报图,不发链接;新增推送逻辑文档与链路说明 |
|
||||
|
||||
@@ -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 已写在脚本内),无需复制链接,直接运行上述命令即可。
|
||||
|
||||
---
|
||||
|
||||
|
||||
48
02_卡人(水)/水桥_平台对接/Soul创业实验/上传/推送逻辑.md
Normal file
48
02_卡人(水)/水桥_平台对接/Soul创业实验/上传/推送逻辑.md
Normal file
@@ -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 "<md路径>" --part part-4 --chapter chapter-9 --price 1.0`。
|
||||
3. 推送飞书群:`scripts/send_chapter_poster_to_feishu.py <章节id> "<章节标题>" --md "<同一 md 路径>"`。
|
||||
|
||||
推送后,飞书群收到:① 标题 + 前 6% 正文;② 海报图(含该章节小程序码)。不收到任何小程序链接。
|
||||
84
02_卡人(水)/水桥_平台对接/Soul创业实验/上传/飞书妙记转文章并发布.md
Normal file
84
02_卡人(水)/水桥_平台对接/Soul创业实验/上传/飞书妙记转文章并发布.md
Normal file
@@ -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
|
||||
- 以后换别的妙记,只需把「一句话指令」里的链接换成新链接,步骤不变。
|
||||
@@ -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 派对场次文章都用这套
|
||||
|
||||
79
02_卡人(水)/水桥_平台对接/飞书管理/卡猫复盘/SKILL.md
Normal file
79
02_卡人(水)/水桥_平台对接/飞书管理/卡猫复盘/SKILL.md
Normal file
@@ -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 + 飞书 + 卡猫群 |
|
||||
@@ -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"
|
||||
}
|
||||
87
02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_feishu_log.py
Normal file
87
02_卡人(水)/水桥_平台对接/飞书管理/脚本/write_today_feishu_log.py
Normal file
@@ -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()
|
||||
59
02_卡人(水)/水溪_整理归档/项目调研/SKILL.md
Normal file
59
02_卡人(水)/水溪_整理归档/项目调研/SKILL.md
Normal file
@@ -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.项目调研` 并按项目存放。
|
||||
87
03_卡木(木)/木叶_视频内容/抖音发布/SKILL.md
Normal file
87
03_卡木(木)/木叶_视频内容/抖音发布/SKILL.md
Normal file
@@ -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 <path> --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 条(同一应用下)。
|
||||
46
03_卡木(木)/木叶_视频内容/抖音发布/参考资料/抖音开放平台_登录与发布流程.md
Normal file
46
03_卡木(木)/木叶_视频内容/抖音发布/参考资料/抖音开放平台_登录与发布流程.md
Normal file
@@ -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 或本参考资料,脚本可改为调用对应接口。
|
||||
96
03_卡木(木)/木叶_视频内容/抖音发布/脚本/douyin_publish.py
Normal file
96
03_卡木(木)/木叶_视频内容/抖音发布/脚本/douyin_publish.py
Normal file
@@ -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())
|
||||
4
03_卡木(木)/木叶_视频内容/抖音发布/脚本/tokens.json.example
Normal file
4
03_卡木(木)/木叶_视频内容/抖音发布/脚本/tokens.json.example
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"access_token": "抖音开放平台 OAuth 获取的 access_token",
|
||||
"open_id": "用户 open_id"
|
||||
}
|
||||
@@ -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 | 单视频一键成片 | ⭐⭐ |
|
||||
|
||||
108
03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md
Normal file
108
03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md
Normal file
@@ -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`。
|
||||
124
03_卡木(木)/木叶_视频内容/视频切片/参考资料/主题片段提取规则.md
Normal file
124
03_卡木(木)/木叶_视频内容/视频切片/参考资料/主题片段提取规则.md
Normal file
@@ -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。
|
||||
33
03_卡木(木)/木叶_视频内容/视频切片/参考资料/竖屏中段裁剪参数说明.md
Normal file
33
03_卡木(木)/木叶_视频内容/视频切片/参考资料/竖屏中段裁剪参数说明.md
Normal file
@@ -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"
|
||||
```
|
||||
58
03_卡木(木)/木叶_视频内容/视频切片/参考资料/视频结构_提问回答与高光.md
Normal file
58
03_卡木(木)/木叶_视频内容/视频切片/参考资料/视频结构_提问回答与高光.md
Normal file
@@ -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秒展示,并做完整去语助词。
|
||||
@@ -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的文字稿应该是这样的格式:
|
||||
|
||||
313
03_卡木(木)/木叶_视频内容/视频切片/脚本/chapter_themes_to_highlights.py
Normal file
313
03_卡木(木)/木叶_视频内容/视频切片/脚本/chapter_themes_to_highlights.py
Normal file
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
76
03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_vertical_crop.py
Normal file
76
03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_vertical_crop.py
Normal file
@@ -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()
|
||||
@@ -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** |
|
||||
|
||||
81
运营中枢/参考资料/giffgaff发短信收短信_流程史记.md
Normal file
81
运营中枢/参考资料/giffgaff发短信收短信_流程史记.md
Normal file
@@ -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*
|
||||
73
运营中枢/参考资料/卡猫复盘格式_固定规则.md
Normal file
73
运营中枢/参考资料/卡猫复盘格式_固定规则.md
Normal file
@@ -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`;触发词含「卡猫复盘」「婼瑄复盘」等。
|
||||
@@ -59,6 +59,16 @@
|
||||
|
||||
查报表、对账、同步 → 上表;做财务分析、报表生成 → 用土簿/财务管理 Skill。
|
||||
|
||||
### 项目调研(统一归档)
|
||||
|
||||
| 路径 | 说明 |
|
||||
|:---|:---|
|
||||
| **根目录** | `/Users/karuo/Documents/开发/7.项目调研` |
|
||||
| 规范与项目列表 | `开发/7.项目调研/README.md` |
|
||||
| Skill | 水溪 · 项目调研(平台分析、聊天记录、对话分类、APP 资料 → 按项目归档) |
|
||||
|
||||
凡「项目调研」「平台分析」「A 群/聊天记录清理」「对话分类」「按项目归档」→ 产出与归档均进 `7.项目调研`,由水溪 项目调研 Skill 处理。
|
||||
|
||||
### 开发文档(已整合到 运营中枢)
|
||||
|
||||
| 路径/文档 | 说明 |
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
────────────────────────────────────────────────────────────────────────
|
||||
服务器/磁盘/NAS/备份/数据库/局域网/iPhone -> 卡资 → 金仓 -> 检查、清理、备份、管理
|
||||
存客宝/远程部署/数据库/微信解析 -> 卡资 → 金盾 -> 存客宝、部署、数据
|
||||
整理/归档/飞书/纪要/需求拆解/任务规划/小程序 -> 卡人 → 水溪/水泉/水桥 -> 整理、规划、同步
|
||||
整理/归档/飞书/纪要/需求拆解/任务规划/小程序/项目调研/平台分析/聊天记录清理 -> 卡人 → 水溪/水泉/水桥 -> 整理、规划、同步、调研归档
|
||||
视频切片/逆向/项目/模板/档案/前端 -> 卡木 → 木叶/木根/木果 -> 视频、逆向、项目
|
||||
全栈/消息/读书/文档清洗/代码修复/追问/本地模型 -> 卡火 → 火炬/火锤/火眼/火种 -> 开发、修复、学习
|
||||
商业/技能工厂/流量/招商/财务/报表 -> 卡土 → 土基/土砖/土渠/土簿 -> 算账、复制、流量、财务
|
||||
@@ -33,7 +33,7 @@
|
||||
| **设备管理** | iPhone管理、局域网控制、iCloud管理 | 金仓 |
|
||||
| **数据安全** | 容灾备份、微信管理、照片分类 | 金仓 |
|
||||
| **开发辅助** | 代码修复、智能追问、开发模板、项目生成、v0模型集成、Vercel部署、存客宝、个人档案生成器、任务规划 | 金盾 |
|
||||
| **信息流程** | 飞书管理、智能纪要、小程序管理、需求拆解与计划制定、对话归档 | 水桥 |
|
||||
| **信息流程** | 飞书管理、智能纪要、小程序管理、需求拆解与计划制定、对话归档、**项目调研** | 水桥/水溪 |
|
||||
| **知识管理** | 全栈开发、读书笔记、文档清洗 | 火炬 |
|
||||
| **效率工具** | 上帝之眼、前端生成、技能工厂、流量自动化、视频切片、网站逆向分析 | 火眸 |
|
||||
| **商业运营** | 财务管理、商业工具集、手机流量自动操作 | 土簿 |
|
||||
|
||||
@@ -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 个 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
| 小程序管理 | 水桥/小程序管理/SKILL.md |
|
||||
| 需求拆解与计划制定 | 水泉/需求拆解与计划制定/SKILL.md |
|
||||
| 对话归档 | 水溪/对话归档/SKILL.md |
|
||||
| **项目调研** | 水溪_整理归档/项目调研/SKILL.md |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user