All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m22s
密码管理:用户自助修改密码(个人中心弹窗)、管理员重置用户密码(审计日志记录) 错误提示:补全火山 ARK 错误码映射(+7 个)、修复创建失败时前端不显示真实错误、 轮询失败走 ERROR_MESSAGES 映射、前端 catch 统一取后端 message Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
138 lines
4.8 KiB
Python
138 lines
4.8 KiB
Python
"""Volcano Engine ARK video generation API client."""
|
|
|
|
import requests
|
|
from django.conf import settings
|
|
|
|
|
|
# API error code → user-friendly Chinese message
|
|
ERROR_MESSAGES = {
|
|
# Input content moderation
|
|
'InputImageSensitiveContentDetected.PrivacyInformation': '参考图片中检测到真实人脸,系统不允许处理包含真人面部的图片',
|
|
'InputImageSensitiveContentDetected': '参考图片包含敏感内容,请更换图片后重试',
|
|
'InputVideoSensitiveContentDetected': '参考视频包含敏感内容,请更换视频后重试',
|
|
'InputTextSensitiveContentDetected': '提示词包含敏感内容,请修改后重试',
|
|
# Output content moderation
|
|
'OutputVideoSensitiveContentDetected': '生成的视频包含敏感内容,已被系统拦截',
|
|
'OutputImageSensitiveContentDetected': '生成的图片包含敏感内容,已被系统拦截',
|
|
# Parameter & rate limit errors
|
|
'InvalidParameter': '请求参数无效,请检查输入',
|
|
'RateLimitExceeded': 'API 调用频率超限,请稍后重试',
|
|
'ConcurrencyLimitExceeded': '并发数超限,请稍后重试',
|
|
# Account & billing
|
|
'InsufficientBalance': '账户余额不足,请联系管理员充值',
|
|
# Server errors
|
|
'ServerOverloaded': '服务器繁忙,请稍后重试',
|
|
'InternalError': '服务内部错误,请稍后重试',
|
|
'Timeout': '生成超时,请重试',
|
|
}
|
|
|
|
|
|
class AirDramaAPIError(Exception):
|
|
"""Raised when video generation 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 ('airdrama' or 'airdrama_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 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 AirDramaAPIError(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')
|