Skip to content

Local setup: product

A developer who has just cloned ai-sticker-pack and wants the bot, the kiosk API and the tests running on a Windows machine. No Docker is used anywhere in this project.

  • Two processes, one SQLite file: python bot\main.py (bot + queue worker + delivery sweeper + cleanup) and uvicorn api:app on port 8000 (kiosk API, kiosk page, admin panel, leads endpoint).
  • All configuration lives in bot\.env. Set APP_ENV=dev locally; the default is prod and prod refuses to start without keys.
  • The kiosk opens at http://localhost:8000/kiosk/. The camera works only on HTTPS or on localhost.
  • Only one main.py may run per data directory. A second one exits with STARTUP отменён.
  • python -m pytest needs no keys and makes no network calls. Tools that call a real AI model are listed below with their built-in spending caps. Never run them without an agreed budget.
  • Restart both processes after you change code or bot\.env.
Requirement Version Notes
Windows + PowerShell Windows 10 or 11 All commands on this page are PowerShell, run from the repository root.
Python 3.14 locally, 3.12 on the server The existing local virtual environments use Python 3.14.3. The server runs Ubuntu 24.04 with Python 3.12. bot/requirements.txt is pinned for Linux + Python 3.12 (binary wheels only); its header records that the full test suite passed on 3.12.13 and on 3.14 locally.
Git any recent The repository sets * text=auto eol=lf in .gitattributes; do not override it, deploy scripts break on CRLF.
Node.js optional Only for the kiosk JavaScript test. Without node on PATH that one test is skipped.
Edge or Chrome optional Only for tools/kiosk_check.py and tools/kiosk_screens.py (headless browser).
A Telegram bot required to run main.py Create a separate test bot with @BotFather. Never reuse the production token: two processes polling one token conflict.

You do not need AI keys to run the API, the kiosk page or the tests.

Terminal window
git clone https://github.com/<owner>/ai-sticker-pack.git
git -C ai-sticker-pack config user.name "<your-name>"
git -C ai-sticker-pack config user.email "<your-email>"
cd ai-sticker-pack
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r bot\requirements-dev.txt # runtime pins + pytest + httpx
Copy-Item bot\.env.example bot\.env

bot\requirements-dev.txt includes bot\requirements.txt and adds pytest and httpx.

The file is read by bot/config.py at import time, from the bot\ folder, regardless of the current directory. A variable set in the operating system environment wins over the file.

.env.example ships with production values. Change at least these for local work:

Variable Local value Why
APP_ENV dev Turns missing-key errors into warnings (see below).
TELEGRAM_BOT_TOKEN token of your test bot main.py cannot start without it, even in dev.
TELEGRAM_BOT_USERNAME username of your test bot, without @ Used to build the QR deep link.
BASE_URL http://localhost:8000 or empty Only used in logs and in the /admin_stats panel link.
ADMIN_PANEL_PASSWORD any string, 12+ characters Empty in dev = the admin panel answers 503.
ADMIN_USER_IDS your own Telegram user id Without it nobody can call /stats.
EVENT_NAME any label Prefix of alert messages.
LEADS_SECRET empty Empty = POST /leads answers 404. Fill it only when you work on leads.

The full list is on Environment variables. Comments in .env must be on their own line; the server loads the same file through systemd, which does not understand trailing comments.

Config.validate() sorts every problem into one of three groups. dev changes only the middle one.

Check prod dev
TELEGRAM_BOT_TOKEN missing, when starting the bot error error (not relaxed)
TELEGRAM_BOT_TOKEN missing, when starting the API error warning
TELEGRAM_BOT_USERNAME missing error warning (the QR deep link will not work)
Neither GEMINI_API_KEY nor OPENAI_API_KEY set error warning
ADMIN_CHAT_ID missing (bot) error warning (alerts go to the log only)
ADMIN_PANEL_PASSWORD missing (API) error warning; /admin answers 503
GEMINI_API_KEY equals TEST_GEMINI_API_KEY error warning
LEADS_SECRET shorter than 24 characters, or LEADS_IP_SALT missing, while leads are on error warning
Typos and out-of-range values (STICKER_RENDER_MODE, ART_STYLE, PHOTO_TTL_HOURS, SHEET_LAYOUT, …) error error (never relaxed)

In dev the bot also logs APP_ENV=dev — проверки ключей ослаблены, НЕ запускать так на ивенте (“key checks are relaxed, do NOT run an event like this”).

Open two terminals in the repository root, activate the venv in both.

Terminal 1 — bot, worker, delivery sweeper, cleanup

Terminal window
python bot\main.py

Terminal 2 — kiosk API

Terminal window
python -m uvicorn api:app --app-dir bot --host localhost --port 8000

Then open http://localhost:8000/kiosk/. GET / redirects there. Check http://localhost:8000/health.

  • To open the kiosk from another device on your network, bind uvicorn to all interfaces instead of localhost (the --host value used in the in-repo notes). The camera will not work there: browsers allow getUserMedia only on HTTPS or on localhost.
  • The API does not need the bot to be running. Without main.py sessions stay in idle because nobody answers /start, and queued jobs are never processed.

main.py takes an exclusive OS lock on bot\data\main.lock and writes its PID to bot\data\main.pid. A second start logs:

STARTUP отменён: main.py уже запущен (PID …, блокировка …)

(“startup cancelled: main.py is already running”) and exits with code 1. The lock exists because two workers on one queue each render jobs with their own, possibly stale, configuration. The lock is released by the OS when the process ends, including a crash.

Do not add --reload to uvicorn while you edit code against a database you care about. Every saved .py file restarts the API, and Database.init() runs schema migrations on start-up against whatever DB_PATH points to. --reload also does not notice changes to bot\.env.

The migration itself is defensive: it writes a backup app.db.bak-<timestamp> next to the database, rehearses on a copy, then applies the change in one transaction. Still, decide when it runs.

Path Content In git
bot\data\app.db SQLite database shared by both processes (DB_PATH) no
bot\data\sessions\<token>\ Selfie (selfie_<hex>.jpg), generated sheets, stickers of one session no
bot\data\debug_sheets\ Debug copies of generated sheets, anchor portraits, consistency reports no
bot\data\main.lock, main.pid Single-instance lock of main.py no
bot\data\app.db.bak-<timestamp> Automatic backup before a schema migration or a guest reset no
logs\app.log Bot and worker log, rotated at 5 MB, 3 backups (LOG_DIR) no
logs\api.log API log, same rotation, separate file so two processes never rotate one file no
out\ Output of tools (screenshots, reports, previews) no

Files with faces in sessions\ and debug_sheets\ are deleted after PHOTO_TTL_HOURS, but the cleanup loop lives in main.py: nothing is deleted while main.py is stopped. See Backups & data lifecycle.

Which mode a process really uses is in the first log lines: STARTUP render_mode=… (источник: …) pid=… appears in both app.log and api.log, and names where the value came from (bot\.env, the OS environment, or the default).

A desktop browser with a webcam replaces the tablet, because localhost is a secure context.

  1. Put a real GEMINI_API_KEY (or OPENAI_API_KEY) of a test project into bot\.env.
  2. Optional: set ALLOW_MULTIPLE_PACKS=1 so the same Telegram account can repeat the flow.
  3. Start both processes.
  4. Open http://localhost:8000/kiosk/ and press the start button. The page calls POST /session and shows a QR.
  5. Scan the QR with your phone. If you have no phone at hand, open the deep link https://t.me/<bot_username>?start=<session_token> on the same machine; both values are in the POST /session response and in logs\api.log.
  6. In Telegram press ✅ Согласен (“I agree”). The kiosk page switches to the outfit screen within a second.
  7. Tap an outfit, allow the camera, let the countdown finish.
  8. The kiosk returns to the start screen immediately. Watch logs\app.log for STAGE_SUMMARY job=… and wait for the pack link in Telegram.

To repeat with the default one-pack limit, reset yourself:

Terminal window
python tools\reset_users.py <telegram_user_id> # dry run: shows what would be deleted
python tools\reset_users.py <telegram_user_id> --yes # backs up the DB, then deletes

Useful kiosk URL switches for manual checks:

URL Effect
/kiosk/?theme=light or /kiosk/light Light theme (KIOSK_THEME sets the default)
/kiosk/?preview=1200x1920 Render the kiosk inside a frame of another size
/kiosk/?cursor=1 Keep the mouse cursor visible
/kiosk/?camera=always Open the camera on load and keep it open (fallback behaviour)
/kiosk/?idle_sec=3&idle_prompt_sec=3 Short idle timers on the outfit screen (test only)
/kiosk/?qr_timeout_sec=6 Short QR timeout instead of 90 s (test only)

Without spending anything:

  • python -m pytest tests\test_worker_pipeline.py runs the whole worker with a fake AI and a fake Telegram bot.
  • python tools\kiosk_check.py drives the real kiosk page in a headless browser against a temporary database, with Chromium’s fake camera, without any bot or AI.

All tools run from the repository root: python tools\<name>.py.

Tool What it does Needs
kiosk_check.py Starts the API on a temporary database, opens the kiosk headless in several viewport sizes, checks QR decoding, single-session on repeated taps, timeouts, camera lifecycle, themes, idle prompt, console errors. Report in out\kiosk_check\report.md. Exit code 0 = all passed. --no-perf skips the slow-network measurement (2–3 min). Edge or Chrome
kiosk_screens.py Screenshots of every kiosk screen in both themes and orientations, plus an index.html gallery in out\kiosk_screens\. Edge or Chrome
preview_figure.py Cuts an existing figure sheet exactly as the worker does and builds a contact sheet with automatic checks (6 figures, no edge contact, no green left, file ≤ 512 KB). A sheet PNG: pass --sheet <file>. The default input file is git-ignored and absent in a fresh clone.
preview_template.py Composes real heads on a body template and checks the result (no magenta or green left, neck seam, outline not cropped). --variants adds rows with different head scales. A head sheet: --sheet <file>, or a previous composite job in bot\data\sessions.
check_pack.py Runs the two delivery checks on an existing pack name: getStickerSet and the public pack page. Exit code 0 = ready. TELEGRAM_BOT_TOKEN, network. Free, but it is a live Telegram call.
reset_users.py Deletes one guest’s users, sessions and jobs rows so they can start again. Dry run by default; --yes backs up the DB first. Files and the Telegram pack are not touched. Local DB (--db to point elsewhere)
make_placeholder_templates.py Regenerates the placeholder body templates. —
make_template_from_raw.py Turns a raw body drawing into a composite template. See Outfits, templates and art styles. A raw PNG
likeness_postprocess_check.py Replays the hybrid colour post-processing on 12 existing stickers and writes a before/after grid. A folder of stickers
cost_report.py Read-only spend report by day and stage from the api_usage table. Local DB. Its --ssh option reads the production database; that needs server access and is an operations task.
make_outfit_previews.py --recompress, --neckdown, --pick Re-encode or re-crop outfit preview cards from files already on disk. See the outfits page

These call a real model. Each one calls event_guard.require_test_key() before the first request.

Tool What it spends on Built-in cap Confirmation
style_compare.py One sheet per art style on one photo, plus one vision call ≤ 8 generations and ≤ $2 per run. Larger --budget or --max-generations values are refused. --price-per-image is mandatory; the budget is re-checked before every generation. Prints an estimate and asks; --yes skips the question, not the limits.
make_outfit_previews.py (default mode, --force, --candidates, --character) Text-only image generation of outfit cards ≤ 6 images and ≤ $0.50 per run, hard. --price-per-image is mandatory. Always asks.
face_box_check.py Vision calls only (cheap model), no images ≤ 3 runs per start Always asks.
consistency_run.py Full packs (image + vision) for every photo × outfit × mode × layout No dollar cap in the script. It prints the number of runs and expected calls per pack. Limit the run yourself with --photos, --outfits, --modes, --repeat. Asks unless --yes.
load_test.py A stream of full packs, meant to run on the server with its own database and a fake Telegram --budget, default $6: no new job is queued when spend + estimate would exceed it; stops on the first “out of credit” answer. Asks unless --yes.
tests/quick_test.py without --selftest Head sheets on the configured models --max-usd, default $1 Asks unless --yes. Not covered by the event guard: it reads keys only from the OS environment.

The EVENT_MODE / TEST_GEMINI_API_KEY guard

Section titled “The EVENT_MODE / TEST_GEMINI_API_KEY guard”

bot/services/event_guard.py decides which key a paid tool may use:

TEST_GEMINI_API_KEY EVENT_MODE Result
set, different from GEMINI_API_KEY any The tool runs on the test key. OPENAI_API_KEY is blanked for that process, so no fallback to the production provider happens.
empty 0 The tool runs on GEMINI_API_KEY from bot\.env.
empty 1 The tool exits before any call: it would spend the daily image quota of event guests.

The bot and the API never read TEST_GEMINI_API_KEY. In prod they refuse to start if it equals GEMINI_API_KEY.

Symptom Cause Fix
Конфигурация некорректна: … and exit code 1 validate() found an error. The message names the variable. Fix bot\.env. Locally check that APP_ENV=dev.
STARTUP отменён: main.py уже запущен Another main.py holds the lock. Stop the other process (its PID is in the message).
A pack was rendered in the wrong mode Two main.py processes, or the process was started before .env was edited. .env is read only at start. Restart. Compare JOB_RENDER_MODE job=N mode=… (обработка, worker pid=…) in app.log — the worker’s line decides.
API and bot behave as if on different code uvicorn restarted (--reload) and main.py did not. Restart both after every change.
STICKER_RENDER_MODE “empty value” error The key is present but empty or misspelt. Remove the key (default composite) or set composite / figure.
First key in .env is not recognised File saved with a byte-order mark by a text editor. Handled: the loader reads utf-8-sig. If it still fails, check for trailing comments on the value line.
Camera does not start Page opened over plain HTTP on a non-localhost address. Use http://localhost:8000/kiosk/ or HTTPS.
Guest got Дорабатываем детали… and later Что-то пошло не так… The 5-minute and 15-minute delivery deadlines also cover queue and generation time. Look for STICKERSET_DELIVERY_FAILED and the job error; see Pack not delivered.
429 RESOURCE_EXHAUSTED in the log Provider rate limit or quota. The pipeline backs off 5/15/30 s on the same provider. See Rate limit 429.
Kiosk shows the maintenance screen, bot answers Сейчас идёт техобслуживание… MAINTENANCE_MODE=1, or an automatic pause after an “out of credit” (402) answer stored in the database. /resume in the bot, or set the variable to 0 and restart. See Billing 402.
Figure sheet rejected (“N figures, expected 6”) The model broke the layout. The worker retries FIGURE_SHEET_ATTEMPTS times, then falls back to composite (FIGURE_FALLBACK_TO_COMPOSITE in the log). Usually nothing. If frequent, review SHEET_LAYOUT / FIGURE_SHEET_ASPECT.
WinError 64 during polling Network blip on Windows. None; aiogram reconnects.
AFC is enabled… printed by the Gemini SDK Informational. None.
Old selfies still on disk Cleanup runs only inside main.py. Start main.py; it cleans on start and then hourly.
Deploy scripts fail with \r errors on the server Files checked out with CRLF. Keep .gitattributes as is; do not set core.autocrlf overrides for this repository.
bot\.env, bot\data\, *.db, preview_*.png appear in git status They must never be committed (secrets, guest data, faces). Check .gitignore is intact.

Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.