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_FILE 或 YTDLP_COOKIES_FROM_BROWSER 提供登录态。
3
并行素材分析
下载完成后前端同时触发 triggerTranscribe 和 analyzeJob:音频路生成字幕/节奏/背景音,视觉路自动抽 6 张人物随机参考帧。
4
资产包准备
用户可删除/补选参考帧并生成相似主体视图;参考帧到这里为止,后续首尾帧和视频不再把原关键帧当画面真源。产品图上传后自动识别视角、结构和风险,并补缺角度,形成产品资产包。
5
首尾帧闸门
按逐句时间轴生成竖向分镜行;每行先规划镜头类型、人物描述、是否需要人物/产品、首帧、尾帧和产品出现方式,再按需求从相似主体 6/10 视图里选择合适人物视角,并和产品素材一起生成首尾帧,先看图确认,不直接批量提交视频。
@@ -611,7 +611,7 @@
后端核心
- api/main.py | FastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回,并在保存 state.json 时同步数据库元数据。 |
+ api/main.py | FastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回,并在保存 state.json 时同步数据库元数据。TK 下载使用 yt-dlp,支持 YTDLP_COOKIES_FILE / YTDLP_COOKIES_FROM_BROWSER 给受限视频提供登录态。 |
api/database.py | 后端数据库层:当前内置 SQLite,维护 documents、jobs、media_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 张透明骨架参考图,用于相似主体高清视图包的创意方向选择。 |
@@ -912,7 +912,7 @@ ProductRefStateItem {
| 运行配置 / 模型标注 | GET /health | getRuntimeHealth、ModelTrace | 返回 models:ASR、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 product_view、GPT 图像模型、主体 6 视图 GPT 图像模型、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 REWRITE_MODEL、AUDIO_REWRITE_MODEL 和 VISION_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 /jobs | listJobs | 所有 job 精简列表(id/document_id/source_kind/workflow_mode/url/status/thumbnail/mtime…),按 state.json mtime 倒序。前端 URL 无 ?job= 时拉它回填全部历史;带 limit 可截断。 |
- | 创建任务 | POST /jobs | createJob | 提交 TK 链接,后台开始下载;后端自动建立 document_id=job_id、source_kind=tiktok_link、workflow_mode=feed_recreation 的 document,并在状态保存时同步 DB。 |
+ | 创建任务 | POST /jobs | createJob | 提交 TK 链接,后台开始下载;后端自动建立 document_id=job_id、source_kind=tiktok_link、workflow_mode=feed_recreation 的 document,并在状态保存时同步 DB。下载阶段优先使用 YTDLP_COOKIES_FILE,其次使用 YTDLP_COOKIES_FROM_BROWSER,TikTok 要求登录态时会把错误归一化为“上传 MP4 或配置后端 cookies”。 |
| 上传视频 | POST /jobs/upload | uploadJob | 保存 source.mp4,然后同样进入下载完成状态;后端自动建立 source_kind=upload、workflow_mode=uploaded_reference 的 document。当前上传后也加入第一步队列,下载完成后自动解析音频。 |
| 删除输入视频 | DELETE /jobs/{id} | deleteJob | 从任务队列、URL 和磁盘 jobs/<id> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。 |
| 解析视频 | POST /jobs/{id}/analyze?frames=&target=&mode=&quality= | analyzeJob | 后续阶段保留的抽帧能力。默认 frames=6;target 支持人物随机、透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。当前“开始分析”和源视频旁的“自动抽帧 6 张”都会显式用 target=random_subject、quality=accurate、mode=replace 生成清晰人物候选里的随机参考帧池。 |
@@ -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_FILE 和 YTDLP_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 && (
+
+ )}
{onDelete && (
{
const res = await fetch(`${API_BASE}/health`)
if (!res.ok) throw new Error(`health ${res.status}`)