review(p6-2): 회차 1 지적 반영
- crates/kebab-config/src/lib.rs:
• `OcrCfg.endpoint: String` (\"\" sentinel) → `Option<String>` 으로 교체.
`#[serde(default)]` 적용. `KEBAB_IMAGE_OCR_ENDPOINT=\"\"` (빈 값) 도
None 으로 매핑하는 분기 추가.
• 신규 회귀 테스트 `image_ocr_endpoint_empty_env_value_is_none`.
- crates/kebab-parse-image/src/ocr.rs:
• `OllamaVisionOcr::new` 의 endpoint fallback 로직을 새 `Option<String>`
스키마에 맞춰 정리 (`as_deref` + match).
• `OllamaGenerateResponse` 의 dead `_other: HashMap<String, Value>` 필드
제거. `serde_json::Value` import 도 같이 정리.
• `OllamaGenerateRequest.images: Vec<&'a str>` → `[&'a str; 1]`
(호출당 vec! 알로케이션 제거, multi-image 는 OcrEngine trait 가
단일 이미지를 받으므로 OOS).
• `downscale_to_long_edge` 단일-디코드로 리팩터. PNG passthrough
hot path 보존 (header sniff 만으로 분기), 그 외 모든 경로는
decode 1회 + (필요 시) resize + PNG re-encode 1회로 통일.
• `pub fn max_pixels(&self) -> u32` accessor 추가 — clamp 결과
검증 용 (단순 inspector).
- crates/kebab-parse-image/tests/ocr.rs:
• `cfg_for_endpoint` / 통합 테스트가 `Some(endpoint)` 형태로 갱신.
• `from_parts_clamps_max_pixels_into_legal_range` 가 새 accessor
로 실제 클램프 결과 (256 / 4096 / 1024) 를 검증하도록 강화.
• 통합 테스트가 폰트 부재 시 panic 대신 skip 하도록 분기.
- crates/kebab-parse-image/tests/common/mod.rs:
• `hello_world_png` 가 `anyhow::Result<Vec<u8>>` 반환하도록 변경.
expect(\"DejaVu Sans Bold required\") 메시지를 \"only the opt-in
OCR integration fixture needs this font\" 로 의도 명확화.
cargo test -p kebab-parse-image — 28 pass + 1 ignored.
cargo test -p kebab-config — 21 pass (+1 회귀).
cargo clippy --workspace --all-targets -- -D warnings — pass.
Reviewer-suggested workspace.dependencies 통합 (reqwest / base64) 은
P6-3 와 함께 처리할 수 있도록 follow-up 으로 두고 본 PR scope 에서
제외 (회차 1 본문에서 명시).
This commit is contained in:
@@ -20,7 +20,7 @@ use crate::common::red_100x50_png;
|
||||
|
||||
fn cfg_for_endpoint(endpoint: &str) -> Config {
|
||||
let mut cfg = Config::defaults();
|
||||
cfg.image.ocr.endpoint = endpoint.to_string();
|
||||
cfg.image.ocr.endpoint = Some(endpoint.to_string());
|
||||
cfg.image.ocr.model = "gemma4:e4b".to_string();
|
||||
cfg.image.ocr.languages = vec!["eng".to_string(), "kor".to_string()];
|
||||
cfg.image.ocr.max_pixels = 1024;
|
||||
@@ -321,13 +321,26 @@ async fn ocr_downscales_large_image_before_sending() {
|
||||
|
||||
#[test]
|
||||
fn from_parts_clamps_max_pixels_into_legal_range() {
|
||||
// Below MIN_LONG_EDGE — bumped up to the floor.
|
||||
let too_small = OllamaVisionOcr::from_parts("http://x", "m", vec![], 10).unwrap();
|
||||
let too_big = OllamaVisionOcr::from_parts("http://x", "m", vec![], 99_999).unwrap();
|
||||
// We can't read the private field directly, but engine_version is
|
||||
// observable; assert the engine constructed at all (clamp didn't
|
||||
// panic) and run a synthetic prompt.
|
||||
assert_eq!(too_small.engine_name(), "ollama-vision");
|
||||
assert_eq!(too_big.engine_name(), "ollama-vision");
|
||||
assert_eq!(
|
||||
too_small.max_pixels(),
|
||||
256,
|
||||
"max_pixels must be raised to MIN_LONG_EDGE"
|
||||
);
|
||||
|
||||
// Above MAX_LONG_EDGE — capped at the ceiling.
|
||||
let too_big =
|
||||
OllamaVisionOcr::from_parts("http://x", "m", vec![], 99_999).unwrap();
|
||||
assert_eq!(
|
||||
too_big.max_pixels(),
|
||||
4096,
|
||||
"max_pixels must be capped at MAX_LONG_EDGE"
|
||||
);
|
||||
|
||||
// Inside the legal range — pass through untouched.
|
||||
let in_range = OllamaVisionOcr::from_parts("http://x", "m", vec![], 1024).unwrap();
|
||||
assert_eq!(in_range.max_pixels(), 1024);
|
||||
}
|
||||
|
||||
// ── Integration test against real Ollama (opt-in) ────────────────────────
|
||||
@@ -355,11 +368,18 @@ async fn ocr_integration_real_ollama_transcribes_text() {
|
||||
let model =
|
||||
std::env::var("KEBAB_IMAGE_OCR_MODEL").unwrap_or_else(|_| "gemma4:e4b".to_string());
|
||||
|
||||
// Generate a fixture with known text.
|
||||
let bytes = common::hello_world_png();
|
||||
// Generate a fixture with known text. If the DejaVu font is
|
||||
// missing from this dev box, skip rather than crash.
|
||||
let bytes = match common::hello_world_png() {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
eprintln!("skipping ocr_integration: {e:#}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let cfg = {
|
||||
let mut c = Config::defaults();
|
||||
c.image.ocr.endpoint = endpoint;
|
||||
c.image.ocr.endpoint = Some(endpoint);
|
||||
c.image.ocr.model = model;
|
||||
c.image.ocr.max_pixels = 1024;
|
||||
c
|
||||
|
||||
Reference in New Issue
Block a user