fix: 统一AI生成失败提示
This commit is contained in:
@@ -299,8 +299,8 @@ class ProjectApiTests(TestCase):
|
||||
# 没二次预扣额度(复用直接 return,根本没走到 create_ai_task)
|
||||
self.assertEqual(CreditLedger.objects.filter(team=self.team, ledger_type=CreditLedger.Type.RESERVE).count(), 0)
|
||||
|
||||
def test_extract_status_reports_running_then_failure(self):
|
||||
"""extract-status:在途→running=True;失败→running=False 且把可读错误透给前端(刷新后也能看到失败原因)。"""
|
||||
def test_extract_status_reports_running_then_safe_failure(self):
|
||||
"""extract-status:在途→running=True;失败→安全错误对象,原始任务错误不透给前端。"""
|
||||
project = Project.objects.create(team=self.team, created_by=self.user, product=self.product, name="P4")
|
||||
t = AITask.objects.create(
|
||||
team=self.team, created_by=self.user, project=project, model_config=self.model,
|
||||
@@ -317,7 +317,8 @@ class ProjectApiTests(TestCase):
|
||||
res = self.client.get(f"/api/projects/{project.id}/extract-status/")
|
||||
self.assertFalse(res.data["running"])
|
||||
self.assertEqual(res.data["status"], "failed")
|
||||
self.assertIn("未返回有效 JSON", res.data["error"])
|
||||
self.assertEqual(res.data["error"]["code"], "unknown")
|
||||
self.assertNotIn("有效 JSON", res.data["error_message"])
|
||||
|
||||
@patch("apps.ai.services._store_generated_media")
|
||||
@patch("apps.ai.services.get_image_provider")
|
||||
@@ -811,7 +812,8 @@ class ProjectApiTests(TestCase):
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("语音合成未配置", response.json()["detail"])
|
||||
self.assertEqual(response.json()["error"]["code"], "provider_config_error")
|
||||
self.assertIn("配音暂时不可用", response.json()["detail"])
|
||||
self.assertEqual(CreditLedger.objects.filter(team=self.team, ledger_type=CreditLedger.Type.CHARGE).count(), 0)
|
||||
|
||||
def test_export_command_mixes_voiceover_above_bgm(self):
|
||||
|
||||
@@ -38,6 +38,7 @@ from apps.assets.serializers import AssetFileSerializer
|
||||
from apps.assets.storage import TosStorage
|
||||
from apps.common.api import TeamScopedViewSetMixin
|
||||
from apps.common.celery_health import require_worker
|
||||
from apps.ai.generation_errors import classify_generation_error, public_error_for_task
|
||||
|
||||
from .models import (
|
||||
BaseAssetGroup,
|
||||
@@ -504,7 +505,19 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
try:
|
||||
task = submit_extract_entities(project=project, user=request.user)
|
||||
except ValueError as exc: # 无脚本 / 无分镜 / 无模型 / 额度不足,立即反馈
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
message = str(exc)
|
||||
internal_kind = (
|
||||
"user_credit_insufficient" if "额度不足" in message
|
||||
else "model_unavailable" if "模型" in message
|
||||
else "invalid_input"
|
||||
)
|
||||
public_error = classify_generation_error(
|
||||
exc, operation="entity_extract", internal_kind=internal_kind
|
||||
)
|
||||
return Response(
|
||||
{"detail": public_error.fallback_message, "error": public_error.as_dict()},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response({"task_id": str(task.id), "status": task.status})
|
||||
|
||||
@action(detail=True, methods=["get"], url_path="extract-status")
|
||||
@@ -514,17 +527,24 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
project = self.get_object()
|
||||
inflight = get_inflight_extraction(project)
|
||||
if inflight is not None:
|
||||
return Response({"running": True, "status": inflight.status, "error": "", "entities": []})
|
||||
return Response({"running": True, "status": inflight.status, "error": None, "error_message": "", "entities": []})
|
||||
last = (
|
||||
AITask.objects.filter(project=project, task_type=AITask.Type.ENTITY_EXTRACTION)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
if last is None:
|
||||
return Response({"running": False, "status": "", "error": "", "entities": []})
|
||||
return Response({"running": False, "status": "", "error": None, "error_message": "", "entities": []})
|
||||
entities = (project.metadata or {}).get("script_entities", []) if last.status == AITask.Status.SUCCEEDED else []
|
||||
public_error = public_error_for_task(last, operation="entity_extract")
|
||||
return Response(
|
||||
{"running": False, "status": last.status, "error": last.error_message or "", "entities": entities}
|
||||
{
|
||||
"running": False,
|
||||
"status": last.status,
|
||||
"error": public_error.as_dict() if public_error else None,
|
||||
"error_message": public_error.fallback_message if public_error else "",
|
||||
"entities": entities,
|
||||
}
|
||||
)
|
||||
|
||||
@action(detail=True, methods=["post"], url_path="generate-base-asset")
|
||||
@@ -546,7 +566,14 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
auto_triview=False,
|
||||
)
|
||||
except ValueError as exc: # 无可用模型 / 余额不足等,立即反馈
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
internal_kind = "user_credit_insufficient" if str(exc).strip().lower() == "insufficient credit" else ""
|
||||
public_error = classify_generation_error(
|
||||
exc, operation="base_asset_generate", internal_kind=internal_kind
|
||||
)
|
||||
return Response(
|
||||
{"detail": public_error.fallback_message, "error": public_error.as_dict()},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
stage, _ = ProjectStage.objects.get_or_create(project=project, stage=ProjectStage.Stage.BASE_ASSETS)
|
||||
stage.status = ProjectStage.Status.NEEDS_REVIEW
|
||||
stage.save(update_fields=["status", "updated_at"])
|
||||
@@ -749,7 +776,14 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
try:
|
||||
task = generate_person_triview(project=project, user=request.user, portrait_asset=portrait_asset)
|
||||
except ValueError as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
internal_kind = "user_credit_insufficient" if str(exc).strip().lower() == "insufficient credit" else ""
|
||||
public_error = classify_generation_error(
|
||||
exc, operation="triview_generate", internal_kind=internal_kind
|
||||
)
|
||||
return Response(
|
||||
{"detail": public_error.fallback_message, "error": public_error.as_dict()},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# 异步:image_edit 出图在 worker 里跑,秒回 RESERVED 任务,前端轮询取结果后刷新项目即见三视图
|
||||
return Response({"task": {"id": str(task.id), "status": task.status}}, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
@@ -1010,11 +1044,15 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
project = self.get_object()
|
||||
segment = VideoSegment.objects.get(project=project, id=request.data.get("video_segment_id"))
|
||||
# 火山/中转报错(如人脸需走素材库的 InputImageSensitiveContentDetected)→ 返回真实报错的 JSON,
|
||||
# 前端如实显示(段已被 submit_video_segment 标 FAILED + error_message);绝不让它变 500 HTML → 前端白屏。
|
||||
# 任务仍保留原始错误供排障,但普通用户仅收到安全错误对象,不让 500 HTML 导致前端白屏。
|
||||
try:
|
||||
submit_video_segment(video_segment=segment, user=request.user, prompt=request.data.get("prompt", ""))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_502_BAD_GATEWAY)
|
||||
public_error = classify_generation_error(exc, operation="video_generate")
|
||||
return Response(
|
||||
{"detail": public_error.fallback_message, "error": public_error.as_dict()},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
# worker 在线由上面的闸保证;此处入队失败只剩极小窗口(刚提交完 broker 闪断),
|
||||
# 任务已在 ARK,前端轮询仍可兜底取回,故不让提交 500。
|
||||
try:
|
||||
@@ -1184,7 +1222,27 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
speed_ratio=float(request.data.get("speed_ratio") or 1.0),
|
||||
)
|
||||
except (TtsNotConfigured, ValueError) as exc:
|
||||
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
message = str(exc)
|
||||
internal_kind = (
|
||||
"user_credit_insufficient" if "insufficient credit" in message.lower()
|
||||
else "model_unavailable" if "audio model" in message.lower()
|
||||
else "invalid_input"
|
||||
)
|
||||
public_error = classify_generation_error(
|
||||
exc,
|
||||
operation="voiceover_generate",
|
||||
internal_kind=internal_kind if not isinstance(exc, TtsNotConfigured) else "",
|
||||
)
|
||||
return Response(
|
||||
{"detail": public_error.fallback_message, "error": public_error.as_dict()},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 — TTS 上游异常已在任务中留原始信息,HTTP 仅返回安全提示。
|
||||
public_error = classify_generation_error(exc, operation="voiceover_generate")
|
||||
return Response(
|
||||
{"detail": public_error.fallback_message, "error": public_error.as_dict()},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
return Response({"voiceover": voiceover}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=True, methods=["post", "put"], url_path="save-timeline")
|
||||
|
||||
Reference in New Issue
Block a user