From 31794af87f62dfb9e22d96da81e7e3e404316848 Mon Sep 17 00:00:00 2001 From: karuo Date: Sun, 22 Feb 2026 13:57:31 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=84=20=E5=8D=A1=E8=8B=A5AI=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=202026-02-22=2013:57=20|=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=9A=E9=87=91=E4=BB=93=E3=80=81=E6=B0=B4=E6=BA=AA=E6=95=B4?= =?UTF-8?q?=E7=90=86=E5=BD=92=E6=A1=A3=E3=80=81=E5=8D=A1=E6=9C=A8=E3=80=81?= =?UTF-8?q?=E8=BF=90=E8=90=A5=E4=B8=AD=E6=9E=A2=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=20|=20=E6=8E=92=E9=99=A4=20>20MB:=208=20=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../金仓_存储备份/服务器管理/SKILL.md | 10 +- .../references/Node项目未启动_MODULE_NOT_FOUND修复指南.md | 37 +- .../服务器管理/references/宝塔Node项目管理_SKILL.md | 4 +- .../scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh | 171 +++++++++ .../scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py | 363 ++++++++++++++++++ .../scripts/腾讯云_TAT_word_ai_hair_is_phone_诊断修复.py | 8 +- .../记忆系统/structured/last_chat_collect_date.txt | 2 +- .../视频切片/脚本/identify_highlights.py | 23 +- .../木叶_视频内容/视频切片/脚本/soul_enhance.py | 167 ++++++-- 运营中枢/工作台/gitea_push_log.md | 1 + 运营中枢/工作台/代码管理.md | 1 + 11 files changed, 713 insertions(+), 74 deletions(-) create mode 100644 01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh create mode 100644 01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/SKILL.md b/01_卡资(金)/金仓_存储备份/服务器管理/SKILL.md index f1108250..3936658d 100644 --- a/01_卡资(金)/金仓_存储备份/服务器管理/SKILL.md +++ b/01_卡资(金)/金仓_存储备份/服务器管理/SKILL.md @@ -140,11 +140,12 @@ sshpass -p 'zhiqun1984' ssh -p 22022 -o StrictHostKeyChecking=no ckb@43.139.27.9 SSH 风控时,在 **kr宝塔 宝塔面板 → 终端** 上传脚本后执行。详见 `references/宝塔Node项目管理_SKILL.md`。 -**kr宝塔 中文路径 + MODULE_NOT_FOUND 全量修复**(符号链接、修正启动命令、批量重启): +**kr宝塔 中文目录改英文迁移**(删映射、重命名为英文、更新 site.db/Nginx、只用宝塔 Nginx、启动 Node): ```bash -./scripts/.venv_tx/bin/python scripts/腾讯云_TAT_kr宝塔_中文路径与MODULE修复.py +# 宝塔终端执行(推荐):上传 scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh 后 bash 执行 +# 或 TAT:./scripts/.venv_tx/bin/python scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py ``` -脚本会:① 创建 ext→扩展、client→客户、self→自营 符号链接;② 修正 site.db 中 `node /path` 错误启动命令为 `cd /path && npm run start`;③ pnpm install;④ 批量重启全部 Node 项目。 +迁移后路径:`/www/wwwroot/self/`、`/www/wwwroot/client/`、`/www/wwwroot/ext/`(不再使用 自营/客户/扩展)。 ### 4a. www.lytiao.com Docker 化(存客宝 · 可多服务器复用) @@ -382,7 +383,8 @@ ss -tlnp | grep :端口号 | 脚本 | 功能 | 位置 | |------|------|------| -| `腾讯云_TAT_kr宝塔_中文路径与MODULE修复.py` | kr宝塔 符号链接+修正启动命令+批量重启(TAT) | `./scripts/.venv_tx` | +| `腾讯云_TAT_kr宝塔_中文目录改英文迁移.py` | kr宝塔 中文目录改英文、删映射、更新 site.db/Nginx(TAT) | `./scripts/.venv_tx` | +| `kr宝塔_中文目录改英文_宝塔终端执行.sh` | 同上,宝塔终端手动执行 | `./scripts/` | | `腾讯云_TAT_word_ai_hair_is_phone_诊断修复.py` | word/ai_hair/is_phone 日志诊断、MODULE_NOT_FOUND 修复、重启(宝塔 API) | `./scripts/` | | `kr宝塔_node项目批量修复.py` | 批量启动 kr宝塔 Node 项目(服务器内执行,宝塔 API) | `./scripts/` | | `kr宝塔_宝塔API_修复502.py` | 修复 502(重启 Nginx + soul 相关 Node) | `./scripts/` | diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/references/Node项目未启动_MODULE_NOT_FOUND修复指南.md b/01_卡资(金)/金仓_存储备份/服务器管理/references/Node项目未启动_MODULE_NOT_FOUND修复指南.md index 60c0d6e0..6655ef20 100644 --- a/01_卡资(金)/金仓_存储备份/服务器管理/references/Node项目未启动_MODULE_NOT_FOUND修复指南.md +++ b/01_卡资(金)/金仓_存储备份/服务器管理/references/Node项目未启动_MODULE_NOT_FOUND修复指南.md @@ -4,6 +4,20 @@ --- +## 〇、中文目录改英文迁移(推荐) + +**彻底消除中文路径**:删除映射、重命名目录、更新 site.db 与 Nginx、只用宝塔 Nginx、启动 Node。 +```bash +# 方式 1:宝塔终端(推荐) +# 打开 https://43.139.27.93:9988 → 终端 → 上传 scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh → 执行 bash kr宝塔_中文目录改英文_宝塔终端执行.sh + +# 方式 2:TAT +./scripts/.venv_tx/bin/python scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py +``` +迁移后路径:`/www/wwwroot/self/`、`/www/wwwroot/client/`、`/www/wwwroot/ext/`;子目录 `wanzhi`、`tools`。 + +--- + ## 一、批量启动(先执行) 在 **宝塔面板 → 终端** 执行 `references/宝塔面板终端_Node批量启动指南.md` 中的脚本,先尝试批量启动所有未运行项目。 @@ -26,16 +40,16 @@ Error: Cannot find module '/www/wwwroot/自营/wzdj' | 项目 | 根目录 | 建议启动命令 | 说明 | |------|--------|--------------|------| -| **玩值大屏** | /www/wwwroot/自营/玩值/玩值大屏 | `cd /www/wwwroot/自营/玩值/玩值大屏 && node server.js` 或 `npm run start` | 先确认目录内有 server.js / package.json 的 scripts.start | -| **wzdj** | /www/wwwroot/自营/wzdj | `cd /www/wwwroot/自营/wzdj && node server.js` 或 `npm run start` | 同上 | -| **tongzhi** | /www/wwwroot/自营/玩值/tongzhi | `cd /www/wwwroot/自营/玩值/tongzhi && node server.js` 或 `npm run start` | 同上 | -| **is_phone** | /www/wwwroot/自营/kr/kr-phone | `cd /www/wwwroot/自营/kr/kr-phone && node server.js` 或 `npm run start` | 同上 | -| **ai_hair** | /www/wwwroot/客户/ai_hair | 同上 | 同上 | -| **word** | /www/wwwroot/自营/word 或 扩展/word | `cd 项目根目录 && npm run start` | Next.js,按实际路径 | -| **AITOUFA** | /www/wwwroot/扩展/小工具/AITOUFA | `cd /www/wwwroot/扩展/小工具/AITOUFA && npm run start` | 参考 Skill §4.6 | -| **zhiji** | /www/wwwroot/... | 同上 | 按实际结构 | -| **ymao** | /www/wwwroot/扩展/ymao | 同上 | 同上 | -| **zhaoping** | /www/wwwroot/客户/zhaoping | 同上 | 同上 | +| **玩值大屏** | /www/wwwroot/self/wanzhi/玩值大屏 | `cd /www/wwwroot/self/wanzhi/玩值大屏 && (pnpm start || npm run start)` | 迁移后使用英文路径 | +| **wzdj** | /www/wwwroot/self/wzdj | 同上 | 同上 | +| **tongzhi** | /www/wwwroot/self/wanzhi/tongzhi | 同上 | 同上 | +| **is_phone** | /www/wwwroot/self/kr/kr-phone | 同上 | 同上 | +| **ai_hair** | /www/wwwroot/client/ai_hair | 同上 | 同上 | +| **word** | /www/wwwroot/self/word | 同上 | 同上 | +| **AITOUFA** | /www/wwwroot/ext/tools/AITOUFA | 同上 | 同上 | +| **zhiji** | /www/wwwroot/self/zhiji | 同上 | 同上 | +| **ymao** | /www/wwwroot/ext/ymao | 同上 | 同上 | +| **zhaoping** | /www/wwwroot/client/zhaoping | 同上 | 同上 | --- @@ -44,6 +58,7 @@ Error: Cannot find module '/www/wwwroot/自营/wzdj' 1. **Next.js**:`cd 项目根目录 && npm run start` 或 `pnpm start`;若用 standalone:`node .next/standalone/server.js` 2. **Express / 普通 Node**:`cd 项目根目录 && node server.js` 或 `node index.js` 3. **禁止**:`node /www/wwwroot/xxx/项目名`(目录不能当入口) +4. **路径规范**:使用英文路径 `/www/wwwroot/self/`、`/www/wwwroot/client/`、`/www/wwwroot/ext/`,不再使用 自营/客户/扩展。 --- @@ -62,7 +77,7 @@ Error: Cannot find module '/www/wwwroot/自营/wzdj' 在宝塔终端执行,确认各项目有入口文件: ```bash -for d in /www/wwwroot/自营/玩值/玩值大屏 /www/wwwroot/自营/wzdj /www/wwwroot/自营/玩值/tongzhi /www/wwwroot/自营/kr/kr-phone; do +for d in /www/wwwroot/self/wanzhi/玩值大屏 /www/wwwroot/self/wzdj /www/wwwroot/self/wanzhi/tongzhi /www/wwwroot/self/kr/kr-phone; do echo "=== $d ===" ls -la "$d/" 2>/dev/null | grep -E "server\.js|index\.js|package\.json|\.next" || echo " (无常见入口)" done diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/references/宝塔Node项目管理_SKILL.md b/01_卡资(金)/金仓_存储备份/服务器管理/references/宝塔Node项目管理_SKILL.md index c814dcab..4b653631 100644 --- a/01_卡资(金)/金仓_存储备份/服务器管理/references/宝塔Node项目管理_SKILL.md +++ b/01_卡资(金)/金仓_存储备份/服务器管理/references/宝塔Node项目管理_SKILL.md @@ -179,12 +179,12 @@ python3 "01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔 ### 4.6 MODULE_NOT_FOUND(如 AITOUFA) -**现象**:`Error: Cannot find module '/www/wwwroot/扩展/小工具/AITOUFA'`,Node 把项目根目录当入口执行。 +**现象**:`Error: Cannot find module '/www/wwwroot/ext/tools/AITOUFA'`(迁移前为 扩展/小工具),Node 把项目根目录当入口执行。 **原因**:启动命令配置错误,例如写成 `node /项目根目录` 而非 `node server.js` 或 `npm start`。 **处理**:宝塔 **Node 项目 → 编辑该项目**,将启动命令改为: -- Next.js:`cd /www/wwwroot/扩展/小工具/AITOUFA && npm run start` 或 `pnpm start` +- Next.js:`cd /www/wwwroot/ext/tools/AITOUFA && npm run start` 或 `pnpm start` - 或正确的入口:`node server.js` / `node index.js`(在项目根目录执行) ### 4.7 宝塔与 PM2 冲突 diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh new file mode 100644 index 00000000..dd441c55 --- /dev/null +++ b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# kr宝塔 中文目录改英文迁移 - 在宝塔面板「终端」中执行 +# 1. 删映射 2. 重命名目录 3. 更新 site.db 4. 更新 Nginx 5. 只用宝塔 Nginx 6. 启动 Node + +echo "=== kr宝塔 中文目录改英文迁移 ===" + +# 1. 停止 Node(面板先到 Node 项目 手动停止,或下方 API) +echo "" +echo "【1】停止 Node" +python3 -c ' +import hashlib,json,time,urllib.request,urllib.parse,ssl +ssl._create_default_https_context=ssl._create_unverified_context +K="qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT" +t=int(time.time()) +tk=hashlib.md5((str(t)+hashlib.md5(K.encode()).hexdigest()).encode()).hexdigest() +def post(p,d=None): + r=urllib.request.Request("https://127.0.0.1:9988"+p,data=urllib.parse.urlencode({"request_time":t,"request_token":tk,**({}if d is None else d)}).encode()) + return json.loads(urllib.request.urlopen(r,timeout=15).read().decode()) +for it in post("/project/nodejs/get_project_list").get("data")or post("/project/nodejs/get_project_list").get("list")or[]: + n=it.get("name") + if n: + try: post("/project/nodejs/stop_project",{"project_name":n});print(" stop:",n) + except: pass + time.sleep(0.3) +' +sleep 3 + +# 2. 删除符号链接 +echo "" +echo "【2】删除符号链接" +cd /www/wwwroot +for x in ext client self archive test; do [ -L "$x" ] && rm -f "$x" && echo " rm $x"; done + +# 3. 重命名 +echo "" +echo "【3】重命名目录" +cd /www/wwwroot +([ -d "扩展" ] && [ ! -e "ext" ] && mv 扩展 ext && echo " 扩展->ext") || true +([ -d "客户" ] && [ ! -e "client" ] && mv 客户 client && echo " 客户->client") || true +([ -d "自营" ] && [ ! -e "self" ] && mv 自营 self && echo " 自营->self") || true +([ -d "self/玩值" ] && [ ! -e "self/wanzhi" ] && mv self/玩值 self/wanzhi && echo " 玩值->wanzhi") || true +([ -d "ext/小工具" ] && [ ! -e "ext/tools" ] && mv ext/小工具 ext/tools && echo " 小工具->tools") || true +([ -d "归档" ] && [ ! -e "archive" ] && mv 归档 archive && echo " 归档->archive") || true +([ -d "测试" ] && [ ! -e "test" ] && mv 测试 test && echo " 测试->test") || true + +# 4. 更新 site.db +echo "" +echo "【4】更新 site.db" +python3 << 'PYDB' +import json,os,sqlite3 +R=[("/www/wwwroot/自营/玩值/","/www/wwwroot/self/wanzhi/"),("/www/wwwroot/自营/","/www/wwwroot/self/"),("/www/wwwroot/扩展/小工具/","/www/wwwroot/ext/tools/"),("/www/wwwroot/扩展/","/www/wwwroot/ext/"),("/www/wwwroot/客户/","/www/wwwroot/client/"),("/www/wwwroot/归档/","/www/wwwroot/archive/"),("/www/wwwroot/测试/","/www/wwwroot/test/")] +def rp(s): + if not s: return s + for a,b in R: s=s.replace(a,b) + return s +def ro(o): + if isinstance(o,dict): return {k:ro(v) for k,v in o.items()} + if isinstance(o,list): return [ro(x) for x in o] + if isinstance(o,str) and "/www/wwwroot/" in o: return rp(o) + return o +db="/www/server/panel/data/db/site.db" +if os.path.isfile(db): + c=sqlite3.connect(db) + cur=c.cursor() + cur.execute("SELECT id,path,project_config FROM sites") + n=0 + for row in cur.fetchall(): + sid,path,cfg=row[0],row[1]or"",row[2]or"{}" + np=rp(path) + try: nc=json.dumps(ro(json.loads(cfg)),ensure_ascii=False) + except: nc=rp(cfg) + if np!=path or nc!=cfg: cur.execute("UPDATE sites SET path=?,project_config=? WHERE id=?",(np,nc,sid)); n+=1 + c.commit();c.close() + print(" 更新%d条"%n) +PYDB + +# 5. 更新 Nginx +echo "" +echo "【5】更新 Nginx 配置" +for d in /www/server/panel/vhost/nginx /www/server/nginx/conf/vhost; do + [ -d "$d" ] || continue + for f in "$d"/*.conf; do + [ -f "$f" ] || continue + if grep -qE '自营|扩展|客户|玩值|小工具|归档|测试' "$f" 2>/dev/null; then + sed -i 's|/www/wwwroot/自营/玩值/|/www/wwwroot/self/wanzhi/|g; s|/www/wwwroot/自营/|/www/wwwroot/self/|g; s|/www/wwwroot/扩展/小工具/|/www/wwwroot/ext/tools/|g; s|/www/wwwroot/扩展/|/www/wwwroot/ext/|g; s|/www/wwwroot/客户/|/www/wwwroot/client/|g; s|/www/wwwroot/归档/|/www/wwwroot/archive/|g; s|/www/wwwroot/测试/|/www/wwwroot/test/|g' "$f" + echo " 更新: $f" + fi + done +done + +# 6. 只用宝塔 Nginx +echo "" +echo "【6】Nginx 只用宝塔版" +killall nginx 2>/dev/null || true +sleep 2 +/www/server/nginx/sbin/nginx -c /www/server/nginx/conf/nginx.conf 2>/dev/null +sleep 1 +nginx -t 2>/dev/null && nginx -s reload 2>/dev/null +echo " 宝塔 Nginx 已启动" + +# 7. 更新 Node 启动命令 +echo "" +echo "【7】更新 Node 启动命令" +python3 << 'PYNC' +import json,sqlite3 +P={"玩值大屏":"/www/wwwroot/self/wanzhi/玩值大屏","tongzhi":"/www/wwwroot/self/wanzhi/tongzhi","is_phone":"/www/wwwroot/self/kr/kr-phone","ai_hair":"/www/wwwroot/client/ai_hair","AITOUFA":"/www/wwwroot/ext/tools/AITOUFA","wzdj":"/www/wwwroot/self/wzdj","zhiji":"/www/wwwroot/self/zhiji","ymao":"/www/wwwroot/ext/ymao","zhaoping":"/www/wwwroot/client/zhaoping","神射手":"/www/wwwroot/self/kr/kr-use","word":"/www/wwwroot/self/word"} +db="/www/server/panel/data/db/site.db" +if __import__("os").path.isfile(db): + c=sqlite3.connect(db) + cur=c.cursor() + cur.execute("SELECT id,name,project_config FROM sites WHERE project_type='Node'") + for row in cur.fetchall(): + sid,name,cfg=row[0],row[1],row[2]or"{}" + path=P.get(name) + if path: + try: j=json.loads(cfg) + except: j={} + cmd="cd %s && (pnpm start 2>/dev/null || npm run start)"%path + j["project_script"]=j["run_cmd"]=cmd + cur.execute("UPDATE sites SET project_config=? WHERE id=?",(json.dumps(j,ensure_ascii=False),sid)) + print(" ",name) + c.commit();c.close() +PYNC + +# 8. 启动 Node +echo "" +echo "【8】启动 Node 项目" +python3 -c ' +import hashlib,json,time,urllib.request,urllib.parse,ssl,subprocess,re,os +ssl._create_default_https_context=ssl._create_unverified_context +K="qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT" +def sign(): + t=int(time.time()) + return {"request_time":t,"request_token":hashlib.md5((str(t)+hashlib.md5(K.encode()).hexdigest()).encode()).hexdigest()} +def post(p,d=None): + pl=sign() + if d: pl.update(d) + r=urllib.request.Request("https://127.0.0.1:9988"+p,data=urllib.parse.urlencode(pl).encode()) + return json.loads(urllib.request.urlopen(r,timeout=30).read().decode()) +def pids(port): + try: return {int(x) for x in re.findall(r"pid=(\d+)",subprocess.check_output("ss -tlnp 2>/dev/null | grep \":%s \" || true"%port,shell=True).decode())} + except: return set() +def ports(it): + cfg=it.get("project_config") or {} + if isinstance(cfg,str): cfg=json.loads(cfg) if cfg else {} + p=[] + if cfg.get("port"): p.append(int(cfg["port"])) + p.extend(int(m) for m in re.findall(r"-p\s*(\d+)",str(cfg.get("project_script","")))) + return p +items=post("/project/nodejs/get_project_list").get("data")or post("/project/nodejs/get_project_list").get("list")or[] +for it in items: + n=it.get("name") + if not n: continue + try: + for port in ports(it): + for pid in pids(port): subprocess.call("kill -9 %s 2>/dev/null"%pid,shell=True) + pf="/www/server/nodejs/vhost/pids/%s.pid"%n + if os.path.exists(pf): open(pf,"w").write("0") + post("/project/nodejs/stop_project",{"project_name":n}) + time.sleep(0.5) + r=post("/project/nodejs/start_project",{"project_name":n}) + print(" %s: %s"%(n,"OK" if r.get("status") or "成功" in str(r.get("msg","")) else "FAIL")) + except: print(" %s: ERR"%n) + time.sleep(1) +time.sleep(5) +r2=post("/project/nodejs/get_project_list") +items2=r2.get("data")or r2.get("list")or[] +print(" 运行 %d/%d"%(sum(1 for x in items2 if x.get("run")),len(items2))) +' +echo "" +echo "=== 完成 ===" diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py new file mode 100644 index 00000000..5867a567 --- /dev/null +++ b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +腾讯云 TAT:kr宝塔 中文目录改英文迁移 +1. 停止全部 Node 项目 +2. 删除 /www/wwwroot 下所有符号链接(ext->扩展、client->客户、self->自营 等) +3. 重命名中文目录为英文:扩展→ext、客户→client、自营→self、玩值→wanzhi、小工具→tools +4. 更新 site.db 中所有 path、project_config 路径 +5. 更新 Nginx vhost 配置中所有中文路径 +6. 强制只用宝塔 Nginx(killall nginx 后启动宝塔版) +7. 批量启动全部 Node 项目 +""" +import base64 +import json +import os +import re +import sys +import time + +KR_INSTANCE_ID = "ins-aw0tnqjo" +REGION = "ap-guangzhou" + +# 中文路径 → 英文路径(用于 site.db、nginx 批量替换) +PATH_REPLACES = [ + ("/www/wwwroot/自营/玩值/", "/www/wwwroot/self/wanzhi/"), + ("/www/wwwroot/自营/", "/www/wwwroot/self/"), + ("/www/wwwroot/扩展/小工具/", "/www/wwwroot/ext/tools/"), + ("/www/wwwroot/扩展/", "/www/wwwroot/ext/"), + ("/www/wwwroot/客户/", "/www/wwwroot/client/"), + ("/www/wwwroot/归档/", "/www/wwwroot/archive/"), + ("/www/wwwroot/测试/", "/www/wwwroot/test/"), +] +# 替换顺序:先替换更长的路径,避免 /自营/ 误替换 /自营/玩值/ 的前缀 + +SHELL_SCRIPT = r'''#!/bin/bash +echo "=== kr宝塔 中文目录改英文迁移 ===" + +# 0. 宝塔面板 +echo "" +echo "【0】宝塔面板" +if ! ss -tlnp 2>/dev/null | grep -q ':9988 '; then + /etc/init.d/bt start 2>/dev/null || /www/server/panel/bt start 2>/dev/null || true + sleep 5 +fi + +# 1. 停止全部 Node 项目 +echo "" +echo "【1】停止 Node 项目" +python3 - << 'PY1' +import hashlib, json, time, urllib.request, urllib.parse, ssl +ssl._create_default_https_context = ssl._create_unverified_context +PANEL, K = "https://127.0.0.1:9988", "qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT" +def sign(): + t = int(time.time()) + return {"request_time": t, "request_token": __import__("hashlib").md5((str(t) + __import__("hashlib").md5(K.encode()).hexdigest()).encode()).hexdigest()} +def post(p, d=None): + pl = sign() + if d: pl.update(d) + r = urllib.request.Request(PANEL + p, data=urllib.parse.urlencode(pl).encode()) + with urllib.request.urlopen(r, timeout=30) as resp: + return json.loads(resp.read().decode()) +items = post("/project/nodejs/get_project_list").get("data") or post("/project/nodejs/get_project_list").get("list") or [] +for it in items: + name = it.get("name") + if name: + try: post("/project/nodejs/stop_project", {"project_name": name}); print(" 停:", name) + except: pass + time.sleep(0.3) +PY1 + +sleep 3 + +# 2. 删除符号链接 +echo "" +echo "【2】删除符号链接" +cd /www/wwwroot +for x in ext client self archive test; do + if [ -L "$x" ]; then + rm -f "$x" && echo " 删除链接: $x" + fi +done + +# 3. 重命名中文目录为英文(按依赖顺序) +echo "" +echo "【3】重命名目录" +cd /www/wwwroot +([ -d "扩展" ] && [ ! -e "ext" ] && mv "扩展" "ext" && echo " 扩展 -> ext") || true +([ -d "客户" ] && [ ! -e "client" ] && mv "客户" "client" && echo " 客户 -> client") || true +([ -d "自营" ] && [ ! -e "self" ] && mv "自营" "self" && echo " 自营 -> self") || true +([ -d "self/玩值" ] && [ ! -e "self/wanzhi" ] && mv "self/玩值" "self/wanzhi" && echo " 玩值 -> wanzhi") || true +([ -d "ext/小工具" ] && [ ! -e "ext/tools" ] && mv "ext/小工具" "ext/tools" && echo " 小工具 -> tools") || true +([ -d "归档" ] && [ ! -e "archive" ] && mv "归档" "archive" && echo " 归档 -> archive") || true +([ -d "测试" ] && [ ! -e "test" ] && mv "测试" "test" && echo " 测试 -> test") || true + +# 4. 更新 site.db +echo "" +echo "【4】更新 site.db" +python3 - << 'PY2' +import json, os, re, sqlite3 + +REPLACES = [ + ("/www/wwwroot/自营/玩值/", "/www/wwwroot/self/wanzhi/"), + ("/www/wwwroot/自营/", "/www/wwwroot/self/"), + ("/www/wwwroot/扩展/小工具/", "/www/wwwroot/ext/tools/"), + ("/www/wwwroot/扩展/", "/www/wwwroot/ext/"), + ("/www/wwwroot/客户/", "/www/wwwroot/client/"), + ("/www/wwwroot/归档/", "/www/wwwroot/archive/"), + ("/www/wwwroot/测试/", "/www/wwwroot/test/"), +] + +def replace_path(s): + if not s or not isinstance(s, str): return s + for a, b in REPLACES: + s = s.replace(a, b) + return s + +def replace_in_obj(obj): + if isinstance(obj, dict): + return {k: replace_in_obj(v) for k, v in obj.items()} + if isinstance(obj, list): + return [replace_in_obj(x) for x in obj] + if isinstance(obj, str) and "/www/wwwroot/" in obj: + return replace_path(obj) + return obj + +db = "/www/server/panel/data/db/site.db" +if os.path.isfile(db): + conn = sqlite3.connect(db) + c = conn.cursor() + # 获取需要 path 或 project_config 的列 + c.execute("PRAGMA table_info(sites)") + cols = [r[1] for r in c.fetchall()] + path_cols = [x for x in cols if "path" in x.lower() or "config" in x.lower()] + c.execute("SELECT id, path, project_config FROM sites") + n = 0 + for row in c.fetchall(): + sid, path, cfg = row[0], row[1] or "", row[2] or "{}" + new_path = replace_path(path) + try: + cfg_obj = json.loads(cfg) if cfg else {} + new_cfg = replace_in_obj(cfg_obj) + new_cfg_str = json.dumps(new_cfg, ensure_ascii=False) + except: + new_cfg_str = replace_path(cfg) + if new_path != path or new_cfg_str != cfg: + c.execute("UPDATE sites SET path=?, project_config=? WHERE id=?", (new_path, new_cfg_str, sid)) + n += 1 + conn.commit() + conn.close() + print(" 更新 %d 条 sites 记录" % n) +else: + print(" site.db 不存在") +PY2 + +# 5. 更新 Nginx 配置 +echo "" +echo "【5】更新 Nginx 配置" +for f in /www/server/panel/vhost/nginx/*.conf /www/server/nginx/conf/vhost/*.conf 2>/dev/null; do + [ -f "$f" ] || continue + if grep -q "自营\|扩展\|客户\|玩值\|小工具\|归档\|测试" "$f" 2>/dev/null; then + sed -i 's|/www/wwwroot/自营/玩值/|/www/wwwroot/self/wanzhi/|g' "$f" + sed -i 's|/www/wwwroot/自营/|/www/wwwroot/self/|g' "$f" + sed -i 's|/www/wwwroot/扩展/小工具/|/www/wwwroot/ext/tools/|g' "$f" + sed -i 's|/www/wwwroot/扩展/|/www/wwwroot/ext/|g' "$f" + sed -i 's|/www/wwwroot/客户/|/www/wwwroot/client/|g' "$f" + sed -i 's|/www/wwwroot/归档/|/www/wwwroot/archive/|g' "$f" + sed -i 's|/www/wwwroot/测试/|/www/wwwroot/test/|g' "$f" + echo " 已更新: $f" + fi +done + +# 6. 强制只用宝塔 Nginx +echo "" +echo "【6】Nginx 只用宝塔版" +killall nginx 2>/dev/null || true +sleep 2 +/www/server/nginx/sbin/nginx -c /www/server/nginx/conf/nginx.conf 2>/dev/null || true +sleep 1 +nginx -t 2>/dev/null && nginx -s reload 2>/dev/null +echo " 宝塔 Nginx 已启动并重载" + +# 7. 更新 Node 项目 project_config 中的启动命令路径 +echo "" +echo "【7】更新 Node 启动命令路径" +python3 - << 'PY3' +import hashlib, json, os, sqlite3, time, urllib.request, urllib.parse, ssl +ssl._create_default_https_context = ssl._create_unverified_context +REPLACES = [ + ("/www/wwwroot/自营/玩值/", "/www/wwwroot/self/wanzhi/"), + ("/www/wwwroot/自营/", "/www/wwwroot/self/"), + ("/www/wwwroot/扩展/小工具/", "/www/wwwroot/ext/tools/"), + ("/www/wwwroot/扩展/", "/www/wwwroot/ext/"), + ("/www/wwwroot/客户/", "/www/wwwroot/client/"), +] +def replace_path(s): + for a, b in REPLACES: s = s.replace(a, b) + return s + +PROJECT_CMD = { + "玩值大屏": "/www/wwwroot/self/wanzhi/玩值大屏", + "tongzhi": "/www/wwwroot/self/wanzhi/tongzhi", + "is_phone": "/www/wwwroot/self/kr/kr-phone", + "ai_hair": "/www/wwwroot/client/ai_hair", + "AITOUFA": "/www/wwwroot/ext/tools/AITOUFA", + "wzdj": "/www/wwwroot/self/wzdj", + "zhiji": "/www/wwwroot/self/zhiji", + "ymao": "/www/wwwroot/ext/ymao", + "zhaoping": "/www/wwwroot/client/zhaoping", + "神射手": "/www/wwwroot/self/kr/kr-use", + "word": "/www/wwwroot/self/word", +} +db = "/www/server/panel/data/db/site.db" +if os.path.isfile(db): + conn = sqlite3.connect(db) + c = conn.cursor() + c.execute("SELECT id, name, project_config FROM sites WHERE project_type='Node'") + for row in c.fetchall(): + sid, name, cfg_str = row[0], row[1], row[2] or "{}" + path = PROJECT_CMD.get(name) + if not path: continue + try: cfg = json.loads(cfg_str) + except: cfg = {} + cmd = "cd %s && (pnpm start 2>/dev/null || npm run start)" % path + cfg["project_script"] = cmd + cfg["run_cmd"] = cmd + c.execute("UPDATE sites SET project_config=? WHERE id=?", (json.dumps(cfg, ensure_ascii=False), sid)) + print(" %s" % name) + conn.commit() + conn.close() +PY3 + +# 8. 批量启动 Node 项目 +echo "" +echo "【8】批量启动 Node 项目" +python3 - << 'PY4' +import hashlib, json, os, re, subprocess, time, urllib.request, urllib.parse, ssl +ssl._create_default_https_context = ssl._create_unverified_context +PANEL, K = "https://127.0.0.1:9988", "qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT" +def sign(): + t = int(time.time()) + s = str(t) + hashlib.md5(K.encode()).hexdigest() + return {"request_time": t, "request_token": hashlib.md5(s.encode()).hexdigest()} +def post(p, d=None): + pl = sign() + if d: pl.update(d) + r = urllib.request.Request(PANEL + p, data=urllib.parse.urlencode(pl).encode()) + with urllib.request.urlopen(r, timeout=30) as resp: + return json.loads(resp.read().decode()) +def pids(port): + try: + o = subprocess.check_output("ss -tlnp 2>/dev/null | grep ':%s ' || true" % port, shell=True, universal_newlines=True) + return {int(x) for x in re.findall(r"pid=(\d+)", o)} + except: return set() +def ports(it): + cfg = it.get("project_config") or {} + if isinstance(cfg, str): + try: cfg = json.loads(cfg) + except: cfg = {} + ps = [] + if cfg.get("port"): ps.append(int(cfg["port"])) + for m in re.findall(r"-p\s*(\d+)", str(cfg.get("project_script",""))): ps.append(int(m)) + return ps + +r0 = post("/project/nodejs/get_project_list") +items = r0.get("data") or r0.get("list") or [] +for it in items: + name = it.get("name") + if not name: continue + try: + for port in ports(it): + for pid in pids(port): + subprocess.call("kill -9 %s 2>/dev/null" % pid, shell=True) + pf = "/www/server/nodejs/vhost/pids/%s.pid" % name + if os.path.exists(pf): + try: open(pf,"w").write("0") + except: pass + post("/project/nodejs/stop_project", {"project_name": name}) + time.sleep(0.5) + r = post("/project/nodejs/start_project", {"project_name": name}) + ok = r.get("status") is True or "成功" in str(r.get("msg","")) + print(" %s: %s" % (name, "OK" if ok else "FAIL")) + except Exception as e: + print(" %s: ERR" % name) + time.sleep(1) + +time.sleep(5) +r1 = post("/project/nodejs/get_project_list") +items2 = r1.get("data") or r1.get("list") or [] +run_c = sum(1 for x in items2 if x.get("run")) +print(" 运行 %d / %d" % (run_c, len(items2))) +PY4 + +echo "" +echo "=== 迁移完成 ===" +''' + +def _read_creds(): + d = os.path.dirname(os.path.abspath(__file__)) + for _ in range(6): + if os.path.isfile(os.path.join(d, "运营中枢", "工作台", "00_账号与API索引.md")): + with open(os.path.join(d, "运营中枢", "工作台", "00_账号与API索引.md")) 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"SecretId[^|]*\|\s*`([^`]+)`", line, re.I) + if m and "AKID" in m.group(1): sid = m.group(1).strip() + m = re.search(r"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") + d = os.path.dirname(d) + return None, None + + +def main(): + sid, skey = _read_creds() + if not sid or not skey: + print("❌ 未配置腾讯云凭证"); return 1 + try: + from tencentcloud.common import credential + from tencentcloud.tat.v20201028 import tat_client, models + except ImportError: + print("pip install tencentcloud-sdk-python-tat"); return 1 + + cred = credential.Credential(sid, skey) + client = tat_client.TatClient(cred, REGION) + req = models.RunCommandRequest() + req.Content = base64.b64encode(SHELL_SCRIPT.encode("utf-8")).decode() + req.InstanceIds = [KR_INSTANCE_ID] + req.CommandType = "SHELL" + req.Timeout = 600 + req.CommandName = "kr宝塔_中文目录改英文迁移" + resp = client.RunCommand(req) + print("✅ TAT 已下发 InvocationId:", resp.InvocationId) + print(" 步骤: 停 Node → 删映射 → 重命名目录 → 更新 site.db → 更新 Nginx → 只启用宝塔 Nginx → 启动 Node") + print(" 等待 180s...") + time.sleep(180) + try: + req2 = models.DescribeInvocationTasksRequest() + f = models.Filter() + f.Name, f.Values = "invocation-id", [resp.InvocationId] + req2.Filters = [f] + r2 = client.DescribeInvocationTasks(req2) + for t in (r2.InvocationTaskSet or []): + print(" 状态:", getattr(t, "TaskStatus", "")) + tr = getattr(t, "TaskResult", None) + if tr: + j = json.loads(tr) if isinstance(tr, str) else {} + out = j.get("Output", "") + if out: + try: out = base64.b64decode(out).decode("utf-8", errors="replace") + except: pass + print(" 输出:\n", (out or "")[:6000]) + except Exception as e: + print(" 查询:", e) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_word_ai_hair_is_phone_诊断修复.py b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_word_ai_hair_is_phone_诊断修复.py index 0da84053..b9778331 100644 --- a/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_word_ai_hair_is_phone_诊断修复.py +++ b/01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_word_ai_hair_is_phone_诊断修复.py @@ -21,9 +21,9 @@ TARGET_NAMES = ["word", "ai_hair", "is_phone"] # 已知项目路径(API 无 path 时的后备) PATH_MAP = { - "word": "/www/wwwroot/自营/word", - "ai_hair": "/www/wwwroot/客户/ai_hair", - "is_phone": "/www/wwwroot/自营/kr/kr-phone", + "word": "/www/wwwroot/self/word", + "ai_hair": "/www/wwwroot/client/ai_hair", + "is_phone": "/www/wwwroot/self/kr/kr-phone", } SHELL_SCRIPT = r'''#!/bin/bash @@ -33,7 +33,7 @@ python3 - << 'PYEOF' import hashlib, json, os, subprocess, time, urllib.request, urllib.parse, ssl ssl._create_default_https_context = ssl._create_unverified_context PANEL, K = "https://127.0.0.1:9988", "qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT" -PATH_MAP = {"word": "/www/wwwroot/自营/word", "ai_hair": "/www/wwwroot/客户/ai_hair", "is_phone": "/www/wwwroot/自营/kr/kr-phone"} +PATH_MAP = {"word": "/www/wwwroot/self/word", "ai_hair": "/www/wwwroot/client/ai_hair", "is_phone": "/www/wwwroot/self/kr/kr-phone"} def sign(): t = int(time.time()) s = str(t) + hashlib.md5(K.encode()).hexdigest() diff --git a/02_卡人(水)/水溪_整理归档/记忆系统/structured/last_chat_collect_date.txt b/02_卡人(水)/水溪_整理归档/记忆系统/structured/last_chat_collect_date.txt index d245bbfc..0ec317cc 100644 --- a/02_卡人(水)/水溪_整理归档/记忆系统/structured/last_chat_collect_date.txt +++ b/02_卡人(水)/水溪_整理归档/记忆系统/structured/last_chat_collect_date.txt @@ -1 +1 @@ -2026-02-16 \ No newline at end of file +2026-02-22 \ No newline at end of file diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py index 9a20cec3..6f3f379d 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/identify_highlights.py @@ -15,8 +15,8 @@ from pathlib import Path OLLAMA_URL = "http://localhost:11434" DEFAULT_CTA = "关注我,每天学一招私域干货" CLIP_COUNT = 8 -MIN_DURATION = 45 -MAX_DURATION = 150 +MIN_DURATION = 60 # 1 分钟起 +MAX_DURATION = 180 # 3 分钟 def parse_srt_segments(srt_path: str) -> list: @@ -42,16 +42,16 @@ def parse_srt_segments(srt_path: str) -> list: def fallback_highlights(transcript_path: str, clip_count: int) -> list: - """规则备用:按时长均匀切分,取每段首句为 Hook""" + """规则备用:按时长均匀切分,每段 60-180 秒""" segments = parse_srt_segments(transcript_path) if not segments: return [] total = segments[-1]["end_sec"] if segments else 0 - interval = max(60, total / (clip_count + 1)) + interval = max(120, total / clip_count) # 每段约 2 分钟 result = [] for i in range(clip_count): - start_sec = int(interval * (i + 0.2)) - end_sec = min(int(start_sec + 90), int(total - 5)) + start_sec = int(interval * i + 30) + end_sec = min(int(start_sec + 120), int(total - 5)) # 约 2 分钟 if end_sec <= start_sec + 30: continue # 找该时间段内的字幕 @@ -86,14 +86,15 @@ def srt_to_timestamped_text(srt_path: str) -> str: def _build_prompt(transcript: str, clip_count: int) -> str: - """构建高光识别 prompt(完整观点+干货,全中文)""" - txt = transcript[:15000] if len(transcript) > 15000 else transcript + """构建高光识别 prompt(完整观点+干货,1-3分钟,全中文)""" + txt = transcript[:18000] if len(transcript) > 18000 else transcript return f"""你是资深短视频策划师。请从视频文字稿中识别 {clip_count} 个**完整的核心观点/干货片段**。 【切片原则】 -- 每个片段必须是**完整的一句话/一个观点**,有头有尾,不能截断 -- 优先选:金句、完整故事、可操作方法论、反常识观点、情绪高点 -- 每个片段时长 {MIN_DURATION}-{MAX_DURATION} 秒,相邻片段间隔至少 30 秒 +- 每个片段必须是**完整的一个话题/观点**,有头有尾,逻辑闭环,不能截断 +- 时长 **60-180 秒(1-3 分钟)**,尽量接近 2 分钟,确保内容完整 +- 优先选:金句、完整故事、可操作方法论、反常识观点、情绪高点、成体系讲解 +- 相邻片段间隔至少 60 秒 【输出字段】所有内容**必须使用简体中文**,若原文是英文请翻译后填写: - title: 核心观点标题(15字内,用于文件名) diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py index 0bf640d9..92b2729d 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/soul_enhance.py @@ -92,22 +92,27 @@ KEYWORDS = [ '核心', '关键', '重点', '赚钱', '收入', '利润', ] -# 字体优先级(Mac 优先苹方,更清晰) +# 字体优先级(封面用更好看的字体) FONT_PRIORITY = [ - "/System/Library/Fonts/PingFang.ttc", # 苹方-简(Mac 默认,清晰) + "/System/Library/Fonts/PingFang.ttc", # 苹方-简 + "/System/Library/Fonts/Supplemental/Songti.ttc", # 宋体-简 "/System/Library/Fonts/STHeiti Medium.ttc", "/Library/Fonts/Arial Unicode.ttf", ] +COVER_FONT_PRIORITY = [ + "/System/Library/Fonts/PingFang.ttc", # 苹方,封面优先 + "/System/Library/Fonts/Supplemental/Songti.ttc", +] -# 样式配置(字体更大、关键词更突出) +# 样式配置 STYLE = { 'cover': { - 'bg_blur': 30, - 'overlay_alpha': 180, + 'bg_blur': 35, + 'overlay_alpha': 200, 'duration': 2.5, }, 'hook': { - 'font_size': 76, + 'font_size': 82, # 更大更清晰 'color': (255, 255, 255), 'outline_color': (30, 30, 50), 'outline_width': 5, @@ -210,6 +215,73 @@ def parse_srt_for_clip(srt_path, start_sec, end_sec): return subtitles +def _is_mostly_chinese(text): + if not text or not isinstance(text, str): + return False + chinese = sum(1 for c in text if "\u4e00" <= c <= "\u9fff") + return chinese / max(1, len(text.strip())) > 0.3 + + +def _translate_to_chinese(text): + """Ollama 翻译英文为中文""" + if not text or _is_mostly_chinese(text): + return text + try: + import requests + r = requests.post( + "http://localhost:11434/api/generate", + json={ + "model": "qwen2.5:1.5b", + "prompt": f"将以下翻译成简体中文,只输出中文:\n{text[:150]}", + "stream": False, + "options": {"temperature": 0.1, "num_predict": 80}, + }, + timeout=15, + ) + if r.status_code == 200: + out = r.json().get("response", "").strip().split("\n")[0][:80] + if out: + return out + except Exception: + pass + return text + + +def detect_burned_subs(video_path, num_samples=2): + """检测视频是否已有烧录字幕/图片(OCR 采样底部区域)""" + try: + import pytesseract + pytesseract.get_tesseract_version() + except Exception: + return False # 无 tesseract 则假定无字幕,执行烧录 + try: + duration = get_video_info(video_path).get("duration", 0) + if duration < 1: + return False + for i in range(num_samples): + t = duration * (0.25 + 0.25 * i) + frame = tempfile.mktemp(suffix=".jpg") + subprocess.run([ + "ffmpeg", "-y", "-ss", str(t), "-i", video_path, + "-vframes", "1", "-q:v", "2", frame + ], capture_output=True) + if os.path.exists(frame): + try: + img = Image.open(frame) + w, h = img.size + crop = img.crop((0, int(h * 0.65), w, h)) # 底部 35% + text = pytesseract.image_to_string(crop, lang="chi_sim+eng") + os.remove(frame) + if text and len(text.strip()) > 15: + return True + except Exception: + if os.path.exists(frame): + os.remove(frame) + except Exception: + pass + return False + + def get_video_info(video_path): """获取视频信息""" cmd = [ @@ -239,8 +311,19 @@ def get_video_info(video_path): # ============ 封面生成 ============ +def get_cover_font(size): + """封面专用字体(更好看)""" + for path in COVER_FONT_PRIORITY + FONT_PRIORITY + [FONT_BOLD]: + if path and os.path.exists(path): + try: + return ImageFont.truetype(path, size) + except Exception: + continue + return ImageFont.load_default() + + def create_cover_image(hook_text, width, height, output_path, video_path=None): - """创建封面贴片(简体中文)""" + """创建封面贴片(简体中文,字体优化)""" hook_text = _to_simplified(str(hook_text or "").strip()) if not hook_text: hook_text = "精彩切片" @@ -281,8 +364,8 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None): alpha = 150 - i * 40 draw.rectangle([0, height - i*3 - 2, width, height - i*3], fill=(255, 215, 0, alpha)) - # Hook文字(自动换行) - font = get_font(FONT_HEAVY, hook_style['font_size']) + # Hook 文字(封面用更好看的字体) + font = get_cover_font(hook_style['font_size']) # 计算换行 max_width = width - 80 @@ -301,14 +384,13 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None): if current_line: lines.append(current_line) - # 绘制文字(整体向右偏移 6%,减少右侧空白) + # 绘制文字(完全居中) line_height = hook_style['font_size'] + 15 total_height = len(lines) * line_height start_y = (height - total_height) // 2 - x_offset = int(width * 0.06) # 向右偏移 for i, line in enumerate(lines): line_w, line_h = get_text_size(draw, line, font) - x = (width - line_w) // 2 + x_offset + x = (width - line_w) // 2 y = start_y + i * line_height draw_text_with_outline( @@ -336,9 +418,8 @@ def create_subtitle_image(text, width, height, output_path): kw_font = get_font(FONT_HEAVY, kw_size) # 关键词用粗体+大字 text_w, text_h = get_text_size(draw, text, font) - # 字幕整体向右偏移 6%,减少右侧空白 - x_offset = int(width * 0.06) - base_x = (width - text_w) // 2 + x_offset + # 字幕完全居中 + base_x = (width - text_w) // 2 base_y = height - text_h - style['margin_bottom'] # 背景条 @@ -480,8 +561,9 @@ def _parse_clip_index(filename: str) -> int: return int(m.group()) if m else 0 -def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_path): - """增强单个切片""" +def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_path, + force_burn_subs=False, skip_subs=False): + """增强单个切片。检测原片是否已有字幕,有则跳过烧录,无则烧录中文""" print(f"\n处理: {os.path.basename(clip_path)}") @@ -493,36 +575,37 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa hook_text = highlight_info.get('hook_3sec') or highlight_info.get('title') or '' if not hook_text and clip_path: - # 从文件名提取标题(soul106_01_标题.mp4) m = re.search(r'\d+[_\s]+(.+?)(?:_enhanced)?\.mp4$', os.path.basename(clip_path)) if m: hook_text = m.group(1).strip() cover_duration = STYLE['cover']['duration'] - # 1. 生成封面图片 + # 1. 生成封面 cover_img = os.path.join(temp_dir, 'cover.png') create_cover_image(hook_text, width, height, cover_img, clip_path) print(f" ✓ 封面生成") - # 2. 解析字幕 - start_time = highlight_info.get('start_time', '00:00:00') - parts = start_time.split(':') - 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) - print(f" ✓ 字幕解析 ({len(subtitles)}条)") - - # 3. 生成字幕图片 + # 2. 字幕逻辑:有字幕/图片则跳过,无则烧录中文 sub_images = [] - for i, sub in enumerate(subtitles[:50]): # 限制50条 - img_path = os.path.join(temp_dir, f'sub_{i:04d}.png') - create_subtitle_image(sub['text'], width, height, img_path) - sub_images.append({ - 'path': img_path, - 'start': sub['start'], - 'end': sub['end'] - }) + do_burn_subs = not skip_subs and (force_burn_subs or not detect_burned_subs(clip_path)) + if skip_subs: + print(f" ⊘ 跳过字幕烧录(--skip-subs)") + elif not do_burn_subs: + print(f" ⊘ 跳过字幕烧录(检测到原片已有字幕/图片)") + else: + start_time = highlight_info.get('start_time', '00:00:00') + parts = start_time.split(':') + 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) + for sub in subtitles: + if not _is_mostly_chinese(sub['text']): + sub['text'] = _translate_to_chinese(sub['text']) or sub['text'] + print(f" ✓ 字幕解析 ({len(subtitles)}条),已转中文") + for i, sub in enumerate(subtitles[:50]): + img_path = os.path.join(temp_dir, f'sub_{i:04d}.png') + create_subtitle_image(sub['text'], width, height, img_path) + sub_images.append({'path': img_path, 'start': sub['start'], 'end': sub['end']}) print(f" ✓ 字幕图片 ({len(sub_images)}张)") # 4. 检测静音 @@ -620,6 +703,8 @@ def main(): parser.add_argument("--highlights", "-l", help="highlights.json 路径") parser.add_argument("--transcript", "-t", help="transcript.srt 路径") parser.add_argument("--output", "-o", help="输出目录") + parser.add_argument("--skip-subs", action="store_true", help="跳过字幕烧录(原片已有字幕时用)") + parser.add_argument("--force-burn-subs", action="store_true", help="强制烧录字幕(忽略检测)") args = parser.parse_args() clips_dir = Path(args.clips) if args.clips else CLIPS_DIR @@ -646,9 +731,7 @@ def main(): print("="*60) output_dir.mkdir(parents=True, exist_ok=True) - # 清空已有增强切片,避免重复 - for f in output_dir.glob("*.mp4"): - f.unlink() + # 同名直接覆盖,不预先清空 with open(highlights_path, 'r', encoding='utf-8') as f: highlights = json.load(f) @@ -668,7 +751,9 @@ def main(): temp_dir = tempfile.mkdtemp(prefix='enhance_') try: - if enhance_clip(str(clip_path), str(output_path), highlight_info, temp_dir, str(transcript_path)): + if enhance_clip(str(clip_path), str(output_path), highlight_info, temp_dir, str(transcript_path), + force_burn_subs=getattr(args, 'force_burn_subs', False), + skip_subs=getattr(args, 'skip_subs', False)): success_count += 1 finally: shutil.rmtree(temp_dir, ignore_errors=True) diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index 59cb42a2..9d101230 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -88,3 +88,4 @@ | 2026-02-22 11:58:17 | 🔄 卡若AI 同步 2026-02-22 11:58 | 更新:金仓、水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | | 2026-02-22 12:42:56 | 🔄 卡若AI 同步 2026-02-22 12:42 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | | 2026-02-22 13:08:21 | 🔄 卡若AI 同步 2026-02-22 13:08 | 更新:卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 8 个 | +| 2026-02-22 13:45:50 | 🔄 卡若AI 同步 2026-02-22 13:45 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index 9f61e9c3..01050453 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -91,3 +91,4 @@ | 2026-02-22 11:58:17 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 11:58 | 更新:金仓、水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-22 12:42:56 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 12:42 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-02-22 13:08:21 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 13:08 | 更新:卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | +| 2026-02-22 13:45:50 | 成功 | 成功 | 🔄 卡若AI 同步 2026-02-22 13:45 | 更新:金仓、卡木、运营中枢工作台 | 排除 >20MB: 8 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |