🔄 卡若AI 同步 2026-03-22 21:22 | 更新:Cursor规则、金仓、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个
This commit is contained in:
@@ -41,6 +41,7 @@ alwaysApply: true
|
||||
- **终端命令**:一律直接执行,不询问,50 字内说明后执行
|
||||
- **常规操作**:优先命令行 + 复用现成流程,不提问
|
||||
- **默认零提问(强制)**:开发、改需求、跑脚本、查日志、部署类任务,**禁止**向卡若发起「是否执行」「要不要我…」「请选一个」等确认式提问。缺信息时:**先读仓库配置 / 代码 / 环境变量 / 文档** → 合理默认 → **直接做完**。仅当 **客观上无法代劳** 时极简说明缺什么(如:本机短信验证码、支付密码、用户明文密钥未配置、明确不可逆删除且规范要求人工确认)。
|
||||
- **Skill 更新后·流水线「下一步」= 立即开跑(强制)**:用户说「下一步」「接着跑」「从跑完下一步」「重新剪辑」「直到全部完成」「别问我直接执行」等,或**刚同步完**某条 Skill(如 `Soul竖屏切片`、`木叶_视频切片`)后要继续产出时,视为已授权**连续执行**:按该 Skill 的标准步骤从**当前缺口**自动往下做(例:缺 `transcript.srt` 则先抽音频+MLX/Whisper,再 `batch_clip`,再 `soul_enhance` 成片)。**第一步起就不要问**「要不要帮你跑」;用本机路径惯例(如 `~/Movies/soul视频/第N场_*_output/`、`卡若AI/03_卡木/.../视频切片/脚本`)与仓库内 `场次稿`/`highlights.json` 合理补全。仅当**客观阻塞**(如无原片文件、磁盘满、需用户独占凭证)时用**一句话**说明卡点,不展开选项问卷。
|
||||
- **直接执行模式**:当用户明确要求「直接做 / 不要罗列 / 别讲写了什么」时,以**改代码与跑通为主**,正文**极简**(可≤3句);**复盘块仍放在回复最末且保持完整五块**(与 `soul-karuo-dialogue.mdc` 一致)。
|
||||
- **飞书日志**:直接执行 `write_today_three_focus.py`,不询问
|
||||
- **对外输出**:报告/图片 → `/Users/karuo/Documents/卡若Ai的文件夹/` 下对应子目录;图片登记 `图片/图片索引.md`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"updated": "2026-03-22T06:29:42.195575+00:00",
|
||||
"updated": "2026-03-22T13:21:51.214081+00:00",
|
||||
"conversations": [
|
||||
{
|
||||
"对话ID": "9f39025b-f695-4d7b-aff7-c124226e307e",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: Soul竖屏切片
|
||||
description: Soul 派对视频→竖屏成片;字幕暖色条+金边(与封面墨绿区分)、纠错词表+关键词高亮(同字号单行)、concat 前导空白对齐主轨时间轴、音轨PTS同步补偿;推荐 --typewriter-subs 逐字渐显。流程含裁剪检查→soul_enhance。MLX→visual_enhance v8 可选。
|
||||
description: Soul 派对视频→竖屏成片;高光**单主题完整**(30s~5min/条,每场约 8~30 条)、去穿插话题;封面底**约 10% 轻模糊**(非全糊)。字幕暖色条+金边、纠错+关键词高亮、concat 前导空白、音轨 PTS;推荐 --typewriter-subs。流程:裁剪检查→soul_enhance。MLX→visual_enhance v8 可选。
|
||||
triggers: Soul竖屏切片、视频切片、热点切片、竖屏成片、派对切片、裁剪检查、重新截图、全画面标定、竖屏裁剪、全画面成片、letterbox、画面显示全、白边、飞书录屏、LTX、AI生成视频、Retake重剪、字幕优化、字幕同步、逐字字幕
|
||||
owner: 木叶
|
||||
group: 木
|
||||
version: "1.9"
|
||||
version: "2.1"
|
||||
updated: "2026-03-22"
|
||||
---
|
||||
|
||||
@@ -24,13 +24,17 @@ updated: "2026-03-22"
|
||||
|
||||
不再单独生成 `clips_enhanced`、`clips_竖屏`;成片由 `soul_enhance` 一步直出到 `成片/`。
|
||||
|
||||
**卡若 Cursor 执行约定**:用户说「下一步 / 接着跑 / 重新剪辑 / 直到完成」或刚更新本 Skill 后要出片时,Agent 按 `卡若AI/.cursor/rules/karuo-ai.mdc` **流水线续跑**条**直接执行**(从转录、切片到成片),**不先询问是否执行**。
|
||||
|
||||
---
|
||||
|
||||
## 二、视频结构:提问→回答 + 前3秒高光 + 去语助词
|
||||
|
||||
- **每个话题前均优先提问→回答**:先看片段有没有人提问;**有提问**则把**提问的问题**放到前3秒(封面/前贴),先展示问题再播回答;无提问则用金句/悬念作 hook。
|
||||
- **成片链路**:前3秒展示问题(或金句)→ 正片回答 → **整片去除语助词**(提问与回答部分均由 soul_enhance 清理)。
|
||||
- **高光**:按「3秒高光亮点」剪,每段 30~300 秒完整语义单元;高光识别若有提问须填 `question`,且 `hook_3sec` 与之一致。
|
||||
- **每个话题前均优先提问→回答**:先看片段有没有人提问;**有提问**则把**提问的问题**放到前3秒(封面/前贴),先展示问题再播回答;无提问则从**该片段口播里**抽**吸睛句或反问**作 `hook_3sec`,禁止与正片内容脱节。
|
||||
- **成片链路**:前3秒展示问题(或 hook)→ 正片展开 → **整片去除语助词**(提问与回答部分均由 soul_enhance 清理)。
|
||||
- **节奏与高光**:竖屏段内全程**高光清晰、节奏清楚**(剪去拖沓静音与跑题可在 enhance 链路的静音裁剪中体现;**话题边界以 highlights 起止为准,剪辑阶段就要裁干净**)。
|
||||
- **单主题、完整、有趣**:**一条成片只服务一个主题**;口播里若**临时插进其他话题**,在 `highlights` 对应时间段上**剪掉无关段**(或拆成另一条高光),保证**有头有尾、逻辑闭环**;选题要**有传播点**(反差、数字、痛点、金句),忌又长又平。
|
||||
- **高光**:按「约 3 秒开场」剪,每段 **30 秒~5 分钟(≤300 秒)**;若有提问须填 `question`,且 `hook_3sec` 与之一致或同气质。
|
||||
|
||||
详见:`参考资料/视频结构_提问回答与高光.md`、`参考资料/高光识别提示词.md`。
|
||||
|
||||
@@ -111,14 +115,16 @@ python3 soul_enhance.py \
|
||||
|
||||
---
|
||||
|
||||
## 四、高光与切片(30 秒~300 秒)
|
||||
## 四、高光与切片(30 秒~5 分钟 · 单主题 · 8~30 条)
|
||||
|
||||
| 项 | 规则 |
|
||||
|----|------|
|
||||
| **单段时长** | **30~300 秒**,由完整片段起止决定 |
|
||||
| **完整性** | 每段是一个完整话题/情节,有头有尾 |
|
||||
| **标题** | **一句刺激性观点**,**4~6 个汉字**为宜(单行封面好读、主题一眼懂);忌长句当文件名 |
|
||||
| **数量** | 建议 ≤10 段/场 |
|
||||
| **单段时长** | **30~300 秒(不超过 5 分钟)**;不足 30s 的亮点可合并进同主题相邻段,或放弃;超过 5min 必须拆主题或砍无关穿插 |
|
||||
| **单主题(硬规则)** | **一个 mp4 = 一个主题**,整段**完整**;中间任何**无关主题**的闲聊、插话、跑题段落一律**不收录**(在粗剪/高光表里收窄 `start_time`/`end_time` 或改切两条) |
|
||||
| **完整性 + 有趣** | 每段**有头有尾**(观众能听懂结论/态度);同时标题与 hook 要**有张力**(数字、对比、反常识、痛点),避免「正确的废话」 |
|
||||
| **条数/场** | 依内容密度 **约 8~30 条/场**(平均常见十几条);宁少勿滥,保证每条都达标:单主题 + 时长窗口 + 有趣 |
|
||||
| **时间戳** | `start_time`/`end_time` 必须以**整场 transcript.srt** 核对,避免「标题与画面」错位 |
|
||||
| **标题** | **一句刺激性观点**,**4~6 个汉字**为宜(单行封面好读);忌长句当文件名 |
|
||||
| **语助词** | 识别与剪辑须符合 `参考资料/高光识别提示词.md`,成片由 soul_enhance 统一去语助词 |
|
||||
|
||||
---
|
||||
@@ -126,7 +132,9 @@ python3 soul_enhance.py \
|
||||
## 五、成片:封面 + 字幕 + 竖屏
|
||||
|
||||
- **封面**:竖条画布内**不超出界面**;**半透明质感**(背景 alpha=165);深色渐变、左上角 Soul logo;**封面显示标题 = 成片文件名 = highlights.title**(去杠、去下划线后一致,无 `:|—/_`、无序号);标题严格居中、多行自动换行。透明度由 `VERTICAL_COVER_ALPHA` 调节。
|
||||
- **字幕**:封面结束后先留**约 3 秒纯画面**(无字幕),再开始叠字幕;字幕**居中**在竖条内。先尝试**单次 FFmpeg 通道**(一次 pass 完成所有字幕叠加,最快);若失败自动回退到分批模式(batch_size=40);语助词在解析阶段已由 `clean_filler_words` 去除。重新加字幕时加 `--force-burn-subs`。⚠️ 注意:当前 FFmpeg 不支持 drawtext/subtitles 滤镜,只能用 PIL 图像 overlay 方案。(脚本常量:`SUBS_START_AFTER_COVER_SEC`,默认 3.0)
|
||||
- **封面底层模糊(重要)**:**不要全屏强糊**。`soul_enhance.py` 默认 **`STYLE['cover']['bg_blur_mix']=0.1`**:清晰视频帧与一层高斯模糊按 **约 10% 混合**(`bg_blur_radius` 生成模糊层),界面仍大致可辨,仅轻微虚化衬托文字。若需更强/更弱,改脚本内两常量,勿回到「整帧 radius=50+ 全糊」。
|
||||
- **字幕**:**封面一结束即叠字幕**(无额外「空几秒再等字」);SRT 安全起点为封面结束 + **约 0.05s** epsilon,避免与最后一帧封面打架。字幕**居中**在竖条内。先尝试**单次 FFmpeg 通道**(一次 pass 完成所有字幕叠加,最快);若失败自动回退到分批模式(batch_size=40);语助词在解析阶段已由 `clean_filler_words` 去除。重新加字幕时加 `--force-burn-subs`。⚠️ 注意:当前 FFmpeg 不支持 drawtext/subtitles 滤镜,只能用 PIL 图像 overlay 方案。(脚本常量:`SUBS_START_AFTER_COVER_SEC`,**默认 0.0**)
|
||||
- **字幕字形**:Whisper 词级轴常在**中日文之间插空格**,逐字/逐词显字时会像「字与字被撑开」;脚本在 `improve_subtitle_punctuation` 路径对 **CJK 相邻空白**做折叠(`_collapse_cjk_interchar_spaces`),保证整句显示正常、无异常中空。
|
||||
- **封面标题**:高光 `title` 建议 **4~6 个汉字**;成片内封面主标题最多显示 **6 个汉字**(超长由 `soul_enhance` 自动截断,与文件名 `--title-only` 一致)。
|
||||
- **竖屏竖条**:**高固定 1080,宽 = analyze 的 OUTPUT_SIZE**,默认不压 498;细节见 `参考资料/竖屏中段裁剪参数说明.md`
|
||||
|
||||
@@ -137,8 +145,8 @@ python3 soul_enhance.py \
|
||||
| **与封面对比** | 封面为**半透明墨绿渐变**;字幕为**暖深棕圆角条 + 琥珀色描边**,避免与主题绿混成一团 |
|
||||
| **纠错** | `transcript.srt` 解析时走 `_improve_subtitle_text`(繁转简、CORRECTIONS 错词、违禁替换、去语助词);**渲染每一帧前**再走 `improve_subtitle_punctuation`,与口播稿对齐 |
|
||||
| **重点词** | `KEYWORDS` 列表命中则**亮金色高亮**(同字号同基线,仅颜色区分,避免大字号造成"两排字"),长词优先匹配 |
|
||||
| **逐字渐显** | 推荐成片加 **`--typewriter-subs`**:同一条字幕时间内前缀逐步加长,更贴人声节奏 |
|
||||
| **音画对齐** | 默认 `SUBTITLE_DELAY_SEC` + **音轨/视频首帧 PTS 差**按比例补偿(脚本内动态计算),减轻「字比声快」 |
|
||||
| **逐字渐显** | 推荐成片加 **`--typewriter-subs`**:同一条字幕时间内前缀逐步加长,更贴人声节奏;配合 CJK 去空格避免字间假空白 |
|
||||
| **音画对齐** | ① 切片起点与高光表一致(见上表 batch_clip)。② 默认 `SUBTITLE_DELAY_SEC=0`;若 ffprobe 首包音/视频 PTS 差 > 阈值则加小延迟。③ 仍偏差时用 `soul_enhance.py --subtitle-extra-delay 0.15`(秒,正数推迟字幕)整场微调。④ 成片 **先叠字幕再 10% 加速**,字幕与对白同倍率,不因加速错位。 |
|
||||
|
||||
### ⚠️ 字幕烧录常见坑(已修复)
|
||||
|
||||
@@ -147,12 +155,15 @@ python3 soul_enhance.py \
|
||||
| 字幕全跳过(转录稿异常误判) | `_parse_clip_index` 取到场次号(如 119)而非切片序号(01),导致 highlight_info 为空,start_sec=0 落入噪声区 | 改为取 `_数字_` 模式中**最小值**,119→01=1 ✓ |
|
||||
| 标题/文件名有下划线 | `sanitize_filename` 保留了 `_` | 现在 `_` 也替换为空格 |
|
||||
| 字幕烧录极慢(N/5 次 encode) | 原 batch_size=5,180 条字幕需 36 次 FFmpeg 重编码 | 改为单次通道(1 次 pass);失败时 batch_size=40 兜底 |
|
||||
| **字幕超前于说话(字幕比声音早)** | `batch_clip -ss` 输入端 seeking 导致切片从关键帧开始(早于请求时间 1-4s),字幕按请求时间算相对位置,导致超前 | **动态 PTS 检测**:`enhance_clip` 对每条切片用 FFprobe 检测首帧 PTS,动态计算精确 delay(不再用固定值);`SUBTITLE_DELAY_SEC=2.0` 作为兜底 |
|
||||
| **字幕与声音不对齐** | ① **主因**:`batch_clip -f`(stream copy)且 `-ss` 在 `-i` 前,输出从**关键帧**起剪,实际起点常比 `highlights.start_time` **早 0~3s**,整场 `transcript.srt` 仍按绝对时间裁 → 字与声错位。② 成片 **先烧字幕再整体 setpts/atempo 加速**,字幕与音轨同倍率,加速本身不引入相对漂移。 | **批量切片请用默认精确模式(勿加 `-f`)**:`batch_clip.py` 已改为「`-ss` 粗定位 + `-i` 后再 `-ss` 细裁 + 重编码」,起点与高光表一致;`SUBTITLE_DELAY_SEC=0`,仅当 ffprobe 音/视频首包 PTS 差 > 阈值时小幅 delay。 |
|
||||
| **封面期间出现字幕** | 字幕时间计算使字幕落在封面段(前 2.5s)内 | `write_clip_srt` 强制过滤 `end <= cover_duration` 的条目,并 `start = max(start, cover_duration)` |
|
||||
| **字幕含 ASR 噪声行(单字母 L / Agent)** | MLX Whisper 对静音/噪声段产生幻觉字符 | `_is_noise_line()` 提前过滤单字母、重复字符、噪声 token |
|
||||
| **繁体字幕未转简体** | Soul 派对录音有港台口音,ASR 输出繁体 | `_to_simplified()` 兜底 + CORRECTIONS 扩充 50+ 繁体常用字映射 |
|
||||
| **字幕「两排字」** | 关键词用更大字号(`keyword_size_add=6`)+ `base_y-1` 偏移,与正文交错形成两行错觉 | `keyword_size_add=0`、关键词 `base_y` 与正文一致、仅颜色区分(v1.9) |
|
||||
| **字幕整体前移 + 封面叠字幕** | concat demuxer 输出从 t=0 累加,未在开头插入 `[0, subtitle_overlay_start)` 透明段,导致字幕轨比主视频短约 5.5s | 在 `sub_concat.txt` 开头插入 `blank` + `duration=subtitle_overlay_start`(v1.9) |
|
||||
| **封面后长时间无字幕** | 旧版 `SUBS_START_AFTER_COVER_SEC=3` + SRT `safe_start` 再 +0.3s | 默认改为 **0** + **0.05s** epsilon(v2.0) |
|
||||
| **逐字字幕字间像被撑开** | Whisper `word_times` 在汉字间带空格 | `_collapse_cjk_interchar_spaces` 写入标点/安全替换链路(v2.0) |
|
||||
| **封面底图全糊、界面看不清** | 旧版整帧 `GaussianBlur(radius≈52)` + 叠层 | 改为 **清晰帧与模糊层 `Image.blend` 约 10%**(`bg_blur_mix` / `bg_blur_radius`,v2.1) |
|
||||
| **highlights 时间戳不准** | 某些高光段实际对应静音区,Whisper 产生幻觉 | 在转录稿中搜索话题关键词确认真实时间戳,修正后重新切片 |
|
||||
|
||||
---
|
||||
@@ -195,6 +206,7 @@ python3 analyze_feishu_ui_crop.py "/path/本场原片.mp4" --at 0.2 --save-dir "
|
||||
python3 batch_clip.py -i "原视频.mp4" -l highlights.json -o clips/ -p soul112
|
||||
# 或输出到 切片/,则成片时 -c 指向 切片/
|
||||
```
|
||||
⚠️ **不要加 `-f` / `--fast`** 做最终成片:copy 模式关键帧对齐会导致切片起点早于高光表,字幕与声音必歪。草稿试剪可 fast,定稿对齐请用默认(重编码)模式。
|
||||
|
||||
**3. 成片**(竖屏条 + 封面 + 字幕 + 去语助词;vf 以 analyze 为准)
|
||||
```bash
|
||||
@@ -238,7 +250,9 @@ xxx_output/
|
||||
| 文件夹 | **切片(或 clips)** + **成片**;另 **`裁剪检查/`** 放标定素材 |
|
||||
| 成片尺寸 | 竖条 **W×1080**(默认 W 由 analyze);`--vertical-fit-full` 时为 498×1080 letterbox |
|
||||
| 成片文件名 | 纯标题(无 01、无 _enhanced) |
|
||||
| 单段时长 | 30~300 秒 |
|
||||
| 单段时长 | 30~300 秒(≤5 分钟),每段**仅一个主题**且**完整** |
|
||||
| 条数/场 | 约 **8~30**(随密度),兼顾**有趣**与**信息完整** |
|
||||
| 封面底模糊 | 约 **10%** 混入(`bg_blur_mix`),非全糊 |
|
||||
| 高光/语助词 | 见 `参考资料/高光识别提示词.md` |
|
||||
|
||||
详细 crop 说明见:`参考资料/竖屏中段裁剪参数说明.md`。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
## 2. 高光规则(脚本内已落地)
|
||||
|
||||
- **封面大字**:优先 `hook_3sec` → 其次 `question` → 最后 `title`(吸睛、观点感强)。
|
||||
- **封面视觉**:竖屏为「视频帧强模糊 + 渐变叠层」;横版为双阶高斯模糊。
|
||||
- **封面视觉**:竖屏/横版均为「清晰帧 + 约 10% 模糊层混合(`bg_blur_mix`)+ 渐变叠层」,非全屏强糊;见 `Soul竖屏切片_SKILL.md` v2.1。
|
||||
- **去停顿**:`silencedetect` 检出的静音段用 `trim+concat` 切除,并**整体平移**字幕时间轴。
|
||||
- **字幕**:默认逐字;相对已导出切片时间轴,音画 PTS 明显错位时才加小幅延迟。
|
||||
- **片尾**:`cta_ending` 在成片末尾约 2~3.8 秒以字幕条形式固定出现。
|
||||
|
||||
@@ -107,6 +107,11 @@ def sanitize_filename(name: str, max_length: int = 50, chinese_only: bool = Fals
|
||||
return result.strip(" _-") or "片段"
|
||||
|
||||
|
||||
# 精确切片:-ss 在 -i 之前会落在关键帧上,输出起点常比 highlights.start 早 0~3s,
|
||||
# transcript 按绝对时间裁切 → 字幕会整体偏早/偏晚。先粗 seek 再在 -i 之后细 -ss(重编码)可对齐口播。
|
||||
_PRESEEK_MARGIN_SEC = 120.0
|
||||
|
||||
|
||||
def clip_video(input_path: str, start_time: str, end_time: str, output_path: str,
|
||||
fast_mode: bool = False):
|
||||
"""
|
||||
@@ -117,7 +122,7 @@ def clip_video(input_path: str, start_time: str, end_time: str, output_path: str
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
output_path: 输出路径
|
||||
fast_mode: 快速模式(使用copy编码,可能不精确)
|
||||
fast_mode: 快速模式(使用 copy 编码,可能不精确)
|
||||
"""
|
||||
# 使用 -t duration 避免 -to 在 ffmpeg 中的歧义(-to 可能被解释为输出时长)
|
||||
start_sec = parse_timestamp(start_time)
|
||||
@@ -125,23 +130,28 @@ def clip_video(input_path: str, start_time: str, end_time: str, output_path: str
|
||||
duration_sec = end_sec - start_sec
|
||||
|
||||
if fast_mode:
|
||||
# 快速模式:使用 copy 编码,-t 明确指定输出时长
|
||||
# 快速模式:stream copy + input 侧 -ss,起点可能早于 start_time(关键帧),
|
||||
# 与整场 transcript 对齐烧录时易出现「声对字不对」。成片要求对齐时请用默认精确模式。
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-ss", start_time,
|
||||
"-i", input_path,
|
||||
"-t", str(duration_sec),
|
||||
"-c", "copy",
|
||||
"-avoid_negative_ts", "1",
|
||||
"-avoid_negative_ts", "make_zero",
|
||||
"-y",
|
||||
output_path
|
||||
]
|
||||
else:
|
||||
# 精确模式:重新编码,-t 明确指定输出时长,体积可控
|
||||
# 精确模式:粗 seek + 解码后细裁,起点与 highlights 一致,字幕与声音可对齐
|
||||
preseek = max(0.0, start_sec - _PRESEEK_MARGIN_SEC)
|
||||
inner_ss = start_sec - preseek
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-ss", start_time,
|
||||
"-y",
|
||||
"-ss", str(preseek),
|
||||
"-i", input_path,
|
||||
"-ss", str(inner_ss),
|
||||
"-t", str(duration_sec),
|
||||
"-c:v", "libx264",
|
||||
"-preset", "fast",
|
||||
@@ -150,7 +160,7 @@ def clip_video(input_path: str, start_time: str, end_time: str, output_path: str
|
||||
"-maxrate", "4M",
|
||||
"-c:a", "aac",
|
||||
"-b:a", "128k",
|
||||
"-y",
|
||||
"-avoid_negative_ts", "make_zero",
|
||||
output_path
|
||||
]
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"""
|
||||
Soul切片增强脚本 v2.0
|
||||
功能:
|
||||
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏带强模糊视频底 + 渐变
|
||||
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏底图为**清晰帧 + 约 10% 轻模糊混入**(非全糊)+ 渐变
|
||||
2. 烧录字幕(关键词高亮、可选逐字)
|
||||
3. 切除检出的长静音并重映射字幕时间轴
|
||||
4. 片尾 CTA(cta_ending)字幕条
|
||||
5. 视频加速约 10%
|
||||
5. 视频加速约 10%(字幕在加速前已烧进中间成片,再与音轨同倍率 setpts/atempo,相对口播不因此漂移)
|
||||
6. 转录纠错 / 语气词过滤(见 CORRECTIONS、FILLER 等)
|
||||
"""
|
||||
|
||||
@@ -124,7 +124,7 @@ def build_typewriter_subtitle_images(
|
||||
逐词/逐字渐显:
|
||||
- 若字幕带 word_times(whisper word-level SRT),按词的真实开始时间逐词追加,与人声严格同步;
|
||||
- 否则按字符数等分句子时长(兜底)。
|
||||
subtitle_overlay_start:最早显示字幕的时间轴(秒),须 ≥ 封面结束 + 留白。
|
||||
subtitle_overlay_start:最早显示字幕的时间轴(秒),须 ≥ 封面结束(默认与封面紧接,无额外留白)。
|
||||
"""
|
||||
sub_images = []
|
||||
img_idx = 0
|
||||
@@ -368,7 +368,9 @@ VERTICAL_COVER_ALPHA = 165 # 0~255,越大越不透明
|
||||
# 样式配置
|
||||
STYLE = {
|
||||
'cover': {
|
||||
'bg_blur': 52, # 底层视频帧高斯模糊,越大越「电影感」、越不抢字
|
||||
# 封面底图:原画与「高斯模糊层」按 bg_blur_mix 混合(0.1≈10% 模糊感),保留界面可辨;勿再用全幅强模糊
|
||||
'bg_blur_mix': 0.10,
|
||||
'bg_blur_radius': 14, # 仅用于生成模糊层的高斯半径,再与清晰帧 blend
|
||||
'overlay_alpha': 200,
|
||||
'duration': 2.5,
|
||||
},
|
||||
@@ -399,7 +401,7 @@ STYLE = {
|
||||
SUBTITLE_DELAY_SEC = 0.0
|
||||
SUBTITLE_PTS_OFFSET_THRESHOLD = 0.18 # 超过此秒数才加 delay
|
||||
SUBTITLE_DELAY_MAX = 1.2
|
||||
SUBS_START_AFTER_COVER_SEC = 3.0
|
||||
SUBS_START_AFTER_COVER_SEC = 0.0
|
||||
# 至少切除的静音总时长(秒)才触发重编码,避免无意义抖动
|
||||
MIN_SILENCE_TRIM_TOTAL_SEC = 0.12
|
||||
COVER_TITLE_MAX_CJK = 6
|
||||
@@ -539,6 +541,19 @@ def apply_platform_safety(text: str) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def _collapse_cjk_interchar_spaces(text: str) -> str:
|
||||
"""去掉 CJK 字符之间的空白(Whisper 词级时间轴常插空格,逐字/逐词显字时会像字间被撑开)。"""
|
||||
if not text:
|
||||
return text
|
||||
s = text
|
||||
prev = None
|
||||
while prev != s:
|
||||
prev = s
|
||||
s = re.sub(r"([\u4e00-\u9fff])\s+([\u4e00-\u9fff])", r"\1\2", s)
|
||||
s = re.sub(r" +", " ", s)
|
||||
return s.strip()
|
||||
|
||||
|
||||
def improve_subtitle_punctuation(text: str) -> str:
|
||||
"""为字幕句子补充标点,让意思更清晰。
|
||||
|
||||
@@ -553,7 +568,7 @@ def improve_subtitle_punctuation(text: str) -> str:
|
||||
return t
|
||||
# 末尾已有标点则不重复加
|
||||
if t and t[-1] in ',。?!,.:!?;':
|
||||
return apply_platform_safety(t)
|
||||
return apply_platform_safety(_collapse_cjk_interchar_spaces(t))
|
||||
# 疑问词检测
|
||||
question_words = ('吗', '吧', '呢', '么', '嘛', '什么', '怎么', '为什么',
|
||||
'哪', '哪里', '谁', '几', '多少', '是否', '可以吗', '对吗')
|
||||
@@ -568,7 +583,7 @@ def improve_subtitle_punctuation(text: str) -> str:
|
||||
t = t + '!'
|
||||
elif len(t) >= 5:
|
||||
t = t + '。'
|
||||
return apply_platform_safety(t)
|
||||
return apply_platform_safety(_collapse_cjk_interchar_spaces(t))
|
||||
|
||||
def _detect_clip_pts_offset(clip_path: str) -> float:
|
||||
"""探测切片实际起始 PTS(秒),用于补偿 -ss input seeking 的关键帧偏移。
|
||||
@@ -843,8 +858,8 @@ def _sec_to_srt_time(sec):
|
||||
|
||||
|
||||
def write_clip_srt(srt_path, subtitles, cover_duration, subs_after_cover_sec=SUBS_START_AFTER_COVER_SEC):
|
||||
"""写出用于烧录的 SRT(仅保留封面结束+留白后的字幕,时间已相对片段)"""
|
||||
safe_start = cover_duration + subs_after_cover_sec + 0.3
|
||||
"""写出用于烧录的 SRT(仅保留封面结束后的字幕,时间已相对片段)"""
|
||||
safe_start = cover_duration + subs_after_cover_sec + 0.05
|
||||
lines = []
|
||||
idx = 1
|
||||
for sub in subtitles:
|
||||
@@ -1007,7 +1022,7 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
|
||||
is_vertical = _is_vertical_strip_canvas(width, height)
|
||||
|
||||
if is_vertical:
|
||||
# 竖屏成片:底层为「视频帧强模糊」+ 渐变叠层,字更突出、背景更「电影虚化」
|
||||
# 竖屏成片:底层为「清晰帧 + 少量模糊混入(默认约 10%)」+ 渐变,避免全糊看不清界面
|
||||
base = Image.new("RGBA", (width, height), (*VERTICAL_COVER_TOP, 255))
|
||||
if video_path and os.path.exists(video_path):
|
||||
temp_frame = output_path.replace(".png", "_vframe.jpg")
|
||||
@@ -1029,8 +1044,14 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
|
||||
)
|
||||
if os.path.exists(temp_frame):
|
||||
try:
|
||||
bf = Image.open(temp_frame).convert("RGBA").resize((width, height))
|
||||
bf = bf.filter(ImageFilter.GaussianBlur(radius=style["bg_blur"]))
|
||||
sharp = Image.open(temp_frame).convert("RGBA").resize((width, height))
|
||||
mix = float(style.get("bg_blur_mix", 0.10))
|
||||
r = float(style.get("bg_blur_radius", 14))
|
||||
if mix > 0.001:
|
||||
blurred = sharp.filter(ImageFilter.GaussianBlur(radius=r))
|
||||
bf = Image.blend(sharp, blurred, mix)
|
||||
else:
|
||||
bf = sharp
|
||||
dim = Image.new("RGBA", (width, height), (0, 0, 0, 115))
|
||||
base = Image.alpha_composite(bf, dim)
|
||||
finally:
|
||||
@@ -1048,7 +1069,7 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
|
||||
img = Image.alpha_composite(img, overlay)
|
||||
draw = ImageDraw.Draw(img)
|
||||
else:
|
||||
# 横版:沿用视频帧模糊背景
|
||||
# 横版:清晰帧 + 少量模糊混入(与竖条封面一致)
|
||||
if video_path and os.path.exists(video_path):
|
||||
temp_frame = output_path.replace('.png', '_frame.jpg')
|
||||
subprocess.run([
|
||||
@@ -1056,9 +1077,14 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
|
||||
'-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"]))
|
||||
bg = bg.filter(ImageFilter.GaussianBlur(radius=6))
|
||||
sharp = Image.open(temp_frame).resize((width, height)).convert('RGBA')
|
||||
mix = float(style.get("bg_blur_mix", 0.10))
|
||||
r = float(style.get("bg_blur_radius", 14))
|
||||
if mix > 0.001:
|
||||
blurred = sharp.filter(ImageFilter.GaussianBlur(radius=r))
|
||||
bg = Image.blend(sharp, blurred, mix)
|
||||
else:
|
||||
bg = sharp
|
||||
os.remove(temp_frame)
|
||||
else:
|
||||
bg = Image.new('RGB', (width, height), (25, 35, 30))
|
||||
@@ -1520,7 +1546,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, vertical=False,
|
||||
crop_vf=None, overlay_x=None, typewriter_subs=False,
|
||||
vertical_fit_full=False, trim_silence=True):
|
||||
vertical_fit_full=False, trim_silence=True,
|
||||
subtitle_extra_delay=0.0):
|
||||
"""增强单个切片。vertical=True 时输出竖条,宽由 --crop-vf 决定(原生包络常见 560~750×1080;旧 498 为两段裁或 scale)。
|
||||
vertical_fit_full:整幅 16:9 缩放入 498×1080 + 上下黑边。
|
||||
"""
|
||||
@@ -1589,7 +1616,8 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
end_sec = start_sec + original_duration
|
||||
|
||||
# 已导出切片:默认 delay=0。仅当音/视频首帧 PTS 明显不一致时小幅推迟字幕,贴人声。
|
||||
actual_delay = float(SUBTITLE_DELAY_SEC)
|
||||
# subtitle_extra_delay:整场微调(秒),正数整体推迟字幕,用于个别素材仍略有偏差时手工对齐。
|
||||
actual_delay = float(SUBTITLE_DELAY_SEC) + float(subtitle_extra_delay or 0.0)
|
||||
try:
|
||||
pts_cmd = [
|
||||
"ffprobe", "-v", "quiet", "-select_streams", "v:0",
|
||||
@@ -1617,11 +1645,15 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
audio_pts = float(audio_r.stdout.strip().split("\n")[0].strip())
|
||||
offset = abs(first_pts - audio_pts)
|
||||
if offset > SUBTITLE_PTS_OFFSET_THRESHOLD:
|
||||
actual_delay = min(SUBTITLE_DELAY_MAX, offset * 0.85)
|
||||
print(f" ✓ 音画 PTS 差 {offset:.2f}s → 字幕延迟补偿 {actual_delay:.2f}s", flush=True)
|
||||
pts_delay = min(SUBTITLE_DELAY_MAX, offset * 0.85)
|
||||
actual_delay = float(SUBTITLE_DELAY_SEC) + float(subtitle_extra_delay or 0.0) + pts_delay
|
||||
print(f" ✓ 音画 PTS 差 {offset:.2f}s → 字幕延迟补偿 +{pts_delay:.2f}s(含基准与 extra)", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if abs(float(subtitle_extra_delay or 0.0)) > 1e-6:
|
||||
print(f" ✓ 字幕额外延迟 --subtitle-extra-delay={float(subtitle_extra_delay):.3f}s", flush=True)
|
||||
|
||||
subtitles = parse_srt_for_clip(transcript_path, start_sec, end_sec, delay_sec=actual_delay)
|
||||
for sub in subtitles:
|
||||
if not _is_mostly_chinese(sub['text']):
|
||||
@@ -1917,6 +1949,12 @@ def main():
|
||||
action="store_true",
|
||||
help="不去除静音长停顿(默认会切除 silencedetect 检出的静音并同步平移字幕时间轴)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--subtitle-extra-delay",
|
||||
type=float,
|
||||
default=0.0,
|
||||
help="字幕整体时间轴再加若干秒(正数=字幕更晚出现),用于个别素材在精确切片后仍须微调时",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
clips_dir = Path(args.clips) if args.clips else CLIPS_DIR
|
||||
@@ -2016,6 +2054,7 @@ def main():
|
||||
typewriter_subs=typewriter,
|
||||
vertical_fit_full=vfit,
|
||||
trim_silence=not getattr(args, "no_trim_silence", False),
|
||||
subtitle_extra_delay=float(getattr(args, "subtitle_extra_delay", 0.0) or 0.0),
|
||||
):
|
||||
success_count += 1
|
||||
finally:
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
- **大文件** >20MB → `金仓_存储备份/大文件外置/`;`.venv` 不入库
|
||||
- **数据库**:唯一 MongoDB(27017,库名 `karuo_site`)
|
||||
- **终端命令**:直接执行不询问
|
||||
- **流水线续跑**:用户说「下一步」「接着跑」「重新剪辑」「直到完成」或刚更新某 Skill 后要产出时,按 `.cursor/rules/karuo-ai.mdc` 中 **Skill 更新后·流水线「下一步」= 立即开跑**,从第一步起**不询问**、按 Skill 顺序自动做完(缺环节则先补该环节)。
|
||||
- **风格**:中文优先,产品名保留原文(Cursor/GitHub/Gitea/v0/MongoDB/Synology/宝塔等)
|
||||
|
||||
## 九、平台适配
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **一张表查所有技能**。任何 AI 拿到这张表,就能按关键词找到对应技能的 SKILL.md 路径并执行。
|
||||
> 77 技能 + 3 卡路Cursor入口 | 15 成员 | 5 负责人
|
||||
> 版本:5.10 | 更新:2026-03-23
|
||||
> 版本:5.11 | 更新:2026-03-20
|
||||
>
|
||||
> **技能配置、安装、删除、掌管人登记** → 见 **`运营中枢/工作台/01_技能控制台.md`**。
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
2. 找到行 → 读「SKILL 路径」列的文件
|
||||
3. 按 SKILL.md 里的步骤执行
|
||||
|
||||
**Cursor 续跑**:用户说「下一步」「接着跑」「重新剪辑」「直到完成」或刚更新某 Skill 后要产出时,**直接执行**(不先问是否运行),见 `BOOTSTRAP.md` 八·流水线续跑、`卡若AI/.cursor/rules/karuo-ai.mdc`。
|
||||
|
||||
多技能匹配时按 **金→水→木→火→土** 优先级。用户可用 `@成员名` 指定。
|
||||
|
||||
**基因胶囊查阅**:所有技能均已导出为基因胶囊,可于 `卡若Ai的文件夹/导出/基因胶囊/README_基因胶囊导出说明.md` 查看全量胶囊清单、流程图及 unpack 用法。支持「查胶囊」「胶囊列表」「继承能力」等触发。
|
||||
|
||||
@@ -419,3 +419,4 @@
|
||||
| 2026-03-22 13:21:45 | 🔄 卡若AI 同步 2026-03-22 13:21 | 更新:金仓、水桥平台对接、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 2026-03-22 13:22:16 | 🔄 卡若AI 同步 2026-03-22 13:22 | 更新:运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 2026-03-22 13:23:40 | 🔄 卡若AI 同步 2026-03-22 13:23 | 更新:金仓、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 2026-03-22 14:38:11 | 🔄 卡若AI 同步 2026-03-22 14:38 | 更新:Cursor规则、金仓、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
|
||||
@@ -422,3 +422,4 @@
|
||||
| 2026-03-22 13:21:45 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 13:21 | 更新:金仓、水桥平台对接、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-22 13:22:16 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 13:22 | 更新:运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-22 13:23:40 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 13:23 | 更新:金仓、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-22 14:38:11 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 14:38 | 更新:Cursor规则、金仓、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
Reference in New Issue
Block a user