Files
ViGent2/Docs/DevLogs/Day25.md
Kevin Wong 0a5a17402c 更新
2026-02-24 16:55:29 +08:00

12 KiB
Raw Blame History

🔧 文案提取助手修复 — 抖音链接无法提取文案 (Day 25)

概述

文案提取助手粘贴抖音链接后无法提取文案yt-dlp 报错 Fresh cookies are needed,手动回退方案也因抖音页面结构变化失效。本日完成了完整修复,并清理了不再需要的 DOUYIN_COOKIE 配置。


🐛 问题诊断

错误链路

  1. yt-dlp 失败ERROR: [Douyin] Fresh cookies (not necessarily logged in) are needed
    • yt-dlp 版本 2025.12.08 过旧
    • 抖音 API aweme/v1/web/aweme/detail/ 需要签名 cookies_v_web_id 等),即使升级 yt-dlp 到最新版 + 传入 cookie 仍无法解决,属 yt-dlp 已知问题
  2. 手动回退失败Could not find RENDER_DATA in page
    • 旧方案通过桌面端用户主页 + modal_id 访问,抖音 SSR 已不再返回 videoDetail 数据
  3. .envDOUYIN_COOKIE:时间戳 2024 年 12 月,早已过期

修复方案:移动端分享页 + 自动获取 ttwid

核心思路

放弃依赖 yt-dlp 下载抖音视频和手动维护 cookie改为

  1. 自动从 ByteDance 公共 API 获取新鲜 ttwid(匿名令牌,不绑定账号)
  2. ttwid 访问移动端分享页 m.douyin.com/share/video/{id}
  3. 从页面内嵌 JSON 中提取 play_addr 播放地址并下载

关键代码(_download_douyin_manual 重写)

# 1. 获取新鲜 ttwid
ttwid_resp = await client.post(
    "https://ttwid.bytedance.com/ttwid/union/register/",
    json={"region": "cn", "aid": 6383, "service": "www.douyin.com", ...}
)
ttwid = ttwid_resp.cookies.get("ttwid", "")

# 2. 访问移动端分享页
page_resp = await client.get(
    f"https://m.douyin.com/share/video/{video_id}",
    headers={"cookie": f"ttwid={ttwid}", ...}
)

# 3. 提取 play_addr
addr_match = re.search(r'"play_addr":\{"uri":"([^"]+)","url_list":\["([^"]+)"', page_text)
video_url = addr_match.group(2).replace(r"\u002F", "/")

优势

  • 不再依赖手动维护的 DOUYIN_COOKIEttwid 每次请求自动获取
  • 不受 yt-dlp 对抖音支持状况影响
  • 所有用户通用,不绑定特定账号

DOUYIN_COOKIE 仅用于文案提取,新方案不再需要,已从以下位置删除:

文件 变更
backend/.env 删除 DOUYIN_COOKIE 配置项及注释
backend/app/core/config.py 删除 DOUYIN_COOKIE: str = "" 字段定义
backend/app/modules/tools/service.py 删除 yt-dlp 传 cookie 逻辑和 _write_netscape_cookies 辅助函数

🔤 前端文案修正

将文案提取界面中的"AI 洗稿结果"改为"AI 改写结果"。

文件 变更
frontend/src/features/home/ui/ScriptExtractionModal.tsx AI 洗稿结果AI 改写结果
backend/app/modules/tools/service.py 注释中"洗稿"→"改写"
backend/app/services/glm_service.py docstring 中"洗稿"→"改写文案"

📦 其他变更

  • yt-dlp 升级2025.12.082026.2.21
  • yt-dlp 初始化修正:改为 YoutubeDL(ydl_opts) 直接传参初始化(原先空初始化后 update params 不生效)
  • User-Agent 更新yt-dlp 中 Chrome/91Chrome/131

涉及文件汇总

后端修改

文件 变更
backend/app/modules/tools/service.py 重写 _download_douyin_manual(移动端分享页方案);修正 yt-dlp 初始化;清理 cookie 相关代码;注释改写
backend/app/services/glm_service.py docstring "洗稿" → "改写文案"
backend/app/core/config.py 删除 DOUYIN_COOKIE 字段
backend/.env 删除 DOUYIN_COOKIE 配置
backend/requirements.txt yt-dlp 版本升级

前端修改

文件 变更
frontend/src/features/home/ui/ScriptExtractionModal.tsx "AI 洗稿结果" → "AI 改写结果"

✏️ AI 智能改写 — 自定义提示词功能

概述

文案提取助手的"AI 智能改写"原先使用硬编码 prompt用户无法定制改写风格。本次在 checkbox 右侧新增"自定义提示词"折叠区域,用户可编辑自定义 prompt持久化到 localStorage后端按需替换默认 prompt。

后端修改

路由层 (router.py)extract_script_tool 新增可选 Form 参数 custom_prompt: Optional[str] = Form(None),透传给 service。

服务层 (service.py)extract_script() 签名新增 custom_prompt,透传给 glm_service.rewrite_script(script, custom_prompt)

AI 层 (glm_service.py)rewrite_script(self, text, custom_prompt=None),若 custom_prompt 有值则用自定义 prompt + 原文拼接,否则保持原有默认 prompt。

if custom_prompt and custom_prompt.strip():
    prompt = f"""{custom_prompt.strip()}

原始文案:
{text}"""
else:
    prompt = f"""请将以下视频文案进行改写。...(原有默认)"""

前端修改

Hook (useScriptExtraction.ts)

  • 新增 customPrompt / showCustomPrompt 状态
  • 初始值从 localStorage.getItem("vigent_rewriteCustomPrompt") 恢复
  • customPrompt 变化时防抖 300ms 保存到 localStorage
  • handleExtract() 中若 doRewrite && customPrompt.trim() 有值,追加 formData.append("custom_prompt", ...)
  • modal 重置时不清空 customPrompt持久化偏好

UI (ScriptExtractionModal.tsx)

  • checkbox 同行右侧新增"自定义提示词 ▼"按钮(仅 doRewrite 时显示)
  • 点击展开 textarea 编辑区域,底部提示"留空则使用默认提示词"
  • 取消勾选 AI 智能改写时,自定义提示词区域自动隐藏

涉及文件

文件 变更
backend/app/modules/tools/router.py 新增 custom_prompt Form 参数
backend/app/modules/tools/service.py extract_script() 透传 custom_prompt
backend/app/services/glm_service.py rewrite_script() 支持自定义 prompt
frontend/.../useScriptExtraction.ts 新增状态、localStorage 持久化、FormData 传参
frontend/.../ScriptExtractionModal.tsx UI 按钮 + 展开 textarea

验证

  • 后端 python -m py_compile 三个文件通过
  • 前端 npx tsc --noEmit 通过

🐛 SSR 构建修复 — localStorage is not defined

问题

npm run build 报错 ReferenceError: localStorage is not defined,因为 useScriptExtraction.tsuseState 的初始化函数在 SSRNode.js环境下也会执行而服务端没有 localStorage

修复

useState 初始化加 typeof window !== "undefined" 守卫:

const [customPrompt, setCustomPrompt] = useState(
    () => typeof window !== "undefined" ? localStorage.getItem(CUSTOM_PROMPT_KEY) || "" : ""
);
文件 变更
frontend/.../useScriptExtraction.ts useState 初始化增加 SSR 安全守卫

🎬 片头副标题功能

概述

新增片头副标题secondary_title显示在主标题下方用于补充说明或悬念引导。副标题有独立的样式配置字体、字号、颜色等可由 AI 同时生成20 字限制,仅在视频画面中显示,不参与发布标题。

命名约定:后端 secondary_titlesnake_case前端 videoSecondaryTitlecamelCase用户界面"片头副标题"。


后端修改

文件 变更
backend/app/modules/videos/schemas.py GenerateRequest 新增 4 个可选字段:secondary_titlesecondary_title_style_idsecondary_title_font_sizesecondary_title_top_margin
backend/app/services/glm_service.py AI prompt 增加副标题生成要求不超过20字JSON 格式新增 secondary_title 字段
backend/app/modules/ai/router.py GenerateMetaResponse 增加 secondary_title: str = ""endpoint 返回时取 result.get("secondary_title", "")
backend/app/modules/videos/workflow.py use_remotion 条件增加 or req.secondary_title;副标题样式解析复用 get_style("title", ...);字号/间距覆盖;prepare_style_for_remotion 处理副标题字体;remotion_service.render() 传入 secondary_title + secondary_title_style
backend/app/services/remotion_service.py render() 新增 secondary_titlesecondary_title_style 参数,构建 CLI 参数 --secondaryTitle--secondaryTitleStyle

Remotion 修改

文件 变更
remotion/render.ts RenderOptions 新增 secondaryTitle? + secondaryTitleStyle?parseArgs() 新增 switch caseinputProps 新增两个字段
remotion/src/components/Title.tsx TitleProps 新增 secondaryTitle?secondaryTitleStyle?AbsoluteFill 改为 flexDirection: 'column' 垂直堆叠;主标题 <h1> 后增加副标题 <h2>,独立样式(默认字号 48px、字重 700共享淡入淡出动画副标题字体使用独立 @font-faceSecondaryTitleFont)避免与主标题冲突
remotion/src/Video.tsx VideoProps 新增 secondaryTitle? + secondaryTitleStyle?;传递给 <Title> 组件;渲染条件改为 {(title || secondaryTitle) && ...}
remotion/src/Root.tsx defaultProps 新增 secondaryTitle: undefined + secondaryTitleStyle: undefined

前端修改

文件 变更
frontend/src/shared/lib/title.ts 新增 SECONDARY_TITLE_MAX_LENGTH = 20clampSecondaryTitle()
frontend/src/features/home/model/useHomeController.ts 新增状态 videoSecondaryTitleselectedSecondaryTitleStyleIdsecondaryTitleFontSizesecondaryTitleTopMarginsecondaryTitleSizeLocked;新建 secondaryTitleInput = useTitleInput({ maxLength: 20 })(不 sync 到发布页);handleGenerateMeta() 接收并填充 secondary_titlehandleGenerate() 构建 payload 增加副标题字段return 暴露所有新状态
frontend/src/features/home/model/useHomePersistence.ts 新增 localStorage keysecondaryTitlesecondaryTitleStylesecondaryTitleFontSizesecondaryTitleTopMargin;对应的恢复和保存 effect
frontend/src/features/home/ui/TitleSubtitlePanel.tsx Props 新增副标题相关;主标题输入框下方添加"片头副标题限制20个字"输入框;副标题样式选择器(复用 titleStyles 预设、字号滑块30-100px、间距滑块0-100px
frontend/src/features/home/ui/FloatingStylePreview.tsx 标题预览改为 flex column 布局;主标题下方增加副标题预览行,独立样式渲染
frontend/src/features/home/ui/HomePage.tsx useHomeController 解构新状态,传给 TitleSubtitlePanel

🐛 参考音频上传 — 中文文件名 InvalidKey 修复

问题

上传中文名参考音频(如"我的声音.wav"Supabase Storage 报 InvalidKey,因为存储路径直接使用了原始中文文件名。

修复

ref_audios/service.py 新增 sanitize_filename() 函数,将存储路径的文件名清洗为 ASCII 安全字符(仅 A-Za-z0-9._-

  • NFKD 规范化 → 丢弃非 ASCII → 非法字符替换为 _
  • 纯中文/emoji 清洗后为空时,使用 MD5 哈希兜底(如 audio_e924b1193007
  • 文件名限长 50 字符
  • 原始中文文件名保留在 metadata 中作为展示名,前端显示不受影响
修复前: cbbe.../1771915755_我的声音.wav  → InvalidKey
修复后: cbbe.../1771915755_audio_xxxxxxxx.wav → 上传成功
文件 变更
backend/app/modules/ref_audios/service.py 新增 sanitize_filename() 函数,上传路径使用清洗后文件名