模特库标签分页与全能创作收口:藏长视频、选择器分页
角色库导入打标签并支持筛选;模特库与全能创作角色/商品选择改为每页 20 条分页。临时限制成片 ≤60 秒,过滤对话里的超长时长选项,并收拢本地全能创作与后台用户相关修复。
This commit is contained in:
@@ -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,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user