Imported from SweetyHake/Tokenatra (
AGENTS.md). Install upstream withnpx skills add SweetyHake/Tokenatra. Copyright stays with the author.
Tokenatra — AGENTS.md
Commands
start.bat # auto-installs deps + launches desktop app (pywebview, no console)
launch.vbs # launches start.bat completely hidden (no window at all)
python app.py # launches with console (for debugging)
python server.py # standalone Flask on :7878 (no GUI window)
python server.py --remove-bg <file> # CLI: remove background, saves <stem>_nobg.webp
python server.py --to-webp <file> # CLI: convert to WebP, deletes original
Build
build_installer.bat # Step 1: PyInstaller -> Step 2: Inno Setup installer
build.bat # PyInstaller only (portable folder)
PyInstaller output: dist/Tokenatra/Tokenatra.exe
Installer output: dist/installer/Tokenatra_Setup_v*.exe
GitHub Releases: ассеты по платформам
Релиз содержит: Tokenatra_v<ver>_windows_x86_64_setup.exe (Windows), Tokenatra_v<ver>_macos_arm64.dmg / _x86_64.dmg (macOS), Tokenatra_v<ver>_linux_x86_64.AppImage + .tar.gz (Linux). Все собирает CI (build-release.yml); имена ассетов значимы — updater ищет файл по суффиксу платформы и архитектуры (setup, _macos_arm64., _linux_x86_64.).
Автообновление работает на всех трёх платформах:
- Windows: качает Setup-установщик (приоритет в
_find_exe_asset) — тихая установка через bat. - macOS: качает DMG своей архитектуры;
/apply_updateпишет_update.sh, который после закрытия приложения монтирует образ, подменяет .app (с бэкапом на время подмены) и перезапускает. Запуск из /Volumes или вне .app → отказ с подсказкой. - Linux: качает AppImage; атомарный
os.replaceповерх работающего файла + перезапуск. Работает только из AppImage ($APPIMAGE).
Голый dist/Tokenatra/Tokenatra.exe — тонкий загрузчик без _internal; отдельно не публиковать.
Публикация релиза (пошагово)
Версии по схеме год.мажор.фикс (2026 → 26.x.y), тег v26.x.y.
CI: пуш тега
v*запускает.github/workflows/build-release.yml— он сам собирает все платформы и прикладывает ассеты к релизу:Tokenatra_v*.exe(Windows),Tokenatra_v*_macos_*.dmg,Tokenatra_v*_linux_*.AppImage/.tar.gz. Ручные шаги 4-5 ниже нужны только если CI недоступен.
- Бамп версии:
version.py→__version__ = "26.x.y"(имя установщика =Tokenatra_Setup_v26.x.y.exe; CI передаёт версию в Inno через/DMyAppVersion=…, в installer.iss остаётся только fallback)
- Коммит и пуш в
main:git add -A git commit -m "v26.x.y: <что исправлено>" git push - Тег (только после пуша кода, чтобы тег указывал на релизный коммит):
git tag v26.x.y git push --tags - Сборка установщика:
build_installer.bat(PyInstaller → ISCC), либо вручнуюpython -m PyInstaller build.spec+iscc installer.iss. Результат:dist/installer/Tokenatra_Setup_v26.x.y.exe. - Создать GitHub release и залить ассет (токен из Windows Credential Manager через
git credential fill):$token = ("protocol=https`nhost=github.com`n" | git credential fill 2>$null | Select-String '^password=' | ForEach-Object { ($_ -split '=', 2)[1] }) # release body — JSON, напр. { "tag_name": "v26.x.y", "name": "Tokenatra 26.x.y", "body": "## Исправления...", "draft": false, "prerelease": false } $resp = curl.exe -s -X POST -H "Authorization: Bearer $token" -H "Accept: application/vnd.github+json" -H "Content-Type: application/json" --data-binary "@release_body.json" "https://api.github.com/repos/SweetyHake/Tokenatra/releases" $id = $resp | python -c "import json,sys; print(json.load(sys.stdin).get('id'))" curl.exe -s -X POST -H "Authorization: Bearer $token" -H "Accept: application/vnd.github+json" -H "Content-Type: application/octet-stream" --data-binary "@Z:\Tokenatra\dist\installer\Tokenatra_Setup_v26.x.y.exe" "https://uploads.github.com/repos/SweetyHake/Tokenatra/releases/$id/assets?name=Tokenatra_Setup_v26.x.y.exe" - Проверка:
https://api.github.com/repos/SweetyHake/Tokenatra/releases/latest→ тег и имя ассета. В приложении появится баннер «доступна новая версия» (ссылкой на GitHub).
Model files (*.onnx) are NOT bundled — user places them into the models/ folder next to the exe and picks one in Settings (только .onnx).
Inno Setup
To build the installer, install Inno Setup from https://jrsoftware.org/isdl.php
and run build_installer.bat. Or manually:
python -m PyInstaller build.spec
iscc installer.iss
Bundled assets (included in .exe build)
templates/— Jinja2 HTML templatesstatic/— JS, CSS, workerstoken_rings/— ring overlay imagespresets/— preset mask imagesversion.py— version & repo config
Dependencies
Единый requirements.txt — источник правды для start.bat, start.sh и CI.
Маркеры окружения ставят onnxruntime-directml только в Windows; в macOS/Linux — обычный onnxruntime.
Version & updates
version.py:GITHUB_REPO— set to"SweetyHake/Tokenatra"before building- On startup,
updater.pychecks GitHub Releases for newer version - If newer version found, splash screen shows download button
model.onnxis NOT auto-downloaded — the user places it intomodels/and selects it in Settings (see "External assets")
Architecture
- Desktop shell: pywebview (edgechromium) →
app.py:138-148 - Web server: Flask on
127.0.0.1:7878→server.py - AI inference: ONNX Runtime with a model from
models/folder (BiRefNet или RMBG-2.0/IS-Net — обе поддерживаются), выбор модели в настройках (/models_list,/select_model).get_providers()inserver.pydetects physical GPUs via WMI (virtual/Parsec/RDP adapters are filtered), then picks CUDA (NVIDIA) → ROCm → DirectML → CPU. Runtime fallback to CPU if the GPU provider fails to load. - Frontend: Vanilla JS, global
stateobject mutated directly by all modules - Canvas: internal size =
CONFIG.SCALE_SIZES[state.canvasScale]({1:1536, 2:3072, 3:6144}, default 2 → 3072×3072 px; варианты: 1536 «производительность», 3072 «баланс», 6144 «качество»), логические координаты в 1024 px пространстве (scale factor = 2×canvasScale). Масштаб меняется в Settings → Производительность → «Рабочий канвас» (TokenCanvas.setCanvasScale(), сохраняется вconfig.jsonкакcanvasScale). Экспорт НЕ зависит от рабочего масштаба:renderForSave()рендерит вsaveSettings.quality × 3(независимыйcoordScale). Перф-инвариант:getImageData/putImageDataпо всему канвасу запрещены в горячих путях (тень —source-in, цветокоррекция — кэш_getColorCorrectedImage(), ластик — воркер). - Ластик (воркер, протокол v2): воркер хранит альфа-зеркала масок (1 байт/px,
setMask) и защиту (setProtection,state.protectionAlpha). Пачка несёт только штрихи (Float64Array [cx,cy,drawX,drawY,flags]) — ноль чтений маски на главном потоке; воркер сам считает регион и возвращает RGBA-патч (RGB=255). Пайплайн глубиной 2 (_workerInFlight). Инвариант: любое изменение маски в обход ластика (undo/redo, resetMask/resetImageMask, пресетTokenPresets.apply, заливка при удалении фона,createMask/createImageMask) обязано вызватьTokenCanvas._pushMaskToWorker(pink)— иначе воркер применит штрихи к устаревшему зеркалу. Точки в очереди не дропаются никогда (схлопывание близких). Ввод — Pointer Events +getCoalescedEvents(touch-action:none на.canvas-area). История — только альфа-снапшоты ({a,w,h}, ~1 МБ на маску).
Critical file: server.py gotchas
- Duplicate route functions: проверено — дубликатов нет, каждый
@app.routeобъявлен ровно один раз. - Блок
if __name__ == '__main__'обязан быть в КОНЦЕserver.py: при запускеpython server.pyapp.run()блокирует модуль, и маршруты ниже не регистрируются (получали 404 на/config,/save_file,/shutdownв dev-режиме). - Every
@app.routemust be declared exactly once with the decorator. - Models live in
BASE_DIR/models/(get_selected_model_path(): configselected_model→ первый*.onnx). При первом запускеmodel.onnxизBASE_DIRавтоматически перемещается вmodels/.
JS load order (strict, from index.html lines 713-725)
config.js → state.js → utils.js → i18n.js → urlManager.js → tokenEffects.js →
tokenHistory.js → tokenPresets.js → tokenCanvas.js → tokenEditor.js →
portraitGenerator.js → hotkeySettings.js → remover.js → main.js
Breaking this order causes runtime errors because objects reference each other.
Frontend conventions
- ObjectURLs must use
urlManager.create()/urlManager.revoke()— neverURL.createObjectURL()directly $(id)=document.getElementById(id)fromutils.jsdebounce(fn, ms)fromutils.jsfor expensive history saves- No frameworks, no inline CSS (except dynamic
.style.display), CSS custom properties in:root - All ObjectURLs in
state.userImage*freed viaurlManager.revokeAll()on new image load
Server routes (non-obvious)
Face detection cascade
Haar → heuristic
If OpenCV DNN model files (opencv_face_detector_uint8.pb + .pbtxt) exist in OpenCV's data dir, DNN is tried before Haar.
External models (.gitignored):
model.onnx— BiRefNet или RMBG-2.0/IS-Net (background removal)
model.onnx требования
- Вход: 1024×1024×3 RGB, ImageNet-нормализация (mean 0.485/0.456/0.406, std 0.229/0.224/0.225) — всё это делает
server.pyсам - Выход: одноканальная маска 1024×1024. Если выход уже в [0,1] (RMBG-2.0
alphasсодержит sigmoid в графе) — sigmoid повторно НЕ применяется (server.pydetect:mask.min() >= 0 and mask.max() <= 1); для сырых logits (BiRefNet) — применяется - Вход/выход могут быть объявлены как
dynamic(напр.[1,3,'height','width']), но фактически модель может требовать ровно 1024×1024 (фикс. split в декодере) - Источник: HuggingFace, напр.
briaai/RMBG-2.0(onnx/model_fp16.onnx, ~514 МБ) илиDanielLavric/BiRefNet-ONNX server.pyгрузит сORT_ENABLE_EXTENDED, при падении (InsertedPrecisionFreeCast— известный баг ORT на LayerNorm-fusion) откатывается наORT_ENABLE_BASIC
| Route | Note |
|---|---|
/save_file POST |
Opens native Windows file dialog via tkinter. Expects file + filename in form. |
/pick_folder GET |
Native folder picker via tkinter. |
/save_to_folder POST |
Write file to a given folder path. |
/config GET/POST |
Reads/writes config.json in BASE_DIR (.gitignored). |
/presets_list |
Встроенные пресеты из RESOURCE_DIR/presets (builtin: true) + пользовательские из USER_DATA_DIR/presets (новые сверху, builtin: false). |
/preset_file/<filename> |
Сначала ищет в пользовательском каталоге пресетов, затем во встроенном. |
/add_preset POST |
Сохраняет текущую маску как PNG-пресет в USER_DATA_DIR/presets (multipart image), имя авто «Пресет N». |
/delete_preset POST |
Удаляет ТОЛЬКО пользовательский пресет (встроенные в ресурсах недоступны для записи). |
/process POST |
Background removal. Accepts image (file), format (webp/png/jpg), quality (int 10-100), edge_blur (float 0-10). Изображения с альфой предварительно композитятся на белый фон (иначе тёмная кайма по краям). |
New features (v2 integration)
Hotkeys
Stored in config.json, managed by AppConfig (JS) with a rebindable UI in hotkeySettings.js. Defaults in config.js:DEFAULT_HOTKEYS. Hardcoded secondary bindings in config.js:MOVE_KEYS, ROTATE_KEYS, ERASER_SIZE_KEYS.
New modules (beyond old CLAUDE.md)
portraitGenerator.js— separate canvas for portrait-oriented token creationhotkeySettings.js— UI for rebinding keyboard shortcutseraserWorker.js— Web Worker for performant eraser brush application on mask canvascontext_menu_helper.py— Windows context menu handler (registered/unregistered byapp.py)
Common pitfalls
| Symptom | Likely cause |
|---|---|
| /process returns 500 | Модель missing в models/ (баннер + выбор в настройках) |
| Rings don't load | Syntax error in tokenPresets.js prevents parsing |
| Image doesn't render | state.userImage or state.maskCanvas is null |
| ObjectURL leak | urlManager bypassed |
| Route 404 | Duplicate def without @app.route shadowing the real one |
| Flask won't start | Port 7878 already in use |
External assets
model.onnx— BiRefNet или RMBG-2.0/IS-Net, кладётся вручную вmodels/(не в репозитории, в.gitignore)mask.png— защита областей для ластика (поставляется с приложением). Включается тумблером «Защита» в редакторе (state.protectionEnabled, configprotectionEnabled), по умолчанию ВКЛЮЧЕНА — при включённой тёмные области маски защищены от стирания (и ластик, и розовая маска пропускают их). Маска — полноразмерный дизайн всего канваса (у пользователяmask.png= 6144×6144):buildProtectionCanvasFromImg()растягивает её на весь рабочий канвас (случай 1:1 приcanvasScale3). ВАЖНО: при выключенной защите маска не грузится вовсе — иначе тёмныйmask.pngблокирует стирание (баг «ластик не работает»).token_rings/— folder with ring PNG/WebP files (optional)presets/— folder with preset mask images (optional)- Images loaded from clipboard/files are never stored on disk