TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Overall Architecture: From the Phone to the Judgment Server, and What Leaves the Device

Status: Draft (2026-10-26), reflects actual measurements. The architecture is based on the repository code as of 2026-10-25, and numbers are measured directly by us, with dates and conditions attached to each table. Actual user path latency over the internet and field accuracy on mobile phones have not yet been measured. Writing rules: Measured data is the default, and unmeasured items are marked as "Unmeasured".

Table of Contents

1. How Many Machines Are Attached to a Single Scoreboard?

Using TT Lab at a table tennis club is simple. You set up a phone next to the table tennis table, open /tt/play/cam , and tap "Turn on Camera and Start." A large scoreboard appears on the screen over the camera view, and tapping the half of the screen belonging to the scoring player increases their score. The scoreboard informs you of the server turn, deuces, and court changes at the end of a game according to the rules.

Up to this point, there is only a single machine, the phone. Scoring rules, history, score reading, and recording all run inside the phone browser. The server only comes into play when the user chooses one of three options:

  1. Turn on auto-scoring. Camera frames are sent to the server's GPU worker to receive suggestions for rally winners.
  2. Share live. Only scoring events are sent to the server so that others can watch in real time.
  3. Tap "Record this match". Game scores of finished matches are saved on the server and reflected in ratings after opponent confirmation.

There is also a path to upload and analyze recordings separately (/tt/videos), but that is a separate feature where users upload video files directly.

This article draws this architecture as a single map, detailing what travels between columns, and specifically what leaves the device and what does not. The inner workings of judgment rules are unveiled in parts 2 and 3, model principles in part 4, training in part 5, and records in part 6.

2. A Map of Four Columns

① 휴대폰 브라우저 손 점수판 · 이력 · 점수 읽기 · 녹화와 편집 이력은 휴대폰 localStorage 에 — 기본으로 서버에 안 감 라이브 공유 득점 이벤트 기록하기 게임 점수만 자동 점수 512×288 JPEG ↑ 판정 제안 ② 웹 서버 (같은 코드 두 대) 로그인 · 시간 몫 · 자동 점수 중계(WebSocket) 경기 기록 · 레이팅 · 라이브 · 고친 기록 → DB 프레임 → (저장 안 함) ← 판정 올린 영상 내부 API 만 ③ 실시간 판정 작업자 공 → 탁구대 → 이벤트 → 심판 서버 GPU 시간 조각 하나 세션 3개까지 프레임은 창 3장만 메모리에 ④ 녹화본 분석 작업자 같은 엔진으로 올린 영상 분석 DB·저장소 열쇠 없음 동시 작업 1 원본은 30일 뒤 지움 모델 저장소(공유 볼륨) — 엔진 코드 · 모델 코드 · 가중치 sha256 을 대조해 심고, 작업자를 다시 시작하면 그것이 배포 읽기 읽기
Figure: Four columns consisting of the phone browser, web server, real-time judgment worker, and recording analysis worker, and what travels between them
Column Role Where It Runs Code
(1) Phone Browser Score rules, history, undo, score reading, recording and break time exclusion, auto-scoring chip, sending frames User phone backend/static/tt/play/ (Buildless ES modules)
(2) Web Server Login, auto-scoring relay (WebSocket), live viewing, match records and ratings, saving corrected records Two web servers (same code) backend/app/tt_*.py (FastAPI)
(3) Real-Time Judgment Worker Ball detection, table detection and tracking, event extraction, refereeing, "confirmation required" judgment A time slice on a single server GPU (24GB class) live/(GPU part), ttscore/(pure Python engine)
(4) Recording Analysis Worker Analyzes uploaded videos with the same engine to create match records and highlights A slice of the same server GPU (concurrent task 1) Batch part of the engine folder

Why the Engine Was Split into Two Pieces

Looking closely inside (3), there are two layers: the part requiring GPU and OpenCV (live/), and the pure Python engine(ttscore/) which only receives ball coordinates and events. Since the engine is a collection of functions that require neither GPU nor OpenCV, tests run in CI using only synthetic trajectories without video. Even if the ball model is swapped out, the referee rules must continue to pass tests unchanged. This separation was detailed in the previous series, Part 1, across four layers: "perception, events, refereeing, and slow confirmation."

Match rules are duplicated once more. Because the phone screen must count scores without calling the server, it usesttscore/rules.py, which ports the Python rule engine ( backend/static/tt/play/lib/scorekeeper.mjs ) to JavaScript. The live viewing server (backend/app/tt_watch.py) also has a third copy of the same rules. Since having three copies inevitably leads to divergence, the Python tests and Node tests were configured to read the same case files(backend/static/tt/play/data/rules-cases.json). Modifying only one side causes the tests on the other side to fail.

Phone Side: Buildless Modules and Service Workers

The screen code consists of ES modules read directly by the browser without a bundler. Module addresses use import maps with content hashes (?v=), ensuring that code changes alter the address and prevent mixing with old caches. The table tennis zone has a service worker (/tt/sw.js, scope /tt/) that caches screen code but does not cache APIs and record screens. This is because the manual scoreboard must keep running even if the table tennis club Wi-Fi drops, and score records must always reflect the latest server values.

/tt/play/cam The first screen of

does not initially load heavy chunks (auto-scoring, recording, live) until they are used, as most people only use the manual scoreboard.

3. What Happens When Auto-Scoring Is Turned On

  1. Auto-scoring is the longest path in this architecture. Let's trace step-by-step from when a frame is downscaled and sent until a score suggestion is returned. The camera video is downscaled to 512×288, compressed to JPEG quality 0.7, and sent at up to 30 frames per second (backend/static/tt/play/cam-auto.js, lib/live.mjs). Even if displayed flipped like a mirror on the screen, the sent version is the unflipped original. An 8-byte header (sequence number, timestamp in ms) is prepended to each frame. If 3 frames are sent without receiving a response, no new frames are sent. This is a mechanism to prevent the queue from growing indefinitely on slow connections.
  2. The web server guards the gate. WebSocket relay (backend/app/tt_live.py) only accepts users logged in via Google and tracks their slot and time quotas. Frames simply pass through.
  3. The worker converts frames into ball coordinates. The order of operations is: increase brightness → ball model (BlurBall) → select candidates near the previous ball → maintain ball trajectory for the last 2.5 seconds → fill short gaps → extract events → filter using learned bounds (veto) (live/core.py).
  4. The engine makes the judgment. The referee runs only after receiving the "Start Match" signal, and when a rally ends, it outputs the winner, reason, confidence, and whether "Confirmation Required" is true (Part 3).
  5. The browser asks with a chip. The judgment does not go directly to the scoreboard but appears as a chip. If a human does not respond, definitive judgments are applied as-is after 4 seconds, and confirmation-required judgments after 9 seconds (Part 3).

The ping-pong table is detected separately outside this flow. "Table detection" is run once before the match starts (using a segmentation model much heavier than the ball model), and thereafter tracked every frame using optical flow (Part 4).

Time Quotas and Limits

GPUs are expensive and shared resources. Therefore, there are multiple layers of limits. Below are the default values in the code (we could not verify if separate values exist in production config files).

Limit Value Target
Continuous usage time per session 20 minutes Web Relay
Per-user daily quota 40 minutes Web Relay
Site-wide daily quota 360 minutes Web Relay
Simultaneous active slots 3 Both Web Relay and Worker
Size of a single frame 120,000 bytes Web Relay
Frames per second 36 frames (burst up to 10 more) Web Relay
Disconnect on inactivity 15 seconds (20 seconds for workers) Web Relay & Worker
Table pre-detection 30 times per person per hour Web Relay

If the connection drops, the browser attempts to reconnect up to five times at intervals of 1, 2, 4, 8, and 15 seconds. Reconnection is not attempted for closures where reconnecting is useless, such as when login is required or quotas are exhausted (close codes 4401, 4429, 4430, 4431, etc.).

Measurements: When concurrent sessions increase

A single worker handles up to three sessions. When three sessions arrive simultaneously, they share a single GPU time slice among the three. Below are the measured values under a condition where the same GPU was shared with a text-to-speech service on 2026-10-11 (docs/tt/PERFORMANCE.md 4.2).

Concurrent Sessions Processed fps per session Frame Round-Trip Time
1 29.4 p99 approx. 32ms
3 16–20 p50 70–95ms, p99 approx. 127ms

With three sessions, 21–30% of sent frames were overwritten by new frames and discarded before being processed. This is because the worker has a single GPU loop, and every time the loop runs, only the most recent windowof each session is grouped into a single batch for processing, allowing newer windows to overwrite delayed ones (live/server.py Header, "Latency First"). For scoring judgments, being on timeis more important than not missing a single frame. The trade-off is that in fast rallies, 30fps effectively drops to 16–20fps, and as shown in the previous series Part 5, lower frame rates in this engine increase "incorrect judgments without confirmation" instances.

Measurements: From rally end to score display

The target is "End of rally → Judgment within 1.5 seconds" (real-time budget set by the research director). The engine considers a rally ended if there is no new bounce for 1.5 seconds after the last bounce or if the ball is not visible for 1 second. In other words, a significant portion of the budget is spent waiting to see if it has truly ended.

Metric Condition Value
Judgment latency (Rally end → Judgment) Public video test 7 matches, clean conditions, real-time path simulation (2026-10-13–14, director E00·E07) p90 1.31s
Judgment latency Eight shooting conditions E2E (2026-10-14–15, PERFORMANCE 2.16) Median 0.67–0.90s
Time until table proposal appears Camera scoreboard E2E (2026-10-14, PERFORMANCE 2.13) 2.8–3.4s
Single table detection run 211 test images on production real-time worker, server GPU (24GB class), post-warmup (2026-10-25–26, director E44) Median 1.73s, p95 2.14s
First table detection call Same conditions, first time loading the model into memory 12.9s
Real user path over the internet — Unmeasured

Attempts to reduce latency by shortening the end threshold (such as ending sooner when the ball disappears) were made multiple times but all rejected (director E03·E10·E24·E28). Ending early results in scoring too prematurely during long defensive rallies.

4. Deployment: Reading Code and Weights from the Model Repository

The deployment methods for the web and workers differ. Since this difference once caused an incident, it is described in detail.

Web via images

The web server follows a standard approach. When pushed to the repository, CI runs tests, builds a container image, and updating the image tag in the GitOps repository allows ArgoCD to automatically synchronize it to development and production environments. Frontend code (backend/static/tt/) is also included inside this image.

Workers use shared volumes instead of images

GPU workers do not build new images. They reuse the exact AI service image (PyTorch 2.9, Python 3.11) that was already running on the same GPU server, and load the engine code, BlurBall model code, weights, and OpenCV wheel from the model repository (shared volume) . Since engine code changes frequently while heavy dependencies like PyTorch rarely do, you don't need to rebuild multi-gigabyte CUDA images every time you modify the code. The trade-off is that the Python version of the image becomes locked, which is revisited in the SAM 3 story in Part 4.

The script that populates the code and weights (live/seed-pvc.sh) works as follows:

  1. It verifies the weight's sha256 against the value specified in the code. The ball model default weights 3545206c…, and the table tennis model (SAM 2.1·EfficientTAM) fetches the official distribution bundle, verifies its sha256, and extracts it.
  2. It downloads new files to the .new folder first, and replaces them all at once after the download completes. This prevents workers from reading partially downloaded code.
  3. It restarts the worker. This constitutes deployment.

When reading weights, the worker re-verifies the sha256 and reads them strictly using PyTorch's weights_only=True (live/blurball.py). Since weight files are pickles and can execute arbitrary code, this option prevents the creation of any objects other than tensors.

Features controlled by environment variable switches

Almost all new features follow the code default off, enabled via environment variable pattern. As of 2026-10-25, the switches enabled in the live worker configuration are as follows:

Switch What it changes Part
TTSCORE_BOUNCE_VETO Discard fake bounds generated by rules using learned 1D CNN Parts 3 and 4
TTSCORE_BRIGHTEN Increase frame brightness before searching for the ball Part 4
TTSCORE_REVIEW_CAL Determine "Needs Review" using a learned regression instead of a confidence of 0.6 Parts 3 and 4
TTSCORE_DIRECT_MISS Decision direction for serves where the first bounce is hidden from the server side Part 3
TTSCORE_EDGE_REVIEW Landings near the end line marked as needing review Part 3
TT_TABLE_FINDERS Order of the table tennis table detection model Part 4
TT_TABLE_KP Integrate the learned vertex model into table tennis table detection (Enabled 2026-10-25) Part 4

Conversely, the fine-tuned ball weights (TTSCORE_BALL_WEIGHTS) and trajectory filling (TTSCORE_BALLISTIC_FILL) have finished experimentation but failed to meet the conditions for activation and are absent from the configuration (disabled). The advantage of the switch approach is that disabling is easy. By simply deleting a single line of configuration while leaving the code and weights intact, it reverts to the previous behavior.

An incident caused by this approach

We also experienced the downside firsthand. Unlike the web application, worker configurations exist outside of automated deployment, meaning a human must apply them while code is populated separately. Consequently, configuration and code can drift apart. On October 18, 2026, an actual incident occurred where TTSCORE_EDGE_REVIEW=1 was enabled in the configuration, but the engine code in the model repository did not yet contain that feature (Log E41). The switch was turned on, but nothing happened, and no errors were raised. We resolved this by re-populating the code, and since then, we strictly follow the sequence when enabling features: "Apply configuration → Populate code → Restart → Verify that the feature exists in the code inside the worker."

For the same reason, when enabling the vertex model (TT_TABLE_KP) on October 25, 2026, we did not rely solely on the configuration file; we actually sent test images to the production worker and checked the responses for traces of the vertex model. All 212 responses recorded evidence that the vertex model had executed (Log E44, Part 4).

Fences between compartments

Because workers handle user video feeds, we erected multiple layers of fences.

5. What leaves the device and what does not

휴대폰 안에 머무는 것 서버로 가는 것 카메라 원본 영상 원본 해상도 자동 점수를 켤 때만 512×288 JPEG · 저장 안 함 경기 이력 전체 localStorage 「이 경기 기록하기」 게임 점수만 녹화 파일 · 편집본 폰에 저장 「라이브로 공유」 득점 이벤트만 점수 읽어 주기 브라우저 음성 합성 칩에 답한 기록 영상 없음 · 동의 표시 설정 · 리모컨 키 localStorage 올린 영상(별도 기능) 줄여 올림 · 30일 보관 기기 경계 오른쪽은 모두 사용자가 켜거나 누를 때만 일어난다. 켜지 않으면 서버로 아무것도 가지 않는다.
Figure: What stays on the mobile phone versus what goes to the server by feature

The guiding principle of the TT Lab table tennis screen is stated in backend/static/tt/play/cam.js a single line in the preamble: Camera footage never leaves the device. Breaking it down by feature:

Feature Stays on mobile phone Goes to server Remains on server
Manual scoreboard Entire match history (localStorage), settings None None
Score reading aloud Browser speech synthesis (speechSynthesis) None None
Recording and trimming breaks Recording file (saved on phone), motion indicators None None
Live sharing Video Score, undo, and end events (sequenced) Live matches and event queues
Recording match history Per-rally history Game scores, match duration, opponent Match records, ratings
Auto scoring Original resolution video 512×288 JPEG frames(up to 30 per second) Frames are not saved. Corrected decision records (including consent indicators, no video)
Upload recording for analysis — User-selected video file (browser compresses and uploads at 360p60 500kbps) Originals for 30 days, analysis results

Auto-scoring is an exceptionto this principle because finding the ball requires a GPU to inspect the video, and those GPUs currently reside on the server. Therefore, we made the exception narrow.

Recorded video analysis is a feature where users manually select and upload files, so its nature differs. Originals are deleted after 30 days, managed by a daily early-morning cleanup job.

Long-term plan: Moving GPUs to mobile phones

The path to bringing auto-scoring back within the principle is performing ball detection directly on mobile devices. Although the ball model is small at 1.49 million parameters, whether it can achieve 60fps in mobile browsers has not yet been measured. For now, "sending to the server without storing" is a pragmatic compromise.

6. Conclusion

A word from TT: A scoreboard originally just needs a single phone. The server is only a friend you call when you want it to "help watch the ball." And that friend has promised not to write down what it sees in a diary.

References

  1. T. Gossard, F. Radovic, A. Ziegler, A. Zell. "BlurBall: Joint Ball and Motion Blur Estimation for Table Tennis Ball Tracking." CVPR Workshops (CVsports), 2026. arXiv:2509.18387.
  2. N. Ravi et al. "SAM 2: Segment Anything in Images and Videos." ICLR, 2025. arXiv:2408.00714.
  3. Y. Xiong et al. "Efficient Track Anything." ICCV, 2025. arXiv:2411.18933.
  4. Our measurements: Repository docs/tt/PERFORMANCE.md 2.13·2.16·4.2, Research logs E00·E07·E41·E44.

Further Reading

Sign in to like

Comments

No comments yet.

Sign in to leave a comment