Guest session flow
Who is this for
Section titled “Who is this for”Developers and on-site operators who need to know exactly what happens between a guest pressing Start on the tablet and the tablet becoming free for the next guest.
- The kiosk creates a session only when a guest presses Start (
POST /session). The QR code carries a Telegram deep link with the session token. - The bot checks the pack limit first, links the session to the Telegram account, and asks for consent. Nothing continues without the consent button.
- The outfit is chosen on the tablet, not in the bot. The API accepts the choice only when the session is
consented. - The tablet takes a 3-frame burst, uploads the sharpest frame, and is free again about 5 seconds later. It never waits for generation.
- Session statuses:
idle → linked → consented → template_chosen → photo_taken, plusexpiredfrom any state beforephoto_taken. - Maintenance mode answers
503with codemaintenance. An exhausted pack limit answers409with codepack_limit. The kiosk shows a friendly screen for both.
Sequence
Section titled “Sequence”sequenceDiagram
autonumber
participant G as Guest
participant K as Kiosk
participant A as API
participant D as SQLite
participant B as Bot
G->>K: Press Start
K->>A: POST /session
A->>D: insert session, status idle
A-->>K: session_token and deep_link
K->>A: GET /session/TOKEN/qr.svg
A-->>K: QR image
loop every 1 s
K->>A: GET /session/TOKEN
A-->>K: status, template_id, notice
end
G->>B: Scan QR, bot receives /start TOKEN
B->>D: save profile, check pack limit
alt limit exhausted
B->>D: session expired with notice
B-->>G: Link to existing pack or in-progress text
K-->>G: Notice screen for 7 s, then idle
else limit not reached
B->>D: idle to linked
B-->>G: Consent message with button
G->>B: Tap consent button
B->>D: users.consented_at, linked to consented
B-->>G: Go back to the tablet
K-->>G: Outfit cards, camera warms up
G->>K: Tap an outfit card
K->>A: POST /session/TOKEN/template
A->>D: consented to template_chosen
K-->>G: Look at the camera, countdown 3-2-1
K->>A: POST /session/TOKEN/photo
A->>D: photo_taken plus job queued, one transaction
A-->>K: job_id, status queued
A->>B: background task sends Generating text
K-->>G: Done screen for 5 s, then idle
end
Steps, tied to code
Section titled “Steps, tied to code”- Kiosk boots.
kiosk.jsapplies texts, loads outfit cards (GET /templates, retried with a 1 s → 10 s backoff), loads idle timeouts (GET /kiosk-config), loads optional footer logos (GET /branding) and asks for camera permission once, then switches the camera off again. The camera stays off until a guest has consented. - Start.
startSession()ignores repeated taps (startingflag, 1 s cooldown) and callsPOST /session.api.pycreate_sessiongenerates the token withsecrets.token_urlsafe(12)and inserts a row with statusidle. The response containssession_tokenanddeep_link(https://t.me/<bot_username>?start=<token>). - QR. The kiosk loads
GET /session/{token}/qr.svg. The server renders the QR itself (librarysegno, error correction level M, 4-module quiet zone,Cache-Control: no-store). The QR is shown only when the image has loaded; the 90 s timer starts at that moment. Polling ofGET /session/{token}starts, once per second. - Bot
/start <token>. The handler inhandlers/start.pyworks only in private chats. In this order it:- saves first name, last name and username (
db.save_user_profile); - checks the pack limit (
db.pack_quota). If exhausted, it closes the kiosk session with a notice (db.block_session) and replies with the existing pack link (У тебя уже есть набор: …— “You already have a pack”) orТвой набор уже готовится ⏳ …(“Your pack is already being prepared”); - rejects an unknown token (
Не нашёл сессию с планшета…— “Could not find the tablet session”); - rejects a session the tablet already closed (
Сессия закрыта, нажми «СТАРТ» на планшете.— “Session closed, press START on the tablet”); - links the session (
db.link_session_to_user):idle → linked. The same guest may repeat/start; a session already linked to another account is not taken over (Этот QR-код уже устарел…— “This QR code is stale”); - sends the consent message with one inline button, callback data
consent:<token>.
- saves first name, last name and username (
- Consent.
handlers/consent.pywritesusers.consented_atfirst, then moves the sessionlinked → consented(only for the same Telegram account). The message is edited toСпасибо! Теперь вернись к планшету…(“Thanks! Now go back to the tablet”). - Outfit on the tablet. On
consentedthe kiosk renders the cards, opens the camera to warm it up and starts the idle timer (idle.js). A tap callsPOST /session/{token}/template.choose_templatevalidates the id against the catalogue, repeats the pack-limit check, then runsdb.set_session_template(consented | template_chosen → template_chosen). - Photo.
takePhotoFlow()shows “look at the camera” for 2 s, counts 3-2-1, grabs 3 frames 150 ms apart, scores sharpness (variance of a Laplacian on a 320 px grey copy) and encodes the best frame as JPEG, quality 0.92. - Upload.
POST /session/{token}/photo(multipart fieldfile).upload_photo:- reads at most 15 MB (
MAX_UPLOAD_BYTES); _normalize_photo: must decode as an image, is rotated by EXIF, converted to RGB, must be at least 256 px on the short side, is downscaled to 2048 px and re-encoded as JPEG quality 92. Re-encoding drops EXIF metadata;- saves the file as
data/sessions/<token>/selfie_<random>.jpg; - calls
db.take_photo_and_enqueue, oneBEGIN IMMEDIATEtransaction: session must betemplate_chosen, the pack limit is checked again, the session becomesphoto_takenand a job row is inserted with statusqueued. A losing duplicate request deletes its own file.
- reads at most 15 MB (
- Kiosk is free. The API answers
{"job_id": …, "status": "queued"}and only then sendsГенерирую твой персональный набор стикеров…(“Generating your personal sticker pack”) as a background task. The kiosk shows the done screen for 5 s and returns to idle. What happens to the job is described in Generation.
Kiosk timeouts and limits
Section titled “Kiosk timeouts and limits”Source: CFG in kiosk.js, idle.js, and GET /kiosk-config.
| Setting | Value | Effect |
|---|---|---|
pollMs |
1 s | Status polling interval |
startCooldownMs |
1 s | Minimum gap between two session creations |
qrTimeoutMs |
90 s | QR shown without a scan → POST …/expire?reason=qr_timeout, back to idle |
linkedTimeoutMs |
3 min | Guest scanned but did not consent → reset (reason kiosk_reset) |
KIOSK_IDLE_SEC |
90 s (minimum 5) | No touch on the outfit screen → prompt Ты здесь? Нажми, чтобы продолжить (“Are you here?”) |
KIOSK_IDLE_PROMPT_SEC |
15 s (minimum 3) | Prompt countdown; at zero → reason=idle_timeout, back to idle |
lookAtCameraMs / countdownFrom |
2 s / 3 | Before the burst |
burstFrames / burstGapMs |
3 / 150 ms | Burst shape |
uploadRetries |
3 | Upload attempts, pauses 1 s, 2 s, 3 s. A 4xx other than 408/429 is not retried |
captureAttempts |
2 | Full “shoot + upload” attempts per session |
doneScreenMs |
5 s | Done screen |
noticeScreenMs |
7 s | Pack-limit notice screen |
errorScreenMs |
4 s | Error screen |
staffHoldMs |
3 s | Hold the hidden staff zone to reset (reason=staff_reset) |
For station checks the kiosk accepts ?qr_timeout_sec=, ?idle_sec= and ?idle_prompt_sec= overrides, and
?camera=always keeps the camera open permanently.
Expire reasons
Section titled “Expire reasons”POST /session/{token}/expire?reason=<code> moves a session to expired from idle, linked, consented or
template_chosen and stores the code in sessions.closed_reason. An unknown code is stored as kiosk_reset.
| Code | Sent when |
|---|---|
qr_timeout |
90 s without a scan |
idle_timeout |
Idle prompt ran out on the outfit screen |
cancel |
Guest pressed Cancel under the QR |
staff_reset |
Staff held the reset zone for 3 s |
camera_error |
Camera could not be opened |
kiosk_reset |
Default: consent timeout, lost session, any other return to idle |
An expired session costs the guest nothing: no job exists, so the pack limit is untouched. After the photo the same
call is a no-op, because photo_taken is not in the list of expirable statuses.
Failure branches
Section titled “Failure branches”| Situation | HTTP answer | What the guest sees |
|---|---|---|
Maintenance mode on POST /session, …/template, …/photo |
503, {"detail": {"code": "maintenance"}} |
Home screen with Сейчас идёт техобслуживание, загляни чуть позже (“Maintenance, come back later”) |
Pack limit on …/template or …/photo |
409, {"detail": {"code": "pack_limit", "state", "notice"}} |
Notice screen; the bot sends the existing link or the in-progress text |
| Outfit chosen before consent, or session in another state | 409 with the current status |
Hint to try again; polling moves the kiosk on |
| Unknown or hidden outfit id | 422 |
Hint to try again |
Photo for a session that is not template_chosen (double tap, second tablet) |
409 |
“Session finished” screen, then idle |
| File over 15 MB | 413 |
Counts as a failed capture attempt |
| Not an image, or short side under 256 px | 422 |
“Photo did not work”, automatic retake (once) |
| Unknown session token | 404 |
Kiosk returns to idle |
| Network error during polling | — | “No connection” badge; polling continues |
| Camera cannot be opened | — | “Camera unavailable” screen for 4 s, then idle with reason=camera_error |
Caddy caps request bodies at 20 MB, above the API’s own 15 MB limit, so an oversized upload still reaches the API and
gets a clean 413
(deploy/caddy/stickers.caddy).
Pack limit (anti-duplicate)
Section titled “Pack limit (anti-duplicate)”db._quota in db/db.py counts the
guest’s jobs in queued, processing, delivering and done. Failed jobs do not count. The allowance is
1 + users.extra_packs_allowed; ALLOW_MULTIPLE_PACKS=1 removes the limit (tests and demos only). The limit is
checked three times: in the bot on /start, in the API on outfit choice, and atomically when the job is created.
Where to go next
Section titled “Where to go next”- Generation — what the worker does with the queued job.
- State machines — session and job statuses in one place.
- Kiosk component — screens, themes and static files.
- API reference — request and response shapes.
- Runbook: station stuck — what to do when the tablet does not move on.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.