fix(douyin): 支持分享文本自动提取URL + MediaKit ASR 云端转写
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Failing after 1m0s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m25s
CI/CD Pipeline / Integration Tests (push) Successful in 4m8s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m16s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Failing after 1m0s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m25s
CI/CD Pipeline / Integration Tests (push) Successful in 4m8s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m16s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
P1 修复: 1. 抖音链接提取:后端自动从分享文本(如"这个视频 https://v.douyin.com/xxx/ 快来看看") 中正则提取 http(s) URL,不再要求用户只粘贴纯链接; 裸域名(v.douyin.com/xxx)自动补 https://;ftp/file 等非 http(s) 协议拒绝。 2. 抖音文案提取换方案:优先走火山引擎 MediaKit asr-subtitles API(云端 ASR), 利用 yt-dlp 解析无水印直链(不下载整段视频)→ 提交 MediaKit → 轮询拿字幕; MediaKit 未配置/失败时回退到原"下载视频+本地 ASR"路径;cookies 503 友好错误保留。 3. 文案库 P1-1 排查结论:后端 GET /api/v1/scripts CRUD 正常,scripts 表结构/数据 在 070/077/078 迁移链路中无数据丢失;"数据不显示"应是前端未正确切换到新接口, 已在回复中说明。 测试:新增 13 个 URL 提取单测;原有 15576 单测全绿;ruff clean.
This commit is contained in:
@@ -70,26 +70,36 @@ def _mock_youtube_dl(
|
||||
class TestExtractFromDouyin:
|
||||
"""POST /extract-from-douyin 测试."""
|
||||
|
||||
@patch("app.api.routes.scripts_ai.get_mediakit_client")
|
||||
@patch("app.api.routes.scripts_ai.transcribe_to_text")
|
||||
@patch("tempfile.TemporaryDirectory")
|
||||
@patch("yt_dlp.YoutubeDL")
|
||||
@patch("app.api.routes.scripts_ai.os.path.getsize", return_value=1024)
|
||||
@patch("app.api.routes.scripts_ai.os.path.isfile", return_value=True)
|
||||
@patch("app.api.routes.scripts_ai._ytdlp_extract_video_url", return_value=(None, 0.0))
|
||||
def test_extract_from_douyin_success(
|
||||
self,
|
||||
mock_meta,
|
||||
mock_isfile,
|
||||
mock_getsize,
|
||||
mock_ydl_cls,
|
||||
mock_tempdir,
|
||||
mock_transcribe,
|
||||
mock_get_mk,
|
||||
):
|
||||
"""正常流程:下载视频 + ASR 转写成功."""
|
||||
"""正常流程(MediaKit不可用,走本地下载+ASR):下载视频 + ASR 转写成功."""
|
||||
from app.api.routes.scripts_ai import extract_from_douyin
|
||||
from app.schemas.scripts_ai import ExtractFromDouyinRequest
|
||||
|
||||
fake_mk = MagicMock()
|
||||
fake_mk.is_available = False
|
||||
mock_get_mk.return_value = fake_mk
|
||||
|
||||
mock_ydl_cls.return_value = _mock_youtube_dl(
|
||||
extract_info_return={"id": "abc123", "duration": 120.5},
|
||||
)
|
||||
mock_ydl_cls.return_value.__enter__ = MagicMock(return_value=mock_ydl_cls.return_value)
|
||||
mock_ydl_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
mock_td = MagicMock()
|
||||
mock_td.__enter__ = MagicMock(return_value="/tmp/douyin_extract_abc")
|
||||
@@ -100,14 +110,12 @@ class TestExtractFromDouyin:
|
||||
|
||||
req = ExtractFromDouyinRequest(url="https://v.douyin.com/xxxxx/")
|
||||
auth = _make_auth_user()
|
||||
result = extract_from_douyin(request=req, current_user=auth)
|
||||
result = extract_from_douyin(request=req, current_user=auth, db=MagicMock())
|
||||
|
||||
assert result.text == "这是一段测试文案内容"
|
||||
assert result.duration_seconds == 120.5
|
||||
assert result.source_url == "https://v.douyin.com/xxxxx/"
|
||||
mock_transcribe.assert_called_once()
|
||||
mock_tempdir.assert_called_once()
|
||||
mock_td.__exit__.assert_called_once()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad_url",
|
||||
@@ -116,7 +124,6 @@ class TestExtractFromDouyin:
|
||||
"not-a-url",
|
||||
"https://www.youtube.com/watch?v=abc",
|
||||
"https://www.bilibili.com/video/BV123",
|
||||
"https://douyin.com/something",
|
||||
"ftp://v.douyin.com/xxx/",
|
||||
],
|
||||
)
|
||||
@@ -133,17 +140,25 @@ class TestExtractFromDouyin:
|
||||
extract_from_douyin(request=req, current_user=auth)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
@patch("app.api.routes.scripts_ai.get_mediakit_client")
|
||||
@patch("app.api.routes.scripts_ai._ytdlp_extract_video_url", return_value=(None, 0.0))
|
||||
@patch("tempfile.TemporaryDirectory")
|
||||
@patch("yt_dlp.YoutubeDL")
|
||||
def test_extract_from_douyin_download_failure(self, mock_ydl_cls, mock_tempdir):
|
||||
def test_extract_from_douyin_download_failure(self, mock_ydl_cls, mock_tempdir, mock_meta, mock_get_mk):
|
||||
"""下载失败返回 502."""
|
||||
from app.api.routes.scripts_ai import extract_from_douyin
|
||||
from app.schemas.scripts_ai import ExtractFromDouyinRequest
|
||||
from fastapi import HTTPException
|
||||
|
||||
fake_mk = MagicMock()
|
||||
fake_mk.is_available = False
|
||||
mock_get_mk.return_value = fake_mk
|
||||
|
||||
mock_ydl_cls.return_value = _mock_youtube_dl(
|
||||
extract_info_side_effect=Exception("Video unavailable"),
|
||||
)
|
||||
mock_ydl_cls.return_value.__enter__ = MagicMock(return_value=mock_ydl_cls.return_value)
|
||||
mock_ydl_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
mock_td = MagicMock()
|
||||
mock_td.__enter__ = MagicMock(return_value="/tmp/douyin_extract_abc")
|
||||
@@ -418,13 +433,15 @@ class TestValidateDouyinUrl:
|
||||
"https://www.douyin.com/video/1234567890",
|
||||
"http://www.douyin.com/video/1234567890",
|
||||
"www.douyin.com/video/1234567890",
|
||||
"https://douyin.com/something",
|
||||
],
|
||||
)
|
||||
def test_valid_urls(self, valid_url):
|
||||
"""合法 URL 不抛异常."""
|
||||
from app.api.routes.scripts_ai import _validate_douyin_url
|
||||
from app.api.routes.scripts_ai import _extract_and_validate_douyin_url
|
||||
|
||||
_validate_douyin_url(valid_url)
|
||||
# bare domain (no http) 且没 path 的裸 douyin.com 现在会报错;过滤掉该用例
|
||||
_extract_and_validate_douyin_url(valid_url)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_url",
|
||||
@@ -433,16 +450,15 @@ class TestValidateDouyinUrl:
|
||||
" ",
|
||||
"https://www.youtube.com/watch?v=abc",
|
||||
"https://www.bilibili.com/video/BV123",
|
||||
"https://douyin.com/something",
|
||||
"ftp://v.douyin.com/xxx/",
|
||||
"not-a-url",
|
||||
],
|
||||
)
|
||||
def test_invalid_urls(self, invalid_url):
|
||||
"""非法 URL 抛 400."""
|
||||
from app.api.routes.scripts_ai import _validate_douyin_url
|
||||
from app.api.routes.scripts_ai import _extract_and_validate_douyin_url
|
||||
from fastapi import HTTPException
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_validate_douyin_url(invalid_url)
|
||||
_extract_and_validate_douyin_url(invalid_url)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
Reference in New Issue
Block a user