mod: localized English version of README and UI texts, enhanced accessibility and internationalization features
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 51s
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 51s
This commit is contained in:
@@ -302,8 +302,8 @@ class BlogPostAdminForm(CodeMirrorFormMixin):
|
||||
)
|
||||
etp_language = forms.ChoiceField(
|
||||
label="Язык типографики",
|
||||
choices=[('ru', 'Русский'), ('en', 'English'), ('ru,en', 'Ru + En')],
|
||||
initial='ru',
|
||||
choices=[('en', 'English'), ('ru', 'Русский'), ('en,ru', 'En + Ru'), ('ru,en', 'Ru + En')],
|
||||
initial='en',
|
||||
required=False
|
||||
)
|
||||
etp_quotes = forms.BooleanField(
|
||||
|
||||
@@ -47,13 +47,13 @@ class TbHypn0Item(models.Model):
|
||||
• rescore(): пересчёт рейтинга f_score
|
||||
"""
|
||||
class Level(models.IntegerChoices):
|
||||
CANDIDATE = 0, 'Candidate: Шум сознания' # Свежая генерация. Запрет на удаление до Х дней.
|
||||
LEVEL_1 = 10, 'Voted: Плеск бессознательного' # Прогрето, есть "лайки", не проверено модератором, можно удалять при низком score
|
||||
LEVEL_2 = 30, 'Moderated: Одобрено Мозговым Слизнем' # Есть "лайки", проверено, можно удалять при низком score
|
||||
IMMORTAL = 1000, 'Locked: Глубокий транс' # Нельзя удалять, даже при низком score
|
||||
CANDIDATE = 0, 'Candidate: Conscious Noise' # Свежая генерация. Запрет на удаление до Х дней.
|
||||
LEVEL_1 = 10, 'Voted: Subcon\u00adscious Ripple' # Прогрето, есть "лайки", не проверено модератором, можно удалять при низком score
|
||||
LEVEL_2 = 30, 'Moderated: Approved by\u00a0Brain Slug' # Есть "лайки", проверено, можно удалять при низком score
|
||||
IMMORTAL = 1000, 'Locked: Deep Trance' # Нельзя удалять, даже при низком score
|
||||
# Отрицательные / проблемные уровни (аномалии)
|
||||
SHAMED = -20, 'Шейминг (аномальный наплыв жалоб)'
|
||||
SUSPICIOUS = -10, 'Подозрение на накрутку (спайк лайков)'
|
||||
SHAMED = -20, 'Shamed (anomalous report flood)'
|
||||
SUSPICIOUS = -10, 'Suspicious (vote spike anomaly)'
|
||||
|
||||
id = models.AutoField(
|
||||
primary_key=True,
|
||||
|
||||
@@ -10,39 +10,44 @@ import random
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# Словари для локального генератора гипно-тайтлов
|
||||
# Словари для локального генератора гипно-тайтлов на английском языке
|
||||
HYPNO_ADJECTIVES = [
|
||||
"мерцающий", "астральный", "гипнотический", "вульгарный", "сюрреалистический", "абсурдный", "психоделический",
|
||||
"пульсирующий", "неоновый", "трансцендентный", "мозговой", "ментальный", "бессознательный", "космический",
|
||||
"галлюциногенный", "фрактальный", " отрешённый", "синестетический", "параллельный", "иллюзорный", "эфемерный",
|
||||
"гипножабий", "проводящий", "тошнотворный", "завораживающий", "магнетический", "беспечный", "сновидческий",
|
||||
"психотропный", "мистический", "потусторонний", "беспечный", "сновидческий", "психотропный", "мистический",
|
||||
"потусторонний", "спиральный", "зыбкий", "пахучий", "скользкий", "туманный", "онейрический", "эфирный",
|
||||
"трансовый", "квантовый", "сочный", "гипно-абсурдный", "психоактивный", "сюрреалистичный", "магический",
|
||||
"синтетический", "хроматический", "перцептивный", "психоделичный", "галлюцинаторный", "трансцендентальный",
|
||||
"перфорированный", "перцепторный", "гипно-иллюзорный", "трансцендентальный", "вульгарный",
|
||||
"Shimmering", "Astral", "Hypnotic", "Vulgar", "Surreal", "Absurd", "Psyche­delic",
|
||||
"Pulsating", "Neon", "Trans­cendent", "Cerebral", "Mental", "Subcon­scious", "Cosmic",
|
||||
"Halluci­nogenic", "Fractal", "Detached", "Synes­thetic", "Parallel", "Illusory", "Ephemeral",
|
||||
"Hypnot­oadian", "Conductive", "Mesme­rizing", "Magnetic", "Carefree", "Oneiric",
|
||||
"Psychot­ropic", "Mystical", "Other­worldly", "Spiral", "Quivering", "Slippery", "Misty",
|
||||
"Ethereal", "Trance", "Quantum", "Juicy", "Hypno-Absurd", "Psychoactive", "Magical",
|
||||
"Synthetic", "Chromatic", "Perceptive", "Perforated", "Vibra­tional", "Neural",
|
||||
"Subliminal", "Glitched", "Radioactive", "Brain-Melting", "Galactic", "Phospho­rescent",
|
||||
"Chaotic", "Hypnagogic", "Interdim­ensional", "Cybernetic", "Uncanny",
|
||||
]
|
||||
|
||||
HYPNO_NOUNS = [
|
||||
"транс", "пульсар", "взгляд", "слизень", "вихрь", "портал", "путь", "фрактал", "сигнал", "спектр",
|
||||
"резонанс", "морок", "оазис", "омут", "лабиринт", "импульс", "монолит", "оракул", "вектор",
|
||||
"сгусток", "каскад", "горизонт", "мухомор", "паттерн", "контур", "фантом", "всплеск", "мираж", "транквилизатор",
|
||||
"взмах", "взгляд", "морок", "туман", "свет", "кристалл", "поток", "сингулятор", "снег", "пульс", "глюк",
|
||||
"качан", "сигнал", "поток", "свет", "синтез", "рык", "трепет", "плюх", "Му", "бip-бip", "карачун", "фронтир",
|
||||
"сторож", "привратник", "шёпот", "шлепок по жопе", "гриб", "плевок", "червяк", "пузырь", "психо-паук",
|
||||
"перцептор",
|
||||
"Trance", "Pulsar", "Gaze", "Brain Slug", "Vortex", "Portal", "Path", "Fractal", "Signal", "Spectrum",
|
||||
"Resonance", "Mirage", "Oasis", "Abyss", "Labyrinth", "Impulse", "Monolith", "Oracle", "Vector",
|
||||
"Clot", "Cascade", "Horizon", "Toadstool", "Pattern", "Contour", "Phantom", "Splash", "Tranqui­lizer",
|
||||
"Mist", "Crystal", "Stream", "Singulator", "Pulse", "Glitch", "Synthesis", "Roar", "Tremor",
|
||||
"Frontier", "Sentinel", "Gatekeeper", "Whisper", "Spore", "Worm", "Bubble", "Psycho-Spider",
|
||||
"Receptor", "Mind-Warp", "Hypnotoad", "Neuron", "Oscil­lation", "Echo", "Hologram", "Matrix",
|
||||
"Mind-drill", "Neurotoxin", "Dreamscape", "Substance",
|
||||
]
|
||||
|
||||
HYPNO_GENITIVES = [
|
||||
"сознания", "Гипножабы", "бессознательного", "пустоты", "матрицы", "психо-паутины", "слизня", "ноосферы",
|
||||
"нео-модерна", "транса", "глубины", "эфира", "плесени", "измерения", "пуантилизма", "субстанции",
|
||||
"неопределенности", "континуума", "кислоты", "сына подруги твоей мамы", "перцепции", "сновидений",
|
||||
"галлюцинаций", "психоделики", "трансцендентности", "потустороннего мира",
|
||||
"of Consciousness", "of the Hypnotoad", "of the Subcon­scious",
|
||||
"of the Void", "of the Matrix", "of the Psycho-Web", "of the Brain Slug",
|
||||
"of the Noosphere", "of the Neo-Modern", "of the Deep Trance",
|
||||
"of the Ether", "of the 4th Dimension", "of Pointillism", "of the Substance",
|
||||
"of Uncer­tainty", "of the Continuum", "of Acid", "of Pure Perception",
|
||||
"of Lucid Dreams", "of Halluci­nations", "of Psyche­delia", "of Trans­cendence",
|
||||
"of the Nether­world", "of DOOP Regul­ations", "of Galactic Frequencies",
|
||||
"of Grade 24 Bureaucracy",
|
||||
]
|
||||
|
||||
HYPNO_POSTFIXES = [
|
||||
"#42", "v3.14", "(Ultra)", "(Фаза 7)", "(Альфа)", "(Омега)", "(Протокол 09)", "(Ревизия 14)", "(Экстракт)",
|
||||
"(Слой 9)", "(Палата №6)", "(Ангар 18)", "(Неоконченное произведение)", "(Секретный проект)",
|
||||
"#42", "v3.14", "(Ultra)", "(Phase 7)", "(Alpha)", "(Omega)", "(Protocol 09)", "(Revision 14)",
|
||||
"(Extract)", "(Layer 9)", "(Ward 106)", "(Hangar 18)", "(Unfinished Opus)", "(Secret Project)",
|
||||
"[Classified]", "(DOOP-66)", "(Sub-Level 4)", "(MK-Ultra)", "С90"
|
||||
]
|
||||
|
||||
|
||||
@@ -50,25 +55,36 @@ def generate_hypno_title(seed: Optional[int] = None) -> str:
|
||||
"""
|
||||
Генерирует сочное гипнотическое название для картины на основе словарей.
|
||||
Примеры:
|
||||
- «Мерцающий Транс сознания #42»
|
||||
- «Гипножабий Пульсар психо-паутины»
|
||||
- «Астральный Взгляд v3.14»
|
||||
- «Hypnotic Pulsar of the Hypnotoad #42»
|
||||
- «Astral Gaze»
|
||||
- «Brain Slug of the Void»
|
||||
- «Transcendent (Ultra)»
|
||||
- «Hypnotic»
|
||||
- «Vortex»
|
||||
"""
|
||||
rng = random.Random(seed)
|
||||
adj = rng.choice(HYPNO_ADJECTIVES)
|
||||
noun = rng.choice(HYPNO_NOUNS)
|
||||
|
||||
pattern_type = rng.randint(1, 3)
|
||||
if pattern_type == 1:
|
||||
genitive = rng.choice(HYPNO_GENITIVES)
|
||||
postfix = rng.choice(HYPNO_POSTFIXES)
|
||||
return f"{adj} {noun} {genitive} {postfix}"
|
||||
elif pattern_type == 2:
|
||||
genitive = rng.choice(HYPNO_GENITIVES)
|
||||
return f"{adj} {noun} {genitive}"
|
||||
else:
|
||||
postfix = rng.choice(HYPNO_POSTFIXES)
|
||||
return f"{adj} {noun} {postfix}"
|
||||
genitive = rng.choice(HYPNO_GENITIVES)
|
||||
postfix = rng.choice(HYPNO_POSTFIXES)
|
||||
|
||||
patterns = [
|
||||
# Однословные варианты
|
||||
lambda: noun,
|
||||
lambda: adj,
|
||||
# Двухсловные комбинации
|
||||
lambda: f"{adj} {noun}",
|
||||
lambda: f"{noun} {genitive}",
|
||||
lambda: f"{noun} {postfix}",
|
||||
lambda: f"{adj} {postfix}",
|
||||
# Трех- и четырехсловные гипно-конструкции
|
||||
lambda: f"{adj} {noun} {genitive}",
|
||||
lambda: f"{adj} {noun} {postfix}",
|
||||
lambda: f"{adj} {noun} {genitive} {postfix}",
|
||||
]
|
||||
|
||||
chosen_pattern = rng.choice(patterns)
|
||||
return chosen_pattern()
|
||||
|
||||
|
||||
# =====================================================================
|
||||
@@ -80,10 +96,10 @@ def generate_title_openrouter(
|
||||
api_key: Optional[str] = None,
|
||||
model: str = "google/gemini-2.5-flash",
|
||||
system_prompt: str = (
|
||||
"Ты — глашатай всемогущей Гипножабы. Придумай короткое (2-4 слова), "
|
||||
"яркое, иронично-абсурдное и гипнотическое название на русском языке "
|
||||
"для полутоновой SVG-картины, изображенной на миниатюре. "
|
||||
"Отвечай ТОЛЬКО названием, без кавычек и вводных слов."
|
||||
"You are the herald of the almighty Hypnotoad. Create a short (2-4 words), "
|
||||
"punchy, ironically absurd, and hypnotic English title "
|
||||
"for a halftone SVG artwork shown in the thumbnail. "
|
||||
"Reply ONLY with the title, without quotes or introductory words."
|
||||
),
|
||||
timeout: int = 5,
|
||||
) -> Optional[str]:
|
||||
@@ -101,7 +117,7 @@ def generate_title_openrouter(
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Дай гипнотическое название для этой картины:"},
|
||||
{"type": "text", "text": "Give a hypnotic title for this artwork:"},
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_data}"}}
|
||||
]
|
||||
}
|
||||
|
||||
+100
-1
@@ -1,4 +1,5 @@
|
||||
import io
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
@@ -322,11 +323,16 @@ class NamingServiceTests(BaseMediaTestCase):
|
||||
def test_generate_hypno_title(self):
|
||||
title = generate_hypno_title(seed=42)
|
||||
self.assertIsInstance(title, str)
|
||||
self.assertTrue(len(title) > 5)
|
||||
self.assertTrue(len(title) > 0)
|
||||
|
||||
# Проверка детерминированности по seed
|
||||
self.assertEqual(generate_hypno_title(seed=42), title)
|
||||
|
||||
# Проверяем генерацию множества названий разной длины
|
||||
titles = [generate_hypno_title(seed=i) for i in range(100)]
|
||||
self.assertTrue(any(" " in t for t in titles))
|
||||
self.assertTrue(any(" " not in t for t in titles))
|
||||
|
||||
def test_openrouter_stub(self):
|
||||
res = generate_title_openrouter(b"dummy_bytes")
|
||||
self.assertIsNone(res)
|
||||
@@ -1519,3 +1525,96 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
|
||||
slug="test-plain",
|
||||
)
|
||||
self.assertEqual(post.s_title_plain, "Записки Гипножабы: Раздел&Смысл")
|
||||
|
||||
|
||||
class RenameGenerationsCommandTests(BaseMediaTestCase):
|
||||
"""Тестирование management-команды массового переименования генераций rename_generations."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.vid = "123e4567-e89b-12d3-a456-426614174000"
|
||||
svg_bytes = b'<svg><circle/></svg>'
|
||||
|
||||
self.item_ru1 = TbHypn0Item(
|
||||
s_title="Мерцающий транс сознания #42",
|
||||
file_svg=ContentFile(svg_bytes, name="ru1.svg"),
|
||||
is_public=True,
|
||||
)
|
||||
self.item_ru1.save(visitor_uuid_or_fp=self.vid)
|
||||
|
||||
self.item_ru2 = TbHypn0Item(
|
||||
s_title="Астральный взгляд Гипножабы",
|
||||
file_svg=ContentFile(svg_bytes, name="ru2.svg"),
|
||||
is_public=True,
|
||||
)
|
||||
self.item_ru2.save(visitor_uuid_or_fp=self.vid)
|
||||
|
||||
self.item_en = TbHypn0Item(
|
||||
s_title="Brain Slug Vortex",
|
||||
file_svg=ContentFile(svg_bytes, name="en.svg"),
|
||||
is_public=True,
|
||||
)
|
||||
self.item_en.save(visitor_uuid_or_fp=self.vid)
|
||||
|
||||
def test_rename_dry_run_does_not_mutate_db(self):
|
||||
out = io.StringIO()
|
||||
call_command("rename_generations", dry_run=True, stdout=out)
|
||||
output = out.getvalue()
|
||||
|
||||
self.assertIn("[DRY RUN]", output)
|
||||
self.assertIn("Мерцающий транс сознания #42", output)
|
||||
|
||||
self.item_ru1.refresh_from_db()
|
||||
self.item_ru2.refresh_from_db()
|
||||
self.item_en.refresh_from_db()
|
||||
|
||||
self.assertEqual(self.item_ru1.s_title, "Мерцающий транс сознания #42")
|
||||
self.assertEqual(self.item_ru2.s_title, "Астральный взгляд Гипножабы")
|
||||
self.assertEqual(self.item_en.s_title, "Brain Slug Vortex")
|
||||
|
||||
def test_rename_only_cyrillic_by_default(self):
|
||||
out = io.StringIO()
|
||||
call_command("rename_generations", stdout=out)
|
||||
|
||||
self.item_ru1.refresh_from_db()
|
||||
self.item_ru2.refresh_from_db()
|
||||
self.item_en.refresh_from_db()
|
||||
|
||||
# Русские заголовки должны быть заменены на английские (без кириллицы)
|
||||
self.assertFalse(re.search(r"[а-яА-ЯёЁ]", self.item_ru1.s_title))
|
||||
self.assertFalse(re.search(r"[а-яА-ЯёЁ]", self.item_ru2.s_title))
|
||||
self.assertTrue(len(self.item_ru1.s_title) > 0)
|
||||
self.assertTrue(len(self.item_ru2.s_title) > 0)
|
||||
|
||||
# Английский заголовок не должен измениться
|
||||
self.assertEqual(self.item_en.s_title, "Brain Slug Vortex")
|
||||
|
||||
def test_rename_all_flag_updates_all_items(self):
|
||||
out = io.StringIO()
|
||||
call_command("rename_generations", all=True, stdout=out)
|
||||
|
||||
self.item_ru1.refresh_from_db()
|
||||
self.item_ru2.refresh_from_db()
|
||||
self.item_en.refresh_from_db()
|
||||
|
||||
self.assertFalse(re.search(r"[а-яА-ЯёЁ]", self.item_ru1.s_title))
|
||||
self.assertFalse(re.search(r"[а-яА-ЯёЁ]", self.item_ru2.s_title))
|
||||
# С флагом --all английский заголовок тоже перегенерируется детерминированно
|
||||
self.assertFalse(re.search(r"[а-яА-ЯёЁ]", self.item_en.s_title))
|
||||
|
||||
def test_rename_deterministic_and_seed_offset(self):
|
||||
out = io.StringIO()
|
||||
call_command("rename_generations", seed_offset=100, stdout=out)
|
||||
self.item_ru1.refresh_from_db()
|
||||
first_title = self.item_ru1.s_title
|
||||
|
||||
# Повторный вызов с тем же seed_offset дает точно такой же результат
|
||||
call_command("rename_generations", seed_offset=100, all=True, stdout=out)
|
||||
self.item_ru1.refresh_from_db()
|
||||
self.assertEqual(self.item_ru1.s_title, first_title)
|
||||
|
||||
def test_rename_empty_database(self):
|
||||
TbHypn0Item.objects.all().delete()
|
||||
out = io.StringIO()
|
||||
call_command("rename_generations", stdout=out)
|
||||
self.assertIn("нет генераций для обработки", out.getvalue())
|
||||
|
||||
@@ -68,6 +68,16 @@ def get_floor_top(limit: int | None = 8, offset: int = 0):
|
||||
return qs
|
||||
|
||||
|
||||
# Опции сортировки в галерее (отображение в выпадающем списке на фронтенде)
|
||||
GALLERY_SORT_OPTIONS = [
|
||||
{"id": "gravity", "title": "By gravity (f_score)", "icon": "🌀"},
|
||||
{"id": "new", "title": "Fresh (by date)", "icon": "✨"},
|
||||
{"id": "likes", "title": "By likes", "icon": "♥"},
|
||||
{"id": "views", "title": "Rare (few views)", "icon": "👁"},
|
||||
{"id": "popular", "title": "Popular (most views)", "icon": "🔥"},
|
||||
]
|
||||
|
||||
|
||||
def gallery_archive(request: HttpRequest) -> HttpResponse:
|
||||
"""
|
||||
Общий архив галереи транса с фильтрацией по этажам и сортировкой.
|
||||
@@ -123,20 +133,12 @@ def gallery_archive(request: HttpRequest) -> HttpResponse:
|
||||
page_number = request.GET.get("page", 1)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
sort_options = [
|
||||
{"id": "gravity", "title": "По гравитации (f_score)", "icon": "🌀"},
|
||||
{"id": "new", "title": "Свежие (по дате)", "icon": "✨"},
|
||||
{"id": "likes", "title": "По числу лайков", "icon": "♥"},
|
||||
{"id": "views", "title": "Редкие (мало показов)", "icon": "👁"},
|
||||
{"id": "popular", "title": "Популярные по показам", "icon": "🔥"},
|
||||
]
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"current_floor": floor,
|
||||
"current_sort": sort,
|
||||
"counts": counts,
|
||||
"sort_options": sort_options,
|
||||
"sort_options": GALLERY_SORT_OPTIONS,
|
||||
}
|
||||
return render(request, "gallery/archive.html", context)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user