🔄 卡若AI 同步 2026-03-23 13:35 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 11 个
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"access_token": "u-fNvjYaC8h9SWRJjTGmOKuulh3c3xghUjWMGaJA4023lE",
|
||||
"refresh_token": "ur-eSq5MlNZ5d9GkVd9PbvCDGlh1A3xghUjrwGaUQ4027hE",
|
||||
"access_token": "u-ddWeP4OxxfuqcXBDh7p2jTlh3eb1ghorOMGaUBk023hI",
|
||||
"refresh_token": "ur-eMcHDsoKl9gEs0DER_14ailh1CxxghohpMGaIRk022lI",
|
||||
"name": "飞书用户",
|
||||
"auth_time": "2026-03-23T08:28:12.167751"
|
||||
}
|
||||
@@ -73,11 +73,13 @@ ROWS = {
|
||||
'129': [ 'AI手机金融坏账投流', 200, 0, 250, 14, 187, 4, 561, 21, 31 ],
|
||||
# 130场 2026-03-21:视频号直播结束页 02:25:49≈146min;观众总数2278、最高在线355、新增关注4、总热度3、送礼1;Soul推流无截图数据填0→脚本跳过第5行保留空
|
||||
'130': [ 'Soul爆量脸视频号问微信', 146, 0, 2278, 0, 3, 1, 3, 4, 355 ],
|
||||
# 131场 2026-03-23:结束页 02:05:55≈126min;观众总数1144、最高在线75、新增关注4;点赞1595+评论498+分享12=2105;礼物/灵魂力/人均未给填0;Soul推流无填0→跳过第5行
|
||||
'131': [ '视频号中枢Soul哨兵', 126, 0, 1144, 0, 2105, 0, 0, 4, 75 ],
|
||||
}
|
||||
# 场次→按日期列填写时的日期(表头为当月日期 1~31)
|
||||
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23', '113': '2', '114': '3', '115': '4', '116': '5', '117': '6', '118': '7', '119': '8', '124': '14', '126': '17', '127': '18', '128': '19', '129': '20', '130': '21'}
|
||||
SESSION_DATE_COLUMN = {'105': '20', '106': '21', '107': '23', '113': '2', '114': '3', '115': '4', '116': '5', '117': '6', '118': '7', '119': '8', '124': '14', '126': '17', '127': '18', '128': '19', '129': '20', '130': '21', '131': '23'}
|
||||
# 场次→月份(用于选择 2月/3月 等工作表标签,避免写入错月)
|
||||
SESSION_MONTH = {'105': 2, '106': 2, '107': 2, '113': 3, '114': 3, '115': 3, '116': 3, '117': 3, '118': 3, '119': 3, '124': 3, '126': 3, '127': 3, '128': 3, '129': 3, '130': 3}
|
||||
SESSION_MONTH = {'105': 2, '106': 2, '107': 2, '113': 3, '114': 3, '115': 3, '116': 3, '117': 3, '118': 3, '119': 3, '124': 3, '126': 3, '127': 3, '128': 3, '129': 3, '130': 3, '131': 3}
|
||||
|
||||
# 派对录屏(飞书妙记)链接:场次 → 完整 URL,填表时写入「派对录屏」行对应列
|
||||
# 从飞书妙记复制链接后填入,新场次需补全
|
||||
@@ -133,6 +135,7 @@ MINIPROGRAM_EXTRA_3 = {
|
||||
'19': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月19日 128场
|
||||
'20': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月20日 129场
|
||||
'21': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月21日 130场(从小程序后台更新)
|
||||
'23': {'访问次数': 0, '访客': 0, '交易金额': 0}, # 3月23日 131场(从小程序后台更新)
|
||||
}
|
||||
|
||||
|
||||
@@ -414,7 +417,7 @@ def main():
|
||||
session = (sys.argv[1] if len(sys.argv) > 1 else '104').strip()
|
||||
row = ROWS.get(session)
|
||||
if not row:
|
||||
print('❌ 未知场次,可用: 96, 97, 98, 99, 100, 103, 104, 105, 106, 107, 113, 114, 115, 116, 117, 118, 119, 124, 126, 127, 128, 129, 130')
|
||||
print('❌ 未知场次,可用: 96, 97, 98, 99, 100, 103, 104, 105, 106, 107, 113, 114, 115, 116, 117, 118, 119, 124, 126, 127, 128, 129, 130, 131')
|
||||
sys.exit(1)
|
||||
token = load_token() or refresh_and_load_token()
|
||||
if not token:
|
||||
@@ -471,9 +474,9 @@ def main():
|
||||
if os.environ.get('SOUL_PARTY_PUSH_GROUP', '').strip() != '1':
|
||||
print('⏭️ 已跳过飞书群推送(需 export SOUL_PARTY_PUSH_GROUP=1 后重跑本脚本才会推送运营数据到群)')
|
||||
return
|
||||
if sess not in ('105', '106', '107', '113', '114', '115', '116', '117', '118', '119', '124', '126', '127', '128', '129', '130'):
|
||||
if sess not in ('105', '106', '107', '113', '114', '115', '116', '117', '118', '119', '124', '126', '127', '128', '129', '130', '131'):
|
||||
return
|
||||
date_label = {'105': '2月20日', '106': '2月21日', '107': '2月23日', '113': '3月2日', '114': '3月3日', '115': '3月4日', '116': '3月5日', '117': '3月6日', '118': '3月7日', '119': '3月8日', '124': '3月14日', '126': '3月17日', '127': '3月18日', '128': '3月19日', '129': '3月20日', '130': '3月21日'}.get(sess, sess + '场')
|
||||
date_label = {'105': '2月20日', '106': '2月21日', '107': '2月23日', '113': '3月2日', '114': '3月3日', '115': '3月4日', '116': '3月5日', '117': '3月6日', '118': '3月7日', '119': '3月8日', '124': '3月14日', '126': '3月17日', '127': '3月18日', '128': '3月19日', '129': '3月20日', '130': '3月21日', '131': '3月23日'}.get(sess, sess + '场')
|
||||
report_link = OPERATION_REPORT_LINK if sheet_id == SHEET_ID else f'https://cunkebao.feishu.cn/wiki/wikcnIgAGSNHo0t36idHJ668Gfd?sheet={sheet_id}'
|
||||
lines = [
|
||||
'【Soul 派对运营报表】',
|
||||
@@ -484,7 +487,7 @@ def main():
|
||||
for i, label in enumerate(LABELS_GROUP):
|
||||
val = raw_vals[i] if i < len(raw_vals) else ''
|
||||
lines.append(f'{label}:{val}')
|
||||
src_date = {'105': '20260220', '106': '20260221', '107': '20260223', '113': '20260302', '114': '20260303', '115': '20260304', '116': '20260305', '117': '20260306', '118': '20260307', '119': '20260308', '124': '20260314', '126': '20260317', '127': '20260318', '128': '20260319', '129': '20260320', '130': '20260321'}.get(sess, '20260220')
|
||||
src_date = {'105': '20260220', '106': '20260221', '107': '20260223', '113': '20260302', '114': '20260303', '115': '20260304', '116': '20260305', '117': '20260306', '118': '20260307', '119': '20260308', '124': '20260314', '126': '20260317', '127': '20260318', '128': '20260319', '129': '20260320', '130': '20260321', '131': '20260323'}.get(sess, '20260220')
|
||||
lines.append(f'数据来源:soul 派对 {sess}场 {src_date}.txt')
|
||||
msg = '\n'.join(lines)
|
||||
ok, _ = send_feishu_group_message(FEISHU_GROUP_WEBHOOK, msg)
|
||||
|
||||
@@ -6,6 +6,7 @@ B站视频发布 — 纯 API 优先 + Playwright 兜底
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -22,6 +23,16 @@ UA = (
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
def _playwright_headless() -> bool:
|
||||
"""默认 True(无窗口);PUBLISH_PLAYWRIGHT_HEADLESS=0 时有头调试。"""
|
||||
return os.environ.get("PUBLISH_PLAYWRIGHT_HEADLESS", "1").strip().lower() not in (
|
||||
"0",
|
||||
"false",
|
||||
"no",
|
||||
"off",
|
||||
)
|
||||
|
||||
|
||||
TITLES = {
|
||||
"AI最大的缺点是上下文太短,这样来解决.mp4":
|
||||
"AI的短板是记忆太短,上下文一长就废了,这个方法能解决 #AI工具 #效率提升 #小程序 卡若创业派对",
|
||||
@@ -84,8 +95,16 @@ async def _api_publish(video_path: str, title: str, scheduled_time=None) -> Publ
|
||||
|
||||
if scheduled_time:
|
||||
dtime = int(scheduled_time.timestamp())
|
||||
now_ts = int(time.time())
|
||||
# B站 API:发布时间须 ≥5 分钟且 ≤15 天(21173)
|
||||
min_d = now_ts + 330
|
||||
if dtime < min_d:
|
||||
dtime = min_d
|
||||
meta["dtime"] = dtime
|
||||
print(f" [API] 定时发布: {scheduled_time.strftime('%Y-%m-%d %H:%M')}", flush=True)
|
||||
print(
|
||||
f" [API] 定时发布: {time.strftime('%Y-%m-%d %H:%M', time.localtime(dtime))}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
page = video_uploader.VideoUploaderPage(
|
||||
path=video_path,
|
||||
@@ -131,14 +150,15 @@ async def _api_publish(video_path: str, title: str, scheduled_time=None) -> Publ
|
||||
|
||||
|
||||
async def _playwright_publish(video_path: str, title: str) -> PublishResult:
|
||||
"""方案二:Playwright 可见浏览器(兜底)"""
|
||||
"""方案二:Playwright 兜底(默认 headless,无弹窗)"""
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
t0 = time.time()
|
||||
hl = _playwright_headless()
|
||||
|
||||
async with async_playwright() as pw:
|
||||
browser = await pw.chromium.launch(
|
||||
headless=False,
|
||||
headless=hl,
|
||||
args=["--disable-blink-features=AutomationControlled"],
|
||||
)
|
||||
ctx = await browser.new_context(
|
||||
@@ -281,7 +301,10 @@ async def publish_one(video_path: str, title: str, idx: int = 1, total: int = 1,
|
||||
print(f" [方案一失败] {err_msg}", flush=True)
|
||||
|
||||
# 方案二:Playwright 兜底
|
||||
print(" [方案二] 降级到 Playwright 可见浏览器...", flush=True)
|
||||
print(
|
||||
f" [方案二] 降级 Playwright({'headless 无窗口' if _playwright_headless() else '有头浏览器'})...",
|
||||
flush=True,
|
||||
)
|
||||
try:
|
||||
result = await _playwright_publish(video_path, title)
|
||||
print(f" {result.log_line()}", flush=True)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,25 +3,28 @@ name: 多平台分发
|
||||
description: >
|
||||
一键将视频分发到 5 个平台(抖音、B站、视频号、小红书、快手)。
|
||||
API 优先策略:视频号纯 API、B站 bilibili-api-python、抖音纯 API。
|
||||
支持定时排期(第1条立即发,后续 30-120 分钟随机间隔)、并行分发、去重、失败自动重试。
|
||||
支持定时排期(默认智能错峰;可选 legacy)、默认静默不自动弹窗登录视频号、并行分发、去重、失败自动重试。
|
||||
triggers: 多平台分发、一键分发、全平台发布、批量分发、视频分发
|
||||
owner: 木叶
|
||||
group: 木
|
||||
version: "4.1"
|
||||
updated: "2026-03-20"
|
||||
version: "4.5"
|
||||
updated: "2026-03-23"
|
||||
---
|
||||
|
||||
# 多平台分发 Skill(v4.1)
|
||||
# 多平台分发 Skill(v4.5)
|
||||
|
||||
> **核心原则**:API 发布为主,Playwright 为辅。确保确定性地分发到各平台。
|
||||
> **v4.1 变更**:视频号 Cookie 双路径自动对齐;指定 `--platforms 视频号` 时 Cookie 失效自动调起扫码登录;登录完成即写入并同步中央存储;执行上**先直达目的**(跑命令、保存 Cookie、再发),**不对用户反问**。
|
||||
> **v4.5 视频号铁律**:**必须先等微信扫码完成(Cookie 连续校验通过),再上传。** `video_channels_resume.py` **默认弹出 Chromium 窗口**扫码,扫完再自动上传;`--silent-login` 才是无头。
|
||||
> **v4.4**:默认无窗口、B 站 headless、定时 ≥5min、`--until-success`、二维码路径标记。
|
||||
> **v4.3**:默认不自动弹视频号登录。**v4.2**:智能排期与去重下标对齐。
|
||||
|
||||
## 〇、执行原则(第一性原理)
|
||||
|
||||
- **目标优先**:用户要「发到视频号 / 全平台」→ 直接执行 `distribute_all.py` 与必要登录脚本,再简短汇报结果。
|
||||
- **视频号两步(强制)**:① 微信扫码登录助手并落盘 Cookie;② 再跑上传。**禁止**在 Cookie 未确认有效时上传。一键:`video_channels_resume.py`(**默认弹窗** Chromium;`--silent-login` 无头;`--step1-only` 只登录)。
|
||||
- **目标优先**:全平台时若含视频号且 Cookie 无效 → 先完成第①步,再 `distribute_all`;或直接跑 `video_channels_resume.py` 仅发视频号。
|
||||
- **Cookie 优先**:任何登录成功 → **必须落盘**;视频号同时写入 `视频号发布/脚本/channels_storage_state.json` 与 `多平台分发/cookies/视频号_cookies.json`(脚本已自动同步)。
|
||||
- **少问多做**:缺 Cookie 时自动打开 `channels_login.py`(仅发视频号场景);除非环境无法弹窗,否则不先停下来问「要不要登录」。
|
||||
- **扫码只在 Cursor 里**:`channels_login.py` 用 `cursor://…/simple-browser` 打开登录页,**不**唤起 Safari/Chrome;要**完全避免**回退 Chromium,请用带 `--remote-debugging-port=9223` 的方式启动 Cursor(见脚本内说明)。
|
||||
- **默认静默**:批量分发**不弹窗**、不自动拉起登录流程,适合 Cursor 内无人值守跑命令。
|
||||
- **需要扫码时**:显式 `python3 channels_login.py`,或 `distribute_all.py --auto-channels-login`,或 `CHANNELS_AUTO_LOGIN=1 python3 channels_api_publish.py`。`channels_login.py` 可用 `cursor://…/simple-browser`;完全避免 Chromium 回退见脚本内 CDP 说明。
|
||||
|
||||
---
|
||||
|
||||
@@ -39,6 +42,9 @@ updated: "2026-03-20"
|
||||
> 按《视频号与腾讯相关 API 整理》结论,微信官方目前**没有开放「短视频上传/发布」接口**;本 Skill 中的视频号发布能力,属于对 `https://channels.weixin.qq.com` 视频号助手网页协议的逆向封装(DFS 上传 + `post_create`),仅在你本机使用,需自行承担协议变更与合规风险。
|
||||
> 官方可控能力(直播记录、橱窗、留资、罗盘数据、本地生活等)的服务端 API 入口为:`https://developers.weixin.qq.com/doc/channels/api/`。**整合脑图与接口速查**见同木叶的 `视频号发布/REFERENCE_开放能力_数据与集成.md`;开放平台凭证约定见 `视频号发布/credentials/README.md`(`.env.open_platform`)。
|
||||
|
||||
> **「视频号 API token」与成片上传**:微信公众号 **`access_token`**(`cgi-bin/token`)用于开放平台文档中的各类接口,**不能**替代本链路里的 **视频号助手网页态**。`distribute_all` → `channels_api_publish.py` 发表短视频,依赖的是 **`channels_storage_state.json`**(Cookie + `localStorage`,如 `__ml::aid`、`_finger_print_device_id`),经 `auth/auth_data` 校验通过后方可 DFS 上传与 `post_create`。`channels_token.json` 只是登录脚本写出的摘要字段,**不能单独当「发视频 token」用**。若要用 **appid+secret** 拉直播/罗盘等数据,走 `视频号发布/脚本/channels_open_fetch.py`,**与上传 127 场成片无关**。
|
||||
> **127 场全平台(静默)**(助手态与各平台 Cookie 已就绪):`python3 distribute_all.py --video-dir "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片"`
|
||||
|
||||
---
|
||||
|
||||
## 二、一键命令
|
||||
@@ -46,10 +52,13 @@ updated: "2026-03-20"
|
||||
```bash
|
||||
cd /Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本
|
||||
|
||||
# 定时排期:第1条立即,后续 30-120min 随机间隔
|
||||
# 定时排期:默认智能错峰(条数自适应 + 尽量避开本地 0–7 点)
|
||||
python3 distribute_all.py
|
||||
|
||||
# 立即全部发布
|
||||
# 旧版:固定 30–120min 随机间隔
|
||||
python3 distribute_all.py --legacy-schedule
|
||||
|
||||
# 立即全部发布(不排期)
|
||||
python3 distribute_all.py --now
|
||||
|
||||
# 只发指定平台
|
||||
@@ -62,22 +71,42 @@ python3 distribute_all.py --video-dir "/path/to/videos/"
|
||||
python3 distribute_all.py --check
|
||||
python3 distribute_all.py --retry
|
||||
|
||||
# 仅视频号:Cookie 失效时禁止自动弹窗登录(CI/无头环境)
|
||||
python3 distribute_all.py --platforms 视频号 --no-auto-channels-login --video-dir "/path/to/成片"
|
||||
# 或环境变量:NO_AUTO_CHANNELS_LOGIN=1
|
||||
# 视频号 Cookie 失效且希望脚本自动弹窗扫码(默认不弹窗)
|
||||
python3 distribute_all.py --platforms 视频号 --auto-channels-login --video-dir "/path/to/成片"
|
||||
|
||||
# 强制静默(即使写了 --auto-channels-login 也不弹窗):NO_AUTO_CHANNELS_LOGIN=1
|
||||
# 独立跑视频号脚本且允许自动登录:CHANNELS_AUTO_LOGIN=1 python3 ../视频号发布/脚本/channels_api_publish.py
|
||||
|
||||
# 视频号仅要二维码到对话:无窗口,终端会打印 SOUL_QR_IMAGE_FOR_CHAT 路径
|
||||
cd ../视频号发布/脚本 && CHANNELS_SILENT_QR=1 python3 channels_login.py
|
||||
|
||||
# 全平台直到全部成功(间隔 90s,无限轮;加 --until-success-max-rounds 20 可封顶)
|
||||
python3 distribute_all.py --video-dir "/path/to/成片" --until-success
|
||||
|
||||
# 仅查看断点:各平台已成功/待传(不执行上传);加 --resume-report-detail 列出文件名
|
||||
python3 distribute_all.py --resume-report --resume-report-detail --video-dir "/path/to/成片"
|
||||
|
||||
# 视频号一条龙:后台静默登录 → 终端打印二维码路径 → 你在对话里让助手读图扫码 → Cookie 好后自动仅发视频号(断点+until-success)
|
||||
python3 video_channels_resume.py --video-dir "/path/to/成片"
|
||||
# 无头(不弹窗):加 --silent-login
|
||||
# 只做扫码不上传:加 --step1-only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、定时排期(v4.0 优化)
|
||||
## 三、定时排期(v4.2 默认智能错峰)
|
||||
|
||||
### 3.1 排期规则
|
||||
- **第 1 条**:立即发布(`first_delay=0`)
|
||||
- **第 2 条起**:前一条 + random(30, 120) 分钟
|
||||
- 若总跨度 > 24h,自动按比例压缩
|
||||
- 12 条视频典型跨度 ~10-14h
|
||||
### 3.1 默认规则(`schedule_generator.generate_smart_schedule`)
|
||||
- **第 1 条**:立即(`first_delay=0`);视频号侧 2 分钟内仍视为立即(`_scheduled_ts_for_channels`)
|
||||
- **间隔与总跨度**:随条数 `n` 自适应(`suggest_stagger_params`):条数越多略缩短单条间隔、允许更长总跨度(如 8 条约 28–48h 量级,具体随机)
|
||||
- **凌晨规避**:本地时间 0–7 点附近的点会挪到当日/次日 12:xx(`refine_avoid_late_night`);关闭:`SCHEDULE_NO_NIGHT_REFINE=1`
|
||||
- **回退旧逻辑**:`python3 distribute_all.py --legacy-schedule`(仍可用 `--min-gap` / `--max-gap` / `--max-hours`)
|
||||
- **去重对齐**:排期与 `videos` 列表下标一致;中间已发布的文件跳过,其余仍按原文件名顺序对应原定时间
|
||||
|
||||
### 3.2 各平台定时实现
|
||||
### 3.2 独立跑 `channels_api_publish.py` 时
|
||||
- 与上相同的 `generate_smart_schedule` → Unix 定时,与分发器一致
|
||||
|
||||
### 3.3 各平台定时实现
|
||||
|
||||
| 平台 | 定时方式 | 参数 |
|
||||
|------|----------|------|
|
||||
@@ -134,8 +163,9 @@ meta.hashtags("视频号") # … + #小程序卡若创业派对 #公众号卡
|
||||
- 中央存储:`多平台分发/cookies/{平台}_cookies.json`
|
||||
- **视频号双路径**:`sync_channels_cookie_files()`;登录脚本写 legacy 后 **copy** 到 `cookies/视频号_cookies.json`
|
||||
- **登录页只在 Cursor 内打开**:`channels_login.py` v7 用 `cursor://vscode.simple-browser/show?url=…` 唤起 **Simple Browser**,**不**用系统默认浏览器。
|
||||
- **不落盘会话**:在 Cursor 已开 `--remote-debugging-port`(默认脚本连 `CHANNELS_CDP_URL=http://127.0.0.1:9223`)时,Playwright **CDP 附着** Cursor,从 Simple Browser 上下文导出 `storage_state`;**无 CDP** 时才回退独立 Chromium(`--playwright-only` 可强制只走 Chromium)。
|
||||
- `distribute_all.py` 指定 `--platforms 视频号` 且 Cookie 失效时自动跑 `channels_login.py`(可用 `--no-auto-channels-login` 关闭)
|
||||
- **不落盘会话**:在 Cursor 已开 `--remote-debugging-port`(默认脚本连 `CHANNELS_CDP_URL=http://127.0.0.1:9223`)时,Playwright **CDP 附着** Cursor,从 Simple Browser 上下文导出 `storage_state`;**无 CDP** 时回退 Chromium;默认 **持久化用户目录** `~/.soul-channels-playwright-profile`(`CHANNELS_PERSISTENT_LOGIN=0` / `--no-persistent` 可关),减少重复扫码。
|
||||
- **无 Cookie 发片**:微信助手 API **无**官方「access_token 上传短视频」接口,见 `视频号发布/脚本/channels_open_platform_publish.py` 与 `REFERENCE_开放能力_数据与集成.md`。
|
||||
- **`distribute_all.py`**:默认**不**自动跑 `channels_login.py`;仅 `--auto-channels-login` 且 Cookie 无效时才调起。历史参数 `--no-auto-channels-login` 仍接受(无效果,与默认一致)。
|
||||
- API 预检:各平台 auth API
|
||||
|
||||
---
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_127_resume.pid
Normal file
1
03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_127_resume.pid
Normal file
@@ -0,0 +1 @@
|
||||
37720
|
||||
1
03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_127场_全平台_run.pid
Normal file
1
03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_127场_全平台_run.pid
Normal file
@@ -0,0 +1 @@
|
||||
69111
|
||||
@@ -1,24 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
多平台一键分发 v3 — 全链路自动化 + 定时排期
|
||||
- 定时排期:30-120 分钟随机间隔,超 24h 自动压缩
|
||||
- 定时排期(默认):generate_smart_schedule — 按条数自适应间隔/总跨度,并尽量避开本地 0–7 点
|
||||
- 旧排期:--legacy-schedule + --min-gap / --max-gap / --max-hours(原 30–120min 随机)
|
||||
- 并行分发:5 平台同时上传(asyncio.gather)
|
||||
- 去重机制:已成功发布的视频自动跳过
|
||||
- 失败重试:--retry 自动重跑历史失败任务
|
||||
- Cookie 预警:过期/即将过期自动通知
|
||||
- 智能标题:优先手动字典,否则文件名自动生成
|
||||
- 结果持久化:JSON Lines 日志 + 控制台汇总
|
||||
- 去重:每条视频按其在目录中的序号对齐排期(不因前面跳过而错位)
|
||||
- 失败重试:--retry;Cookie 预警;结果写入 publish_log.json
|
||||
- 视频号登录:默认静默(仅同步 Cookie 路径,不弹窗);需要自动扫码时加 --auto-channels-login;NO_AUTO_CHANNELS_LOGIN=1 强制静默
|
||||
|
||||
用法:
|
||||
python3 distribute_all.py # 定时排期并行分发
|
||||
python3 distribute_all.py # 智能错峰定时排期
|
||||
python3 distribute_all.py --now # 立即发布(不排期)
|
||||
python3 distribute_all.py --legacy-schedule # 固定随机间隔(旧逻辑)
|
||||
python3 distribute_all.py --platforms B站 快手 # 只发指定平台
|
||||
python3 distribute_all.py --check # 检查 Cookie
|
||||
python3 distribute_all.py --retry # 重试失败任务
|
||||
python3 distribute_all.py --video /path/to.mp4 # 发单条视频
|
||||
python3 distribute_all.py --no-dedup # 跳过去重检查
|
||||
python3 distribute_all.py --serial # 串行模式(调试用)
|
||||
python3 distribute_all.py --min-gap 30 --max-gap 120 # 自定义间隔
|
||||
python3 distribute_all.py --min-gap 30 --max-gap 120 # 仅与 --legacy-schedule 联用
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
@@ -44,18 +44,23 @@ from cookie_manager import (
|
||||
from publish_result import (PublishResult, print_summary, save_results,
|
||||
load_published_set, load_failed_tasks)
|
||||
from title_generator import generate_title
|
||||
from schedule_generator import generate_schedule, format_schedule
|
||||
from schedule_generator import (
|
||||
generate_schedule,
|
||||
generate_smart_schedule,
|
||||
format_schedule,
|
||||
)
|
||||
from video_metadata import VideoMeta
|
||||
|
||||
CHANNELS_LOGIN_SCRIPT = BASE_DIR / "视频号发布" / "脚本" / "channels_login.py"
|
||||
|
||||
|
||||
def _ensure_channels_cookie_or_login(skip_auto: bool) -> None:
|
||||
"""指定发视频号时:先对齐双路径 Cookie;无效则直接调起扫码登录(保存后继续)。"""
|
||||
if skip_auto or os.environ.get("NO_AUTO_CHANNELS_LOGIN"):
|
||||
sync_channels_cookie_files()
|
||||
return
|
||||
def _ensure_channels_cookie_or_login(*, auto_login: bool) -> None:
|
||||
"""发视频号前对齐双路径 Cookie。默认静默;仅 auto_login 且未设 NO_AUTO_CHANNELS_LOGIN 时才调起扫码。"""
|
||||
sync_channels_cookie_files()
|
||||
if os.environ.get("NO_AUTO_CHANNELS_LOGIN", "").strip().lower() in ("1", "true", "yes"):
|
||||
return
|
||||
if not auto_login:
|
||||
return
|
||||
ok, _ = check_cookie_valid("视频号")
|
||||
if ok:
|
||||
return
|
||||
@@ -139,6 +144,34 @@ def check_cookies_with_alert() -> tuple[list[str], list[str]]:
|
||||
return available, alerts
|
||||
|
||||
|
||||
def print_resume_report(
|
||||
targets: list[str],
|
||||
videos: list[Path],
|
||||
published_set: set,
|
||||
*,
|
||||
detail: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
断点续传说明:publish_log.json 里 success=true 的 (平台, 文件名) 会跳过,其余重传。
|
||||
"""
|
||||
print(f"\n{'─' * 60}")
|
||||
print(" 断点续传 / 待传清单(已成功条目自动跳过)")
|
||||
print(f"{'─' * 60}")
|
||||
total_pending = 0
|
||||
for p in targets:
|
||||
pending = [v for v in videos if (p, v.name) not in published_set]
|
||||
done = len(videos) - len(pending)
|
||||
total_pending += len(pending)
|
||||
print(f" [{p}] 已成功 {done}/{len(videos)} | 待上传 {len(pending)}")
|
||||
if detail and pending:
|
||||
for v in pending[:50]:
|
||||
print(f" · {v.name}")
|
||||
if len(pending) > 50:
|
||||
print(f" … 另有 {len(pending) - 50} 条")
|
||||
print(f" 合计待传任务: {total_pending} 条(多平台分别计数)")
|
||||
print(f"{'─' * 60}\n")
|
||||
|
||||
|
||||
def send_feishu_alert(alerts: list[str]):
|
||||
"""通过飞书 Webhook 发送 Cookie 过期预警"""
|
||||
import os
|
||||
@@ -217,19 +250,19 @@ async def distribute_to_platform(
|
||||
success=True, status="skipped", message="去重跳过(已发布)",
|
||||
))
|
||||
|
||||
publish_schedule = None
|
||||
if schedule_times and len(to_publish) > 0:
|
||||
if len(schedule_times) >= len(to_publish):
|
||||
publish_schedule = schedule_times[:len(to_publish)]
|
||||
else:
|
||||
publish_schedule = generate_schedule(len(to_publish))
|
||||
idx_by_vp = {vp: j for j, vp in enumerate(videos)}
|
||||
schedule_ok = bool(schedule_times) and len(schedule_times) == len(videos)
|
||||
|
||||
total = len(to_publish)
|
||||
pub_fn = getattr(module, "publish_one_compat", None) or module.publish_one
|
||||
for i, vp in enumerate(to_publish):
|
||||
vmeta = VideoMeta.from_filename(str(vp))
|
||||
title = vmeta.title(platform)
|
||||
stime = publish_schedule[i] if publish_schedule else None
|
||||
stime = (
|
||||
schedule_times[idx_by_vp[vp]]
|
||||
if schedule_ok
|
||||
else None
|
||||
)
|
||||
try:
|
||||
r = await pub_fn(str(vp), title, i + 1, total, scheduled_time=stime)
|
||||
if isinstance(r, PublishResult):
|
||||
@@ -348,29 +381,98 @@ async def main():
|
||||
parser.add_argument("--no-dedup", action="store_true", help="跳过去重")
|
||||
parser.add_argument("--serial", action="store_true", help="串行模式")
|
||||
parser.add_argument("--now", action="store_true", help="立即发布(不排期)")
|
||||
parser.add_argument("--min-gap", type=int, default=30, help="最小间隔(分钟)")
|
||||
parser.add_argument("--max-gap", type=int, default=120, help="最大间隔(分钟)")
|
||||
parser.add_argument("--max-hours", type=float, default=24.0, help="最大排期跨度(小时)")
|
||||
parser.add_argument("--min-gap", type=int, default=30, help="最小间隔(分钟),仅 --legacy-schedule 生效")
|
||||
parser.add_argument("--max-gap", type=int, default=120, help="最大间隔(分钟),仅 --legacy-schedule 生效")
|
||||
parser.add_argument("--max-hours", type=float, default=24.0, help="最大排期跨度(小时),仅 --legacy-schedule 生效")
|
||||
parser.add_argument(
|
||||
"--legacy-schedule",
|
||||
action="store_true",
|
||||
help="使用固定随机间隔(--min-gap/--max-gap/--max-hours);默认智能错峰排期",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--auto-channels-login",
|
||||
action="store_true",
|
||||
help="视频号 Cookie 失效时自动调起扫码登录(默认静默,不弹窗)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-auto-channels-login",
|
||||
action="store_true",
|
||||
help="禁用「仅发视频号时」Cookie 失效自动弹窗登录",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-ui-browser",
|
||||
action="store_true",
|
||||
help="B站 Playwright 兜底使用有头浏览器(默认无窗口 headless)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--until-success",
|
||||
action="store_true",
|
||||
help="失败时每轮间隔后整表重试,直到全部成功或达 --until-success-max-rounds",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--until-success-sleep",
|
||||
type=int,
|
||||
default=90,
|
||||
help="--until-success 每轮间隔秒数(默认 90)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--until-success-max-rounds",
|
||||
type=int,
|
||||
default=0,
|
||||
help="--until-success 最大轮数,0 表示不限制",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resume-report",
|
||||
action="store_true",
|
||||
help="仅打印各平台已成功/待传条数与清单,不执行上传",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resume-report-detail",
|
||||
action="store_true",
|
||||
help="与 --resume-report 合用,列出待传文件名",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if (
|
||||
if not args.allow_ui_browser:
|
||||
os.environ.setdefault("PUBLISH_PLAYWRIGHT_HEADLESS", "1")
|
||||
|
||||
will_touch_channels = (
|
||||
not args.check
|
||||
and not args.retry
|
||||
and args.platforms
|
||||
and "视频号" in args.platforms
|
||||
):
|
||||
_ensure_channels_cookie_or_login(args.no_auto_channels_login)
|
||||
|
||||
available, alerts = check_cookies_with_alert()
|
||||
if alerts:
|
||||
send_feishu_alert(alerts)
|
||||
and (not args.platforms or "视频号" in args.platforms)
|
||||
)
|
||||
if will_touch_channels:
|
||||
_ensure_channels_cookie_or_login(
|
||||
auto_login=bool(args.auto_channels_login) and not args.no_auto_channels_login,
|
||||
)
|
||||
|
||||
if args.check:
|
||||
available, alerts = check_cookies_with_alert()
|
||||
if alerts:
|
||||
send_feishu_alert(alerts)
|
||||
return 0
|
||||
|
||||
if args.resume_report:
|
||||
available, alerts = check_cookies_with_alert()
|
||||
if alerts:
|
||||
send_feishu_alert(alerts)
|
||||
targets = args.platforms if args.platforms else available
|
||||
targets = [t for t in targets if t in available]
|
||||
video_dir = Path(args.video_dir) if args.video_dir else DEFAULT_VIDEO_DIR
|
||||
if args.video:
|
||||
videos = [Path(args.video)]
|
||||
else:
|
||||
videos = sorted(video_dir.glob("*.mp4"))
|
||||
if not videos:
|
||||
print(f"\n[✗] 未找到视频: {video_dir}")
|
||||
return 1
|
||||
published_set = set() if args.no_dedup else load_published_set()
|
||||
if not targets:
|
||||
print("\n[✗] 无可用平台,无法生成续传报告")
|
||||
return 1
|
||||
print_resume_report(
|
||||
targets, videos, published_set, detail=args.resume_report_detail
|
||||
)
|
||||
return 0
|
||||
|
||||
if args.retry:
|
||||
@@ -380,18 +482,59 @@ async def main():
|
||||
save_results(results)
|
||||
return 0
|
||||
|
||||
round_num = 0
|
||||
while True:
|
||||
round_num += 1
|
||||
if args.until_success and round_num > 1:
|
||||
print(
|
||||
f"\n{'#' * 20} until-success 第 {round_num} 轮 "
|
||||
f"({args.until_success_sleep}s 后开始){'#' * 20}\n",
|
||||
flush=True,
|
||||
)
|
||||
await asyncio.sleep(args.until_success_sleep)
|
||||
|
||||
exit_code, failed_count = await _publish_one_round(args)
|
||||
|
||||
if not args.until_success:
|
||||
return exit_code
|
||||
# 无可发平台 / 无视频等致命错误,勿无限重试
|
||||
if failed_count >= 9990:
|
||||
return exit_code
|
||||
if failed_count == 0:
|
||||
print("\n[✓] until-success:本轮无失败条目,结束。", flush=True)
|
||||
return 0
|
||||
if args.until_success_max_rounds and round_num >= args.until_success_max_rounds:
|
||||
print(
|
||||
f"\n[✗] until-success:已达最大轮数 {args.until_success_max_rounds},"
|
||||
f"仍有约 {failed_count} 条失败。",
|
||||
flush=True,
|
||||
)
|
||||
return 1
|
||||
print(
|
||||
f"\n[i] until-success:仍有失败,约 {failed_count} 条;"
|
||||
f"{args.until_success_sleep}s 后重试(已成功写入日志会去重跳过)…",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
async def _publish_one_round(args: argparse.Namespace) -> tuple[int, int]:
|
||||
"""执行一轮分发。返回 (exit_code, 非跳过且失败的条数)。"""
|
||||
available, alerts = check_cookies_with_alert()
|
||||
if alerts:
|
||||
send_feishu_alert(alerts)
|
||||
|
||||
if not available:
|
||||
print("\n[✗] 没有可用平台,请先登录:")
|
||||
for p, c in PLATFORM_CONFIG.items():
|
||||
login = str(c["script"]).replace("publish", "login").replace("pure_api", "login")
|
||||
print(f" {p}: python3 {login}")
|
||||
return 1
|
||||
return 1, 9999
|
||||
|
||||
targets = args.platforms if args.platforms else available
|
||||
targets = [t for t in targets if t in available]
|
||||
if not targets:
|
||||
print("\n[✗] 指定的平台均不可用")
|
||||
return 1
|
||||
return 1, 9999
|
||||
|
||||
video_dir = Path(args.video_dir) if args.video_dir else DEFAULT_VIDEO_DIR
|
||||
if args.video:
|
||||
@@ -400,10 +543,13 @@ async def main():
|
||||
videos = sorted(video_dir.glob("*.mp4"))
|
||||
if not videos:
|
||||
print(f"\n[✗] 未找到视频: {video_dir}")
|
||||
return 1
|
||||
return 1, 9999
|
||||
|
||||
published_set = set() if args.no_dedup else load_published_set()
|
||||
|
||||
if not args.no_dedup:
|
||||
print_resume_report(targets, videos, published_set, detail=False)
|
||||
|
||||
mode = "串行" if args.serial else "并行"
|
||||
total_new = 0
|
||||
for p in targets:
|
||||
@@ -411,15 +557,17 @@ async def main():
|
||||
if (p, v.name) not in published_set:
|
||||
total_new += 1
|
||||
|
||||
# 生成排期
|
||||
schedule_times = None
|
||||
if not args.now and total_new > 1:
|
||||
schedule_times = generate_schedule(
|
||||
len(videos),
|
||||
min_gap=args.min_gap,
|
||||
max_gap=args.max_gap,
|
||||
max_hours=args.max_hours,
|
||||
)
|
||||
if args.legacy_schedule:
|
||||
schedule_times = generate_schedule(
|
||||
len(videos),
|
||||
min_gap=args.min_gap,
|
||||
max_gap=args.max_gap,
|
||||
max_hours=args.max_hours,
|
||||
)
|
||||
else:
|
||||
schedule_times = generate_smart_schedule(len(videos))
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" 分发计划 ({mode})")
|
||||
@@ -427,7 +575,10 @@ async def main():
|
||||
print(f" 视频数: {len(videos)}")
|
||||
print(f" 目标平台: {', '.join(targets)}")
|
||||
print(f" 新任务: {total_new} 条")
|
||||
print(f" 发布方式: {'立即发布' if args.now or not schedule_times else '定时排期'}")
|
||||
sched_label = "立即发布"
|
||||
if schedule_times:
|
||||
sched_label = "定时排期(智能错峰)" if not args.legacy_schedule else "定时排期(legacy 随机间隔)"
|
||||
print(f" 发布方式: {sched_label if not args.now else '立即发布'}")
|
||||
if not args.no_dedup:
|
||||
skipped = len(videos) * len(targets) - total_new
|
||||
if skipped > 0:
|
||||
@@ -440,7 +591,7 @@ async def main():
|
||||
|
||||
if total_new == 0:
|
||||
print("[i] 所有视频已发布到所有平台,无新任务")
|
||||
return 0
|
||||
return 0, 0
|
||||
|
||||
t0 = time.time()
|
||||
if args.serial:
|
||||
@@ -461,7 +612,8 @@ async def main():
|
||||
if failed_count > 0:
|
||||
print(f"\n 有 {failed_count} 条失败,可执行: python3 distribute_all.py --retry")
|
||||
|
||||
return 0 if ok == total else 1
|
||||
failed_non_success = sum(1 for r in actual_results if not r.success)
|
||||
return (0 if ok == total else 1), failed_non_success
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -285,3 +285,115 @@
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": false, "status": "error", "message": "post_create errCode=300002 request failed", "elapsed_sec": 21.92174482345581, "timestamp": "2026-03-23 07:10:26"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": false, "status": "error", "message": "post_create errCode=300002 request failed", "elapsed_sec": 21.752349138259888, "timestamp": "2026-03-23 07:10:50"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": false, "status": "error", "message": "post_create errCode=300002 request failed", "elapsed_sec": 48.84101176261902, "timestamp": "2026-03-23 07:11:42"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 1.4709701538085938, "timestamp": "2026-03-23 11:09:12"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.24030470848083496, "timestamp": "2026-03-23 11:09:15"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13891935348510742, "timestamp": "2026-03-23 11:09:18"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15856194496154785, "timestamp": "2026-03-23 11:09:21"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.2452690601348877, "timestamp": "2026-03-23 11:09:25"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15202879905700684, "timestamp": "2026-03-23 11:09:28"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题很好起量", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.24415278434753418, "timestamp": "2026-03-23 11:09:31"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.16850519180297852, "timestamp": "2026-03-23 11:09:34"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.12019515037536621, "timestamp": "2026-03-23 11:09:37"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13576292991638184, "timestamp": "2026-03-23 11:09:41"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.16388773918151855, "timestamp": "2026-03-23 11:09:44"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1238703727722168, "timestamp": "2026-03-23 11:09:47"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.158128023147583, "timestamp": "2026-03-23 11:09:50"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.14139485359191895, "timestamp": "2026-03-23 11:09:53"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "很大产值不是写代码", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1378459930419922, "timestamp": "2026-03-23 11:09:56"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.18117499351501465, "timestamp": "2026-03-23 11:09:59"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.12108397483825684, "timestamp": "2026-03-23 11:10:03"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.2141871452331543, "timestamp": "2026-03-23 11:10:06"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1297130584716797, "timestamp": "2026-03-23 11:10:09"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.11802816390991211, "timestamp": "2026-03-23 11:10:12"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13814401626586914, "timestamp": "2026-03-23 11:10:15"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": false, "status": "failed", "message": "Playwright: 投稿超时", "screenshot": "/tmp/bilibili_result.png", "elapsed_sec": 108.12666606903076, "timestamp": "2026-03-23 11:11:04"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历", "success": true, "status": "reviewing", "message": "纯API投稿成功 (3.8s)", "elapsed_sec": 3.753028154373169, "timestamp": "2026-03-23 11:11:10"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个", "success": true, "status": "reviewing", "message": "纯API投稿成功 (3.3s)", "elapsed_sec": 3.284956932067871, "timestamp": "2026-03-23 11:11:17"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.9s)", "elapsed_sec": 2.9468610286712646, "timestamp": "2026-03-23 11:11:23"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.3s)", "elapsed_sec": 2.2636637687683105, "timestamp": "2026-03-23 11:11:28"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来", "success": true, "status": "reviewing", "message": "纯API投稿成功 (1.9s)", "elapsed_sec": 1.9184420108795166, "timestamp": "2026-03-23 11:11:33"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题很好起量", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.4s)", "elapsed_sec": 2.3539421558380127, "timestamp": "2026-03-23 11:11:38"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.7s)", "elapsed_sec": 2.7068850994110107, "timestamp": "2026-03-23 11:11:44"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块", "success": true, "status": "reviewing", "message": "纯API投稿成功 (5.7s)", "elapsed_sec": 5.657810211181641, "timestamp": "2026-03-23 11:11:53"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": false, "status": "error", "message": "双方案均失败: Locator.click: Timeout 30000ms exceeded.\nCall log:\n - waiting for locator(\"inpu", "elapsed_sec": 0.0, "timestamp": "2026-03-23 11:16:35"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": false, "status": "error", "message": "双方案均失败: Locator.click: Timeout 30000ms exceeded.\nCall log:\n - waiting for locator(\"inpu", "elapsed_sec": 0.0, "timestamp": "2026-03-23 11:21:18"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭", "success": true, "status": "reviewing", "message": "纯API投稿成功 (4.4s)", "elapsed_sec": 4.356952905654907, "timestamp": "2026-03-23 11:21:25"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.9s)", "elapsed_sec": 2.901702880859375, "timestamp": "2026-03-23 11:21:31"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨", "success": true, "status": "reviewing", "message": "纯API投稿成功 (5.3s)", "elapsed_sec": 5.3057701587677, "timestamp": "2026-03-23 11:21:39"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "很大产值不是写代码", "success": true, "status": "reviewing", "message": "纯API投稿成功 (3.3s)", "elapsed_sec": 3.320775270462036, "timestamp": "2026-03-23 11:21:46"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量", "success": true, "status": "reviewing", "message": "纯API投稿成功 (8.7s)", "elapsed_sec": 8.691925048828125, "timestamp": "2026-03-23 11:21:57"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.6s)", "elapsed_sec": 2.6495907306671143, "timestamp": "2026-03-23 11:22:03"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.1s)", "elapsed_sec": 2.1320738792419434, "timestamp": "2026-03-23 11:22:08"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.8s)", "elapsed_sec": 2.7808477878570557, "timestamp": "2026-03-23 11:22:14"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": false, "status": "error", "message": "双方案均失败: Locator.click: Timeout 30000ms exceeded.\nCall log:\n - waiting for locator(\"inpu", "elapsed_sec": 0.0, "timestamp": "2026-03-23 11:26:56"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": false, "status": "error", "message": "双方案均失败: Locator.click: Timeout 30000ms exceeded.\nCall log:\n - waiting for locator(\"inpu", "elapsed_sec": 0.0, "timestamp": "2026-03-23 11:31:39"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 50.02102184295654, "timestamp": "2026-03-23 11:10:25"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 49.1489679813385, "timestamp": "2026-03-23 11:11:41"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 49.1381778717041, "timestamp": "2026-03-23 11:12:56"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 49.14575791358948, "timestamp": "2026-03-23 11:14:12"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.655869007110596, "timestamp": "2026-03-23 11:15:28"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.75113916397095, "timestamp": "2026-03-23 11:16:43"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题很好起量", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.504446029663086, "timestamp": "2026-03-23 11:17:58"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.730623960494995, "timestamp": "2026-03-23 11:19:13"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.763344287872314, "timestamp": "2026-03-23 11:20:29"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.68367004394531, "timestamp": "2026-03-23 11:21:44"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.9231276512146, "timestamp": "2026-03-23 11:22:59"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.8387770652771, "timestamp": "2026-03-23 11:24:15"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.93092584609985, "timestamp": "2026-03-23 11:25:30"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.95681095123291, "timestamp": "2026-03-23 11:26:46"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "很大产值不是写代码", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.8231360912323, "timestamp": "2026-03-23 11:28:01"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.829030990600586, "timestamp": "2026-03-23 11:29:16"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.778688192367554, "timestamp": "2026-03-23 11:30:32"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.829610109329224, "timestamp": "2026-03-23 11:31:47"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.864104986190796, "timestamp": "2026-03-23 11:33:02"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.94530510902405, "timestamp": "2026-03-23 11:34:18"}
|
||||
{"platform": "小红书", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": true, "status": "likely_published", "message": "发布按钮+确认已点击,视频可能仍在处理", "screenshot": "/tmp/xhs_result.png", "elapsed_sec": 48.90583395957947, "timestamp": "2026-03-23 11:35:34"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 1.4672038555145264, "timestamp": "2026-03-23 11:43:08"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.14421701431274414, "timestamp": "2026-03-23 11:43:11"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1285099983215332, "timestamp": "2026-03-23 11:43:14"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.12726902961730957, "timestamp": "2026-03-23 11:43:18"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.7475981712341309, "timestamp": "2026-03-23 11:43:21"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13030028343200684, "timestamp": "2026-03-23 11:43:24"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题很好起量", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.13919687271118164, "timestamp": "2026-03-23 11:43:28"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1264021396636963, "timestamp": "2026-03-23 11:43:31"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15427303314208984, "timestamp": "2026-03-23 11:43:34"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.3109300136566162, "timestamp": "2026-03-23 11:43:37"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.19164180755615234, "timestamp": "2026-03-23 11:43:40"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.14249396324157715, "timestamp": "2026-03-23 11:43:43"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.16753792762756348, "timestamp": "2026-03-23 11:43:47"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.148911714553833, "timestamp": "2026-03-23 11:43:50"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "很大产值不是写代码", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.24453401565551758, "timestamp": "2026-03-23 11:43:53"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.2290198802947998, "timestamp": "2026-03-23 11:43:56"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15469002723693848, "timestamp": "2026-03-23 11:43:59"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.18268704414367676, "timestamp": "2026-03-23 11:44:03"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.1401972770690918, "timestamp": "2026-03-23 11:44:06"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.15677309036254883, "timestamp": "2026-03-23 11:44:09"}
|
||||
{"platform": "抖音", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": false, "status": "error", "message": "Cookie 已过期", "elapsed_sec": 0.18844985961914062, "timestamp": "2026-03-23 11:44:12"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆", "success": true, "status": "reviewing", "message": "纯API投稿成功 (4.2s)", "elapsed_sec": 4.178177118301392, "timestamp": "2026-03-23 11:43:12"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.2s)", "elapsed_sec": 2.2191109657287598, "timestamp": "2026-03-23 11:43:17"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.2s)", "elapsed_sec": 2.229668140411377, "timestamp": "2026-03-23 11:43:22"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.4s)", "elapsed_sec": 2.3824262619018555, "timestamp": "2026-03-23 11:43:27"}
|
||||
{"platform": "B站", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事", "success": true, "status": "reviewing", "message": "纯API投稿成功 (2.8s)", "elapsed_sec": 2.760422945022583, "timestamp": "2026-03-23 11:43:33"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆 #Soul派对 #创业日记 #小程序 卡若创业派对", "success": false, "status": "error", "message": "异常: Locator.set_input_files: Timeout 10000ms exceeded.\nCall log:\n - waiting for loc", "elapsed_sec": 23.14011812210083, "timestamp": "2026-03-23 13:01:37"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历 #Soul派对 #创业日记 #小程序 卡若创业派对", "success": false, "status": "error", "message": "异常: Page.goto: Timeout 30000ms exceeded.\nCall log:\n - navigating to \"https://channe", "elapsed_sec": 30.034138917922974, "timestamp": "2026-03-23 13:02:14"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/Soul上像开老茶馆.mp4", "title": "Soul上像开老茶馆 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/Soul上像开老茶馆_5_verify.png", "elapsed_sec": 30.81173086166382, "timestamp": "2026-03-23 13:02:52"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百七十万罚单亲历.mp4", "title": "三百七十万罚单亲历 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/三百七十万罚单亲历_5_verify.png", "elapsed_sec": 30.594141244888306, "timestamp": "2026-03-23 13:03:31"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三百简历只要两三个.mp4", "title": "三百简历只要两三个 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/三百简历只要两三个_5_verify.png", "elapsed_sec": 31.392528772354126, "timestamp": "2026-03-23 13:04:10"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/三角洲模型怎么卖.mp4", "title": "三角洲模型怎么卖 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/三角洲模型怎么卖_5_verify.png", "elapsed_sec": 31.27242684364319, "timestamp": "2026-03-23 13:04:50"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/上麦讲你上月做啥.mp4", "title": "上麦讲你上月做啥 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/上麦讲你上月做啥_5_verify.png", "elapsed_sec": 31.388854026794434, "timestamp": "2026-03-23 13:05:29"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/二百七十万推流从哪来.mp4", "title": "二百七十万推流从哪来 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/二百七十万推流从哪来_5_verify.png", "elapsed_sec": 32.773988008499146, "timestamp": "2026-03-23 13:06:10"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/什么话题最好起量.mp4", "title": "什么话题最好起量 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/什么话题最好起量_5_verify.png", "elapsed_sec": 32.07719111442566, "timestamp": "2026-03-23 13:06:50"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/保镖业务先讲清模式.mp4", "title": "保镖业务先讲清模式 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/保镖业务先讲清模式_5_verify.png", "elapsed_sec": 31.477304935455322, "timestamp": "2026-03-23 13:07:30"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/分他靠自己赚不到的那块.mp4", "title": "分他靠自己赚不到的那块 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/分他靠自己赚不到的那块_5_verify.png", "elapsed_sec": 32.22468280792236, "timestamp": "2026-03-23 13:08:10"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/场景里拍视频就链接.mp4", "title": "场景里拍视频就链接 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/场景里拍视频就链接_5_verify.png", "elapsed_sec": 31.937192916870117, "timestamp": "2026-03-23 13:08:50"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/学历不如可验证实操.mp4", "title": "学历不如可验证实操 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/学历不如可验证实操_5_verify.png", "elapsed_sec": 29.13404893875122, "timestamp": "2026-03-23 13:09:27"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/性格不对团队白搭.mp4", "title": "性格不对团队白搭 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/性格不对团队白搭_5_verify.png", "elapsed_sec": 30.99875521659851, "timestamp": "2026-03-23 13:10:06"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/想两万月薪先看月烧多少.mp4", "title": "想两万月薪先看月烧多少 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/想两万月薪先看月烧多少_5_verify.png", "elapsed_sec": 30.932358026504517, "timestamp": "2026-03-23 13:10:45"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/政商金融矿产华侨.mp4", "title": "政商金融矿产华侨 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/政商金融矿产华侨_5_verify.png", "elapsed_sec": 30.35584306716919, "timestamp": "2026-03-23 13:11:24"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/最大产值不是写代码.mp4", "title": "最大产值不是写代码 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/最大产值不是写代码_5_verify.png", "elapsed_sec": 30.229849815368652, "timestamp": "2026-03-23 13:12:02"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/现在缺的是流量.mp4", "title": "现在缺的是流量 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/现在缺的是流量_5_verify.png", "elapsed_sec": 30.722687244415283, "timestamp": "2026-03-23 13:12:41"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/筛对了人能跟十二年.mp4", "title": "筛对了人能跟十二年 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/筛对了人能跟十二年_5_verify.png", "elapsed_sec": 30.178684949874878, "timestamp": "2026-03-23 13:13:19"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/线上更适合阿米巴.mp4", "title": "线上更适合阿米巴 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/线上更适合阿米巴_5_verify.png", "elapsed_sec": 30.138293981552124, "timestamp": "2026-03-23 13:13:57"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/群主和交付师不一样.mp4", "title": "群主和交付师不一样 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/群主和交付师不一样_5_verify.png", "elapsed_sec": 30.115286827087402, "timestamp": "2026-03-23 13:14:36"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/职场课为什么卖不动.mp4", "title": "职场课为什么卖不动 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/职场课为什么卖不动_5_verify.png", "elapsed_sec": 30.429933071136475, "timestamp": "2026-03-23 13:15:14"}
|
||||
{"platform": "视频号", "video_path": "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片/链接要落到具体事.mp4", "title": "链接要落到具体事 #Soul派对 #创业日记", "success": true, "status": "reviewing", "message": "API确认,列表未匹配 (未在列表前20条中找到)", "screenshot": "/tmp/channels_ss/链接要落到具体事_5_verify.png", "elapsed_sec": 30.445733070373535, "timestamp": "2026-03-23 13:15:53"}
|
||||
|
||||
@@ -1,16 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
定时排期生成器 v2
|
||||
规则:
|
||||
1. 第一条视频 **立即发布**(first_delay=0)
|
||||
2. 第二条起,每条间隔 30~120 分钟(随机)
|
||||
3. 若总时长超过 max_hours,按比例压缩
|
||||
4. 支持各平台原生定时 API 所需的 datetime 对象
|
||||
定时排期生成器 v3
|
||||
- v2:固定 30~120min 随机间隔
|
||||
- v3:generate_smart_schedule — 按视频条数自适应间隔/总跨度,并可选避开本地凌晨低活跃时段(适合视频号等多条错峰发)
|
||||
"""
|
||||
import os
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def suggest_stagger_params(n: int) -> tuple[int, int, float]:
|
||||
"""
|
||||
根据待发布条数给出建议:min_gap, max_gap(分钟), max_hours。
|
||||
条数越多,略缩短间隔、允许更长总跨度,避免全挤在 24h 内过密。
|
||||
"""
|
||||
if n <= 1:
|
||||
return 30, 120, 24.0
|
||||
if n <= 4:
|
||||
return 45, 150, 28.0
|
||||
if n <= 8:
|
||||
return 38, 125, max(28.0, min(48.0, n * 3.2))
|
||||
if n <= 15:
|
||||
return 32, 105, max(36.0, min(60.0, n * 2.8))
|
||||
if n <= 25:
|
||||
return 28, 95, max(48.0, min(72.0, n * 2.4))
|
||||
return 25, 85, min(96.0, max(56.0, n * 2.0))
|
||||
|
||||
|
||||
def refine_avoid_late_night(
|
||||
times: list[datetime],
|
||||
min_after_prev: int = 25,
|
||||
) -> list[datetime]:
|
||||
"""
|
||||
将落在 **本机本地时间** 00:00–07:59 的排期顺延到当日/次日 12:xx,
|
||||
并保持与上一条至少 min_after_prev 分钟(请把系统时区设为发布面向地区,如中国用北京时间)。
|
||||
关闭:环境变量 SCHEDULE_NO_NIGHT_REFINE=1(在 generate_smart_schedule 层已读)。
|
||||
"""
|
||||
if len(times) <= 1:
|
||||
return list(times)
|
||||
|
||||
def strip_tz(t: datetime) -> datetime:
|
||||
return t.replace(tzinfo=None) if t.tzinfo else t
|
||||
|
||||
out = [strip_tz(times[0])]
|
||||
for raw in times[1:]:
|
||||
t = max(strip_tz(raw), out[-1] + timedelta(minutes=min_after_prev))
|
||||
if 0 <= t.hour < 8:
|
||||
minute_slot = random.randint(0, 50)
|
||||
candidate = t.replace(hour=12, minute=minute_slot, second=0, microsecond=0)
|
||||
if candidate <= out[-1] + timedelta(minutes=min_after_prev):
|
||||
nd = out[-1].date() + timedelta(days=1)
|
||||
candidate = datetime(nd.year, nd.month, nd.day, 12, random.randint(0, 50), 0)
|
||||
t = candidate
|
||||
while t <= out[-1] + timedelta(minutes=min_after_prev):
|
||||
t += timedelta(minutes=30)
|
||||
out.append(t)
|
||||
return out
|
||||
|
||||
|
||||
def generate_smart_schedule(
|
||||
n: int,
|
||||
start_time: datetime | None = None,
|
||||
avoid_late_night: bool | None = None,
|
||||
) -> list[datetime]:
|
||||
"""
|
||||
推荐默认:按条数自适应间隔 + 总跨度,并避开凌晨。
|
||||
avoid_late_night 默认 True;SCHEDULE_NO_NIGHT_REFINE=1 可关闭。
|
||||
"""
|
||||
if n <= 0:
|
||||
return []
|
||||
if avoid_late_night is None:
|
||||
avoid_late_night = os.environ.get("SCHEDULE_NO_NIGHT_REFINE", "").strip() not in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
)
|
||||
min_gap, max_gap, max_hours = suggest_stagger_params(n)
|
||||
times = generate_schedule(
|
||||
n,
|
||||
min_gap=min_gap,
|
||||
max_gap=max_gap,
|
||||
max_hours=max_hours,
|
||||
first_delay=0,
|
||||
start_time=start_time,
|
||||
)
|
||||
if avoid_late_night and n > 1:
|
||||
times = refine_avoid_late_night(times)
|
||||
return times
|
||||
|
||||
|
||||
def generate_schedule(
|
||||
n: int,
|
||||
min_gap: int = 30,
|
||||
|
||||
224
03_卡木(木)/木叶_视频内容/多平台分发/脚本/video_channels_resume.py
Normal file
224
03_卡木(木)/木叶_视频内容/多平台分发/脚本/video_channels_resume.py
Normal file
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
视频号:严格两步——① 等微信扫码完成(Cookie 校验通过)② 再上传。
|
||||
|
||||
默认:弹出 Chromium 窗口扫码(与手机微信同一套实时码,易成功);扫完并保存 Cookie 后自动进入上传。
|
||||
无头:加 --silent-login(仅终端/对话看图,易与真实会话不一致)。
|
||||
|
||||
用法:
|
||||
cd 多平台分发/脚本
|
||||
python3 video_channels_resume.py --video-dir "/path/to/成片"
|
||||
python3 video_channels_resume.py --video-dir "/path/to/成片" --silent-login
|
||||
|
||||
铁律:
|
||||
- 未检测到有效 Cookie 前,绝不会启动 distribute_all。
|
||||
- Cookie 须「连续两次」间隔数秒检测通过,避免半写入就开传。
|
||||
- 仅第一步:--step1-only
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
BASE = SCRIPT_DIR.parent.parent
|
||||
CHANNELS_LOGIN = BASE / "视频号发布" / "脚本" / "channels_login.py"
|
||||
DISTRIBUTE = SCRIPT_DIR / "distribute_all.py"
|
||||
|
||||
QR_PATH = Path("/tmp/channels_qr.png")
|
||||
LOGIN_LOG = Path("/tmp/channels_login_last.log")
|
||||
|
||||
|
||||
def _cookie_ok() -> bool:
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
from cookie_manager import check_cookie_valid, sync_channels_cookie_files
|
||||
|
||||
sync_channels_cookie_files()
|
||||
ok, _ = check_cookie_valid("视频号")
|
||||
return ok
|
||||
|
||||
|
||||
def _step1_confirm_cookie() -> bool:
|
||||
if not _cookie_ok():
|
||||
return False
|
||||
time.sleep(4)
|
||||
return _cookie_ok()
|
||||
|
||||
|
||||
def _run_visible_login() -> int:
|
||||
"""阻塞运行 channels_login --playwright-only,弹窗 Chromium 扫码。"""
|
||||
env = os.environ.copy()
|
||||
env.pop("CHANNELS_SILENT_QR", None)
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
print(
|
||||
"\n[i] ▶ 即将弹出 Chromium 窗口:请用 **微信扫一扫** 扫窗口里的码,"
|
||||
"并在手机上确认登录。\n"
|
||||
" 登录成功并写入 Cookie 后,本脚本会自动进入下一步上传。\n",
|
||||
flush=True,
|
||||
)
|
||||
return subprocess.run(
|
||||
[sys.executable, str(CHANNELS_LOGIN), "--playwright-only"],
|
||||
cwd=str(CHANNELS_LOGIN.parent),
|
||||
env=env,
|
||||
).returncode
|
||||
|
||||
|
||||
def _run_silent_login_poll(*, poll_interval: int, poll_max: int) -> bool:
|
||||
"""后台 headless 登录 + 轮询 Cookie。"""
|
||||
env = os.environ.copy()
|
||||
env["CHANNELS_SILENT_QR"] = "1"
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
LOGIN_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_f = open(LOGIN_LOG, "w", encoding="utf-8")
|
||||
print(f"[i] 已启动静默登录(headless),日志:{LOGIN_LOG}", flush=True)
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(CHANNELS_LOGIN)],
|
||||
cwd=str(CHANNELS_LOGIN.parent),
|
||||
env=env,
|
||||
stdout=log_f,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
log_f.close()
|
||||
|
||||
time.sleep(14)
|
||||
if QR_PATH.exists():
|
||||
desk = Path.home() / "Desktop" / "channels_login_qr.png"
|
||||
try:
|
||||
desk.write_bytes(QR_PATH.read_bytes())
|
||||
except Exception:
|
||||
desk = None
|
||||
print("\n========== SOUL_QR_IMAGE_FOR_CHAT ==========", flush=True)
|
||||
print(str(QR_PATH.resolve()), flush=True)
|
||||
if desk and desk.exists():
|
||||
print(f"file://{desk.resolve()}", flush=True)
|
||||
print("→ 请扫 **与 headless 会话对应** 的码(见上图路径)。", flush=True)
|
||||
print("========== END_SOUL_QR_MARKER ==========\n", flush=True)
|
||||
|
||||
print(
|
||||
f"\n[i] STEP 1:轮询 Cookie(每 {poll_interval}s),最长约 "
|
||||
f"{poll_max * poll_interval // 60} 分钟…\n",
|
||||
flush=True,
|
||||
)
|
||||
ok_phase = False
|
||||
for i in range(poll_max):
|
||||
time.sleep(poll_interval)
|
||||
if _cookie_ok():
|
||||
time.sleep(4)
|
||||
if _cookie_ok():
|
||||
print(
|
||||
f"\n[✓] STEP 1 完成:Cookie 已落盘(约 {(i + 1) * poll_interval}s)。\n",
|
||||
flush=True,
|
||||
)
|
||||
ok_phase = True
|
||||
break
|
||||
if i % 5 == 4:
|
||||
print(
|
||||
f" …仍等待扫码({(i + 1) * poll_interval}s)",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
try:
|
||||
proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
return ok_phase
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import argparse
|
||||
|
||||
p = argparse.ArgumentParser(
|
||||
description="视频号:先扫码登录,再仅发视频号(断点 + until-success)",
|
||||
)
|
||||
p.add_argument("--video-dir", required=True, help="成片目录(含 mp4)")
|
||||
p.add_argument("--poll-interval", type=int, default=12, help="静默模式轮询秒数")
|
||||
p.add_argument("--poll-max", type=int, default=150, help="静默模式最多轮询次数")
|
||||
p.add_argument("--until-success-sleep", type=int, default=90, help="until-success 轮间隔")
|
||||
p.add_argument(
|
||||
"--silent-login",
|
||||
action="store_true",
|
||||
help="无头登录(不弹窗);默认弹 Chromium 窗口扫码",
|
||||
)
|
||||
p.add_argument(
|
||||
"--step1-only",
|
||||
action="store_true",
|
||||
help="只做第一步:Cookie 有效即退出,不上传",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
vd = Path(args.video_dir)
|
||||
if not vd.is_dir():
|
||||
print(f"[✗] 目录不存在: {vd}", flush=True)
|
||||
return 1
|
||||
|
||||
print("\n" + "=" * 60, flush=True)
|
||||
print(" 【STEP 1 / 2】微信扫码登录视频号助手", flush=True)
|
||||
if args.silent_login:
|
||||
print(" 模式:静默(headless)", flush=True)
|
||||
else:
|
||||
print(" 模式:弹窗 Chromium(推荐)", flush=True)
|
||||
print(" 未完成本步之前,不会开始上传。", flush=True)
|
||||
print("=" * 60 + "\n", flush=True)
|
||||
|
||||
if _step1_confirm_cookie():
|
||||
print(
|
||||
"[✓] STEP 1:Cookie 已连续两次校验通过,跳过登录。\n",
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
if not CHANNELS_LOGIN.exists():
|
||||
print(f"[✗] 未找到 {CHANNELS_LOGIN}", flush=True)
|
||||
return 1
|
||||
|
||||
if args.silent_login:
|
||||
if not _run_silent_login_poll(
|
||||
poll_interval=args.poll_interval,
|
||||
poll_max=args.poll_max,
|
||||
):
|
||||
print("[✗] STEP 1 超时:仍未获得有效 Cookie。", flush=True)
|
||||
return 1
|
||||
else:
|
||||
rc = _run_visible_login()
|
||||
if rc != 0:
|
||||
print(f"[✗] 登录进程退出码 {rc},未保存 Cookie。", flush=True)
|
||||
return 1
|
||||
time.sleep(2)
|
||||
if not _step1_confirm_cookie():
|
||||
print(
|
||||
"[✗] 弹窗登录结束但 Cookie 仍未通过连续校验,请重试或改用 --silent-login。",
|
||||
flush=True,
|
||||
)
|
||||
return 1
|
||||
print("[✓] STEP 1 完成:弹窗扫码已落盘。\n", flush=True)
|
||||
|
||||
if args.step1_only:
|
||||
print("[i] --step1-only:不上传,结束。", flush=True)
|
||||
return 0
|
||||
|
||||
print("\n" + "=" * 60, flush=True)
|
||||
print(" 【STEP 2 / 2】扫码已完成 → 开始仅视频号上传", flush=True)
|
||||
print("=" * 60 + "\n", flush=True)
|
||||
time.sleep(2)
|
||||
|
||||
r = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(DISTRIBUTE),
|
||||
"--platforms",
|
||||
"视频号",
|
||||
"--video-dir",
|
||||
str(vd),
|
||||
"--until-success",
|
||||
"--until-success-sleep",
|
||||
str(args.until_success_sleep),
|
||||
],
|
||||
cwd=str(SCRIPT_DIR),
|
||||
)
|
||||
return r.returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: Soul竖屏切片
|
||||
description: Soul 派对视频→竖屏成片;高光**单主题完整**(30s~5min/条,每场约 8~30 条)、去穿插话题;封面底**约 10% 轻模糊**(非全糊)。字幕暖色条+金边、纠错+关键词高亮、concat 前导空白、音轨 PTS;推荐 --typewriter-subs。流程:裁剪检查→soul_enhance。MLX→visual_enhance v8 可选。
|
||||
triggers: Soul竖屏切片、视频切片、热点切片、竖屏成片、派对切片、裁剪检查、重新截图、全画面标定、竖屏裁剪、全画面成片、letterbox、画面显示全、白边、飞书录屏、LTX、AI生成视频、Retake重剪、字幕优化、字幕同步、逐字字幕
|
||||
triggers: Soul竖屏切片、视频切片、热点切片、竖屏成片、派对切片、裁剪检查、重新截图、全画面标定、竖屏裁剪、全画面成片、letterbox、画面显示全、白边、飞书录屏、LTX、AI生成视频、Retake重剪、字幕优化、字幕同步、逐字字幕、上麦项目、月营收、融资、前三秒钩子
|
||||
owner: 木叶
|
||||
group: 木
|
||||
version: "2.1"
|
||||
updated: "2026-03-22"
|
||||
version: "2.3"
|
||||
updated: "2026-03-23"
|
||||
---
|
||||
|
||||
# Soul 竖屏切片 · 专用 Skill
|
||||
@@ -30,7 +30,10 @@ updated: "2026-03-22"
|
||||
|
||||
## 二、视频结构:提问→回答 + 前3秒高光 + 去语助词
|
||||
|
||||
- **每个话题前均优先提问→回答**:先看片段有没有人提问;**有提问**则把**提问的问题**放到前3秒(封面/前贴),先展示问题再播回答;无提问则从**该片段口播里**抽**吸睛句或反问**作 `hook_3sec`,禁止与正片内容脱节。
|
||||
- **上麦案例:项目与钱优先占满前3秒(高于泛提问)**
|
||||
当片段里是**对方在讲自己的项目/业务**(做什么、服务谁、阶段),且口播中出现 **月收入/营收/融资/投资/到账/定价/预算/工资档位/烧多少钱** 等**可量化钱**或**项目身份**信息时:`hook_3sec` **优先**写成一句 **「项目一句话 + 钱/阶段信号」**(优质上麦案例逻辑),让用户前3秒知道「这人是谁、项目多硬、钱的故事是什么」。
|
||||
**在此之后**再考虑「主持人提问」作 hook:若前3秒用项目+钱已足够抓人,可将 `question` 保留为片中互动句,**不必**把泛提问顶掉项目钩子。仅当片段**没有**清晰项目/钱信息时,才回到「有提问用提问、无提问用金句」的旧规则。
|
||||
- **每个话题前均优先提问→回答(与上条叠加时的裁决)**:默认先看有没有人提问;**但若本段核心是对方分享项目**,则 **hook = 项目+钱摘要** 优先,**正片**再展开主持人追问与回答;禁止前3秒与「对方在讲的事」脱节。
|
||||
- **成片链路**:前3秒展示问题(或 hook)→ 正片展开 → **整片去除语助词**(提问与回答部分均由 soul_enhance 清理)。
|
||||
- **节奏与高光**:竖屏段内全程**高光清晰、节奏清楚**(剪去拖沓静音与跑题可在 enhance 链路的静音裁剪中体现;**话题边界以 highlights 起止为准,剪辑阶段就要裁干净**)。
|
||||
- **单主题、完整、有趣**:**一条成片只服务一个主题**;口播里若**临时插进其他话题**,在 `highlights` 对应时间段上**剪掉无关段**(或拆成另一条高光),保证**有头有尾、逻辑闭环**;选题要**有传播点**(反差、数字、痛点、金句),忌又长又平。
|
||||
@@ -49,7 +52,7 @@ updated: "2026-03-22"
|
||||
```
|
||||
|
||||
- **batch_clip**:输出到 `clips/`
|
||||
- **soul_enhance -o 成片/ --vertical --title-only**:**文件名 = 封面标题 = highlights 的 title**(去杠:`:|、—、/` 等替换为空格),名字与标题一致、无序号无杠;字幕烧录(随语音走动);完整去语助词;竖屏裁剪直出到 `成片/`
|
||||
- **soul_enhance -o 成片/ --title-only**(**推荐仍写 `--vertical`**):自 v2.3 起,只要带了 **`--title-only` 和/或 `--crop-vf` / `--vertical-fit-full`**,脚本会**默认启用竖屏直出**,避免漏写 `--vertical` 误出 1920×1080 横版。**文件名 = 封面标题 = highlights 的 title**(去杠:`:|、—、/` 等替换为空格);字幕烧录;去语助词;竖条裁剪直出到 `成片/`
|
||||
|
||||
### 3.1 全画面参数(必做约定)
|
||||
|
||||
@@ -133,6 +136,7 @@ python3 soul_enhance.py \
|
||||
|
||||
- **封面**:竖条画布内**不超出界面**;**冷色半透明渐变**(`VERTICAL_COVER_ALPHA` 约 148)+ **底部电影感渐隐**(`STYLE['cover']` 的 `vignette_*`)+ **顶栏单条 Soul 绿 + 可选 1px 淡金线** + **细白内框**;主标题为**柔阴影暖白字**(非粗描边),字体优先思源黑体 Bold;左上角双圈 Soul 标。**封面文案**优先 `hook_3sec`(见 `pick_cover_hook_text`)。成片文件名仍与 `highlights.title` 规则一致。
|
||||
- **封面底层模糊(重要)**:**不要全屏强糊**。`soul_enhance.py` 默认 **`STYLE['cover']['bg_blur_mix']=0.1`**:清晰视频帧与一层高斯模糊按 **约 10% 混合**(`bg_blur_radius` 生成模糊层),界面仍大致可辨,仅轻微虚化衬托文字。若需更强/更弱,改脚本内两常量,勿回到「整帧 radius=50+ 全糊」。
|
||||
- **竖条封面取景 = 成片取景(重要)**:竖条模式(非 `--vertical-fit-full`)下,封面底图必须用与最终成片 **相同的 `--crop-vf`** 从横版切片抽帧,**禁止**用整幅 1920×1080 再压成竖条——否则分发/素材库里「封面」与「成片第一帧」不是同一条竖条,观感错位。脚本内由 `create_cover_image(..., cover_extract_vf=vf_use)` 保证与成片一致。
|
||||
- **字幕**:**封面一结束即叠字幕**(无额外「空几秒再等字」);SRT 安全起点为封面结束 + **约 0.05s** epsilon,避免与最后一帧封面打架。字幕**居中**在竖条内。先尝试**单次 FFmpeg 通道**(一次 pass 完成所有字幕叠加,最快);若失败自动回退到分批模式(batch_size=40);语助词在解析阶段已由 `clean_filler_words` 去除。重新加字幕时加 `--force-burn-subs`。⚠️ 注意:当前 FFmpeg 不支持 drawtext/subtitles 滤镜,只能用 PIL 图像 overlay 方案。(脚本常量:`SUBS_START_AFTER_COVER_SEC`,**默认 0.0**)
|
||||
- **字幕字形**:Whisper 词级轴常在**中日文之间插空格**,逐字/逐词显字时会像「字与字被撑开」;脚本在 `improve_subtitle_punctuation` 路径对 **CJK 相邻空白**做折叠(`_collapse_cjk_interchar_spaces`),保证整句显示正常、无异常中空。
|
||||
- **封面标题**:高光 `title` 建议 **4~6 个汉字**;成片内封面主标题最多显示 **6 个汉字**(超长由 `soul_enhance` 自动截断,与文件名 `--title-only` 一致)。
|
||||
|
||||
@@ -10,17 +10,18 @@
|
||||
|
||||
---
|
||||
|
||||
## 二、提问→回答 结构
|
||||
## 二、提问→回答 结构(含:上麦项目+钱优先)
|
||||
|
||||
| 步骤 | 动作 | 说明 |
|
||||
|------|------|------|
|
||||
| 0 | **上麦是否在讲自己的项目+钱** | 若有「做什么项目 + 营收/融资/薪资/预算/API消耗」等,**前3秒优先** `hook_3sec` = 一句话项目优质案例逻辑;`question` 可记主持人追问,不必挤掉 hook |
|
||||
| 1 | 看片段是否有人提问 | 高光识别时判断:该段时间内是否有观众/连麦者提出问题 |
|
||||
| 2 | 若有提问 | 提取**提问原文**(可去语助词、精简一句),填 `question`,`hook_3sec` 与之一致 |
|
||||
| 3 | 前3秒 | 封面/前贴展示「提问的问题」,让用户先看到问题 |
|
||||
| 4 | 正片 | 从回答开始,或「先问后答」完整保留,形成完整提问→回答链路 |
|
||||
| 5 | 若无提问 | 前3秒用金句/悬念/数据等 hook_3sec,无 question 字段 |
|
||||
| 2 | 若有提问且未执行第0步 | 提取**提问原文**(可去语助词、精简一句),填 `question`,`hook_3sec` 与之一致 |
|
||||
| 3 | 前3秒 | 封面/前贴展示 **hook_3sec**(项目+钱 或 提问) |
|
||||
| 4 | 正片 | 从回答开始,或「先问后答」完整保留,形成完整链路 |
|
||||
| 5 | 若无提问且无上麦项目句 | 前3秒用金句/悬念/数据等 hook_3sec,无 question 字段 |
|
||||
|
||||
**目的**:用户先被「问题」抓住,再听回答,完播率更高;提问与回答中的语助词在成片阶段**完整去除**。
|
||||
**目的**:用户前3秒先看到「这人项目多硬/钱的故事」或「问题」,再听展开;语助词在成片阶段**完整去除**。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -49,17 +49,30 @@
|
||||
- 吊人胃口的开场
|
||||
- "接下来这段更精彩"
|
||||
|
||||
# 提问→回答 结构(每个话题前均优先)
|
||||
# 上麦优质项目 → 前3秒优先(高于泛提问)
|
||||
|
||||
**每个话题/高光片段均优先采用提问→回答形式**:先展示**提问**(前3秒),再进入**回答**(正片)。若片段里有人提问,必须把**提问的问题**放到前3秒。
|
||||
当高光段以**连麦者/嘉宾自述**为主,且出现以下任一,**优先**把前3秒留给「项目+钱/阶段」摘要:
|
||||
|
||||
- **项目身份**:在做什么、给谁用、OPC/社群/代运营/孵化/产品名等可一句话说清的业态;
|
||||
- **钱与规模**:月收入、营收、融资/投资/条款/到账、定价、预算、岗位薪资区间、每月烧 API/投流金额等**具体数字或档位**。
|
||||
|
||||
**写法**:`hook_3sec` 用一句 **12~24 字内**的「优质上麦案例」钩子(项目 + 钱/阶段),例如「北京AI项目刚拿机构投资」「人事总助写一块招不到人」。
|
||||
仍可在 JSON 里填 `question` 记录主持人后面的追问,但 **封面/前3秒优先烧 `hook_3sec`**(`pick_cover_hook_text` 已优先 hook)。
|
||||
**仅当本段没有清晰项目/钱信息时**,再完全沿用下方「提问→回答」规则。
|
||||
|
||||
---
|
||||
|
||||
# 提问→回答 结构(每个话题前均优先 — 与上条叠加时)
|
||||
|
||||
**默认**:每个话题/高光片段采用提问→回答形式:先展示**提问**(前3秒),再进入**回答**(正片)。**若已触发上条「项目+钱优先」**,前3秒用 hook,正片内再问再答。
|
||||
|
||||
1. **判断**:看该高光片段文字稿中是否有人提问(观众/连麦者问的问题)。
|
||||
2. **若有提问**:
|
||||
2. **若有提问且未用「项目+钱」占前3秒**:
|
||||
- 提取**提问的原文**(可去语助词、精简为一句),填入 `question` 字段;
|
||||
- `hook_3sec` 优先用该提问内容(与 question 一致),成片前3秒封面/贴片展示「问题」,正片从回答开始或先问后答。
|
||||
3. **若无提问**:`hook_3sec` 用金句/悬念/数据等抓眼球开场,`question` 可省略。
|
||||
|
||||
**目的**:前3秒让用户看到「问题」,再听回答,形成完整**提问→回答**链路;提问与回答部分的语助词均在成片时完整去除。
|
||||
**目的**:前3秒让用户看到「问题」或「项目+钱」,再听回答,形成完整链路;语助词在成片时完整去除。
|
||||
|
||||
# 前3秒Hook设计原则
|
||||
|
||||
|
||||
10
03_卡木(木)/木叶_视频内容/视频切片/场次稿/第129场_20260320_highlights.json
Normal file
10
03_卡木(木)/木叶_视频内容/视频切片/场次稿/第129场_20260320_highlights.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{"title": "百条分发怎么铺", "start_time": "00:10:28", "end_time": "00:13:10", "hook_3sec": "十条乘十平台一天百条分发", "question": "多平台分发怎么测?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "内容产出与一键分发", "reason": "项目_分发规模+数字"},
|
||||
{"title": "派对百二十场价值", "start_time": "00:32:10", "end_time": "00:34:55", "hook_3sec": "派对开到一百二十场图啥", "question": "坚持百场背后价值?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "方向与执行", "reason": "项目_派对复盘"},
|
||||
{"title": "老板群九十五人链", "start_time": "00:44:38", "end_time": "00:47:15", "hook_3sec": "一个群能挤九十五个老板", "question": "怎么进高质量群?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "老板群与私域", "reason": "项目_资源链接"},
|
||||
{"title": "AI切片背后人力", "start_time": "00:51:14", "end_time": "00:54:00", "hook_3sec": "磨AI和token几十人在扛", "question": "简单按钮背后多少成本?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "研发运营与token", "reason": "项目+钱_成本"},
|
||||
{"title": "ENTP孩子与AI教育", "start_time": "01:15:52", "end_time": "01:18:45", "hook_3sec": "ENTP爱表现是啥产业人格", "question": "孩子性格怎么接AI?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "ENTP与表现欲", "reason": "上麦案例_人格+教育"},
|
||||
{"title": "派对场观五到十万", "start_time": "01:42:15", "end_time": "01:45:05", "hook_3sec": "派对一天场观五到十万", "question": "曝光和进房怎么算?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "场观与深链", "reason": "月营收级_曝光数字"},
|
||||
{"title": "特步模型投资流水", "start_time": "02:21:38", "end_time": "02:25:45", "hook_3sec": "七千店复制拿投资月流水五十万", "question": "大品牌合作怎么拆?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "7500店+50万流水干股", "reason": "优质项目_规模+月营收+投资"},
|
||||
{"title": "创始人决定能谈成", "start_time": "02:32:03", "end_time": "02:35:00", "hook_3sec": "创始人硬才能谈特步营收", "question": "合作先看人还是模型?", "cta_ending": "今天就到这里,点个关注下次不迷路", "transcript_excerpt": "创始人与营收数据", "reason": "优质项目_创始人+营收"}
|
||||
]
|
||||
@@ -2,7 +2,7 @@
|
||||
"""
|
||||
Soul切片增强脚本 v2.0
|
||||
功能:
|
||||
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏底图为**清晰帧 + 约 10% 轻模糊混入**(非全糊)+ 冷色渐变;**顶栏单条 Soul 绿 + 底部电影感渐隐 + 细内框 + 柔阴影标题**(避免粗描边与多条绿边廉价感)
|
||||
1. 封面贴片:高光 hook_3sec 优先(吸睛),竖屏底图为**清晰帧 + 约 10% 轻模糊混入**(非全糊)+ 冷色渐变;**顶栏单条 Soul 绿 + 底部电影感渐隐 + 细内框 + 柔阴影标题**(避免粗描边与多条绿边廉价感)。**竖条成片**:封面取帧须与最终成片同一 `-vf`(如 `crop=598:1080:493:0`),禁止用整幅 1920 横版再压成竖条(会与正片取景不一致)。
|
||||
2. 烧录字幕(关键词高亮、可选逐字)
|
||||
3. 切除检出的长静音并重映射字幕时间轴
|
||||
4. 片尾 CTA(cta_ending)字幕条
|
||||
@@ -1060,8 +1060,18 @@ def _strip_cover_number_prefix(text):
|
||||
return text.strip()
|
||||
|
||||
|
||||
def create_cover_image(hook_text, width, height, output_path, video_path=None):
|
||||
"""创建封面贴片。竖条:视频底 + 冷色渐变 + 底栏渐隐 + 顶栏 Soul 绿与细金线 + 内框 + 柔阴影标题 + 左上角标。"""
|
||||
def create_cover_image(
|
||||
hook_text,
|
||||
width,
|
||||
height,
|
||||
output_path,
|
||||
video_path=None,
|
||||
cover_extract_vf=None,
|
||||
):
|
||||
"""创建封面贴片。竖条:视频底 + 冷色渐变 + 底栏渐隐 + 顶栏 Soul 绿与细金线 + 内框 + 柔阴影标题 + 左上角标。
|
||||
|
||||
cover_extract_vf:竖条模式时与成片最终 `-vf` 一致(如 crop 竖条),从横版切片中截取与成片同取景的底图;不传则仍用整幅帧拉伸(易与成片不一致,仅兼容旧行为)。
|
||||
"""
|
||||
hook_text = _to_simplified(str(hook_text or "").strip())
|
||||
hook_text = _strip_cover_number_prefix(hook_text)
|
||||
if not hook_text:
|
||||
@@ -1075,25 +1085,26 @@ def create_cover_image(hook_text, width, height, output_path, video_path=None):
|
||||
base = Image.new("RGBA", (width, height), (*VERTICAL_COVER_TOP, 255))
|
||||
if video_path and os.path.exists(video_path):
|
||||
temp_frame = output_path.replace(".png", "_vframe.jpg")
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-ss",
|
||||
"0.35",
|
||||
"-i",
|
||||
video_path,
|
||||
"-vframes",
|
||||
"1",
|
||||
"-q:v",
|
||||
"3",
|
||||
temp_frame,
|
||||
],
|
||||
capture_output=True,
|
||||
)
|
||||
vf = (cover_extract_vf or "").strip()
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-ss",
|
||||
"0.35",
|
||||
"-i",
|
||||
video_path,
|
||||
]
|
||||
if vf:
|
||||
cmd.extend(["-vf", vf])
|
||||
cmd.extend(["-vframes", "1", "-q:v", "3", temp_frame])
|
||||
subprocess.run(cmd, capture_output=True)
|
||||
if os.path.exists(temp_frame):
|
||||
try:
|
||||
sharp = Image.open(temp_frame).convert("RGBA").resize((width, height))
|
||||
sharp = Image.open(temp_frame).convert("RGBA")
|
||||
if sharp.size != (width, height):
|
||||
sharp = sharp.resize(
|
||||
(width, height), Image.Resampling.LANCZOS
|
||||
)
|
||||
mix = float(style.get("bg_blur_mix", 0.10))
|
||||
r = float(style.get("bg_blur_radius", 14))
|
||||
if mix > 0.001:
|
||||
@@ -1785,10 +1796,15 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# 3. 封面图(与去静音后首帧一致)
|
||||
# 3. 封面图(与去静音后首帧一致;竖条须与成片同 crop-vf 取底图,勿整幅横版压竖条)
|
||||
print(f" [1/5] 封面生成中…", flush=True)
|
||||
cover_img = os.path.join(temp_dir, "cover.png")
|
||||
create_cover_image(hook_text, out_w, out_h, cover_img, working_clip)
|
||||
cover_extract_vf = None
|
||||
if vertical and not vertical_fit_full:
|
||||
cover_extract_vf = vf_use
|
||||
create_cover_image(
|
||||
hook_text, out_w, out_h, cover_img, working_clip, cover_extract_vf=cover_extract_vf
|
||||
)
|
||||
print(f" ✓ 封面生成", flush=True)
|
||||
|
||||
if subtitles:
|
||||
@@ -1964,8 +1980,10 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
# 5.4 输出:竖条(宽由 vf)或全画面 letterbox
|
||||
if vertical and not vertical_fit_full:
|
||||
print(f" [5/5] 竖屏输出({out_w}×{out_h})…", flush=True)
|
||||
elif vertical and vertical_fit_full:
|
||||
print(f" [5/5] 竖屏输出(全画面 letterbox)…", flush=True)
|
||||
else:
|
||||
print(f" [5/5] 竖屏输出…", flush=True)
|
||||
print(f" [5/5] 横版输出(未开竖屏)…", flush=True)
|
||||
if vertical and vertical_fit_full:
|
||||
r = subprocess.run([
|
||||
'ffmpeg', '-y', '-i', current_video,
|
||||
@@ -2065,14 +2083,22 @@ def main():
|
||||
|
||||
vertical = getattr(args, 'vertical', False)
|
||||
title_only = getattr(args, 'title_only', False)
|
||||
print("="*60)
|
||||
print("🎬 Soul切片增强" + ("(成片竖屏直出)" if vertical else ""))
|
||||
print("="*60)
|
||||
crop_vf_arg = (getattr(args, "crop_vf", "") or "").strip()
|
||||
overlay_x_arg = getattr(args, "overlay_x", -1)
|
||||
overlay_x_arg = None if overlay_x_arg < 0 else overlay_x_arg
|
||||
typewriter = getattr(args, "typewriter_subs", False)
|
||||
vfit = getattr(args, "vertical_fit_full", False)
|
||||
# Soul 成片:--title-only 或塑形相关参数时默认竖屏直出,避免只传 --crop-vf 却漏 --vertical 误出 1920×1080 横版
|
||||
if not vertical and (title_only or crop_vf_arg or vfit):
|
||||
vertical = True
|
||||
print(
|
||||
"ℹ️ 已默认启用竖屏直出(因 --title-only 和/或 --crop-vf / --vertical-fit-full);"
|
||||
"显式加 --vertical 亦同。",
|
||||
flush=True,
|
||||
)
|
||||
print("="*60)
|
||||
print("🎬 Soul切片增强" + ("(成片竖屏直出)" if vertical else ""))
|
||||
print("="*60)
|
||||
print(
|
||||
f"功能: 封面+字幕+加速10%+去语气词"
|
||||
+ ("+去长静音" if not getattr(args, "no_trim_silence", False) else "")
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
微信**未开放**「用开放平台 API 直接上传/发布短视频」;短视频自动化目前只可走 **A 轨**。
|
||||
**B 轨**负责经营数据、带货、留资、直播场次等**合规官方能力**。
|
||||
|
||||
**无法规避助手 Cookie 的原因(结论):** 官方接口列表页当前**无**「上传视频 / 发表动态 / 创建短视频」路径;社区答复亦明确**不支持**接口直发视频号 Feed。
|
||||
**缓解重复扫码(工程侧)**:`channels_login.py` 默认启用 **Playwright 持久化用户目录**(`~/.soul-channels-playwright-profile`),同机同账号在腾讯会话有效期内通常**无需每次扫码**;禁用:`CHANNELS_PERSISTENT_LOGIN=0` 或 `--no-persistent`。
|
||||
占位与自检脚本:`脚本/channels_open_platform_publish.py`。
|
||||
|
||||
---
|
||||
|
||||
## 二、助手后台「开放能力」能联想到的产品动作(想象力清单)
|
||||
|
||||
@@ -12,18 +12,19 @@ updated: "2026-03-23"
|
||||
|
||||
# 视频号发布 Skill(v3.0)
|
||||
|
||||
> **核心能力**:纯 API(httpx)视频号发布,零 Playwright 依赖。
|
||||
> **核心能力**:发布链路纯 **httpx**;**登录**阶段用 Playwright(默认持久化 Chromium,减少重复扫码)。
|
||||
> **实测**:120 场 12 条切片全部 API 直发成功,单条 5~9 秒。
|
||||
> **去重**:基于 publish_log.jsonl,同一视频不重复发。
|
||||
> **Cookie 有效期**:~24-48h,通过 channels_login.py 扫码刷新。
|
||||
> **Cookie 有效期**:~24-48h,过期需刷新。`channels_login.py` 默认 **持久化 Chromium 用户目录**(`~/.soul-channels-playwright-profile`),同机同账号在腾讯会话未失效时**通常不必每次扫码**;`CHANNELS_PERSISTENT_LOGIN=0` 或 `--no-persistent` 可关。
|
||||
> **开放平台 access_token**:**不能**替代助手 Cookie 发短视频(官方助手 API 列表无上传发表接口),见 `REFERENCE_开放能力_数据与集成.md` 与 `脚本/channels_open_platform_publish.py`。
|
||||
|
||||
---
|
||||
|
||||
## 一、纯 API 完整流程(5 步)
|
||||
|
||||
```
|
||||
[Step 1] Cookie 认证(一次性)
|
||||
Playwright 微信扫码 → channels_storage_state.json
|
||||
[Step 1] Cookie 认证(助手态)
|
||||
Playwright(默认持久化 profile,减少重复扫码)→ channels_storage_state.json
|
||||
登录地址: https://channels.weixin.qq.com/login
|
||||
获取: cookies, localStorage (_finger_print_device_id, __ml::aid)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -31,6 +32,7 @@ VIDEO_DIR = Path("/Users/karuo/Movies/soul视频/soul_派对_121场_20260311_out
|
||||
|
||||
sys.path.insert(0, str(SCRIPT_DIR.parent.parent / "多平台分发" / "脚本"))
|
||||
from publish_result import PublishResult, is_published, save_results, print_summary
|
||||
from schedule_generator import generate_smart_schedule
|
||||
|
||||
UA = (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
@@ -265,6 +267,19 @@ def _gen_task_id() -> str:
|
||||
return str(ts_ms * 10**7 + rand)
|
||||
|
||||
|
||||
def _payload_raw_key_buff(finder_raw: str | None):
|
||||
"""localStorage finder_raw → post_create / clip 的 rawKeyBuff(字符串或已解析 JSON)。"""
|
||||
if not finder_raw or not str(finder_raw).strip():
|
||||
return None
|
||||
s = str(finder_raw).strip()
|
||||
if (s.startswith("{") and s.endswith("}")) or (s.startswith("[") and s.endswith("]")):
|
||||
try:
|
||||
return json.loads(s)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return s
|
||||
|
||||
|
||||
def _micro_headers(cookie_str: str, uin: str, finger_print: str = "") -> dict:
|
||||
h = {
|
||||
"Cookie": cookie_str,
|
||||
@@ -405,8 +420,10 @@ async def create_post(
|
||||
upload_cost: int,
|
||||
scheduled_ts: int = 0,
|
||||
original: bool = True,
|
||||
finder_raw: str | None = None,
|
||||
) -> dict:
|
||||
now_ts = int(time.time())
|
||||
rkb = _payload_raw_key_buff(finder_raw)
|
||||
|
||||
payload = {
|
||||
"objectType": 0,
|
||||
@@ -460,7 +477,7 @@ async def create_post(
|
||||
"timestamp": str(int(time.time() * 1000)),
|
||||
"_log_finder_uin": "",
|
||||
"_log_finder_id": finder_id,
|
||||
"rawKeyBuff": None,
|
||||
"rawKeyBuff": rkb,
|
||||
"pluginSessionId": None,
|
||||
"scene": 7,
|
||||
"reqScene": 7,
|
||||
@@ -469,6 +486,13 @@ async def create_post(
|
||||
if scheduled_ts > 0:
|
||||
payload["postTimingInfo"] = {"timing": 1, "postTime": scheduled_ts}
|
||||
|
||||
if not rkb:
|
||||
print(
|
||||
" [!] 警告: Cookie 文件缺少 finder_raw,post_create 可能返回 300002;"
|
||||
"请用 channels_login 完整登录(勿提前关浏览器)。",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# 尝试 CGI_PREFIX(标准助手端点)和 MICRO_PREFIX 两个路径
|
||||
for prefix, referer in [
|
||||
(CGI_PREFIX, "https://channels.weixin.qq.com/platform/post/create"),
|
||||
@@ -517,6 +541,7 @@ async def publish_one(
|
||||
idx: int,
|
||||
total: int,
|
||||
scheduled_ts: int = 0,
|
||||
finder_raw: str | None = None,
|
||||
) -> PublishResult:
|
||||
fname = Path(video_path).name
|
||||
real_path = Path(video_path).resolve()
|
||||
@@ -611,6 +636,7 @@ async def publish_one(
|
||||
vinfo, fsize, finder_id, uin, finger_print, aid,
|
||||
clip_key, draft_id, upload_cost,
|
||||
scheduled_ts=scheduled_ts, original=True,
|
||||
finder_raw=finder_raw,
|
||||
)
|
||||
elapsed = time.time() - t0
|
||||
|
||||
@@ -680,6 +706,19 @@ async def _ensure_ctx() -> dict:
|
||||
if not up_params:
|
||||
raise RuntimeError("获取 upload_params 失败")
|
||||
|
||||
finder_raw_val = ls.get("finder_raw") or ""
|
||||
if isinstance(finder_raw_val, str):
|
||||
finder_raw_val = finder_raw_val.strip()
|
||||
else:
|
||||
finder_raw_val = str(finder_raw_val or "").strip()
|
||||
|
||||
if not finder_raw_val:
|
||||
raise RuntimeError(
|
||||
"localStorage 缺少 finder_raw(rawKeyBuff),post_create 会报 300002。"
|
||||
"请运行: python3 视频号发布/脚本/channels_login.py --playwright-only "
|
||||
"扫码登录后勿关 Chromium,直至终端出现「rawKeyBuff 已就绪」或「Cookie 已保存」。"
|
||||
)
|
||||
|
||||
_ctx.update({
|
||||
"ready": True,
|
||||
"cookie_str": cookie_str,
|
||||
@@ -688,6 +727,7 @@ async def _ensure_ctx() -> dict:
|
||||
"finger_print": finger_print,
|
||||
"aid": aid,
|
||||
"up_params": up_params,
|
||||
"finder_raw": finder_raw_val or None,
|
||||
})
|
||||
return _ctx
|
||||
|
||||
@@ -724,6 +764,7 @@ async def publish_one_compat(
|
||||
ctx["uin"], ctx["finger_print"], ctx["aid"],
|
||||
ctx["up_params"], video_path, title, idx, total,
|
||||
scheduled_ts=sched_ts,
|
||||
finder_raw=ctx.get("finder_raw"),
|
||||
)
|
||||
if result.success:
|
||||
new_p = await get_upload_params(ctx["cookie_str"], ctx["finder_id"])
|
||||
@@ -736,7 +777,18 @@ async def publish_one_compat(
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _run_login_then_retry():
|
||||
def _env_truthy(name: str) -> bool:
|
||||
return os.environ.get(name, "").strip().lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _channels_auto_login_allowed() -> bool:
|
||||
"""默认静默;仅 CHANNELS_AUTO_LOGIN=1 且未设 NO_AUTO_CHANNELS_LOGIN 时才允许自动弹窗登录。"""
|
||||
if _env_truthy("NO_AUTO_CHANNELS_LOGIN"):
|
||||
return False
|
||||
return _env_truthy("CHANNELS_AUTO_LOGIN")
|
||||
|
||||
|
||||
def _run_login_then_retry() -> bool:
|
||||
"""Cookie 无效时自动调起登录,写回 storage 后由调用方重试。"""
|
||||
login_script = SCRIPT_DIR / "channels_login.py"
|
||||
if not login_script.exists():
|
||||
@@ -747,18 +799,18 @@ def _run_login_then_retry():
|
||||
return COOKIE_FILE.exists() and COOKIE_FILE.stat().st_size > 100
|
||||
|
||||
|
||||
def _gen_schedule(count: int) -> list[int]:
|
||||
"""生成定时发布时间戳列表:第一条立即(0),后续30-120分钟递增"""
|
||||
def _maybe_login_then_retry() -> bool:
|
||||
if not _channels_auto_login_allowed():
|
||||
return False
|
||||
return _run_login_then_retry()
|
||||
|
||||
|
||||
def _gen_schedule_unix(count: int) -> list[int]:
|
||||
"""与 distribute_all 一致:智能错峰 datetime → 视频号定时 Unix;近未来视为立即(0)。"""
|
||||
if count <= 0:
|
||||
return []
|
||||
result = [0]
|
||||
base = int(time.time())
|
||||
accumulated = 0
|
||||
for _ in range(1, count):
|
||||
gap = random.randint(30, 120) * 60
|
||||
accumulated += gap
|
||||
result.append(base + accumulated)
|
||||
return result
|
||||
times = generate_smart_schedule(count)
|
||||
return [_scheduled_ts_for_channels(dt) for dt in times]
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -766,10 +818,14 @@ async def main():
|
||||
|
||||
state = load_state()
|
||||
if not state:
|
||||
if _run_login_then_retry():
|
||||
if _maybe_login_then_retry():
|
||||
state = load_state()
|
||||
if not state:
|
||||
print("[!] Cookie 文件不存在且登录未完成", flush=True)
|
||||
print(
|
||||
"[!] Cookie 文件不存在。默认静默:请先手动执行同目录 channels_login.py,"
|
||||
"或设置 CHANNELS_AUTO_LOGIN=1 后再运行本脚本。",
|
||||
flush=True,
|
||||
)
|
||||
return 1
|
||||
|
||||
cookie_str = get_cookie_str(state)
|
||||
@@ -778,19 +834,22 @@ async def main():
|
||||
aid = ls.get("__ml::aid", "").strip('"')
|
||||
finger_print = ls.get("_finger_print_device_id", "")
|
||||
if not aid:
|
||||
if _run_login_then_retry():
|
||||
if _maybe_login_then_retry():
|
||||
state = load_state()
|
||||
cookie_str = get_cookie_str(state)
|
||||
ls = get_local_storage(state)
|
||||
aid = ls.get("__ml::aid", "").strip('"')
|
||||
finger_print = ls.get("_finger_print_device_id", "")
|
||||
if not aid:
|
||||
print("[!] localStorage 缺少 __ml::aid", flush=True)
|
||||
print(
|
||||
"[!] localStorage 缺少 __ml::aid(助手态不完整)。默认静默:请手动 channels_login 或 CHANNELS_AUTO_LOGIN=1。",
|
||||
flush=True,
|
||||
)
|
||||
return 1
|
||||
|
||||
auth = await auth_check(cookie_str)
|
||||
if not auth:
|
||||
if _run_login_then_retry():
|
||||
if _maybe_login_then_retry():
|
||||
state = load_state()
|
||||
cookie_str = get_cookie_str(state)
|
||||
ls = get_local_storage(state)
|
||||
@@ -798,7 +857,10 @@ async def main():
|
||||
finger_print = ls.get("_finger_print_device_id", "")
|
||||
auth = await auth_check(cookie_str)
|
||||
if not auth:
|
||||
print("[!] Cookie 仍无效,请稍后重试发布", flush=True)
|
||||
print(
|
||||
"[!] Cookie 仍无效。默认静默:请手动 channels_login 或设置 CHANNELS_AUTO_LOGIN=1 后重试。",
|
||||
flush=True,
|
||||
)
|
||||
return 1
|
||||
|
||||
fu = auth.get("finderUser", {})
|
||||
@@ -823,7 +885,7 @@ async def main():
|
||||
print("[OK] 全部已发布", flush=True)
|
||||
return 0
|
||||
|
||||
schedule = _gen_schedule(len(need_pub))
|
||||
schedule = _gen_schedule_unix(len(need_pub))
|
||||
|
||||
results = []
|
||||
consecutive_fail = 0
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""视频号登录 v7 — 优先 Cursor 内置 Simple Browser 扫码;会话落盘优先 CDP 附着 Cursor,失败再回退 Playwright。"""
|
||||
"""视频号登录 v7 — Simple Browser / CDP / Playwright。
|
||||
静默二维码:`CHANNELS_SILENT_QR=1` 或 `--silent-qr` → headless、不弹窗,二维码写入 /tmp/channels_qr.png,
|
||||
stdout 打印 SOUL_QR_IMAGE_FOR_CHAT 标记路径,便于在 Cursor 对话中由助手展示给你扫。
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
@@ -18,6 +21,10 @@ LOGIN_URL = "https://channels.weixin.qq.com/login"
|
||||
QR_SCREENSHOT = Path("/tmp/channels_qr.png")
|
||||
|
||||
DEFAULT_CDP = os.environ.get("CHANNELS_CDP_URL", "http://127.0.0.1:9223")
|
||||
# 持久化 Chromium:同目录保留登录态,显著减少重复扫码(腾讯侧过期/风控时仍需重登)
|
||||
PERSISTENT_PROFILE_DIR = Path(
|
||||
os.environ.get("CHANNELS_CHROMIUM_USER_DATA", str(Path.home() / ".soul-channels-playwright-profile"))
|
||||
)
|
||||
|
||||
UA = (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
@@ -30,6 +37,11 @@ REQUIRED_LS_KEYS = [
|
||||
]
|
||||
|
||||
|
||||
def _ls_ready_for_publish(merged_items: dict) -> bool:
|
||||
"""发表 API 需要 finder_raw;仅有 __ml::aid 不够(post_create 会 300002)。"""
|
||||
return bool((merged_items.get("finder_raw") or "").strip())
|
||||
|
||||
|
||||
def open_cursor_simple_browser(url: str) -> None:
|
||||
"""在 Cursor 编辑器内打开 Simple Browser(不唤起系统默认外部浏览器)。"""
|
||||
enc = urllib.parse.quote(url, safe="")
|
||||
@@ -74,6 +86,13 @@ async def extract_ls(page, keys):
|
||||
return {}
|
||||
|
||||
|
||||
def _use_persistent_chromium() -> bool:
|
||||
if "--no-persistent" in sys.argv:
|
||||
return False
|
||||
v = os.environ.get("CHANNELS_PERSISTENT_LOGIN", "1").strip().lower()
|
||||
return v not in ("0", "false", "no", "off")
|
||||
|
||||
|
||||
async def wait_until_logged_in(page, label: str = "") -> bool:
|
||||
prefix = f"[{label}] " if label else ""
|
||||
for i in range(120):
|
||||
@@ -117,20 +136,39 @@ async def save_session_from_context(context, page, ls_vals: dict) -> bool:
|
||||
COOKIE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2))
|
||||
_sync_to_central_cookie_store()
|
||||
|
||||
cookies = await context.cookies()
|
||||
cookie_dict = {c["name"]: c["value"] for c in cookies}
|
||||
cookie_dict: dict[str, str] = {}
|
||||
try:
|
||||
cookies = await context.cookies()
|
||||
cookie_dict = {c["name"]: c["value"] for c in cookies}
|
||||
except Exception:
|
||||
cookie_dict = {c["name"]: c["value"] for c in state.get("cookies", []) if c.get("name")}
|
||||
|
||||
page_url = ""
|
||||
try:
|
||||
page_url = page.url or ""
|
||||
except Exception:
|
||||
page_url = ""
|
||||
|
||||
token_data = {
|
||||
"sessionid": cookie_dict.get("sessionid", ""),
|
||||
"wxuin": cookie_dict.get("wxuin", ""),
|
||||
"cookie_str": "; ".join(f'{c["name"]}={c["value"]}' for c in cookies),
|
||||
"cookie_str": "; ".join(f"{k}={v}" for k, v in cookie_dict.items()),
|
||||
"finder_raw": ls_vals.get("finder_raw", ""),
|
||||
"finder_username": ls_vals.get("finder_username", ""),
|
||||
"finder_uin": ls_vals.get("finder_uin", ""),
|
||||
"finder_login_token": ls_vals.get("finder_login_token", ""),
|
||||
"url": page.url,
|
||||
"url": page_url,
|
||||
}
|
||||
TOKEN_FILE.write_text(json.dumps(token_data, ensure_ascii=False, indent=2))
|
||||
return bool(ls_vals.get("finder_raw"))
|
||||
|
||||
ready = _ls_ready_for_publish(merged_items)
|
||||
if not ready:
|
||||
print(
|
||||
"[!] 已写入 Cookie,但 localStorage 仍缺 finder_raw(rawKeyBuff),"
|
||||
"勿关窗口,继续等待直至本项为 ✓。",
|
||||
flush=True,
|
||||
)
|
||||
return ready
|
||||
except Exception as e:
|
||||
print(f"[!] 保存会话异常: {e}", flush=True)
|
||||
return False
|
||||
@@ -210,72 +248,167 @@ async def try_capture_via_cdp(pw, cdp_url: str) -> bool:
|
||||
return ok
|
||||
|
||||
|
||||
async def capture_via_playwright_external() -> bool:
|
||||
"""回退:独立 Chromium 窗口(仅当 CDP 不可用时)。"""
|
||||
print("\n[i] 未使用 CDP → 将打开本机 Chromium 窗口仅用于写入 Cookie 文件(可扫码后尽快关闭)\n", flush=True)
|
||||
ls_vals = {}
|
||||
def _silent_qr_mode() -> bool:
|
||||
return "--silent-qr" in sys.argv or os.environ.get("CHANNELS_SILENT_QR", "").strip().lower() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
)
|
||||
|
||||
|
||||
def _print_qr_for_chat_dialog() -> None:
|
||||
""" stdout 标记:便于把路径复制到对话,由助手 Read 图片展示给你扫。"""
|
||||
p = QR_SCREENSHOT.resolve()
|
||||
print("\n========== SOUL_QR_IMAGE_FOR_CHAT ==========", flush=True)
|
||||
print(str(p), flush=True)
|
||||
print(
|
||||
"→ 用微信扫视频号登录码:在 Cursor 里打开上述路径图片,或把路径发给助手展示。",
|
||||
flush=True,
|
||||
)
|
||||
print("========== END_SOUL_QR_MARKER ==========\n", flush=True)
|
||||
|
||||
|
||||
async def _assistant_login_flow_save(context, page) -> bool:
|
||||
"""在已有 context/page 上完成:打开登录页 → 等跳转 → 抽 localStorage → 写 COOKIE_FILE。"""
|
||||
ls_vals: dict = {}
|
||||
await page.goto(LOGIN_URL, timeout=60000)
|
||||
await asyncio.sleep(3)
|
||||
await page.screenshot(path=str(QR_SCREENSHOT))
|
||||
print(f"[QR] 二维码截图: {QR_SCREENSHOT}", flush=True)
|
||||
if _silent_qr_mode():
|
||||
_print_qr_for_chat_dialog()
|
||||
|
||||
if not await wait_until_logged_in(page, "Playwright"):
|
||||
return False
|
||||
|
||||
print(
|
||||
"[!] 请勿立即关闭 Chromium:须等脚本写入 Cookie;"
|
||||
"看到「Cookie 已保存」或本进程退出后再关窗。",
|
||||
flush=True,
|
||||
)
|
||||
print("[i] 登录成功,2 秒后先落盘一轮会话(防止提前关窗导致 Cookie 未写入)…", flush=True)
|
||||
await asyncio.sleep(2)
|
||||
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
|
||||
snapshot_ready = await save_session_from_context(context, page, ls_vals)
|
||||
if snapshot_ready:
|
||||
print("[✓] 早期快照已含上传所需 localStorage 关键项", flush=True)
|
||||
elif COOKIE_FILE.exists():
|
||||
print("[i] 已写入部分会话,将继续等待 rawKeyBuff / __ml::aid 补全…", flush=True)
|
||||
|
||||
print("[i] 等待平台 JS 加载完成...", flush=True)
|
||||
await asyncio.sleep(10)
|
||||
|
||||
for attempt in range(60):
|
||||
try:
|
||||
_ = page.url
|
||||
except Exception:
|
||||
print("[!] 页面或浏览器已关闭,使用此前快照结束。", flush=True)
|
||||
return snapshot_ready
|
||||
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
|
||||
if ls_vals.get("finder_raw"):
|
||||
print(f"[✓] rawKeyBuff 已就绪 (等待 {attempt}s)", flush=True)
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
if attempt % 15 == 14:
|
||||
print(f" 等待 localStorage 写入... ({attempt + 1}s)", flush=True)
|
||||
|
||||
if not ls_vals.get("finder_raw"):
|
||||
print("[i] rawKeyBuff 未出现,尝试访问内容列表页...", flush=True)
|
||||
try:
|
||||
await page.goto(
|
||||
"https://channels.weixin.qq.com/platform/post/list",
|
||||
timeout=15000,
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
await asyncio.sleep(8)
|
||||
for _ in range(30):
|
||||
try:
|
||||
_ = page.url
|
||||
except Exception:
|
||||
return snapshot_ready
|
||||
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
|
||||
if ls_vals.get("finder_raw"):
|
||||
print("[✓] 导航后 rawKeyBuff 已就绪", flush=True)
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
print(f"[!] 导航异常: {e}", flush=True)
|
||||
|
||||
print("\n[i] localStorage 提取结果:", flush=True)
|
||||
for k in REQUIRED_LS_KEYS:
|
||||
v = ls_vals.get(k, "")
|
||||
status = "✓" if v else "✗"
|
||||
display = f"{v[:60]}..." if len(v) > 60 else (v or "(空)")
|
||||
print(f" {status} {k}: {display}", flush=True)
|
||||
|
||||
final_ok = await save_session_from_context(context, page, ls_vals)
|
||||
return final_ok or snapshot_ready
|
||||
|
||||
|
||||
async def capture_via_playwright_external(*, headless: bool) -> bool:
|
||||
"""CDP 不可用时:本机 Chromium;headless=True 时不弹窗口(配合 CHANNELS_SILENT_QR 输出二维码路径)。"""
|
||||
persistent = _use_persistent_chromium()
|
||||
if persistent:
|
||||
print(
|
||||
f"\n[i] 持久化 Chromium(headless={headless}),目录: {PERSISTENT_PROFILE_DIR}\n"
|
||||
f" 关闭持久化:CHANNELS_PERSISTENT_LOGIN=0 或 --no-persistent\n",
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
print(f"\n[i] 临时 Chromium 会话(headless={headless})\n", flush=True)
|
||||
|
||||
async with async_playwright() as pw:
|
||||
browser = await pw.chromium.launch(headless=False)
|
||||
if persistent:
|
||||
PERSISTENT_PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
context = await pw.chromium.launch_persistent_context(
|
||||
str(PERSISTENT_PROFILE_DIR),
|
||||
headless=headless,
|
||||
user_agent=UA,
|
||||
viewport={"width": 1280, "height": 720},
|
||||
args=["--disable-blink-features=AutomationControlled"],
|
||||
)
|
||||
await context.add_init_script(
|
||||
"Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
|
||||
)
|
||||
page = context.pages[0] if context.pages else await context.new_page()
|
||||
try:
|
||||
ok = await _assistant_login_flow_save(context, page)
|
||||
finally:
|
||||
await context.close()
|
||||
return ok
|
||||
|
||||
browser = await pw.chromium.launch(
|
||||
headless=headless,
|
||||
args=["--disable-blink-features=AutomationControlled"],
|
||||
)
|
||||
context = await browser.new_context(user_agent=UA, viewport={"width": 1280, "height": 720})
|
||||
await context.add_init_script(
|
||||
"Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
|
||||
)
|
||||
page = await context.new_page()
|
||||
await page.goto(LOGIN_URL, timeout=60000)
|
||||
await asyncio.sleep(3)
|
||||
await page.screenshot(path=str(QR_SCREENSHOT))
|
||||
print(f"[QR] 二维码截图: {QR_SCREENSHOT}", flush=True)
|
||||
|
||||
if not await wait_until_logged_in(page, "Playwright"):
|
||||
try:
|
||||
ok = await _assistant_login_flow_save(context, page)
|
||||
finally:
|
||||
await browser.close()
|
||||
return False
|
||||
|
||||
print("[i] 等待平台 JS 加载完成...", flush=True)
|
||||
await asyncio.sleep(10)
|
||||
|
||||
for attempt in range(60):
|
||||
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
|
||||
if ls_vals.get("finder_raw"):
|
||||
print(f"[✓] rawKeyBuff 已就绪 (等待 {attempt}s)", flush=True)
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
if attempt % 15 == 14:
|
||||
print(f" 等待 localStorage 写入... ({attempt + 1}s)", flush=True)
|
||||
|
||||
if not ls_vals.get("finder_raw"):
|
||||
print("[i] rawKeyBuff 未出现,尝试访问内容列表页...", flush=True)
|
||||
try:
|
||||
await page.goto(
|
||||
"https://channels.weixin.qq.com/platform/post/list",
|
||||
timeout=15000,
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
await asyncio.sleep(8)
|
||||
for _ in range(30):
|
||||
ls_vals = await extract_ls(page, REQUIRED_LS_KEYS)
|
||||
if ls_vals.get("finder_raw"):
|
||||
print("[✓] 导航后 rawKeyBuff 已就绪", flush=True)
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
print(f"[!] 导航异常: {e}", flush=True)
|
||||
|
||||
print("\n[i] localStorage 提取结果:", flush=True)
|
||||
for k in REQUIRED_LS_KEYS:
|
||||
v = ls_vals.get(k, "")
|
||||
status = "✓" if v else "✗"
|
||||
display = f"{v[:60]}..." if len(v) > 60 else (v or "(空)")
|
||||
print(f" {status} {k}: {display}", flush=True)
|
||||
|
||||
ok = await save_session_from_context(context, page, ls_vals)
|
||||
await browser.close()
|
||||
return ok
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
# --playwright-only:直接弹 Chromium 窗口扫码(与 headless / Simple Browser 分流)
|
||||
force_pw = "--playwright-only" in sys.argv or os.environ.get("CHANNELS_LOGIN_PLAYWRIGHT_ONLY")
|
||||
skip_cursor_tab = "--no-cursor-tab" in sys.argv
|
||||
cdp_url = DEFAULT_CDP
|
||||
silent_qr = _silent_qr_mode()
|
||||
|
||||
if silent_qr:
|
||||
print(
|
||||
"[i] 静默扫码模式(CHANNELS_SILENT_QR=1 或 --silent-qr):"
|
||||
"不打开 Simple Browser / 不弹 Chromium 窗口,二维码见标记路径。\n",
|
||||
flush=True,
|
||||
)
|
||||
ok = await capture_via_playwright_external(headless=True)
|
||||
print(f"\n[{'✓' if ok else '✗'}] Cookie 保存: {COOKIE_FILE}", flush=True)
|
||||
return 0 if ok else 1
|
||||
|
||||
if not force_pw:
|
||||
if not skip_cursor_tab:
|
||||
@@ -303,7 +436,7 @@ async def main() -> int:
|
||||
print(f"\n[✓] Cookie 已保存: {COOKIE_FILE}", flush=True)
|
||||
return 0
|
||||
|
||||
ok = await capture_via_playwright_external()
|
||||
ok = await capture_via_playwright_external(headless=False)
|
||||
print(f"\n[{'✓' if ok else '✗'}] Cookie 保存: {COOKIE_FILE}", flush=True)
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
40
03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_open_platform_publish.py
Normal file
40
03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_open_platform_publish.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
视频号「开放平台 access_token」能否上传发表短视频?——不能(截至官方文档现状)。
|
||||
|
||||
官方「视频号助手 API」列表:
|
||||
https://developers.weixin.qq.com/doc/channels/api/channels/index.html
|
||||
仅含:直播记录/预约、橱窗、留资、大屏、罗盘等,**无**短视频上传/发表接口。
|
||||
|
||||
因此:
|
||||
- 无法用 AppID/AppSecret 换得的 access_token 替代 channels_storage_state.json(助手 Cookie + localStorage)
|
||||
- 自动化发片仍须走 channels_api_publish.py(助手 Web 协议)
|
||||
|
||||
若微信未来在文档中新增「内容上传」类接口,请在此模块实现并与 distribute_all 对接。
|
||||
|
||||
缓解重复扫码:见 channels_login.py 持久化 Chromium(CHANNELS_PERSISTENT_LOGIN 默认开启)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class OpenPlatformShortVideoNotSupported(RuntimeError):
|
||||
"""微信未开放助手 API + access_token 的短视频发表能力。"""
|
||||
|
||||
DOC = "https://developers.weixin.qq.com/doc/channels/api/channels/index.html"
|
||||
LOCAL = "视频号发布/REFERENCE_开放能力_数据与集成.md"
|
||||
|
||||
|
||||
def assert_can_publish_without_assistant_session() -> None:
|
||||
"""占位:若误调用「无 Cookie 发片」入口,立即给出明确错误。"""
|
||||
raise OpenPlatformShortVideoNotSupported(
|
||||
"微信未提供「仅用 access_token、无需助手 Cookie」的短视频上传/发表官方接口;"
|
||||
f"见 {OpenPlatformShortVideoNotSupported.DOC} 与仓库内 {OpenPlatformShortVideoNotSupported.LOCAL}。"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
assert_can_publish_without_assistant_session()
|
||||
except OpenPlatformShortVideoNotSupported as e:
|
||||
print(e, flush=True)
|
||||
raise SystemExit(2) from e
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sessionid": "BgAAO0pkRXR1Cnpkg8qKYYMziAr6J6R%2FMVqC2Jat0R1fmRXawRqzqlxIdOy47RaOqNJ8YCfDBsY9VFSx0UMt4JmiGxqBL6Wa6v0LDXbmykU%3D",
|
||||
"wxuin": "1519919758",
|
||||
"cookie_str": "sessionid=BgAAO0pkRXR1Cnpkg8qKYYMziAr6J6R%2FMVqC2Jat0R1fmRXawRqzqlxIdOy47RaOqNJ8YCfDBsY9VFSx0UMt4JmiGxqBL6Wa6v0LDXbmykU%3D; wxuin=1519919758",
|
||||
"sessionid": "BgAANwiqhAKk98isXZLPiw2icF1oS7Vpofo9%2FFNByPRqbhQJqyNGTUHji1WoozeYLHGzcAKaWZZYCf2AF88%2FpVZsF3XuTF9TVnLk%2Bi9kXac%3D",
|
||||
"wxuin": "1187669785",
|
||||
"cookie_str": "sessionid=BgAANwiqhAKk98isXZLPiw2icF1oS7Vpofo9%2FFNByPRqbhQJqyNGTUHji1WoozeYLHGzcAKaWZZYCf2AF88%2FpVZsF3XuTF9TVnLk%2Bi9kXac%3D; wxuin=1187669785",
|
||||
"finder_raw": "",
|
||||
"finder_username": "v2_060000231003b20faec8c5e48919cbd5cb05e53db077dd1924028a806c10cffd891eb5a80ce7@finder",
|
||||
"finder_uin": "",
|
||||
|
||||
@@ -421,3 +421,4 @@
|
||||
| 2026-03-22 13:23:40 | 🔄 卡若AI 同步 2026-03-22 13:23 | 更新:金仓、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 2026-03-22 14:38:11 | 🔄 卡若AI 同步 2026-03-22 14:38 | 更新:Cursor规则、金仓、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 2026-03-22 21:22:06 | 🔄 卡若AI 同步 2026-03-22 21:22 | 更新:Cursor规则、金仓、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
| 2026-03-23 09:48:42 | [强制] 🔄 卡若AI 同步 2026-03-23 09:48 | 更新:Cursor规则、金仓、水桥平台对接、卡木、火炬、运营中枢、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
|
||||
@@ -424,3 +424,4 @@
|
||||
| 2026-03-22 13:23:40 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 13:23 | 更新:金仓、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-22 14:38:11 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 14:38 | 更新:Cursor规则、金仓、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-22 21:22:06 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-22 21:22 | 更新:Cursor规则、金仓、卡木、总索引与入口、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-23 09:48:42 | 成功(强制) | 成功 | 🔄 卡若AI 同步 2026-03-23 09:48 | 更新:Cursor规则、金仓、水桥平台对接、卡木、火炬、运营中枢、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
Reference in New Issue
Block a user