video-shuoshan/backend/utils/seedance_client.py
seaislee1209 f803a1ba71 feat: v0.8.1 — Seedance API 友好错误提示 + 前端 Mock 数据清理
- 新增 SeedanceAPIError 异常类 + ERROR_MESSAGES 错误码中文映射
- views.py 异常处理区分 SeedanceAPIError,存储友好错误信息
- 移除 DEV 环境 7 个 mock 任务,消除 404 轮询错误

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 22:31:14 +08:00

126 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Volcano Engine Seedance (ARK) video generation API client."""
import requests
from django.conf import settings
# Seedance API error code → user-friendly Chinese message
ERROR_MESSAGES = {
'InputImageSensitiveContentDetected.PrivacyInformation': '参考图片中检测到真实人脸Seedance 不允许处理包含真人面部的图片',
'InputImageSensitiveContentDetected': '参考图片包含敏感内容,请更换图片后重试',
'InputVideoSensitiveContentDetected': '参考视频包含敏感内容,请更换视频后重试',
'InvalidParameter': '请求参数无效,请检查输入',
'RateLimitExceeded': 'API 调用频率超限,请稍后重试',
'InsufficientBalance': '账户余额不足,请联系管理员充值',
}
class SeedanceAPIError(Exception):
"""Raised when Seedance API returns an error response."""
def __init__(self, code, message, status_code=400):
self.code = code
self.api_message = message
self.status_code = status_code
# Use friendly message if available, otherwise use API message
self.user_message = ERROR_MESSAGES.get(code, message)
super().__init__(self.user_message)
MODEL_MAP = {
'seedance_2.0': 'doubao-seedance-2-0-260128',
'seedance_2.0_fast': 'doubao-seedance-2-0-fast-260128',
}
def _headers():
return {
'Content-Type': 'application/json',
'Authorization': f'Bearer {settings.ARK_API_KEY}',
}
def create_task(prompt, model, content_items, aspect_ratio, duration, generate_audio=True):
"""Create a video generation task.
Args:
prompt: Text prompt for video generation.
model: Model key ('seedance_2.0' or 'seedance_2.0_fast').
content_items: List of media content dicts (image_url, video_url, audio_url).
aspect_ratio: Video aspect ratio ('16:9', '9:16', etc.).
duration: Video duration in seconds.
generate_audio: Whether to generate audio with the video.
Returns:
dict: API response with task id and status.
"""
url = f'{settings.ARK_BASE_URL}/contents/generations/tasks'
content = []
if prompt:
content.append({'type': 'text', 'text': prompt})
content.extend(content_items)
payload = {
'model': MODEL_MAP.get(model, model),
'content': content,
'generate_audio': generate_audio,
'ratio': aspect_ratio,
'duration': duration,
'watermark': False,
}
resp = requests.post(url, json=payload, headers=_headers(), timeout=60)
if resp.status_code != 200:
# Extract human-readable error from Seedance API response
try:
err = resp.json().get('error', {})
code = err.get('code', '')
message = err.get('message', resp.text)
except Exception:
code, message = '', resp.text
raise SeedanceAPIError(code, message, resp.status_code)
return resp.json()
def query_task(task_id):
"""Query a video generation task by its ARK task ID.
Returns:
dict: Task status, content (video URL on success), usage info.
"""
url = f'{settings.ARK_BASE_URL}/contents/generations/tasks/{task_id}'
resp = requests.get(url, headers=_headers(), timeout=30)
resp.raise_for_status()
return resp.json()
def extract_video_url(task_response):
"""Extract the video URL from a completed task response."""
content = task_response.get('content')
if not content:
return None
# content could be a list of items or a dict
if isinstance(content, list):
for item in content:
if item.get('type') == 'video_url':
return item.get('video_url', {}).get('url')
elif isinstance(content, dict):
if 'video_url' in content:
url = content['video_url']
return url.get('url') if isinstance(url, dict) else url
return None
def map_status(ark_status):
"""Map ARK task status to our DB status."""
mapping = {
'running': 'processing',
'submitted': 'queued',
'queued': 'queued',
'succeeded': 'completed',
'failed': 'failed',
}
return mapping.get(ark_status, 'processing')