Local setup: product
Who is this for
Section titled “Who is this for”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) anduvicorn api:appon port 8000 (kiosk API, kiosk page, admin panel, leads endpoint). - All configuration lives in
bot\.env. SetAPP_ENV=devlocally; the default isprodandprodrefuses to start without keys. - The kiosk opens at
http://localhost:8000/kiosk/. The camera works only on HTTPS or onlocalhost. - Only one
main.pymay run per data directory. A second one exits withSTARTUP отменён. python -m pytestneeds 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.
Prerequisites
Section titled “Prerequisites”| 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.
Install
Section titled “Install”git clone https://github.com/<owner>/ai-sticker-pack.gitgit -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 + httpxCopy-Item bot\.env.example bot\.envbot\requirements-dev.txt includes bot\requirements.txt and adds pytest and httpx.
Configure bot\.env
Section titled “Configure bot\.env”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.
What APP_ENV=dev relaxes
Section titled “What APP_ENV=dev relaxes”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
python bot\main.pyTerminal 2 — kiosk API
python -m uvicorn api:app --app-dir bot --host localhost --port 8000Then 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--hostvalue used in the in-repo notes). The camera will not work there: browsers allowgetUserMediaonly on HTTPS or onlocalhost. - The API does not need the bot to be running. Without
main.pysessions stay inidlebecause nobody answers/start, and queued jobs are never processed.
Single-instance lock
Section titled “Single-instance lock”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.
The --reload pitfall
Section titled “The --reload pitfall”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.
Where data and logs land
Section titled “Where data and logs land”| 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).
Try the full flow without a tablet
Section titled “Try the full flow without a tablet”A desktop browser with a webcam replaces the tablet, because localhost is a secure context.
- Put a real
GEMINI_API_KEY(orOPENAI_API_KEY) of a test project intobot\.env. - Optional: set
ALLOW_MULTIPLE_PACKS=1so the same Telegram account can repeat the flow. - Start both processes.
- Open
http://localhost:8000/kiosk/and press the start button. The page callsPOST /sessionand shows a QR. - 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 thePOST /sessionresponse and inlogs\api.log. - In Telegram press
✅ Согласен(“I agree”). The kiosk page switches to the outfit screen within a second. - Tap an outfit, allow the camera, let the countdown finish.
- The kiosk returns to the start screen immediately. Watch
logs\app.logforSTAGE_SUMMARY job=…and wait for the pack link in Telegram.
To repeat with the default one-pack limit, reset yourself:
python tools\reset_users.py <telegram_user_id> # dry run: shows what would be deletedpython tools\reset_users.py <telegram_user_id> --yes # backs up the DB, then deletesUseful 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.pyruns the whole worker with a fake AI and a fake Telegram bot.python tools\kiosk_check.pydrives 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.
Free tools (no paid calls)
Section titled “Free tools (no paid calls)”| 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 |
PAID or live tools
Section titled “PAID or live tools”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.
Troubleshooting
Section titled “Troubleshooting”| 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. |
Where to go next
Section titled “Where to go next”- Environment variables — every variable with its default and validation rule.
- Testing — what the test suite covers and the pre-commit checklist.
- Code rules — the hard rules, including the one about paid calls.
- API reference — the endpoints the kiosk calls.
- Local setup: landing — the other repository.
- Deploy & rollback — how the same code runs on the server.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.