完成极速成片
This commit is contained in:
@@ -20,10 +20,10 @@ from apps.ai.services import (
|
||||
create_export_job,
|
||||
generate_base_asset,
|
||||
generate_person_triview,
|
||||
get_default_model,
|
||||
poll_storyboard,
|
||||
submit_storyboard,
|
||||
submit_video_segment,
|
||||
video_segment_has_inflight_task,
|
||||
)
|
||||
from apps.assets import assets_client
|
||||
from apps.assets.models import Asset
|
||||
@@ -43,11 +43,13 @@ from apps.projects.services.pipeline import (
|
||||
adopt_script_version,
|
||||
finish_storyboard_stage,
|
||||
finish_video_stage,
|
||||
sync_video_segments_to_script,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
POLL_DELAY_SECONDS = 10
|
||||
QUICK_SCRIPT_MODEL_NAME = "doubao-seed-2-1-pro-260628"
|
||||
SCRIPT_POLL_SECONDS = 5
|
||||
SCRIPT_TIMEOUT = timedelta(minutes=4)
|
||||
SCRIPT_STOLEN_AFTER = timedelta(seconds=25)
|
||||
@@ -70,6 +72,24 @@ def _is_finished(job: QuickCreateJob) -> bool:
|
||||
return job.status in _FINISHED_STATUSES
|
||||
|
||||
|
||||
def get_quick_script_model() -> ModelConfig | None:
|
||||
"""极速成片固定复用专业创作的豆包 Seed 2.1 Pro 脚本模型。
|
||||
|
||||
不回退到默认文本模型,避免默认配置切到 DeepSeek 后两条创作链路的成片质量不一致。
|
||||
"""
|
||||
return (
|
||||
ModelConfig.objects.select_related("provider")
|
||||
.filter(
|
||||
name=QUICK_SCRIPT_MODEL_NAME,
|
||||
capability=ModelConfig.Capability.TEXT,
|
||||
status=ModelConfig.Status.ACTIVE,
|
||||
provider__status="active",
|
||||
)
|
||||
.order_by("created_at")
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def _quick_settings(project: Project) -> dict:
|
||||
wizard = dict((project.metadata or {}).get("wizard") or {})
|
||||
return {
|
||||
@@ -97,16 +117,22 @@ TRANSIENT_RETRY_LIMIT = 8
|
||||
def _safe_error(exc: Exception) -> str:
|
||||
raw = str(exc or "").strip()
|
||||
lower = raw.lower()
|
||||
if QUICK_SCRIPT_MODEL_NAME in lower:
|
||||
return "极速成片需要的豆包 Seed 2.1 Pro 模型未启用,请联系管理员配置"
|
||||
if "insufficient credit" in lower or "额度不足" in raw:
|
||||
return "可用积分不足,极速成片已暂停"
|
||||
if "no active" in lower or "not configured" in lower or "没有可用" in raw:
|
||||
return "当前缺少可用的生成模型,请联系管理员配置"
|
||||
if "review" in lower or "审核" in raw:
|
||||
return "生成素材未通过审核,请进入专业模式调整后重试"
|
||||
if _is_retryable_exc(exc):
|
||||
return "脚本已保留,后续步骤遇到网络波动。点重试会从上次进度继续"
|
||||
return "极速成片暂未完成,请稍后重试或进入专业模式查看"
|
||||
|
||||
|
||||
def _is_retryable_exc(exc: Exception) -> bool:
|
||||
if isinstance(exc, TimeoutError):
|
||||
return True
|
||||
text = str(exc or "").lower()
|
||||
return any(
|
||||
token in text
|
||||
@@ -123,6 +149,10 @@ def _is_retryable_exc(exc: Exception) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _transient_internal(job: QuickCreateJob) -> bool:
|
||||
return _is_retryable_exc(Exception(str((job.metadata or {}).get("internal_error") or "")))
|
||||
|
||||
|
||||
def _videos_have_started(job: QuickCreateJob) -> bool:
|
||||
if (job.metadata or {}).get("video_started"):
|
||||
return True
|
||||
@@ -131,9 +161,12 @@ def _videos_have_started(job: QuickCreateJob) -> bool:
|
||||
|
||||
def _should_mark_project_failed(job: QuickCreateJob) -> bool:
|
||||
"""编排在「还没申请生成视频」时超时,不能把整个项目打成失败。"""
|
||||
if job.phase != QuickCreateJob.Phase.PRODUCTION:
|
||||
return True
|
||||
return _videos_have_started(job)
|
||||
if job.phase == QuickCreateJob.Phase.PRODUCTION:
|
||||
return _videos_have_started(job)
|
||||
# 脚本已经落库时,资产/脚本阶段的网络抖动不要把整个项目打成失败。
|
||||
if job.phase in {QuickCreateJob.Phase.SCRIPT, QuickCreateJob.Phase.ASSETS} and _adopted_script(job.project) is not None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def restore_false_failed_quick_creates(team) -> None:
|
||||
@@ -241,9 +274,9 @@ def _consume_script_agent(job: QuickCreateJob) -> None:
|
||||
user = job.created_by or project.created_by
|
||||
if user is None:
|
||||
raise ValueError("极速成片任务缺少创建人")
|
||||
model_config = get_default_model(ModelConfig.Capability.TEXT)
|
||||
model_config = get_quick_script_model()
|
||||
if model_config is None:
|
||||
raise ValueError("no active text model configured")
|
||||
raise ValueError(f"{QUICK_SCRIPT_MODEL_NAME} is not configured")
|
||||
|
||||
error_detail = ""
|
||||
stream = stream_script_agent(
|
||||
@@ -452,11 +485,8 @@ def _start_base_assets(job: QuickCreateJob) -> None:
|
||||
with transaction.atomic():
|
||||
job = QuickCreateJob.objects.select_for_update().select_related("project").get(id=job.id)
|
||||
metadata = dict(job.metadata or {})
|
||||
if metadata.get("base_asset_task_ids") or metadata.get("assets_started"):
|
||||
if metadata.get("base_asset_task_ids"):
|
||||
return
|
||||
metadata["assets_started"] = True
|
||||
job.metadata = metadata
|
||||
job.save(update_fields=["metadata", "updated_at"])
|
||||
entities = _ensure_fallback_entities(job.project)
|
||||
specs = [
|
||||
(BaseAssetGroup.Kind.PRODUCT, job.project.product.title, job.project.product.title),
|
||||
@@ -477,6 +507,7 @@ def _start_base_assets(job: QuickCreateJob) -> None:
|
||||
task_ids.append(str(task.id))
|
||||
metadata = dict(job.metadata or {})
|
||||
metadata["base_asset_task_ids"] = task_ids
|
||||
metadata["assets_started"] = True
|
||||
_save_job(job, metadata=metadata, message="正在生成商品、模特与场景资产", progress=52)
|
||||
stage, _ = ProjectStage.objects.get_or_create(project=job.project, stage=ProjectStage.Stage.BASE_ASSETS)
|
||||
stage.status = ProjectStage.Status.RUNNING
|
||||
@@ -622,14 +653,33 @@ def _reviews_ready(job: QuickCreateJob) -> bool | None:
|
||||
def _start_videos(job: QuickCreateJob) -> None:
|
||||
from apps.projects.tasks import poll_video_segment_task
|
||||
|
||||
adopted = _adopted_script(job.project)
|
||||
if adopted is not None:
|
||||
sync_video_segments_to_script(job.project, adopted)
|
||||
|
||||
settings = _quick_settings(job.project)
|
||||
segments = list(job.project.video_segments.order_by("sort_order"))
|
||||
claimed_ids: list = []
|
||||
with transaction.atomic():
|
||||
segments = list(
|
||||
VideoSegment.objects.select_for_update()
|
||||
.filter(project=job.project)
|
||||
.order_by("sort_order")
|
||||
)
|
||||
for segment in segments:
|
||||
if segment.status in {VideoSegment.Status.RUNNING, VideoSegment.Status.SUCCEEDED}:
|
||||
continue
|
||||
if video_segment_has_inflight_task(segment):
|
||||
continue
|
||||
if segment.status == VideoSegment.Status.QUEUED and timezone.now() - segment.updated_at < timedelta(minutes=2):
|
||||
continue
|
||||
segment.status = VideoSegment.Status.QUEUED
|
||||
segment.save(update_fields=["status", "updated_at"])
|
||||
claimed_ids.append(segment.id)
|
||||
|
||||
submitted = 0
|
||||
last_error: Exception | None = None
|
||||
for segment in segments:
|
||||
if segment.status in {VideoSegment.Status.RUNNING, VideoSegment.Status.QUEUED, VideoSegment.Status.SUCCEEDED}:
|
||||
submitted += 1
|
||||
continue
|
||||
for segment_id in claimed_ids:
|
||||
segment = VideoSegment.objects.get(id=segment_id)
|
||||
try:
|
||||
submit_video_segment(
|
||||
video_segment=segment,
|
||||
@@ -644,13 +694,22 @@ def _start_videos(job: QuickCreateJob) -> None:
|
||||
except Exception as exc: # noqa: BLE001 — 单镜提交失败下一轮再试,不把整单打断
|
||||
last_error = exc
|
||||
logger.warning("quick create job %s failed to start video %s: %s", job.id, segment.sort_order, exc)
|
||||
if segment.status == VideoSegment.Status.QUEUED and not video_segment_has_inflight_task(segment):
|
||||
segment.status = VideoSegment.Status.NOT_STARTED
|
||||
segment.save(update_fields=["status", "updated_at"])
|
||||
if not _is_retryable_exc(exc):
|
||||
raise
|
||||
break
|
||||
metadata = dict(job.metadata or {})
|
||||
if last_error is not None:
|
||||
metadata["internal_error"] = str(last_error)[:2000]
|
||||
if submitted >= len(segments) and segments:
|
||||
segments = list(job.project.video_segments.order_by("sort_order"))
|
||||
started = all(
|
||||
segment.status in {VideoSegment.Status.RUNNING, VideoSegment.Status.QUEUED, VideoSegment.Status.SUCCEEDED}
|
||||
or video_segment_has_inflight_task(segment)
|
||||
for segment in segments
|
||||
)
|
||||
if started and segments:
|
||||
metadata["video_started"] = True
|
||||
_save_job(
|
||||
job,
|
||||
@@ -757,6 +816,7 @@ def _advance_production(job: QuickCreateJob) -> int | None:
|
||||
if not review_state:
|
||||
return POLL_DELAY_SECONDS
|
||||
|
||||
job.refresh_from_db(fields=["metadata"])
|
||||
if not (job.metadata or {}).get("video_started"):
|
||||
_start_videos(job)
|
||||
return POLL_DELAY_SECONDS
|
||||
@@ -897,6 +957,17 @@ def recover_quick_create(job: QuickCreateJob) -> None:
|
||||
return
|
||||
if job.status == QuickCreateJob.Status.FAILED:
|
||||
_restore_project_after_orchestrator_timeout(job)
|
||||
job.refresh_from_db()
|
||||
retries = int((job.metadata or {}).get("transient_retries") or 0)
|
||||
if (
|
||||
job.status == QuickCreateJob.Status.FAILED
|
||||
and job.phase in {QuickCreateJob.Phase.SCRIPT, QuickCreateJob.Phase.ASSETS}
|
||||
and _adopted_script(job.project) is not None
|
||||
and _transient_internal(job)
|
||||
and retries < TRANSIENT_RETRY_LIMIT
|
||||
):
|
||||
_save_job(job, status=QuickCreateJob.Status.RUNNING, error_message="", message="网络波动,正在继续生成…")
|
||||
_enqueue_advance(job)
|
||||
return
|
||||
if _is_finished(job):
|
||||
return
|
||||
|
||||
@@ -45,15 +45,15 @@ class QuickCreateApiTests(TestCase):
|
||||
category=Asset.Category.PRODUCT_IMAGE,
|
||||
)
|
||||
|
||||
@patch("apps.projects.views.get_default_model", return_value=object())
|
||||
@patch("apps.projects.services.quick_create.get_quick_script_model", return_value=object())
|
||||
@patch("apps.projects.views.get_default_model")
|
||||
@patch("apps.projects.views.advance_quick_create_task.apply_async")
|
||||
@patch("apps.projects.views.require_worker_task")
|
||||
@patch("apps.projects.views._store_uploaded_asset")
|
||||
def test_submit_creates_product_project_and_persistent_job(self, store_asset, require_worker_task, enqueue, get_model):
|
||||
def test_submit_creates_product_project_and_persistent_job(self, store_asset, require_worker_task, enqueue, get_model, get_quick_model):
|
||||
store_asset.side_effect = self._uploaded_asset
|
||||
video_model_id = uuid.uuid4()
|
||||
get_model.side_effect = [
|
||||
object(),
|
||||
object(),
|
||||
SimpleNamespace(
|
||||
id=video_model_id,
|
||||
@@ -95,18 +95,20 @@ class QuickCreateApiTests(TestCase):
|
||||
self.assertEqual(enqueue.call_args.kwargs["args"], [str(job.id)])
|
||||
self.assertEqual(enqueue.call_args.kwargs["queue"], "airshelf.quick")
|
||||
require_worker_task.assert_called_once_with("apps.projects.tasks.advance_quick_create_task")
|
||||
self.assertEqual(get_model.call_count, 3)
|
||||
self.assertEqual(get_model.call_count, 2)
|
||||
get_quick_model.assert_called_once()
|
||||
self.assertEqual(len(response.data["product_images"]), 2)
|
||||
self.assertEqual(
|
||||
{item["asset_id"] for item in response.data["product_images"]},
|
||||
{str(image.asset_id) for image in product.images.order_by("sort_order")},
|
||||
)
|
||||
|
||||
@patch("apps.projects.services.quick_create.get_quick_script_model", return_value=object())
|
||||
@patch("apps.projects.views.get_default_model", return_value=object())
|
||||
@patch("apps.projects.views.advance_quick_create_task.apply_async")
|
||||
@patch("apps.projects.views.require_worker_task")
|
||||
@patch("apps.projects.views._store_uploaded_asset")
|
||||
def test_submit_reuses_source_product_images(self, store_asset, require_worker_task, enqueue, get_model):
|
||||
def test_submit_reuses_source_product_images(self, store_asset, require_worker_task, enqueue, get_model, get_quick_model):
|
||||
source_asset = Asset.objects.create(
|
||||
team=self.team,
|
||||
created_by=self.user,
|
||||
@@ -117,7 +119,7 @@ class QuickCreateApiTests(TestCase):
|
||||
)
|
||||
source = Product.objects.create(team=self.team, created_by=self.user, title="旧商品", cover_asset=source_asset)
|
||||
ProductImage.objects.create(product=source, asset=source_asset, sort_order=0, is_primary=True)
|
||||
get_model.side_effect = [object(), object(), SimpleNamespace(id=uuid.uuid4(), name="seedance", display_name="Seedance", metadata={})]
|
||||
get_model.side_effect = [object(), SimpleNamespace(id=uuid.uuid4(), name="seedance", display_name="Seedance", metadata={})]
|
||||
|
||||
response = self.client.post(
|
||||
"/api/projects/quick-create/",
|
||||
@@ -138,11 +140,14 @@ class QuickCreateApiTests(TestCase):
|
||||
self.assertEqual(product.images.first().asset_id, source_asset.id)
|
||||
self.assertEqual(response.data["product_images"][0]["asset_id"], str(source_asset.id))
|
||||
|
||||
@patch("apps.projects.services.quick_create.get_quick_script_model", return_value=object())
|
||||
@patch("apps.projects.views.get_default_model", return_value=object())
|
||||
@patch("apps.projects.views.require_worker_task")
|
||||
def test_submit_rejects_foreign_source_product_images(self, require_worker_task):
|
||||
def test_submit_rejects_foreign_source_product_images(self, require_worker_task, get_model, get_quick_model):
|
||||
other = User.objects.create_user(username="quick-image-other", password="pass")
|
||||
other_team = Team.objects.create(name="Image Other Team", owner=other)
|
||||
foreign = Product.objects.create(team=other_team, created_by=other, title="别人的图")
|
||||
get_model.side_effect = [object(), SimpleNamespace(metadata={})]
|
||||
response = self.client.post(
|
||||
"/api/projects/quick-create/",
|
||||
{
|
||||
@@ -193,9 +198,10 @@ class QuickCreateApiTests(TestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertFalse(Product.objects.filter(title="错误文件").exists())
|
||||
|
||||
@patch("apps.projects.services.quick_create.get_quick_script_model", return_value=object())
|
||||
@patch("apps.projects.views.get_default_model", return_value=object())
|
||||
@patch("apps.projects.views.require_worker_task")
|
||||
def test_submit_rejects_resolution_not_supported_by_selected_model(self, require_worker_task, get_model):
|
||||
def test_submit_rejects_resolution_not_supported_by_selected_model(self, require_worker_task, get_model, get_quick_model):
|
||||
provider = ModelProvider.objects.create(name="quick-video-provider", display_name="Quick", status="active")
|
||||
model = ModelConfig.objects.create(
|
||||
provider=provider,
|
||||
@@ -520,6 +526,51 @@ class QuickCreateCoordinatorTests(TestCase):
|
||||
self.assertEqual(self.job.status, QuickCreateJob.Status.SUCCEEDED)
|
||||
self.assertEqual(self.job.phase, QuickCreateJob.Phase.COMPLETE)
|
||||
|
||||
@patch("apps.projects.services.quick_create.generate_base_asset", side_effect=TimeoutError("Timeout reading from socket"))
|
||||
def test_asset_start_timeout_retries_without_locking_the_job(self, _generate):
|
||||
script = ScriptVersion.objects.create(project=self.project, title="脚本", content="口播", is_adopted=True)
|
||||
ScriptSegment.objects.create(script_version=script, sort_order=0, narration="开场")
|
||||
self.job.status = QuickCreateJob.Status.RUNNING
|
||||
self.job.phase = QuickCreateJob.Phase.ASSETS
|
||||
self.job.save(update_fields=["status", "phase", "updated_at"])
|
||||
|
||||
delay = advance_quick_create(str(self.job.id))
|
||||
self.job.refresh_from_db()
|
||||
self.project.refresh_from_db()
|
||||
self.assertEqual(delay, 10)
|
||||
self.assertEqual(self.job.status, QuickCreateJob.Status.RUNNING)
|
||||
self.assertFalse(self.job.metadata.get("assets_started"))
|
||||
self.assertFalse(self.job.metadata.get("base_asset_task_ids"))
|
||||
self.assertNotEqual(self.project.status, Project.Status.FAILED)
|
||||
|
||||
@patch("apps.projects.tasks.advance_quick_create_task.apply_async")
|
||||
def test_recover_resumes_asset_timeout_after_script_exists(self, enqueue):
|
||||
script = ScriptVersion.objects.create(project=self.project, title="脚本", content="口播", is_adopted=True)
|
||||
ScriptSegment.objects.create(script_version=script, sort_order=0, narration="开场")
|
||||
self.job.status = QuickCreateJob.Status.FAILED
|
||||
self.job.phase = QuickCreateJob.Phase.ASSETS
|
||||
self.job.error_message = "脚本已保留,后续步骤遇到网络波动。点重试会从上次进度继续"
|
||||
self.job.metadata = {"internal_error": "Timeout reading from socket", "transient_retries": 1}
|
||||
self.job.save(update_fields=["status", "phase", "error_message", "metadata", "updated_at"])
|
||||
|
||||
recover_quick_create(self.job)
|
||||
self.job.refresh_from_db()
|
||||
self.assertEqual(self.job.status, QuickCreateJob.Status.RUNNING)
|
||||
enqueue.assert_called_once()
|
||||
|
||||
def test_timeout_after_script_does_not_fail_project(self):
|
||||
script = ScriptVersion.objects.create(project=self.project, title="脚本", content="口播", is_adopted=True)
|
||||
ScriptSegment.objects.create(script_version=script, sort_order=0, narration="开场")
|
||||
self.job.status = QuickCreateJob.Status.RUNNING
|
||||
self.job.phase = QuickCreateJob.Phase.ASSETS
|
||||
self.job.save(update_fields=["status", "phase", "updated_at"])
|
||||
|
||||
fail_quick_create(self.job, "脚本已保留,后续步骤遇到网络波动。点重试会从上次进度继续", internal_error="Timeout reading from socket")
|
||||
self.job.refresh_from_db()
|
||||
self.project.refresh_from_db()
|
||||
self.assertEqual(self.job.status, QuickCreateJob.Status.FAILED)
|
||||
self.assertNotEqual(self.project.status, Project.Status.FAILED)
|
||||
|
||||
@patch("apps.projects.services.quick_create._reviews_ready", return_value=True)
|
||||
def test_production_counts_ready_videos_without_adding_version_ids(self, _reviews):
|
||||
self.project.video_segments.exclude(sort_order=0).delete()
|
||||
@@ -623,3 +674,16 @@ class QuickCreateCoordinatorTests(TestCase):
|
||||
self.assertEqual(call.kwargs["model_config_id"], "11111111-1111-4111-8111-111111111111")
|
||||
self.job.refresh_from_db()
|
||||
self.assertIn("30秒 16:9", self.job.message)
|
||||
|
||||
@patch("apps.projects.tasks.poll_video_segment_task.apply_async")
|
||||
@patch("apps.projects.services.quick_create.submit_video_segment")
|
||||
def test_start_videos_does_not_resubmit_when_called_twice(self, submit_video, _schedule_poll):
|
||||
self.project.video_segments.exclude(sort_order__lt=2).delete()
|
||||
self.assertEqual(self.project.video_segments.count(), 2)
|
||||
|
||||
_start_videos(self.job)
|
||||
_start_videos(self.job)
|
||||
|
||||
self.assertEqual(submit_video.call_count, 2)
|
||||
statuses = list(self.project.video_segments.order_by("sort_order").values_list("status", flat=True))
|
||||
self.assertEqual(statuses, [VideoSegment.Status.QUEUED, VideoSegment.Status.QUEUED])
|
||||
|
||||
@@ -499,7 +499,10 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
if total_duration not in {15, 30, 45, 60}:
|
||||
return Response({"detail": "视频时长仅支持15、30、45或60秒"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
text_model = get_default_model(ModelConfig.Capability.TEXT)
|
||||
# 极速成片和专业创作使用同一款豆包脚本模型;绝不因默认模型变化而退回 DeepSeek。
|
||||
from .services.quick_create import get_quick_script_model
|
||||
|
||||
text_model = get_quick_script_model()
|
||||
image_model = get_default_model(ModelConfig.Capability.IMAGE)
|
||||
requested_video_model_id = str(request.data.get("video_model_config_id") or "").strip()
|
||||
video_model = None
|
||||
@@ -523,9 +526,14 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
|
||||
missing = [
|
||||
label
|
||||
for model, label in ((text_model, "文本"), (image_model, "图像"), (video_model, "视频"))
|
||||
for model, label in ((image_model, "图像"), (video_model, "视频"))
|
||||
if model is None
|
||||
]
|
||||
if text_model is None:
|
||||
return Response(
|
||||
{"detail": "极速成片需要的豆包 Seed 2.1 Pro 模型未启用,请联系管理员配置"},
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
)
|
||||
if missing:
|
||||
return Response(
|
||||
{"detail": f"当前缺少可用的{'、'.join(missing)}模型,请联系管理员配置"},
|
||||
@@ -988,7 +996,13 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
"""本项目在途的基础资产出图任务(立绘/商品图/三视图)。前端进基础资产趴时据此重建
|
||||
「生成中」占位卡 loading —— 出图在 worker 跑,刷新页面后内存态 genBusy 丢了,用它从后端认领恢复。"""
|
||||
project = self.get_object()
|
||||
inflight = [AITask.Status.CREATED, AITask.Status.RESERVED, AITask.Status.SUBMITTED, AITask.Status.POLLING]
|
||||
inflight = [
|
||||
AITask.Status.CREATED,
|
||||
AITask.Status.RESERVED,
|
||||
AITask.Status.SUBMITTED,
|
||||
AITask.Status.POLLING,
|
||||
AITask.Status.POSTPROCESSING,
|
||||
]
|
||||
image_types = [AITask.Type.PRODUCT_IMAGE, AITask.Type.PERSON_IMAGE, AITask.Type.SCENE_IMAGE]
|
||||
tasks = AITask.objects.filter(project=project, task_type__in=image_types, status__in=inflight)
|
||||
pending = []
|
||||
@@ -999,6 +1013,7 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
|
||||
"kind": payload.get("kind") or "",
|
||||
"label": payload.get("label") or "",
|
||||
"is_triview": bool(payload.get("triview_of")),
|
||||
"triview_of": payload.get("triview_of") or "",
|
||||
"status": task.status,
|
||||
})
|
||||
return Response({"pending": pending})
|
||||
|
||||
Reference in New Issue
Block a user