diff --git a/.cursor/rules/karuo-ai.mdc b/.cursor/rules/karuo-ai.mdc index 07150108..748a29b3 100644 --- a/.cursor/rules/karuo-ai.mdc +++ b/.cursor/rules/karuo-ai.mdc @@ -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` diff --git a/01_卡资(金)/金仓_存储备份/聊天记录管理/fallback/recent_chats_fallback.json b/01_卡资(金)/金仓_存储备份/聊天记录管理/fallback/recent_chats_fallback.json index 78795961..c15ed131 100644 --- a/01_卡资(金)/金仓_存储备份/聊天记录管理/fallback/recent_chats_fallback.json +++ b/01_卡资(金)/金仓_存储备份/聊天记录管理/fallback/recent_chats_fallback.json @@ -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", diff --git a/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md b/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md index 8fd3a571..0ab32f97 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md +++ b/03_卡木(木)/木叶_视频内容/视频切片/Soul竖屏切片_SKILL.md @@ -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`。 diff --git a/03_卡木(木)/木叶_视频内容/视频切片/流程说明_高光成片.md b/03_卡木(木)/木叶_视频内容/视频切片/流程说明_高光成片.md index 53da6de2..34e269e4 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/流程说明_高光成片.md +++ b/03_卡木(木)/木叶_视频内容/视频切片/流程说明_高光成片.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 秒以字幕条形式固定出现。 diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/batch_clip.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/batch_clip.py index b1f93136..a6bdfa2a 100755 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/batch_clip.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/batch_clip.py @@ -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 ] diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py index 1a57d98a..786d0fa3 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py @@ -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: diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 019194e1..6c25486e 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -92,6 +92,7 @@ - **大文件** >20MB → `金仓_存储备份/大文件外置/`;`.venv` 不入库 - **数据库**:唯一 MongoDB(27017,库名 `karuo_site`) - **终端命令**:直接执行不询问 +- **流水线续跑**:用户说「下一步」「接着跑」「重新剪辑」「直到完成」或刚更新某 Skill 后要产出时,按 `.cursor/rules/karuo-ai.mdc` 中 **Skill 更新后·流水线「下一步」= 立即开跑**,从第一步起**不询问**、按 Skill 顺序自动做完(缺环节则先补该环节)。 - **风格**:中文优先,产品名保留原文(Cursor/GitHub/Gitea/v0/MongoDB/Synology/宝塔等) ## 九、平台适配 diff --git a/SKILL_REGISTRY.md b/SKILL_REGISTRY.md index 03dd66f6..22370b58 100644 --- a/SKILL_REGISTRY.md +++ b/SKILL_REGISTRY.md @@ -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 用法。支持「查胶囊」「胶囊列表」「继承能力」等触发。 diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index f4803842..20037d89 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -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 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 2b0a9553..4f8e3d99 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -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) |