ð æŠèŠ
ã¢ã¯ã·ã§ã³ãšäž»èŠ KPI ã®ã¹ãããã·ã§ããã詳现ã¯å·Šã¡ãã¥ãŒããåã¿ããžã
ðš ä»ããã¹ãããš â ã¢ã¯ã·ã§ã³ãå¿ èŠãïŒ
ð ã¹ãããã·ã§ãã
ð° ããžãã¹
æ¬ãªãªãŒã¹åãªã®ã§çŸç¶ã®æ°å€ã¯ãã¹ãããŒã¿ã§ããããã§ã¯ã·ããªãªèšç® (preset = æ²èг / æšæº / 楜芳) ã§èšç»å€ã確èªããŸãã
çŸç¶ã®å®çžŸã¯ æè¡ / ã³ã¹ã ã¿ãã§åç
§ã§ããŸãã
ð¯ ãã¡ãã« EV ã·ãã¥ã¬ãŒã¿ â Trial1 (14æ¥) â Paywall â Trial2 (14æ¥) â èªå課é
æ°ãã㌠(Free ãã©ã³ãªã): install â
Trial 1 (14æ¥ç¡æã»CCäžèŠ) â Paywall â
Trial 2 (14æ¥ç¡æã»CCãã) â
èªå課é â ææ¬¡ churn ã§æžè¡°
æä¹
ç¡æãŠãŒã¶ãŒã¯ååšããªããTrial 1 ã®ã³ã¹ãã¯å
š install åãèªç€Ÿè² æ
ã
ã¬ããŒ: äŸ¡æ Œèšèš (æé¡ / å¹Žé¡æ¯ç / 幎é¡å²åŒ) à Organic æ¯ç à Funnel å質 à ã¹ããŒãžå¥ CAC/installã
ð€ ã¹ããŒãžã¿ã€ã ã©ã€ã³ â Pre-Seed 6ã¶æ â Seed 6ã¶æ â Series-A 24ã¶æ (åèš 36ã¶æ)
ð ã¹ããŒãžå¥ ã¡ããªã¯ã¹ â åã funnel å質ã§ãã¹ããŒãžæ¯ã®çµæžæ§
| ã¹ããŒãž | CAC | install/æ | å®å¹ CAC / paying |
LTV / CAC | Payback | æ°èŠ paying / æ |
å®åžž ææ¬¡ OP |
|---|
ð 36ã¶æåçã·ãã¥ã¬ãŒã·ã§ã³ â ã¹ããŒãžé·ç§»ä»ããäœã¶æã§é»ååïŒ
åæ: ã¹ããŒãžããšã« install / CAC ãåãæ¿ãããTrial1 (14æ¥ å šå¡) + Trial2 (14æ¥ acceptors) ã®ç¿æãã課ééå§ãææ¬¡ churn ã§ active ãæžè¡°ã
ð ãããã¯ã
ãŠãŒã¶ãŒã®å¢æžãå®çãAI 粟床ããã£ãŒãããã¯ã®éçŽã
ð æé· â ãŠãŒã¶ãŒã¯å¢ããå®çããŠãããïŒ
ð çŽè¿14æ¥ã®æ°èŠç»é²
ð ãªãã³ã·ã§ã³ (Day1/7/30)
| ã³ããŒã | ãµã³ãã«æ° | æ®å | æ®åç |
|---|
N æ¥åã«ç»é²ãã人ã®ãã¡ãä»ã掻åããŠããå²åããµã³ãã«æ°ãå°ãªããšæ°å€ã¯ãã€ãºãå€ãã
ð¥ 補å KPI â AI 粟床ã»ã³ã¹ãæé©åã¯ã©ããïŒ
ð LLM åŒã³åºã caller æ¯ç (30d)
â ïž ã«ããªãŒèª€ãå ±å â ãŠãŒã¶ãŒããããããããããšæãã meal
| æå» | ãŠãŒã¶ãŒ | æçå | ç»é² kcal | lookup_key / meal_id |
|---|
ð¬ ãŠãŒã¶ãŒãã£ãŒããã㯠â ãŠãŒã¶ãŒã¯äœãæ±ããŠããïŒ
| æå» | ãŠãŒã¶ãŒ | å 容 |
|---|
âïž æè¡ / ã³ã¹ã
ã¬ã€ãã³ã·ããšã©ãŒãã³ã¹ãå èš³ã
𩺠æè¡ health â ãŠãŒã¶ãŒã¯äœç§åŸ ããããŠãããïŒ ãšã©ãŒã¯ïŒ
ð¯ 1次å¿çã¬ã€ãã³ã· (ãã€ã¯é¢ã â å¹ãåºã衚瀺)
ð¯ ã«ããªãŒåæ ã¬ã€ãã³ã· (å±¥æŽã«æçµkcal衚瀺ãŸã§)
ð 1次å¿çã®å èš³: ã©ãã«æéãããã£ãŠããã (7då¹³å)
â± è£å©: caller å¥ LLM åäœã¬ã€ãã³ã· (7d, OpenRouter API éšåã®ã¿)
| caller | n | p50 | p95 | p99 |
|---|
ð çŽè¿ã®ãšã©ãŒ (7d, æå€§20ä»¶)
| æå» | caller | error |
|---|
ðž ã³ã¹ã詳现 â ã©ãã«éãããã£ãŠããïŒ
ð æ¥æ¬¡ã³ã¹ãæšç§» (30d, OpenRouter ã®ã¿)
ð€ ã³ã¹ãäžäœãŠãŒã¶ãŒ (30d)
| user | plan | calls | cost |
|---|
ð caller à model å¥ (30d)
| caller | model | calls | cost |
|---|
ð€ Soniox æ¥æ¬¡ (30d, /v1/usage-logs ãã)
| æ¥ä» | åæ° | é³å£°ç§ | ã³ã¹ã |
|---|
ð LP αçãã¹ã Waitlist
ã¡ã¢ãç»é²ãæµå ¥å ãé ä¿¡åæ¢ç¶æ ãåå¥/äžæ¬ã¡ãŒã«éä¿¡ã
| æå» (JST) | Source | ç¶æ | éä¿¡å±¥æŽ | Referrer |
|---|
ð§ ã¡ãŒã«éä¿¡
ãã³ãã¬ãŒããéžã³ãå¿
èŠãªãã£ãŒã«ããå
¥åããŠéä¿¡ããŸããæ¬æã¯ç·šéã§ããŸãã(ãã³ãã¬ç®¡ç㯠functions/_lib/templates.ts)ã
ð ã¡ãŒã«ãã¬ãã¥ãŒ
å®éã«éä¿¡ãããå 容ã§ããéä¿¡åã®æçµç¢ºèªã«äœ¿ã£ãŠãã ãã (詊éã¯ãããŸãã)ã
ð ã¢ãŒããã¯ã㣠ã¬ãã¥ãŒ
7 芳ç¹ã§ shiwake ã®çŸç¶ãæŽçããèšèšãªãã¡ã¬ã³ã¹ã
åã»ã¯ã·ã§ã³ã¯ãð çŸç¶ / â
匷㿠/ â ïž æžå¿µ / ð TODOãã® 4 ãµãæ§é ã§çµ±äžã
æŽæ°ã¯ admin/index.html ã® data-tab="architecture" ã»ã¯ã·ã§ã³ã
ðº 1. ã·ã¹ãã å šäœããã â 1 æã§é ã«å ¥ããïŒ
ð çŸç¶
SwiftUI + Keychain"] Backend["âïž shiwake-daily-api
Hono on Workers"] D1B[("ðŸ D1: shiwake_daily")] R2[("𪣠R2: media")] Soniox{{"ð€ Soniox
STT realtime + async"}} OR{{"ð€ OpenRouter
Gemini 3 Flash / GPT-4o-mini"}} Apple{{"ð Apple
AppAttest + StoreKit 2"}} Weather{{"ð€ Open-Meteo
weather"}} LP["ð LP shiwake-lp(-stg)
Pages + Functions"] D1L[("ðŸ D1: shiwake-lp")] SMTP{{"ð§ ãåå.com SMTP"}} Admin["ð shiwake-admin
Pages SPA"] iOS -->|"Bearer access_token"| Backend iOS -.->|"WS stt-rt-v4 (temp key)"| Soniox iOS -->|"AppAttest assertion"| Backend iOS -->|"StoreKit JWS"| Backend Backend --> D1B Backend --> R2 Backend -->|"chat/completions"| OR Backend -->|"async stt"| Soniox Backend -->|"verify attest + JWS"| Apple Backend --> Weather Backend -->|"S2S notif"| Apple LP --> D1L LP -->|"SMTP 465"| SMTP Admin -->|"Basic auth"| Backend Admin -->|"Basic auth"| LP
â 匷ã¿
- Cloudflare ã«å¯ããã£ãåäžãã©ãããã©ãŒã (Workers + Pages + D1 + R2) ã§éçšé¢ã¯ã·ã³ãã«
- iOS â å€éš API ãžã®çŽæ¥åŒã³åºããæå°å (Soniox WS ã®ã¿ãtemp key çµç±)
- backend / lp / admin ã® 3 ãªãç圹å²ãæç¢º
â ïž æžå¿µ
- Backend åäžç°å¢: backend 㯠stg/prod ãåãããŠããªã (LP ã¯åãããŠãã)ãæ¬çªãããã€ã§å£ãããå³åœ±é¿
- Cloudflare é害æã®ä»£æ¿è·¯ãªã: D1 / Workers ãšãã«å瀟
ð TODO
- backend ã« shiwake-daily-api-stg ãåã (D1 ãå¥ instance)
- D1 ã®å®æã¹ãããã·ã§ããéçš
ð 2. äž»èŠ user flow â ãããŒã®åçŽæ§ã責åã®æç¢ºã
ð çŸç¶: ãçºè©± â ã«ããªãŒèšé²ã (äž»èŠ critical path)
â meal_nutrition_cache ã«ä¿å API->>D1: UPDATE meals.kcal (åŸè¿œã)
ð äž»èŠãããŒäžèЧ (æ¬ããŒãžã§å®æã¡ã³ããã¹ã)
- çºè©± â ã«ããªãŒèšé² (äžå³)
- äœéèšé²: POST /api/weights â D1 (LLM äžèŠ)
- å±¥æŽã®ä¿®æ£: PUT /api/meals/:id, /api/meals/:id/rename
- ãµããªè¡šç€º: GET /api/summary/weekly (éèšã¯ãšãª)
- ãµã€ã³ã¢ãã: AppAttest challenge â æ€èšŒ â users è¡äœæ â access_token è¿åŽ
- IAP 賌èª: StoreKit â JWS æœåº â /api/iap/verify â users.subscription_status æŽæ°
â 匷ã¿
- 1 次å¿ç㯠LLM 1 å (Gemini Flash) ã§åž°ããã«ããªãŒæ°å€ã¯ BG ã§åŸè¿œã (UI ã¯ä»®å€è¡šç€º â kcal 確å®ã§å·®ãæ¿ã)
- åå²ã®å°ãªã sequential flow ã§èŠ³æž¬ãããã
â ïž æžå¿µ
- BG ã¿ã¹ã¯å€±æã®å¯èŠåã匱ã: mealLookup 倱ææããŠãŒã¶ãŒã«ã¯èŠãã«ãã (kcal ããâãã®ãŸãŸæ®ã)
- WS ã®æ¥ç¶å€±æãã©ãŒã«ããã¯ãªã: Soniox WS ãèœã¡ãæãasync REST ãžã®ãã©ãŒã«ããã¯ã¯æªå®è£
ð TODO
- åãããŒã 1 ã»ã¯ã·ã§ã³ãã€å³åŒå (äœé/å±¥æŽç·šé/ãµããª/IAP)
- 倱æãã¹ã®è¿œå (Soniox åæãOpenRouter timeout)
ðŸ 3. ããŒã¿ã¢ãã« â æ£èŠåãš PII ã®å±åš
ð çŸç¶
D1 = SQLiteãJOIN ã¯äœ¿ããåæ£ã¯æèããªã (rows-per-account ãæ¥µå°)ã
| ããŒãã« | çšé | PII? |
|---|---|---|
users | account + profile + affinity + sub status + AppAttest signup | â åå/ã¡ã¢ã/èªçæ¥ |
meals | é£äºèšé² (transcript, name, kcal, lookup_key, photo_key) | â é£äº photo |
weights | äœéèšé² | â äœéå€ |
exercises | éåèšé² | â |
notes | ãã®ä»ã¡ã¢ | â |
meal_nutrition_cache | æ£èŠå kcal cache (30d TTLæ³å®) | â |
user_facts | assistant åŠç¿æžã¿äºå® (奜ã¿/å¶çŽ) | â |
conversation_log | çŽè¿ N ã¿ãŒã³ (ingest context) | â |
weather_cache | Open-Meteo 1h cache | â |
api_usage_log | OpenRouter/Soniox call ledger | â |
request_timing | per-request latency milestone | â |
feedback | é³å£°ãã£ãŒããã㯠transcript | â |
attest_challenges | çæ AppAttest nonce | â |
invitations | legacy æåŸ ã³ãŒã redeem | â |
waitlist (lp) | α ãã¹ã ã¡ã¢ã + unsub token | â ã¡ã¢ã |
email_log (lp) | éä¿¡å±¥æŽ | â ã¡ã¢ã |
â 匷ã¿
- PII ã
users+ åãã°ç³»ã«éäžãç©çåé€ 1 ãŠãŒã¶ãŒ = ã«ã©ã åé€ã§å®çµ (GDPR æ³å®å®¹æ) - cache ç³» (meal_nutrition_cache, weather_cache) 㯠user_id ãæããªãã®ã§åé€åœ±é¿ãªã
â ïž æžå¿µ
- åé€ã«ã¹ã±ãŒãæªå®è£ : ãŠãŒã¶ãŒå逿ã®åããŒãã«å逿é ãã³ãŒãåãããŠããªã
- conversation_log ã® TTL ãªã: ç¡å¶éã«äŒžã³ãå¯èœæ§ â ãµã€ãºç£èŠãå¿ èŠ
ð TODO
- åããŒãã«ã«ä¿ææéããªã·ãŒ (äŸ: meal_nutrition_cache=30d, conversation_log=ææ° 50 turns, request_timing=90d)
DELETE /api/meãšã³ããã€ã³ã + å šããŒãã« cleanup ã¹ã¯ãªãã
ð 4. ã»ãã¥ãªãã£ã»èªèšŒ â æ»æé¢ãææ¡ã§ããŠããã
ð çŸç¶
| ã¬ã€ã€ | æ¹åŒ | ã¬ãŒã |
|---|---|---|
| iOS â backend | Authorization: Bearer <access_token> | requireUser ã D1 ã® users.access_token ãçŽæ¥ lookup (opaque random) |
| ãµã€ã³ã¢ãã | Apple AppAttest | backend/src/appattest.ts: cert chain + nonce + bundle/team æ€èšŒ (node-app-attest) |
| iOS â Soniox WS | backend çµç±ã§ temp key çºè¡ | /api/auth/soniox-temp-key (èŠ user èªèšŒ) |
| admin /api/admin/* | HTTP Basic admin:ADMIN_PASSWORD | middleware over /api/admin/* |
| IAP S2S notif | JWS payload ã decode | â ïž çœ²åæ€èšŒã¯æªå®è£ (TODO) |
| LP /api/waitlist | ç¡èªèšŒ (å ¬éãã©ãŒã ) | email regex + ip hash + éè€æ€ç¥ã®ã¿ãrate limit ãªã |
| LP /api/unsubscribe | token çµç± (RFC 8058) | 16 byte ã©ã³ãã token |
â 匷ã¿
- iOS ãµã€ã³ã¢ããã« AppAttest ãæ¡çš â åœç«¯æ«ããã®èªåã¢ã«ãŠã³ã倧éäœæã鲿¢
- access_token 㯠opaque random (JWT 鵿ŒæŽ©ãªã¹ã¯ãªã)ãrevoke 㯠DB æŽæ° 1 è¡
- Soniox èªèšŒæ å ±ã iOS ãã³ãã«ã«åã蟌ãŸãªã (temp key æ¹åŒ)
â ïž æžå¿µ
- IAP JWS çœ²åæªæ€èšŒ â åœ receipt ã§ premium åãããå¯èœæ§ (èŽåœç)
- LP waitlist rate limit ãªã â SPAM / å«ãããç»é²ã«ãã D1 å§è¿« + ã¡ã¢ãæ±æ
- access_token ã®ããŒããŒã·ã§ã³ãªã â 端æ«çŽå€±æã«æåã§ DB æŽæ°ãå¿ èŠ
- admin Basic auth ãã·ã³ã°ã«ãã¹ã¯ãŒã â å ±æäºæ æã«ããŒãå¿ é
ð TODO
- iap.ts ã« x5c chain æ€èšŒ + issuer/bundleId/environment æ€èšŒãå ¥ãã (çŸç¶ skeleton)
- LP waitlist ã« Cloudflare Turnstile or IP-based rate limit
- access_token ã«æå¹æé + refresh ã®æ€èš
- æ°èŠãšã³ããã€ã³ãè¿œå æã®ã»ãã¥ãªã㣠ãã§ãã¯ãªã¹ãå
â¡ 5. ããã©ãŒãã³ã¹ & ã³ã¹ã â ããã«ããã¯ã¯ç¹å®æžã¿ã
ð çŸç¶: 1 次å¿çã¬ã€ãã³ã·äºç®
| step | ç®æš p95 | åè |
|---|---|---|
| Soniox WS ç¢ºå® (çºè©±çµäº â æçµ transcript) | ~1.5s | realtime stt-rt-v4 |
| ingest (Gemini 3 Flash) | ~1.5s | intent + entity æœåº |
| persona (GPT-4o-mini) | ~0.8s | å¿çã¡ãã»ãŒãžçæ |
| åèš (1 次å¿ç) | ~3-4s | å±¥æŽ UI ã«å¹ãåºã衚瀺 |
| mealLookup (BG, web search) | ~6-10s | UI 㯠kcal ãâãâ 確å®ã§å·®ãæ¿ã |
ð çŸç¶: 1 active user / æ¥ ã³ã¹ãåŒ
10 çºè©±/æ¥ Ã 10 ç§ Ã (Soniox $0.0025/ç§ + Gemini Flash + GPT-4o-mini) à cache HIT ç (mealLookup 㯠HIT ã§ 0)
= çŽ Â¥2-3/æ¥ (cache HIT 60%+ æ³å®) / Â¥4-5/æ¥ (HIT 30% æ³å®)
宿ž¬ã¯ æè¡ / ã³ã¹ã ã¿ãåç §ã
â 匷ã¿
- ã¯ãªãã£ã«ã«ãã¹ã¯ LLM 1 段 (Gemini Flash) â ã³ã¹ã/ã¬ã€ãã³ã·äºæž¬ãããã
- mealLookup 㯠web search ã䌎ãã®ã§ BG åãUI ãåŸ ãããªã
- meal_nutrition_cache ã§ååæçã¯ç§ã§ kcal ç¢ºå® (LLM ã³ã¹ã 0)
â ïž æžå¿µ
- cache HIT çãã³ã¹ãæ§é ãæ¯é ãHIT ãäžãããš OpenRouter ã³ã¹ããç·åœ¢ã«äžæ
- persona ã®å¿ççæãé ããªããš 1 次å¿çãäœææªå (çŸç¶ GPT-4o-mini ãçªç¶é ããªããªã¹ã¯)
- Soniox WS ã®ãããã¯ãŒã¯æ¡ä»¶äŸåæ§ã倧 (å°äžéç)
ð TODO
- cache HIT çã®èŠå ±éŸå€ (e.g. 7d å¹³å 50% 以äžã§ alert)
- persona ã Gemini Flash ã«çµ±äžããéžæè¢ã®è©äŸ¡
- ãªãã©ã€ã³é²é³ â åŸéä¿¡ã¢ãŒã (Soniox äžå®å®æ)
ð 6. å€éšäŸåãªã¹ã¯ â çè¶³ãæãã³ã¹ããèŠããã
ð çŸç¶
| äŸåå | çšé | ããã¯ã€ã³ | ä»£æ¿ | å€äžãèæ§ |
|---|---|---|---|---|
| Cloudflare | Workers + Pages + D1 + R2 | ðŽ é« (D1 ç§»è¡ = SQLite ãã³ã + åæ§ç¯) | Fly.io + libSQL, AWS Lambda + DynamoDB | ð¢ 倧 |
| Soniox | realtime + async STT | ð¡ äž (WS ãããã³ã« + temp key API) | Deepgram, AssemblyAI, OpenAI Whisper API | ð¡ äž |
| OpenRouter | Gemini 3 Flash + GPT-4o-mini ã«ãŒã¿ | ð¢ äœ (Anthropic/OpenAI/Google çŽå©ãã«åæ¿å®¹æ) | å LLM ãã³ããŒçŽæ¥ | ð¢ 倧 (ãã¹ã¹ã«ãŒ) |
| Apple AppAttest | ãµã€ã³ã¢ããé²åŸ¡ | ðŽ é« (iOS ã®éžæè¢ãšããŠã¯å¯äž) | ãªã | â |
| Apple StoreKit / IAP | 課é | ðŽ é« (iOS ã§ã¯å¿ é ) | ãªã | ð¡ Apple 30% â 15% (Small Biz) |
| ãåå.com SMTP | α æåŸ / é ä¿¡åæ¢ã¡ãŒã« | ð¢ äœ (Resend, SendGrid ã«ä¹ãæã容æ) | Resend, SES, SendGrid | ð¢ 倧 |
| Open-Meteo | å€©æ° | ð¢ äœ (ãªãã·ã§ãã«æ©èœ) | OpenWeatherMap | ð¢ 倧 |
â 匷ã¿
- LLM 㯠OpenRouter ã§æœè±¡å â ã¢ãã«å€æŽã§ 1 è¡ä¿®æ£
- SMTP / 倩æ°ã¯èãã©ããã§èŠãããŠããç§»è¡å®¹æ
â ïž æžå¿µ
- D1 ããã¯ã€ã³ãæå€§çŽããªã¬ãŒã·ã§ã³ + éèšã SQL ã«å¯ããŠãããã SQLite äºæ (Turso ç) 以å€ãžã®ç§»è¡ã¯ã³ã¹ã倧
- iOS 㯠Apple å šäŸå (åé¿äžå¯èœãããžãã¹ãªã¹ã¯ãšããŠç¹ã蟌ã)
ð TODO
- D1 ã®ã¹ããŒã â Turso (libSQL) ç§»è¡ã³ã¹ãã詊ç®ããŠãã
- Soniox é害æã« Whisper API ãžãã©ãŒã«ããã¯ããçµè·¯ã®è©Šäœ
𧪠7. æè¡è² åµ & æ¢ç¥ã® TODO â ãªãã¡ã¯ã¿åªå é äœ
ð çŸç¶: äžèЧ (grep ç±æ¥ + ã¬ãã¥ãŒæã«è¿œèš)
| åªå 床 | é ç® | å Žæ | åè |
|---|---|---|---|
| ðŽ é« | IAP JWS çœ²åæ€èšŒ | backend/src/iap.ts:12 | x5c chain + issuer/bundleId/env æ€èšŒãæªå®è£ ãæ¬ãªãªãŒã¹å å¿ é |
| ðŽ é« | backend stg ç°å¢ | â | shiwake-daily-api-stg ãåã |
| ð¡ äž | LP waitlist ã® rate limit | lp/functions/api/waitlist.ts | SPAM 察ç |
| ð¡ äž | conversation_log ã® TTL | backend/migrations | ç¡å¶éã«äŒžã³ã |
| ð¡ äž | åé€ã«ã¹ã±ãŒã (GDPR) | backend å šäœ | DELETE /api/me + å šããŒãã« cleanup |
| ð¢ äœ | ãã³ã㬠URL ã® XXXXXX | lp/functions/_lib/templates.ts:68,270 | UI ããå·®ãæ¿ãå¯èœãªã®ã§ã³ãŒã倿ŽäžèŠ |
| ð¢ äœ | legacy invitation redeem | backend/src/index.ts | 䜿ã£ãŠãªããã°åé€ |
ð éçšã«ãŒã«
- æ°èŠ TODO ã¯ããã«è¿œèš (該åœç®æã®ãœãŒã¹ã³ãŒã ã«
// TODOæ®ã + ããã«ãšã³ããªè¿œå ) - è§£æ¶æã¯ããŒãã«è¡ãåé€ (å±¥æŽã¯ git ã§è¿œãã)
- ã¬ãã¥ãŒäŒã¯ååæããšãåªå 床ãèŠçŽã
ð¡ ãã®ããŒãžã¯éçããã¥ã¡ã³ãã§ããããŒã¿ãœãŒã¹ã¯ admin/index.html å
ã®ããŒãã³ãŒãã
ã¢ãŒããã¯ãã£ã«å€æŽããã£ããšã㯠PR ã§æŽæ°ããŠãã ããã
ð 仿§æž
iOS å
š 11 ç»é¢ã®æ©èœèŠä»¶ (FR) / éæ©èœèŠä»¶ (NFR) / 飿º API / ããŒã¿ã¢ãã«ãç»é¢åäœã§æŽçã
暪æèŠä»¶ãš API ã«ã¿ãã°ãæ«å°Ÿã«äœµèŒãæŽæ°ã¯ admin/index.html ã® data-tab="docs" ã»ã¯ã·ã§ã³ã
â ïž å®è£ ã®ã£ãã (調æ»ã§çºèŠ)
POST /api/feedbackã®ãµãŒãå®è£ ãèŠåœãããªã â iOS ã®FeedbackButton.swiftãšAPIClient.submitFeedbackTextã¯åŒãã§ãããbackend/src/index.tsã«è©²åœãã³ãã©ç¡ããfeedbackè¡ã¯çŸç¶/api/meals/:id/flag-calorieçµç±ã§ã®ã¿äœãããã- Soniox é害æã« Apple SF ãžã®èªåãã©ãŒã«ããã¯ç¡ã â Premium ãŠãŒã¶ã¯ Soniox temp key ååŸå€±ææã«ãšã©ãŒæèšãåºãã ãã§é²é³ç¶ç¶äžèœã
â ãªã³ããŒãã£ã³ã° â ååèµ·åããæåã® 1 åã®èšé²ãŸã§
OnboardingView ios/Sources/Views/OnboardingView.swift
ããã¯ããŒã 1 é ç®ã ãã§ãµã€ã³ã¢ãããå®äºãããã¹ãã©ãã·ã¥çç»é¢ãèåŸã§ AppAttest signup (宿©) / æ§ invite redeem (ã·ãã¥ã¬ãŒã¿) ãèªåå®è¡ã
TRIGGERPrivacyConsent ééåŸ & token äžåšæ©èœèŠä»¶ (FR)
- FR-OB-01ããã¯ããŒã 1 ãã£ãŒã«ãã®ã¿è¡šç€º / 空çœä»¥å€ãå¿ é å
- FR-OB-02ãã¯ãããããã¿ã³ã§
autoSignUp(displayName)ãèµ·å - FR-OB-03éä¿¡äžã¯ ProgressView + ãã¿ã³ disabled + ã©ãã«ãç»é²äžâŠã
- FR-OB-04å
¥åãã£ãŒã«ãã« 0.2s åŸ auto-focusã
submitLabel=done - FR-OB-05AppAttest signup (宿©) â token/userId ã Keychain ä¿åãPhase B å®è£ æž
- FR-OB-06ãµã€ã³ã¢ãã確å®ã®ç¬éã«
needsProfileSetup=trueãå è¡ã»ãã (ãã©ã€ã鲿¢)
éæ©èœèŠä»¶ (NFR)
- NFR-OB-P1ãµã€ã³ã¢ãã㯠P50 < 2s (challenge + attest + signup ã® 3 åŸåŸ©)
- NFR-OB-S1access_token 㯠Keychain (kSecClassGenericPassword, service
jp.newmeta.shiwake.auth) ä¿å - NFR-OB-A1
preferredColorScheme(.dark)åºå®ããã¿ã³ã¯ 44pt 以äžã®ã¿ããé å - NFR-OB-X1æåŸ ã³ãŒãæŠå¿µããŠãŒã¶ãŒã«èŠããªã (UI ããå®å šæé€)
PrivacyConsentView ios/Sources/Views/PrivacyConsentView.swift
ååèµ·åæã«ãã©ã€ãã·ãŒèŠçŽ 4 é ç®ãšã«ã¡ã©åæåäœã®éžæãæç€ºãåæååŸã
TRIGGER@AppStorage("privacy_agreed_v1") == false ã®èµ·åæ©èœèŠä»¶ (FR)
- FR-PC-01ååŸæ å ± / 第äžè éä¿¡ / ä¿åæé / ã»ãã¥ãªã㣠㮠4 ãµããªãŒè¡šç€º
- FR-PC-02ã«ã¡ã©èµ·åèšå®ãã©ãžãªã§ 2 æ (ãã©ã€ãã·ãŒåªå
/ 峿®åœ±)ã
camera_instant_readyã«ä¿å - FR-PC-03ãå
šæãèªããã§
PrivacyPolicyFullViewã sheet 衚瀺 - FR-PC-04ãåæããŠã¯ããããã§
privacy_agreed_v1=true - FR-PC-05å šæã¯ 8 ç« (ååŸæ å ±, å©çšç®ç, 第äžè éä¿¡, ä¿åæé, ãŠãŒã¶ãŒæš©å©, ã»ãã¥ãªãã£, 倿Ž, é£çµ¡å )
- FR-PC-06é£çµ¡å
contact_app@newmeta.co.jpãæç€º
éæ©èœèŠä»¶ (NFR)
- NFR-PC-L1åæ æ³/GDPR æ³å®ã§ãååŸé ç®ã»å©çšç®çã»ç¬¬äžè æäŸã»ä¿åæéã»ååå ããå¿ é èšèŒ
- NFR-PC-L2åæãã©ã°ã¯ããŒãžã§ã³ä»ã (
privacy_agreed_v1) ã§æ¹å®æã«ååæå¯èœ - NFR-PC-A1Dynamic Type å¯Ÿå¿ (.subheadline),
fixedSizeã§æè¿ãä¿æ - NFR-PC-S1HTTPS/WSSãKeychain ä¿åãææå
ProfileSetupView ios/Sources/Views/ProfileSetupView.swift
7 ã¹ããããŠã£ã¶ãŒã (身é·âäœéâäœèèªâ掻åéâé£äºç®æšâç®æšäœéâéææ¥)ãé³å£°äžå¿ + æå ¥åãã©ãŒã«ããã¯ã
TRIGGERãµã€ã³ã¢ããçŽåŸ or auth.needsProfileSetup=trueæ©èœèŠä»¶ (FR)
- FR-PS-017 ã¹ãããã progress bar ã§å¯èŠå
- FR-PS-02ãã€ã¯ãã¿ã³é·æŒã (DragGesture) ã§é²é³ â é¢ããŠéä¿¡
- FR-PS-03STT äºåæµ: Premium=Soniox WebSocket / Free=Apple SFSpeechRecognizer
- FR-PS-04èªèããã¹ãã
/api/setup/parseãž â value_double/level/goal/date_iso/skipped ãæœåº - FR-PS-05çµæãå¹ãåºãã§ã身é·ã¯ 170.5cm ã§åã£ãŠã?ã圢åŒã§å確èª
- FR-PS-06æå ¥åã¢ãŒãã«: wheel picker / ãªã¹ã / DatePickerã芪æåã«å€§ããªæ±ºå®ãã¿ã³
- FR-PS-07bodyFat / activity 㯠"ã¹ããã" å¯ããã以å€ã¯åå ¥å匷å¶
- FR-PS-08ç¯å²ããªããŒã·ã§ã³: èº«é· 100-220cm, äœé 20-300kg, äœèèª 3-70%
- FR-PS-09ãã£ã©ã¯ã¿ãŒ (idle/listening/thinking) ã 160pt åºå®é«ã§è¡šç€º
- FR-PS-10å®äºæã« TDEE ãšç®æšã«ããªãŒã
Bmr.tdee + TargetCalories.deriveã§ã¯ã©ã€ã¢ã³ãç®åº - FR-PS-11
PUT /api/me/targetsã§èº«é·/äœèèª/ç®æšäœé/ææ¥/é£äºç®æš/ã«ããªãŒãäžæ¬ä¿å - FR-PS-12åæã«
POST /api/weightsã§åæäœéã 1 ä»¶èšé² - FR-PS-13ä¿åæåæã«å
šé¢ç·ãã§ã㯠overlay ã 1.8s 衚瀺ããŠãã
markProfileComplete() - FR-PS-14æªå ¥åé ç®ãããã°æåã®æªå ¥å step ã«èªåã§æ»ã
éæ©èœèŠä»¶ (NFR)
- NFR-PS-P1
/api/setup/parse㯠OpenAI strict-mode JSON è§£æãP50 < 1.5s - NFR-PS-A1è§ŠèŠ: é²é³éå§ 0.9 / 忢ãã« / å®äº success notification
- NFR-PS-X1声ãåºããªãå Žé¢ã®ãã©ãŒã«ãã㯠(æå ¥å wheel) ãå š step ã§æäŸ
- NFR-PS-X2é³å£°æ¥ç¶å€±ææã¯ errorMessage 衚瀺ã isRecording=false ã«æ»ã
PaywallView ios/Sources/Views/PaywallView.swift
ProfileSetup çŽåŸã«æ¿å ¥ããã 1 ã¶æç¡æ â Â¥490/æ ã®ãµãã¹ã¯ãªãã·ã§ã³å§èªãApple èŠçŽæºæ ã
TRIGGERprofileSetup å®äº & subscriptionStatus != "premium_active"æ©èœèŠä»¶ (FR)
- FR-PW-01ããããŒãShiwake Premium / æåã® 1 ã¶æã¯ç¡æã
- FR-PW-024 ãããã£ãã (é¢ä¿æ§æ·±å / é³å£°ç¡å¶é / èšæ¶ / é«ç²ŸåºŠã«ããªãŒ) ã icon+desc ã§èšŽæ±
- FR-PW-03äŸ¡æ Œã«ãŒã: Â¥0/æåã® 1 ã¶æ + ãã®åŸ
displayPriceèªåæŽæ° - FR-PW-04CTAã1 ã¶æç¡æã§ã¯ããããâ
StoreKitManager.purchase()â JWS ã/api/iap/verify - FR-PW-05ã賌å
¥ã埩å
ãã§
StoreKit.restore()+ refreshProfile + Premium ç¢ºèªæ onContinue - FR-PW-06èŠçŽ 3 é ç®ã caption ã§æç€º (è§£çŽæ¹æ³ / 24h å / Apple ID å ±æ)
- FR-PW-07product æªããŒã or è³Œå ¥åŠçäžã¯ãã¿ã³ disable
- FR-PW-08賌å
¥å€±æ (userCancelled é€ã) ã¯
store.lastErrorãããã¹ã衚瀺
éæ©èœèŠä»¶ (NFR)
- NFR-PW-L1App Store èŠçŽ: äŸ¡æ Œã»èª²éé »åºŠã»èªåæŽæ°ã»è§£çŽæ¹æ³ã»èŠçŽãªã³ã¯ãå¿ é æ²èŒ
- NFR-PW-S1ã¬ã·ãŒãæ€èšŒã¯ç«¯æ«ã§ãªãå¿
ã
/api/iap/verify(ãµãŒãåŽ JWS æ€èšŒ) - NFR-PW-X1StoreKit æªããŒãæã¯ Â¥490/æ ã®ãã©ãŒã«ããã¯è¡šèš
â¡ èšé²ã«ãŒã (ã³ã¢ããªã¥ãŒ) â èµ·åå³èšé²ã®å·®å¥ååºé
CaptureView ios/Sources/Views/CaptureView.swift
ã¡ã€ã³ç»é¢ãã«ã¡ã©åžžæèµ·å + å·Šäžãã€ã¯é·æŒã + å³äžã«ã¡ã©ã¿ãã + ã¢ã·ã¹ã¿ã³ãå¹ãåºã + çŽè¿ 6 ä»¶ãã«ã
TRIGGERéåžžèµ·å (èªèšŒæž & profileSetup å®äº)æ©èœèŠä»¶ (FR)
- FR-CAP-01å
šç»é¢
CameraPreviewãåžžæçšŒåãäžéš 34% çœè§äžžãäžååã¯å·Šå³ 50:50 ã®ãã€ã¯/ã«ã¡ã©å€å®ãšãªã¢ - FR-CAP-02
cameraInstantReady=falseãªãã«ã¡ã©ã« UltraThinMaterial overlayã1 ã¿ããã§è§£é€ â ããã¯ãªã³ 4 é ãã©ã±ããã2 ã¿ããç®ã§capturePhoto() - FR-CAP-03å·Šãã€ã¯ãšãªã¢ DragGesture: é·æŒã â startCapture â é¢ã㊠stop â
/api/ingestãž transcript - FR-CAP-04STT äºåæµ: useApple=!isPremium ã§ Apple SF / Soniox ãåæ¿
- FR-CAP-05äžéšãã£ã©ã¯ã¿ãŒ (idle/listening/thinking) + å¹ãåºã + 尻尟ãé²é³äžã¯å°»å°Ÿãé ã
- FR-CAP-06å¹ãåºãããã¹ãã¯ã©ã³ãã æœéžããŒã« (æé垯 à 芪å¯åºŠ Ã ç¶æ ã§ 50+ ãã¿ãŒã³)
- FR-CAP-07æéåž¯å¥æšæ¶ (æ/åå/æŒ/ååŸ/å€/å€/æ·±å€) ã Lv1-2=ãã / Lv3+=ãã ã§æœéž
- FR-CAP-08äžã¹ã¯ã€ã (> 40px) ã§ãã£ã©/å¹ãåºããæãããã¿ãpullDownTab ã§åŸ©åž°
- FR-CAP-09çŽè¿ 6 ä»¶ã® meal/weight ããã« (æçå+kcal / äœé+kg) ã§å³åŽçžŠäžŠã³ã仿¥/æšæ¥/äžæšæ¥ ã©ãã«ä»ã
- FR-CAP-10èšç®äž meal ãå«ãŸããé㯠3s ééã§ reloadRecentMeals ã polling
- FR-CAP-11
capturePhoto: AVCapturePhoto ã/api/meals(multipart) ã« POST - FR-CAP-12
/api/ingestã® advance_event ã¬ã¹ãã³ã¹ã§ AdvanceEventModal ã sheet 衚瀺 - FR-CAP-13persona_comment ãããã°åªå ãç¡ã count=0 ãªã "èšé²ãªã"ãcount>0 ãªã "N ä»¶èšé²ãã"
- FR-CAP-14èšé²æåæã¯ success haptic +
.shiwakeDataChangedã 0/4/8/15s ã§è€æ°å post - FR-CAP-15ååèµ·åã®ã¿ 0.6s é
å»¶ã㊠CaptureTutorialOverlay 衚瀺 (
hasSeenCaptureTutorial=trueã§ææ¢) - FR-CAP-16
scenePhase=.backgroundæã« camera.stop()ã埩垰æ start + ããã¹ãããåæå¹å + æšæ¶å·®æ¿
éæ©èœèŠä»¶ (NFR)
- NFR-CAP-P1ingest 1 次å¿ç P50 ç®æš (persona_comment å³è¿åŽ)ãmealLookup ã¯
waitUntilã§éåæ - NFR-CAP-P2ã«ã¡ã©èµ·åããæåã®ãã¬ãŒã ãŸã§ P50 < 500ms (camera.start ã task ã§äžŠåå®è¡)
- NFR-CAP-S1ãã©ã€ãã·ãŒ: èµ·åæããã¬ã©ã¹ãscenePhase å€åã§ç¢ºå®ã« stopããã¡ã€ã³ããŒè§£é€ã¯ã»ãã·ã§ã³æ¯ã«ãªã»ãã
- NFR-CAP-A1è§ŠèŠ: é²é³éå§ 1.0 / 忢 1.0 / æå .success / 倱æ .error / éè«æç« .selectionChanged
- NFR-CAP-A2ããã¯ãªã³æ 㯠cornerSize 44 / strokeWidth 5 ã§èŠèªæ§ç¢ºä¿
- NFR-CAP-O1ãªãã©ã€ã³: token åãã¯
bubblePhrase(.authExpired)ãé信倱æã¯.sendFailed - NFR-CAP-M1è§ŠèŠ
prepare()ã onAppear / onEnded æ¯ã«åŒãã§ã¬ã€ãã³ã·æå°å
CaptureTutorialOverlay ios/Sources/Views/CaptureTutorialOverlay.swift
åå CaptureView 衚瀺æã«åºã 3 ã¹ããã overlay (å°å ¥ / ãã€ã¯ / ã«ã¡ã©)ã
TRIGGERhasSeenCaptureTutorial=false ã§ CaptureView 衚瀺 0.6s åŸæ©èœèŠä»¶ (FR)
- FR-TUT-01é»å¹ 72% + äžå€®ã¡ãã»ãŒãžã«ãŒã + äžéšç¢å°ã§ 3 ã¹ãããæ¡å
- FR-TUT-02step1: ãã€ã¯é å (ç»é¢äž 25% x) ãž arrow.down + ããŠã³ã¹ã¢ãã¡
- FR-TUT-03step2: ã«ã¡ã©é å (ç»é¢äž 75% x) ãž arrow.down
- FR-TUT-04ãã¿ã³: æ»ã / 次㞠/ (æçµ)å§ãã
- FR-TUT-05ãå§ãããã§
hasSeenCaptureTutorial=trueä¿å â èªåæ¶å» - FR-TUT-06Settings ã®ããã¥ãŒããªã¢ã«ãå衚瀺ãã§ AppStorage ãåé€ããã°å衚瀺
éæ©èœèŠä»¶ (NFR)
- NFR-TUT-A1æå¹ã¿ããã¯ç¡èŠ (誀é·ç§»é²æ¢)ãã¹ãããã€ã³ãžã±ãŒã¿ããã衚瀺
- NFR-TUT-A2ç¢å°ã¯
ArrowBounceã§äžäž 10pt æ¯å (泚ç®èªå°)
AdvanceEventModal ios/Sources/Views/AdvanceEventModal.swift
é¢ä¿æ§ã¬ãã«ææ Œ (Lv N â N+1) ã®ç¥çŠã¢ãŒãã«ãAI ããã® opening line ãæŒåºè¡šç€ºã
TRIGGER/api/ingest ãŸã㯠/api/me ã advance_event ãè¿ããæãAuthStore.pendingAdvanceEvent çµç±
æ©èœèŠä»¶ (FR)
- FR-AEM-01ããŒã fill (84pt) ã
spring(0.6, 0.55)ã§ãã§ãŒãã€ã³ + æ¡å€§ - FR-AEM-02Lv ãããé·ç§» "Lv N â Lv N+1" ãç¢å°ã§è¡šç€º
- FR-AEM-03
event.openingLineã pink 8% èæ¯ + 16pt medium ã§äžå€®è¡šç€º - FR-AEM-04ãããããšãããã¿ã³ (pink ã°ã©ã Capsule) ã§
onDismiss - FR-AEM-05sheet detents [.medium, .large]
- FR-AEM-06衚瀺åŸ
AuthStore.pendingAdvanceEvent=nilã«å³ã¯ãªã¢ (äºéè¡šç€ºé²æ¢)
éæ©èœèŠä»¶ (NFR)
- NFR-AEM-X1one-shot é ä¿¡: ingest waitUntil ã§ advance ãæ±ºãŸã£ããæ¬¡ã® /api/me ã§ 1 åã ãå±ãå³ NULL
- NFR-AEM-A1è§ŠèŠé£åãªã (èŠèŠã¢ãã¡ã§ååãªæŒåºåŒ·åºŠ)
⢠æ¯ãè¿ã â ã°ã©ã / å±¥æŽ / ç·šéã® hub
SummaryView ios/Sources/Views/SummaryView.swift
ã°ã©ã (äœé / ã«ããªãŒ) + 7 æ¥ãµãŒã¯ã« + æ¥å¥å±¥æŽäžèЧãç·šé / åé€ / åèšç® / ã«ããªãŒèª€ãå ±åã® hubã
TRIGGERã¡ã€ã³äžéšãã°ã©ãããã¿ã³ã§ fullScreenCoveræ©èœèŠä»¶ (FR)
- FR-SUM-01ãµããªããã㌠(ã¿ã€ãã« + èšå®æ¯è»)
- FR-SUM-02todaySection: 7 æ¥ãµãŒã¯ã« (ç®æš vs å®çžŸ) + æå/æ¶è²»ãã°ã« + éžææ¥ã®é£äºã«ãŒãäžèЧ + ã«ããªãŒããŒ
- FR-SUM-03longTermSection: äœé/ã«ããªãŒã°ã©ãã®ãã°ã«ãæé day/week/month/6month/yearãã°ã©ãå ã¿ããã§ selectedDate åæ¿
- FR-SUM-04äœéã°ã©ãã¯
weight_kg + body_fat_percent(dual axis æ³å®) - FR-SUM-05å±¥æŽã»ã¯ã·ã§ã³: meal/weight/note ãæå»éé ããŒãžãæ¥ä»ã©ãã«åºåããæå€§ 50 ä»¶ãè¡å ãŽãç®± (1 ã¿ããã§ â/à 㮠2 段é確èª)
- FR-SUM-06é£äºåã®ã¿ããã§ editingMealSheet (rename) èµ·åãä¿åã§
POST /api/meals/:id/renameâ mealLookup åèšç® â æ¥œèŠ³çæŽæ°åŸ polling - FR-SUM-07é£äºè¡ã®ã!ãã§ flag-calorie alertããã¯ããã§
POST /api/meals/:id/flag-calorieâ äžéš toast 2.5s - FR-SUM-08äœéè¡ã¿ããã§ EditWeightSheetContentãä¿åã§
POST /api/weights/:id/update - FR-SUM-09åé€: meal/weight/note ã DELETEãæ¥œèгçã«ç»é¢ããæ¶ã
- FR-SUM-10èšç®äž meal ãããéã¯æ¥œèŠ³å€ (
optimisticMeals) ãä¿æãããµãŒãèšç®å®äºãŸã§äžæžããé²ã - FR-SUM-11
.shiwakeDataChangedéç¥ + refreshTimer ã§èªåãªããŒã - FR-SUM-12FeedbackButton ããªãŒããŒã¬ã€è¡šç€º (à ãã¿ã³å³é£)
- FR-SUM-13CloseButton (å·Šäž 78pt å) ã§ fullScreenCover dismiss
éæ©èœèŠä»¶ (NFR)
- NFR-SUM-P1
/api/meals(æéæå®) ã§ 1 åååŸãã¯ã©ã€ã¢ã³ãåŽã§æ¥å¥éèš - NFR-SUM-P2ç·šéæã¯
processingIdsã«è¿œå ããŠãä»åãä¿®æ£äžâŠãoverlay 衚瀺 - NFR-SUM-A1DynamicTypeãè¡ã¢ã¯ã»ã³ãã® heart ã¢ã€ã³ã³ã¯ 13pt ç³»
- NFR-SUM-O1ãšã©ãŒã¯ç»é¢äžéšã«
err:...monospace 9pt ã§éçºè åã衚瀺
FeedbackButton ios/Sources/Views/FeedbackButton.swift
SummaryView ã® Ã é£ã«åžžé§ãããã«ãé·æŒãã§ Apple SF ãæåèµ·ãããããã¹ãã /api/feedback ã«éä¿¡ã
æ©èœèŠä»¶ (FR)
- FR-FB-01暪é·ãã«ãéçºãã£ãŒããã㯠/ é·æŒãã§éä¿¡ããé·æŒãäžã¯èµ€è² + scale 1.05
- FR-FB-02LongPressGesture(0.2s) ã§ startRecordingãDragGesture.onEnded ã§ stopRecording â upload
- FR-FB-03é²é³äžã¯ç»é¢äžå€® 18% äœçœ®ã«å€§ã㪠transcript ã«ãŒã衚瀺 (鿬¡æŽæ°)
- FR-FB-04ã¢ããããŒãäžã¯ ProgressView ã¢ã€ã³ã³ããã« opacity 0.5ãdisable
- FR-FB-05çµæããŒã¹ã: æå (ç·) / èãåãã (æ©) / 倱æ (èµ€) ã 2.5s 衚瀺
- FR-FB-06Apple Speech (
LocalSpeechRecognizer) ã§å®å šãªã³ããã€ã¹æåèµ·ãã (ã³ã¹ããŒã)
éæ©èœèŠä»¶ (NFR)
- NFR-FB-P1ããã¹ãã®ã¿éä¿¡ (é³å£°ãã¡ã€ã«ç¡ã) ã§åž¯åã»ã³ã¹ãåæž
- NFR-FB-A1è§ŠèŠ: é²é³éå§ medium 1 å
- NFR-FB-O1空 transcript ã¯ããŒã¹ãã§ç¥ããéä¿¡ã¹ããã
⣠èšå® / ããåºç€ â ãããã£ãŒã«åç·šéãš Premium 管ç
SettingsView ios/Sources/Views/SettingsView.swift
ãããã£ãŒã«ç·šé / ç®æšåèšå® / ã«ã¡ã©èšå® / é¢ä¿æ§è¡šç€º / Premium 管ç / Facts é²èЧ / æåŸ çºè¡ / ãã°ã¢ãŠãã»ãªã»ããã
TRIGGERSummaryView å³äžã®æ¯è»ãã fullScreenCoveræ©èœèŠä»¶ (FR)
- FR-SET-01ãããã£ãŒã«: ããã¯ããŒã / èº«é· / äœèèªç / çŸåšäœé / BMR (ç®åº) / 掻åé / é£äºç®æš / TDEE (ç®åº)
- FR-SET-02ç®æš: ç®æšäœé / éæææ¥ (DatePicker, 仿¥ä»¥é) / ç®æšã«ããªãŒ (èªåç®åº)
- FR-SET-03ã«ã¡ã©èšå® Toggle:
camera_instant_readyãåæ - FR-SET-04é¢ä¿æ§: heart 5 段ãLv ã® Picker (ãããã°çš)ãåŒã³æ¹è¡šç€ºã奜æåºŠ pt 衚瀺
- FR-SET-05Premium ã¹ããŒã¿ã¹è¡šç€º + Apple ãµãã¹ã¯ç®¡çãªã³ã¯ + ãããã°åæ¿ Picker
- FR-SET-06ããã¥ãŒããªã¢ã«ãå衚瀺ãã§
hasSeenCaptureTutorialåé€ â dismiss - FR-SET-07ã¢ã·ã¹ã¿ã³ããèŠããŠããããš:
user_factsã 30+ ã«ããŽãªã©ãã«ã§è¡šç€ºãswipeActions ã§åé€å¯ - FR-SET-08æåŸ
ã³ãŒã: èªåã®ã³ãŒã衚瀺 + ãä»äººãæãã³ãŒããçºè¡ãã§
POST /api/inviteâ ShareLink - FR-SET-09ãã°ã¢ãŠã (Keychain ã¯ãªã¢) / åæèšå®ãããçŽã / ã¢ããªå®å šãªã»ãã ã® 3 段éç Žå£æäœ
- FR-SET-10FloatingSaveButton: ç·šéäž or hasUnsavedChanges ã®æã®ã¿è¡šç€º
- FR-SET-11ç·šéäž (focusedField != nil) 㯠CloseButton ãé ã (誀ã¿ãã鲿¢)
éæ©èœèŠä»¶ (NFR)
- NFR-SET-S1ç Žå£æäœã¯ ConfirmationDialog ã§å¿ ã確èª
- NFR-SET-X1å€å€åæ€ç¥ã¯
origXxxã¹ãããã·ã§ãããšã®æ¯èŒã§ hasUnsavedChanges ç®åº - NFR-SET-A1ããã¹ãç·šéäžã®ä¿åãã¿ã³åªå 床ãé«ãããà ãã¿ã³ãšæä»å¶åŸ¡
RootView ios/Sources/Views/RootView.swift
ããã²ãŒã·ã§ã³åºç€ãCaptureView ãå šç»é¢ã«çœ®ããäžéšãã°ã©ãããã«ãã SummaryView ã fullScreenCover ã§éãã
TRIGGERèªèšŒæž & profileSetup å®äº ã§è¡šç€ºæ©èœèŠä»¶ (FR)
- FR-RV-01ZStack(.bottom): CaptureView + äžéšã°ã©ããã¿ã³ (ultraThinMaterial Capsule)
- FR-RV-02sheet enum (settings, chart) ã fullScreenCover ã§è¡šç€º
- FR-RV-03ã·ãŒãé·ç§»ã¢ãã¡ãŒã·ã§ã³ã
transaction.disablesAnimations=trueã§æå¶ (ç¬æåæ¿) - FR-RV-04
scenePhase=.backgroundã§ã·ãŒããèªå close - FR-RV-05sheet nilânon-nil æ
.shiwakeLeftCaptureãéã§.shiwakeReturnedToCaptureã post (ã«ã¡ã© stop/start å¶åŸ¡) - FR-RV-06CloseButton (å·Šäž 78pt) ãš FloatingSaveButton (å³äž 78pt) ã® 2 åå©çšã³ã³ããŒãã³ãæäŸ
éæ©èœèŠä»¶ (NFR)
- NFR-RV-P1èšå® / ãµããªé·ç§»ã¯ã¢ãã¡ç¡å¹åã§ã峿ç»é¢åæ¿ãäœæ
- NFR-RV-S1ããã¯ã°ã©ãŠã³ãæã®ã·ãŒãèªå close ã§æå³ãã¬ç»é¢æ®çã鲿¢
†暪æèŠä»¶ â ç»é¢æšªæã§é©çšããã NFR
ð èªèšŒ / ã»ãã¥ãªãã£
- NFR-X-AUTH-01 Bearer ããŒã¯ã³æ¹åŒ (
Authorization: Bearer <hex32>)ãauth.ts ã®requireUserã§ SELECT id FROM users WHERE access_token=? - NFR-X-AUTH-02 access_token 㯠iOS Keychain (kSecClassGenericPassword, service
jp.newmeta.shiwake.auth) - NFR-X-AUTH-03 ãµã€ã³ã¢ãã㯠AppAttest çµç± (
/api/auth/challengeâ DCAppAttestService â/api/auth/signup)ãDEBUG ãã«ãã®ã¿æ§ invite redeem ãžãã©ãŒã«ãã㯠- NFR-X-AUTH-04 æ¢ãã°ã€ã³æã® invite URL 㯠iOS åŽã§æ©æ return (ç¡é§ãªåŸåŸ©åé¿)
- NFR-X-AUTH-05 IAP 㯠Apple JWS ã
/api/iap/verifyã§ãµãŒãåŽæ€èšŒ (端æ«åŽã¬ã·ãŒãæ€èšŒã¯ä¿¡çšããªã) â 眲å chain æ€èšŒã¯ TODO - NFR-X-AUTH-06 Apple Server-to-Server éç¥çšã«
/api/iap/notification(no auth, çœ²åæ€èšŒã¯ TODO) - NFR-X-AUTH-07 AppAttest ã® Apple å ¬ééµ + nonce æ€èšŒã§åœç«¯æ«ã®å€§éãµã€ã³ã¢ããã鲿¢ (Phase A å®äº)
- NFR-X-AUTH-08 ããã³ãåé¢: å
š D1 ã¯ãšãªã§
user_idã WHERE å¿ é (æŒããããã°å³ä¿®æ£å¯Ÿè±¡)
â¡ ããã©ãŒãã³ã¹
- NFR-X-PERF-01
/api/ingest1 次å¿ç (persona_comment) 㯠P50 < 2sãmealLookup ã¯ctx.waitUntilã§éåæ - NFR-X-PERF-02 OpenAI prompt cache ã
contextBundleã§æŽ»çšãã·ã¹ãã ããã³ãããå®å®å - NFR-X-PERF-03
meal_nutrition_cache(0007) ã§ååæçã®åèšç®ãåé¿ - NFR-X-PERF-04 CaptureView ã¯èšç®äž meal æ€åºæã®ã¿ 3s pollingãå®äºã§èªå忢
- NFR-X-PERF-05 Cloudflare Workers ãšããžå®è¡ + D1 prepared statement
- NFR-X-PERF-06
weather_cache(0011) ã§å€éš API åŒã³åºãã 1h ããŒã«ã«ãã£ãã·ã¥
ð¡ å¯çšæ§ / é害ææå
- NFR-X-AVAIL-01 OpenRouter/OpenAI é害æ: persona_comment ã¯ãã©ãŒã«ããã¯å®åæãmealLookup 㯠notes "èšç®äž" ã®ãŸãŸçã眮ã
- NFR-X-AVAIL-02 Soniox é害æ (Premium): temp key ååŸå€±æ â ãšã©ãŒæèšãApple SF ãžã®ãã©ãŒã«ããã¯ã¯æªå®è£ (å°æ¥èª²é¡)
- NFR-X-AVAIL-03 D1 é害æ: 5xx ãè¿ã iOS ã¯
bubblePhrase(.sendFailed)ã§å詊è¡èªå° - NFR-X-AVAIL-04 R2 é害æ:
image_r2_keyã null ã§ meal ãäœæ â åŸè¿œãåã¢ããæªå®è£ - NFR-X-AVAIL-05
/healthãšã³ããã€ã³ãã§çåç¢ºèª ({ ok, ts }) - NFR-X-AVAIL-06
/api/meãäžæå€±æããŠãã¯ã©ã€ã¢ã³ãã¯ååå€ãä¿æ (profileLoaded=falseã®é splash)
ð èšæž¬ / ãã° / ç£æ»
- NFR-X-OBS-01
api_usage_log(0012): OpenRouter/OpenAI/Soniox ã® tokenã»ã³ã¹ãã»ã¢ãã«å¥å©çšãã° - NFR-X-OBS-02
request_timing(0013): ãšã³ããã€ã³ãæ¯ã®ã¬ã€ãã³ã·ååž - NFR-X-OBS-03
conversation_log(0014): user çºè©± / assistant è¿çã®å¯Ÿè©±å±¥æŽ (persona æ¹åçš) - NFR-X-OBS-04
feedback(0018): éçºè åããã£ãŒãããã¯æ +[calorie_flag]ãã¬ãã£ãã¯ã¹ã§ã«ããªãŒèª€ãå ±å - NFR-X-OBS-05 Admin
/api/admin/stats: OpenRouter credits / å¹³åæ¥æ¬¡ã³ã¹ã / runway æ¥æ° / ãã£ãŒãããã¯çŽè¿äžèЧ - NFR-X-OBS-06 Admin 㯠Basic èªèšŒ (
app.use("/api/admin/*", basic auth))
ð ãã©ã€ãã·ãŒ / æ³å
- NFR-X-PRIV-01
privacy_agreed_v1åæãã©ã° + å šæããªã·ãŒ (8 ç« , å¶å® 2026-05-24) - NFR-X-PRIV-02 åæååŸé ç®: ããã¯ããŒã / èº«é· / äœé / äœèèª / é£äº (text/voice/photo) / å©çšæå» / ããã€ã¹æ å ±
- NFR-X-PRIV-03 第äžè éä¿¡: é³å£°èªè (Soniox) / AI è§£æ (OpenAI/OpenRouter) / ã€ã³ãã© (Cloudflare) ãæç€º
- NFR-X-PRIV-04 åé€äŸé Œã¯
contact_app@newmeta.co.jpããµãŒãåŽåé€ã¯æåãªãã¬ãŒã·ã§ã³ (DELETE /api/me 㯠TODO) - NFR-X-PRIV-05 ã«ã¡ã©ã¯ããã¯ã°ã©ãŠã³ãå³ stopãèµ·åæããã¬ã©ã¹ã default (ãã©ã€ãã·ãŒåªå )
- NFR-X-PRIV-06 feedback ã¢ããããŒãã¯ããã¹ãã®ã¿ (é³å£°éä¿å)
ðž ã³ã¹ã / ãªãœãŒã¹
- NFR-X-COST-01 OpenRouter æé¡äºç®ã¯ admin/stats ã®
runway_daysã§ç£èŠ - NFR-X-COST-02 Free ãŠãŒã¶ã¯ STT ã Apple SF (ã³ã¹ããŒã) ã«åŒ·å¶ãPremium ã®ã¿ Soniox
- NFR-X-COST-03
meal_nutrition_cacheã§éè€æçã® LLM åŒã³åºããåæž - NFR-X-COST-04 prompt cache 掻çšã§ OpenAI å ¥åããŒã¯ã³ã 50%+ åæžçã
- NFR-X-COST-05 R2 ã¯é£äºåçã®ã¿ä¿ç®¡ (é³å£°ã¯ä¿åããªã)
- NFR-X-COST-06 ãã£ãŒãããã¯ã¯ããã¹ãååŸéä¿¡ (é³å£°ãã¡ã€ã«éä¿¡ãªã)
â¿ åœéå / ã¢ã¯ã»ã·ããªãã£
- NFR-X-I18N-01 æ¥æ¬èªã®ã¿ (Locale ja_JP åºå®)ãAsia/Tokyo TZ ã§æ¥ä»åŠç
- NFR-X-A11Y-01 Dynamic Type ã semantic font (
.subheadline / .caption) ã§å¯Ÿå¿ - NFR-X-A11Y-02 VoiceOver: æå
¥åãã¿ã³ã«
accessibilityLabel("æå ¥åãã") - NFR-X-A11Y-03 ã¿ããé å 44pt 以äžãæè (CloseButton/SaveButton 㯠78pt å)
- NFR-X-A11Y-04 è§ŠèŠãã£ãŒããã㯠(impact/notification/selection) ã§èŠèŠã«äŸããªãç¶æ éç¥
ð é ä¿¡ / ãªãªãŒã¹
- NFR-X-REL-01 iOS: TestFlight â App Store Connect çµç±é åž (IAP 㯠In-App Subscription, StoreKit2)
- NFR-X-REL-02 ããã¯ãšã³ã: Cloudflare Workers (
wrangler deploy), D1 ãã€ã°ã¬ãŒã·ã§ã³wrangler d1 migrations apply - NFR-X-REL-03 LP: Cloudflare Pages,
lp/functions/api/waitlist.tsã§ Edge Function - NFR-X-REL-04 Admin: Cloudflare Pages
shiwake-admin.pages.devãSPA + Basic auth lookup via backend - NFR-X-REL-05 AASA:
/.well-known/apple-app-site-associationã§ Universal Link/invite/* - NFR-X-REL-06
privacy_agreed_v1ã®ããŒãžã§ã³çªå·ã§åæã®äžä»£ç®¡ç
⥠API ã«ã¿ãã° â ããã¯ãšã³ãå šãšã³ããã€ã³ã (37)
èªèšŒ / æåŸ
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/auth/challenge | none | AppAttest çš nonce çºè¡ (5min TTL) |
| POST | /api/auth/signup | none | AppAttest assertion ãæ€èšŒ â user äœæ + access_token è¿åŽ |
| POST | /api/invite | none | å¿å invite code çºè¡ (DEBUG ãã©ãŒã«ãã㯠/ Settings ä»è æåŸ ) |
| POST | /api/auth/redeem | none | invite code ã access_token ãšäº€æ (409=æ¶è²»æž) |
| GET | /invite/:code | none | Universal Link çšãã¢ããªæªã€ã³ã¹ããŒã«æã®ãã©ãŒã«ãã㯠|
| GET | /.well-known/apple-app-site-association | none | iOS Universal Link èšå® |
ãŠãŒã¶ãŒ / ãããã£ãŒã«
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| GET | /api/me | Bearer | ãããã£ãŒã« + streak + subscription + trial + pending_advance_event äžæ¬ååŸ |
| PUT | /api/me/targets | Bearer | 身é·/äœèèª/ç®æšäœé/ææ¥/ç®æšã«ããªãŒ/é£äºç®æš/ããã¯ããŒã äžæ¬æŽæ° |
| PUT | /api/me/affinity | Bearer | é¢ä¿æ§ points çŽæ¥ã»ãã (ãããã°) |
| PUT | /api/me/subscription | Bearer | ãµãã¹ã¯ç¶æ æååæ¿ (ãããã°) |
| POST | /api/setup/parse | Bearer | wizard çšé³å£° transcript â value/level/goal/date_iso æœåº |
| GET | /api/me/facts | Bearer | ã¢ã·ã¹ã¿ã³ããåŠç¿ãã user facts äžèЧ |
| DEL | /api/me/facts/:id | Bearer | fact åé€ |
é£äº (meals / ingest)
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/meals | Bearer | åçããŒã¹ meal ç»é² (multipart: image+transcript) |
| GET | /api/meals | Bearer | æéæå®ã§ meal äžèЧååŸ |
| DEL | /api/meals/:id | Bearer | meal åé€ |
| PUT | /api/meals/:id | Bearer | meal ç·šé (raw_text ç) |
| POST | /api/meals/:id/rename | Bearer | æçåå€æŽ â mealLookup åèšç® |
| POST | /api/meals/:id/flag-calorie | Bearer | ã«ããªãŒèª€ãå ±å â feedback ããŒãã«ã« [calorie_flag] æ¿å ¥ |
| POST | /api/ingest | Bearer | ããã¹ã transcript ã§é£äº/äœé/éè«ãä»åã (persona_comment + advance_event 忢±) |
| POST | /api/ingest/voice | Bearer | é³å£°ãã€ã㪠â STT â ingest çµ±å |
| POST | /api/auth/soniox-temp-key | Bearer | Premium çš Soniox WebSocket äžæããŒçºè¡ |
äœé / éå / ããŒã
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/weights | Bearer | äœéèšé² (kg + note + body_fat_percent) |
| POST | /api/weights/:id/update | Bearer | æ¢åäœéã®äžæžãç·šé |
| GET | /api/weights | Bearer | æéæå®ååŸ |
| DEL | /api/weights/:id | Bearer | åé€ |
| GET | /api/exercises | Bearer | éåèšé² æéæå®ååŸ |
| DEL | /api/exercises/:id | Bearer | éååé€ (POST 㯠ingest çµç±ã§èªåäœæ) |
| GET | /api/notes | Bearer | ããªãŒããŒãäžèЧ |
| DEL | /api/notes/:id | Bearer | ããŒãåé€ |
| GET | /api/summary/weekly | Bearer | éå» 7 æ¥ã®éèš (meal_count/total_kcal/avg_kcal + weights é å) |
ãã£ãŒãããã¯
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/feedback | Bearer | ããã¹ããã£ãŒãããã¯éä¿¡ â ïž ãµãŒãå®è£ æªç¢ºèª |
IAP / ãµãã¹ã¯ãªãã·ã§ã³
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/iap/verify | Bearer | StoreKit JWS ããµãŒãåŽæ€èšŒ â users.subscription_status æŽæ° |
| POST | /api/iap/notification | none (çœ²åæ€èšŒ) | Apple Server-to-Server éç¥åä¿¡ |
Admin / LP / ãã«ã¹
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| GET | /api/admin/stats | Basic | OpenRouter credits / æ¥æ¬¡ã³ã¹ã / runway / ãã£ãŒãããã¯çŽè¿äžèЧ (/api/admin/* å šäœã Basic) |
| POST | /api/waitlist (LP) | none (IP hash) | ã¡ã¢ã + source + UA + referrer ã waitlist ããŒãã«ã«ä¿å |
| GET | /api/admin/waitlist (LP) | Basic | waitlist ååŸ (JSON or CSV ãšã¯ã¹ããŒã) |
| GET | / | none | ã«ãŒã (å°ç·è¿ã) |
| GET | /health | none | { ok: true, ts: now } |
ð¡ ãã®ããŒãžã¯éçããã¥ã¡ã³ãã§ããããŒã¿ãœãŒã¹ã¯ admin/index.html ã® data-tab="docs" ã»ã¯ã·ã§ã³ + admin/admin.js ã® docFilter* 颿°ã
ç»é¢è¿œå ã»API å€æŽæã« PR ã§æŽæ°ããŠãã ããã
ð€ LLM èšèšæž
shiwake-daily 㯠OpenRouter çµç±ã§è€æ°ã® LLM ãçµã¿åãããŠåããŠããã
æ¬ããŒãžã¯ååŒã³åºãã® ç®çã»ã¢ãã«ã»ããã³ããæ§é ã»ã³ã¹ã + åšèŸºã® ããŒãã¹ãšã³ãžãã¢ãªã³ã° ã®èšèšããŸãšããã
æŽæ°ã¯ admin/index.html ã® data-tab="llm" ã»ã¯ã·ã§ã³ãå®ã³ãŒããšä¹é¢ãããçŽãã
ðº 1. å šäœå (1 çºè©±ãããã® LLM åŒã³åºããã§ãŒã³)
ð é³å£° 1 çºè©± â èšé²å®äºãŸã§ã®åŒã³åºãé åº
(Gemini 3 Flash) participant Per as ð¬ persona
(gpt-4o-mini) participant ML as ðœ mealLookup
(Gemini 3 :online) participant Judge as ð judge
(gpt-4o-mini) participant DB as ðŸ D1 iOS->>API: POST /api/ingest (transcript) API->>DB: loadRecentContext + facts + convLog (䞊å) API->>Ing: chat.completions (tools=create_meal/weight/exercise/...) Ing-->>API: tool_calls[] (JSON args) API->>DB: INSERT meals/weights/exercises (kcal=0 æ«å®) API->>Per: chat.completions (ç§æžçºèšçæ) Per-->>API: "ã©ãŒã¡ã³ã§ããããç²ãããŸã" API-->>iOS: 200 OK (transcript / created / persona_comment) Note over API,ML: â waitUntil ã§è£ã§ã«ããªãŒåèšç® â par API->>ML: lookupMealNutrition (1é£ãã€) ML-->>API: { calories_kcal, PFC, source_note } API->>DB: UPDATE meals SET calories_kcal=... and API->>Judge: evaluateLevelAdvance (æ¡ä»¶æç«æã®ã¿) Judge-->>API: { decision, openingLine? } API->>DB: UPDATE users SET affinity_level/cooldown end
â
primary_response = ingest + persona ã®çŽå 2 åŒã³åºããŸã§ã§ iOS ã« 200 ãè¿ã (~3-4 ç§ç®æš)ã
â
calorie_complete = mealLookup ã® web æ€çŽ¢ã waitUntil å
ã§å®äºãããŸã§ (~10-15 ç§ãèšç®äžâŠè¡šç€º)ã
ð 2. åŒã³åºãæ©èŠè¡š
| caller | ã¢ãã« | çšé | åŒã³åºãå Žæ | temperature | åºååœ¢åŒ |
|---|---|---|---|---|---|
ingest |
google/gemini-3-flash-preview | é³å£°ããã¹ã â tool calls (create_meal / weight / exercise ç) | ingest.ts |
0.1 | tool_calls (parallel) |
meal_lookup |
google/gemini-3-flash-preview:online | æçå â å ¬åŒå€ããŒã¹ã® kcal + PFC (web æ€çŽ¢ Exa) | mealLookup.ts |
0.1 | json_schema strict |
normalize_dish |
openai/gpt-4o-mini | ç·šéæã«ååã lookup_key / unit / count ã«åè§£ | mealLookup.ts |
0.1 | json_schema strict |
persona |
openai/gpt-4o-mini | èšé²çµæã«å¯Ÿããç§æžã® 1-2 æã³ã¡ã³ã | persona.ts |
0.7 | plain text (max 300 tok) |
judge |
openai/gpt-4o-mini | é¢ä¿æ§ Lv ææ Œãã¹ãã (advance / wait) | relationshipJudge.ts |
0.4 | json_schema strict |
forced_opening |
openai/gpt-4o-mini | 匷å¶ã©ã€ã³å°éæã®ããã£ãšè¿·ã£ãŠããäžèš | relationshipJudge.ts |
0.7 | plain text (max 100 tok) |
premium_unlock |
openai/gpt-4o-mini | 課éæã®ããã£ãšè©±ãããã£ããäžèš | relationshipJudge.ts |
0.7 | plain text (max 100 tok) |
photo_dish_extraction |
google/gemini-3-flash-preview (vision) | æçåç â dish[] (kcal ã¯åºããªã) | openai.ts |
0.1 | json_schema strict |
ð¡ caller 㯠api_usage_log ããŒãã«ã® caller åãšäžèŽããæè¡ / ã³ã¹ããã¿ãã§å
èš³ãèŠããã
ð 3. ingest (Phase A: ä»åã)
ð 圹å²
Soniox STT ã§æžãèµ·ããããçºè©±ããæ§é åããã IngestAction[] ã«åè§£ããã
parallel tool calls å¿
é :ãæã¯ã©ãŒã¡ã³é£ã¹ããäœé 70kg ã ã£ããã 1 åã®åŒã³åºãã§ create_meal + create_weight ã«åå²ããã
ð å ¥å / åºå
input : transcript (string) + RecentContext (çŽè¿ 7 æ¥ã®é£äº / äœé / éå)
+ userName / knownFacts / contextBundle / äŒè©±å±¥æŽ
output : IngestAction[] = (create_meal | create_weight | create_exercise
| update_meal | update_weight | update_exercise
| delete_* | reply_only | record_user_fact)[]
ð ããã³ããæ§é
(å®å šåºå®ã»cache prefix)
~ 4000 token"] B["nowIso + userName
(åçã»å°)"] C["RecentContext
(meals/weights/exercises å10ä»¶)"] D["knownFactsBlock
(ãŠãŒã¶ãŒèšæ¶)"] E["contextBundleBlock
(倩æ°ã»è¡åéèš)"] F["conversationHistoryBlock
(çŽè¿äŒè©±)"] A --> B --> C --> D --> E --> F
ð STATIC_PROMPT_PREFIX ã«æžããŠããããš (æç²)
- 䜿ãã tool äžèЧãšåŒã³æ¹
- éåå€å®ã«ãŒã«: ãã¹ã¯ã¯ãã 10 åãã®ãããªåæ°ããŒã¹çºè©±ãå¿ ã create_exercise ãåŒã¶
- calories_kcal ã®æšå®æ¹æ³: MET 衚 + äœé à æé (h)ãåæ°ããŒã¹ã¯ 1 ã»ãã ~3 åæç®
- category åé¡: aerobic / anaerobic / other
- é£äºå€å®ã«ãŒã«: æçåã»åºååç¬ã§ã create_meal (åè©äžèŠ)
- STT 誀åèšæ£ãã¿ãŒã³: ãæ°ã©ãŒã¡ã³ãâãèŸã©ãŒã¡ã³ãããã©ãŒã¡ã³äºå·ãâãã©ãŒã¡ã³äºéããªã©åé³ç°çŸ©èªãé»ã£ãŠèšæ£
- update_meal ã®åªå é äœ: calories_kcal æç€º â name/quantity 倿Žã§å lookup â recorded_at ã ããªãæ®ã眮ã
ð åŒã³åºããã©ã¡ãŒã¿ (å®ã³ãŒã)
callOpenRouterChat({
caller: "ingest",
body: {
model: "google/gemini-3-flash-preview", // gpt-4o-mini ãã ~4à é«ããåé¡ç²ŸåºŠãåªå
messages: [
{ role: "system", content: systemPrompt(...) },
{ role: "user", content: transcript },
],
tools: FUNCTION_TOOLS, // 13 åã® function å®çŸ©
tool_choice: "auto",
parallel_tool_calls: true, // â
1 çºè©± â è€æ° tool å¿
é
temperature: 0.1, // å®å®ããåé¡ã®ããäœæž©
},
})
â ïž æ¢ç¥ã®èœãšã穎
- OpenAI strict mode ã§
requiredã«åæãããã£ãŒã«ã㯠null 蚱容ã«ããªããšãLLM ãçç¥ â schema errorãã«ãªããcreate_meal 㯠required ã§ 11 ååæãã代ããã«å€åã["number", "null"]ã«ã - åæ°ããŒã¹ã®éåã¯
reps + duration_minutes (~3 å)ã®äž¡æ¹ãåããããæç€ºããŠãããiOS 衚瀺㯠reps åªå ã§åºããªããšã3 åããåºãŠããŸã (ä¿®æ£æž)ã - STT 㯠åé³ç°çŸ©èªã®åãéã ãèµ·ãã (ãçå± ã©ã³ãâãé«å± ã©ã³ããªã©)ã誀åèšæ£ã»ã¯ã·ã§ã³ãããã³ããã«æèšããŠããããæªç»é²ãã¿ãŒã³ã«ã¯åŒ±ãã
ðœ 4. mealLookup (Phase B: ã«ããªãŒ web æ€çŽ¢)
ð 圹å²
ingest ãåºãã lookup_key + lookup_unit ãå
¥åã«ã1 åäœãããã® kcal + PFC ã web æ€çŽ¢ã§åãã«è¡ãã
ingest ãšåé¢ããããšã§:
â ããã³ãããã£ãã·ã¥ãå¹ã (ingest åŽã¯ web æ€çŽ¢ãªãã®è»œéåŒã³åºãã§ãã£ãã·ã¥ ãããçãæå€§å)ã
â¡ ãã£ãã·ã¥ããŒã "çäžãžã§ãã|1æ¯" ã®ããã«éãšç¬ç«ãééãã§åãããªãã
ð OpenRouter :online ãµãã£ãã¯ã¹
google/gemini-3-flash-preview:online ãæå®ããã ãã§ OpenRouter åŽã Exa ã䜿ã£ã web æ€çŽ¢ãèªåä»äžã
ã³ã¹ã: éåžž token 課é + $0.005 / call ãã©ãã (PRICING table ã® webSearchFlatPerCall)ã
ð ããã³ããã®ãã€ã³ã (æç²)
- å¿ ã web æ€çŽ¢çµæãåç §ããŠããæ é€æåãç¢ºå® (æšæž¬ã®ã¿ã¯çŠæ¢)
- 1 åäœããã = ãŠãŒã¶ãŒåŽã§ count ãæããåæ
- ãã·ãã·ã»å€§çã»ç¹ç㯠count ã«åºã (lookup_key ã«ã¯å ¥ããªã)
- ãã ããã©ãŒã¡ã³äºé å°ã€ãµã€ãã·ãã·ãã®ãããªåºæã¡ãã¥ãŒåã¯éå¢ã蟌ã¿ã§ OK
- å ¬åŒå€ãç¡ããã°é¡äŒŒæçã§ä»£çšã source_note ã«ãã®æšãæèš
- äžç¢ºå®ãªã é«ãã«èŠç©ãã (éå°è©äŸ¡ããªã)
- åèå€: çäž ~145-200 / ã©ãŒã¡ã³äºé å° ~1300-1700 / ãã㯠ããã°ãã㯠~525 çã 10 ä»¶çšåºŠæç€º
ð ãã£ãã·ã¥æŠç¥
cacheKey = NFKCæ£èŠå(lookup_key) + "|" + NFKCæ£èŠå(lookup_unit)
äŸ: "çããŒã« äžãžã§ãã|1æ¯"
"ã©ãŒã¡ã³äºé å°ã€ãµã€ãã·ãã·|1æ¯"
D1 ããŒãã«: meal_nutrition_cache (key, calories_kcal, carbs_g, protein_g, fat_g, source_note, created_at)
TTL: 30 æ¥ (å
¬åŒã¡ãã¥ãŒæ¹èšãåžå)
ð· 5. analyzeMeal (åç vision)
ð 圹å²
åç + (ä»»æã®é³å£° transcript) ã vision ã¢ãã«ã«èŠããŠãæçå / quantity / lookup_key / count ãæœåºã
kcal 㯠絶察ã«åºããªã (LLM ã®ãã«ã·ããŒã·ã§ã³æå¶ã®ãã)ãåºãã dish[] ããã®ãŸãŸ mealLookup ã«æµããŠå
¬åŒå€ãååŸããã
ð ã¢ãã«éžå®
ingest ãšçµ±äžã㊠google/gemini-3-flash-preview (multimodal)ãGemini ã® vision 粟床㯠gpt-4o-mini ãšåç以äžã§ãåçå©çšã¯å°ãªãæ³å® (æã³ã¹ã誀差ã¬ãã«)ã
ð åºå schema
{
dishes: [
{ name, quantity, display_label, lookup_key, lookup_unit, count, confidence }
],
notes: "åçãšçºè©±ããèªã¿åã£ãå
šäœã¡ã¢ (1-2 æ)"
}
ð¬ 6. persona (ç§æžã³ã¡ã³ãçæ)
ð 圹å²
ingest å®äºçŽåŸã«ããã£ã©ã¯ã¿ãŒ (çŸåšã¯ ayaka ã®ã¿) ã® 1-2 æã®èªç¶ãªãªã¢ã¯ã·ã§ã³ãçæã
Lv1-5 ã§å£èª¿ãå€ãã (æ¬èªã®ã¿ â ã¿ã¡å£ â ãã å)ãæåæ°äžéã»çµµæåããªã·ãŒã Lv å¥ã
ð system prompt æ§é (cache æŠç¥)
(å š Lv å ±éã»å®å šåºå®)
â cache prefix"] P2["getCharacterFraming(Lv)
(Lvâ€2 / Lvâ¥3 ã§ 2 çš®é¡)"] P3["getResponseStyle(Lv)
(maxChars / emoji / tone / GOOD / NG äŸ)"] P4["honorificRule
(userName + Lv å¥ äºäººç§°)"] P1 --> P2 --> P3 --> P4
â
PERSONA_COMMON_RULES ã system prompt ã®å
é ã«åºå®ã§çœ®ãããšã§ OpenAI ã®èªåããã³ãããã£ãã·ã¥ã«ããããããã
åäžãŠãŒã¶ãŒé£ç¶åŒã³åºããªãå
±ééš + Lv éšåãŸã§å
šãŠ cache hitã
ð user prompt ã«è©°ãããã®
- contextBundleBlock (ä»ãã®ç¬é: 倩æ°ã»è¡åéèš)
- chatStateBlock (å¯Ÿè©±ç¶æ ã¬ã€ãã³ã¹ãéè«ã«ãŒã鲿¢)
- knownFactsBlock (ãŠãŒã¶ãŒèæ¯æ å ±) + ãé»ã£ãŠãããã«ãŒã«
- previousAssistantMessage (çŽåã®æšæ¶ãªã©ãäŒè©±é£ç¶æ§)
- èšé²ããå 容 (createdSummaries)
- Lv å¥ questioningPolicy / sensitiveRule
- forceOpening ãããã°ãåé ã«å¿ ãå«ããã
ð 絶察ã«ãŒã« (ããã³ããåºå®)
- 説æã»å±è²¬ã»ã«ããªãŒçްããææã¯çŠæ¢
- BMI / PFC ãªã©å°éçšèªçŠæ¢
- äžã® Lv ã®å£èª¿ãå åãããªã (Lv1 ã§ããããïŒãNG)
- ãå 茩ããšã¯çµ¶å¯ŸåŒã°ãªã (ãŠãŒã¶ãŒã¯å¹Žäžèšå®)
- çè¿äºã»äºåå¿ç NG: ãç¹ã«èšé²ãããã®ã¯ãªãããã§ãããNG
- èªåãã話ãåºããªã (ããšããã§ããããããã°ãNG)
- èæ¯æ å ±ãæã¡åºããªã: ãããã®è©±ãªã©ããŠãŒã¶ãŒãè§Šããªãéãèšåããªã
ð 7. relationshipJudge (é¢ä¿æ§ææ Œ)
ð 2 段æ§ãã®å€å®
newPoints < min[next] â æ®ã眮ã
min[next] <= newPoints < forced â LLM judge (advance / wait)
wait ãªã 3 æ¥ cooldown
newPoints >= forced[next] â LLM ç¡èŠã§åŒ·å¶ææ Œ
LEVEL_MIN_THRESHOLDS = { 2: 10, 3: 30, 4: 80, 5: 200 }
LEVEL_FORCED_THRESHOLDS = { 2: 30, 3: 80, 4: 200, 5: 500 }
ð sub-call 1: judge
caller=judge / temperature=0.4 / json_schema strictã
å
¥åã«çŽè¿ 10 ä»¶ã® transcript + ä»ãã®çºè©± + 环èšèšé²æ°ãæž¡ãã{ decision, reason, opening_line } ãè¿ãããã
å€æè»ž: ãäºåç â 人éçããªå€åãäŒè©±ã«æ»²ãã§ãããã
ð sub-call 2: forced_opening
匷å¶ã©ã€ã³å°éæã®ããã£ãšè¿·ã£ãŠããåçœæçæã
caller=forced_opening / temperature=0.7 / max_tokens=100ãæ¬æã®ã¿è¿ãããã倱ææã¯åºå®ãã©ãŒã«ããã¯æã
ð sub-call 3: premium_unlock
ç¡æ â Premium 課éæã«ããã£ãšè©±ãããã£ããããããã¯ããããã£ãŠåŒã°ããŠããçæã
ç¡ææéäžã«è²¯ãŸã£ã preservedPoints ãããã£ãšè£ã§æããŠããããšèªãããã
ð ç¡æ / Premium / Trial ã® handling
- ç¡æ / è§£çŽæž:
effectiveLevel = min(raw, 2)ã§ Lv2 ã§é æã¡ - premium_active: raw level ãçŽ éã (Lv5 ãŸã§)
- trial_ends_at > now: åããçŽ éã (Premium åç)
- IAP launch åã¯æ°èŠãŠãŒã¶ã«é·æ trial ãä»äž â å®è³ªèª°ã§ã Premium çŽ
ð§ 8. ããŒãã¹ãšã³ãžãã¢ãªã³ã°
ð æŠèŠ
ãããã³ãããæžããã ãã§ãªããLLM ãæ¬çªéçšã«ä¹ããããã® ã¬ã¯ (harness) ã®èšèšã
shiwake ã§ã¯ 7 ã€ã®å±€ã«åããŠæŽçããŠããã
ð â çµ±äžãšã³ããªãã€ã³ã callOpenRouterChat
å
š LLM åŒã³åºã㯠apiUsageLog.ts ã®åäžé¢æ°ãçµç±ãããçŽæ¥ fetch ãæžããªã (analyzeMeal 㯠legacy ã§äŸå€)ã
callOpenRouterChat({
openRouterKey, caller, body, log
}) â { raw, data }
責å:
1. fetch (OpenRouter REST)
2. 倱ææã® Error æãçŽã ({caller}_http_{status})
3. usage.prompt_tokens / completion_tokens ãåãåºã
4. PRICING table ã§ USD ã³ã¹ãç®åº
5. api_usage_log ããŒãã«ã« INSERT (provider, caller, model, tokens, cost_usd, status, latency_ms)
ð â¡ ããã³ãããã£ãã·ã¥æé©å
- system prompt ã®å é ãå®å šåºå®: STATIC_PROMPT_PREFIX (ingest) / PERSONA_COMMON_RULES (persona)
- åºå®éš â å€åéš (Lv å¥ / userName å¥) ã® é åºãå³å®ãé åºãå€ãããš cache miss
- OpenAI / Gemini ã¯ãããã 5 åååŸã®èªåãã£ãã·ã¥ TTL ãæã€ã®ã§ãåäžãŠãŒã¶ãŒé£ç¶çºè©±ãªã hit ãã
- ãã£ãã·ã¥ hit åŸã®æã³ã¹ã詊ç®: 1 ãŠãŒã¶ à 150 ãªã¯ = ~Â¥42 / æ (ingest)
ð ⢠2 段éåŒã³åºã (ingest ãš mealLookup ã®åé¢)
ingest å ã§ web æ€çŽ¢ãåããªãçç±:
- web æ€çŽ¢ã® latency (~5-10 ç§) ã primary_response (= 200 OK) ãé å»¶ããã
- ingest ã¯ãã£ãã·ã¥éèŠã§è»œéåãmealLookup 㯠web æ€çŽ¢ç¹åã§ãéãã»é ãããåé¢
- mealLookup ã®çµæã¯
meal_nutrition_cacheã«ä¿åãã2 åç®ä»¥é㯠LLM ãåŒã°ãã«å³è¿åŽ - iOS åŽã¯ ãèšç®äžâŠã衚瀺 â polling ã§ UPDATE åæ ãšããäºçžã¢ãã«
ð ⣠åºå schema strict
- json_schema ã䜿ãåŒã³åºã㯠strict: true ãå¿ ãæå® (mealLookup / normalize_dish / judge / analyzeMeal)
- OpenAI strict mode ã®å¶çŽ:
requiredåæãããã£ãŒã«ã㯠null äžå¯ â null 蚱容ã«ããããã°type: ["number", "null"]åœ¢åŒ - tool calling åŽ (ingest) ãåãå¶çŽãcreate_meal 㯠11 required + null 蚱容ã§éãã
- Gemini ã json_schema åºåã« code fence ãä»ãã ããšãããã®ã§ãåãåãåŽã§
```jsonãå¥ãã sanitize åŠçãçµ±äž
ð †tool calling ã®æåå¶åŸ¡
- tool_choice: "auto" + parallel_tool_calls: true ãã»ãã (ingest)
- 1 çºè©±ã§è€æ° tool åŒã³åºããèš±å¯ããããšã§ãã©ãŒã¡ã³é£ã¹ãŠ 70kg ã ã£ããã 1 ãªã¯ãšã¹ãã§åŠç
- reply_only tool ãçšæããããšã§ãäœãèšé²ããªãéè«ããæç€ºçã«è¡šçŸ (è¿ããªãéžæãåãããªã)
- update_* / delete_* tool ã«ã¯ context ããæŸã£ã id ã匷å¶ããæé ãé²ã
ð ⥠ãã®ã³ã°ã»ã³ã¹ãèšæž¬
- api_usage_log ã«æ¯å 1 è¡ INSERT (provider, caller, model, tokens, cost_usd, status, latency_ms, user_id)
enqueueLogã§ fire-and-forget (waitUntil ã§éåæ INSERTãå¿ç latency ã«åœ±é¿ãããªã)- ãæè¡ / ã³ã¹ããã¿ãã§ caller å¥ã®éèšã衚瀺 (ã©ãã«ããã䜿ã£ãŠããå¯èŠå)
- 倱ææã status="error" + error_message ã§æ®ã â ãšã©ãŒçã¢ãã¿ãªã³ã°
ð ⊠ãšã©ãŒèæ§ / ãã©ãŒã«ããã¯
- relationshipJudge ã®å sub-call ã¯
try/catchã§å²ã¿ã倱ææã¯åºå®æãè¿ã (UX ã¯æ¢ããªã) - mealLookup ã倱æããŠã meal ã¬ã³ãŒãã¯æ®ã (kcal=0 ã®ãŸãŸãiOS ã§ãèšç®äžâŠã衚瀺ãç¶ã)
- persona ã空æåãè¿ãããåŒã³åºãåŽã§æ¡ãã€ã¶ã (空ã³ã¡ã³ãã iOS ã«æµããªã)
- ingest ããªãã©ã€ãå¿ èŠãšããç²åºŠã®å€±æ (rate limit ç) 㯠OpenRouter åŽã§åžåãããæ³å®
ð â§ ã¬ã€ãã³ã·èšæž¬ startTiming
startTiming(endpoint, userId) â tm
tm.mark("ingest_done") // ingest åŒã³åºãå®äº
tm.mark("primary_response_sent") // iOS ã« 200 ãè¿ããç¬é
tm.mark("calorie_complete") // mealLookup å®äº
tm.finalize() // user_perceived_latency_ms ã DB ã«èšé²
ãŠãŒã¶ãŒç®ç·ã®ã¬ã€ãã³ã· (çºè©± â 200 OK ãŸã§ã®æé) ãšãè£ã§ã«ããªãŒèšç®ãçµãããŸã§ã®æéãåããŠèšæž¬ã
ð° 9. ã³ã¹ã詊ç®
ð PRICING ããŒãã« (apiUsageLog.ts)
| ã¢ãã« | input ($/1M tok) | output ($/1M tok) | web æ€çŽ¢ |
|---|---|---|---|
| openai/gpt-4o-mini | $0.15 | $0.60 | â |
| openai/gpt-4o | $2.50 | $10.00 | â |
| google/gemini-3-flash-preview | $0.50 | $3.00 | â |
| google/gemini-3-flash-preview:online | $0.50 | $3.00 | +$0.005 / call |
ð æ³å® 1 çºè©±ãããã®ã³ã¹ã
| åŒã³åºã | typical input | typical output | $ / çºè©± |
|---|---|---|---|
| ingest (Gemini 3 Flash) | ~3000 tok (cache hit æ³å®) | ~300 tok | ~$0.002 |
| persona (gpt-4o-mini) | ~1500 tok | ~100 tok | ~$0.0003 |
| mealLookup (online, 1é£) | ~800 tok | ~150 tok | ~$0.006 (web 蟌) |
| judge (çºç«é »åºŠ ~1%) | ~800 tok | ~80 tok | ~$0.0002 |
| åèš (é£äº 1 ä»¶ããçºè©±) | ~$0.008 / çºè©± |
ð 1 ãŠãŒã¶æã³ã¹ã詊ç®
DAU æ³å®: 1 æ¥ 3-5 çºè©± â æ ~120 çºè©± â ~$1.0 / æ / ãŠãŒã¶ãŒ (~Â¥150)
Premium äŸ¡æ Œ Â¥980 / æ æ³å®ãªã®ã§ gross margin ~85%ãsoniox STT (~$0.001 / 30sec çºè©±) ãè¶³ããŠãäœè£ã
ð¡ å®çžŸã¯ãæè¡ / ã³ã¹ããã¿ãã§ api_usage_log ã caller å¥ / model å¥ã«éèšè¡šç€ºã
ð 10. çŸè¡ããã³ãã (æ¬çªãããã€äžã®æ¬æ)
GET /api/admin/prompts ãå©ããŠãåã¢ãžã¥ãŒã«ã export ããŠãã ããã³ãã宿°ããã®ãŸãŸè¿ãã
åçéšå (Lv / userName / çŽ¯èš / preservedPoints ç) 㯠admin åŽã§ãµã³ãã«åŒæ°ãå
¥ããŠã¬ã³ããªã³ã°ããæååã
ð¡ ãã®ããŒãžã¯éçããã¥ã¡ã³ããæŽæ°ã¯ admin/index.html ã® data-tab="llm" ã»ã¯ã·ã§ã³ã
ããã³ããæ¬äœãã¢ãã«å€æŽæã« PR ã§åãããŠçŽããå®ã³ãŒããšä¹é¢ããªãããã倿޿ã¯ãã®ããŒãžã®è©²åœç®æãæŽæ°ããããšã
Section 10 㯠API ããæ¬çªå€ãçŽæ¥åãã®ã§ãã³ãŒããšä¹é¢ããªã (export ã墿žããã admin.ts ã® /api/admin/prompts ãæŽæ°)ã