P6-1/P6-2/P6-3 의 라이브러리 (`ImageExtractor`, `OllamaVisionOcr`,
`apply_caption`) 가 그동안 CLI 에서 보이지 않던 미완 구간을 완성.
이제 `kebab ingest` 가 markdown 외에 이미지 자산을 end-to-end 로
색인하고, `kebab search` / `kebab ask` 가 OCR 텍스트 + caption 으로
이미지를 매칭/인용한다.
## kebab-app
- `[dependencies]` 에 `kebab-parse-image` 추가.
- `ingest_with_config` 진입 시 `image.ocr.enabled` / `image.caption.enabled`
플래그에 따라 `OllamaVisionOcr` / `OllamaLanguageModel` 을 **ingest
세션당 1회** 빌드. 자산 루프에서 trait object 로 공유.
reqwest::blocking::Client 의 내부 Arc 덕분에 알로케이션 비용은
자산 수와 무관.
- 두 어댑터 + ImageExtractor 를 한 묶음으로 `ImagePipeline` 구조체에
담아 `ingest_one_asset` 매개변수 폭증 차단 (clippy::too_many_arguments
대응).
- `ingest_one_asset` 의 markdown-only 가드를 `match media_type` 으로
교체 — Markdown 은 기존 경로, Image(_) 는 새 `ingest_one_image_asset`
로 분기, PDF/Audio/Other 는 종전대로 skipped.
- 신규 `ingest_one_image_asset`:
- bytes 읽기 → `ImageExtractor::extract` (실패 시 caller 가 errors+=1)
- `apply_ocr` (Lenient — 실패 시 ProvenanceKind::Warning 이벤트 +
`IngestItem.warnings` 에 \"ocr_failed: ...\", `block.ocr` 는 None
유지)
- `apply_caption` (동일 Lenient 정책)
- 기존 `MdHeadingV1Chunker` 호출 — 청커는 이미 `Block::ImageRef` 를
단일 청크로 emit
- 기존 persist + embed 시퀀스 그대로 (markdown 과 byte-identical)
- `lang_hint_from_doc` — `Lang(\"und\")` 또는 빈 문자열을 None 으로
매핑 (image-pipeline 어댑터의 build_prompt 가 \"und\" 를 silent drop
하지 않도록 caller 측에서 미리).
## kebab-chunk
- `render_block_text` 의 `Block::ImageRef` 분기를 P6-4 (β) plain
concat 정책으로 교체 — `[alt, ocr.joined, caption.text]` 를 `\\n\\n`
로 join, 빈 부분은 drop. alt 가 비면 `src` 의 basename 으로 fallback
(P6-1 contract 의 defensive guard).
- 신규 unit 테스트 `image_ref_p6_4_plain_concat_drops_empty_parts` —
alt-only / alt+ocr / alt+caption / alt+ocr+caption / 빈 alt → src
fallback 다섯 케이스 모두 검증.
- 기존 `image_ref_emits_own_chunk_zero_tokens` 그대로 통과 — 청커의
per-block dispatch 는 변경 없음, text 렌더링만 갱신.
## 통합 테스트 (kebab-app/tests/image_pipeline.rs)
wiremock 으로 Ollama 를 stub. 5건:
1. OCR-only happy path — 1 PNG + ocr.enabled → 1 doc + 1 chunk emit,
`block.ocr.joined` 가 mock 의 \"Hello World 2026\".
2. OCR + caption 동시 활성 — 두 필드 모두 채워지고 chunk text 에
alt + ocr + caption 세 부분 모두 포함.
3. Lenient 실패 검증 — OCR 503 시 자산은 indexed (kind=New),
`errors=0`, ProvenanceKind::Warning attributed to \"kb-app\",
`IngestItem.warnings` 에 \"ocr_failed:\" 노트.
4. 양쪽 비활성 — `image.ocr.enabled=false && image.caption.enabled=false`
여도 자산은 chunk 1개로 indexed (chunk text=filename), EXIF +
dimensions 그대로 채워짐.
5. 결정성 (re-ingest) — 동일 PNG 두 번 ingest 시 두 번째는
`Updated` + 동일 `doc_id`.
## SMOKE.md
`kebab search --mode lexical \"Hello World\"` 단계를 명령 시퀀스에
추가. `[image.ocr]` / `[image.caption]` config 절 예시 + ingest 시간
추정 (자산당 ~5-10초) 추가. \"책은 P7 PDF 라인으로\" 가이드를 검증
체크리스트 와 \"알려진 동작\" 양쪽에 박음.
## 실 Ollama 통합 검증
192.168.0.47 + gemma4:e4b 기준:
```
$ kebab --config /tmp/kebab-smoke/config.toml ingest
scanned 2 new 2 updated 0 skipped 0 errors 0 (18395 ms)
$ kebab inspect doc <image_doc_id>
parser_version: image-meta-v1
blocks: [{
alt: \"hello.png\",
ocr: \"Hello World 2026\",
caption: \"The image displays the text \\\"Hello World 2026\\\" in a large, black, sans-serif font.\"
}]
$ kebab --json ask \"Hello World 텍스트가 어디에 있나?\" --mode hybrid
grounded: true
citations: [{marker: \"[1]\", doc_path: \"hello.png\"}]
```
## 검증
- `cargo test --workspace --no-fail-fast -j 1` — 전부 pass
- `cargo clippy --workspace --all-targets -- -D warnings` — pass
- `cargo test -p kebab-chunk image_ref` — 2 pass (P1-5 회귀 + P6-4
신규 unit)
- `cargo test -p kebab-app --test image_pipeline` — 5 pass
## 의존성 경계
- `kebab-app` 이 `kebab-parse-image` 추가 — spec Allowed dep 그대로.
- 새 forbidden 침범 없음 (기존 `kebab-tui` / `kebab-desktop` /
`kebab-eval` 미참조 유지).
- 본 task 가 신설하는 image-specific 비즈니스 로직 0줄 — 모두
`kebab-parse-image` 에 위임.
`tasks/p6/p6-4-image-ingest-wiring.md` status: planned → completed.
contract: docs/superpowers/specs/2026-04-27-kebab-final-form-design.md
sections: §3.4 ImageRefBlock, §6.1 ingest pipeline, §7.2
Extractor/Chunker traits, §9.1 image extraction policy.
187 lines
7.6 KiB
Markdown
187 lines
7.6 KiB
Markdown
---
|
||
title: "kebab 스모크 실행 가이드"
|
||
date: 2026-05-01
|
||
---
|
||
|
||
# kebab 스모크 실행 가이드
|
||
|
||
P3-5 머지 후 (`kebab-app::ingest` / `search` / `list` / `inspect` 와이어링) 부터, 그리고 P4-3 머지 후 (`kebab ask` 와이어링) 부터 사용자가 자기 설치본을 직접 검증할 수 있다. 이 문서는 사용자 환경 (`~/.config/kebab/`, `~/.local/share/kebab/`) 을 건드리지 않고 임시 디렉토리에 격리된 KB 를 띄워 전체 파이프라인을 1세션 안에 한 번 돌리는 절차다.
|
||
|
||
## 준비
|
||
|
||
빌드:
|
||
|
||
```bash
|
||
cargo build --release -p kebab-cli # debug 도 무방. 디버그가 더 빠르게 빌드됨.
|
||
```
|
||
|
||
원격 Ollama (선택, `kebab ask` 만 필요):
|
||
|
||
```bash
|
||
# Mac 등 별도 호스트에서
|
||
OLLAMA_HOST=0.0.0.0:11434 ollama serve
|
||
ollama pull gemma4:26b # 또는 qwen2.5:32b 등 — 자세한 비교는 README
|
||
```
|
||
|
||
본 머신에서 reachability 검증:
|
||
|
||
```bash
|
||
curl http://<host>:11434/api/tags
|
||
```
|
||
|
||
`{"models": [...]}` 가 나오면 네트워크 + 방화벽 OK.
|
||
|
||
## 격리된 워크스페이스 생성
|
||
|
||
```bash
|
||
mkdir -p /tmp/kebab-smoke/{workspace,data}
|
||
cat > /tmp/kebab-smoke/workspace/intro.md <<'EOF'
|
||
---
|
||
title: 인사말
|
||
tags: [demo]
|
||
lang: ko
|
||
---
|
||
# 안녕
|
||
|
||
이 문서는 스모크 테스트 fixture 다.
|
||
EOF
|
||
```
|
||
|
||
여러 파일을 시드하고 싶으면 본인 KB 일부를 `cp -r` 으로 복사해도 좋다 (다음 절차는 6개 markdown 가정).
|
||
|
||
## 격리된 config
|
||
|
||
`/tmp/kebab-smoke/config.toml`:
|
||
|
||
```toml
|
||
schema_version = 1
|
||
|
||
[workspace]
|
||
root = "/tmp/kebab-smoke/workspace"
|
||
include = ["**/*.md"]
|
||
exclude = [".git/**", "node_modules/**", ".obsidian/**"]
|
||
|
||
[storage]
|
||
data_dir = "/tmp/kebab-smoke/data"
|
||
sqlite = "{data_dir}/kebab.sqlite"
|
||
vector_dir = "{data_dir}/lancedb"
|
||
asset_dir = "{data_dir}/assets"
|
||
artifact_dir = "{data_dir}/artifacts"
|
||
model_dir = "{data_dir}/models"
|
||
runs_dir = "{data_dir}/runs"
|
||
copy_threshold_mb = 100
|
||
|
||
[indexing]
|
||
max_parallel_extractors = 2
|
||
max_parallel_embeddings = 1
|
||
watch_filesystem = false
|
||
|
||
[chunking]
|
||
target_tokens = 500
|
||
overlap_tokens = 80
|
||
respect_markdown_headings = true
|
||
chunker_version = "md-heading-v1"
|
||
|
||
[models.embedding]
|
||
provider = "fastembed" # "none" 으로 두면 lexical-only — Ollama 불필요
|
||
model = "multilingual-e5-small"
|
||
version = "v1"
|
||
dimensions = 384
|
||
batch_size = 64
|
||
|
||
[models.llm]
|
||
provider = "ollama"
|
||
model = "gemma4:26b" # 사용자 환경에 맞춰 교체
|
||
context_tokens = 16384
|
||
endpoint = "http://192.168.0.47:11434"
|
||
temperature = 0.2
|
||
seed = 42
|
||
|
||
[search]
|
||
default_k = 10
|
||
hybrid_fusion = "rrf"
|
||
rrf_k = 60
|
||
snippet_chars = 220
|
||
|
||
[rag]
|
||
prompt_template_version = "rag-v1"
|
||
score_gate = 0.05 # RRF 정규화 후 [0, 1] 범위라 default 그대로 OK
|
||
explain_default = false
|
||
max_context_tokens = 6000
|
||
```
|
||
|
||
`KEBAB_*` 환경변수로 override 가능 (`KEBAB_MODELS_LLM_MODEL=qwen2.5:32b kebab …` 등). 자세한 키 목록은 `crates/kebab-config/src/lib.rs` 의 `apply_env` 매치 암.
|
||
|
||
## 명령 시퀀스
|
||
|
||
```bash
|
||
KEBAB() { ./target/debug/kebab --config /tmp/kebab-smoke/config.toml "$@"; }
|
||
|
||
KB doctor # 1. health check
|
||
KB ingest # 2. 워크스페이스 색인 (markdown + image)
|
||
KB list docs # 3. 색인 결과 목록 (markdown + image 모두 표시)
|
||
KB search --mode lexical "코루틴" --k 3 # 4. lexical 검색
|
||
KB search --mode vector "memory safety" --k 3 # 5. vector 검색
|
||
KB search --mode hybrid "Cargo workspace" --k 3 # 6. hybrid 검색
|
||
KB search --mode lexical "Hello World" --k 3 # 7. image OCR 텍스트 검색 (P6-4)
|
||
KB inspect chunk <chunk_id> # 8. raw chunk 보기
|
||
KB ask "이 KB 안에서 ..." --mode hybrid --k 5 # 9. RAG 답변 (Ollama 필요)
|
||
KB --json ask "..." --mode hybrid # 10. 기계 친화 출력 검증
|
||
```
|
||
|
||
## P6-4 이미지 ingestion 옵션
|
||
|
||
`config.toml` 에 다음 절을 추가하면 `kebab ingest` 가 `**/*.png` / `**/*.jpg` 등 이미지 자산도 함께 색인합니다 (텍스트만 색인하려면 생략):
|
||
|
||
```toml
|
||
[workspace]
|
||
include = ["**/*.md", "**/*.png", "**/*.jpg"]
|
||
|
||
[image.ocr]
|
||
enabled = true # vision LM 으로 이미지 안 텍스트 전사
|
||
engine = "ollama-vision"
|
||
model = "gemma4:e4b" # 사용자 환경의 비전 모델
|
||
endpoint = "http://192.168.0.47:11434" # 비우면 models.llm.endpoint fallback
|
||
languages = ["eng", "kor"]
|
||
max_pixels = 1600 # long-edge cap
|
||
|
||
[image.caption]
|
||
enabled = true # vision LM 으로 한 문장 객관 설명 생성
|
||
max_pixels = 768
|
||
prompt_template_version = "caption-v1"
|
||
```
|
||
|
||
이미지 자산 한 장당 OCR 1 호출 + Caption 1 호출 → ~3-6초 (`gemma4:e4b` 기준). 다이어그램 / 카메라 사진 / 스크린샷 위주 워크스페이스에 권장. 책 / 스캔본은 P7 PDF 라인으로 (P7 머지 후).
|
||
|
||
각 명령은 0 종료 코드면 정상. `kebab ask` 는 거절 시 종료 코드 1 (`RefusalSignal`) — 의도된 동작.
|
||
|
||
## 검증 체크리스트
|
||
|
||
- `kebab doctor` 가 `--config` path 를 honor 하고 그 안의 `storage.data_dir` 를 출력 (XDG default 가 아님).
|
||
- `kebab ingest` idempotent — 두 번째 실행이 `new=0 updated=N`.
|
||
- `kebab list docs` 출력에 frontmatter 의 `title` 이 아닌 deterministic `doc_id` (32-hex) + `workspace_path` 가 보임.
|
||
- `kebab search --mode hybrid` 의 `fusion_score` 가 `[0, 1]` 범위 (top-1 종종 1.0 — 두 retriever 모두 rank 1 일 때).
|
||
- `kebab ask` JSON 응답에 `model.id` 가 config 의 모델 (`gemma4:26b` 등) 과 일치, `embedding.id = multilingual-e5-small`, `citations[].marker` 가 `[1]` / `[2]` 형식 (square-bracketed bare index).
|
||
- 코퍼스에 없는 주제로 `kebab ask` → `refusal_reason: "llm_self_judge"` (또는 `no_chunks` / `score_gate`) + `grounded: false`.
|
||
- (P6-4) `image.ocr.enabled = true` 로 PNG 자산을 ingest 하면 `kebab list docs` 가 markdown 옆에 image doc 도 출력 (`workspace_path` 가 `*.png`). `kebab inspect doc <image_doc_id>` 의 `block.ocr.joined` 가 vision LM 의 OCR 결과 (예: 스크린샷 안의 텍스트). `kebab search --mode lexical "<OCR text>"` 가 그 image chunk 를 반환하면 wiring 정상.
|
||
- OCR / caption 부분 실패는 `errors` 카운터 미증가 — `kebab inspect doc <id>` 의 Provenance Warning 이벤트 또는 `--debug` 로그에서만 확인.
|
||
|
||
## 정리
|
||
|
||
```bash
|
||
rm -rf /tmp/kebab-smoke/data # 데이터만 날리고 다시 ingest 가능
|
||
rm -rf /tmp/kebab-smoke # 통째로 정리
|
||
```
|
||
|
||
`~/.config/kebab/` 와 `~/.local/share/kebab/` 는 한 번도 터치되지 않는다 (`--config` flag 가 정확히 honor 되는 경우 — P3-5 hotfix 이후 보장).
|
||
|
||
## 알려진 동작
|
||
|
||
- 첫 `kebab ingest` 시 fastembed 모델 다운로드 (~470MB) — `data_dir/models/fastembed/` 에 캐시.
|
||
- `kebab ask` 응답 시간 = LLM 토큰 throughput 에 종속. M4 Pro 48GB + gemma4:26b 기준 답변 50–100 토큰에 20–55초.
|
||
- `--config` path 가 존재하지 않거나 malformed 면 `kebab doctor` 가 hard fail (defaults 가 silently mask 하지 않게 하는 hotfix 동작).
|
||
- 매 CLI invocation 마다 fastembed 모델 init 비용 (~4초) — process-level 캐시 부재 때문. P9 TUI 진입 시 `App` 의 `OnceLock` 으로 세션 동안 한 번만 init.
|
||
- (P6-4) `image.ocr.enabled = true` + `image.caption.enabled = true` 인 워크스페이스에 PNG 가 N장 있으면 ingest 시간 ≈ markdown_time + N × (OCR + Caption latency). `gemma4:e4b` + 192.168.0.47 로 자산당 ~5-10초. 다수의 책 페이지를 이미지로 넣지 말 것 — 책은 P7 PDF 라인 사용 권장 (P7 머지 후).
|
||
|
||
자세한 history 와 발견된 버그는 [tasks/HOTFIXES.md](../tasks/HOTFIXES.md) 참조.
|