feat: support tiktok download cookies
This commit is contained in:
@@ -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 环境变量归一化保护。
|
||||
|
||||
1
RULES.md
1
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` 依次尝试,便于区分路径不对和整条语音服务不可用
|
||||
|
||||
@@ -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
|
||||
|
||||
39
api/main.py
39
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(
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -572,7 +572,7 @@
|
||||
<p>当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧是信息流复刻工作表。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动音频文案路和视频视觉路。音频文案路提取原音频文案/字幕,分析讲话人、语速节奏、背景音乐/环境声/音效,并为后续新口播和分镜文案提供时间轴;视频视觉路同步抽取参考帧,参考帧只用于人工选择主体并生成相似主体白底视图。产品图上传后独立形成产品资产包:自动识别视角、左右/上下/内外侧、结构点、比例和风险,并补缺角度。最终分镜规划按逐句时间轴把文案、相似主体资产和产品资产汇合;当前暂停直接调视频模型,先逐条生成并审核首帧/尾帧,确认后再决定哪些分镜进入视频候选。</p>
|
||||
<div class="pipeline">
|
||||
<div class="step"><div class="num">1</div><h3>导入素材</h3><p>粘贴 TK / 信息流视频链接或上传本地视频;“开始”只把任务放入第一步队列。</p></div>
|
||||
<div class="step"><div class="num">2</div><h3>下载源视频</h3><p>后端用 yt-dlp 或本地上传文件落 <code>source.mp4</code>,记录时长、尺寸和视频只读地址。</p></div>
|
||||
<div class="step"><div class="num">2</div><h3>下载源视频</h3><p>后端用 yt-dlp 或本地上传文件落 <code>source.mp4</code>,记录时长、尺寸和视频只读地址;TikTok 受限视频可通过 <code>YTDLP_COOKIES_FILE</code> 或 <code>YTDLP_COOKIES_FROM_BROWSER</code> 提供登录态。</p></div>
|
||||
<div class="step"><div class="num">3</div><h3>并行素材分析</h3><p>下载完成后前端同时触发 <code>triggerTranscribe</code> 和 <code>analyzeJob</code>:音频路生成字幕/节奏/背景音,视觉路自动抽 6 张人物随机参考帧。</p></div>
|
||||
<div class="step"><div class="num">4</div><h3>资产包准备</h3><p>用户可删除/补选参考帧并生成相似主体视图;参考帧到这里为止,后续首尾帧和视频不再把原关键帧当画面真源。产品图上传后自动识别视角、结构和风险,并补缺角度,形成产品资产包。</p></div>
|
||||
<div class="step"><div class="num">5</div><h3>首尾帧闸门</h3><p>按逐句时间轴生成竖向分镜行;每行先规划镜头类型、人物描述、是否需要人物/产品、首帧、尾帧和产品出现方式,再按需求从相似主体 6/10 视图里选择合适人物视角,并和产品素材一起生成首尾帧,先看图确认,不直接批量提交视频。</p></div>
|
||||
@@ -611,7 +611,7 @@
|
||||
<h3>后端核心</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回,并在保存 <code>state.json</code> 时同步数据库元数据。</td></tr>
|
||||
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回,并在保存 <code>state.json</code> 时同步数据库元数据。TK 下载使用 <code>yt-dlp</code>,支持 <code>YTDLP_COOKIES_FILE</code> / <code>YTDLP_COOKIES_FROM_BROWSER</code> 给受限视频提供登录态。</td></tr>
|
||||
<tr><td><code>api/database.py</code></td><td>后端数据库层:当前内置 SQLite,维护 <code>documents</code>、<code>jobs</code>、<code>media_assets</code> 三类元数据;文档是顶层归类,一条 TK 链接或一次上传默认一个 document,媒体文件仍保留在 <code>jobs/<jobId>/</code>。</td></tr>
|
||||
<tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr>
|
||||
<tr><td><code>api/character_library/skg-characters</code></td><td>内置相似主体形象库:从桌面 5 套策划形象导入,<code>manifest.json</code> 记录运动阳光男、都市型男、优雅白领女、运动辣妹、绅士大叔,每套含 7 张透明骨架参考图,用于相似主体高清视图包的创意方向选择。</td></tr>
|
||||
@@ -912,7 +912,7 @@ ProductRefStateItem {
|
||||
<tr><td>运行配置 / 模型标注</td><td><code>GET /health</code></td><td><code>getRuntimeHealth</code>、<code>ModelTrace</code></td><td>返回 <code>models</code>:ASR、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 <code>product_view</code>、GPT 图像模型、主体 6 视图 GPT 图像模型、Azure OpenAI TTS、视频别名和 Seedance 服务商。当前 <code>REWRITE_MODEL</code>、<code>AUDIO_REWRITE_MODEL</code> 和 <code>VISION_MODEL</code> 默认使用 <code>gpt-4o</code>;如果旧环境变量仍写 <code>gemini-*</code>,后端会归一化回 <code>GPT_TEXT_MODEL</code> / <code>REWRITE_MODEL</code>。语音只走 Azure OpenAI TTS,<code>models.voice_tts_paths</code> 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。<code>database</code> 回传 DB 是否启用、URL、schema 版本和 document/job/asset 计数。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。</td></tr>
|
||||
<tr><td>文档列表</td><td><code>GET /documents</code></td><td>后续文档侧栏 / 素材库入口</td><td>从数据库读取 document 归类列表,包含 source_kind、workflow_mode、primary_job_id、storage_prefix、job_count、asset_count 和更新时间。当前前端还未接主入口,后端已可作为多视频/多上传文档管理的索引。</td></tr>
|
||||
<tr><td>历史列表</td><td><code>GET /jobs</code></td><td><code>listJobs</code></td><td>所有 job 精简列表(id/document_id/source_kind/workflow_mode/url/status/thumbnail/mtime…),按 state.json mtime 倒序。前端 URL 无 <code>?job=</code> 时拉它回填全部历史;带 <code>limit</code> 可截断。</td></tr>
|
||||
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载;后端自动建立 <code>document_id=job_id</code>、<code>source_kind=tiktok_link</code>、<code>workflow_mode=feed_recreation</code> 的 document,并在状态保存时同步 DB。</td></tr>
|
||||
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载;后端自动建立 <code>document_id=job_id</code>、<code>source_kind=tiktok_link</code>、<code>workflow_mode=feed_recreation</code> 的 document,并在状态保存时同步 DB。下载阶段优先使用 <code>YTDLP_COOKIES_FILE</code>,其次使用 <code>YTDLP_COOKIES_FROM_BROWSER</code>,TikTok 要求登录态时会把错误归一化为“上传 MP4 或配置后端 cookies”。</td></tr>
|
||||
<tr><td>上传视频</td><td><code>POST /jobs/upload</code></td><td><code>uploadJob</code></td><td>保存 source.mp4,然后同样进入下载完成状态;后端自动建立 <code>source_kind=upload</code>、<code>workflow_mode=uploaded_reference</code> 的 document。当前上传后也加入第一步队列,下载完成后自动解析音频。</td></tr>
|
||||
<tr><td>删除输入视频</td><td><code>DELETE /jobs/{id}</code></td><td><code>deleteJob</code></td><td>从任务队列、URL 和磁盘 <code>jobs/<id></code> 目录移除整个 job,包括源视频、关键帧、元素提取图和生成视频。</td></tr>
|
||||
<tr><td>解析视频</td><td><code>POST /jobs/{id}/analyze?frames=&target=&mode=&quality=</code></td><td><code>analyzeJob</code></td><td>后续阶段保留的抽帧能力。默认 <code>frames=6</code>;<code>target</code> 支持人物随机、透明骨架人、综合、清晰主体、转场变化、表情瞬间、动作峰值。当前“开始分析”和源视频旁的“自动抽帧 6 张”都会显式用 <code>target=random_subject</code>、<code>quality=accurate</code>、<code>mode=replace</code> 生成清晰人物候选里的随机参考帧池。</td></tr>
|
||||
@@ -1044,6 +1044,18 @@ ProductRefStateItem {
|
||||
<h2>变更记录</h2>
|
||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-18 · TK 受限视频下载支持 cookies 登录态</h3>
|
||||
<span class="tag violet">API</span>
|
||||
<span class="tag rose">UI</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>部分 TikTok 链接会返回“Log in for access”,后端已经接到任务并创建 job,但裸 <code>yt-dlp</code> 下载没有登录态,任务只能失败。</p>
|
||||
<p><strong>改动:</strong><code>api/main.py</code> 新增 <code>YTDLP_COOKIES_FILE</code> 和 <code>YTDLP_COOKIES_FROM_BROWSER</code>,下载时优先传 <code>--cookies</code>,其次传 <code>--cookies-from-browser</code>。TikTok 登录态错误会归一化成“上传 MP4 或配置后端 cookies 后重试”。<code>web/lib/api.ts</code> 新增失败文案格式化,<code>web/app/page.tsx</code> 在任务失败时弹 toast,<code>web/components/ad-recreation-board.tsx</code> 在素材卡显示受限下载提示。</p>
|
||||
<p><strong>影响:</strong>不要在前端接 TikTok 账号密码或登录弹窗;cookies 文件属于敏感登录态,只能放本机或服务器私有路径。普通公开视频仍裸下载,受限视频可上传 MP4 兜底。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-18 · 删除个人语音通道残留</h3>
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
@@ -3880,6 +3882,12 @@ function MaterialCard({
|
||||
<Metric label="文案" value={job.audio_script?.source_text || job.transcript.length ? "ready" : "-"} compact />
|
||||
<Metric label="段落" value={`${job.transcript.length}`} compact />
|
||||
</div>
|
||||
{job.status === "failed" && errorText && (
|
||||
<div className="mt-2 flex gap-1.5 rounded-md border border-rose-300/18 bg-rose-500/[0.08] px-2 py-1.5 text-[11px] leading-snug text-rose-100/82">
|
||||
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||
<span className="line-clamp-3">{errorText}</span>
|
||||
</div>
|
||||
)}
|
||||
{onDelete && (
|
||||
<span
|
||||
role="button"
|
||||
|
||||
@@ -626,6 +626,25 @@ export function apiAssetUrl(path?: string | null): string {
|
||||
return `${API_BASE}${path.startsWith("/") ? "" : "/"}${path}`
|
||||
}
|
||||
|
||||
export function isRestrictedDownloadError(error?: string | null): boolean {
|
||||
const text = (error ?? "").toLowerCase()
|
||||
return (
|
||||
text.includes("tiktok 下载需要登录态") ||
|
||||
text.includes("log in for access") ||
|
||||
text.includes("cookies-from-browser") ||
|
||||
text.includes("ytdlp_cookies_file") ||
|
||||
(text.includes("tiktok") && text.includes("cookies"))
|
||||
)
|
||||
}
|
||||
|
||||
export function formatJobError(error?: string | null): string {
|
||||
if (!error) return ""
|
||||
if (isRestrictedDownloadError(error)) {
|
||||
return "这个 TikTok 视频需要登录态。请上传 MP4,或让后端配置 YTDLP_COOKIES_FROM_BROWSER / YTDLP_COOKIES_FILE 后重试。"
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
export async function getHealth(): Promise<BackendHealth> {
|
||||
const res = await fetch(`${API_BASE}/health`)
|
||||
if (!res.ok) throw new Error(`health ${res.status}`)
|
||||
|
||||
Reference in New Issue
Block a user