From 42806248107a982d2753724c4f4bbc62289f3f33 Mon Sep 17 00:00:00 2001 From: kang Date: Mon, 18 May 2026 16:35:29 +0800 Subject: [PATCH] feat: support tiktok download cookies --- .memory/status.md | 3 ++ RULES.md | 1 + api/.env.example | 2 ++ api/main.py | 39 ++++++++++++++++++++++++-- deploy/.env.production.example | 4 +++ docs/source-analysis.html | 18 ++++++++++-- web/app/page.tsx | 4 +++ web/components/ad-recreation-board.tsx | 8 ++++++ web/lib/api.ts | 19 +++++++++++++ 9 files changed, 92 insertions(+), 6 deletions(-) diff --git a/.memory/status.md b/.memory/status.md index 7657eda..0f85d6a 100644 --- a/.memory/status.md +++ b/.memory/status.md @@ -17,6 +17,7 @@ | 任务 | 当前模型 / 通道 | 备注 | |---|---|---| +| TK 下载 | `yt-dlp` + 可选 cookies | 公开视频裸下载;受限视频可配 `YTDLP_COOKIES_FILE` 或 `YTDLP_COOKIES_FROM_BROWSER`,也可直接上传 MP4。 | | 远端 ASR | `ASR_MODEL=whisper-1` | 失败后进本机 ASR,再进多模态兜底。 | | 本机 ASR | `LOCAL_ASR_MODEL=mlx-community/whisper-tiny` | 默认二级兜底,优先产出真实逐句时间轴。 | | ASR 兜底 / 音频分析 | `ASR_FALLBACK_MODEL=gemini-2.5-flash` | 多模态音频兜底;后端会拒绝假字幕、重复文本和覆盖率过低结果。 | @@ -94,8 +95,10 @@ POST /jobs/{id}/frames/{idx}/storyboard/video 9. 文档是顶层业务归类:每个 TK 链接或上传视频默认一个 `document`,`job` 归属到 `document_id`;DB 存元数据和文件索引,视频 / 图片 / 音频文件不进 DB。 10. 后端长任务不要用 `--reload`。 11. 关键帧 `index` 是稳定 ID,不等于数组下标;前端取帧用 `frames.find(x => x.index === idx)`。 +12. TikTok cookies 属于账号登录态,只能放本机 / 服务器私有环境;不要提交 cookies 文件或账号密码。 ## 最近变更 +- 2026-05-18:TK 链接下载新增 `YTDLP_COOKIES_FILE` / `YTDLP_COOKIES_FROM_BROWSER` 支持;受限视频失败时前端提示上传 MP4 或配置后端 cookies 登录态。 - 2026-05-18:清理个人语音通道残留,`/health`、前端类型、环境模板和文档不再暴露相关字段或配置。 - 2026-05-18:新增后端数据库层,SQLite 默认落在 `APP_DB_URL` / `DATABASE_URL` 或 `JOBS_DIR/app.db`;`/documents` 返回文档归类列表,`/health.database` 返回 DB 状态。 - 2026-05-18:`VISION_MODEL`、`REWRITE_MODEL`、`AUDIO_REWRITE_MODEL` 切到 GPT 默认模型 `gpt-4o`,并加旧 Gemini 环境变量归一化保护。 diff --git a/RULES.md b/RULES.md index a4e6bbd..08978a3 100644 --- a/RULES.md +++ b/RULES.md @@ -65,6 +65,7 @@ - `IMAGE_BASE_URL` / `IMAGE_API_KEY` / `IMAGE_MODEL`:OpenAI 兼容生图网关;当前所有生图入口一律强制使用 `gpt-image-2`,不做其他图片模型 fallback - `GPT_IMAGE_MODEL` / `SUBJECT_ASSET_IMAGE_MODEL` / `SUBJECT_ASSET_IMAGE_MODELS`:保留兼容旧环境变量名,但服务端会强制主体 6 视图和所有其他生图入口都只使用 `gpt-image-2` - `AI_HTTP_PROXY` / `IMAGE_HTTP_PROXY`:可选的 AI 网关出站代理;本地 launchd 后台进程不一定继承 shell 的 `http_proxy/https_proxy`,如生图报 DNS / ConnectError,可在本地 `api/.env` 配置后重启后端。`/health` 只回传是否配置代理,不回传代理地址。 +- `YTDLP_COOKIES_FILE` / `YTDLP_COOKIES_FROM_BROWSER`:可选 TikTok 下载登录态;优先使用 cookies 文件,其次读取本机浏览器 cookies。cookies 文件属于敏感登录态,只能放本机或服务器私有路径,不允许入库。 - `VOICE_PROVIDER`:配音通道,服务端固定使用 `azure_openai` - `AZURE_OPENAI_BASE_URL` / `AZURE_OPENAI_API_KEY`:微软 Azure OpenAI 协议配音网关;本地未单独配置 Key 时回退复用 `LLM_API_KEY` - `AZURE_TTS_MODEL` / `AZURE_TTS_VOICE_ID` / `AZURE_TTS_VOICE_POOL` / `AZURE_TTS_PATH` / `AZURE_TTS_PATHS`:Azure OpenAI TTS 模型、默认音色、音色池和 OpenAI 协议语音路径;后端会按 `AZURE_TTS_PATHS` 依次尝试,便于区分路径不对和整条语音服务不可用 diff --git a/api/.env.example b/api/.env.example index e480e98..afee6f5 100644 --- a/api/.env.example +++ b/api/.env.example @@ -29,6 +29,8 @@ SUBJECT_ASSET_IMAGE_MODEL=gpt-image-2 SUBJECT_ASSET_IMAGE_MODELS=gpt-image-2 # 可选:本地网络需要代理访问 ai.skg.com 时配置;launchd 不一定继承 shell 代理变量。 AI_HTTP_PROXY= +YTDLP_COOKIES_FILE= +YTDLP_COOKIES_FROM_BROWSER= VIDEO_MODEL=seedance VIDEO_MODEL_SEEDANCE=seedance-2-fast VIDEO_MODEL_KLING=kling-omni diff --git a/api/main.py b/api/main.py index 3b9d4be..582f4a8 100644 --- a/api/main.py +++ b/api/main.py @@ -92,6 +92,8 @@ PRODUCT_ASSET_MIN_LONG_SIDE = max(512, int(os.getenv("PRODUCT_ASSET_MIN_LONG_SID PRODUCT_ASSET_MIN_SHORT_SIDE = max(320, int(os.getenv("PRODUCT_ASSET_MIN_SHORT_SIDE", "600"))) PRODUCT_ASSET_JPEG_QUALITY = max(80, min(95, int(os.getenv("PRODUCT_ASSET_JPEG_QUALITY", "92")))) VIDEO_MODEL = os.getenv("VIDEO_MODEL", "seedance").strip() or "seedance" +YTDLP_COOKIES_FILE = os.getenv("YTDLP_COOKIES_FILE", "").strip() +YTDLP_COOKIES_FROM_BROWSER = os.getenv("YTDLP_COOKIES_FROM_BROWSER", "").strip() AUDIO_PRODUCT_BRIEF = os.getenv( "AUDIO_PRODUCT_BRIEF", "SKG 智能按摩产品,主打日常肩颈、腰背、眼部、膝盖或足部放松;广告表达要高级、干净、可信,不做医疗疗效承诺。", @@ -1031,6 +1033,35 @@ def run(cmd: list[str], cwd: Path | None = None) -> str: return res.stdout +def ytdlp_cookie_args() -> list[str]: + if YTDLP_COOKIES_FILE: + cookies = Path(YTDLP_COOKIES_FILE).expanduser() + if not cookies.exists(): + raise RuntimeError("TikTok cookies 文件不可用,请检查 YTDLP_COOKIES_FILE 配置。") + return ["--cookies", str(cookies)] + if YTDLP_COOKIES_FROM_BROWSER: + return ["--cookies-from-browser", YTDLP_COOKIES_FROM_BROWSER] + return [] + + +def normalize_download_error(error: Exception) -> str: + raw = str(error) + lower = raw.lower() + auth_required = ( + "log in for access" in lower + or "login" in lower and "cookies" in lower + or "cookies-from-browser" in lower + or "sign in" in lower and "tiktok" in lower + ) + if auth_required: + return ( + "TikTok 下载需要登录态。请上传视频文件,或在后端配置 " + "YTDLP_COOKIES_FILE / YTDLP_COOKIES_FROM_BROWSER 后重试。" + f"原始错误:{raw}" + ) + return raw + + # ---- 启发式选帧工具 ---- import imagehash import numpy as np @@ -1696,13 +1727,15 @@ def pipeline_download(job_id: str) -> None: update(job, status="downloading", message="本地上传 · 跳过下载", progress=15) else: update(job, status="downloading", message="yt-dlp 下载中…", progress=5) - run([ + cmd = [ "yt-dlp", "-f", "best[ext=mp4]/best", "-o", str(mp4), "--no-warnings", "--no-playlist", "--retries", "3", + *ytdlp_cookie_args(), job.url, - ]) + ] + run(cmd) if not mp4.exists(): raise RuntimeError("下载完成但找不到 source.mp4") @@ -1725,7 +1758,7 @@ def pipeline_download(job_id: str) -> None: ) except Exception as e: message = "视频元数据解析失败" if stage == "metadata" else "下载失败" - update(job, status="failed", error=str(e), message=message) + update(job, status="failed", error=normalize_download_error(e), message=message) def pipeline_analyze( diff --git a/deploy/.env.production.example b/deploy/.env.production.example index 6793290..3dc1b87 100644 --- a/deploy/.env.production.example +++ b/deploy/.env.production.example @@ -36,6 +36,10 @@ SUBJECT_ASSET_IMAGE_MODELS=gpt-image-2 # Optional outbound proxy for AI gateway calls. Leave blank on normal VPS networking. AI_HTTP_PROXY= +# Optional TikTok download login state for yt-dlp. Keep cookies files private. +YTDLP_COOKIES_FILE= +YTDLP_COOKIES_FROM_BROWSER= + # Audio rewrite and Azure OpenAI TTS AUDIO_REWRITE_MODEL=gpt-4o AUDIO_PRODUCT_BRIEF="SKG smart massage products for daily neck, shoulder, back, eye, knee, and foot relaxation. Keep claims premium, clean, credible, and non-medical." diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 6192a85..e6bee04 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -572,7 +572,7 @@

当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧是信息流复刻工作表。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动音频文案路和视频视觉路。音频文案路提取原音频文案/字幕,分析讲话人、语速节奏、背景音乐/环境声/音效,并为后续新口播和分镜文案提供时间轴;视频视觉路同步抽取参考帧,参考帧只用于人工选择主体并生成相似主体白底视图。产品图上传后独立形成产品资产包:自动识别视角、左右/上下/内外侧、结构点、比例和风险,并补缺角度。最终分镜规划按逐句时间轴把文案、相似主体资产和产品资产汇合;当前暂停直接调视频模型,先逐条生成并审核首帧/尾帧,确认后再决定哪些分镜进入视频候选。

1

导入素材

粘贴 TK / 信息流视频链接或上传本地视频;“开始”只把任务放入第一步队列。

-
2

下载源视频

后端用 yt-dlp 或本地上传文件落 source.mp4,记录时长、尺寸和视频只读地址。

+
2

下载源视频

后端用 yt-dlp 或本地上传文件落 source.mp4,记录时长、尺寸和视频只读地址;TikTok 受限视频可通过 YTDLP_COOKIES_FILEYTDLP_COOKIES_FROM_BROWSER 提供登录态。

3

并行素材分析

下载完成后前端同时触发 triggerTranscribeanalyzeJob:音频路生成字幕/节奏/背景音,视觉路自动抽 6 张人物随机参考帧。

4

资产包准备

用户可删除/补选参考帧并生成相似主体视图;参考帧到这里为止,后续首尾帧和视频不再把原关键帧当画面真源。产品图上传后自动识别视角、结构和风险,并补缺角度,形成产品资产包。

5

首尾帧闸门

按逐句时间轴生成竖向分镜行;每行先规划镜头类型、人物描述、是否需要人物/产品、首帧、尾帧和产品出现方式,再按需求从相似主体 6/10 视图里选择合适人物视角,并和产品素材一起生成首尾帧,先看图确认,不直接批量提交视频。

@@ -611,7 +611,7 @@

后端核心

- + @@ -912,7 +912,7 @@ ProductRefStateItem { - + @@ -1044,6 +1044,18 @@ ProductRefStateItem {

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-18 · TK 受限视频下载支持 cookies 登录态

+ API + UI +
+
+

问题:部分 TikTok 链接会返回“Log in for access”,后端已经接到任务并创建 job,但裸 yt-dlp 下载没有登录态,任务只能失败。

+

改动:api/main.py 新增 YTDLP_COOKIES_FILEYTDLP_COOKIES_FROM_BROWSER,下载时优先传 --cookies,其次传 --cookies-from-browser。TikTok 登录态错误会归一化成“上传 MP4 或配置后端 cookies 后重试”。web/lib/api.ts 新增失败文案格式化,web/app/page.tsx 在任务失败时弹 toast,web/components/ad-recreation-board.tsx 在素材卡显示受限下载提示。

+

影响:不要在前端接 TikTok 账号密码或登录弹窗;cookies 文件属于敏感登录态,只能放本机或服务器私有路径。普通公开视频仍裸下载,受限视频可上传 MP4 兜底。

+
+

2026-05-18 · 删除个人语音通道残留

diff --git a/web/app/page.tsx b/web/app/page.tsx index ba51654..80f70db 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -19,6 +19,7 @@ import { AdRecreationBoard } from "@/components/ad-recreation-board" import { addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteJob, deleteFrame, deleteGeneratedImage, deleteGeneratedVideo, deleteCutout, generateStoryboardVideo, triggerTranscribe, describeFrame, updateStoryboard, copyProductLibraryAsset, + formatJobError, type Job, type ImageRef, type KeyFrame, type ProductFusionShot, type StoryboardScene, type FrameExtractMode, type FrameExtractQuality, type FrameExtractTarget, } from "@/lib/api" import { TRANSPARENT_HUMAN_NEGATIVE_PROMPT, TRANSPARENT_HUMAN_VIDEO_PROMPT } from "@/lib/workflow-target" @@ -864,6 +865,9 @@ export default function Home() { if (job?.status === "downloaded" && prevStatusRef.current !== "downloaded") { toast.info("视频已下载,音频解析会自动开始;也可以在右侧手动重试", { duration: 6000 }) } + if (job?.status === "failed" && prevStatusRef.current !== "failed") { + toast.error(formatJobError(job.error) || "任务失败", { duration: 10000 }) + } prevStatusRef.current = job?.status ?? null const TERMINAL: Job["status"][] = ["downloaded", "frames_extracted", "transcribed", "failed"] diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index 0011b2b..bb14014 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -32,6 +32,7 @@ import { cutoutElement, deleteSubjectAsset, effectiveFrameUrl, + formatJobError, generateSceneAsset, generateProductAngleAsset, generateSubjectAssets, @@ -3859,6 +3860,7 @@ function MaterialCard({ onDelete?: () => void }) { const tone = statusTone(job) + const errorText = formatJobError(job.error) return (
+ {job.status === "failed" && errorText && ( +
+ + {errorText} +
+ )} {onDelete && ( { const res = await fetch(`${API_BASE}/health`) if (!res.ok) throw new Error(`health ${res.status}`)
api/main.pyFastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回,并在保存 state.json 时同步数据库元数据。
api/main.pyFastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回,并在保存 state.json 时同步数据库元数据。TK 下载使用 yt-dlp,支持 YTDLP_COOKIES_FILE / YTDLP_COOKIES_FROM_BROWSER 给受限视频提供登录态。
api/database.py后端数据库层:当前内置 SQLite,维护 documentsjobsmedia_assets 三类元数据;文档是顶层归类,一条 TK 链接或一次上传默认一个 document,媒体文件仍保留在 jobs/<jobId>/
api/product_library/skg-products内置 SKG 白底产品图库:manifest.json 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,images/ 存 45 张参考图。
api/character_library/skg-characters内置相似主体形象库:从桌面 5 套策划形象导入,manifest.json 记录运动阳光男、都市型男、优雅白领女、运动辣妹、绅士大叔,每套含 7 张透明骨架参考图,用于相似主体高清视图包的创意方向选择。
运行配置 / 模型标注GET /healthgetRuntimeHealthModelTrace返回 models:ASR、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 product_view、GPT 图像模型、主体 6 视图 GPT 图像模型、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 REWRITE_MODELAUDIO_REWRITE_MODELVISION_MODEL 默认使用 gpt-4o;如果旧环境变量仍写 gemini-*,后端会归一化回 GPT_TEXT_MODEL / REWRITE_MODEL。语音只走 Azure OpenAI TTS,models.voice_tts_paths 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。database 回传 DB 是否启用、URL、schema 版本和 document/job/asset 计数。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。
文档列表GET /documents后续文档侧栏 / 素材库入口从数据库读取 document 归类列表,包含 source_kind、workflow_mode、primary_job_id、storage_prefix、job_count、asset_count 和更新时间。当前前端还未接主入口,后端已可作为多视频/多上传文档管理的索引。
历史列表GET /jobslistJobs所有 job 精简列表(id/document_id/source_kind/workflow_mode/url/status/thumbnail/mtime…),按 state.json mtime 倒序。前端 URL 无 ?job= 时拉它回填全部历史;带 limit 可截断。
创建任务POST /jobscreateJob提交 TK 链接,后台开始下载;后端自动建立 document_id=job_idsource_kind=tiktok_linkworkflow_mode=feed_recreation 的 document,并在状态保存时同步 DB。
创建任务POST /jobscreateJob提交 TK 链接,后台开始下载;后端自动建立 document_id=job_idsource_kind=tiktok_linkworkflow_mode=feed_recreation 的 document,并在状态保存时同步 DB。下载阶段优先使用 YTDLP_COOKIES_FILE,其次使用 YTDLP_COOKIES_FROM_BROWSER,TikTok 要求登录态时会把错误归一化为“上传 MP4 或配置后端 cookies”。
上传视频POST /jobs/uploaduploadJob保存 source.mp4,然后同样进入下载完成状态;后端自动建立 source_kind=uploadworkflow_mode=uploaded_reference 的 document。当前上传后也加入第一步队列,下载完成后自动解析音频。
删除输入视频DELETE /jobs/{id}deleteJob从任务队列、URL 和磁盘 jobs/<id> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。
解析视频POST /jobs/{id}/analyze?frames=&target=&mode=&quality=analyzeJob后续阶段保留的抽帧能力。默认 frames=6target 支持人物随机、透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。当前“开始分析”和源视频旁的“自动抽帧 6 张”都会显式用 target=random_subjectquality=accuratemode=replace 生成清晰人物候选里的随机参考帧池。