Files
inkling/docs/superpowers/specs/2026-05-09-v031-cut-f-design.md
altair823 7d2b8c95ec docs(v028+): F17~F25 dogfood + roadmap + Cut A~G specs + Cut A plan
v0.2.7 release 후 dogfood 9건 누적 (F17~F25) 정리:
- F17 휴지통 의미 분기 / F18 사유 입력 / F19 recall / F20 raw_text 가변
- F21 다기기 sync / F22 이미지 렌더링 (이미 v0.2.8 promoted) / F23 Ollama-less
- F24 멀티모달 vision / F25 사이드바 + 저장소

추가:
- v0.2.8+ roadmap: 7 cut 분할 (A~G), 12주 시간선, dependency graph
- Cut A~G design specs (각 cut 별 design 결정 + schema + UI + 테스트 전략)
- Cut A implementation plan (이미 v0.2.8 머지로 실행 완료, 참고 보존)

PR #26 머지 후 main 에 doc commits rebase 안 되어 manual merge 진행:
- F22 entry 는 origin/main 의 promoted 형태 우선
- 신규 9 파일 (specs/plan/roadmap) 은 origin/main 에 없는 파일
- "다음 항목 자리" 안내 F23 → F26 갱신

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:09:02 +09:00

8.5 KiB
Raw Blame History

v0.3.1 — Cut F Design (멀티모달 vision AI)

작성일: 2026-05-09 선행 문서:

  • docs/superpowers/specs/2026-04-25-dogfood-feedback.md (F24)
  • docs/superpowers/strategy/v028plus-roadmap.md Cut F

Cut 라벨: v0.3.1 — patch (vision 추가, 기존 기능 영향 X)


1. Cut 정체성

Ollama vision 모델 (gemma3 family default) 활용 — 이미지 + raw_text 결합 prompt 또는 이미지 단독 분석 → title/summary/tags 자동 생성. F22 prerequisite (Cut A) 이미 완료.


2. 범위

항목 결정
F24 default 모델 gemma3 family (한국어 + 이미지 둘 다 강함, 본인 메모 gemma4:e4b 텍스트 모델과 같은 가족)
prompt 모드 단일 vision 모델 호출 (vision 모델이 텍스트도 처리). 모델 capability 부족 시 2단계 fallback (자동)
capability detection app launch 시 1회 + 설정 페이지 manual refresh 버튼
F23 OFF 시 자동 OFF ai_enabled=false → vision 도 자동 OFF (자명)

3. Capability Detection

3-1. Ollama API 활용

GET /api/tags → 사용자 Ollama instance 의 모델 목록. response:

{
  "models": [
    { "name": "gemma4:e4b", "details": { "family": "gemma" } },
    { "name": "gemma3:12b-vision", "details": { "family": "gemma3", "families": ["gemma3"] } },
    { "name": "llava:13b", "details": { "family": "llava" } }
  ]
}

vision capable 판정 — 모델 이름 또는 family 기반:

const VISION_FAMILIES = new Set(['gemma3', 'llava', 'llama3.2-vision', 'minicpm-v', 'pixtral']);
const VISION_NAME_HINTS = ['vision', 'vl', 'multimodal', 'gemma3'];

function isVisionCapable(model: { name: string; details?: { family?: string; families?: string[] } }): boolean {
  if (model.details?.family && VISION_FAMILIES.has(model.details.family)) return true;
  if (model.details?.families?.some(f => VISION_FAMILIES.has(f))) return true;
  return VISION_NAME_HINTS.some(h => model.name.toLowerCase().includes(h));
}

3-2. Settings storage

interface SettingsSchema {
  // ... 기존
  vision_model?: string;       // 사용자 명시 모델 (빈 값 = 비활성)
  vision_capable_cache?: string[];  // launch 시 detected 결과 cache
  vision_cache_at?: string;    // ISO timestamp
}

3-3. AppLaunchDetect

// src/main/index.ts whenReady 안 (settings 초기화 후)
async function refreshVisionCache(): Promise<void> {
  if (!settingsService.get('ai_enabled', true)) return;
  try {
    const tags = await fetch(`${endpoint}/api/tags`).then(r => r.json());
    const capable = tags.models.filter(isVisionCapable).map((m: any) => m.name);
    settingsService.set('vision_capable_cache', capable);
    settingsService.set('vision_cache_at', new Date().toISOString());
  } catch {
    // network fail — silent, cache 유지
  }
}

void refreshVisionCache();

3-4. 설정 페이지 UI (AI 제공자 섹션 확장)

[AI 제공자]
Endpoint: [http://localhost:11434]
모델: [gemma4:e4b]

[이미지 분석 모델 (선택사항)]
[gemma3:12b-vision ▾]   ← dropdown, 비어 있으면 비활성
   가능한 모델: gemma3:12b-vision, llava:13b, ...
[ 다시 감지 ]   마지막 감지: 2026-05-09 14:30

dropdown — vision_capable_cache 결과 + 빈 옵션. "다시 감지" → refreshVisionCache() + UI 갱신.


4. InferenceProvider 확장

4-1. 인터페이스

// src/main/ai/InferenceProvider.ts
interface GenerateInput {
  text: string;
  images?: Array<{ base64: string; mime: string }>;  // NEW
  todayKst: string;
  dueDateCandidates: string[];
  vocab?: string[];
}

interface InferenceProvider {
  generate(input: GenerateInput, opts?: { visionModel?: string }): Promise<AiResponse>;
  abort?(): void;
}

4-2. LocalOllamaProvider 갱신

async generate(input: GenerateInput, opts?: { visionModel?: string }): Promise<AiResponse> {
  const useVision = !!opts?.visionModel && (input.images?.length ?? 0) > 0;
  const model = useVision ? opts.visionModel : this.textModel;

  const body: any = {
    model,
    prompt: useVision
      ? buildVisionPrompt(input.text, input.todayKst, input.dueDateCandidates, input.vocab ?? [])
      : buildPrompt(input.text, input.todayKst, input.dueDateCandidates, input.vocab ?? []),
    stream: false,
    format: 'json'
  };
  if (useVision) {
    body.images = input.images!.map(i => i.base64);
  }

  const res = await request(`${this.endpoint}/api/generate`, body);
  // ... 기존 parse
}

4-3. buildVisionPrompt

function buildVisionPrompt(text: string, todayKst: string, dueCandidates: string[], vocab: string[]): string {
  return `다음 메모와 첨부 이미지를 종합 분석해 한국어로 요약하세요.

메모 본문 (비어 있을 수 있음):
${text || '(이미지만 있음)'}

이미지 분석 시 주요 시각적 정보 (텍스트, 사람, 장면) 도 포함해 요약하세요.
출력 JSON: { "title": "...", "summary": "...", "tags": [...], "due_date": "..." }
오늘: ${todayKst}
가능한 due 후보: ${dueCandidates.join(', ')}
빈출 태그: ${vocab.slice(0, 20).join(', ')}`;
}

5. AiWorker 통합

CaptureService 가 capture 시 image 첨부했으면 → notes.media 에 저장 + pending_jobs INSERT. AiWorker 가 job 처리 시:

// src/main/ai/AiWorker.ts
async processJob(noteId: string): Promise<void> {
  const note = this.repo.getById(noteId);
  const media = this.repo.listMediaByNote(noteId);
  const visionModel = this.settings.get('vision_model');

  let images: Array<{ base64: string; mime: string }> | undefined;
  if (visionModel && media.length > 0) {
    images = await Promise.all(media.map(async (m) => ({
      base64: (await fs.readFile(this.mediaStore.absolutePath(m.relPath))).toString('base64'),
      mime: m.mime
    })));
  }

  const provider = this.providerHolder.get();
  const response = await provider.generate({ text: note.rawText, images, ... }, { visionModel });
  // ... 기존 결과 적용
}

media.length > 0 && visionModel 둘 다 true 일 때만 vision path. 그 외는 기존 text-only.


6. 이미지만 있는 capture

raw_text 빈 값 + media 첨부만:

  • 기존 동작: notes INSERT (raw_text=''), AiWorker 가 빈 prompt 로 호출 → ai_status='failed' 또는 무의미 응답
  • vision enabled: AiWorker 가 vision prompt + images → 의미 있는 title/summary/tags 응답
  • vision disabled (visionModel 빈 값): notes 저장만, ai_status='disabled' 신규 enum 활용 (Cut B 의 ai_enabled false 와 비슷한 의미 — 그러나 부분 disable, 즉 "이미지 only 라 처리 불가" 상태)

추천: vision disabled + image-only capture 시 ai_status='skipped' 신규 enum (Cut B 의 'disabled' 와 다름). title fallback = "(이미지 N개)" 또는 첫 이미지 파일명.


7. 테스트 전략

영역 단위
isVisionCapable family / families / name hint 별 판정
refreshVisionCache mock /api/tags → capable 추출 + settings 저장
설정 페이지 dropdown cache 기반 옵션 + "다시 감지" 클릭 → IPC
LocalOllamaProvider.generate vision path images 비어있음 → text-only / images 있음 + visionModel → vision body
buildVisionPrompt 빈 text + images 만 케이스 정확 prompt
AiWorker.processJob vision integration media + visionModel 있을 때만 base64 변환
이미지 only capture raw_text='' + media → vision 결과 정상 또는 'skipped' 분기

목표: 단위 555 → 약 575 (+20), typecheck 0.


8. Risk

Risk 대응
vision 모델 추론 latency 큼 (수 초~분) AiWorker backend 처리 — 사용자 대기 X. NoteCard 가 ai_status='processing' 표시
이미지 base64 메모리 부담 media 1개당 평균 < 1MB. 다중 이미지 시 N×base64 = 메모리 N배. cap (이미지당 max size 5MB) 적용
capability detection 실패 시 fallback cache 부재 → vision dropdown 비어있음 표시 + "다시 감지" 안내
vision 모델 한국어 정확도 dogfood 검증. gemma3 가 한국어 약하면 다른 family 추천 갱신 (메모리 정책 갱신)
Ollama 가 vision images 필드 무시 (모델이 multimodal 미지원) 자동 2단계 fallback — vision 모델로 caption 추출 → 텍스트 모델로 종합 (capability 부족 시)

9. v0.3.1 후

Cut G (v0.3.2) — F25 사이드바 + notebook_id.

dogfood verify:

  1. 이미지 capture 빈도 (가설: 일 ≥ 1건 = vision 가치)
  2. vision 결과 사용자 수정 비율 (정확도 측정)
  3. capability detection 정확도 (false-positive / false-negative)