fix(scripts): extract-from-douyin 异常路径全部返回业务码,杜绝 500 INTERNAL_ERROR
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m18s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m43s
AI Code Review / AI Code Review (pull_request) Successful in 6m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 12m6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 10s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 36s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m18s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m43s
AI Code Review / AI Code Review (pull_request) Successful in 6m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 12m6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 10s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 36s
问题:POST /api/v1/scripts/extract-from-douyin 在抖音短链无效/网络异常/ASR 兜底异常等 场景下冒泡未捕获 Exception,被全局 general_exception_handler 吞成 500 INTERNAL_ERROR, 前端无法区分业务错误。 修复(apps/api/app/api/routes/scripts_ai.py): 1. yt-dlp 导入失败(缺依赖)→ 503(提示功能不可用) 2. yt_dlp.utils.DownloadError 单独捕获: - 含 404/not found/unable to download/unsupported url → 400「无法解析该抖音链接」 - 其它网络/上游异常 → 502「视频下载失败」 3. 下载 info 成功但文件未落地(isfile False / size 0)→ 502「未获取到有效文件」 4. duration 非数字(TypeError/ValueError)→ 安全降级为 0 5. ASR 转写兜底 catch Exception → 502「语音识别失败」(原只 catch 两个自定义异常) 6. 最外层兜底 catch Exception → 500 但带具体 detail(不再是通用 INTERNAL_ERROR) 7. text/duration 提前初始化,避免异常路径 UnboundLocalError 测试: - 新增 tests/unit/test_extract_from_douyin_errors.py,覆盖 8 个失败场景 - 更新 tests/unit/test_scripts_ai.py 三个成功/ASR 路径测试,mock os.path.isfile/getsize - 全量单测 15531 passed
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
@@ -80,10 +81,20 @@ def extract_from_douyin(
|
||||
if not re.match(r"^https?://", url_for_download, re.IGNORECASE):
|
||||
url_for_download = "https://" + url_for_download
|
||||
|
||||
# 使用临时目录下载视频,退出时自动清理
|
||||
text: str = ""
|
||||
duration: float = 0.0
|
||||
|
||||
try:
|
||||
with tempfile.TemporaryDirectory(prefix="douyin_extract_") as temp_dir:
|
||||
import yt_dlp
|
||||
# 延迟导入 yt-dlp,避免模块缺失时影响其他路由启动
|
||||
try:
|
||||
import yt_dlp
|
||||
except ImportError as exc:
|
||||
logger.error("yt-dlp 未安装,抖音提取功能不可用: %s", exc)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="抖音提取功能暂不可用(缺少依赖 yt-dlp)",
|
||||
) from exc
|
||||
|
||||
ydl_opts = {
|
||||
"format": "best[ext=mp4]/best",
|
||||
@@ -96,11 +107,23 @@ def extract_from_douyin(
|
||||
try:
|
||||
ydl = yt_dlp.YoutubeDL(ydl_opts)
|
||||
info = ydl.extract_info(url_for_download, download=True)
|
||||
except yt_dlp.utils.DownloadError as exc:
|
||||
# yt-dlp 官方异常类型:HTTP 错误、短链失效、视频下架等
|
||||
msg = str(exc)
|
||||
logger.warning("抖音下载失败: url=%s error=%s", source_url, msg)
|
||||
# 404/视频不存在/不可下载 → 400;网络问题/上游异常 → 502
|
||||
is_bad_url = any(
|
||||
kw in msg.lower() for kw in ("404", "not found", "unable to download webpage", "unsupported url", "no video formats")
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST if is_bad_url else status.HTTP_502_BAD_GATEWAY,
|
||||
detail=("无法解析该抖音链接,请确认链接有效且视频未被下架" if is_bad_url else f"视频下载失败: {msg[:200]}"),
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
logger.error("抖音视频下载失败: url=%s error=%s", source_url, exc)
|
||||
logger.exception("抖音视频下载异常: url=%s", source_url)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"视频下载失败: {exc}",
|
||||
detail=f"视频下载失败: {str(exc)[:200]}",
|
||||
) from exc
|
||||
|
||||
if info is None:
|
||||
@@ -110,9 +133,20 @@ def extract_from_douyin(
|
||||
)
|
||||
|
||||
video_path = ydl.prepare_filename(info)
|
||||
duration = float(info.get("duration") or 0)
|
||||
try:
|
||||
duration = float(info.get("duration") or 0)
|
||||
except (TypeError, ValueError):
|
||||
duration = 0.0
|
||||
|
||||
# ASR 转写
|
||||
# 校验下载的文件是否真的存在(某些 yt-dlp 版本可能 info 成功但未下载到文件)
|
||||
if not os.path.isfile(video_path) or os.path.getsize(video_path) == 0:
|
||||
logger.error("yt-dlp 未产生有效视频文件: path=%s", video_path)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="视频下载异常:未获取到有效文件",
|
||||
)
|
||||
|
||||
# ASR 转写(兜底捕获所有异常,避免 500)
|
||||
try:
|
||||
text = transcribe_to_text(video_path)
|
||||
except ASRNotConfiguredError as exc:
|
||||
@@ -125,9 +159,22 @@ def extract_from_douyin(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
logger.exception("ASR 转写异常: path=%s", video_path)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"语音识别失败: {str(exc)[:200]}",
|
||||
) from exc
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
# 最后兜底:任何未捕获异常都转成 502/400,不允许冒泡成 500
|
||||
logger.exception("抖音文案提取未预期异常: url=%s", source_url)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"抖音文案提取失败: {str(exc)[:200]}",
|
||||
) from exc
|
||||
|
||||
return ExtractFromDouyinResponse(
|
||||
text=text,
|
||||
|
||||
Reference in New Issue
Block a user