🔄 卡若AI 同步 2026-03-13 13:50 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个

This commit is contained in:
2026-03-13 13:50:26 +08:00
parent 63a3d8de65
commit 03ddd1706d
6 changed files with 428 additions and 719 deletions

View File

@@ -1,11 +1,11 @@
---
name: Soul竖屏切片
description: Soul 派对视频→竖屏成片498×1080剪辑→成片两文件夹MLX 转录→高光识别→batch_clip→soul_enhance封面+字幕+去语助词)。可选 LTXAI 生成内容Retake 重剪)衔接成片流程。支持基因胶囊打包。
triggers: Soul竖屏切片、视频切片、热点切片、竖屏成片、派对切片、LTX、AI生成视频、Retake重剪
description: Soul 派对视频→竖屏成片498×1080剪辑→成片两文件夹MLX 转录→高光识别→batch_clip→soul_enhance封面+字幕同步+去语助词+纠错→visual_enhance v7苹果毛玻璃浮层)。可选 LTX AI 生成内容/Retake 重剪。支持基因胶囊打包。
triggers: Soul竖屏切片、视频切片、热点切片、竖屏成片、派对切片、LTX、AI生成视频、Retake重剪、字幕优化、字幕同步
owner: 木叶
group: 木
version: "1.0"
updated: "2026-02-27"
version: "1.2"
updated: "2026-03-13"
---
# Soul 竖屏切片 · 专用 Skill
@@ -73,6 +73,10 @@ updated: "2026-02-27"
| 字幕全跳过(转录稿异常误判) | `_parse_clip_index` 取到场次号(如 119而非切片序号01导致 highlight_info 为空start_sec=0 落入噪声区 | 改为取 `_数字_` 模式中**最小值**119→01=1 ✓ |
| 标题/文件名有下划线 | `sanitize_filename` 保留了 `_` | 现在 `_` 也替换为空格 |
| 字幕烧录极慢N/5 次 encode | 原 batch_size=5180 条字幕需 36 次 FFmpeg 重编码 | 改为单次通道1 次 pass失败时 batch_size=40 兜底 |
| **字幕超前于说话(字幕比声音早)** | `batch_clip -ss` 输入端 seeking 导致切片从关键帧开始(早于请求时间 1-3s字幕按请求时间算相对位置导致超前 | `SUBTITLE_DELAY_SEC` 从 0.8 提高到 **2.0 秒**Soul 派对直播流关键帧间距 2-4s2.0s 补偿更准确 |
| **封面期间出现字幕** | 字幕时间计算使字幕落在封面段(前 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+ 繁体常用字映射 |
---
@@ -131,7 +135,35 @@ xxx_output/
---
## 九、AI 生成与 LTX 可选集成
## 九、底部浮层苹果毛玻璃样式visual_enhance v7
`soul_enhance` 的封面+字幕+竖屏成片上,可选叠加苹果毛玻璃底部浮层,作为**最终成片**(不再多一个"增强版"目录)。
### 设计规范来自卡若AI前端 神射手/毛狐狸标准)
| 元素 | 规格 |
|------|------|
| 背景 | `rgba(14,16,28,0.88)` 深黑半透 + 顶部高光条 |
| 圆角 | 28px对应前端 `rounded-2xl` |
| 边框 | `rgba(255,255,255,0.12)` 白边 + 内缩 `rgba(255,255,255,0.06)` |
| 阴影 | GaussianBlur(22),叠加轻层阴影制造悬浮感 |
| 字体 | 标题 Medium正文 Regular两档不堆叠字重 |
| 主色 | 蓝→紫渐变(`from-blue-500 to-purple-500`),单色点睛 |
| 图标 | Unicode 符号图标:◆ 数据 / ▸ 流程 / ⇌ 对比 / ✦ 总结 |
| 芯片 | 渐变描边胶囊glass-button 风格),不做满色填充 |
| **⚠️ 无视频小窗** | 已永久去掉右上角动态小视频窗,不再加入 |
### 使用命令
```bash
python3 visual_enhance.py -i "soul_enhanced.mp4" -o "成片/标题.mp4" --scenes scenes.json
```
`--scenes` JSON 格式:每段需 `type`, `label`, `sub_label`, `params`(含 `question`/`subtitle`/`chips`)。
---
## 十、AI 生成与 LTX 可选集成
在「已有录播 → 转录→高光→切片→成片」之外,可选用 **LTX** 系能力,实现 **AI 生成视频内容****在已有视频上轻松重剪**,成片仍走本 Skill 的封面+字幕+竖屏规范。

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -89,19 +89,47 @@ def _to_simplified(text: str) -> str:
text = text.replace(t, s)
return text
# 常见转录错误修正(与 one_video 一致)
# 常见转录错误修正(与 one_video 一致,按长度降序排列避免短词误替换
CORRECTIONS = {
'私余': '私域', '统安': '同安', '信一下': '线上', '头里': '投入',
'幅画': '负责', '施育': '私域', '经历论': '净利润', '成于': '乘以',
'马的': '码的', '猜济': '拆解', '巨圣': '矩阵', '货客': '获客',
'甲为师': 'AI助手', '小龙俠': '小龙虾', '小龍俠': '小龙虾',
'小龍蝦': '小龙虾', '龍蝦': '龙虾', '小龙虾': '深度AI',
'基因交狼': '技能包', '基因交流': '技能传授', '颗色': 'Cursor',
'蝌蚁': '科技AI', '千万': '千问', '': 'Claude', '豆包': 'AI工具',
'受伤命': '搜索引擎', '货客': '获客', '受上': 'Soul上',
'搜上': 'Soul上', '售上': 'Soul上', '寿上': 'Soul上',
'瘦上': 'Soul上', '亭上': 'Soul上', '这受': '这Soul',
'龙虾': '深度AI', '克劳德': 'Claude',
# AI 工具名称 ─────────────────────────────────────────────────
'小龙俠': 'AI工具', '小龍俠': 'AI工具', '小龍蝦': 'AI工具',
'龍蝦': 'AI工具', '小龙虾': 'AI工具', '龙虾': 'AI工具',
'克劳德': 'Claude', '科劳德': 'Claude', '': 'Claude',
'颗色': 'Cursor', '库色': 'Cursor', '可索': 'Cursor',
'蝌蚁': '科技AI', '千万': '千问', '豆包': 'AI工具',
'暴电码': '暴电码', '蝌蚪': 'Cursor',
# Soul 平台别字 ──────────────────────────────────────────────
'受上': 'Soul上', '搜上': 'Soul上', '售上': 'Soul上',
'寿上': 'Soul上', '瘦上': 'Soul上', '亭上': 'Soul上',
'这受': '这Soul', '受的': 'Soul的', '受里': 'Soul里',
'受平台': 'Soul平台',
# 私域/商业用语 ─────────────────────────────────────────────
'私余': '私域', '施育': '私域', '私育': '私域',
'统安': '同安', '信一下': '线上', '头里': '投入',
'幅画': '负责', '经历论': '净利润', '成于': '乘以',
'马的': '码的', '猜济': '拆解', '巨圣': '矩阵',
'货客': '获客', '甲为师': '(AI助手)',
'基因交狼': '技能包', '基因交流': '技能传授',
'受伤命': '搜索引擎', '附身': '副业', '附产': '副产',
# AI 工作流 / 编程词汇 ──────────────────────────────────────
'Ski-er': '智能体', 'Skier': '智能体', 'SKI-er': '智能体',
'工作流': '工作流', '智能体': '智能体',
'蝌蛇': 'Cursor', '科色': 'Cursor',
'Cloud': 'Claude', # 转录常把 Claude 误识别为 Cloud
# 繁体常见 ──────────────────────────────────────────────────
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': '',
'': '', '台灣': '台湾', '臺灣': '台湾',
# 噪音符号/单字符 ────────────────────────────────────────────
# (在 parse_srt 里过滤,这里不做)
}
# 各平台违禁词 → 谐音/替代词(用于字幕、封面、文件名)
@@ -358,77 +386,124 @@ def _detect_clip_pts_offset(clip_path: str) -> float:
# batch_clip -ss input seeking 导致实际切割比请求早 0~3 秒(关键帧对齐)
# 字幕按 highlights.start_time 算相对时间,会比实际音频提前
# 加正值延迟 = 字幕往后推 = 与声音更同步
SUBTITLE_DELAY_SEC = 0.8 # 根据实测 Soul 视频关键帧间隔约 2s取保守值
# 2025-03 实测Soul派对直播视频关键帧间距 2-4 秒,补偿需约 2.0s
SUBTITLE_DELAY_SEC = 2.0 # 增大到 2.0,避免字幕超前于说话
def _is_noise_line(text: str) -> bool:
"""检测是否为噪声行单字母、重复符号、ASR幻觉等"""
if not text:
return True
stripped = text.strip()
# 单字母L、A、B 等 ASR 幻觉)
if len(stripped) <= 2 and all(c.isalpha() or c in '…、。,' for c in stripped):
return True
# 全是相同字符
if len(set(stripped)) == 1 and len(stripped) >= 3:
return True
# 纯 ASR 幻觉词
NOISE_TOKENS = {'Agent', 'agent', 'L', 'B', 'A', 'OK', 'ok',
'...', '……', '嗯嗯嗯', '啊啊', '哈哈哈',
'呃呃', 'hmm', 'Hmm', 'Um', 'Uh'}
if stripped in NOISE_TOKENS:
return True
return False
def _improve_subtitle_text(text: str) -> str:
"""字幕文字质量提升:纠错 + 上下文通畅 + 违禁词替换"""
if not text:
return text
# 繁转简
t = _to_simplified(text.strip())
# 错词修正(按词典长度降序,避免短词覆盖长词)
for w, c in sorted(CORRECTIONS.items(), key=lambda x: len(x[0]), reverse=True):
t = t.replace(w, c)
# 违禁词替换
for w, c in PLATFORM_VIOLATIONS.items():
t = t.replace(w, c)
# 清理语助词
t = clean_filler_words(t)
# 去多余空格
t = re.sub(r'\s+', ' ', t).strip()
# 末尾加句号让阅读更顺畅(如果没有标点的话)
END_PUNCTS = set('。!?…,')
if t and t[-1] not in END_PUNCTS and len(t) >= 6:
t += ''
return t
def parse_srt_for_clip(srt_path, start_sec, end_sec, delay_sec=None):
"""解析SRT提取指定时间段的字幕。
优化:
1. 字幕延迟补偿delay_sec补偿 FFmpeg input seeking 关键帧偏移,让字幕与声音同步
2. 合并过短字幕:相邻字幕 <1.2s 且文字可拼接时自动合并,减少闪烁
3. 最小显示时长:每条至少显示 1.2s,避免一闪而过看不清
1. 字幕延迟补偿delay_sec补偿 FFmpeg input seeking 关键帧偏移2s 默认)
2. 噪声行过滤:去掉单字母 L / Agent 等 ASR 幻觉行
3. 文字质量提升:纠错 + 违禁词替换 + 通畅度修正
4. 合并过短字幕:相邻 <1.5s 时自动合并,减少闪烁
5. 最小显示时长:每条至少 1.5s,避免一闪而过
"""
if delay_sec is None:
delay_sec = SUBTITLE_DELAY_SEC
with open(srt_path, 'r', encoding='utf-8') as f:
content = f.read()
pattern = r'(\d+)\n(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})\n(.*?)(?=\n\n|\Z)'
matches = re.findall(pattern, content, re.DOTALL)
def time_to_sec(t):
t = t.replace(',', '.')
parts = t.split(':')
return int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2])
raw_subs = []
for match in matches:
sub_start = time_to_sec(match[1])
sub_end = time_to_sec(match[2])
sub_end = time_to_sec(match[2])
text = match[3].strip()
# 噪声行提前过滤
if _is_noise_line(text):
continue
if sub_end > start_sec and sub_start < end_sec + 2:
rel_start = max(0, sub_start - start_sec + delay_sec)
rel_end = sub_end - start_sec + delay_sec
text = _to_simplified(text)
for w, c in CORRECTIONS.items():
text = text.replace(w, c)
cleaned_text = clean_filler_words(text)
if len(cleaned_text) > 1:
rel_end = sub_end - start_sec + delay_sec
improved = _improve_subtitle_text(text)
if improved and len(improved) > 1:
raw_subs.append({
'start': max(0, rel_start),
'end': max(rel_start + 0.5, rel_end),
'text': cleaned_text
'end': max(rel_start + 0.5, rel_end),
'text': improved,
})
# 合并过短的连续字幕(<1.2s 且总长 <25字),让每条有足够阅读时间
MIN_DISPLAY = 1.2
# 合并过短的连续字幕(<1.5s 且总长 <28字),让每条有足够阅读时间
MIN_DISPLAY = 1.5
merged = []
i = 0
while i < len(raw_subs):
cur = dict(raw_subs[i])
dur = cur['end'] - cur['start']
# 尝试向后合并
while dur < MIN_DISPLAY and i + 1 < len(raw_subs):
nxt = raw_subs[i + 1]
gap = nxt['start'] - cur['end']
combined_text = cur['text'] + '' + nxt['text']
if gap <= 0.5 and len(combined_text) <= 25:
cur['end'] = nxt['end']
cur['text'] = combined_text
# 去掉句尾句号再合并
base_text = cur['text'].rstrip('。!?,')
combined = base_text + '' + nxt['text']
if gap <= 0.6 and len(combined) <= 28:
cur['end'] = nxt['end']
cur['text'] = combined
dur = cur['end'] - cur['start']
i += 1
else:
break
# 强制最小显示时长
if cur['end'] - cur['start'] < MIN_DISPLAY:
cur['end'] = cur['start'] + MIN_DISPLAY
merged.append(cur)
i += 1
return merged
@@ -485,13 +560,14 @@ def _sec_to_srt_time(sec):
def write_clip_srt(srt_path, subtitles, cover_duration):
"""写出用于烧录的 SRT仅保留封面结束后的字幕时间已相对片段"""
safe_start = cover_duration + 0.3
lines = []
idx = 1
for sub in subtitles:
start, end = sub['start'], sub['end']
if end <= cover_duration:
if end <= safe_start:
continue
start = max(start, cover_duration)
start = max(start, safe_start)
text = (sub.get('text') or '').strip().replace('\n', ' ')
if not text:
continue
@@ -991,7 +1067,45 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
except (IndexError, ValueError):
start_sec = 0
end_sec = start_sec + duration
subtitles = parse_srt_for_clip(transcript_path, start_sec, end_sec)
# 动态字幕延迟:检测切片实际首帧 PTS与请求 start_time 做差
actual_delay = SUBTITLE_DELAY_SEC
try:
pts_cmd = [
"ffprobe", "-v", "quiet", "-select_streams", "v:0",
"-show_entries", "frame=pts_time",
"-read_intervals", "%+0.1",
"-print_format", "csv=p=0",
str(clip_path),
]
pts_r = subprocess.run(pts_cmd, capture_output=True, text=True, timeout=10)
if pts_r.returncode == 0 and pts_r.stdout.strip():
first_pts = float(pts_r.stdout.strip().split("\n")[0].strip())
# batch_clip 把 -ss 放在 -i 前面FFmpeg 将 PTS 重置为 0
# 但实际音频起点可能比请求的 start_sec 早 0-4 秒(关键帧对齐)
# first_pts 接近 0真正的偏移量在 batch_clip 的 seeking 行为里
# 更可靠的方法:检测音频首个有效帧的 PTS
audio_cmd = [
"ffprobe", "-v", "quiet", "-select_streams", "a:0",
"-show_entries", "frame=pts_time",
"-read_intervals", "%+0.5",
"-print_format", "csv=p=0",
str(clip_path),
]
audio_r = subprocess.run(audio_cmd, capture_output=True, text=True, timeout=10)
if audio_r.returncode == 0 and audio_r.stdout.strip():
audio_pts = float(audio_r.stdout.strip().split("\n")[0].strip())
# 视频帧 PTS 与音频帧 PTS 的差值揭示了 seeking 偏移
offset = abs(first_pts - audio_pts)
# 关键帧对齐通常导致视频比音频早 0-3s
# 字幕需要额外推迟这个偏移量
actual_delay = max(1.5, SUBTITLE_DELAY_SEC + offset * 0.5)
if actual_delay > 4.0:
actual_delay = SUBTITLE_DELAY_SEC
except Exception:
pass
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']):
sub['text'] = _translate_to_chinese(sub['text']) or sub['text']

View File

@@ -325,3 +325,4 @@
| 2026-03-13 11:05:21 | 🔄 卡若AI 同步 2026-03-13 11:05 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-13 11:10:45 | 🔄 卡若AI 同步 2026-03-13 11:10 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-13 11:14:47 | 🔄 卡若AI 同步 2026-03-13 11:14 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 |
| 2026-03-13 11:49:08 | 🔄 卡若AI 同步 2026-03-13 11:49 | 更新:卡土、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 |

View File

@@ -328,3 +328,4 @@
| 2026-03-13 11:05:21 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-13 11:05 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-13 11:10:45 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-13 11:10 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-13 11:14:47 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-13 11:14 | 更新:运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-03-13 11:49:08 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-13 11:49 | 更新:卡土、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |