Превратите обычный текст в рисунок из символов. Например, Hello становится большой надписью, которую можно вставить в README, сообщение или текстовый файл. Здесь нет картинок: каждая линия состоит из букв, пробелов и знаков.
Запуск готовой версии:
go run ./cmd/webиз корня репозитория. Затем откройте http://127.0.0.1:8080. Без настройки AI всё работает в демо-режиме, без ключа и внешней сети.Исходная реализация команды в
cmd/server, исходные шаблоны и исходные алгоритмы сохранены без изменений. Готовая версия подключается отдельной точкой входаcmd/webи дополняет те же пакеты. Командаgo run cmd/server/main.goзапускает первоначальный сервер, в котором AI-обработчики ещё пустые. Для проверки доработанной версии используйте команду выше.
- Генерирует ASCII-арт в трёх стилях: Standard, Shadow, Thinkertoy.
- Поддерживает многострочный ввод, CRLF и запись
\n; ограничение — 1000 символов. - Принимает печатные ASCII-символы: латиницу, цифры, пробелы и знаки. Кириллица, emoji, табуляция и управляющие символы не входят в комплект шрифтов и получают понятную ошибку.
- Предлагает 3–5 продолжений текста после паузы в наборе.
- Подбирает стиль по объяснимым правилам без обращения к AI.
- Предлагает 3–5 творческих вариантов с готовым стилем.
- Позволяет копировать результат, скачивать
.txtи повторять последние восемь надписей. - Работает на узком экране, с клавиатуры и без JavaScript: без JS остаются обычная HTML-форма и генерация.
Технологии: Go и его стандартная библиотека (net/http, html/template, encoding/json), обычные HTML, CSS и JavaScript. Нет frontend-сборки, npm-зависимостей, CDN, базы данных или обязательного облачного сервиса.
| Участник | Вклад |
|---|---|
| asamat | Исходная серверная структура, форма, стили, генерация ASCII и заготовка AIClient; вклад виден в истории Git. |
| abaitas | Участник исходной команды по информации владельца проекта. В полученной истории нет отдельного автора с этим логином, поэтому конкретные коммиты ему не приписаны. |
| ysoltamb | Интеграция надстроек: безопасная генерация, HTTP-обработчики, AI-сервис, интерфейс Studio, проверки и документация. |
История и авторство прежних коммитов сохранены. Все 22 прежних файла, кроме изначально пустого README.md, сохранены побайтно относительно 76b2ff96c1ded7eaa04df02fec4c457a0b09d0f5. Проверка: python3 scripts/verify_preserved.py.
-
Установите Go 1.27.1 или новее — эта версия указана в исходном
go.mod. Go нужен, чтобы собрать и запустить сервер. -
Скачайте репозиторий и откройте терминал в его папке:
git clone https://01.tomorrow-school.ai/git/asamat/symbol-web.git cd symbol-web -
Запустите:
go run ./cmd/web
-
Откройте в браузере http://127.0.0.1:8080.
-
Введите
Hello, world!, выберите стиль и нажмите «Создать ASCII-арт». -
Чтобы остановить сервер, нажмите
Ctrl+Cв терминале.
Если в окружении уже задан адрес LLM, для гарантированного демо-запуска:
LLM_BASE_URL= LLM_MODEL= go run ./cmd/webНа Windows PowerShell:
$env:LLM_BASE_URL = ""
$env:LLM_MODEL = ""
go run ./cmd/web| Переменная | Если не задана | Назначение |
|---|---|---|
LLM_BASE_URL |
Пусто → mock | Адрес OpenAI-совместимого сервиса. Для Ollama: http://127.0.0.1:11434. |
LLM_MODEL |
Пусто | Имя установленной модели. Обязательно при непустом адресе. |
LLM_API_KEY |
Пусто | Необязательный ключ для сервиса с Bearer-авторизацией. Ollama локально обычно не требует ключа. |
ADDR |
127.0.0.1:8080 |
Адрес и порт самого приложения. |
APP_ROOT |
. |
Папка с templates, static и тремя файлами баннеров. |
Исходный .env.example сохранён. Добавленный .env.studio.example показывает все параметры и безопасный демо-запуск. Go не читает .env автоматически. В POSIX-оболочке можно загрузить переменные явно:
cp .env.studio.example .env
set -a
. ./.env
set +a
go run ./cmd/webДля реального LLM установите и запустите выбранный совместимый сервер, подготовьте в нём модель и задайте её реальное имя:
export LLM_BASE_URL="http://127.0.0.1:11434"
export LLM_MODEL="имя-установленной-модели"
go run ./cmd/webПри адресе без пути приложение использует /v1/chat/completions. При адресе с путём, например https://ваш-сервис/v1, добавляется /chat/completions. Передавайте базовый URL, а не полный адрес метода. URL с логином/паролем, query-параметрами или fragment отклоняется. Модель и адрес не зашиты в код.
В live-режиме текст подсказки передаётся выбранному сервису; интерфейс сообщает об этом. Ключ передаётся только сервером и не попадает в HTML, JavaScript, историю браузера или сообщения об ошибках. При недоступной LLM генератор и локальный подбор стиля продолжают работать. Неудачный live-запрос возвращает ошибку, а не маскируется демо-ответом.
go build -o bin/symbol-web ./cmd/web
./bin/symbol-web
ADDR=127.0.0.1:8090 go run ./cmd/webЗапускайте из корня проекта либо задавайте абсолютный APP_ROOT. Программа читает ресурсы из файлов: бинарник нужно распространять вместе с ними. Для доступа из контейнера или другой машины можно явно задать ADDR=:8080. Для публичного размещения нужны отдельно настроенные TLS, ограничения трафика и инфраструктура; готовый сервер по умолчанию доступен только локально.
cmd/
server/main.go исходная точка входа, сохранена
web/main.go точка входа готовой надстройки
internal/
ascii/
generator.go исходный код, сохранён
safe_renderer.go проверяемое чтение и локальная сборка строк
safe_renderer_test.go примеры аудита, границы, параллельность, фаззинг
testdata/ эталоны из выданного аудита
ai/
client.go исходный AIClient, используется надстройкой
rules.go детерминированный подбор стиля
service.go mock/live, таймаут, проверка ответа, кэш
service_test.go реальный тестовый HTTP-backend и проверки отказов
handlers/
ascii.go исходная реализация, сохранена
web.go HTTP-надстройка и связывание пакетов
web_test.go маршруты, ошибки, XSS, конкуренция и фаззинг
... исходные файлы остаются на местах
templates/index.html интерфейс Studio и результат генерации
templates/studio-error.html страница ошибки
static/css/studio.css адаптивное оформление
static/js/studio.js асинхронные действия и локальная история
standard.txt / shadow.txt / thinkertoy.txt
.env.example / .env.studio.example
Makefile
scripts/verify_preserved.py
AUDIT_ANSWERS.md
AUDIT_REPORT.md
Надстройка сохраняет цепочку cmd → handlers → ascii/ai. Она не вызывает небезопасные для HTTP-запросов старые Generate/Banner: у них общая переменная и завершение процесса при ошибке. Вместо этого расширяет те же пакеты отдельными функциями и использует общую исходную формулу initPositions. Нет перезаписи чужого кода, monkey patching или скрытого запуска через init().
Файл баннера содержит 95 символов ASCII, от кода 32 до 126. На каждый приходится пустой разделитель и восемь строк рисунка. Позиция первой строки вычисляется исходной функцией initPositions. После проверки файла генератор идёт по строкам текста, затем по восьми строкам высоты и склеивает соответствующие части букв через локальный strings.Builder.
Ввод проверяется до обращения к файлам. Выбор баннера ограничен тремя именами; путь пользователя не открывается. Файл ограничен 128 KiB, строка рисунка — 64 байтами; проверяются размер таблицы, разделители и печатные символы. Каждый запрос имеет собственный буфер. Пустые строки сохраняются; ввод только из переводов строк возвращает такое же число переводов. Пробелы не обрезаются. Ввод \n намеренно интерпретируется как перенос, как в исходном проекте.
| Метод и путь | Запрос | Успешный ответ |
|---|---|---|
GET / |
— | HTML-страница с формой |
HEAD / |
— | Заголовки страницы без тела |
POST /symbol-art |
URL-encoded поля inputWord, banners |
HTML с результатом; при Accept: application/json — {art,text,banner} |
POST /api/suggest |
JSON {text}; минимум 3 символа после обрезки внешних пробелов |
{suggestions: [строки]} |
POST /api/recommend-banner |
JSON {text} |
{recommended, reasoning, alternatives} |
POST /api/variations |
JSON {text} |
{variations: [{text, description, suggested_banner}]} |
GET /static/css/studio.css |
— | CSS |
GET /static/js/studio.js |
— | JavaScript |
Форма принимает как standard, так и исходное standard.txt (аналогично остальным стилям). API принимает ровно один JSON-объект с полем text. Неизвестные поля и лишние данные отклоняются. Максимальное тело запроса — 16 KiB. AI-ответы всегда JSON, включая ошибки. X-AI-Mode: mock или live показывает режим LLM-методов.
curl -i http://127.0.0.1:8080/
curl -i http://127.0.0.1:8080/symbol-art \
--data-urlencode 'inputWord=Hello' --data-urlencode 'banners=standard'
curl -s http://127.0.0.1:8080/symbol-art \
-H 'Accept: application/json' \
--data-urlencode 'inputWord=Hello' --data-urlencode 'banners=shadow'
curl -s http://127.0.0.1:8080/api/suggest \
-H 'Content-Type: application/json' -d '{"text":"Happy Birth"}'
curl -s http://127.0.0.1:8080/api/recommend-banner \
-H 'Content-Type: application/json' -d '{"text":"WELCOME"}'
curl -s http://127.0.0.1:8080/api/variations \
-H 'Content-Type: application/json' -d '{"text":"Hello"}'| Код | Пример |
|---|---|
200 |
Успешная страница, генерация или AI-ответ |
400 |
Пустой/слишком длинный текст, неподдерживаемый символ, неверный баннер, повреждённый JSON |
404 |
Неизвестный маршрут, отсутствующий шаблон или файл баннера |
405 |
Неподходящий HTTP-метод; ответ содержит Allow |
415 |
Неверный Content-Type |
500 |
Повреждённый ресурс, ошибка шаблона, неправильная конфигурация/ответ LLM |
503 |
Сеть, таймаут, upstream 429/5xx или заняты все восемь LLM-слотов |
API-ошибка имеет вид {"error":"понятное сообщение","status":400}. Внутренние пути и ответы провайдера не отправляются в браузер. Шаблон сначала полностью выполняется в буфер, поэтому ошибка не оставляет страницу с ошибочным статусом 200. Пропавший шаблон ошибки имеет резервный текстовый ответ. Защитные заголовки запрещают внешние скрипты, фреймы и выполнение HTML из пользовательского текста. Содержимое выводится через html/template и textContent.
У сервера есть ограничения времени чтения заголовков/тела, записи и простоя. Ctrl+C/SIGTERM отменяет контекст исходящих вызовов и завершает сервер с ограниченным ожиданием. Программная ошибка обработчика перехватывается; это последний резерв, а не замена обычной обработки ошибок.
Один скрипт без зависимостей использует fetch, AbortController, DOM API и обычные формы. Подсказки ждут 300 ms после ввода, не запрашиваются для текста короче трёх символов и отменяются при новом вводе. Устаревшие ответы не заменяют актуальный ввод. Кнопки показывают состояние ожидания и восстанавливаются после ошибки. ASCII сохраняет пробелы и прокручивается внутри результата, не растягивая страницу.
История хранит только текст и стиль, максимум восемь записей в localStorage. При запрете или переполнении хранилища работает память текущей страницы. Она не сохраняется на сервере. Очистка затрагивает только историю этого приложения. Если браузер запрещает буфер обмена, можно скачать файл или выделить результат вручную.
Mock: при пустом или пробельном LLM_BASE_URL возвращаются предсказуемые примеры. Для Hello: Hello!, Hello World, ~ Hello ~. Они не выдаются за ответы настоящей модели. Длинный ввод для демо-подсказки сокращается до 38 символов, чтобы готовые варианты были короче 50.
Live: используется исходный AIClient с таймаутом 10 секунд. Запрос наследует контекст входящего HTTP-запроса и дополнительный срок 10 секунд. Приложение отправляет непотоковый OpenAI-совместимый запрос. Модель получает инструкцию вернуть JSON; после ответа проверяются конверт, количество, длина, ASCII, уникальность и допустимость стилей. Размер ответа ограничен 64 KiB. Редиректы не выполняются. Ошибочные ответы не кэшируются.
Подбор стиля: сначала обрезаются внешние пробелы. Если есть буквы и все они заглавные или длина не превышает 6 — shadow; иначе при длине больше 12 — standard; иначе — thinkertoy. Порядок правил существенен: длинная надпись заглавными тоже получает shadow. Цифры не считаются заглавными буквами. Вместе с рекомендацией возвращаются две альтернативы и пояснения. Значения score — фиксированные относительные оценки, не вероятности модели.
Кэш: успешные подсказки хранятся 5 минут, максимум 256 ключей. Доступ защищён mutex; массивы копируются при выдаче. По истечении срока вызывается backend заново. Варианты не кэшируются. Одновременно выполняется не более восьми внешних вызовов на процесс; лишние получают 503, без неограниченной очереди. Автоматического retry нет: пользователь может повторить запрос, а остальные функции продолжают работать.
Для основных Go-проверок нужен Go. Для проверки синтаксиса JS — Node.js, для проверки сохранности файлов — Python 3. В рабочем приложении Node/Python не нужны. -race требует поддерживаемую платформу и системный C-компилятор.
go test -race -timeout 90s ./...
go vet ./...
node --check static/js/studio.js
python3 scripts/verify_preserved.pyИли make audit — vet, синтаксис JS, формат новых Go-файлов, тесты с race detector и сборка.
Дополнительно:
make fuzz
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.outТесты не требуют внешней LLM, ключа или интернета: live-протокол проверяется локальными httptest-серверами, включая настоящий десятисекундный таймаут. Требуется разрешение окружения открывать loopback-порты.
Подробный протокол: AUDIT_REPORT.md. Объяснения, вопросы и демонстрационные команды: AUDIT_ANSWERS.md. Покрытие старого кода и main() не следует путать с покрытием новой HTTP-цепочки: они отражаются в общей цифре отдельно.
- Порт занят: остановите прежний процесс или задайте другой
ADDR. - 404 для ресурса: запускайте из корня либо укажите правильный
APP_ROOT; проверьте наличие баннеров и новых шаблонов. - 503 для подсказок: проверьте доступность LLM и нагрузку; генерация продолжает работать. Для демо очистите
LLM_BASE_URLи перезапустите сервер. - 500 для LLM: проверьте модель, базовый URL и формат ответа совместимого сервера; подробность находится в серверном журнале.
- Русский текст не рисуется: комплект баннеров поддерживает только ASCII. Русский язык интерфейса не расширяет алфавит шрифтов.
- Результат «съехал» после вставки: используйте моноширинный шрифт или Markdown-блок кода; не удаляйте пробелы.
- Старый интерфейс или пустые AI-ответы: убедитесь, что запущен
cmd/web, а не сохранённыйcmd/server.
Проверки подтверждают перечисленные сценарии. Они не заменяют испытания конкретной LLM, хостинга или всех существующих браузеров и не означают абсолютной безотказности.