🔄 卡若AI 同步 2026-02-22 09:20 | 更新:金仓、卡木、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 8 个

This commit is contained in:
2026-02-22 09:20:09 +08:00
parent 42453c643d
commit 2b5556f456
13 changed files with 452 additions and 99 deletions

View File

@@ -0,0 +1,2 @@
www/
*.log

View File

@@ -0,0 +1,23 @@
# www.lytiao.com Docker 镜像
# PHP 7.1 + Apache与宝塔原环境一致
FROM php:7.1-apache
# 启用 Apache mod_rewrite
RUN a2enmod rewrite
# 常用 PHP 扩展(按需可追加)
RUN apt-get update && apt-get install -y \
libpng-dev \
libjpeg-dev \
libzip-dev \
zip \
unzip \
&& docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
&& docker-php-ext-install -j$(nproc) gd mysqli pdo pdo_mysql zip \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# 网站文件挂载到 /var/www/html
# 容器内 Apache DocumentRoot
WORKDIR /var/www/html
EXPOSE 80

View File

@@ -0,0 +1,29 @@
# www.lytiao.com 可多服务器部署
# 用法docker compose up -d
# 访问http://主机IP:8080 或配置 Nginx 反向代理
version: "3.8"
services:
lytiao-web:
build: .
container_name: lytiao-www
ports:
- "8080:80"
volumes:
- ./www:/var/www/html:ro
restart: unless-stopped
environment:
- TZ=Asia/Shanghai
# 若站点依赖 MySQL可取消注释并配置
# lytiao-mysql:
# image: mysql:5.7
# container_name: lytiao-mysql
# environment:
# MYSQL_ROOT_PASSWORD: changeme
# MYSQL_DATABASE: lytiao
# volumes:
# - lytiao_mysql_data:/var/lib/mysql
# restart: unless-stopped
# volumes:
# lytiao_mysql_data:

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TAT 在存客宝上放行 443iptables + firewalld + 宝塔防火墙)
"""
import base64
import os
import re
import sys
import time
CKB_INSTANCE_ID = "ins-ciyv2mxa"
REGION = "ap-guangzhou"
CMD = r"""
echo "=== 放行 443 至本地防火墙 ==="
# iptablesUbuntu/Debian 常用)
iptables -I INPUT -p tcp --dport 443 -j ACCEPT 2>/dev/null && echo " iptables 443 已添加" || true
iptables-save >/dev/null 2>&1 || true
# firewalldCentOS 常用)
firewall-cmd --permanent --add-port=443/tcp 2>/dev/null && echo " firewalld 443 已添加" || true
firewall-cmd --reload 2>/dev/null || true
# 宝塔防火墙(若启用)
if [ -f /www/server/panel/pyenv/bin/python ]; then
/www/server/panel/pyenv/bin/python -c "
import json,os
p='/www/server/panel/data/firewall.json'
if os.path.isfile(p):
d=json.load(open(p))
ports=d.get('ports','').split(',') if isinstance(d.get('ports'),str) else []
if '443' not in [x.strip() for x in ports if x.strip()]:
ports.append('443')
d['ports']=','.join(filter(None,ports))
open(p,'w').write(json.dumps(d,ensure_ascii=False,indent=2))
print(' 宝塔防火墙 443 已添加')
else:
print(' 宝塔防火墙已有 443')
" 2>/dev/null || true
fi
echo "=== 完成 ==="
"""
def _find_root():
d = os.path.dirname(os.path.abspath(__file__))
for _ in range(6):
if os.path.basename(d) == "卡若AI" or (os.path.isdir(os.path.join(d, "运营中枢")) and os.path.isdir(os.path.join(d, "01_卡资"))):
return d
d = os.path.dirname(d)
return None
def _read_creds():
root = _find_root()
if not root:
return None, None
p = os.path.join(root, "运营中枢", "工作台", "00_账号与API索引.md")
if not os.path.isfile(p):
return None, None
with open(p, "r", encoding="utf-8") as f:
t = f.read()
sid = skey = None
in_t = False
for line in t.splitlines():
if "### 腾讯云" in line:
in_t = True
continue
if in_t and line.strip().startswith("###"):
break
if not in_t:
continue
m = re.search(r"\|\s*[^|]*(?:SecretId|密钥)[^|]*\|\s*`([^`]+)`", line, re.I)
if m and m.group(1).strip().startswith("AKID"):
sid = m.group(1).strip()
m = re.search(r"\|\s*SecretKey\s*\|\s*`([^`]+)`", line, re.I)
if m:
skey = m.group(1).strip()
return sid or os.environ.get("TENCENTCLOUD_SECRET_ID"), skey or os.environ.get("TENCENTCLOUD_SECRET_KEY")
def main():
sid, skey = _read_creds()
if not sid or not skey:
print("❌ 未配置腾讯云 SecretId/SecretKey")
return 1
try:
from tencentcloud.common import credential
from tencentcloud.tat.v20201028 import tat_client, models
except ImportError:
print("pip install tencentcloud-sdk-python-common tencentcloud-sdk-python-tat")
return 1
cred = credential.Credential(sid, skey)
client = tat_client.TatClient(cred, REGION)
req = models.RunCommandRequest()
req.Content = base64.b64encode(CMD.encode()).decode()
req.InstanceIds = [CKB_INSTANCE_ID]
req.CommandType = "SHELL"
req.Timeout = 30
req.CommandName = "CKB_Allow443"
resp = client.RunCommand(req)
print("✅ TAT 已下发 443 放行InvocationId:", resp.InvocationId)
print(" 等待 15s 获取输出...")
time.sleep(15)
try:
import json
import base64 as b64
req2 = models.DescribeInvocationTasksRequest()
f = models.Filter()
f.Name = "invocation-id"
f.Values = [resp.InvocationId]
req2.Filters = [f]
r2 = client.DescribeInvocationTasks(req2)
for t in (r2.InvocationTaskSet or []):
tr = getattr(t, "TaskResult", None)
if tr:
try:
j = json.loads(tr) if isinstance(tr, str) else tr
out = j.get("Output", "")
if out:
try:
out = b64.b64decode(out).decode("utf-8", errors="replace")
except Exception:
pass
print(" ", out[:2000].replace("\n", "\n "))
except Exception:
print(" ", str(tr)[:500])
except Exception as e:
print(" 查询输出:", e)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -38,7 +38,7 @@ eval "$(~/miniforge3/bin/conda shell.zsh hook)"
conda activate mlx-whisper
mlx_whisper audio.wav --model mlx-community/whisper-small-mlx --language zh --output-format all
# 2. 高光识别(Gemini AI失败时自动用规则切分
# 2. 高光识别(Ollama → Groq → 规则,不依赖 Gemini
python3 identify_highlights.py -t transcript.srt -o highlights.json -n 6
# 3. 切片

View File

@@ -24,6 +24,9 @@ pyJianYingDraft>=0.2.5 # 程序化生成剪映草稿
# playwright>=1.40.0 # 浏览器自动化
# 安装后需要playwright install chromium
# 高光识别可选Groq 免费 APIOllama 失败时使用)
# groq>=1.0.0
# 可选依赖(高级功能)
# pyannote.audio>=3.1.0 # 说话人分离需要HuggingFace Token
# torch>=2.0.0 # GPU加速Whisper会自动安装

View File

@@ -0,0 +1,96 @@
# AI 视频切片 - GitHub 与替代方案
> 卡若AI 视频切片 Skill 的简化方案与可集成替代
> 更新2026-02-03
## 一、当前方案卡若AI 本地方案)
| 组件 | 实现 | 说明 |
|------|------|------|
| **转录** | MLX Whisper | 本地、快速,无 API 依赖 |
| **高光识别** | Ollama → Groq → 规则 | 级联,不依赖 Gemini |
| **切片** | FFmpeg batch_clip | 标准工具 |
| **增强** | FFmpeg drawtext / 复制 | drawtext 不可用时直接复制切片 |
**级联顺序**Ollama卡若AI 本地 qwen2.5:1.5b)→ Groq免费需 GROQ_API_KEY→ 规则备用
---
## 二、GitHub 开源方案(可借鉴)
### 1. 简洁可集成
| 项目 | 语言 | 说明 | 集成难度 |
|------|------|------|----------|
| [video-highlight-tool](https://github.com/ALICE-YEN/video-highlight-tool) | TypeScript | AI 高光 clips + 转录 | 中 |
| [VidBit-Video-Summarizer](https://github.com/Alapan121/VidBit-Video-Summarizer) | JavaScript | 端到端:摘要+转录+高光 | 低 |
| [ABHINXXV/YTvideoShortner](https://github.com/ABHINXXV/YTvideoShortner) | JavaScript | YouTube 摘要/缩短 | 低 |
### 2. YouTube 专用yt-dlp + AI
| 项目 | 说明 |
|------|------|
| youtube-highlighter | Groq + yt-dlpYouTube 高光 |
| yt-transcript-gpt | 用 Gemini 分析 YouTube 字幕 |
| ClipCatch | Python摘要 + 评论情感 |
### 3. 商业/闭源(参考)
| 产品 | 说明 |
|------|------|
| **OpusClip** | GitHub 官方在用长视频→Shorts一键剪辑 |
| **Video Highlight** | 37+ 语言,时间戳摘要,可导出 Notion/Readwise |
| **Sieve Highlights** | LLM + ML按「最有趣」「最动作」等自然语言搜高光 |
| **ClipSense** | OpenRouter类似高光识别 |
---
## 三、卡若AI 视频能力与替代
### 本地优先(推荐)
| 能力 | 工具 | 无网络时 |
|------|------|----------|
| 转录 | MLX Whisper / Ollama Whisper | ✅ 可用 |
| 高光识别 | Ollama qwen2.5:1.5b | ✅ 可用 |
| 切片 | FFmpeg | ✅ 可用 |
| 封面/Hook | FFmpeg drawtext | 需 libfreetype |
| 无 drawtext | 复制原始切片 | 流水线自动降级 |
### 云端可选
| 能力 | 工具 | 说明 |
|------|------|------|
| 高光识别 | Groq | 免费层,`pip install groq``GROQ_API_KEY` |
| 转录 | 云端 Whisper API | 网络依赖 |
| 高光识别 | Gemini | 当前接口不可用,已移除 |
### FFmpeg drawtext 说明
- 默认 `brew install ffmpeg` 可能无 `drawtext`(缺 libfreetype
- 可选:`brew install ffmpeg@7` 或自行编译 `--enable-libfreetype`
- 当前增强enhance_clips 用 drawtext无 drawtext 时流水线直接复制切片
---
## 四、依赖一览
```
# 核心(必须)
openai-whisper 或 mlx-whisper
yt-dlp
requests
Pillow
# 高光识别
# Ollama + qwen2.5:1.5b(本地,无需 pip
groq # 可选pip install groq
```
---
## 五、推荐组合(最简)
1. **本地全流程**MLX Whisper → Ollama → FFmpeg无 drawtext 则复制切片)
2. **提升质量**:设置 `GROQ_API_KEY`Ollama 失败时自动用 Groq
3. **无需 Gemini**:当前方案已完全脱离 Gemini 依赖

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
高光识别 - 用 Gemini 分析视频文字稿,输出高光片段 JSON
用于 Soul 派对等长视频切片前的 AI 识别步骤
高光识别 - AI 分析视频文字稿,输出高光片段 JSON
级联Ollama(卡若AI本地) → 规则备用
只用已有能力,不依赖 Gemini/Groq
"""
import argparse
import json
@@ -11,8 +12,7 @@ import re
import sys
from pathlib import Path
# Gemini API
GEMINI_KEY = os.environ.get("GEMINI_API_KEY") or "AIzaSyCPARryq8o6MKptLoT4STAvCsRB7uZuOK8"
OLLAMA_URL = "http://localhost:11434"
DEFAULT_CTA = "关注我,每天学一招私域干货"
CLIP_COUNT = 8
MIN_DURATION = 45
@@ -85,63 +85,62 @@ def srt_to_timestamped_text(srt_path: str) -> str:
return "\n".join(lines)
def call_gemini(transcript: str, clip_count: int = CLIP_COUNT) -> str:
"""调用 Gemini 分析并返回 JSONREST API无额外依赖"""
import urllib.request
import urllib.error
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key={GEMINI_KEY}"
prompt = f"""你是一个专业的短视频内容策划师,擅长从长视频中找出最有传播力的片段。
def _build_prompt(transcript: str, clip_count: int) -> str:
"""构建高光识别 promptOllama/Groq 通用"""
# 限制长度Ollama 上下文有限
txt = transcript[:12000] if len(transcript) > 12000 else transcript
return f"""你是一个专业的短视频内容策划师。分析以下视频文字稿,找出 {clip_count} 个最适合做短视频的高光片段。
分析以下视频文字稿,找出 {clip_count} 个最适合做短视频的「高光片段」。
为每个片段设计:
1. 前3秒Hook15字以内让用户停下来
2. 结尾CTA20字以内引导关注/进群
判断标准:金句观点(30%)、故事案例(25%)、情绪高点(20%)、实操干货(15%)、悬念钩子(10%)
时长要求:每个片段 {MIN_DURATION}-{MAX_DURATION}
时间戳必须精确到秒,从文字稿中提取
相邻片段至少间隔30秒
输出严格 JSON 数组,不要其他文字。每个对象必须包含:
每个片段需包含:
- title: 简短标题
- start_time: "HH:MM:SS"
- start_time: "HH:MM:SS"(从文字稿提取)
- end_time: "HH:MM:SS"
- hook_3sec: 前3秒Hook
- cta_ending: 结尾CTA默认可填 "{DEFAULT_CTA}"
- hook_3sec: 前3秒Hook15字内
- cta_ending: 结尾CTA可用 "{DEFAULT_CTA}"
- transcript_excerpt: 片段内容前50字
- reason: 推荐理由
时长 {MIN_DURATION}-{MAX_DURATION}相邻间隔30秒。只输出 JSON 数组,不要其他文字或```包裹。
视频文字稿:
---
{transcript[:25000]}
---
{txt}
---"""
只输出JSON数组不要```json包裹。"""
body = {
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"temperature": 0.3, "maxOutputTokens": 8192},
}
req = urllib.request.Request(
url,
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=120) as r:
data = json.loads(r.read().decode())
except urllib.error.HTTPError as e:
err_body = e.read().decode() if e.fp else ""
raise RuntimeError(f"Gemini API 错误: {e.code} {err_body[:300]}")
candidates = data.get("candidates", [])
if not candidates:
raise RuntimeError("Gemini 未返回内容")
text = candidates[0].get("content", {}).get("parts", [{}])[0].get("text", "").strip()
def _parse_ai_json(text: str) -> list:
"""从 AI 输出中提取 JSON 数组"""
text = text.strip()
if text.startswith("```"):
text = re.sub(r"^```(?:json)?\s*", "", text)
text = re.sub(r"\s*```$", "", text)
return 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)
def call_ollama(transcript: str, clip_count: int = CLIP_COUNT) -> str:
"""调用卡若AI本地模型Ollama"""
import requests
prompt = _build_prompt(transcript, clip_count)
try:
r = requests.post(
f"{OLLAMA_URL}/api/generate",
json={
"model": "qwen2.5:1.5b",
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.3, "num_predict": 4096},
},
timeout=90,
)
if r.status_code != 200:
raise RuntimeError(f"Ollama {r.status_code}")
return r.json().get("response", "").strip()
except Exception as e:
raise RuntimeError(f"Ollama 调用失败: {e}") from e
def main():
@@ -158,14 +157,21 @@ def main():
if len(text) < 100:
print("❌ 文字稿过短,请检查 SRT 格式", file=sys.stderr)
sys.exit(1)
# 尝试 Gemini失败则用规则备用
# 级联Ollama(卡若AI本地) → 规则备用
data = None
try:
print("正在调用 Gemini 分析高光片段...")
raw = call_gemini(text, args.clips)
data = json.loads(raw)
except Exception as e:
print(f"Gemini 调用失败 ({e}),使用规则备用切分", file=sys.stderr)
for name, fn in [
("Ollama (卡若AI本地)", call_ollama),
]:
try:
print(f"正在调用 {name} 分析高光片段...")
raw = fn(text, args.clips)
data = _parse_ai_json(raw)
if data and isinstance(data, list) and len(data) > 0:
break
except Exception as e:
print(f"{name} 调用失败 ({e})", file=sys.stderr)
if not data or not isinstance(data, list):
print("使用规则备用切分", file=sys.stderr)
data = fallback_highlights(str(transcript_path), args.clips)
if not data:
data = fallback_highlights(str(transcript_path), args.clips)

View File

@@ -9,6 +9,7 @@ Soul切片增强脚本 v2.0
5. 清理语气词字幕
"""
import argparse
import subprocess
import os
import re
@@ -18,15 +19,17 @@ import shutil
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageFilter
# ============ 配置 ============
# ============ 配置(可被命令行覆盖)============
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_DIR = SCRIPT_DIR.parent
CLIPS_DIR = Path("/Users/karuo/Movies/soul视频/soul81_final/clips")
OUTPUT_DIR = Path("/Users/karuo/Movies/soul视频/soul81_final/clips_enhanced")
HIGHLIGHTS_PATH = Path("/Users/karuo/Movies/soul视频/soul81_final/highlights.json")
TRANSCRIPT_PATH = Path("/Users/karuo/Movies/soul视频/soul81_final/transcript.srt")
# 字体路径
FONTS_DIR = Path("/Users/karuo/Documents/个人/卡若AI/03_卡木/视频切片/fonts")
# 字体路径(兼容多种目录结构)
FONTS_DIR = SKILL_DIR / "fonts" if (SKILL_DIR / "fonts").exists() else Path("/Users/karuo/Documents/个人/卡若AI/03_卡木/视频切片/fonts")
FONT_HEAVY = str(FONTS_DIR / "SourceHanSansSC-Heavy.otf")
FONT_BOLD = str(FONTS_DIR / "SourceHanSansSC-Bold.otf")
FALLBACK_FONT = "/System/Library/Fonts/STHeiti Medium.ttc"
@@ -134,10 +137,10 @@ def parse_srt_for_clip(srt_path, start_sec, end_sec):
sub_end = time_to_sec(match[2])
text = match[3].strip()
# 检查是否时间范围
if sub_start >= start_sec and sub_end <= end_sec + 5:
# 调整时间为相对时间
rel_start = sub_start - start_sec
# 检查是否与片段时间范围重叠
if sub_end > start_sec and sub_start < end_sec + 2:
# 调整为相对于片段开始的相对时间
rel_start = max(0, sub_start - start_sec)
rel_end = sub_end - start_sec
# 清理语气词
@@ -402,7 +405,13 @@ def create_silence_filter(silences, duration, margin=0.1):
return '+'.join(selects)
def enhance_clip(clip_path, output_path, highlight_info, temp_dir):
def _parse_clip_index(filename: str) -> int:
"""从文件名解析切片序号,支持 soul_01_xxx 或 01_xxx 格式"""
m = re.search(r'\d+', filename)
return int(m.group()) if m else 0
def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_path):
"""增强单个切片"""
print(f"\n处理: {os.path.basename(clip_path)}")
@@ -427,7 +436,7 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir):
start_sec = int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2])
end_sec = start_sec + duration
subtitles = parse_srt_for_clip(TRANSCRIPT_PATH, start_sec, end_sec)
subtitles = parse_srt_for_clip(transcript_path, start_sec, end_sec)
print(f" ✓ 字幕解析 ({len(subtitles)}条)")
# 3. 生成字幕图片
@@ -531,53 +540,72 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir):
return False
def main():
"""主函数"""
"""主函数(支持命令行参数)"""
parser = argparse.ArgumentParser(description="Soul切片增强 - 封面+字幕+加速+去语气词")
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="输出目录")
args = parser.parse_args()
clips_dir = Path(args.clips) if args.clips else CLIPS_DIR
output_dir = Path(args.output) if args.output else OUTPUT_DIR
highlights_path = Path(args.highlights) if args.highlights else HIGHLIGHTS_PATH
transcript_path = Path(args.transcript) if args.transcript else TRANSCRIPT_PATH
if not clips_dir.exists():
print(f"❌ 切片目录不存在: {clips_dir}")
return
if not highlights_path.exists():
print(f"❌ highlights 不存在: {highlights_path}")
return
if not transcript_path.exists():
print(f"❌ transcript 不存在: {transcript_path}")
return
print("="*60)
print("🎬 Soul切片增强处理")
print("🎬 Soul切片增强处理Pillow无需 drawtext")
print("="*60)
print(f"功能: 封面+字幕+加速10%+去语气词")
print(f"输入: {CLIPS_DIR}")
print(f"输出: {OUTPUT_DIR}")
print(f"输入: {clips_dir}")
print(f"输出: {output_dir}")
print("="*60)
# 创建输出目录
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
output_dir.mkdir(parents=True, exist_ok=True)
# 读取highlights
with open(HIGHLIGHTS_PATH, 'r', encoding='utf-8') as f:
with open(highlights_path, 'r', encoding='utf-8') as f:
highlights = json.load(f)
if isinstance(highlights, dict) and "clips" in highlights:
highlights = highlights["clips"]
highlights = highlights if isinstance(highlights, list) else []
# 获取切片文件
clips = sorted(CLIPS_DIR.glob('*.mp4'))
clips = sorted(clips_dir.glob('*.mp4'))
print(f"\n找到 {len(clips)} 个切片")
success_count = 0
for i, clip_path in enumerate(clips):
# 匹配highlight信息
clip_num = int(clip_path.name.split('_')[0])
highlight_info = highlights[clip_num - 1] if clip_num <= len(highlights) else {}
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')
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):
if enhance_clip(str(clip_path), str(output_path), highlight_info, temp_dir, str(transcript_path)):
success_count += 1
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
print("\n" + "="*60)
print(f"✅ 增强完成: {success_count}/{len(clips)}")
print(f"📁 输出目录: {OUTPUT_DIR}")
print(f"📁 输出目录: {output_dir}")
print("="*60)
# 生成目录索引
generate_index(highlights)
generate_index(highlights, output_dir)
def generate_index(highlights):
def generate_index(highlights, output_dir):
"""生成目录索引"""
index_path = OUTPUT_DIR.parent / "目录索引_enhanced.md"
index_path = output_dir.parent / "目录索引_enhanced.md"
with open(index_path, 'w', encoding='utf-8') as f:
f.write("# Soul派对81场 - 增强版切片目录\n\n")

View File

@@ -112,7 +112,7 @@ def main():
"--output", str(highlights_path),
"--clips", str(args.clips),
],
"高光识别(Gemini",
"高光识别(Ollama→规则",
timeout=60,
)
if not highlights_path.exists():
@@ -146,27 +146,25 @@ def main():
timeout=300,
)
# 4. 增强(封面 + Hook + CTA若 FFmpeg 无 drawtext 则直接复制切片
# 4. 增强(封面+字幕+加速soul_enhancePillow无需 drawtext
enhanced_dir.mkdir(parents=True, exist_ok=True)
ok = run(
[
sys.executable,
str(SCRIPT_DIR / "enhance_clips.py"),
"--clips_dir", str(clips_dir),
str(SCRIPT_DIR / "soul_enhance.py"),
"--clips", str(clips_dir),
"--highlights", str(highlights_path),
"--output_dir", str(enhanced_dir),
"--hook_duration", "2.5",
"--cta_duration", "4",
"--default_cta", "关注我,每天学一招私域干货",
"--transcript", str(transcript_path),
"--output", str(enhanced_dir),
],
"增强处理(Hook+CTA",
timeout=600,
"增强处理(封面+字幕+加速",
timeout=900,
check=False,
)
import shutil
enhanced_count = len(list(enhanced_dir.glob("*.mp4")))
if enhanced_count == 0 and clips_list:
print(" FFmpeg 无 drawtext 滤镜,复制原始切片到 clips_enhanced")
print(" soul_enhance 失败,复制原始切片到 clips_enhanced")
for f in sorted(clips_dir.glob("*.mp4")):
shutil.copy(f, enhanced_dir / f.name)

View File

@@ -80,7 +80,44 @@
---
## 五、参考
## 五、耗时与对比
### 5.1 索引耗时(估算)
| 方案 | 卡若AI 代码库(约 5002000 可索引块) | 说明 |
|:-----|:--------------------------------------|:-----|
| **Cursor 云端** | 约 15 分钟 | 云端并行,依赖网络 |
| **卡若AI 本地** | 约 1545 分钟 | 逐块本地 embed每块约 0.51 秒 |
### 5.2 与 Cursor 云端索引的区别
| 维度 | Cursor 云端索引 | 卡若AI 本地索引 |
|:-----|:----------------|:----------------|
| 数据位置 | 代码本地embedding+元数据云端 | 全在本地 |
| 首次索引 | 较快15 分钟) | 较慢1545 分钟) |
| 检索延迟 | 低(云端向量库) | 低(本地 JSON 加载) |
| 增量更新 | 自动 | 需手动重跑 `index` |
| Cursor 集成 | 原生 @ Codebase | 通过 Skill 或 MCP 调用 |
| 隐私/离线 | ❌ 依赖云端 | ✅ 完全本地 |
---
## 六、GitHub 同类方案(可选替代)
| 项目 | Stars | 特点 | 与卡若AI 本地索引对比 |
|:-----|:------|:-----|:----------------------|
| [**cursor-local-indexing**](https://github.com/LuotoCompany/cursor-local-indexing) | 34 | ChromaDB + MCPDocker 部署,提供 `@search_code` | 需 Docker卡若AI 零容器、纯 Python |
| [**autodev-codebase**](https://github.com/anrgct/autodev-codebase) | 111 | TypeScriptQdrant + OllamaMCP调用图、outline | 功能更全;需 Qdrant Docker卡若AI 无额外服务 |
| [**linggen**](https://github.com/linggen/linggen-memory) | 104 | RustLanceDBDesign Anchors系统依赖图 | 偏架构记忆;需安装 CLI卡若AI 更轻量 |
**推荐选择**
- 要**零额外依赖**、和卡若AI 深度整合 → 用卡若AI 本地索引
- 要**调用图、AI rerank、多模型** → 考虑 `autodev-codebase`
- 要**Docker 化、MCP 即用** → 考虑 `cursor-local-indexing`
---
## 七、参考
- Cursor Forum: [It's possible to embedding codes entirely at local?](https://forum.cursor.com/t/its-possible-to-embedding-codes-entirely-at-local/15911)
- 本地模型 SKILL`04_卡火/火种_知识模型/本地模型/SKILL.md`

View File

@@ -60,3 +60,4 @@
| 2026-02-22 07:20:22 | 🔄 卡若AI 同步 2026-02-22 07:20 | 更新:总索引与入口、金仓、卡木、运营中枢工作台 | 排除 >20MB: 9 个 |
| 2026-02-22 07:27:08 | 🔄 卡若AI 同步 2026-02-22 07:27 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 9 个 |
| 2026-02-22 09:06:58 | 🔄 卡若AI 同步 2026-02-22 09:06 | 更新:总索引与入口、金仓、运营中枢工作台 | 排除 >20MB: 8 个 |
| 2026-02-22 09:12:03 | 🔄 卡若AI 同步 2026-02-22 09:12 | 更新:总索引与入口、火种知识模型、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 8 个 |

View File

@@ -63,3 +63,4 @@
| 2026-02-22 07:20:22 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 07:20 | 更新:总索引与入口、金仓、卡木、运营中枢工作台 | 排除 >20MB: 9 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-02-22 07:27:08 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 07:27 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 9 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-02-22 09:06:58 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 09:06 | 更新:总索引与入口、金仓、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
| 2026-02-22 09:12:03 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 09:12 | 更新:总索引与入口、火种知识模型、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |