diff --git a/hypn0/hypn0_site/management/commands/rename_generations.py b/hypn0/hypn0_site/management/commands/rename_generations.py new file mode 100644 index 0000000..e78d66b --- /dev/null +++ b/hypn0/hypn0_site/management/commands/rename_generations.py @@ -0,0 +1,164 @@ +import re +from typing import Optional + +from django.core.management.base import BaseCommand +from django.db import transaction + +from hypn0_site.models import TbHypn0Item +from hypn0_site.services.naming import generate_hypno_title + + +class Command(BaseCommand): + """ + Массовое переименование названий генераций (TbHypn0Item.s_title) на английский язык. + + АРХИТЕКТУРНОЕ НАЗНАЧЕНИЕ: + ========================= + 1. Локализация каталога и прод-базы: + Переводит существующие русскоязычные названия картин в базе данных + на сочные англоязычные гипнотические тайтлы в стиле Гипножабы. + + 2. Детерминированность vs Случайность: + По умолчанию использует детерминированный сид на базе ID генерации (item.id + seed_offset), + что обеспечивает стабильность результатов при повторных запусках (идемпотентность). + Флаг --random позволяет генерировать полностью случайные тайтлы. + + 3. Безопасность и оптимизация: + - Пакетное обновление через bulk_update порциями (--batch-size). + - Режим предварительного просмотра (--dry-run). + - Фильтрация по умолчанию только кириллических / пустых названий (или всех с флагом --all). + """ + + help = "Массовое переименование названий генераций на английский язык" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Тестовый прогон: показать изменения без сохранения в базу данных", + ) + parser.add_argument( + "--all", + action="store_true", + help="Переименовать абсолютно все генерации (по умолчанию только с кириллицей или пустые)", + ) + parser.add_argument( + "--only-cyrillic", + action="store_true", + help="Переименовывать только записи с кириллическими символами в названии (включено по умолчанию)", + ) + parser.add_argument( + "--batch-size", + type=int, + default=500, + help="Размер пакета для bulk_update (по умолчанию: 500)", + ) + parser.add_argument( + "--limit", + type=int, + default=None, + help="Ограничить количество обрабатываемых записей", + ) + parser.add_argument( + "--seed-offset", + type=int, + default=0, + help="Смещение для детерминированного генератора сидов (по умолчанию: 0)", + ) + parser.add_argument( + "--random", + action="store_true", + help="Использовать недетерминированные случайные сиды вместо ID записи", + ) + + def handle(self, *args, **options): + dry_run: bool = options["dry_run"] + rename_all: bool = options["all"] + batch_size: int = options["batch_size"] + limit: Optional[int] = options["limit"] + seed_offset: int = options["seed_offset"] + use_random: bool = options["random"] + + self.stdout.write( + self.style.NOTICE( + f"Старт процесса переименования генераций " + f"(dry_run={dry_run}, rename_all={rename_all}, batch_size={batch_size}, random={use_random})..." + ) + ) + + cyrillic_pattern = re.compile(r"[а-яА-ЯёЁ]") + + qs = TbHypn0Item.objects.all().only("id", "s_title").order_by("id") + total_in_db = qs.count() + + if total_in_db == 0: + self.stdout.write(self.style.WARNING("В базе данных нет генераций для обработки.")) + return + + to_update = [] + renamed_count = 0 + preview_samples = [] + + for item in qs.iterator(chunk_size=batch_size): + old_title = item.s_title or "" + + # Проверяем критерий необходимости переименования + needs_rename = False + if rename_all: + needs_rename = True + else: + # По умолчанию: кириллица или пустой заголовок + if not old_title.strip() or cyrillic_pattern.search(old_title): + needs_rename = True + + if not needs_rename: + continue + + # Генерация нового англоязычного тайтла + seed = None if use_random else (item.id + seed_offset) + new_title = generate_hypno_title(seed=seed) + + if len(preview_samples) < 15: + preview_samples.append((item.id, old_title, new_title)) + + item.s_title = new_title + to_update.append(item) + renamed_count += 1 + + if limit is not None and renamed_count >= limit: + break + + # Пакетное сохранение порциями + if not dry_run and len(to_update) >= batch_size: + with transaction.atomic(): + TbHypn0Item.objects.bulk_update(to_update, fields=["s_title"]) + to_update.clear() + + # Сохранение остатка + if not dry_run and to_update: + with transaction.atomic(): + TbHypn0Item.objects.bulk_update(to_update, fields=["s_title"]) + to_update.clear() + + # Вывод примеров переименования + if preview_samples: + self.stdout.write("\nПримеры переименований (первые до 15 записей):") + self.stdout.write("-" * 70) + for item_id, old_t, new_t in preview_samples: + self.stdout.write(f" [ID {item_id:5d}] «{old_t}» -> «{new_t}»") + self.stdout.write("-" * 70) + + self.stdout.write(f"\nВсего записей в базе данных: {total_in_db}") + self.stdout.write(f"Отобрано для переименования: {renamed_count}") + + if dry_run: + self.stdout.write( + self.style.WARNING( + f"\n[DRY RUN] Проверено {renamed_count} записей. Изменения НЕ сохранены в БД. " + "Запустите без флага --dry-run для применения." + ) + ) + else: + self.stdout.write( + self.style.SUCCESS(f"\nУспешно переименовано {renamed_count} генераций в базе данных!") + )