style: auto-format with black + isort + ruff + prettier [skip ci-format-check]
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m57s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m25s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m42s
AI Code Review / AI Code Review (pull_request) Successful in 6m57s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m42s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 13m49s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 15m30s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 16m28s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 17m34s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 37m57s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s

This commit is contained in:
CI Bot
2026-10-02 14:29:35 +00:00
parent 667f5350f8
commit adf0cc5c48
6 changed files with 42 additions and 13 deletions
+13 -5
View File
@@ -1163,7 +1163,9 @@ def _step_render(job: ViralVideoJob, copy_result: dict, tts_audio_url: str | Non
usage = result.get("usage")
if not video_path or not Path(video_path).exists() or Path(video_path).stat().st_size == 0:
raise RuntimeError("Seedance 视频生成失败:返回空文件或路径不存在")
logger.info("[爆款视频] Seedance 单次生成完成: %s size=%d usage=%s", video_path, Path(video_path).stat().st_size, usage)
logger.info(
"[爆款视频] Seedance 单次生成完成: %s size=%d usage=%s", video_path, Path(video_path).stat().st_size, usage
)
return str(video_path), (usage if isinstance(usage, dict) else None)
@@ -1566,6 +1568,7 @@ def _try_refund_viral_video(job: ViralVideoJob) -> None:
"""爆款视频生成失败:若已预扣积分则全额退款。"""
try:
from packages.shared import get_shared_settings
_s = get_shared_settings()
if not _s.points_enabled:
return
@@ -1573,12 +1576,16 @@ def _try_refund_viral_video(job: ViralVideoJob) -> None:
if prepaid <= 0:
return
from packages.domain.points_service import PointsService
svc = PointsService()
# 使用独立 session(避免污染外层事务)
ssn = SessionLocal()
try:
svc.refund_viral_video(
job.user_id, prepaid, getattr(job, "credits_transaction_id", "") or "", ssn,
job.user_id,
prepaid,
getattr(job, "credits_transaction_id", "") or "",
ssn,
)
job.credits_prepaid = 0.0
finally:
@@ -1591,6 +1598,7 @@ def _settle_viral_video(job: ViralVideoJob, usage: dict | None) -> None:
"""爆款视频生成成功:按实际 usage 结算,多退少补,写 credits_cost。"""
try:
from packages.shared import get_shared_settings
_s = get_shared_settings()
if not _s.points_enabled:
job.credits_cost = 0.0
@@ -1602,6 +1610,7 @@ def _settle_viral_video(job: ViralVideoJob, usage: dict | None) -> None:
return
from packages.domain.points_rules import calculate_viral_video_credits, resolve_video_dimensions
from packages.domain.points_service import PointsService
w, h = resolve_video_dimensions(
getattr(job, "video_resolution", "720p") or "720p",
getattr(job, "video_ratio", "9:16") or "9:16",
@@ -1613,7 +1622,8 @@ def _settle_viral_video(job: ViralVideoJob, usage: dict | None) -> None:
actual_tokens = int(at)
actual_credits = calculate_viral_video_credits(
int(getattr(job, "duration", 15) or 15),
w, h,
w,
h,
getattr(job, "video_model", "") or "seedance-2.5",
actual_tokens=actual_tokens,
)
@@ -1683,11 +1693,9 @@ def _run_render_pipeline(job_id: str, session, repo, job) -> dict:
_set_stage(job, repo, session, ViralVideoStage.RENDERING, "正在生成视频(约1-3分钟)...")
video_path = None
usage = None
render_err = None
try:
video_path, usage = _step_render(job, copy_result, tts_url)
except Exception as e:
render_err = e
logger.error("[爆款视频][阶段3] Seedance 生成失败,触发退款: %s", e, exc_info=True)
# 退款
_try_refund_viral_video(job)
+2 -2
View File
@@ -962,9 +962,9 @@ class ViralVideoJobModel(Base):
) # v1.6: 编导脚本结构{overview,scene_and_lighting,shots,hard_constraints,negative_prompts,voiceover_script}
result_video_url = Column(String(1000), nullable=False, default="")
credits_cost = Column(Float, nullable=False, default=0)
video_resolution = Column(String(20), nullable=False, default='720p')
video_resolution = Column(String(20), nullable=False, default="720p")
credits_prepaid = Column(Float, nullable=False, default=0.0)
credits_transaction_id = Column(String(36), nullable=False, default='')
credits_transaction_id = Column(String(36), nullable=False, default="")
error_msg = Column(Text, nullable=False, default="")
retry_count = Column(Integer, nullable=False, default=0)
started_at = Column(DateTime(timezone=True), nullable=True)
+15 -3
View File
@@ -32,9 +32,21 @@ VIRAL_VIDEO_FPS = 24
# 分辨率别名映射 -> 标准 key
_RESOLUTION_ALIASES: dict[str, str] = {
"480p": "480p", "普清": "480p", "default": "480p", "low": "480p", "sd": "480p",
"720p": "720p", "高清": "720p", "medium": "720p", "hd": "720p",
"1080p": "1080p", "超清": "1080p", "high": "1080p", "ultra": "1080p", "全能": "1080p", "fhd": "1080p",
"480p": "480p",
"普清": "480p",
"default": "480p",
"low": "480p",
"sd": "480p",
"720p": "720p",
"高清": "720p",
"medium": "720p",
"hd": "720p",
"1080p": "1080p",
"超清": "1080p",
"high": "1080p",
"ultra": "1080p",
"全能": "1080p",
"fhd": "1080p",
}
# 分辨率 -> 高度
_RESOLUTION_HEIGHT: dict[str, int] = {"480p": 480, "720p": 720, "1080p": 1080}
+3 -1
View File
@@ -328,7 +328,9 @@ class PointsService:
if not res.get("success"):
logger.warning(
"[viral_video] 结算补扣余额不足 user_id=%s extra=%.2f balance=%s (不阻塞任务完成)",
user_id, extra, res.get("balance"),
user_id,
extra,
res.get("balance"),
)
return {"success": bool(res.get("success")), "action": "deduct", "diff": extra, "amount": extra}
except Exception:
+1
View File
@@ -128,6 +128,7 @@ class TestViralVideoJobDefaults:
assert job.result_video_url == ""
assert job.error_msg == ""
class TestViralVideoStage:
"""阶段枚举测试。"""
+8 -2
View File
@@ -212,7 +212,10 @@ class TestCallVideoGeneration:
with patch("packages.shared.ai_service.get_doubao_client") as mock_get:
mock_client = MagicMock()
mock_client.is_available = True
mock_client.video_generation.return_value = {"video_path": str(out), "usage": {"completion_tokens": 1000000}}
mock_client.video_generation.return_value = {
"video_path": str(out),
"usage": {"completion_tokens": 1000000},
}
mock_get.return_value = mock_client
result = call_video_generation(prompt="测试", image_url="https://img/x.jpg", duration=5, ratio="9:16")
assert result is not None and result["video_path"] == str(out)
@@ -238,7 +241,10 @@ class TestCallVideoGenerationV16:
with patch("packages.shared.ai_service.get_doubao_client") as mock_get:
mock_client = MagicMock()
mock_client.is_available = True
mock_client.video_generation.return_value = {"video_path": str(out), "usage": {"completion_tokens": 1500000}}
mock_client.video_generation.return_value = {
"video_path": str(out),
"usage": {"completion_tokens": 1500000},
}
mock_get.return_value = mock_client
result = call_video_generation(
prompt="测试",