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

角色库导入打标签并支持筛选;模特库与全能创作角色/商品选择改为每页 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
@@ -0,0 +1,157 @@
"""把本地「角色库」文件夹批量导入为官方模特(带标签)。
用法:
python manage.py import_character_pack "/Users/Admin/Downloads/角色库"
python manage.py import_character_pack "/path" --dry-run
python manage.py import_character_pack "/path" --team-owner admin
幂等:同一相对路径(metadata.import_key)不会重复导入。
"""
from __future__ import annotations
import mimetypes
import uuid
from pathlib import Path
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from apps.accounts.models import Team, TeamMember, User
from apps.assets.character_tags import display_name_from_file, import_key, parse_character_tags
from apps.assets.models import Asset, AssetFile, Model
from apps.assets.storage import TosStorage
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
class Command(BaseCommand):
help = "Import a local character-pack folder into official Model library with tags"
def add_arguments(self, parser):
parser.add_argument("folder", type=str, help="角色库根目录")
parser.add_argument("--dry-run", action="store_true", help="只解析标签,不上传")
parser.add_argument("--team-owner", default="admin", help="托管官方模特的团队 owner 用户名")
parser.add_argument("--limit", type=int, default=0, help="最多导入 N 张(0=全部)")
def handle(self, *args, **options):
root = Path(options["folder"]).expanduser().resolve()
if not root.is_dir():
raise CommandError(f"目录不存在: {root}")
owner = User.objects.filter(username=options["team_owner"]).first()
if owner is None:
raise CommandError(f"找不到用户 {options['team_owner']}")
team = (
Team.objects.filter(owner=owner, is_personal=True).order_by("created_at").first()
or Team.objects.filter(owner=owner).order_by("created_at").first()
)
if team is None:
team = Team.objects.create(name=f"{owner.username}-official", owner=owner, is_personal=True)
TeamMember.objects.create(team=team, user=owner, role=TeamMember.Role.OWNER)
self.stdout.write(f"已创建托管团队 {team.name}")
files = sorted(
p for p in root.rglob("*")
if p.is_file() and p.suffix.lower() in IMAGE_SUFFIXES and p.name != ".DS_Store"
)
if options["limit"]:
files = files[: options["limit"]]
if not files:
raise CommandError("目录里没有图片")
storage = None if options["dry_run"] else TosStorage()
created = skipped = failed = 0
for path in files:
rel = str(path.relative_to(root)).replace("\\", "/")
folder = path.parent.name if path.parent != root else ""
tags = parse_character_tags(folder=folder, filename=path.name)
name = display_name_from_file(folder=folder, filename=path.name)
key = import_key(rel)
existing = Model.objects.filter(
is_deleted=False,
metadata__import_key=key,
).first()
if existing:
skipped += 1
self.stdout.write(f"skip {rel} tags={tags}")
continue
self.stdout.write(f"{'dry' if options['dry_run'] else 'add '} {rel}{name} tags={tags}")
if options["dry_run"]:
created += 1
continue
try:
self._create_one(
storage=storage,
team=team,
owner=owner,
path=path,
name=name,
tags=tags,
key=key,
folder=folder,
rel=rel,
)
created += 1
except Exception as exc: # noqa: BLE001
failed += 1
self.stderr.write(self.style.ERROR(f"fail {rel}: {exc}"))
self.stdout.write(self.style.SUCCESS(
f"done created={created} skipped={skipped} failed={failed} team={team.id}"
))
def _create_one(self, *, storage, team, owner, path: Path, name: str, tags: list[str], key: str, folder: str, rel: str):
asset_id = uuid.uuid4()
suffix = path.suffix.lower() or ".png"
object_key = f"teams/{team.id}/models/official/{asset_id}{suffix}"
content_type = mimetypes.guess_type(path.name)[0] or "image/png"
with path.open("rb") as fh:
stored = storage.upload_fileobj(
fileobj=fh,
object_key=object_key,
content_type=content_type,
)
with transaction.atomic():
portrait = Asset.objects.create(
id=asset_id,
team=team,
created_by=owner,
name=f"{name}·形象图",
asset_type=Asset.Type.IMAGE,
source=Asset.Source.UPLOAD,
category=Asset.Category.MODEL_PORTRAIT,
metadata={
"kind": "model",
"view": "frontal",
"source": "character_pack",
"tags": tags,
},
)
AssetFile.objects.create(
asset=portrait,
object_key=stored.object_key,
bucket=stored.bucket,
content_type=stored.content_type,
size_bytes=stored.size_bytes,
is_primary=True,
)
Model.objects.create(
team=team,
created_by=owner,
name=name,
is_official=True,
source=Model.Source.UPLOAD,
portrait_asset=portrait,
description=f"角色库 · {folder}" if folder else "角色库",
metadata={
"source": "character_pack",
"tags": tags,
"import_key": key,
"folder": folder,
"relative_path": rel,
},
)