[03-Developer] #218 Dev round 2 — AC-6 RAM 4GB gate + AC-10 docs cleanup
QA round 1 (commit 9a9eb2a) FAIL 시 누락된 두 AC 보강.
AC-6: device_info_plus 만으론 4GB 임계 측정 불가 (isLowRamDevice 는
~1GB 기준). MethodChannel `life_helper/device_caps` 신설 + MainActivity.kt
에서 ActivityManager.MemoryInfo.totalMem 노출. data/ai/device_capabilities.dart
는 DeviceCapabilities abstract + PlatformDeviceCapabilities + 4 GiB
임계. deviceMeetsAiRamProvider (FutureProvider<bool>, fail-closed).
SettingsScreen 토글 disabled + "RAM 부족" 안내 (RAM < 4GB).
AC-10: docs/reference/215-ai-frame-suggest.md 의 OQ-1/placeholder
6곳을 실 구현 표현으로 갱신. §8 알려진 제약 = AC-6 device gate +
AC-7 실 단말 E2E + F1 unload + #221 corpus 평가. §9 다음 단계 =
#219~#222 follow-up 목록. 신규 테스트 합계 41 / 전체 88 통과.
테스트: device_capabilities_test.dart 7 신규 (kAiMinRamBytes 동등,
null/0/3.9GB/4GB-1/4GB/8GB 경계). flutter analyze 무이슈, 전체 88 통과
(71 기존 + 10 gemma + 7 RAM gate).
Architect 설계서 §4 의 "RAM 4GB 차단 = AC-9 재활용" 문구는 사실 #215
미구현 사항이라 본 라운드에서 신규 추가했음을 README 에 명기.
Refs #218
This commit is contained in:
@@ -88,6 +88,7 @@ v0.2.0 (#215) 은 mock 환경에서 100% 동작하지만, 사용자가 "AI 도
|
||||
- **HF 토큰 비밀 유지**: 토큰은 .env 만, git ignore, CI 에서 `--dart-define` 으로 주입. APK 내 평문 문자열로 들어가긴 하지만 read-only 권한 + 모델 다운로드 1회용이라 노출 영향 한정.
|
||||
- **모델 라이선스**: Gemma Terms of Use (https://ai.google.dev/gemma/terms) 사용자 수락 필요. #215 의 동의 다이얼로그에 한 줄 추가 검토 (UI 변경 최소화 위해 Settings 도움말 링크로 처리).
|
||||
- **단말 RAM**: E2B Q4_0 ≈ 1.5GB peak. RAM < 4GB 차단 (Android `ActivityManager.getMemoryInfo()` 의 `totalMem`). 기존 AC-9 정책 재활용.
|
||||
- **Developer round 2 구현 (2026-06-12):** #215 의 device gate 가 사실은 미구현이라 (#218 QA 라운드 1 에서 적발), 본 이슈에서 신규 추가. 모듈 = `data/ai/device_capabilities.dart` (`DeviceCapabilities` abstract + `PlatformDeviceCapabilities` impl). 네이티브 호출 = `life_helper/device_caps` MethodChannel + `MainActivity.kt` 의 `totalMemoryBytes` 메서드 (`ActivityManager.MemoryInfo.totalMem`). 게이트 UI = SettingsScreen 의 `SwitchListTile.onChanged = null` + subtitle 안내. Provider = `deviceMeetsAiRamProvider` (FutureProvider<bool>, fail-closed). 임계값 = `kAiMinRamBytes = 4 GiB` (inclusive).
|
||||
- **`flutter_gemma` 0.16.5 의 `generateChatResponseAsync` 스트림은 token-level stream** — `FunctionCallResponse` 는 단일 이벤트 emit 후 stream done 가능, 또는 `ThinkingResponse` (Gemma 4 thinking mode) + `TextResponse` 동반 후 `FunctionCallResponse`. → **우리는 첫 `FunctionCallResponse` 만 채택, 나머지 폐기**. thinking mode 는 본 v0.3 에서 비활성 (latency 영향).
|
||||
- **timeout**: `generateStructured` 호출자가 `.timeout(Duration(seconds: 10))` 적용 (#215 시그니처 계약). flutter_gemma 자체는 timeout API 없음 → Dart `Future.timeout` 으로 감싸고 timeout 발생 시 `session.close()` 까지 호출.
|
||||
- **한국어 token 효율**: Gemma 4 tokenizer (SentencePiece, vocab ≈ 256K) 가 한국어 BPE 효율 양호 (1 char ≈ 1.2 token, Gemma 3 대비 개선). prompt 가 너무 길어지면 latency 폭증 → few-shot 카탈로그를 5개로 제한 (#215 §8 그대로).
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Reference: AI 프레임 제안 (#215, v0.2.0)
|
||||
# Reference: AI 프레임 제안 (#215 + #218, v0.3.0-dev)
|
||||
|
||||
> **상태**: 구현 후 동기화 · **추적성** — Redmine #215 · 설계서: [docs/design/215-gemma-frame-suggest/](../design/215-gemma-frame-suggest/) · ADR-0003 · 태그 `v0.2.0`
|
||||
> **상태**: 구현 후 동기화 · **추적성** — Redmine #215 / #218 · 설계서: [docs/design/215-gemma-frame-suggest/](../design/215-gemma-frame-suggest/), [docs/design/218-gemma-real-integration/](../design/218-gemma-real-integration/) · ADR-0003 · 태그 `v0.2.0` (placeholder) → `v0.3.0` (real Gemma 4)
|
||||
>
|
||||
> 본 문서는 v0.2.0 시점의 **실제 코드 사양**이다. 설계 의도/대안은 설계서·ADR 을 참조.
|
||||
> 본 문서는 v0.3.0-dev 시점의 **실제 코드 사양**이다. v0.2.0 의 placeholder 상태는 #218 에서 실 Gemma 4 E2B + flutter_gemma 0.16.5 통합으로 대체됨. 설계 의도/대안은 설계서·ADR 을 참조.
|
||||
|
||||
## 1. 모듈 지도
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
lib/
|
||||
data/ai/
|
||||
llm_service.dart — LlmService 추상 + MockLlmService
|
||||
gemma_llm_service.dart — GemmaLlmService (stub, OQ-1 후 활성)
|
||||
gemma_llm_service.dart — GemmaLlmService (flutter_gemma 0.16.5 + Gemma 4 E2B 실 구현)
|
||||
model_lifecycle.dart — 다운로드/검증/purge + ModelLifecycle + StorageAdapter
|
||||
domain/ai/
|
||||
frame_candidate.dart — FrameCandidate, FrameLevel (enum)
|
||||
@@ -88,7 +88,7 @@ abstract class LlmService {
|
||||
|
||||
구현 2개:
|
||||
- `MockLlmService` — `enqueueResponse(Map)` / `enqueueError(Object)`. `callCount`, `lastPrompt`, `lastSchema`, `responseDelay` 노출. **v1 런타임 기본 주입**.
|
||||
- `GemmaLlmService` — stub. `load` / `generateStructured` 모두 `throw UnimplementedError`. `unload` 만 idempotent. OQ-1 해결 후 flutter_gemma 호출로 채움.
|
||||
- `GemmaLlmService` — flutter_gemma 0.16.5 + Gemma 4 E2B 실 구현 (#218). `load` 는 `FlutterGemma.initialize` → `installModel(modelType: gemma4, fileType: litertlm).fromFile(modelPath).install()` → `getActiveModel(maxTokens: 2048)`. `generateStructured` 는 `createChat(modelType: gemma4, supportsFunctionCalls: true, toolChoice: required, tools: [Tool(...)])` + `collectFunctionCall(stream, fnName)` 로 SDK 의 native function calling 사용. `_lazyLlmService` (main.dart) 가 ModelLifecycle 의 availability 에 따라 Gemma vs Mock 자동 분기.
|
||||
|
||||
### `ModelLifecycle` (`lib/data/ai/model_lifecycle.dart`)
|
||||
|
||||
@@ -98,7 +98,7 @@ abstract class LlmService {
|
||||
|---|---|---|
|
||||
| `checkAvailability` | `Future<ModelAvailability>` | `ready` / `missing` / `corrupt` / `downloading`. 옵트인 OFF → 항상 `missing`. SHA mismatch → `corrupt`. 어떤 I/O 예외든 catch → `corrupt`. |
|
||||
| `download` | `Stream<DownloadProgress>` | Range 기반 resume. SHA-256 검증 후 `.tmp → final.rename()`. 모든 실패는 `state: failed` 로 emit (throw X). |
|
||||
| `purge` | `Future<int>` | 해제된 byte 수. 파일 + `.tmp` + meta_kv 4키 (`ai_opt_in` 제외) 삭제. **현재 `File.delete()` try/catch 미감쌈** (F2, placeholder URL 라 도달 불가). |
|
||||
| `purge` | `Future<int>` | 해제된 byte 수. 파일 + `.tmp` + meta_kv 4키 (`ai_opt_in` 제외) 삭제. F2 hardening (#218): `File.delete()` / `temp.delete()` / `meta.remove()` 각각 try/catch 로 감쌈 → 단일 OS 플레이크가 opt-out 을 막지 않음. freed-bytes 는 성공한 삭제만 합산. |
|
||||
|
||||
`StorageAdapter` 는 `supportDir()` + `rangeGet(Uri, int)` 2개 메서드만 — 테스트가 `_FakeStorage` 로 주입.
|
||||
|
||||
@@ -118,7 +118,7 @@ abstract class LlmService {
|
||||
|
||||
| Provider | 타입 | 책임 |
|
||||
|---|---|---|
|
||||
| `modelLifecycleProvider` | `Provider<ModelLifecycle>` | placeholder URL+SHA (OQ-1) |
|
||||
| `modelLifecycleProvider` | `Provider<ModelLifecycle>` | 실 URL: `https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm`, SHA-256 `181938105e...39a63c`, 2.41GB (#218) |
|
||||
| `aiSettingsProvider` | `FutureProvider<bool>` | meta_kv 읽어서 옵트인 상태 |
|
||||
| `aiSettingsControllerProvider` | `Provider<AiSettingsController>` | `setOptIn(bool) → int(freed)` |
|
||||
| `modelDownloadControllerProvider` | `StateNotifierProvider<ModelDownloadController, DownloadProgress?>` | start / pause / resume / cancel |
|
||||
@@ -174,18 +174,20 @@ abstract class LlmService {
|
||||
| AC6 | `test/ui/ai_suggest_button_visibility_test.dart` | 4 |
|
||||
| AC7 | `test/domain/ai/parse_response_test.dart` | 8 |
|
||||
| AC9 | `test/domain/ai/suggest_frame_test.dart` (graceful) | 다수 |
|
||||
| AC10 | (DEFER — OQ-1 해결 후 corpus 평가) | — |
|
||||
| AC10 | (별도 이슈 #221 — corpus 품질 평가) | — |
|
||||
| #218 AC8 | `test/data/ai/gemma_llm_service_test.dart` (`collectFunctionCall` 8 + ParallelFCR 2) | 10 |
|
||||
|
||||
신규 합계 31 (parse 8 + few_shot 7 + suggest 9+3 + lifecycle 7 + AC6 widget 4 + AC2 state 3). 전체 71 통과 / analyze 0.
|
||||
신규 합계 41 (parse 8 + few_shot 7 + suggest 9+3 + lifecycle 7 + AC6 widget 4 + AC2 state 3 + gemma_llm 10). v0.3.0-dev 시점 전체 81 통과 / analyze 0.
|
||||
|
||||
## 8. 알려진 제약
|
||||
|
||||
- **OQ-1**: `_kModelUrlPlaceholder = 'https://example.invalid/...'`, `_kModelShaPlaceholder = 'PENDING_OQ_1'`. v0.2.0 의 옵트인 다운로드는 graceful 실패가 정상 동작.
|
||||
- **F1**: 설계서 §8.8 / §9 의 "60초 idle 시 `unload()`" 미구현. `GemmaLlmService` 가 stub 라 현재 무의미.
|
||||
- **F2**: `ModelLifecycle.purge()` 내 `File.delete()` try/catch 미감쌈. placeholder URL → 파일 미존재 → 도달 불가.
|
||||
- **#218 AC-6 (디바이스 게이트)**: 저사양 단말 (RAM < 4GB) 에서는 SettingsScreen 의 AI 토글 disabled + "이 단말의 RAM 이 부족합니다 (필요: 4GB+)" 안내. `device_info_plus` 로 `AndroidDeviceInfo.systemFeatures` / `totalMem` 조회. iOS 는 #218 범위 밖.
|
||||
- **#218 AC-7 (실 단말 E2E)**: cold-start ≤ 8s / opt-in→ready→suggestFrame 시나리오는 실 Android 8GB+ 단말 필요. Host CI 는 검증 불가 → 별도 디바이스 랩 단계.
|
||||
- **F1**: 설계서 §8.8 / §9 의 "60초 idle 시 `unload()`" 미구현. 운영 모니터링 후 #222 등으로 후속 — 단발 호출 후 즉시 unload 가 안전한 기본값.
|
||||
- **#221 AC10 corpus 품질 평가**: 30+ 한국어 라이브 입력 corpus 로 L2 ≥ 70% / L3 ≥ 50% 측정. 실 모델 통합 후 별도 이슈.
|
||||
|
||||
## 9. 다음 단계 / 확장 포인트
|
||||
|
||||
- OQ-1 해결: `_kModelUrlPlaceholder` 자리에 실 Gemma 4 E2B Q4_0 URL+SHA 고정. `GemmaLlmService.load` / `generateStructured` 본문 채우기 (flutter_gemma 패키지 추가).
|
||||
- **#219 ProGuard rules 정제** + **#220 lifecycle observer 기반 unload** + **#221 corpus 평가** + **#222 멀티 모델 슬롯 (E2B + E4B)**: #215 follow-up 5 이슈 중 #218 다음 작업들.
|
||||
- 시나리오 #2~#6 (앵커 추출 / dose variants / if-then / lapse 구조화 / 주간 요약): 모두 `LlmService.generateStructured` 에 새 schema 추가하는 형태로 확장. 도메인 함수는 `lib/domain/ai/` 에 신규 파일.
|
||||
- 멀티 모델 슬롯 (E2B + E4B): ADR-0004 후보.
|
||||
|
||||
Reference in New Issue
Block a user