feat: 解耦角色与模特并完善模特库

This commit is contained in:
hh
2026-07-13 11:02:30 +08:00
parent 564215dc53
commit fc0f0ca370
22 changed files with 1328 additions and 314 deletions
@@ -0,0 +1,86 @@
"""审计“我的演员”收口为模特前的历史数据;只读,不写库。"""
from django.core.management.base import BaseCommand
from django.db.models import Q
from apps.assets.models import Asset, Model
from apps.projects.models import BaseAssetGroup
class Command(BaseCommand):
help = "只读审计旧 person 资产,输出可安全迁移为 Model 的候选和排除原因。"
def add_arguments(self, parser):
parser.add_argument("--team-id", help="仅审计指定团队 UUID")
parser.add_argument("--limit", type=int, default=50, help="最多打印多少条候选(默认 50)")
def handle(self, *args, **options):
team_id = options.get("team_id")
limit = max(0, options["limit"])
active_people = Asset.objects.filter(
category=Asset.Category.PERSON,
is_deleted=False,
purged_at__isnull=True,
)
if team_id:
active_people = active_people.filter(team_id=team_id)
# 项目基础资产组引用的 person(包含采用版和候选版)一律视为项目角色,不自动迁移。
project_role_asset_ids = set(
Asset.objects.filter(
Q(adopted_base_groups__kind=BaseAssetGroup.Kind.PERSON)
| Q(candidate_base_groups__kind=BaseAssetGroup.Kind.PERSON)
)
.values_list("id", flat=True)
.distinct()
)
if team_id:
project_role_asset_ids &= set(active_people.values_list("id", flat=True))
existing_model_portraits = set(
Model.objects.filter(
is_deleted=False,
purged_at__isnull=True,
portrait_asset__isnull=False,
)
.values_list("portrait_asset_id", flat=True)
)
if team_id:
existing_model_portraits &= set(active_people.values_list("id", flat=True))
saved_people = active_people.filter(in_library=True)
project_generated_ids = set(
active_people.filter(origin_task__project__isnull=False).values_list("id", flat=True)
)
eligible = (
saved_people.exclude(id__in=project_role_asset_ids)
.exclude(id__in=project_generated_ids)
.exclude(id__in=existing_model_portraits)
.order_by("created_at")
)
trash_people = Asset.objects.filter(
category=Asset.Category.PERSON,
is_deleted=True,
purged_at__isnull=True,
)
purged_people = Asset.objects.filter(category=Asset.Category.PERSON, purged_at__isnull=False)
if team_id:
trash_people = trash_people.filter(team_id=team_id)
purged_people = purged_people.filter(team_id=team_id)
self.stdout.write(self.style.WARNING("=== audit_actor_model_migration · DRY-RUN / READ-ONLY ==="))
self.stdout.write(f"正常 person 资产: {active_people.count()}")
self.stdout.write(f" 已有 Model 形象图: {len(existing_model_portraits)}")
self.stdout.write(f" 项目角色引用: {len(project_role_asset_ids)}")
self.stdout.write(f" 项目生成角色: {len(project_generated_ids)}")
self.stdout.write(f" 旧已保存人物候选: {eligible.count()}")
self.stdout.write(f"垃圾桶 person(不迁移): {trash_people.count()}")
self.stdout.write(f"彻底隐藏 person(不迁移): {purged_people.count()}")
self.stdout.write("\n可迁移候选(只打印,不创建 Model):")
for asset in eligible[:limit]:
self.stdout.write(f" {asset.id} | team={asset.team_id} | {asset.name}")
if eligible.count() > limit:
self.stdout.write(f" … 其余 {eligible.count() - limit} 条未打印(可用 --limit 调整)")
self.stdout.write(self.style.WARNING("审计结束:未创建、更新、删除任何记录或文件。"))
@@ -0,0 +1,80 @@
"""把符合规则的历史“已保存演员”补为 Model;默认 dry-run。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from django.db.models import Q
from apps.assets.models import Asset, Model
from apps.projects.models import BaseAssetGroup
def eligible_legacy_actor_assets(team_id=None):
"""只返回正常状态、已保存、非项目角色且尚未拥有 Model 的旧 person 资产。"""
people = Asset.objects.filter(
category=Asset.Category.PERSON,
in_library=True,
is_deleted=False,
purged_at__isnull=True,
)
if team_id:
people = people.filter(team_id=team_id)
project_role_ids = Asset.objects.filter(
Q(adopted_base_groups__kind=BaseAssetGroup.Kind.PERSON)
| Q(candidate_base_groups__kind=BaseAssetGroup.Kind.PERSON)
).values_list("id", flat=True)
existing_model_ids = Model.objects.filter(
is_deleted=False,
purged_at__isnull=True,
portrait_asset__isnull=False,
).values_list("portrait_asset_id", flat=True)
return (
people.exclude(origin_task__project__isnull=False)
.exclude(id__in=project_role_ids)
.exclude(id__in=existing_model_ids)
.order_by("created_at")
)
class Command(BaseCommand):
help = "迁移正常状态的历史已保存演员为 Model;默认 dry-run,--apply 才写库。"
def add_arguments(self, parser):
parser.add_argument("--team-id", help="仅迁移指定团队 UUID")
parser.add_argument("--apply", action="store_true", help="真正创建 Model(默认只打印)")
parser.add_argument("--limit", type=int, default=50, help="最多打印多少条候选(默认 50)")
def handle(self, *args, **options):
apply = options["apply"]
candidates = eligible_legacy_actor_assets(options.get("team_id"))
count = candidates.count()
limit = max(0, options["limit"])
self.stdout.write(self.style.WARNING(f"=== migrate_saved_actors_to_models · {'APPLY' if apply else 'DRY-RUN'} ==="))
self.stdout.write(f"可迁移旧保存人物: {count}")
for asset in candidates[:limit]:
self.stdout.write(f" {asset.id} | team={asset.team_id} | {asset.name}")
if not apply:
self.stdout.write(self.style.WARNING("DRY-RUN 结束,未写库。加 --apply 才会创建 Model。"))
return
created = 0
with transaction.atomic():
for asset in candidates.select_for_update():
# select_for_update 后再次确认,保证并发/重复执行都不会造成重复 Model。
if Model.objects.filter(
portrait_asset=asset,
is_deleted=False,
purged_at__isnull=True,
).exists():
continue
Model.objects.create(
team_id=asset.team_id,
created_by_id=asset.created_by_id,
name=(asset.name or "模特")[:255],
source=Model.Source.UPLOAD if asset.source == Asset.Source.UPLOAD else Model.Source.AI,
portrait_asset=asset,
metadata={"migrated_from_legacy_actor": True},
)
created += 1
self.stdout.write(self.style.SUCCESS(f"完成:创建 {created} 个 Model;未修改任何 Asset、项目、垃圾桶或文件。"))