完成视频复刻和优化

This commit is contained in:
Azmat@qq.com
2026-08-27 11:54:47 +08:00
parent c25060c6c6
commit 51620bf25b
46 changed files with 2575 additions and 663 deletions
+2 -2
View File
@@ -123,7 +123,7 @@ def _upload_asset_to_group(request, *, team, group: FreeAssetGroup, image_only:
file_id = uuid.uuid4()
if kind != "image":
from apps.ai.media_probe import extract_video_poster, probe_duration
from apps.ai.media_probe import duration_in_ref_range, extract_video_poster, probe_duration
with tempfile.TemporaryDirectory(prefix="airshelf-fc-lib-") as tmp:
tmp_path = Path(tmp) / f"in{suffix}"
@@ -131,7 +131,7 @@ def _upload_asset_to_group(request, *, team, group: FreeAssetGroup, image_only:
duration = probe_duration(str(tmp_path))
if duration is None:
return Response({"detail": "媒体文件解析失败,请更换文件"}, status=status.HTTP_400_BAD_REQUEST)
if not (2 <= duration <= 15):
if not duration_in_ref_range(duration):
label = "视频" if kind == "video" else "音频"
return Response({"detail": f"{label}时长需在 2-15 秒之间"}, status=status.HTTP_400_BAD_REQUEST)
if kind == "video":
+72 -8
View File
@@ -116,6 +116,73 @@ def reference_review_state(asset: Asset) -> str:
return "unsubmitted"
def _unsubmitted_review_qs(*, team=None):
qs = Asset.objects.filter(
category__in=Asset.REVIEW_CATEGORIES,
review_status="",
is_deleted=False,
)
if team is not None:
qs = qs.filter(team=team)
return qs
def _processing_review_qs(*, team=None):
qs = Asset.objects.filter(
category__in=Asset.REVIEW_CATEGORIES,
review_status="processing",
is_deleted=False,
)
if team is not None:
qs = qs.filter(team=team)
return qs
def submit_unsubmitted_reviews(*, team=None, limit: int = 25) -> int:
"""把未送审的人脸资产自动丢给火山,不用人去后台点「批量送审」。"""
submitted = 0
cap = max(1, min(int(limit), 50))
for asset in _unsubmitted_review_qs(team=team).order_by("created_at")[:cap]:
if submit_asset_for_review(asset):
submitted += 1
return submitted
def poll_processing_reviews(*, team=None, limit: int = 80) -> dict[str, str]:
cap = max(1, min(int(limit), 200))
out: dict[str, str] = {}
for asset in _processing_review_qs(team=team).order_by("updated_at")[:cap]:
out[str(asset.id)] = poll_asset_review(asset)
return out
def drain_platform_reviews(*, submit_limit: int = 25, poll_limit: int = 80) -> dict:
"""全平台:未送审自动送 + 审核中自动拉状态。worker 兜底循环,人不在后台页也会跑。"""
submitted = submit_unsubmitted_reviews(limit=submit_limit)
statuses = poll_processing_reviews(limit=poll_limit)
return {"submitted": submitted, "polled": len(statuses), "statuses": statuses}
def ensure_review_drain_loop() -> None:
"""第一次有人轮询审核时,顺手把 worker 兜底循环拉起来(没有 beat 也能自转)。"""
import sys
from django.conf import settings as dj_settings
from django.core.cache import cache
if getattr(dj_settings, "CELERY_TASK_ALWAYS_EAGER", False) or "test" in sys.argv:
return
if not cache.add("asset-review-drain-kick", "1", timeout=300):
return
try:
from apps.ai.tasks import drain_asset_reviews_task
drain_asset_reviews_task.delay()
except Exception: # noqa: BLE001
cache.delete("asset-review-drain-kick")
logger.warning("kick asset review drain loop failed", exc_info=True)
def poll_asset_review(asset: Asset) -> str:
"""查单个真人资产审核状态并更新 review_status。返回最新状态。
只在状态变化时落库(保留 updated_at 作为「进入 processing 的时刻」);processing 超时兜底为 failed。"""
@@ -144,11 +211,8 @@ def poll_asset_review(asset: Asset) -> str:
def poll_team_reviews(team) -> dict:
"""轮询该团队所有「审核中」真人资产,更新状态。返回 {asset_id: status}。"""
out: dict[str, str] = {}
pending = Asset.objects.filter(
team=team, category__in=Asset.REVIEW_CATEGORIES, review_status="processing", is_deleted=False
)
for asset in pending:
out[str(asset.id)] = poll_asset_review(asset)
return out
"""先把本团队未送审的人脸资产送出去,再轮询审核中状态。
前端基础资产趴定时打这里:用户不用去后台点送审,也不用等管理员。"""
ensure_review_drain_loop()
submit_unsubmitted_reviews(team=team, limit=20)
return poll_processing_reviews(team=team, limit=80)
+37
View File
@@ -890,3 +890,40 @@ class ReviewScopeTests(TestCase):
with patch("apps.assets.review.assets_client.is_enabled", return_value=False):
out = review.poll_team_reviews(team)
self.assertEqual(set(out.keys()), set(keep.values()))
def test_poll_team_reviews_auto_submits_unsubmitted(self):
from apps.assets import review
_, team = _mk_team("usub", "TeamUSub")
empty = Asset.objects.create(
team=team, name="待审角色", asset_type="image", source="ai_generated",
category=Asset.Category.PERSON, review_status="",
)
with patch("apps.assets.review.submit_asset_for_review", return_value=True) as sub, patch(
"apps.assets.review.ensure_review_drain_loop"
):
review.poll_team_reviews(team)
sub.assert_called()
self.assertEqual(sub.call_args.args[0].id, empty.id)
def test_drain_platform_reviews_submits_then_polls(self):
from apps.assets import review
_, team = _mk_team("drain", "TeamDrain")
Asset.objects.create(
team=team, name="未送", asset_type="image", source="ai_generated",
category=Asset.Category.PERSON, review_status="",
)
proc = Asset.objects.create(
team=team, name="审中", asset_type="image", source="ai_generated",
category=Asset.Category.PERSON, review_status="processing",
)
with patch("apps.assets.review.submit_asset_for_review", return_value=True) as sub, patch(
"apps.assets.review.poll_asset_review", return_value="active"
) as polled:
out = review.drain_platform_reviews()
self.assertEqual(out["submitted"], 1)
self.assertEqual(out["polled"], 1)
self.assertEqual(out["statuses"][str(proc.id)], "active")
sub.assert_called_once()
polled.assert_called_once()