AIUI로 만든 Rokid 안경용 '음수 페이스 코치'
Rokid AI 안경에서 로컬로 음수량을 기록하는 스마트 에이전트를 AIUI로 구축한 사례가 소개됐다.
중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.
눈앞에 착용하는 음수 기록 도우미 — 한마디로 음수를 기록하고, 개인 목표 진행 상황을 확인하며, 언제든지 대조하고 취소할 수 있다.
1. 프로젝트 소개
「물 마시는 리듬 코치」는 Rokid AI 안경을 위한 로컬 음수 기록 에이전트다. 이 프로젝트가 해결하는 핵심 문제는 다음과 같다. 일상적인 업무, 학습 또는 통근 중에 사용자는 방금 마신 물 한 모금을 기록하려고 휴대폰을 꺼내기 불편한 경우가 많고, 음성 대화로 총량만 다시 말하는 것만으로는 "중복으로 한 번 더 기록한 것이 아닌지" 대조하기 어렵다는 점이다.
이 프로젝트는 「기록 — 확인 — 수정」을 항상 눈앞에 나타나는 하나의 화면으로 압축한다. 한마디로 한 건을 기록하면 페이지에 오늘의 누적량, 개인 목표 진행 상황, 최근 기록이 동기화되어 표시되고, 잘못 기록했으면 바로 취소할 수 있으며, 목표는 직접 조정할 수 있고, 데이터는 기기 로컬에만 저장된다.
목표 사용자 는 음성 상호작용에 익숙하고, 하던 일을 방해받지 않으면서 가볍고 신뢰할 수 있게 자신의 음수 리듬을 관리하기를 원하는 사람들이다.
프로젝트의 포지셔닝에는 세 가지 명확한 경계가 있으며, 이는 전 과정을 관통하는 설계 원칙이기도 하다.
- 건강 앱이 아니다 : 목표는 사용자가 스스로 설정한 기록 참고치일 뿐이며, 의학적 조언이 아니고, 목표량을 추천하지도 않고, 목표 달성을 재촉하지도 않고, 과도한 음수를 권장하지도 않는다.
- 요구를 추측하지 않는다 : 신장, 체중, 활동량, 날씨 또는 질병에 근거해 섭수 요구량을 산출하지 않으며, 이미지, 위치 또는 건강 센서 데이터도 수집하지 않는다.
- 데이터는 로컬에 있다 : 당일 기록과 개인 목표는 기기 로컬에만 저장되고 네트워크에 업로드되지 않으며, 날짜는 기기 로컬 날짜에 따라 전환된다.
2. 시나리오와 사용자 의도
2.1 사용자가 어떤 상황에서 호출하는가
전형적인 트리거 시나리오는 매우 짧다.
방금 물 한 컵을 마셨지만 하던 리듬을 끊고 싶지 않다 → "한 컵 기록해"라고 한마디 한다.
오후에 오늘 얼마나 마셨는지 확인하고 싶다 → "오늘 얼마나 마셨어?"
방금 한 건을 중복 기록한 것을 발견했다 → "방금 전 기록 취소해."
처음 사용해서 자신의 참고값을 설정하고 싶다 → "내 목표를 1600밀리리터로 설정해."
새로 시작하고 싶다 → "오늘 기록을 지워."
2.2 에이전트가 어떻게 이해하고 이어받는가
사용자의 표현은 보통 짧고 구어적이다. "한 컵 기록해", "방금 300밀리리터 마셨어", "큰 컵 하나 마셨어". AIUI는 이러한 표현들을 소량의 명확한 페이지 의도 와 구조화된 파라미터 로 매핑하고, 페이지는 다시 동작에 따라 결과를 표시한다.
구체적인 이어받기는 두 층으로 나뉜다.
1. 의도 매핑 : 자연어를 5가지 동작으로 수렴시킨다 — `status`(확인), `log`(기록), `undo`(취소), `set_goal`(목표 설정), `reset`(지우기) — 그리고 `amountMl`(음수량)과 `goalMl`(목표량) 두 파라미터를 추출한다.
2. 결과 시각화 : 페이지는 동작에 따라 총량, 진행률, 최근 기록 및 피드백을 즉시 갱신하여 "실행 결과가 보이고 수정 가능"하게 한다. 사용자는 고정된 명령어를 학습할 필요가 없다.
동시에 시스템은 "수량이 누락될 수 있는" 경우에 대해 명확한 기본 처리를 한다. 사용자가 수량을 말하지 않고 "한 컵 마셨어"라고만 하면 250 ml로 한 건 기록하고, 페이지에 기본값임을 명확히 표시한다.
3. 방안 설계
3.1 AIUI 에이전트 정의
에이전트는 단일 페이지 대화형 애플리케이션으로 구현되며, 핵심 개념은 연속 채팅이 아닌 " 의도 기반 인터페이스 "(Intent-driven UI)다. 안경에서의 주요 요구는 빠르게 한 건을 기록하고, 확인하고, 취소하는 것이지 긴 설명을 읽는 것이 아니므로 단일 페이지 구조를 채택한다.
두 개의 입력 경로가 동일한 상태에 기록된다.
- AIUI 호스트 음성 의도 라우팅 : 시스템이 음성을 구조화된 `query` 파라미터로 변환하여 페이지의 `onLoad(query)`로 전달한다.
- 페이지 내 음성 진입점 : 페이지가 자체 구축한 `SpeechRecognition`으로, "음성 버튼 탭 / 웨이크워드" 후에 인식하고 로컬에서 음수량을 파싱하는 데 사용된다.
이 두 경로는 서로 독립적이지만, 최종적으로는 모두 동일한 기록 처리 로직을 호출하여 누적량이不一致해지는 것을 방지한다.
3.2 페이지 / 카드 설계
페이지는 위에서 아래로 네 개의 영역으로 구성된다.
영역 내용 렌더링 방식 상단 정보 영역 당일 날짜, 오늘 기록된 총량 동적(A2UI) 중앙 상태 영역 개인 목표, 진행률 백분율, 구간 진행 바, 최근 두 건의 기록, 빈 상태 동적(A2UI) 중앙 안내 영역 음성 상태 / 미지원 안내 일반 컴포넌트 하단 조작 영역 한 컵 기록(250 ml), 취소, 목표 ±100, 오늘 지우기 일반 컴포넌트
왜 이렇게 분담했는가 : 고정적이고 빈도가 높은 조작(기록, 취소, 목표 조정)은 일반 AIUI `button` 컴포넌트를 사용해 상호작용이 안정적이고 동적 프로토콜이 필요 없다. AI 의도와 기록 데이터에 따라 내용이 바뀌는 결과 영역 (총량, 진행률, 기록 행, 피드백)은 A2UI로 동적으로 생성한다.
3.3 상태 흐름
초기 상태(기록 없음) │ 사용자가 "300밀리리터 기록해"라고 말함 / "한 컵 기록"을 누름 ▼ 기록 추가(appendRecord)─────────────────┐ │ │ ├─ 사용자가 "취소"라고 말함 → 마지막 한 건 제거(removeLatestRecord) ├─ 사용자가 "목표 1600으로 설정"이라고 말함 → goalMl 갱신(goalIsDemo=false) ├─ 사용자가 "지우기"라고 말함 → records=[](목표 유지) │ ▼ viewState 재계산 → A2UI 갱신 명령 생성 → context.write()로 화면 갱신
모든 동작은 동일한 commit(records, goalMl, goalIsDemo, feedback) 흐름을 거친다. 먼저 로컬에 저장하고, 그다음 뷰 상태를 재계산하고, 마지막으로 A2UI 명령을 생성해 갱신한다. 이는 "음성"과 "탭" 두 진입점의 상태가 항상 일치하도록 보장한다.
3.4 기능 호출 관계
LanguageModel (온디바이스 언어 모델): 페이지 내 음성 인식으로 텍스트를 얻었지만 규칙만으로 수량을 확정할 수 없을 때 음수량을 추정하는 데 사용하며, 사용할 수 없을 때는 parseAmountFromText 정규식 파싱으로 폴백한다.
SpeechRecognition (페이지 음성): 청취, 전사, 오류 및 타임아웃 처리를 담당한다.
wx.setStorageSync / getStorageSync : 목표와 당일 기록을 로컬에 영구 저장한다.
a2ui : 결과 영역을 동적으로 렌더링하고 갱신한다.
4. AIUI 구현
4.1 프로젝트 구조
喝水节奏教练/ ├── AGENTS.md # 에이전트 능력 및 경계 규약 ├── app.js # 앱 진입점(onLaunch) ├── app.json # 페이지 및 권한 선언 ├── package.json # 프로젝트 메타 정보 ├── README.md # 사용 및 구현 설명 └── pages/ └── index/ └── index.ink # AIUI 단일 파일 Page(유일한 페이지)
4.2 app.json: 하나의 페이지 + 최소 권한
app.json은 전체 화면 페이지 하나만 등록하고, 페이지의 음성 인식에 필요한 `RECORD_AUDIO` 권한을 선언합니다. 카메라나 위치 권한은 신청하지 않습니다.
{ "name" : "물 마시기 리듬 코치" , "description" : "자연어로 음수량을 기록하고, 개인 목표 진행 상황을 확인하며, 눈앞에서 즉시 점검하고 취소한다." , "pages" : [ "pages/index/index" ] , "permissions" : [ "RECORD_AUDIO" ] , "window" : { "navigationBarTitleText" : "물 마시기 리듬 코치" } }
4.3 페이지 Schema: 자연어 설명 + 구조화된 의도
index.ink의 <script def>는 페이지의 자연어 설명과 JSON Schema를 선언합니다. Schema에는 5가지 동작 유형과 두 개의 수치 파라미터가 정의되어 있습니다.
{ "action" : { "enum" : [ "status" , "log" , "undo" , "set_goal" , "reset" ] } , "amountMl" : { "type" : "integer" , "minimum" : 1 , "maximum" : 5000 } , "goalMl" : { "type" : "integer" , "minimum" : 100 , "maximum" : 10000 } }
amountMl은 "이번 음수량"에 사용되며, 주어지지 않으면 비워 두고 페이지 기본값은 250입니다. goalMl은 set_goal 동작에서만 사용됩니다.
4.4 상태 핵심: 순수 함수 상태 머신
뷰 상태는 makeViewState(records, goalMl, goalIsDemo, feedback, storageAvailable)로 통일하여 계산하며, 총량, 목표, 진행 백분율, 최근 두 건의 기록, 기록 수, 피드백 문구, 저장 상태를 산출합니다. 기록의 추가/취소는 순수 함수입니다.
function appendRecord ( records, amountMl, source ) { return records. concat ([{ amountMl : Math . round ( Number (amountMl)), source }]); } function removeLatestRecord ( records ) { if (!records. length ) return { records, removed : null }; return { records : records. slice ( 0 , - 1 ), removed : records[records. length - 1 ] }; }
4.5 A2UI: 동적 렌더링 결과 영역
첫 렌더링 시, 페이지는 createSurface와 초기 updateComponents 지시를 직렬화한 문자열을 <a2ui commands="{{initialCommands}}">에 전달합니다. 이후 상태가 바뀔 때마다 a2ui.createA2UIContext('hydration-dashboard')로 컴포넌트 컨텍스트를 얻고, context.write()로 업데이트 지시를 푸시합니다.
function initialA2UICommands ( view ) { return JSON . stringify ([ { version : 'v0.9' , createSurface : { surfaceId : 'main' } }, { version : 'v0.9' , updateComponents : { surfaceId : 'main' , components : buildA2UIComponents (view) } } ]); } flushA2UI ( ) { if (! this . a2uiReady || ! this . pendingA2UI ) return ; const context = a2ui. createA2UIContext ( 'hydration-dashboard' ); context. write ( this . pendingA2UI ); this . pendingA2UI = null ; }
컴포넌트 트리는 기록 상태에 따라 빈 상태 또는 여러 개의 기록 행을 동적으로 생성하며, 새 데이터는 A2UI 페이지 내용만 갱신하고 동적 JavaScript 는 실행하지 않습니다(보안 경계).
4.6 로컬 저장과 날짜 격리
날짜 키로 매일의 기록을 격리하고, 개인 목표는 날짜가 바뀌어도 유지됩니다. 저장을 사용할 수 없을 때도 페이지는 현재 세션 내에서 동작할 수 있으며, 피드백에 "이번에 연 동안에만 유효함"을 안내합니다.
function localDayKey ( ) { return new Date (). toDateString (); } function normalizeSavedRecords ( value, today ) { if (!value || typeof value !== 'object' || value. dayKey !== today || ! Array . isArray (value. records )) return []; return value. records . filter ( ( r ) => r && Number . isFinite ( Number (r. amountMl )) && Number (r. amountMl ) > 0 ) . map ( ( r ) => ({ amountMl : Number (r. amountMl ), source : String (r. source || '기록' ) })); }
onShow에서 날짜 변화를 감지하며, 날짜가 넘어갔으면 당일 기록을 새로 시작하고 목표는 유지합니다.
4.7 수량 지능형 파싱: LLM + 규칙 이중 안전장치
페이지 내 음성 인식으로 텍스트를 얻은 뒤, 먼저 온디바이스 LanguageModel로 음수량을 추정하고, 실패하거나 결과가 유효하지 않으면 정규식 규칙으로 폴백한다("300 밀리리터", "두 잔", "큰 잔 하나", "반 병" 등의 구어 표현 지원).
async handleTranscript ( transcript ) { this . setData ({ voiceState : 'processing' , feedback : '解析中…' }); let amount = await this . parseAmountWithLLM (transcript); if (amount === null ) amount = parseAmountFromText (transcript); if (amount !== null && amount > 0 && amount <= 5000 ) { this . addWater (amount, '语音记录' ); this . endVoiceSession ( `已识别" ${transcript} ",记录 ${ Math .round(amount)} 毫升。` ); } else { this . endVoiceSession ( `听到" ${transcript} ",未识别出饮水量。可说"300 毫升"或"一杯"。` ); } }
5. 상호작용과 경험
5.1 주요 화면
- 기록이 있을 때 : "기록됨 X ml", 개인 목표("내 목표" 또는 "데모 목표 · 조정 가능" 표기), 진행률 퍼센트, 10단계 진행 바, 그리고 최근 두 건의 기록을 표시한다.
- 기록이 없을 때 : 독립적인 빈 상태 안내 "오늘은 아직 기록이 없습니다. 음수량을 말하면 여기에 기록이 생성됩니다."를 표시한다.
- 음성 상태 : 듣는 중(맥동하는 점)/ 파싱 중 / 음성 미지원, 모두 명확한 텍스트 피드백이 있다.
5.2 조작 경로
조작 음성 탭 / 키 기록 한 잔 "한 잔 기록" 「한 잔 기록」 버튼(250 ml) / Enter 정량 기록 "300 밀리리터 기록" 음성 진입점 + 텍스트 파싱 실행 취소 "직전 기록 취소" 「취소」 버튼 목표 설정 "1600 밀리리터로 설정" 「−100 / +100」 버튼 비우기 "오늘 기록 비우기" 「오늘 비우기」 버튼
페이지는 또한 방향키로 선택 이동, Enter로 활성화를 지원하여 터치/음성이 불편한 상황에서도 조작을 완료할 수 있다.
5.3 피드백 메커니즘
매 동작 후 즉시 결과를 피드백한다. 예: "300 밀리리터 기록됨; 언제든지 취소할 수 있습니다".
기본값 투명화: 수량을 주지 않았을 때 "기본 한 잔 250 밀리리터로 기록했습니다"라고 피드백한다.
목표 경계 투명화: 피드백에서 "그것은 단지 당신의 기록 참고일 뿐, 건강 조언이 아닙니다"라고 명확히 밝힌다.
저장 상태 투명화: "이 기기에 저장됨" 또는 "이번 실행에만 유효"를 표시한다.
5.4 가독성 디자인 (AR 안경의 좁은 시야 지향)
- 단색 (녹색 `#40ff5e`) + 가는 구분선 + 짧은 상태 텍스트를 사용하고 색의 의미에 의존하지 않는다.
- 상태를 텍스트, 숫자, 구간선, 기호로 동시에 표현하여 색약 사용자도 식별할 수 있다.
- 밝은 조작 경계, 압축된 글자 크기로 안경의 좁은 시야와 투명 디스플레이에 적합하게 맞춘다.
6. 디버깅과 검증
6.1 AIUI Studio 미리보기
AIUI Studio 중국 사이트 에서 "로컬 가져오기"로 프로젝트 폴더를 불러오고, 기기 시뮬레이션 미리보기에서 하나씩 말해 본다:
"오늘 얼마나 마셨어?"
"물 300 밀리리터 기록해."
"나 방금 한 잔 마셨어." (250 ml 기본값으로 기록하라는 안내가 나와야 함)
"직전 기록 취소해."
"내 목표를 1600 밀리리터로 설정해."
"오늘 기록 비워."
동시에 페이지의 「한 잔 기록」「취소」와 목표 조정 버튼을 누르며 버튼과 음성이 동일한 상태에 쓰는지, 피드백이 동기화되는지 검증한다.
6.2 핵심 검증 지점
- 구조화 파라미터 : `action` / `amountMl` / `goalMl`이 `onLoad(query)`에 올바르게 반영되는지 확인.
- A2UI 갱신 : 기록/취소/목표 조정 후 총량, 진행률, 기록 행, 피드백이 즉시 갱신되는지.
- 날짜 경계 동작 : 기기 날짜를 변경하여 당일 기록이 다시 시작되는지, 개인 목표가 유지되는지 확인.
- 저장 예외 : `wx` 저장 실패를 시뮬레이션하여 페이지가 여전히 사용 가능하고 "이번 실행에만 유효"를 안내하는지 확인.
- 수량 파싱 경계 : 구어 "두 잔" "큰 잔 하나" "반 병"과 순수 숫자 "300"의 파싱 결과가 예상에 부합하는지.
7. 성과와 회고
7.1 실행 가능한 성과
AIUI Studio에 가져올 수 있고 시뮬레이션 미리보기가 가능한 완전한 AIUI 단일 페이지 에이전트 프로젝트.
5종 음성 인텐트 + 5종 페이지 조작을 지원하며, 음성과 탭이 동일한 상태 로직을 공유한다.
A2UI가 총량, 진행률, 최근 기록과 피드백을 동적 렌더링; 목표와 당일 기록을 로컬에 영속화; 날짜가 바뀌면 기록은 자동 초기화되고 목표는 유지된다.
7.2 겪은 문제와 해결 방안
문제 해결 방안 사용자가 "한 잔 마셨다"고 말하며 수량을 주지 않아 기록할 수 없음 250 ml 기본값을 명확히 하고 피드백과 설명에서 투명하게 밝힘 잘못 기록한 뒤 복구가 어려움 각 건의 수량과 출처를 저장하고, 취소는 마지막 한 건만 삭제하며 하루치 전체를 초기화할 필요 없음 목표가 건강 조언으로 오해되기 쉬움 목표를 "사용자 자체 설정 / 데모 값"으로 표시하고, 자동 추정하지 않으며 달성을 재촉하지 않음 실기기 로컬 저장이 제한될 수 있음 wx 읽기/쓰기에 예외 처리를 추가; 실패 시 "이번 실행에만 유효"를 명확히 안내 음성과 탭이 각자 상태를 유지해 누적량이 불일치함 두 진입점이 동일한 기록 상태와 추가/취소 보조 로직을 공유 페이지 내 음성이 구어 수량 파싱이 부정확함 LLM 추정 + 정규식 규칙 이중 안전장치, LLM 사용 불가 시 우아하게 폴백 AR 안경의 좁은 시야에서 가독성이 떨어짐 단색 + 구간선 + 짧은 텍스트 + 밝은 경계, 색의 의미에 의존하지 않음
사용자07190671017
1
글
0
읽음
팬