🔄 卡若AI 同步 2026-02-22 13:57 | 更新:金仓、水溪整理归档、卡木、运营中枢工作台 | 排除 >20MB: 8 个
This commit is contained in:
@@ -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/` |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 冲突
|
||||
|
||||
171
01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh
Normal file
171
01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_中文目录改英文_宝塔终端执行.sh
Normal file
@@ -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 "=== 完成 ==="
|
||||
363
01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py
Normal file
363
01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_中文目录改英文迁移.py
Normal file
@@ -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())
|
||||
@@ -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()
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026-02-16
|
||||
2026-02-22
|
||||
@@ -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字内,用于文件名)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 个 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user