fix: 修复自由创作首尾帧上传人脸图不通过问题
This commit is contained in:
@@ -30,6 +30,8 @@ _AUDIO_TYPES = {"audio/mpeg": ".mp3", "audio/wav": ".wav", "audio/x-wav": ".wav"
|
||||
_IMAGE_MAX = 30 * 1024 * 1024
|
||||
_VIDEO_MAX = 50 * 1024 * 1024
|
||||
_AUDIO_MAX = 15 * 1024 * 1024
|
||||
DEFAULT_GROUP_NAME = "默认素材"
|
||||
DEFAULT_GROUP_MARKER = "__airshelf_free_asset_default__"
|
||||
|
||||
|
||||
def _serialize_asset(fa: FreeAsset) -> dict:
|
||||
@@ -61,6 +63,99 @@ def _assets_api_unavailable() -> Response:
|
||||
return Response({"detail": "素材库服务未配置,请联系管理员"}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
|
||||
|
||||
def _get_or_create_default_group(*, team, user) -> FreeAssetGroup:
|
||||
group = FreeAssetGroup.objects.filter(team=team, is_deleted=False, description=DEFAULT_GROUP_MARKER).first()
|
||||
if group is not None:
|
||||
return group
|
||||
remote_group_id = assets_client.create_asset_group(DEFAULT_GROUP_NAME, DEFAULT_GROUP_MARKER)
|
||||
return FreeAssetGroup.objects.create(
|
||||
team=team,
|
||||
created_by=user,
|
||||
name=DEFAULT_GROUP_NAME,
|
||||
description=DEFAULT_GROUP_MARKER,
|
||||
remote_group_id=remote_group_id,
|
||||
)
|
||||
|
||||
|
||||
def _upload_asset_to_group(request, *, team, group: FreeAssetGroup, image_only: bool = False) -> Response:
|
||||
upload = request.FILES.get("file")
|
||||
if upload is None:
|
||||
return Response({"detail": "缺少文件"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
content_type = (upload.content_type or "").lower()
|
||||
size = upload.size or 0
|
||||
if content_type in _IMAGE_TYPES:
|
||||
kind, suffix, asset_type = "image", _IMAGE_TYPES[content_type], FreeAsset.Type.IMAGE
|
||||
if size > _IMAGE_MAX:
|
||||
return Response({"detail": "图片大小不能超过 30MB"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
elif image_only:
|
||||
return Response({"detail": "快捷上传仅支持图片 JPG/PNG/WebP"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
elif content_type in _VIDEO_TYPES:
|
||||
kind, suffix, asset_type = "video", _VIDEO_TYPES[content_type], FreeAsset.Type.VIDEO
|
||||
if size > _VIDEO_MAX:
|
||||
return Response({"detail": "视频大小不能超过 50MB"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
elif content_type in _AUDIO_TYPES:
|
||||
kind, suffix, asset_type = "audio", _AUDIO_TYPES[content_type], FreeAsset.Type.AUDIO
|
||||
if size > _AUDIO_MAX:
|
||||
return Response({"detail": "音频大小不能超过 15MB"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
{"detail": "不支持的文件格式(图片 JPG/PNG/WebP,视频 MP4/MOV,音频 MP3/WAV)"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
raw = upload.read()
|
||||
duration = None
|
||||
thumb_url = ""
|
||||
storage = TosStorage()
|
||||
file_id = uuid.uuid4()
|
||||
|
||||
if kind != "image":
|
||||
from apps.ai.media_probe import extract_video_poster, probe_duration
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="airshelf-fc-lib-") as tmp:
|
||||
tmp_path = Path(tmp) / f"in{suffix}"
|
||||
tmp_path.write_bytes(raw)
|
||||
duration = probe_duration(str(tmp_path))
|
||||
if duration is None:
|
||||
return Response({"detail": "媒体文件解析失败,请更换文件"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not (2 <= duration <= 15):
|
||||
label = "视频" if kind == "video" else "音频"
|
||||
return Response({"detail": f"{label}时长需在 2-15 秒之间"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if kind == "video":
|
||||
poster = extract_video_poster(str(tmp_path))
|
||||
if poster:
|
||||
poster_key = f"teams/{team.id}/free-create/library/{file_id}-poster.jpg"
|
||||
storage.upload_fileobj(fileobj=BytesIO(poster), object_key=poster_key, content_type="image/jpeg")
|
||||
thumb_url = storage.public_url(object_key=poster_key)
|
||||
|
||||
object_key = f"teams/{team.id}/free-create/library/{file_id}{suffix}"
|
||||
stored = storage.upload_fileobj(fileobj=BytesIO(raw), object_key=object_key, content_type=content_type)
|
||||
url = storage.public_url(object_key=stored.object_key)
|
||||
|
||||
name = str(request.data.get("name") or "").strip() or (upload.name or f"素材{suffix}")
|
||||
name = name[:128]
|
||||
try:
|
||||
remote_asset_id = assets_client.create_asset(group.remote_group_id, url, name=name, asset_type=asset_type)
|
||||
except AssetsAPIError as exc:
|
||||
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
fa = FreeAsset.objects.create(
|
||||
group=group,
|
||||
remote_asset_id=remote_asset_id,
|
||||
name=name,
|
||||
url=url,
|
||||
asset_type=asset_type,
|
||||
thumbnail_url=thumb_url or (url if kind == "image" else ""),
|
||||
duration=duration,
|
||||
status=FreeAsset.Status.PROCESSING,
|
||||
)
|
||||
if not group.thumbnail_url and fa.thumbnail_url:
|
||||
group.thumbnail_url = fa.thumbnail_url
|
||||
group.save(update_fields=["thumbnail_url", "updated_at"])
|
||||
return Response({"group": _serialize_group(group), "asset": _serialize_asset(fa)}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class FreeAssetGroupListView(APIView):
|
||||
"""GET 列素材组 / POST 建组。"""
|
||||
|
||||
@@ -152,82 +247,26 @@ class FreeAssetUploadView(APIView):
|
||||
group = FreeAssetGroup.objects.filter(id=group_id, team=team, is_deleted=False).first()
|
||||
if group is None:
|
||||
return Response({"detail": "素材组不存在"}, status=status.HTTP_404_NOT_FOUND)
|
||||
upload = request.FILES.get("file")
|
||||
if upload is None:
|
||||
return Response({"detail": "缺少文件"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
response = _upload_asset_to_group(request, team=team, group=group)
|
||||
if response.status_code == status.HTTP_201_CREATED:
|
||||
response.data.pop("group", None)
|
||||
return response
|
||||
|
||||
content_type = (upload.content_type or "").lower()
|
||||
size = upload.size or 0
|
||||
if content_type in _IMAGE_TYPES:
|
||||
kind, suffix, asset_type = "image", _IMAGE_TYPES[content_type], FreeAsset.Type.IMAGE
|
||||
if size > _IMAGE_MAX:
|
||||
return Response({"detail": "图片大小不能超过 30MB"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
elif content_type in _VIDEO_TYPES:
|
||||
kind, suffix, asset_type = "video", _VIDEO_TYPES[content_type], FreeAsset.Type.VIDEO
|
||||
if size > _VIDEO_MAX:
|
||||
return Response({"detail": "视频大小不能超过 50MB"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
elif content_type in _AUDIO_TYPES:
|
||||
kind, suffix, asset_type = "audio", _AUDIO_TYPES[content_type], FreeAsset.Type.AUDIO
|
||||
if size > _AUDIO_MAX:
|
||||
return Response({"detail": "音频大小不能超过 15MB"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
{"detail": "不支持的文件格式(图片 JPG/PNG/WebP,视频 MP4/MOV,音频 MP3/WAV)"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
raw = upload.read()
|
||||
duration = None
|
||||
thumb_url = ""
|
||||
storage = TosStorage()
|
||||
file_id = uuid.uuid4()
|
||||
class FreeAssetQuickUploadView(APIView):
|
||||
"""POST /free-assets/quick-upload/ —— 上传图片到默认组,减少首尾帧用户建组步骤。"""
|
||||
|
||||
if kind != "image":
|
||||
from apps.ai.media_probe import extract_video_poster, probe_duration
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="airshelf-fc-lib-") as tmp:
|
||||
tmp_path = Path(tmp) / f"in{suffix}"
|
||||
tmp_path.write_bytes(raw)
|
||||
duration = probe_duration(str(tmp_path))
|
||||
if duration is None:
|
||||
return Response({"detail": "媒体文件解析失败,请更换文件"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not (2 <= duration <= 15):
|
||||
label = "视频" if kind == "video" else "音频"
|
||||
return Response({"detail": f"{label}时长需在 2-15 秒之间"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if kind == "video":
|
||||
poster = extract_video_poster(str(tmp_path))
|
||||
if poster:
|
||||
poster_key = f"teams/{team.id}/free-create/library/{file_id}-poster.jpg"
|
||||
storage.upload_fileobj(fileobj=BytesIO(poster), object_key=poster_key, content_type="image/jpeg")
|
||||
thumb_url = storage.public_url(object_key=poster_key)
|
||||
|
||||
object_key = f"teams/{team.id}/free-create/library/{file_id}{suffix}"
|
||||
stored = storage.upload_fileobj(fileobj=BytesIO(raw), object_key=object_key, content_type=content_type)
|
||||
url = storage.public_url(object_key=stored.object_key)
|
||||
|
||||
name = str(request.data.get("name") or "").strip() or (upload.name or f"素材{suffix}")
|
||||
name = name[:128]
|
||||
def post(self, request):
|
||||
if not assets_client.is_enabled():
|
||||
return _assets_api_unavailable()
|
||||
team = get_current_team(request.user)
|
||||
try:
|
||||
remote_asset_id = assets_client.create_asset(
|
||||
group.remote_group_id, url, name=name, asset_type=asset_type
|
||||
)
|
||||
group = _get_or_create_default_group(team=team, user=request.user)
|
||||
except AssetsAPIError as exc:
|
||||
return Response({"detail": exc.user_message}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
fa = FreeAsset.objects.create(
|
||||
group=group,
|
||||
remote_asset_id=remote_asset_id,
|
||||
name=name,
|
||||
url=url,
|
||||
asset_type=asset_type,
|
||||
thumbnail_url=thumb_url or (url if kind == "image" else ""),
|
||||
duration=duration,
|
||||
status=FreeAsset.Status.PROCESSING,
|
||||
)
|
||||
if not group.thumbnail_url and fa.thumbnail_url:
|
||||
group.thumbnail_url = fa.thumbnail_url
|
||||
group.save(update_fields=["thumbnail_url", "updated_at"])
|
||||
return Response({"asset": _serialize_asset(fa)}, status=status.HTTP_201_CREATED)
|
||||
return _upload_asset_to_group(request, team=team, group=group, image_only=True)
|
||||
|
||||
|
||||
class FreeAssetDetailView(APIView):
|
||||
|
||||
Reference in New Issue
Block a user