핸즈프리 PT
폰을 세워두고 운동만 하면 전면 카메라가 어떤 운동인지 알아보고, 횟수를 소리로 세고, 자세를 교정하고, 기록을 정리하는 웹앱(PWA).
안드로이드 APK와 브라우저용 PWA를 제공한다. 아이폰·갤럭시 브라우저에서는 "홈 화면에 추가"로 사용할 수 있다.
- 포즈 인식: Google MediaPipe Pose Landmarker(관절 33점, 폰 안에서 처리 — 영상은 기기 밖으로 나가지 않음)
- 판단 로직(운동 판별·횟수·세트·자세 교정):
app/js/engine/ — 실제 운동 영상으로 수치를 맞춘 규칙
- 기록: 이 기기의 브라우저 저장소(localStorage). 설정 화면에서 백업 파일로 내보내기·불러오기
바로 써보기
| 어디서 | 방법 |
|---|
| 안드로이드 폰 (앱) | 폰에서 handsfree-pt.apk 받기 → 설치(처음이면 "출처를 알 수 없는 앱 설치" 허용) → 첫 실행 때 카메라 허용 |
| 아무 폰 브라우저 | https://haho240414.github.io/handsfree-pt/ 접속 → 공유/메뉴 → 홈 화면에 추가 |
| 이 맥 (웹캠) | 핸즈프리PT.command 더블클릭 → 브라우저가 열림 → 운동 시작 → 노트북은 탁자 위라 1.5m 안팎이면 전신이 들어옴(상체 운동은 1m 안쪽도 됨) |
| 녹화한 영상 | 설정 → 녹화한 영상으로 분석 → 영상 파일 선택 (재생보다 빠르게 분석해 기록으로 저장) |
폰은 의자·탁자 위(허리 높이)에 세우면 1.5m 안팎, 바닥이면 뒤로 20° 기울여서(바닥에 똑바로 세우면 2.7m 필요). 상체 운동은 허리까지만 보이면 된다(홈 → '어디에 둘까요?').
소리를 켜면 "스쿼트, 하나, 둘…" 음성이 나온다(설정 → 소리 확인).
안드로이드에 설치하기 (처음 한 번)
- 폰에서 위 링크를 열거나 QR을 찍는다 → "이 파일은 기기에 해로울 수 있습니다" 경고가 나오면 다운로드
- 다운로드 알림(또는 내 파일 → 다운로드)에서
handsfree-pt.apk 를 연다
- "출처를 알 수 없는 앱" 안내가 나오면 설정 → 이 출처 허용 을 켜고 돌아와 설치
(삼성: 설정 → 애플리케이션 → 특별한 접근 → 출처를 알 수 없는 앱 설치 → 크롬/내 파일 허용)
- Play 프로텍트가 "알 수 없는 앱"이라고 하면 세부정보 → 무시하고 설치 (스토어에 안 올린 개인 앱이라 나오는 경고)
- 첫 실행 때 카메라 허용 → 설정 → 소리 확인 으로 한국어 음성이 나오는지 확인
- 새 버전은 같은 링크로 받아 설치하면 기록을 유지한 채 업데이트된다 (같은 서명 키로 만들기 때문)
- 뼈대가 안 그려지거나 운동 시작 때 앱이 멈추면 설정 → GPU 가속 끄기(호환 모드). 앱이 AI 가속 중 멈추면 다음 실행 때 자동으로 호환 모드로 바뀐다
- 안드로이드 8.0 이상 (헬스 커넥트 연동 라이브러리 최소 버전)
- 앱을 지우면 기록도 지워진다 → 설정 → 백업 파일 저장(공유 창)으로 가끔 백업
- 참고: 구글은 2026년부터 일부 국가에서, 이후 전 세계적으로 '설치 파일(APK)도 개발자 등록'을 요구하는 정책을 단계적으로 도입 중이다. 한국에 적용되면 개발자 계정 등록이 필요할 수 있다
안드로이드 앱은 어떻게 만들어지나
app/ 웹앱을 Capacitor 로 감싼 네이티브 앱(android/). AI 모델·엔진이 APK 안에 들어 있어 오프라인에서도 동작
- WebView 가 못 하는 것만 네이티브로: 음성은 폰 기본 TTS(
@capacitor-community/text-to-speech), 화면 꺼짐 방지(MainActivity), 뒤로가기(운동 중엔 안 꺼짐), 백업 파일은 공유 창,
헬스 커넥트 쓰기는 앱 안의 플러그인(HealthConnectPlugin.kt — 공개 플러그인들은 운동 세션 쓰기를 지원하지 않아 직접 만듦)
- 빌드는 GitHub Actions(
.github/workflows/android.yml): APK 빌드 → 안드로이드 에뮬레이터(API 34)에 설치해 단계별 자동 점검 → 통과하면 Releases 의 다운로드 링크 갱신. main 에 올리면 자동
- 점검 항목(2026-09-30 v1.0.2 통과): 네이티브 연결(TTS·뒤로가기·공유·파일), 한국어 TTS, 판단 엔진 계산, AI 모델 로드 1.6초·추론 22~64ms, 가상 카메라로 운동 화면 초당 8장 분석, 운동 중 뒤로가기 무시
- 에뮬레이터의 가상 GPU(SwiftShader)로 AI 가속을 켜면 에뮬레이터가 통째로 멈춰서, 점검은 호환 모드(CPU)로 한다. 실제 폰은 GPU 가 기본
- 서명 키는 이 맥의
android-signing/(git 제외)과 GitHub 비밀값에만 있다. 잃어버리면 다음 버전을 기존 앱 위에 업데이트 못 함 → 따로 백업
기능
-
간결한 시작 화면: 자유 운동·운동 선택·루틴을 고르고 시작. 루틴은 운동과 예상 시간을 먼저 보여 주고 설정·나머지 운동은 펼쳐서 확인.
-
카메라 준비 → 3초 후 시작: 화면에 몸과 필요한 관절이 보이는지 확인한 뒤 시작. 준비 화면에서 전후면 전환·촬영 도움말 제공. 영상은 원본 비율을 유지해 표시.
-
일시정지·이어가기: 횟수와 운동 시간이 함께 멈추며, 화면에서 몸을 놓치거나 앱을 벗어나면 자동으로 일시정지. 재개 시 중간 동작을 새 반복으로 세지 않음.
-
실제 횟수로 세트 종료: 목표 전에 끝내도 실제 인식한 수를 저장. 휴식 중 +30초·직전 기록 수정·다음 세트 시작. 다음 목표·무게는 '운동 상세'에서 조정.
-
운동 검색·최근·즐겨찾기: 운동 선택에서 이름 검색과 목록 필터. 선택한 운동의 즐겨찾기는 이 기기에 저장되며 백업에도 포함.
-
운동 메모·체감 강도: 운동 완료에서 세트 수정, 짧은 메모와 체감 강도 기록. 직접 수정 표시와 원래 자세 분석을 분리해 수동 횟수 보정으로 자세 지적이 늘어나지 않음. 기록 화면은 최근 기록부터 표시하고 통계·달력은 펼쳐 보기.
-
오프라인 디자인 자산: Pretendard(폰트)·Phosphor(아이콘)을 APK/서비스 워커에 포함. 폰트와 아이콘 라이선스는 각 자산 폴더에 보관.
-
자동 인식 모드: 운동을 고르지 않고 시작 → 1회째에 "스쿼트 같아요"로 알아챈 걸 보여주고, 2회 이어지면 "스쿼트. 둘"처럼 확정해 1회째부터 셈
-
폰 기울기 보정: 폰을 바닥에 두고 벽에 기대 세우면 카메라가 위를 올려다봐서 몸이 기운 것처럼 보인다 → 폰 기울기 센서로 재서 되돌린다 (시뮬레이션: 25° 기울면 인식 18 → 10~15개, 보정하면 18개 그대로 — node test/robust.mjs)
-
화면 구도 안내 — 운동에 필요한 만큼만: 상체 운동(팔·등·가슴·어깨)은 상체와 팔만, 나머지는 무릎까지 보이면 된다(framing.js).
자동 인식에서 아직 무슨 운동인지 모르면 상체 기준으로 너그럽게 보고 "하체 운동이면 무릎까지"만 글로 알려 준다(2026-10-18).
스쿼트를 골라서 할 땐(오늘의 루틴·하루 목표·하나만 골라 시작) 허리까지만 보여도 된다(아래 '가까운 화면')
-
템포·속도 그래프 (테크노짐·VBT 장비처럼): 반복마다 힘주는 구간(당기기·밀기·일어서기)과 돌아오는 구간(풀기·내리기·앉기)을 몇 초에 했는지, 힘주는 속도(m/s)를 잰다. 운동 중엔 속도 곡선(위 = 힘주기, 아래 = 돌아오기)과 반복별 속도 막대, 처음보다 20% 넘게 느려진 반복은 주황(한계가 가깝다는 신호). 요약 화면에 세트별 평균 템포·막판 속도 저하
-
하루 목표·못 채우면 알림 (2026-10-18): 홈 → 하루 목표에서 운동별 하루 합계(예: 스쿼트 50회·푸시업 20회·플랭크 60초)와 운동 시간(분),
목표가 있는 요일(빠진 요일은 쉬는 날)을 정하면 홈 카드에 오늘 진행 막대·🔥 연속 달성일이 보이고, [남은 목표 하기] 로 남은 양을 바로 루틴(PT 모드)으로 시작한다.
안드로이드 앱은 정한 시각(기본 20:00, 마지막 알림 22:00)까지 못 채웠으면 알림 — 남은 양을 적어서("남은 것: 스쿼트 20회"). 14일치를 미리 걸어 두고 앱을 열 때·운동을 기록할 때마다
다시 계산해서 채운 날은 그날 알림이 지워진다(goal.js reminderPlan, reminders.js). 몇 분 늦어도 되는 알림이라 '정확한 알람' 권한은 안 묻고, 절전 중에도 오게.
알림 시험(5초 뒤 한 번) 버튼, 7일 연속 달성하면 목표 10% 올리기 제안. 웹(브라우저)은 앱이 꺼져 있으면 알림을 못 띄워서 홈 카드로만
- (2026-10-19 추가) 주간 목표(일주일(월
일)에 27일 채우기, 홈 카드에 요일 점 · 남은 날이 모자라면 "이번 주 목표, 오늘 꼭 해야 해요"),
아침 알림(오늘 할 것을 미리, 기본 꺼짐), 연속 기록 문구("오늘 하면 5일 연속!", 마지막 알림은 "4일 연속 기록이 끊기기 직전이에요"),
알림을 누르면 남은 만큼 바로 시작(앱이 열리며 남은 양으로 짠 루틴이 준비 화면까지)
지원 운동 (66종)
O = 실제 영상으로 횟수를 확인한 운동, 베타 = 다양한 촬영 각도·체형의 영상 검증이 부족한 운동(앱에서도 베타 표시)
| 분류 | 운동 | 검증 | 자세 교정 |
|---|
| 하체 | 스쿼트(맨몸·고블릿·바벨) | O | 더 깊게 · 무릎 바깥으로 · 가슴 펴기 |
| 하체 | 런지 | O | 더 깊게 · 상체 세우기 |
| 하체 | 데드리프트(컨벤셔널·루마니안) | O | – |
| 하체 | 카프 레이즈 | 베타 | – |
| 가슴·어깨 | 푸시업 | O | 더 깊게 · 엉덩이 올리기/내리기 |
| 가슴·어깨 | 벤치 프레스 | 베타 | 끝까지 밀기 |
| 가슴·어깨 | 숄더 프레스(양팔·한팔) | O | 끝까지 밀기 |
| 가슴·어깨 | 사이드 레터럴 레이즈 | O | 어깨 높이까지 · 너무 높아요 |
| 가슴·어깨 | 프론트 레이즈 | O | – |
| 등 | 풀업·친업 | O | 더 높이 |
| 등 | 랫풀다운 | O | – |
| 등 | 바벨·덤벨 로우 | 베타 | – |
| 팔 | 덤벨 컬 | O | 끝까지 펴기 · 반동 줄이기 |
| 팔 | 트라이셉 익스텐션(오버헤드) | O | – |
| 팔 | 딥스 | 베타 | – |
| 코어 | 윗몸일으키기·크런치 | O | – |
| 코어 | 힙 브릿지·힙 쓰러스트 | O | 엉덩이 더 높이 |
| 코어 | 레그 레이즈 | O | – |
| 코어 | 플랭크(시간) | 베타 | 엉덩이 올리기/내리기 |
| 유산소 | 점핑잭 | 베타 | 팔 끝까지 |
| 유산소 | 마운틴 클라이머 | O | – |
| 유산소 | 버피 | O | – |
| 유산소 | 케틀벨 스윙 | 베타 | – |
| 하체 | 레그 프레스(기구) | 베타 | – |
| 하체 | 레그 익스텐션(기구) | O | – |
| 하체 | 레그 컬(엎드려 하는 기구) | O | – |
| 하체 | 사이드 런지 | O |
- 무릎 대고 하는 푸시업은 자유 운동에서 '푸시업'으로 센다(실영상 확인). 직접 고르면 '무릎 푸시업'으로 기록하며 무릎 자세에 맞지 않는 발목 기준 허리 처짐 교정은 하지 않는다. 벽 스쿼트는 벤치에 앉아 쉬는 자세와 구별이 안 돼서 운동 골라서 시작할 때만 잰다
- 일반 컬과 해머 컬처럼 그립·도구만 다른 변형은 카메라가 세부 이름을 구별하지 못한다. 직접 고른 이름으로 기록하고, 자유 운동 후보에는 같은 계열을 하나만 둔다. 추가 덤벨 8종과 맨몸 8종 모두 루틴·검색·동작 시범에 연결했다.
플랭크 검증 (2026-10-19)
- 그동안 플랭크는 합성 데이터로만 확인했다. 유튜브 플랭크 시범 11개(팔꿈치·하이 플랭크, 측면·대각선·정면, 각도 전환 편집, 무릎 대고 시작·엎드려 시작, 플랭크 없는 설명 영상)를
장면 시트로 직접 보고 버틴 시간을 세서
test/truth.json 에 넣었다. 사이드 플랭크 설명 영상 1개도 시간 정답을 새로 셌다.
- 처음 돌려 보니 시간이 늘 1~1.5초 짧았다: '가만히 있는지' 1.2초를 본 뒤부터 쟀기 때문 → 그 1.2초 안에서 자세가 이어진 첫 프레임부터 잰다.
무릎을 대고 버티는 자세가 플랭크로 잡혔다(무릎 126
144°, 진짜 플랭크는 버티는 동안 155° 이상) → 무릎 150° 로 가름.
사이드 플랭크는 들기 전·내린 뒤 엉덩이를 바닥에 대고 팔꿈치로 받친 자세까지 셌다(엉덩이가 어깨발목 선보다 0.08~0.11m 아래, 버티는 동안 0.05m 이내) → 0.06m 넘게 처지면 버티는 중이 아니다
- 시간을 바로 재자 느린 버피 시범(엎드린 채 4~5초 멈춤)이 '플랭크 5초'로 바뀌어 버피 3회를 놓쳤다 → 세던(세려던) 운동의 반복 도중이면 버티기로 바꾸지 않는다(버틴 시간은 그대로 처음부터 잰다)
- 결과: 버티기 영상 13개(플랭크 9·사이드 플랭크 2·벽 스쿼트 2) 모두 ±3초 이내(전에는 12개), 플랭크 3개·사이드 플랭크 2개는 ±1초(단위 테스트).
횟수 영상 49개·근력·맨몸·인기 운동 평가·실사용 조건 시뮬레이션(
robust.mjs)은 하나도 안 바뀜
- 알려진 한계(고치지 않음):
- 레그 컬 기구에 엎드려 6초 가만히 있으면 자동 인식이 '플랭크 6초'로 센다(
legcurl_wolfson). 그 자세(몸통 104°·엉덩이 145°·엉덩이 0.15m 높음)가
엉덩이를 조금 든 채 시작한 진짜 플랭크(plank_crossfit 5.2초: 101°·148°·0.14m)와 관절 값이 같아서, 막으면 진짜 플랭크를 못 센다. 레그 컬을 골라서 하면 생기지 않는다
- 앞 대각선 위에서 찍으면 플랭크 들기 전 엎드려 팔꿈치로 받친 자세도 플랭크로 센다(
plank_hold39 4초) — 그 각도에선 엉덩이가 처진 플랭크와 구분이 안 된다. 옆에서 찍기
- 무릎을 대고 하는 플랭크(입문용)는 지금 세지 않는다
자주 하는 근력 운동 추가 검증 (2026-10-02)
- StrengthLog의 운동 기록 통계와 Fitbod 운동 목록을 참고해 기존 기본 운동에 빠진 변형·머신 운동 10종을 추가했다. 통계는 해당 앱 사용자들의 기록이며 모든 지역의 이용자를 대표하는 순위로 해석하지 않는다. 총 66종, 새 10종은 직접 선택하는 베타이며 루틴·검색 별칭·동작 시범에 연결했다.
test/popular-truth.json / node test/popular-eval.mjs로 별도 평가한다. 원본 시범을 보고 기록한 개발 자료 10개는 각 1회이며 모두 일치했다. 15fps와 7.5fps에서 같은 결과다. GIF는 실제 한 주기만 추출하고 1회 자료만 minSetReps=1로 검사했다. 기본 세트 기준 2회는 별도의 합성 두 주기 연속 검사로 확인했다.
- 규칙을 확정한 뒤 별도 사람의 밴드 페이스 풀 1회는 일치했지만, 한 다리 시티드 레그 컬 1회는 0회로 누락됐다. 결과로 규칙을 조정하지 않았으며 이번 시티드 레그 컬은 양다리 동작만 지원한다고 안내한다. 별도 결과·실패 자료도 정답표에 남긴다.
- 어브덕션은 실제로 보이는 양 무릎의 3D 간격을, 시티드 레그 컬은 화면 안 무릎·발목의 각도를 사용한다. 무릎·발목이 가려졌으면 준비 화면에서 안내한다. 케이블 레터럴은 한 팔의 몸 옆 방향을 확인해 프론트 레이즈와 구분하고, 페이스 풀은 팔꿈치를 굽힐 때 손이 높아지는 흐름으로 숄더 프레스의 반대 움직임과 구분한다. 팔/다리 가림·화면 밖 관절·정지 자세 잡음·운동 간 혼동을 회귀 검사했다.
- 그립·벤치·기구의 저항 방향은 포즈 33점만으로 확정할 수 없다. 같은 계열의 변형, 시티드 레그 컬/레그 익스텐션, 체스트 프레스/시티드 로우는 자유 운동 후보에 함께 넣어 이름을 추측하지 않는다. 루틴에서는 각 운동을 순서대로 직접 선택해 기록한다.
- 앱과 동일한 full 모델로 MediaPipe Python 0.10.21/CPU에서 좌표를 추출했다. 자료별 원본 URL·모델 SHA256·환경은 fixture에 기록했다. 원본은 StrengthLog 운동 시범과 PureGym의 체스트 프레스 시범이다. StrengthLog의 체스트 프레스 GIF는 머리 위로 미는 장면이라 개발 정답에서 제외하고 혼동 방지 검사에 사용했다. 원본 사진·영상은 앱에 포함하지 않는다.
- 소수 공개 시범의 결과이며 갤럭시 전면카메라의 체형·거리·조명·기구 가림별 실기 정확도는 아직 검증하지 않았다.
맨몸 운동 추가 검증 (2026-10-02)
- 새 맨몸 8종은 모두 직접 선택하는 베타다. 기존 자동 인식 후보를 늘리지 않으며, 버드 독·데드 버그는 반대 팔·다리 한 쌍마다 1회, 숄더 탭·리버스 런지는 한쪽마다 1회로 센다. 교차 팔다리 운동은 다리와 반대 팔의 움직임을 각각 확인한다. 무릎이 화면 밖이거나 팔이 가려졌으면 새 패턴의 추정 횟수를 만들지 않는다.
test/bodyweight-truth.json과 node test/bodyweight-eval.mjs로 덤벨·기존 자료와 분리해 평가한다. 원본 시범을 먼저 보고 센 정답은 버드 독 6회, 데드 버그·숄더 탭·리버스 런지 각 2회, 나머지 4종 각 1회다. 공개 시범 8개에서 모두 일치했고, 별도로 남겨 둔 데드 버그 영상도 4/4회였다. 15fps 좌표를 7.5fps로 줄인 검사에서도 9개 모두 일치했다. GIF는 실제 한 주기이며 1회 자료만 minSetReps=1로 평가했다.
- 앱의 동일한 full 모델로 MediaPipe Python 0.10.21/CPU에서 좌표를 추출했다. 원본은 StrengthLog, 숄더 탭 시범, Lyndhurst의 버드 독·데드 버그 시범이며 자료별 URL·모델 SHA256·환경은 fixture에 기록했다. 원본 영상·사진은 앱에 포함하지 않는다.
- 새 4개 패턴끼리의 혼동, 일반 푸시업·동키킥·컬·레그 레이즈를 새 운동으로 잘못 세는 경우, 가림·화면 밖 무릎·정지 관절 잡음을 검사했다. 푸시업과 교차 팔다리는 준비 화면에서도 팔과 하체를 함께 확인한다. 의자 스쿼트에는 의자보다 더 깊이 앉으라는 안내를 하지 않고, 인클라인 푸시업은 받침 쪽으로 내려가도록 안내한다.
- 소수 공개 시범에 대한 결과다. 실제 갤럭시 전면카메라의 체형·거리·조명·속도별 정확도는 실기에서 추가 확인해야 한다. 특히 교차 팔다리는 양팔이 잘 보이는 대각선 촬영을 권한다.
덤벨·근력 운동 추가 검증 (2026-10-02)
- 컬·로우·킥백은 양팔을 따로 추적한다. 양팔 동시 반복은 1회, 교대 반복은 한쪽마다 1회이며, 반대쪽 팔을 굽힌 채 쉬어도 움직이는 팔의 반복을 센다. 화면 밖 팔꿈치·손목과 낮은 presence는 팔 신호에 쓰지 않고, 0.6초 넘게 가리면 이전 팔 동작을 새 프레임에 연결하지 않는다.
- 직접 고른 덤벨 상체 운동은 어깨·팔꿈치·손목 구도를 안내한다. 무릎이 잘렸다는 이유로 불필요하게 멀어지도록 안내하지 않는다. 푸시업·하체·맨몸 코어 운동은 전신 안내를 유지한다.
- 기존 full-model 횟수 영상 49개는 정확 34개·±1회 이내 44개·다른 운동으로 오인식 0개로 이전 성적을 유지했다. 버티기 영상 3개도 ±3초 이내다. 편집·설명 구간을 포함한 전체 자료에는 기존 오인식 3개가 남아 있다.
- 초당 약 8장 검사도 정확 31개·±1회 이내 39개·오인식 1개로 이전 성적을 유지했다. 관절에 큰 잡음(±2cm)을 더한 검사는 정확 31개로 이전 32개보다 1개 줄었고, ±1회 이내는 44개로 같았다. 처음부터 손이 가려진 행잉 레그 레이즈를 보수적으로 처리하며 1회가 누락됐다. 충분한 실기 자료 없이 이를 추정 카운트로 보완하지 않는다.
- 새 자료는
test/strength-truth.json과 node test/strength-eval.mjs로 별도 평가한다. PureGym·StrengthLog 공개 시범의 원본 장면을 먼저 보고 정답을 정했다. 앱에 포함된 동일한 full 모델 파일로 MediaPipe Python 0.10.21/CPU에서 좌표를 추출했으며, 앱의 JavaScript/GPU 실행 환경과는 다르다. 자료별 원본 URL·모델 SHA256·추출 환경은 fixture에 남겼다.
재추출은 별도 Python 3.11 가상 환경에 tools/pose-requirements.txt를 설치한 뒤 python tools/extract-native.py 공개시범.mp4 --name 자료이름 --url 원본URL로 한다(15fps·VIDEO·CPU·각 confidence 0.5). 원본 영상과 장면 시트는 app/에 포함하지 않는다.
- 팔이 보이는 개발 자료 8개는 모두 정답과 일치했다(7개는 GIF 또는 짧은 구간의 1회, 덤벨 벤치 프레스는 5회). GIF는 반복하지 않고 실제 1주기만 평가한다. 이 평가에 한해
minSetReps=1을 사용하며, 앱의 기본 기록 기준 2회는 유지한다.
- 별도로 남겨 둔 컨센트레이션 컬 영상은 3회 중 2회를 셌다. 구간 시작 시 첫 동작이 진행 중이어서 1회가 누락됐다. 이 결과를 보고 규칙을 조정하지 않았다. 발 쪽에서 찍은 벤치 프레스 GIF는 포즈가 43프레임 중 5프레임만 검출돼 실제 1회 중 0회를 셌다. 이 실패 자료도 유지한다.
- 새 운동은 모두 베타다. 소수 자료로 실제 갤럭시 전면카메라 정확도를 보장할 수 없으며, 다양한 체형·거리·조명·반복 속도·잘못된 자세는 추가 실기 확인이 필요하다.
검증 (2026-10-01)
유튜브 운동 시범 영상 36개에서 앱과 똑같은 MediaPipe 코드로 관절 좌표를 뽑아(tools/extract.mjs, 헤드리스 크롬) 판단 엔진에 넣고,
영상 장면을 초당 2~3장씩 직접 보고 센 정답과 비교했다(test/truth.json, node test/eval.mjs).
| 구분 | 영상 | 결과 |
|---|
| 규칙을 맞출 때 안 본 영상 (푸시업 2·고블릿 스쿼트 1) | 3 | 3개 모두 정확 |
| 규칙을 맞출 때 본 영상 | 19 | 정확 15 · ±1회 이내 18 (평균 오차 0.37회) |
| 편집·다인원 영상(정답 대신 오작동만 확인) | 14 | 다른 운동으로 잘못 인식 0건 |
| 자세 교정 오경보 (올바른 시범 178회) | – | 지적 1회 (두 사람 나오는 편집 영상) |
| 앱 실제 실행(브라우저, GPU) | 3 | 바벨 스쿼트 6회·런지 5회·데드리프트 3회 정확, 25초 영상을 10초에 분석 |
- 2차 운동 17종(2026-09-30): 운동마다 시범 영상 1개로 규칙을 만들고(조정 영상 30개: 정확 22 · ±1 29), 규칙을 만들 때 안 본 두 번째 영상 12개로 시험했다.
처음 결과는 정확 5 · ±1 6 · 다른 운동으로 오인식 5개 영상 — 첫 영상에 너무 맞춘 규칙(특히 하이니가 사이드 런지·불가리안을 잡음, 무릎이 화면 밖인 푸시다운, 고개 든 바이시클)이었다.
원인을 고친 뒤 같은 12개에서 정확 7 · ±1 8 · 평균 오차 1.0회, 오인식 2개(기구에 앉았다 일어서기를 스쿼트로, 여러 동작이 섞인 하이니 영상). 이 12개도 이제 '본 영상'이라 진짜 처음 보는 영상 성적은 더 확인해야 한다
- 세 번째 영상 묶음(2026-10-01, 16개): 2차 운동 9종의 새 영상을 받아, 엔진 결과를 보기 전에 장면 시트(초당 2.5~10장)로 정답부터 셌다.
횟수를 셀 수 있는 7개 + 편집·다인원이라 오작동만 보는 9개.
- 처음 결과(진짜 처음 보는 영상 성적): 횟수 7개 중 정확 3(레그 익스텐션 3회·업라이트 로우 5회·행잉 레그 레이즈 5회), 16개 중 6개 영상에서 다른 운동으로 오인식
- 원인과 고친 것 (모두 실측 근거, 조정 영상 30개 성적은 그대로 정확 22 · ±1 29):
하이니는 0.33초마다 다리를 바꿔 '바꾸는 순간'(1프레임)이 중앙값 스무딩에 지워졌다 → 스무딩 전 엉덩이 각도로(20회 중 2회 → 20회) ·
동키킥은 쉬는 자세 엉덩이 각이 사람마다 달라(92
98° / 113118°) 절대값 대신 차올린 폭으로(0 → 3회 정확) ·
엎드린 레그 컬을 마운틴 클라이머로 봄 → 클라이머는 엉덩이도 접혀야(5682° vs 레그 컬 142159°) ·
매달린 사람 손이 화면 위로 잘리자 AI 가 지어낸 손목으로 체스트 플라이를 셈 → 플라이는 하체가 그대로여야 ·
45° 레그 프레스를 레그 레이즈·러시안 트위스트로 봄 → 바닥에 누운 몸통(88~100°)·발과 엉덩이 높이 차로 구분
- 고친 뒤 같은 16개: 횟수 7개 중 정확 5 · ±1 6, 오인식 1개 영상. 처음 보는 영상 전체(19개)는 정확 12 · ±1 15 · 평균 오차 0.84회
- 남은 것: 레그 프레스를 뒤에서 비스듬히 찍은 영상은 내려간 순간 무릎이 몸에 가려 AI 가 다리를 놓친다(0/4, 안내대로 옆에서 찍으면 된다) ·
레그 컬 기구에 엎드려 오래 설명하는 장면을 플랭크 6초로 기록(플랭크 시험 영상이 없어 규칙을 근거 없이 바꾸지 않음)
- 카메라를 가까이 둔 화면 (2026-10-18): 스쿼트 영상 4개를 무릎 바로 위에서 잘라(앉으면 엉덩이·무릎이 화면 밖) 앱과 같은 AI 로 관절을 다시 뽑았다(
test/videos5, 정답표 *_close, mode fixed).
고치기 전 엔진은 스쿼트를 골라도 4개 중 2개만 맞았다(정면 1/3·바벨 0/6) → 어깨 높이 보조 신호를 넣고 4개 모두 정확(3·3·6·3회), 원본 영상 성적·실사용 시뮬레이션은 그대로이고
'무릎 아래 잘림' 조건에서 운동을 고른 경우 정확 17 → 21. 실제 앱(가짜 카메라)으로도 루틴 스쿼트 6/6.
상체만 보이게 자른 업라이트 로우 영상은 고치기 전에도 5/5 를 셌지만 안내가 "너무 가까워요. 두세 걸음 뒤로"·테두리 주황이었다 → 고친 뒤 "좋아요. 이 자리에서 시작하세요".
자른 영상은 실제로 폰을 가까이 둔 것(원근이 더 셈)과 다를 수 있어 실기기 확인이 필요하다
- 새 특징값: 좌우 무릎 차·몸 기준 발 간격(좌우/앞뒤)·두 발목 높이 차·손목 3D 간격·어깨→손목 수평 거리·골반 대비 손의 좌우 위치·어깨선 기울기·가슴 방향(누움 +0.9 / 엎드림 -1.0, 고개를 들어도 안 틀어짐)
- 실사용 조건 시뮬레이션(정답 있는 22개,
test/robust.mjs): 그대로 18개 정확 → 폰이 25° 올려다보면 15개·25° 내려다보면 10개로 떨어지지만 기울기 센서 보정 후 18개(센서가 5° 틀려도 18개), 초당 8장 저사양 17개, 발목 잘림 16개, 무릎 잘림 7개
(세 번째 묶음 반영 후 정답 있는 49개: 그대로 정확 34 · ±1 44 · 오인식 0, 25° 기울어도 센서 보정 후 정확 34, 초당 8장 정확 31 — 하이니는 초당 8장이면 20회 중 14회)
- 인식 모델은 정확(full) 이 기본. 빠름(lite)은 같은 영상에서 9개 중 6개만 정확 + 오인식 2건 → 오래된 폰용으로만
- ±1 오차의 대부분은 영상이 올라온 자세에서 시작하거나 끝나는 첫/마지막 1회, 또는 편집 영상의 컷 전환 구간
- 한계: 검증 영상이 운동당 1~3개뿐이다. 실제 폰 카메라·다양한 체형으로 더 확인해야 하고, 틀리면 요약 화면에서 고치면 된다
- 올바른 자세에 잔소리하지 않는 것은 실측했지만, 잘못된 자세를 잡아내는지는 합성 데이터로만 확인했다(얕은 스쿼트 → "더 깊게")
한계 (카메라로 되는 것과 안 되는 것)
- 관절이 카메라에 보여야 한다 → 기구에 몸이 가려지는 머신 운동(레그 익스텐션·펙덱 등)은 정확도가 낮다
- 무게(kg)는 카메라가 모른다 → 운동 고르기 화면이나 요약 화면에서 입력
- 척추가 말리는지 같은 세밀한 자세는 관절 33점으로 판단 불가 (무릎 모임·깊이·엉덩이 처짐·가동범위·반동은 가능). 스마트폰 카메라 관절각 오차는 약 6° 수준
- 헬스장에서 다른 사람이 화면에 크게 들어오면 그 사람을 볼 수 있다 → 폰을 가까운 쪽 벽을 보게 두기
- 비슷한 운동(숄더 프레스↔랫풀다운 등)은 자동 인식보다 운동 골라서 시작이 확실하다
- 기구에 앉았다 일어서는 동작은 스쿼트처럼 보일 수 있다(2회 이어지면 스쿼트로 기록됨 → 요약 화면에서 지우거나 운동 골라서 시작)
- 레그 프레스처럼 기구에 다리가 가리는 운동은 관절이 자주 튄다(베타). 다리가 덜 가리는 쪽에서 찍기
- 무릎이 화면 밖이면 하체 운동은 셀 수 없다(시뮬레이션 22개 중 9개만) → 구도 안내를 따라 무릎까지 보이게
- 엎드려 가만히 있는 자세(레그 컬 기구에 엎드리기, 앞 대각선 위에서 본 엎드려 받친 자세)는 자동 인식이 플랭크로 셀 수 있다 → 그 운동을 골라서 시작하거나 플랭크는 옆에서 찍기
- 템포 m/s 는 AI 3D 좌표(미터 단위 추정)로 잰 어림값이다(바벨 속도계만큼 정확하지 않음). 같은 세트 안에서 빨라졌는지·느려졌는지 비교하는 용도로 보면 정확하다. 끝에서 천천히 멈추는 동작은 시간이 10~25% 짧게 나올 수 있다
구조
app/ 배포되는 웹앱 (정적 파일만)
index.html, css/, icons/, manifest.webmanifest, sw.js
js/main.js 화면 전환·홈·운동 고르기·요약·설정
js/workout.js 카메라/영상 → 포즈 → 추적기 → 음성·화면·세트 기록
js/history.js 기록 화면(주간 통계·부위별 세트·달력·추이 그래프·최고 기록)
js/stats.js 최고 기록·예상 1RM·새 기록·주간 부위별 세트·칼로리 어림값 (순수 계산)
js/health.js 운동 1회 → 헬스 커넥트 기록(세트 구간·세트 종류 번호·메모·칼로리) (순수 계산, 쓰기는 플러그인)
js/goal.js 하루 목표: 오늘 진행·연속 달성·남은 양으로 루틴·알림 일정 (순수 계산)
js/reminders.js 하루 목표 알림 걸기·지우기(@capacitor/local-notifications, 안드로이드 앱만)·배터리 최적화 상태
js/placement.js '폰은 어디에 둘까요?' 거리표 창 (홈·설정·운동 준비 화면 공용, 지금 카메라의 시야각으로)
privacy.html 개인정보 처리방침 (헬스 커넥트 권한 창에서도 연다)
js/voice.js 음성(하나 둘 셋…)·삐 소리
js/tilt.js 폰 기울기 센서 → 카메라 좌표의 '위쪽' (앞뒤 기울기만, iOS·가로 모드 부호 처리)
js/diag.js 진단 기록(관절 좌표만, 메모리에 최근 20분) → gzip 파일
js/camera.js 카메라 목록(방향·줌 범위로 광각 표시)·고른 카메라 열기·넓게 보기(4:3 + 줌 최소)
js/framing.js 구도 문제·너무 먼 거리 안내, 순간 가림을 거르는 표시 지연
js/split.js 부위별(가슴·등·어깨·이두·삼두…) 분할 루틴: 분할별 날·가장 오래 쉰 날 고르기·내 운동 안에서 짜기 (순수 계산)
js/routine.js 오늘의 루틴: 운동별 도구·세트·횟수 표, 루틴 짜기(부위·장소·시간·강도·최근 기록), 진행(PlanRunner)
js/store.js 기록·설정 저장(localStorage)
js/engine/ ★ 판단 엔진 (브라우저·Node 공용 순수 로직)
features.js 관절 → 특징값(관절각·엉덩이 높이·몸통 기울기…, 월드 좌표 미터)
filters.js 스무딩(중앙값+EMA)
counter.js 반복 카운터(히스테리시스 극값 검출, 적응형 진폭)
exercises.js 운동별 규칙표: 신호·진폭·판별 조건·자세 교정 (실측 근거 주석)
tracker.js 모든 운동 카운터 병렬 → 2회 연속이면 확정 → 세트 분리·플랭크·자세 지적 정책·기울기 보정
tempo.js 반복 1회를 힘주기/돌아오기 구간으로 나눠 시간·속도 (멈춘 시간·다른 동작은 빼고 이어진 움직임만)
vendor/mediapipe/ MediaPipe 엔진(wasm)·모델(full/lite) — 오프라인용으로 동봉
android/app/src/main/java/.../
HealthConnectPlugin.kt 헬스 커넥트 쓰기·지우기·권한·상태 (connect-client 1.1.0)
HealthPrivacyActivity.kt 헬스 커넥트가 여는 개인정보 처리방침 화면
CameraInfoPlugin.kt 카메라마다 시야각(렌즈 초점거리·센서 크기) — 넓게 보이는 카메라 고르기·거리 안내
SystemPlugin.kt 배터리 최적화(절전) 상태·'제한 없음' 설정 창 열기 — 하루 목표 알림이 제때 오게
VoicePlugin.kt 운동 음성: 말할 때 음악 줄이기(오디오 포커스)·숫자 소리 미리 만들어 바로 틀기(SoundPool)·목소리/빠르기
tools/
serve.mjs 의존성 없는 로컬 서버 (포트 8860)
extract.mjs/.html 영상 → 관절 좌표(test/fixtures) — 헤드리스 크롬에서 앱과 같은 코드로
features.mjs fixture 의 특징값 시계열 보기 (규칙 조정용)
replay.mjs 앱 진단 기록 재현: 정답(사용자가 고친 값) vs 재현 결과, 운동별 후보 반복과 탈락 이유, --save 로 테스트·정답표에 추가
camera-sim.mjs 카메라 모드 전체 점검: 시범 영상을 가짜 카메라로 헤드리스 크롬(폰 화면)에 넣어 안내·카운트·템포·요약·진단 기록→재현까지 (--edit N: 세트 수정→정답 확인)
make-icons.mjs 아이콘 생성
deploy-pages.sh app/ 만 GitHub Pages 로 배포
test/
unit/ 합성 스켈레톤으로 카운터·세트·자세 교정·템포·기울기, 루틴 짜기·진행, 기록 통계·하루 목표·카메라 거리 단위 테스트
eval.mjs, truth.json 실제 영상 채점 (정답표·조정/처음 본 영상 구분)
robust.mjs 실사용 조건 시뮬레이션: 폰 기울기·옆 기울기·저사양(8fps)·흔들림·발목/무릎 잘림
fixtures/ 영상에서 뽑은 관절 좌표 (영상 자체는 저작권 때문에 git 제외)
개발
npm test # 단위 테스트 (합성 데이터)
node test/eval.mjs --model full # 실제 영상 채점
node test/eval.mjs --debug curl_mccarthy # 한 영상의 반복 후보·탈락 사유
node test/strength-eval.mjs; node test/bodyweight-eval.mjs; node test/popular-eval.mjs # 덤벨·맨몸·인기 운동 정답표 (따로 채점)
node test/robust.mjs # 폰 기울기·몸 잘림 등 실사용 조건 시뮬레이션
node tools/replay.mjs ~/Downloads/handsfree-pt-diag-*.json.gz --why # 사용자 진단 기록 재현
node tools/camera-sim.mjs squat_mensgarage --shots /tmp/sim # 카메라 모드 끝까지 (--tilt 20: 기울기 센서 흉내)
node tools/camera-sim.mjs squat_mensgarage --plan 'squat:3x2:rest=5,lunge:5x1' # 오늘의 루틴(PT 모드) 진행 확인
node tools/features.mjs pushup_nasm elbow,torsoTilt,shoulderOverWrist
VIDEO_DIR=test/videos2 node tools/extract.mjs --model full # 새 영상 관절 좌표 뽑기
새 운동 추가 = exercises.js 에 규칙 한 줄 묶음 + 영상 넣고 extract → truth.json 에 정답 → eval 로 확인.
여럿이(Claude Code·Codex) 같은 main 에 바로 올리므로, 시작 전 pull·올리기 전 시험·파일 담당 같은 작업 규칙은 AGENTS.md.
앱의 설정 → 인식 과정 보기를 켜면 운동 중에 "왜 안 세졌는지"(탈락한 조건)가 화면에 나온다.