模特库标签分页与全能创作收口:藏长视频、选择器分页

角色库导入打标签并支持筛选;模特库与全能创作角色/商品选择改为每页 20 条分页。临时限制成片 ≤60 秒,过滤对话里的超长时长选项,并收拢本地全能创作与后台用户相关修复。
This commit is contained in:
Azmat@qq.com
2026-09-21 16:24:37 +08:00
parent 690eb3d843
commit 0bd1db6bf9
48 changed files with 3386 additions and 896 deletions
+47 -4
View File
@@ -43,6 +43,15 @@ _KNOWN_CATS = [
]
def _not_omni_create_q() -> Q:
"""「不是全能创作产出」。不能只写 ~Q(metadata__feature="omni_create")。
metadata 里没有 feature 键时取值是 SQL NULLNOT(NULL = 'x') 还是 NULL,整行会被筛掉 ——
历史资产(没写过 feature)会从「视频自由创作」「自由创作」「素材」这些分桶里整批消失。
"""
return Q(metadata__feature__isnull=True) | ~Q(metadata__feature="omni_create")
def _tab_q(tab: str) -> Q:
if tab == "people":
return Q(category="person")
@@ -57,13 +66,13 @@ def _tab_q(tab: str) -> Q:
if tab == "image_creations": # 图片自由创作
return Q(category="free_create", asset_type="image")
if tab == "video_creations": # 视频自由创作
return Q(category="free_create", asset_type="video") & ~Q(metadata__feature="omni_create")
return Q(category="free_create", asset_type="video") & _not_omni_create_q()
if tab == "creations": # 自由创作(兼容旧入口:图片+视频)
return Q(category="free_create") & ~Q(metadata__feature="omni_create")
return Q(category="free_create") & _not_omni_create_q()
if tab == "uploads":
return Q(category="upload")
if tab == "materials": # 素材(期3):视频素材 = 所有视频,排除「最终成片」(final_video 隐藏不列)
return ~Q(category="final_video") & Q(asset_type="video") & ~Q(metadata__feature="omni_create")
return ~Q(category="final_video") & Q(asset_type="video") & _not_omni_create_q()
if tab == "others": # 其他(资产库成品化):我的上传 + 未归类非视频(兜底)
return Q(category="upload") | (~Q(category__in=_KNOWN_CATS) & ~Q(asset_type="video"))
if tab == "unclassified": # 未归类且非视频(也不含最终成片)
@@ -482,6 +491,14 @@ class ModelLibraryViewSet(ModelViewSet):
if self.request.query_params.get("q"):
q = self.request.query_params["q"]
qs = qs.filter(Q(name__icontains=q) | Q(description__icontains=q))
# 标签筛选: ?tag=男人 或 ?tag=男人&tag=警察(多标签 AND)
raw_tags = self.request.query_params.getlist("tag") or []
if not raw_tags:
joined = (self.request.query_params.get("tags") or "").strip()
if joined:
raw_tags = [t.strip() for t in joined.split(",") if t.strip()]
for tag in raw_tags:
qs = qs.filter(metadata__tags__contains=[tag])
# 官方模板靠前,再按新→旧
return qs.order_by("-is_official", "-created_at")
@@ -605,6 +622,18 @@ class ModelLibraryViewSet(ModelViewSet):
model.save(update_fields=["purged_at", "updated_at"])
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=False, methods=["get"], url_path="tags")
def tags(self, request):
"""当前可见模特里出现过的标签(供前端筛选条)· 只返回后台标记为显示的。"""
from .model_library_tags import public_tag_rows
tab = (request.query_params.get("tab") or "").strip() or None
if tab not in (None, "official", "mine"):
tab = None
team = self.get_team()
items = public_tag_rows(tab=tab, team_id=getattr(team, "id", None))
return Response({"results": items})
@action(detail=False, methods=["post"], url_path="upload", parser_classes=[MultiPartParser, FormParser])
def upload(self, request):
"""真人上传:一张人像图 → 建 model_portrait 资产 + Model(source=upload)。三视图/声线后续补。"""
@@ -640,13 +669,27 @@ class ModelLibraryViewSet(ModelViewSet):
size_bytes=stored.size_bytes,
is_primary=True,
)
raw_tags = request.data.get("tags")
tags: list[str] = []
if isinstance(raw_tags, str) and raw_tags.strip():
import json
try:
parsed = json.loads(raw_tags)
if isinstance(parsed, list):
tags = [str(t).strip() for t in parsed if str(t).strip()]
else:
tags = [t.strip() for t in raw_tags.split(",") if t.strip()]
except Exception:
tags = [t.strip() for t in raw_tags.split(",") if t.strip()]
elif isinstance(raw_tags, list):
tags = [str(t).strip() for t in raw_tags if str(t).strip()]
model = Model.objects.create(
team=team,
created_by=request.user,
name=name,
source=Model.Source.UPLOAD,
portrait_asset=portrait,
metadata={"source": "upload"},
metadata={"source": "upload", "tags": tags},
)
return Response(ModelLibrarySerializer(model).data, status=status.HTTP_201_CREATED)