FitGuru: 스마트폰으로 스쿼트를 코칭하고 비디오를 업로드하지 않는 방법
요약
FitGuru는 스마트폰 카메라 프레임만으로 실시간 자세 분석 및 운동 코칭을 제공하는 피트니스 앱입니다. 서버로 비디오를 전송할 필요가 없어 네트워크 연결이나 클라우드 인프라의 제약에서 자유롭습니다. 'Reflex Engine'과 'Cognitive Engine'이라는 두 개의 엔진을 분리하여 기기 내에서 모든 처리를 완료합니다.
핵심 포인트
- 비디오 업로드 없이 온디바이스(On-device) 자세 분석 구현
- 관절 각도 상태 기계로 정확한 반복 횟수 계산
- Reflex/Cognitive Engine 분리로 안정적인 코칭 제공
FitGuru: 스마트폰이 당신의 스쿼트를 코칭하고 비디오는 절대 업로드하지 않음
Innovhacks 4.0 · Team ARKA · Android (Kotlin) · 멀티모달 AI
힘든 세트 중 3회차를 진행하고 있습니다. 화면을 볼 수 없습니다. 또한 스쿼트가 충분히 깊었는지, 가슴이 무너졌는지, 마지막 잠금(lockout) 동작이 제대로 계산되었는지 알 수도 없습니다.
대부분의 '스마트' 피트니스 앱은 사용자의 카메라 영상을 서버로 전송하는 방식으로 이 문제를 해결합니다. 헬스장에서 자신의 몸에 대한 비디오를 보내는 것은 상당히 부담스러운 일입니다.
Innovhacks 4.0에서 Team ARKA는 더 좁은 아이디어를 테스트하기 위해 FitGuru를 개발했습니다:
휴대폰이 사용자의 자세를 관찰하고, 세트 수를 계산하며, 실수에 대해 코칭하고, 나중에 피드백을 제공할 수 있지만 — 비디오가 기기를 벗어나지 않도록 하는 것은 가능할까?
간단히 말해: 네, 코치를 두 개의 '뇌'로 분리하면 가능합니다. 모든 카메라 프레임에서 실행되는 저렴한 Reflex Engine과 세트가 끝났을 때만 깨어나고 숫자만을 보는 Cognitive Engine입니다.
데모: [https://drive.google.com/file/d/1yyQeH4TjNK_Y5u2S9msBnvTxCkJeANAF/view?usp=sharing]
코드: [github.com/atharvachaudhari1/innovhacks4.0_arka]
APK: apk/FitGuru-Innovhacks4.apk
MLH를 위해 제작됨
FitGuru는 Major League Hacking (MLH)을 통해 Innovhacks 4.0에 제출한 저희의 작품입니다. 저희에게는 주말 시간이 있었고, 하나의 엄격한 규칙(비디오가 휴대폰을 벗어나지 않는 것)이 있었습니다. 이것이 아키텍처가 거대한 단일 모델 대신 두 개의 두뇌로 구성된 이유입니다.
저희는 또한 MLH 해커톤에서 빌더들에게 자주 제공하는 두 가지 도구를 사용했습니다:
- Gemini 2.5 Flash: 세트 후 생체역학 보고서 및 회복 프로토콜용
- ElevenLabs: 코치 음성용
첫날 받아들인 제약 조건
카메라 피드가 휴대폰을 벗어나면, 저희는 이미 패배한 것입니다.
이 하나의 규칙이 아키텍처를 결정했습니다:
| 휴대폰에 반드시 있어야 하는 것 | 휴대폰 밖으로 나갈 수 있는 것 (선택적) |
|---|---|
| 카메라 프레임 | 시각적인 것은 없음 |
| ... | |
| 아카운트가 필요 없습니다. 클라우드 비디오 파이프라인도 없습니다. 세션 중간에 네트워크가 끊겨도, 코치는 여전히 반복 횟수를 계산하고 계속 말을 합니다. |
FitGuru의 현재 기능
앱을 열고, 운동을 선택하고, 휴대폰을 당신이 볼 수 있는 곳에 두고, 세트를 시작합니다.
18가지 운동에 대한 실시간 자세 분석 (Live form analysis)
스쿼트, 푸시업, 데드리프트, 풀업, 친업, 런지, 플랭크, 이두 컬, 달리기 케이던스, 벤치/가슴 프레스, 어깨 프레스, 레그 프레스/익스텐션/컬, 랫풀다운, 시티드 로우, 삼두 푸시다운.
관절 각도 상태 기계(joint-angle state machines)를 사용한 자동 반복 횟수 계산 스쿼트는 추측이 아닙니다. 반복은 무릎과 고관절이 '내려가는' 임계값을 넘었다가 다시 '올라가는' 임계값을 지나갈 때만 존재합니다.
구두 교정 (Spoken corrections) 세트 중간에 화면을 볼 필요가 없도록 합니다. 세션 중 짧은 큐(cues)를 제공하고,
브라우저에서 동일한 MediaPipe 루프를 사용하고, Apple Vision을 활용하는 iOS SwiftUI 클라이언트가 추가된 웹 컴패니언입니다.
두 개의 뇌, 거대한 하나의 모델이 아닙니다
매력적인 해커톤 아이디어는 다음과 같습니다: 프레임을 LLM(대규모 언어 모델)으로 스트리밍하여 "이 스쿼트 자세 괜찮아?"라고 질문하는 것입니다.
하지만 이는 세 가지 측면에서 모두 실패합니다.
- 지연 시간 (Latency). 언어 모델은 33ms의 카메라 프레임 내에 답변할 수 없습니다.
- 개인 정보 보호 (Privacy). 당신은 방금 운동하는 사람의 비디오를 업로드했습니다.
- RAM. 저희는 실시간 카메라 옆에 실제 온디바이스(on-device) LLM을 구동해 보았습니다. 프로세스가 죽었습니다. 자세한 내용은 나중에 다루겠습니다.
따라서 FitGuru는 모든 센서를 하나의 프롬프트에 쏟아붓는 것이 아니라, **역할(role)**에 따라 멀티모달입니다.
카메라 ──► MediaPipe Pose (온디바이스)
│
▼
...
Reflex는 삼각법과 임계값(thresholds)입니다. 모든 프레임에서 실행하기에 충분히 저렴합니다. Cognitive은 Gemini입니다. 이는 픽셀이 아닌 세션 요약을 인식합니다.
이 분할 구조가 바로 제품의 핵심입니다.
픽셀에서 무릎 각도로
Android 카메라 경로는 CameraX → MediaPipe Pose Landmarker(pose_landmarker_full.task)를 LIVE_STREAM 모드에서 사용합니다. 결과는 노즈, 어깨, 팔꿈치, 손목, 엉덩이, 무릎, 발목, 발가락의 33개 정규화된 3D 랜드마크로 구성됩니다.
우리는 모델에게 "저거 스쿼트였어?"라고 질문하지 않습니다. 우리는 관절에서의 내부 각도를 계산합니다.
// A–B–C로 형성되는 B 지점의 각도
fun calculateAngle(a: Landmark, b: Landmark, c: Landmark): Double {
val v1x = a.x() - b.x(); val v1y = a.y() - b.y()
...
스쿼트의 경우, B는 무릎입니다. A는 엉덩이이고, C는 발목입니다. 동일한 보조 함수를 사용하지만, 다른 관절에 적용하여 다른 모든 리프트에도 사용할 수 있습니다:
| 운동 | 주요 각도 | "내려가는" 모습의 기준 |
|---|---|---|
| 스쿼트 | 무릎 + 엉덩이 | 무릎 < 105° 및 엉덩이 < 115° |
| ... |
오버레이는 골격과 실시간 각도를 화면에 그립니다. HUD(Head-Up Display)는 무시해도 됩니다. 코치는 당신의 귀에 있습니다.
스쿼트는 분류기가 아닌 상태 기계입니다
단일 프레임에서 "반복!"이라고 외치는 비전 모델은 중복 계산을 하거나, 잠금 해제 동작(lockouts)을 놓치고, 반만 수행한 것으로 인정할 것입니다.
각 운동은 작은 자동장치와 같습니다. 스쿼트의 경우:
ready ──(knee < 105° AND hip < 115°)──► down
down ──(knee > 155° AND hip > 155°)──► up (+1 rep)
반복 횟수(rep)는 오직 올라오는 순간에만 카운트됩니다. 즉, 내려갔다가 다시 일어선 경우입니다. 동일한 각도는 자세 점검(form checks)을 유도합니다:
- 엉덩이가 75° 이하로 무너짐 → “가슴을 세우세요.”
- 푸시업 시 엉덩이 처짐 → “엉덩이를 들어 올리세요.”
- 컬 동작 시 팔꿈치 스윙 > 35° → “팔꿈치를 고정하세요.”
- 데드리프트 시 바벨이 아직 낮은 상태에서 엉덩이가 치솟음 → “엉덩이가 너무 빨리 올라갑니다.”
발성되는 지시(Spoken cues)는 2.5초의 냉각 시간을 가집니다. 코치는 유용할 수 있습니다. 하지만 잔소리하는 것은 허용되지 않습니다.
플랭크는 예외입니다. 반복 횟수가 없습니다. 저희는 유지 시간을 좋은 초(good-seconds) / 총 초(total-seconds)로 점수화합니다. 달리기 동작은 반복 횟수가 아닌 케이던스(cadence)를 점수화합니다.
이 전체 레이어가 바로 Reflex Engine입니다. 항상 작동하며, Gemini를 기다리지 않습니다.
Gym Shield: 카메라가 뒤에서 걸어오는 사람에게 초점을 맞추지 않는 이유
피트니스 센터는 포즈 모델(pose models)에 적대적입니다. 누군가 당신 뒤로 지나가면 MediaPipe가 두 번째 몸을 찾아내고, 갑자기 당신이 그들의 스쿼트를 세 번 한 것처럼 기록됩니다.
저희는 여러 포즈를 사용하여 랜드마커(landmarker)를 실행한 다음, 당신에게 고정합니다.
- 몸통 크기와 프레임 중앙과의 근접도를 기준으로 감지된 모든 몸을 점수화합니다.
- 가장 두드러진 몸에 앵커(anchor)를 고정합니다.
- 만약 그 몸이 잠시 사라지면 (누군가 앞에 가로질러 지나갈 경우), 45프레임(~1.5초) 동안 반복 상태를 동결합니다. 증가시키지도, 운동선수를 바꾸지도 않습니다.
- 유예 기간(grace window)이 만료된 후에야 새로운 주 피사체를 선택합니다.
HUD에는 Gym Shield: Passerby Filtered • Holding Rep State라고 표시됩니다. 이 문구는 저희가 이것을 작성하기 전에 낯선 사람들에게 세트 수를 잃었기 때문에 존재합니다.
Local-first는 슬로건이 아닙니다. 데이터 경로입니다.
비디오는 휴대폰을 떠나지 않습니다. 랜드마크도 휴대폰을 떠나지 않습니다. 기록은 장치 내의 Room 테이블에 있습니다:
@Entity(tableName =
클라우드 모델과 대화하는 유일한 시점은 세트가 끝난 **후**이며, Gemini API 키를 붙여넣었을 경우에만 가능합니다. 페이로드(payload)는 숫자들로 이루어진 단락입니다:
Exercise: Squats
Completed Reps: 12
Form Accuracy Score: 88%
...
프레임도, 랜드마크 목록도, 얼굴 정보도 없습니다. 텍스트 약 4KB 정도입니다.
키가 없거나 요청이 실패하더라도 규칙 기반 보고서(rule-based report)가 여전히 나타납니다: 진단, 세 가지 지침(cues), 두 가지 이동성 운동(mobility drills). 따라서 네트워크 문제로 인해 세트 진행이 중단되는 일은 없습니다.
## 디브리핑을 위한 언어 모델 추가하기
세트 중간에는 언어 모델이 적절하지 않습니다. 세트가 끝난 후에야 적절합니다.
`GeminiService`는 **Gemini 2.5 Flash**를 호출하며, 폴백(fallback)으로는 **1.5 Flash**를 사용합니다. 우리는 엄격한 JSON을 요청하고, 모델이 장식적으로 작동할 경우 마크다운 구분자(markdown fences)를 제거합니다.
**생체역학 보고서 (Biomechanics report)**
{
"diagnosis": "…두 문장으로 된 근본 원인…",
"correctiveCues": ["지침 1", "지침 2", "지침 3"],
...
**회복 프로토콜 (Recovery protocol)**
{
"recoveryWindowHours": 36,
"strainAssessment": "…",
...
운동 요약 하단 시트(bottom sheet)에는 이 두 가지가 모두 표시됩니다. **코치 리뷰 듣기 (Hear Coach Review)**를 누르면 ElevenLabs가 선택한 페르소나로 진단과 지침을 읽어줍니다.
이것이야말로 사람들이 실제로 느끼는 멀티모달 루프입니다: _비전(vision)이 세트를 계산하고 → 숫자가 Gemini로 전송되며 → 음성(voice)이 계획을 말해주는 것._
우리가 **하지 않은** 것은 비디오를 Gemini에 보내지 않는 것입니다. 이 모델은 카메라 오퍼레이터가 아니라 통계표를 보는 스포츠 과학자입니다.
## 코치에게 목소리 부여하기
Android TTS는
1. `voiceId + phrase`를 해시합니다 (MD5).
2. 만약 `cacheDir/elevenlabs_cache/<해시>.mp3`가 존재하면 재생합니다. 이것이 카운트 및 스톡 큐의 0ms 경로입니다.
3. 캐시 미스(cache miss) 시에는 텍스트를 POST하고, MP3를 작성한 다음, 재생합니다.
4. 타임아웃, 4xx 에러 또는 API 키가 없는 경우 **Android `TextToSpeech`**로 폴백합니다.
저희는 청크 스트리밍 방식이 아니라 완성된 클립을 다운로드합니다. 첫 번째 구문은 네트워크에 의존하며 (연결 타임아웃 8초 / 읽기 타임아웃 12초), 반복되는 체육관 언어 — _“Good push-up,” “Rest complete,” “Keep your chest up”_ — 는 첫 사용 후 즉시 작동합니다.
오프라인 상태에서는 코치가 더 평이하게(flatter) 변하지만, 침묵하지는 않습니다.
## 바벨을 들고 있기 때문에 핸즈프리 기능 구현
`VoiceCommandManager`가 Android `SpeechRecognizer`를 감싸서 재시작 루프 내에서 계속 듣습니다.
| 사용자가 말하는 것 | FitGuru의 동작 |
| :--- | :--- |
| “finish set” / “end workout” | 카메라 루프를 중지하고, Gemini 요약본을 엽니다. |
| ... |
첫 번째 소음이 심한 체육관 테스트 후에 추가해야 했던 두 가지 가드레일(guardrails) 기능:
- **에코 잠금(Echo lock).** ElevenLabs가 말하고 있는 동안에는 마이크 입력을 무시합니다. 그렇지 않으면 코치가 자기 자신을 듣고 세트를 “완료”해버립니다.
- **2초 디바운스(debounce).** 부분 결과(Partial results)가 자주 발생합니다. 하나의 명령이 세 번이나 발생하는 것을 방지해야 합니다.
## 심박수는 두 번째 앱이 아닌 두 번째 감각
비전은 관절을 봅니다. 하지만 당신이 여전히 BPM 168에 머물고 있다는 것은 볼 수 없습니다.
Wear OS / Galaxy Watch 클라이언트는 Data Layer 경로 `/heart_rate`로 BPM을 전송할 수 있습니다. `DataLayerListenerService`가 이를 라이브 운동 HUD(Head-Up Display)로 브로드캐스트합니다.
**Smart Rest** 기능은 이를 의도적으로 단순하게 사용합니다: 심박수가 이미 120 이하이면 준비되었다고 알리고; 그렇지 않으면 숨을 쉬라고 알려주며 30초를 추가할 수 있게 합니다. 타이머가 0에 도달하면 코치가 _“Rest complete.”_이라고 말합니다.
심박수는 또한 `WorkoutSession`과 함께 Gemini의 회복 프롬프트로 전달됩니다. 워치는 선택 사항입니다. 카메라 코치는 이에 의존하지 않습니다.
## 트레이너가 어깨 너머로 지켜보지 않아도 되는 나머지 체육관 공간
초보자 역도자는 단순히 스쿼트 카운터만 필요한 것이 아닙니다. 랫 풀다운(lat pulldown)에서 어떤 핀이 자신에게 할당된 것인지 알아야 합니다.
`gym_catalog.json`은 총 20개의 기구 가이드입니다: 랫 풀다운, 시티드 로우(seated row), 체스트 프레스(chest press), 숄더 프레스(shoulder press), 레그 프레스/익스텐션/컬(leg press / extension / curl), 케이블(cable), 스미스(Smith), 스쿼트 랙(squat rack), 덤벨(dumbbells), 벤치(bench), 페크 덱(pec deck), 트레드밀(treadmill), 자전거(bike), 로잉 머신(rower), 매트(mat), 데드리프트 플랫폼(deadlift platform), 풀업 바(pull-up bar), 러닝 케이던스(running cadence).
각 카드는 해당 기구가 무엇인지, 근육 라벨, 설정 단계, 실행 방법, 흔한 실수, 그리고 카메라 팁을 담고 있습니다. 만약 그 동작에 상태 머신(state machine)이 있다면, 카드를 열면 실시간 코칭이 시작됩니다.
홈 화면에는 다음 요소들이 있습니다:
- 인사말 (로그인 불필요 - 이름은 SharedPreferences에 저장)
- 주간 스트립(week strip)
- Room을 기반으로 한 주간 반복 목표(weekly rep goal)
- 날짜별로 순환하는 일일 푸시업 챌린지
- 분석(form / volume / HR) 및 체육관 디렉토리로의 바로가기(shortcuts)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기