M9K blog

Заметки ITшника обо всём

Suno. «Не по ГОСТу».

Чутка про жизнь.

Мне тридцать с лишним. Всё нормально.
Есть отпуск, пропуск и пароль.
И даже кризис экзистенциальный
Теперь записан как рабочий сбой.

С утра упал продакшен, вечером — мораль,
Я снова поднимаю то, что рухнуло вчера.
Коллега снова прошлое торжественно достал
И объясняет молодым, как правильно тогда.

Он помнит вкус пломбира, пионерский звонкий хор,
Хотя застал Союз примерно несколько минут.
Ему уютно думать: раньше был другой простор,
Где правильные люди правильнее живут.

А я чиню систему, где опять потерян час,
Где будущее требует исправить каждый раз.
Я знаю: если сервис не проходит по ТЗ,
Возможно, дело в сервисе. А может быть — в ТЗ.

Мне говорят: «Пора уже серьёзней выбирать:
Семья, машина, дети — всё положено иметь».
Но если это взрослость — научиться выбирать
Из списка только то, что полагается иметь?

Я не по ГОСТу, не по плану,
Не по семейному уставу.
Мне говорят: «Ты станешь взрослой».
Я взрослой стала. Что мне стало?

Я не по ГОСТу, не по плану,
Не помещаюсь в ваш чертёж.
Мне говорят: «Когда-нибудь ты
До нашей истины дойдёшь».

Я столько лет училась всё упавшее чинить:
Процессы, базы, сервисы — проверить и поднять.
Но непонятно, как себя обратно подключить,
Когда нигде не пишут, что сломалось и опять.

Вокруг давно размечен обязательный маршрут:
Работа, дом, машина, дети — правильный этап.
И люди точно знают, как приличные живут,
И каждый шаг заранее прописан в roadmap.

А я смотрю на город, где давно известен путь:
Работа, магазин, квартира, снова по кольцу.
Но если это «жизнь» — нельзя ли где-нибудь свернуть?
Желательно направо. По указателю «Аэропорт».

Я не по ГОСТу, не по плану,
Не по семейному уставу.
Мне говорят: «Ты станешь взрослой».
Я взрослой стала. Что мне стало?

Я не по ГОСТу, не по плану,
Не помещаюсь в ваш чертёж.
Мне говорят: «Когда-нибудь ты
До нашей истины дойдёшь».

В аэропорту я становлюсь чуть-чуть живей:
Здесь никому нет дела до отсутствия детей.
Здесь не спросят, почему не куплена квартира,
И не потребуют любить руины старого мира.

Сдаю багаж, прохожу контроль,
Оставив на досмотре социальную роль.
И нелюбимый город уменьшается в окне,
И первый раз за месяцы спокойнее во мне.

Там внизу остались «надо», «принято», «пора»,
Разговоры, как прилично человеку тридцать плюс.
А облака не знают ни начальства, ни двора,
Ни почему от правильного будущего грусть.

В иллюминаторе — оранжевая нить.
Я не решила, как мне дальше, где мне жить.
Но на минуту между облаком и тьмой
Мне хорошо от права просто быть собой.

Но самолёт садится.
Так заведено.
И город возникает
За мокрым стеклом.

Я знаю, как всё чинят.
Как сервисы поднять.
Но как мне дальше жить —
Негде прочитать.

Я не по ГОСТу, не по плану,
Не помещаюсь в ваш чертёж.
Мне говорят: «Когда-нибудь ты
До нашей истины дойдёшь».

Но если ваша жизнь нормальна,
А всё другое — ерунда,
Зачем мне легче ненормальной
На высоте, чем здесь всегда?

Мне тридцать с лишним. Всё нормально.
Есть отпуск, пропуск и пароль.
И только кризис экзистенциальный
Не закрывается как сбой.

Создание пользователей в Debian и Astra Linux

Cкрипт для создания локальных пользователей с настройкой passwordless sudo.
Определяет дистрибутив через /etc/os-release. Для Astra Linux дополнительно выполняет pdpl-user -l 0:63 -i 63.
Проверяет наличие утилит, не пересоздаёт существующих пользователей и не изменяет их пароли. Конфигурация sudo проверяется через visudo. Поддерживает повторный запуск.
Запуск:

./create-users.sh user1 user2 user3
#!/bin/bash
set -Eeuo pipefail

DEFAULT_USERS=(user1 user2 user3)

SUDOERS_DIR="/etc/sudoers.d"
SUDOERS_MODE="0440"
USER_SHELL="/bin/bash"

ASTRA_PDPL_LEVEL="0:63"
ASTRA_INTEGRITY="63"

TEMP_FILE=""

log() {
    printf '[INFO] %s\n' "$*"
}

die() {
    printf '[ERROR] %s\n' "$*" >&2
    exit 1
}

cleanup() {
    if [[ -n "$TEMP_FILE" && -f "$TEMP_FILE" ]]; then
        rm -f -- "$TEMP_FILE"
    fi
}

trap cleanup EXIT

# Проверка привилегий.
if [[ "$EUID" -ne 0 ]]; then
    die "Скрипт необходимо запускать от root."
fi

# Определение дистрибутива.
OS_RELEASE="/etc/os-release"

if [[ ! -r "$OS_RELEASE" ]]; then
    OS_RELEASE="/usr/lib/os-release"
fi

if [[ ! -r "$OS_RELEASE" ]]; then
    die "Не найден файл os-release."
fi

unset ID PRETTY_NAME

. "$OS_RELEASE"

case "${ID:-}" in
    debian)
        OS_TYPE="debian"
        ;;
    astra)
        OS_TYPE="astra"
        ;;
    *)
        die "Неподдерживаемая ОС: ${ID:-unknown}"
        ;;
esac

log "ОС: ${PRETTY_NAME:-$OS_TYPE}"
log "Тип: $OS_TYPE"

# Проверка необходимых компонентов.
REQUIRED_COMMANDS=(
    useradd
    passwd
    getent
    mktemp
    visudo
    chmod
    chown
    mv
)

if [[ "$OS_TYPE" == "astra" ]]; then
    REQUIRED_COMMANDS+=(pdpl-user)
fi

for cmd in "${REQUIRED_COMMANDS[@]}"; do
    if ! command -v "$cmd" >/dev/null 2>&1; then
        die "Не найдена команда: $cmd"
    fi
done

if [[ ! -x "$USER_SHELL" ]]; then
    die "Не найден shell: $USER_SHELL"
fi

if [[ ! -f /etc/sudoers || ! -d "$SUDOERS_DIR" ]]; then
    die "Не найдена конфигурация sudo."
fi

# Проверка существующей конфигурации sudo.
if ! visudo -c >/dev/null; then
    die "Обнаружена ошибка в существующей конфигурации sudo."
fi

# Выбор пользователей.
if [[ "$#" -gt 0 ]]; then
    USERS=("$@")
else
    USERS=("${DEFAULT_USERS[@]}")
fi

# Предварительная проверка имён.
for user in "${USERS[@]}"; do
    if [[ ! "$user" =~ ^[a-z_][a-z0-9_-]*$ ]]; then
        die "Некорректное имя пользователя: $user"
    fi
done

# Создание и настройка пользователей.
for user in "${USERS[@]}"; do

    log "Обработка пользователя: $user"

    # Создание пользователя.
    if getent passwd "$user" >/dev/null; then
        log "Пользователь $user уже существует."
    else
        log "Создание пользователя $user"

        useradd \
            -m \
            -s "$USER_SHELL" \
            "$user" \
            || die "Не удалось создать пользователя $user"

        # Пароль устанавливается только новому пользователю.
        log "Установка пароля для $user"

        if ! passwd "$user"; then
            die "Ошибка установки пароля $user. Пользователь создан, задайте пароль вручную."
        fi
    fi

    # Настройка мандатных атрибутов Astra Linux.
    if [[ "$OS_TYPE" == "astra" ]]; then

        log "Настройка мандатных атрибутов: $user"

        pdpl-user \
            -l "$ASTRA_PDPL_LEVEL" \
            -i "$ASTRA_INTEGRITY" \
            "$user" \
            || die "Ошибка pdpl-user для $user"
    fi

    # Создание временного файла sudoers.
    SUDOERS_FILE="${SUDOERS_DIR}/${user}"

    TEMP_FILE=$(mktemp "${SUDOERS_DIR}/.${user}.XXXXXX") \
        || die "Не удалось создать временный sudoers."

    printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$user" > "$TEMP_FILE"

    chown root:root "$TEMP_FILE"
    chmod "$SUDOERS_MODE" "$TEMP_FILE"

    # Проверка синтаксиса перед установкой.
    if ! visudo -cf "$TEMP_FILE" >/dev/null; then
        die "Некорректная конфигурация sudo для $user"
    fi

    # Установка проверенной конфигурации.
    mv -f -- "$TEMP_FILE" "$SUDOERS_FILE"
    TEMP_FILE=""

    log "Sudo настроен для $user"

done

# Финальная проверка.
visudo -c >/dev/null \
    || die "Ошибка итоговой проверки sudoers."

log "Все пользователи успешно обработаны."

Cost of Hope

Я отдал GSC деньги за ванильный STALKER 2 (предзаказ!), прошёл его на релизе и, несмотря на весь цирк с A-Life, странную мотивацию Скифа; персонажей, которые периодически разговаривали и действовали как люди с очень специфическими повреждениями лобной доли, в целом остался доволен. Где-то кринжевал, где-то матерился, где-то даже сопереживал Бродяге. А Припять дала совершенно роскошное ощущение узнавания.

И дело не в том, что «ой, панельки, ржавчина, СССР». Я живу примерно в такой же постсоветской срани и прекрасно знаю, как она выглядит. Какая плитка бывает в коридоре. Какого цвета красят нижнюю половину стены. Как рядом существуют дверь из восьмидесятых, китайский автомат из двухтысячных и табличка «Двор образцового содержания» образца 1971 года. Как труба идёт не там, где красиво, а там, где сантехник смог её провести, местами переходя в неевклидово пространство. Как бетонный забор, административное здание, подстанция и двор образуют пространство, которое никто не проектировал для эффектного скриншота.

Ванильный STALKER 2 местами попадал в это ощущение почти физически. Он выглядел не как тематический парк «Восточная Европа после апокалипсиса», а как знакомое пространство, в которое зачем-то добавили Зону.

Cost of Hope это ощущение для меня практически уничтожил.

Главная проблема дополнения даже не в плохом сюжете, не в Мавке, не в Зулусе, не в монорельсе, не в мозге в банке и не в затопленных подвалах НИИЧАЗа. Проблема в том, что поменялся сам договор между игрой и игроком. В хорошем STALKER мир существует сам по себе, а ты в нём временно находишься. В Cost of Hope мир слишком часто существует исключительно для того, чтобы через тридцать секунд развлечь тебя следующим аттракционом.

Вот дверь. За ней бой. Вот коридор. В конце катсцена. Вот ЧАЭС. Разумеется, там будет локальный Call of Duty с бесконечными пачками вооружённых болванчиков. Вот лаборатория. Конечно, в ней должен быть пространственный пазл. Вот огромная установка. Надо по ней полазить. Вот мозг в колбе. Надо устроить вокруг него финальную арену. Вот мутанты. Их тоже кинем в перестрелку, потому что вдруг игрок на двадцать секунд останется без «контента».

Особенно смешно это выглядит на фоне того, как GSC два года чинила ванильную игру от синдрома «я повернулся, а в десяти метрах за спиной материализовалась новая жизнь». Они буквально два года приводили A-Life к состоянию, когда игрок меньше замечает спавнер. И после этого в Cost of Hope устраивают на ЧАЭС ту же болезнь в пятикратном масштабе, только уже намеренно.

Первое исследование ЧАЭС ещё работает. Потому что некоторое время игра просто позволяет тебе там находиться. Огромные промышленные сооружения, территория, которая долго была недоступна, само ощущение места. Ничего особенного даже делать не нужно. ЧАЭС сама по себе уже событие.

А потом начинается война группировок, и всё быстро превращается в карту для многопользовательского шутера. Болванчики бегут туда, болванчики бегут сюда, дикий респавн, стрельба , для веселья ещё и мутантов подсыпали.

И чем больше людей игра запихивает на ЧАЭС, тем меньше становится сама ЧАЭС. Если там одновременно вполне нормально функционируют небольшие армии, это уже не запретное сердце Зоны. Это просто горячий район с плохой транспортной доступностью.

Железный лес при этом замечательный. Именно поэтому дополнение особенно раздражает: оно периодически показывает, что всё ещё умеет быть STALKER. Открытые пространства, реальные или хотя бы правдоподобные промышленные объекты, места, в которых география первична, а геймплей к ней приспосабливается.

Потом начинается Лиманск, и я попадаю в сон собаки про нуарный НИИ «Радиоволна».

Я понимаю, почему его любят. Там красиво. Туман. Свет. Мрачные силуэты. Странные интерьеры. Всё очень атмосферно, если атмосфера для вас — это количество атмосферных (простите за тавтологию) объектов на квадратный метр.

Но старый STALKER был сильнее именно тогда, когда сначала показывал что-то абсолютно обыденное. НИИ должен сначала выглядеть местом, где в 1985 году сидела бухгалтерия, курил инженер Коля, а на втором этаже висел стенд по технике безопасности. И уже потом ты узнаёшь, что в подвале кто-то пытался свернуть человеческое сознание в трубочку.

В Cost of Hope некоторые места с порога кричат: «Смотрите, какая у нас загадочная хоррор-локация». После этого они перестают быть загадочными примерно через пять минут.

Отдельный привет истории с монорельсом, соединяющим лаборатории. Мы лазили по этим лабораториям двадцать лет. X18, X16, X-что-то-там. Мы читали документы, таскали папки, встречались с учёными, военными, сталкерами, бывшими сотрудниками исследовательских программ. И никто никогда не считал нужным упомянуть, что под Зоной, оказывается, существует секретное метро стратегического масштаба.

Это не расширение лора. Это реткон из категории «сейчас мы добавим что-нибудь очень большое, потому что большое значит важное».

Причём чем дольше об этом думаешь, тем смешнее становится. Подземная сеть на километры — это не «тайный тоннель». Это проходка, вентиляция, дренаж, энергоснабжение, депо, ремонт, бетон, вывоз грунта, геологи, строители, техника, годы работ и тысячи людей. В какой-то момент сама инфраструктура становится более грандиозным секретным проектом, чем лаборатории, которые она якобы должна незаметно соединять.

Но сценарий просто кладёт этот факт перед тобой и идёт дальше. Прими, пожалуйста. Следующая сцена уже загружается.

Старая серия была умнее именно потому, что многое не объясняла. Мы знали, что лаборатории связаны. Оборудование перемещалось. Документы ходили между объектами. Люди работали в рамках общей программы. Этого было достаточно.

Не каждую дырку в лоре обязательно заливать бетоном.

Иногда вопрос «а как именно всё это было связано?» намного интереснее ответа «там был секретный монорельс».

С нарративом в целом та же болезнь: игра постоянно повышает ставки, совершенно не заботясь о том, повысился ли мой интерес.

Ванильный STALKER 2 тоже был «голливудским». Скиф временами пустой ГГ, мотивация почти анекдотическая; крупные персонажи любят внезапно совершать эффектные поступки вместо разумных. Но история хотя бы имела хребет. Скиф приехал по конкретной причине, оказался втянут в более крупный конфликт и постепенно понял, насколько глубоко в него залез.

Cost of Hope выглядит как кипиш ради кипиша.

Скиф встречает людей, которых знает от силы пять минут, и почти сразу готов ради них лезть в ад. Через некоторое время он уже решает судьбу Долга и Свободы, участвует в гражданской войне, раскапывает очередной пласт секретных программ и спасает персонажей, к которым игра ещё не успела дать мне возможность хоть что-нибудь почувствовать.

Особенно хорошо это видно в лагере в Железном лесу. Игра очень хочет трагедии. Вот «пипл, знакомьтесь, сталкер».Вот карты. Вот сцена. Вот погибшие. Вот серьёзные лица. Пожалуйста, испытывайте эмоции.

А я смотрю на это и думаю: кто все эти люди?

В лагере новичков SoC 2007 года персонажи состояли из трёх полигонов, двух анимаций и одного набора лиц. Но я туда возвращался. Там сидели люди. Кто-то играл на гитаре. Стоял Волк. Был Петруха. Был костёр. Через несколько часов это становилось местом. Игра не включала трагическую музыку, чтобы объяснить мне, что место важно.

Cost of Hope почему-то уверена, что эмоциональную связь можно заменить постановкой.

Мавка и Зулус существуют примерно по тому же принципу. Они яркие, хорошо выглядят, неплохо озвучены и совершенно разваливаются, если попытаться воспринимать их как живых людей, а не механизмы для запуска нужных сценаристу событий.

В нормальной драме персонаж обладает характером, поэтому принимает решение, из которого возникает событие. Здесь слишком часто сначала существует событие, а потом сценарист быстро подкручивает персонажа так, чтобы тот сделал необходимую глупость.

Нужна эскалация — вот вам эскалация. Нужен фанатизм — сейчас будет фанатизм. Нужна жестокость — человек, который пять минут назад выглядел относительно адекватным, внезапно включит режим сюжетного психопата.

Потом игрок зачем-то должен сам собирать из этих кусков цельную психологию.

Наёмников дополнение сливает ещё смешнее. В Железном лесу и Лиманске они почти боги. Организованные, профессиональные, опасные, знающие территорию. Игра долго убеждает тебя, что это действительно серьёзный противник.

Потом приходит лаборатория с мозгом в банке.

И эти элитные убийцы начинают выбегать пачками, получать очередь и освобождать место для следующей пачки. В этот момент они перестают быть наёмниками и становятся расходным материалом encounter designer’а.

Нет более надёжного способа уничтожить угрозу врага, чем заставить игрока убить его сорок раз подряд.

То же самое происходит с мутантами. Первая встреча с кровососом на Агропроме в 2007 году обеспечивала несколько кирпичей не потому, что кровосос обладал искусственным интеллектом уровня DeepMind. Игра просто правильно его подавала. Подземелье, темнота, звуки, неизвестность.

В STALKER 2 кровососа выкатывают примерно через пятнадцать минут после начала. Ну ладно. Фансервис. Ребята, вы купили STALKER — держите кровососа. В Cost of Hope мутанты на ЧАЭС уже окончательно превращаются в специю для боя. Людей недостаточно? Добавим монстра.

Кровосос проходит прекрасный путь от таинственной твари до фирменного маскота, которого можно кинуть в encounter table между снайпером и очередным отрядом NPC.

Подвалы НИИЧАЗа стали для меня главным хоррором дополнения, только не в том смысле, в каком хотелось бы авторам. На последней сложности я умер там около десяти раз и в итоге открыл прохождение.

Это не «сложный момент».

Это провал левел-дизайна.

Когда я умираю, потому что неправильно оценил противника, плохо подготовился или не понял правило аномалии — прекрасно. Я чему-то учусь внутри игры.

Когда после очередной смерти я думаю: «Так, а в какой именно точке дизайнер предполагает, что я должен всплыть и на какую железку запрыгнуть?» — я больше не играю Скифом. Я занимаюсь обратной инженерией маршрута — а это хватает и на работе.

И если для прохождения обязательного линейного участка мне проще посмотреть видео, чтобы понять намерение автора, то автор облажался. Тут даже спорить особенно не о чем.

Гигантомания в НИИЧАЗ и лабораториях выглядит примерно так же. Огромные помещения, огромные установки, многоэтажные конструкции, пространственные головоломки, прыжки по балкам.

Где-то из 1998 года машет рукой Lambda Core. Если это отсылка к Half-Life, то спасибо, конечно. Я понял — я его застал на релизе.

В Half-Life подобные вещи работали, потому что вся Black Mesa была построена на этом художественном языке. Гордон всю игру лазил по лифтам, механизмам, трубам, реакторам и конвейерам. Lambda Core был кульминацией того, чему игра его до этого учила.

В STALKER я хочу войти в лабораторию и подумать: «Что здесь, чёрт возьми, делали?»

В Cost of Hope я захожу в помещение и думаю: «Ага, здесь сейчас будет платформинг».

Или: «Вот это арена».
Или: «По этим балкам мне явно придётся идти».
Как только я начинаю видеть намерение левел-дизайнера раньше самого места, погружение заканчивается.

Колбы с мутантами формально вообще не противоречат лору. Тайные эксперименты были всегда. Но опять же важна подача. В старых лабораториях было ощущение, что здесь когда-то происходило что-то страшное и теперь ты ходишь по последствиям.

Cost of Hope временами устраивает экспозицию.

Вот мутант.
Вот ещё мутант.
Вот большой мозг.
Вот очень большая научная штука.
Получается не заброшенная советская лаборатория, а очередной Resident Evil facility.
Вроде бы слова те же, а язык уже другой.

Даже визуально DLC для меня местами теряет ту самую постсоветскую достоверность ванили. Базовая игра долго разрабатывалась ещё до 2022 года и унаследовала очень характерную историческую мешанину советского, русского, украинского. Именно мешанину, потому что реальное пространства так и выглядят.

В Cost of Hope местами появляется ощущение, что исторический объект начали художественно переписывать в соответствии с очевидной и понятной печальной повесткой дня текущего. Проблема в том, что ЧАЭС и Припять — это материальные объекты Украинской ССР. Они неизбежно содержат смесь СССР, УССР, русского, украинского, советской ведомственной типографики, эмблем, канцелярии и конкретного исторического мусора. И видя очередную «небезпеку» на объекте, который всем своим видом отсылает сильно в другому историческому периоду — погружение ломается.

Если этот слой слишком аккуратно причесать, объект перестаёт выглядеть найденным. Он начинает выглядеть нарисованным.

И для человека из постсоветской среды эта фальшь заметна намного сильнее, чем, вероятно, для рецензента, который видит просто «old Eastern European industrial building».

В итоге с жанром у дополнения тоже получается довольно смешно.

Как хоррор Cost of Hope проигрывает множеству современных инди, которым не нужны огромные бюджеты, чтобы заставить игрока бояться коридора.

Как экшен он временами напоминает какой-то польский шутер про американских морпехов: всё взрывается, все куда-то бегут, враги идут пачками, вокруг происходит Очень Важная Битва.

Как immersive exploration он сам себе постоянно мешает скриптами.

Как нарратив он требует принимать слишком много новых допущений и слишком быстро привязываться к людям, которых игрок почти не знает.

Зато красивые экстерьеры у него действительно красивые.

Вот на них всё и держится.

Поэтому я плохо понимаю тот масштаб восторга, который вызвало дополнение у рецензентов/игрожуров. Видимо, мы просто хотим от STALKER совершенно разных вещей.

Если для человека хороший STALKER — это больше событий, больше катсцен, больше необычных механик, больше сюжетных поворотов, больше боёв и меньше пустого пространства, Cost of Hope действительно может показаться прекрасным развитием формулы.

Для меня STALKER работает ровно наоборот.

Ты идёшь десять минут, и ничего не происходит.
Потом ещё пять.
Вдалеке кто-то стреляет.
Ты решаешь туда не ходить.
Потом заходишь в пустое здание.
Слышишь непонятный звук.
И вот это уже событие.

Современный AAA-дизайнер посмотрит на первые пятнадцать минут и скажет: «Надо улучшить pacing».

Нет.
Это и был pacing.

Я заплатил за дополнение к игре, которая при всех своих недостатках давала мне иллюзию враждебного мира, существующего независимо от моего присутствия.

А получил аттракцион.
Причём не лучший аттракцион.

С посредственным нарративом, местами посредственным геймдизайном, гигантоманией, ретконами, тирными вставками, аренными боями, пространственными головоломками из прошлой эпохи и режиссурой, которая постоянно пытается сообщить мне, что вот сейчас, пожалуйста, надо испытать сильную эмоцию.

Cost of Hope лучше всего работает там, где у дизайнеров меньше свободы и есть реальный прототип, который приходится уважать.

Как только дверь закрывается и разработчики остаются один на один со своей фантазией, я слишком часто перестаю видеть Зону.

Я вижу уровень.

Для STALKER это, пожалуй, худшее, что можно сказать.

Git: превращаем вложенный репозиторий в обычную директорию

Если скопировать один Git-проект внутрь другого и выполнить commit, Git сохранит вложенный проект как gitlink (режим 160000). В GitLab вместо содержимого директории будет отображаться ссылка на commit:
init-server @ 957c7776
Простое удаление вложенного .git не поможет: ссылка уже записана в индексе родительского репозитория.
Решение:

  1. Удаляем gitlink из индекса, сохраняя файлы на диске:
git rm --cached init-server
  1. Удаляем метаданные вложенного репозитория, если они ещё существуют:
rm -rf init-server/.git
  1. Добавляем содержимое как обычные файлы:
git add -A init-server/
  1. Проверяем:
git ls-files --stage init-server

Вместо записи с режимом 160000 должны появиться отдельные файлы с режимами 100644 или 100755.

  1. Фиксируем изменения:
git commit -m "Convert init-server to regular directory"
git push

Suno. «Чужое своё».

Продолжение темы городов — далёких и близких

Мне было пять.
Первый раз — за границу.
Хотя какая граница,
когда тебе пять?

Я потом сюда ездил десятки раз —
Самолётом, поездом, летом, зимой.
И однажды заметил: знакомый пейзаж
Становится дальше от слова «чужой».
Здесь «выше» — до гор, а «ниже» — от них,
И город читается без указаний.
Я к этой системе давно уж привык,
Без карт и подсказок, без лишних названий.

Я не местный — и спорить об этом нелепо,
Но туристом назвать себя тоже смешно.
Я смотрю на проспекты, на крыши, на небо —
И всё это знакомо до странности, но
Здесь другая реальность могла бы сложиться,
Другой адрес, привычки, работа и дом.
Я не знаю, каким бы я стал в этой жизни,
Но порой узнаю себя в городе том.

Алматы — чужое, где вижу своё,
Как жизнь, что могла бы достаться мне.
Другой поворот, пара улиц, район —
И, может быть, я бы остался в нём.
Где горы висят на краю облаков,
Где город знаком, хоть и мне не дан.
Я будто читаю историю сна,
Где жизнь незнакомая слишком родна.

Я знаю язык на смешном бытовом:
«Дүке́н», «кіта́п», «су́» — и ещё с деся́ток.
А «балмұзда́қ» запо́мнил ещё пацано́м.
Ведь мороженое понятно на языке любом.
«Как мне на Абая?» — спросили опять.
«До Космонавтов, направо, кварталов на пять».
«До Байтурсы́нова», — тут же поправлю себя.
Я старого имени не застал никогда.

Но оба названия помню на равных,
Как будто знакомы мне с детства давно.
И странно: чужой город стал настолько понятным,
Что два его имени слились в одно.

Здесь Пушкин и Аба́й, здесь Гоголь и Досты́к —
К соседству этих улиц я давно уже привык.
У каждого названия свой голос и свой след,
Но между ними спора на городской карте нет.

Алматы — чужое, где вижу своё,
Как жизнь, что могла бы достаться мне.
Другой поворот, пара улиц, район —
И, может быть, я бы остался в нём.
Где горы висят на краю облаков,
Где город знаком, хоть и мне не дан.
Я будто читаю историю сна,
Где жизнь незнакомая слишком родна.

А ночами мне снятся не улицы даже.

Меде́у — ступени теряются в утренней мгле.
Шымбұла́қ — и кажется, будто я больше не на земле.
Көк-Жайла́у — тропа растворяется в облаках,
А Алматы́ остаётся внизу, в огнях.

И смешно:
во сне я не думаю,
чей это город,
чья это земля,
чей язык на вывеске.

Я просто знаю дорогу.

И если бы жизнь повернула иначе
Другой самолёт, другой двор, другой класс
Я б горы считал просто частью пейзажа
Не вскидывал голову каждый раз.
Я мог бы всю неделю спешить на работу,
Ругаться на пробки, решать все дела.
А в горы с друзьями уехать в субботу,
Подальше от шума, асфальта, тепла.

Мог бы знать этот город не лучше — иначе:
По рабочим маршрутам, по будням, дождям.
Не искать в нём чудес, не ловить каждый миг,
А с рождения просто считать, что привык.

Алматы — чужое, где вижу своё,
Как жизнь, что прошла в параллельной строке.
Другой поворот, пара улиц, район —
И, может быть, я бы остался в нём.

Мне было пять.
Первый город за границей.

Наверное, поэтому
я так и не научился
считать его заграницей.

Rootless Podman внутри Proxmox LXC: проблема UID/GID mapping

При запуске File Browser через rootless Podman образ скачивался, но распаковка завершалась ошибкой:

potentially insufficient UIDs or GIDs available in user namespace (requested 65534:65534)
lchown /home: invalid argument

Причина: пользователю podman выделено только 50 000 subordinate UID/GID. Образ содержит файлы с UID/GID 65534, которые не помещаются в доступный диапазон.

Механизм

Используются два уровня отображения идентификаторов:
Podman: UID/GID образа → UID/GID внутри Debian LXC.
LXC: UID/GID Debian → UID/GID хоста Proxmox.

Настройки Podman определяются файлами /etc/subuid и /etc/subgid. Внешний mapping LXC задаётся через lxc.idmap.

Исходная конфигурация Proxmox:

root:100000:65536
deploy:165536:65536

Непривилегированному LXC доступны 65 536 идентификаторов. Расширить диапазон непрерывно нельзя: следующие идентификаторы уже выделены пользователю deploy.

Решение:

Выделить свободный диапазон на Proxmox и добавить его в /etc/subuid и /etc/subgid хоста:

root:231072:15536

В конфигурации LXC сохранить исходное отображение и добавить новое:

lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536
lxc.idmap = u 65536 231072 15536
lxc.idmap = g 65536 231072 15536

Внутри LXC контейнера дополнить существующий диапазон пользователя podman:

podman:10000:50000
podman:65536:15536

Настроить аналогично UID и GID, предварительно проверив отсутствие пересечений с другими диапазонами. После остановки приложений перезапустить LXC и выполнить:

podman system migrate

Изменение mapping не переносит владельцев существующих файлов автоматически.

Proxmox 7: включение Docker и Podman в LXC

Для запуска Docker или Podman внутри LXC-контейнера необходимо активировать дополнительные возможности: nesting, keyctl и fuse.

Скрипт запрашивает ID контейнера, проверяет наличие конфигурации, создаёт резервную копию с меткой времени и включает необходимые параметры. Остальные настройки features сохраняются.

Запускать от root на хосте Proxmox.

#!/bin/bash

read -rp "Введите ID контейнера (CTID): " CTID

if [[ ! "$CTID" =~ ^[0-9]+$ || ! -f "/etc/pve/lxc/${CTID}.conf" ]]; then
    echo "Ошибка: некорректный CTID или конфигурация не найдена"
    exit 1
fi

CONF="/etc/pve/lxc/${CTID}.conf"
BACKUP_DIR="/root/lxc-config-backups"

mkdir -p "$BACKUP_DIR" &&
cp "$CONF" "$BACKUP_DIR/${CTID}.conf.$(date +%Y%m%d_%H%M%S).bak" &&
pct set "$CTID" --features "$(awk -F, '/^features:/{sub(/^features:[[:space:]]*/, ""); for(i=1;i<=NF;i++) if($i !~ /^(nesting|keyctl|fuse)=/) printf "%s,", $i} END{print "nesting=1,keyctl=1,fuse=1"}' "$CONF")" &&
pct config "$CTID" | grep '^features:'

Пример выполнения:

Введите ID контейнера (CTID): 177
features: fuse=1,keyctl=1,nesting=1

Резервная копия конфигурации сохраняется в /root/lxc-config-backups/.

После изменения параметров необходимо полностью остановить и запустить контейнер:

pct stop 177 && pct start 177

Update:
Ещё одна ошибка:

Failed to open() /dev/net/tun: No such file or directory
Failed to set up tap device in namespace

Чиним:

#!/bin/bash
set -euo pipefail

CTID=178
CONF="/etc/pve/lxc/${CTID}.conf"
BACKUP="${CONF}.$(date +%Y%m%d%H%M%S).bkp"

modprobe tun
test -c /dev/net/tun

cp -a "$CONF" "$BACKUP"

RULES=(
    "lxc.cgroup2.devices.allow: c 10:200 rwm"
    "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file"
)

for rule in "${RULES[@]}"; do
    grep -Fqx -- "$rule" "$CONF" || printf '%s\n' "$rule" >> "$CONF"
done

echo "Backup: $BACKUP"
echo "Configuration:"
grep -E '^lxc\.(cgroup2\.devices\.allow|mount\.entry):' "$CONF"

Prettymaps в Docker: генератор картографических постеров

Поигрался с либой PrettyMaps, которая умеет строить из OSM прикольные постеры.

Собрал контейнер, который позволяет сделать карту.

Сборка без прокси:

DOCKER_BUILDKIT=1 docker build -t prettymaps-docker-test .

Сборка с SOCKS5-прокси:

DOCKER_BUILDKIT=1 docker build --network=host --build-arg PROXY_SOCKS5=$PROXY_IP:$PROXY_PORT -t prettymaps-docker-test .

Запуск:

docker run --rm \
  -v "$(pwd)":/work \
  -e LAT=54.8482742 \
  -e LON=83.0973263 \
  -e RADIUS_M=1500 \
  -e TITLE="Академгородок" \
  -e OUTPUT_NAME=akadem_coords \
  prettymaps-docker-test

Dockerfile

# syntax=docker/dockerfile:1
#
# Build without proxy:
# DOCKER_BUILDKIT=1 docker build -t prettymaps-docker-test .
#
# Build with SOCKS5 proxy:
# DOCKER_BUILDKIT=1 docker build --network=host \
#     --build-arg PROXY_SOCKS5=$PROXY_IP:$PROXY_PORT \
#     -t prettymaps-docker-test .
#
# Do not upgrade pip (newer pip+urllib3 may break SOCKS).
# Pre-install shapely to avoid resolver loops on shapely[vectorized].

# --------------------------------------------------------------------------
# Stage 1: builder
# --------------------------------------------------------------------------

FROM debian:bookworm-slim AS builder

ARG PROXY_SOCKS5=""
ARG DEBIAN_FRONTEND=noninteractive

ENV LANG=C.UTF-8 \
    LC_ALL=C.UTF-8 \
    PYTHONUNBUFFERED=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_DEFAULT_TIMEOUT=120 \
    PIP_PREFER_BINARY=1

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    rm -f /etc/apt/apt.conf.d/docker-clean && \
    apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        python3 \
        python3-pip \
        python3-venv \
        python3-dev \
        python3-socks \
        build-essential \
        gdal-bin \
        libgdal-dev \
        libgeos-dev \
        libproj-dev \
        libspatialindex-dev \
        git \
        fonts-dejavu-core

# Python environment
RUN python3 -m venv --system-site-packages /opt/venv

ENV PATH="/opt/venv/bin:$PATH"

# Optional SOCKS5 proxy.
# Empty PROXY_SOCKS5 disables the proxy.
# Proxy settings apply only to the builder stage.

ENV ALL_PROXY=${PROXY_SOCKS5:+socks5h://}${PROXY_SOCKS5}

ENV HTTP_PROXY=${ALL_PROXY} \
    HTTPS_PROXY=${ALL_PROXY} \
    http_proxy=${ALL_PROXY} \
    https_proxy=${ALL_PROXY} \
    no_proxy=localhost,127.0.0.1

# --------------------------------------------------------------------------
# Python dependencies
# --------------------------------------------------------------------------

# 1. Base tooling, NumPy and Shapely

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install \
        setuptools \
        wheel \
        pysocks \
        "numpy>=1.26.4" \
        "shapely>=2.0.0"

# 2. Prettymaps runtime dependencies
# Streamlit and Marimo are intentionally excluded.

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install \
        "matplotlib>=3.9.0" \
        "osmnx>=2.0.5" \
        "rasterio>=1.4.3" \
        "rioxarray>=0.18.2" \
        "opencv-python-headless>=4.11.0.86" \
        "scikit-image>=0.25.2" \
        "thefuzz>=0.22.1" \
        "elevation>=1.1.3" \
        "tqdm>=4.70.0" \
        "scikit-learn>=1.6.1" \
        "vsketch>=1.0.0" \
        "ipython" \
        "ipykernel>=6.29.5"

# 3. Prettymaps without optional dependencies

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install prettymaps --no-deps

# --------------------------------------------------------------------------
# Stage 2: runtime
# --------------------------------------------------------------------------

FROM debian:bookworm-slim AS runtime

ARG DEBIAN_FRONTEND=noninteractive

ENV LANG=C.UTF-8 \
    LC_ALL=C.UTF-8 \
    PYTHONUNBUFFERED=1 \
    PATH="/opt/venv/bin:$PATH"

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    rm -f /etc/apt/apt.conf.d/docker-clean && \
    apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates \
        python3 \
        python3-socks \
        gdal-bin \
        libgdal32 \
        libgeos-c1v5 \
        libproj25 \
        libspatialindex6 \
        fonts-dejavu-core \
        libgl1 \
        libglib2.0-0 \
        libxkbcommon0 \
        libdbus-1-3 \
        libxcb-cursor0 \
        libxcb-xinerama0

# Copy Python virtual environment
COPY --from=builder /opt/venv /opt/venv

# Application
WORKDIR /work

VOLUME /work

COPY generate.py /opt/generate.py

# Prefer user-provided script from /work.
# Fall back to the bundled script.

ENTRYPOINT ["sh", "-c", "script=generate.py; if [ -f \"/work/$script\" ]; then exec python3 \"/work/$script\" \"$@\"; else exec python3 \"/opt/$script\" \"$@\"; fi", "--"]

CMD []

Скрипт generate.py

#!/usr/bin/env python3
"""
Generate a prettymaps poster.

Parameters (priority: CLI args > env vars > defaults):

  QUERY / --query              location string (geocoded by osmnx)
  LAT / --lat                  latitude (use with LON instead of QUERY)
  LON / --lon                  longitude
  RADIUS_M / --radius          radius in metres
  CIRCLE / --circle            circular clip (true/false, 1/0, yes/no)
  TITLE / --title              map title
  PRESET / --preset            prettymaps preset name
  OUTPUT_NAME / --output-name  basename for png/pdf (no extension)
  DPI / --dpi                  output dpi
"""

import argparse
import os
from pathlib import Path

import osmnx as ox

# osmnx 2.x: project_gdf left the top-level namespace
if not hasattr(ox, "project_gdf"):
    ox.project_gdf = ox.projection.project_gdf
if not hasattr(ox, "project_graph"):
    if hasattr(ox, "projection") and hasattr(ox.projection, "project_graph"):
        ox.project_graph = ox.projection.project_graph

import prettymaps
import prettymaps.draw as _pm_draw
import matplotlib

matplotlib.use("Agg")

# --- patch: KeyError 'highway' when streets gdf has no highway column ---
_orig_graph_to_shapely = _pm_draw.graph_to_shapely


def _safe_graph_to_shapely(gdf, width=1.0):
    if gdf is None or getattr(gdf, "empty", True):
        from shapely.geometry import GeometryCollection

        return GeometryCollection()
    if isinstance(width, dict) and "highway" not in gdf.columns:
        width = width.get("default", next(iter(width.values()), 1.0))
    return _orig_graph_to_shapely(gdf, width)


_pm_draw.graph_to_shapely = _safe_graph_to_shapely
# -----------------------------------------------------------------------

# --- defaults (single place) ---
DEFAULT_QUERY = "Akademgorodok, Novosibirsk, Russia"
DEFAULT_LAT = None
DEFAULT_LON = None
DEFAULT_RADIUS_M = 2200
DEFAULT_CIRCLE = True
DEFAULT_TITLE = "Академгородок · Новосибирск"
DEFAULT_PRESET = "default"
DEFAULT_OUTPUT_NAME = "akademgorodok_poster"
DEFAULT_DPI = 300
DEFAULT_FIGSIZE = (14, 14)
# --------------------------------


def _env(name, default=None):
    val = os.environ.get(name)
    if val is None or val == "":
        return default
    return val


def _env_bool(name, default):
    val = os.environ.get(name)
    if val is None or val == "":
        return default
    return val.strip().lower() in ("1", "true", "yes", "y", "on")


def _env_float(name, default):
    val = os.environ.get(name)
    if val is None or val == "":
        return default
    return float(val)


def _env_int(name, default):
    val = os.environ.get(name)
    if val is None or val == "":
        return default
    return int(val)


def parse_args():
    p = argparse.ArgumentParser(description="Generate a prettymaps poster")
    p.add_argument(
        "--query",
        default=_env("QUERY", DEFAULT_QUERY),
        help="Location string for geocoding",
    )
    p.add_argument(
        "--lat",
        type=float,
        default=_env_float("LAT", DEFAULT_LAT),
        help="Latitude (use with --lon instead of --query)",
    )
    p.add_argument(
        "--lon",
        type=float,
        default=_env_float("LON", DEFAULT_LON),
        help="Longitude (use with --lat instead of --query)",
    )
    p.add_argument(
        "--radius",
        type=int,
        default=_env_int("RADIUS_M", DEFAULT_RADIUS_M),
        help="Radius in metres",
    )
    p.add_argument(
        "--circle",
        default=None,
        help="Circular clip: true/false (env CIRCLE)",
    )
    p.add_argument(
        "--title",
        default=_env("TITLE", DEFAULT_TITLE),
        help="Map title",
    )
    p.add_argument(
        "--preset",
        default=_env("PRESET", DEFAULT_PRESET),
        help="prettymaps preset name",
    )
    p.add_argument(
        "--output-name",
        default=_env("OUTPUT_NAME", DEFAULT_OUTPUT_NAME),
        help="Output basename without extension",
    )
    p.add_argument(
        "--dpi",
        type=int,
        default=_env_int("DPI", DEFAULT_DPI),
        help="Output DPI",
    )
    args = p.parse_args()

    if args.circle is None:
        args.circle = _env_bool("CIRCLE", DEFAULT_CIRCLE)
    else:
        args.circle = str(args.circle).strip().lower() in (
            "1",
            "true",
            "yes",
            "y",
            "on",
        )

    return args


def main():
    args = parse_args()

    output_dir = Path("/work") if Path("/work").is_dir() else Path(".")
    output_dir.mkdir(parents=True, exist_ok=True)
    out_png = output_dir / f"{args.output_name}.png"
    out_pdf = output_dir / f"{args.output_name}.pdf"

    if args.lat is not None and args.lon is not None:
        location = (args.lat, args.lon)
        location_desc = f"({args.lat}, {args.lon})"
    else:
        location = args.query
        location_desc = args.query

    print(f"Location: {location_desc}")
    print(f"Radius: {args.radius} m, circle={args.circle}, preset={args.preset}")
    print(f"Title: {args.title}")
    print("Downloading OSM data and rendering (this can take several minutes)...")

    plot = prettymaps.plot(
        location,
        radius=args.radius,
        circle=args.circle,
        figsize=DEFAULT_FIGSIZE,
        preset=args.preset,
    )

    if args.title:
        plot.ax.set_title(
            args.title,
            fontsize=28,
            pad=20,
            fontfamily="DejaVu Sans",
        )

    plot.fig.savefig(
        out_png,
        dpi=args.dpi,
        bbox_inches="tight",
        facecolor=plot.fig.get_facecolor(),
        edgecolor="none",
    )
    print(f"Saved: {out_png}")

    plot.fig.savefig(
        out_pdf,
        bbox_inches="tight",
        facecolor=plot.fig.get_facecolor(),
        edgecolor="none",
    )
    print(f"Saved: {out_pdf}")
    print("Done.")


if __name__ == "__main__":
    main()

Suno. «Нелюбовь».

И ещё одна мысль 2026 года: тягучая, неприятная, назойливая.

В восемнадцать выбираешь не город —
институт, общежитие, шанс.
А потом это слово на карте
становится жизнью на пятнадцать лет.

Пары, вписки, работа, любовь, алкоголь,
Первый офис, зарплата, друзья, расставания.
Всё моё — до последней случайной детали,
Но любви почему-то не вышло с годами.

Полтора миллиона — звучит как столица,
Только цифра ещё не рождает масштаб.
Можно меньше — и выглядеть городом мира,
Можно больше — и быть просто массой координат.

Здесь природа как будто осталась за кадром,
Инфраструктура живёт вопреки.
Есть места, где пространство становится городом,
Здесь порой — только люди, дома и гудки.

И я спорю с собой: «Но ведь лучшее — здесь».
Да, здесь юность, друзья, каждый первый успех.
Только память о людях и память о месте —
Это разные вещи. И в этом весь нерв.

Это не ненависть — это нелюбовь.
Слишком долго знакомы, чтоб злиться всерьёз.
Здесь осталась моя молодая кровь,
Но в тебя я корнями не врос.

Я читаю маршруты давно без карт:
Двор, остановка, подъезд, поворот.
Но пятнадцать лет — это ещё не «мы»,
Если каждый отъезд ощущается как взлёт.

Заельцовская — сумерки, первое «нет»,
И теперь этот воздух хранит интонацию.
КСМ — где прогулка легко меняет сюжет,
Краткий курс экстремальной городской навигации.

Горский — высотки съедают закат,
Петухова — дорога за край горизонта.
Автогенная, Вертко́вская — шаг назад,
Мимо стен, что давно не дождутся ремонта.

Оловозавод — в проводах небосвод,
Ржавый снег у ворот, гаражи под откосом.
Пятидесятые кончились — время идёт,
Только здесь это кажется вопросом.

Опыт медленно вычёркивает районы:
Не «не знаю» — я просто не хочу там быть.
И карта становится личным архивом:
Здесь не стой. Здесь сверни. Здесь быстрее пройти.

Ты ночью проедешь его наугад,
Узнаешь кварталы по свету витрин.
Но близость не входит в накопленный стаж —
Ты можешь всё знать и остаться чужим.

Это не ненависть — это нелюбовь.
Слишком много прожито, чтоб просто уйти.
Здесь осталась моя молодая кровь,
Но к тебе почему-то не тянет идти.

Я читаю маршруты давно без карт,
Каждый срез через двор, каждый поздний трамвай.
Но пятнадцать лет — это ещё не «мы».
Это долгое «здесь». Это так и не «мой край».

Академ был открыткой — почти ра́стровой:
Сосны, вывески, свет фонарей.
Я смотрел на него восемнадцатилетний,
Будто прошлое было умнее людей.

А потом оказалось: тяжёлый спальник
С очень громким вчера и невнятным сейчас.
Прошлое здесь цитируют громко и важно,
А настоящее молча проходит сквозь нас.

И, конечно, здесь были лучшие встречи,
Те, кого я и дальше буду беречь.
Но чем дольше живёшь, тем заметнее вещи,
От которых уже невозможно отвлечь.

И приходит вопрос, бесполезный до боли:
«А куда бы свернул, если б знал наперёд?»
Если б в списке тогда оказался другой город,
Если б поезд ушёл на другой поворот?

Другие друзья? Другие ошибки?
Другой человек по другую весну?
Но прошлого нет в сослагательном виде.
Я не сравню две жизни, поставив в одну.

Другой моей юности всё равно нет.
Нельзя пересобрать тот случайный маршрут.
Может, лучше бы было. А может быть — нет.
Но проверить нельзя. В этом весь абсолют.

Это не ненависть — это нелюбовь.
Без красивой развязки и правых сторон.
Здесь столько людей, без которых я был бы другим,
Здесь столько меня, что не вычеркнуть из этих времён.

Я знаю тебя слишком близко для мифа,
Слишком долго для фразы: «Не понял тебя».
Но любовь не приходит за выслугу лет.
Моя жизнь была здесь. Моя молодость — здесь.
А любви к тебе — нет.

Наверное, дом — это место, которое выбираешь,
уже понимая зачем.

И, может быть, место, откуда не ищешь всё время,
куда бы уехать.

Принудительный Full GC Java-процессов по cron

0 3 * * * for pid in $(pgrep -u spring -x java); do java_bin=$(readlink -f "/proc/$pid/exe") && jcmd="$(dirname "$java_bin")/jcmd" && [ -x "$jcmd" ] && "$jcmd" "$pid" GC.run; done >/dev/null 2>&1

Суть:

pgrep -u spring -x java находит все процессы с именем java, принадлежащие пользователю spring.

Для каждого PID через /proc/$pid/exe определяется реальный исполняемый файл Java.

Рядом с найденным java ищется соответствующий jcmd:

jcmd="$(dirname "$java_bin")/jcmd"

Если jcmd существует и исполняемый, выполняется:

jcmd PID GC.run

Команда инициирует Full GC внутри соответствующей JVM.

jcmd должен иметь возможность подключиться к JVM. Сron следует запускать от пользователя spring или от root. Если jcmd отсутствует рядом с используемым java, эта JVM будет молча пропущена.

Default Nano as crontab editor

Чтобы `crontab -e` открывался в `nano`, добавить в профиль пользователя:

printf '\nexport EDITOR=nano\nexport VISUAL=nano\n' >> ~/.profile && . ~/.profile

Suno. «Не по прописке».

Порефлексировал на свой опыт странного, чудного, местами отвратительного и неприятного 2026 года. Кто-то скажет, что галимый нейрослоп — пусть так.

Я здесь никто. То есть формально — турист.
Практически — уже хожу без карты и лишних вопросов.

Выхожу из подъезда — и город как будто
Подсовывает мне декорации прошлого,
Только прошлого этого не было будто,
Но мозг говорит: «Да ладно, всё знакомо же».

Вот панельный квартал, вот балконы, антенны,
Вот собака без повода лает на двор.
Я уверен почти, что бывал здесь когда-то,
Хоть биография против — и паспорт не врёт.

Я не рос здесь.
Не ездил отсюда до школы.
Не курил за гаражом.
Не сбивал здесь коленки.

Но почему этот двор вызывает приколы
Моей памяти, которой здесь нет по определению?

И язык — как сосед через тонкую стену:
Вроде смысл различаешь, но путаешь слова.
Я киваю кассиру уверенно, степенно,
А потом понимаю, что понял едва.

И самое странное — это не быть чужим.

Самое странное — слишком быстро перестать им быть.

Я опять называю «домой»
Комнату, снятую ровно на десять ночей.
Город, ты что-то не то
Делаешь с памятью взрослых людей.

Я тебя не любил,
Я тебя не терял,
Мне по правилам нечего здесь вспоминать.

Но если завтра уеду —
Почему мне уже
Будет чего не хватать?

Нови-Београд — это бетонный архив,
Где эпоха хранится без даты создания.
Каждый дом говорит: «Я функционально красив»,
И прекрасно обходится без оправдания.

А потом — стекляшки, реклама, фасады,
Будто город решил отыграть новый патч.
Там внизу у реки дорогие квадраты,
А здесь сохнет бельё и по телеку матч.

Пекарня.
Кофе.
Ещё пекарня.

Рай, если есть, пахнет свежим тестом.
Я запомнил дорогу быстрее, чем имя хозяина,
То есть начал уже обрастать контекстом.

Вот автобус.
Вот рынок.
Вот нужная дверь.
Вот кафе, где не спрашиваю меню.

Человек — удивительно странный зверь:
Дай неделю — он скажет пространству «люблю».

Хотя нет.
Это слово пока слишком громкое.

Лучше так:
«Мне удобно».
«Привык».
«Нормально».

Так обычно и начинается самое стрёмное:
То, что было временным, вдруг становится своим.

Я опять называю «домой»
Комнату, снятую ровно на десять ночей.
Город, ты что-то не то
Делаешь с памятью взрослых людей.

Я тебя не любил,
Я тебя не терял,
Мне по правилам нечего здесь вспоминать.

Но если завтра уеду —
Почему мне уже
Будет чего не хватать?

Вечером Сава становится чёрным экраном,
По которому город пускает огни.
Самолёт над рекой проплывает упрямо,
Напоминая: отъезд впереди.

И смешно:
я приехал смотреть на чужое,
Фотографировать стены, мосты и еду,
А теперь нахожу здесь знакомые черты,
Будто город чужой, а привычки — мои.

Может, родина — то, что назначили сверху.
Может, дом — это то, что находишь сам.
Это точка, где ты перестал быть экскурсией,
Где чужое пространство становится «нам».

Где ты больше не ищешь маршрут в телефоне.
Где привычно ворчишь: «Ну опять этот дождь».
Где случайно услышишь знакомое слово
И внезапно поймёшь, что уже не уйдёшь —

Не физически.
Нет.
Самолёт по расписанию.
Чемодан.
Регистрация.
Выход.
Контроль.

Просто город останется где-то в сознании
Незаконной пропиской с пометкой «чужой».

Мы называем своим то, что выдали при рождении.

Улицу.
Страну.
Язык.

Как будто сам выбирал точку входа и стартовый набор.

Хотя ты просто появился — а вокруг уже всё было подписано:
«Вот это твоё».

А потом приезжаешь туда,
где никогда раньше не был,

и через несколько дней ловишь себя на фразе:

«Сегодня поздно. Пойдём домой».

И вот тут становится непонятно:

если первое назначили,
а второе ты выбрал сам —

что из этого на самом деле твоё?

Я опять называю «домой»
Комнату, ключ от которой оставлю с утра.
Город, мы оба поймём:
Эта игра оказалась немного не игра.

Я тебя не любил,
Я тебя не терял,
Но теперь почему-то приходится лгать:

«Я всего лишь турист.
Мне пора уезжать.
Мне здесь нечего вспоминать».

Мне здесь нечего вспоминать.

Так почему я уже вспоминаю?

Laku noć, Beograde.

Suno: личный опыт

Неожиданно для себя открыл коммерческую генеративную модель для музыки — Suno.

Если к доступным генераторам картинок и видео у меня по-прежнему есть вопросы — получить нестилизованное изображение, которое не вызывает у восприятия ощущения «здесь что-то не так», всё ещё довольно нетривиальная задача; главная страница Civitai тому регулярно служит наглядным подтверждением, — то с музыкой всё оказалось несколько интереснее.

Музыка в этом смысле куда более схематична и формализуема: Shazam с алгоритмическим анализом музыки в привычном нам виде существует уже добрых полтора десятка лет, а инструменты вроде автотюна, способные вытащить даже весьма безнадёжный вокал, появились тоже далеко не вчера. Поэтому сама идея машины, которая не только разбирает музыку на составляющие, но и собирает их обратно, уже не кажется чем-то совершенно инопланетным.

Но реальность, как обычно, прозаичнее рекламных роликов.

Кнопки «сделать шедевр» там нет.

Есть набор базовых голосов. Есть генератор лирики, который никакой особой магии не делает. Есть возможность описать стиль, инструменты, характер вокала, темп, динамику и структуру композиции. А дальше внезапно выясняется, что всем этим ещё нужно уметь пользоваться.

Нужно возиться с текстовкой: переписывать строки, считать слоги, следить за ритмом и за тем, как слова вообще ложатся на музыку. Нужно расставлять ударения там, где модель упорно слышит их по-своему. Нужно понимать, какие музыкальные теги дадут нужный результат, а какие превратят описание в кашу из жанров. Нужно отдельно думать про интро, куплеты, припевы, бриджи, смену динамики и то, где вокалу стоит разойтись, а где — наоборот, исчезнуть почти до шёпота.

И потом генерировать снова.

И ещё раз.

И брать удачную версию, менять две строчки и обнаруживать, что теперь модель зачем-то переписала половину аранжировки.

То есть никакой пресловутой кнопки «творчество» всё ещё не появилось. Просто возник ещё один инструмент, который очень быстро делает черновую работу и иногда неожиданно хорошо интерпретирует поставленную задачу.

Но чем конкретнее ты понимаешь, что именно хочешь услышать, тем заметнее разница между «я написал запрос нейросети» и «я действительно сделал трек».

В общем, магии не случилось.

Случился ещё один довольно мощный инструмент, который позволяет потратить три часа на то, что, по рекламе, должно было занять три минуты.

Spine Trailer

Симпатичный трейлер экшена второго эшелона, который невольно напомнил о Syndicate 2012-ого года и Sin Episodes года 2006 (где-то на этой цифре мой мозг чутка завис — как давно, оказывается, это было-то ). Фигуристая героиня, бодрая боёвка, приятная графика (спасибо UE5)  — повод последить за релизом.

Witcher 3 — Songs of the Past trailer

Не то чтобы я не припоминал случаев, когда новый контент выходил для совсем уж старых игр — успешно разрабатываемые и продающиеся DLC для Age of Empires II тому живое свидетельство. Но сделать сюжетное дополнение для игры, чьё дополнение — «Кровь и вино» — стало эталоном того, как нужно расширять игровой мир, — это как минимум смело.

Ждём, морально готовимся выделить пару месяцев на повторное прохождение.

Ранее Ctrl + ↓