video-shuoshan/backend/utils/airdrama_client.py
seaislee1209 973a4f049d
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m34s
feat: v0.14.2 推理接入点EP + 参考图片上限9张 + reEdit标签修复
- 推理接入点:model字段优先使用EP接入点ID(ARK_ENDPOINT_SEEDANCE环境变量),无EP降级到模型ID
- 参考图片上限:提交时校验image类型不超过9张,超限返回友好中文提示
- 上传图片标签编号:接着已有素材编号,不再从1重新计数
- 轮询同步assetMentions:polling完成时同时更新references和assetMentions
- reEdit标签修复:用纯文本prompt重建标签,避免blob:URL失效导致图片标签丢失

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 01:43:35 +08:00

190 lines
7.3 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 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.PrivacyInformation': '参考视频中检测到真实人脸,请使用虚拟人像素材替代真人视频',
'InputVideoSensitiveContentDetected': '参考视频包含敏感内容,请更换视频后重试',
'InputTextSensitiveContentDetected': '提示词包含敏感内容,请修改后重试',
'InputAudioSensitiveContentDetected': '参考音频包含敏感内容,请更换音频后重试',
# Output content moderation
'OutputVideoSensitiveContentDetected': '生成的视频包含敏感内容,已被系统拦截,请修改提示词后重试',
'OutputImageSensitiveContentDetected': '生成的图片包含敏感内容,已被系统拦截',
# Parameter errors
'InvalidParameter': '请求参数无效,请检查输入内容',
'InvalidImage': '图片格式或尺寸不符合要求,请检查后重试',
'InvalidVideo': '视频格式或尺寸不符合要求,请检查后重试',
'InvalidAudio': '音频格式不符合要求,请检查后重试',
# Rate limit
'RateLimitExceeded': '请求过于频繁,请稍后重试',
'ConcurrencyLimitExceeded': '当前生成任务过多,请稍后重试',
# Account & billing
'InsufficientBalance': '平台账户余额不足,请联系管理员',
# Asset errors
'AssetNotFound': '引用的素材不存在或已被删除,请检查素材库',
# Server errors
'ServerOverloaded': '服务器繁忙,请稍后重试',
'InternalError': '视频生成服务异常,请稍后重试',
'Timeout': '生成超时,请重试',
}
# 关键词匹配API 返回的 message 中包含这些关键词时,映射为对应中文提示
_MESSAGE_KEYWORDS = {
'face': '检测到真实人脸,请使用虚拟人像素材替代真人照片',
'privacy': '检测到真实人脸,请使用虚拟人像素材替代真人照片',
'sensitive': '内容包含敏感信息,请修改后重试',
'not found': '引用的素材不存在或已被删除,请检查素材库',
'not valid': '请求参数无效,请检查输入内容',
}
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
# 1. 精确匹配 error code
friendly = ERROR_MESSAGES.get(code)
if not friendly:
# 2. 关键词匹配 message 内容
msg_lower = (message or '').lower()
for keyword, hint in _MESSAGE_KEYWORDS.items():
if keyword in msg_lower:
friendly = hint
break
self.user_message = friendly or '生成失败,请重试'
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',
}
# 推理接入点优先:有 EP 用 EP没有降级到模型 ID
def _resolve_model(model):
ep_map = {
'seedance_2.0': settings.ARK_ENDPOINT_SEEDANCE,
'seedance_2.0_fast': settings.ARK_ENDPOINT_SEEDANCE_FAST,
}
ep = ep_map.get(model, '')
if ep:
return ep
return MODEL_MAP.get(model, model)
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, search_mode='off', seed=-1):
"""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.
search_mode: 'smart' to enable internet search, 'off' to disable.
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': _resolve_model(model),
'content': content,
'generate_audio': generate_audio,
'ratio': aspect_ratio,
'duration': duration,
'watermark': False,
'seed': seed,
}
if search_mode and search_mode != 'off':
payload['tools'] = [{'type': 'web_search'}]
import logging
logger = logging.getLogger(__name__)
logger.info('AirDrama API payload: %s', {k: v for k, v in payload.items() if k != 'content'})
# 记录 content 中的非文本项,方便排查素材引用问题
media_items = [ci for ci in content if ci.get('type') != 'text']
if media_items:
logger.info('AirDrama content media items (%d): %s', len(media_items), media_items)
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)
logger.error('AirDrama API error: status=%s code=%s message=%s', resp.status_code, code, message)
except Exception:
code, message = '', resp.text
logger.error('AirDrama API error: status=%s body=%s', resp.status_code, 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')