feat(image.ocr): request_timeout_secs config knob + closure of v0.17.1 미진행
v0.17.1 (PR #162) 가 LLM 쪽 hard-coded 300s 를 [models.llm] request_timeout_secs 로 풀어준 것과 같은 패턴을 OCR 어댑터에 적용. 사용자 결정으로 별 노브 분리 ([image.ocr] request_timeout_secs) — OCR 는 LLM 대비 cold start 패턴이 달라 독립 조절이 편함. - OcrCfg.request_timeout_secs: u64 (serde default 300) - KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS env override - OllamaVisionOcr::build / from_parts 시그니처에 timeout 인자 추가 - REQUEST_TIMEOUT 상수 제거 - 3 신규 unit test (default / env / legacy parse) — LlmCfg 패턴 그대로 - HOTFIXES 2026-05-25 v0.17.1 entry 의 두 미진행 항목 모두 closure (OCR timeout = 본 PR, --stream docs = PR #163 에서 이미 완료) 기존 config / 옛 KB 영향 없음 — 새 필드는 default 로 채워지고 동작도 동일 (300s). vision 모델 cold start 가 길면 env 또는 config 로 늘릴 수 있음. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -228,6 +228,19 @@ pub struct OcrCfg {
|
|||||||
/// Cap the long edge of the image (in pixels) before sending. Larger
|
/// Cap the long edge of the image (in pixels) before sending. Larger
|
||||||
/// images bloat prompt cost. Default `1600`.
|
/// images bloat prompt cost. Default `1600`.
|
||||||
pub max_pixels: u32,
|
pub max_pixels: u32,
|
||||||
|
/// v0.17.2 post-dogfood: Hard ceiling on a single HTTP exchange to
|
||||||
|
/// the OCR endpoint. Sister knob to [`LlmCfg::request_timeout_secs`]
|
||||||
|
/// — kept separate because OCR latency is typically shorter than
|
||||||
|
/// chat-LLM cold start, and large vision models on CPU-only hosts
|
||||||
|
/// occasionally need a different budget. See HOTFIXES 2026-05-25
|
||||||
|
/// for the rationale.
|
||||||
|
///
|
||||||
|
/// **Edge case — `0` is NOT a disable sentinel.** Same semantics as
|
||||||
|
/// `LlmCfg::request_timeout_secs`: `Duration::from_secs(0)` means
|
||||||
|
/// "every request fails immediately", not "no timeout". Use a
|
||||||
|
/// large finite value for an effectively-uncapped budget.
|
||||||
|
#[serde(default = "default_ocr_request_timeout_secs")]
|
||||||
|
pub request_timeout_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OcrCfg {
|
impl OcrCfg {
|
||||||
@@ -239,10 +252,18 @@ impl OcrCfg {
|
|||||||
endpoint: None,
|
endpoint: None,
|
||||||
languages: vec!["eng".to_string(), "kor".to_string()],
|
languages: vec!["eng".to_string(), "kor".to_string()],
|
||||||
max_pixels: 1600,
|
max_pixels: 1600,
|
||||||
|
request_timeout_secs: default_ocr_request_timeout_secs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// v0.17.2 post-dogfood: matches the legacy hard-coded ceiling so
|
||||||
|
/// existing configs that omit the field keep behaving identically.
|
||||||
|
/// Overridable per config / `KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS`.
|
||||||
|
fn default_ocr_request_timeout_secs() -> u64 {
|
||||||
|
300
|
||||||
|
}
|
||||||
|
|
||||||
/// Caption settings (P6-3). Caption uses the same Ollama-vision /
|
/// Caption settings (P6-3). Caption uses the same Ollama-vision /
|
||||||
/// `LanguageModel` pipeline as the rest of the workspace; the trait
|
/// `LanguageModel` pipeline as the rest of the workspace; the trait
|
||||||
/// abstraction is the part the spec demands. `enabled` defaults to
|
/// abstraction is the part the spec demands. `enabled` defaults to
|
||||||
@@ -722,6 +743,11 @@ impl Config {
|
|||||||
self.image.ocr.max_pixels = n;
|
self.image.ocr.max_pixels = n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS" => {
|
||||||
|
if let Ok(n) = v.parse::<u64>() {
|
||||||
|
self.image.ocr.request_timeout_secs = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// image.caption (P6-3)
|
// image.caption (P6-3)
|
||||||
"KEBAB_IMAGE_CAPTION_ENABLED" => {
|
"KEBAB_IMAGE_CAPTION_ENABLED" => {
|
||||||
@@ -1022,6 +1048,107 @@ theme = "dark"
|
|||||||
assert_eq!(c.image.ocr.max_pixels, 1600);
|
assert_eq!(c.image.ocr.max_pixels, 1600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// v0.17.2 post-dogfood: matches the legacy hard-coded 300s cap so
|
||||||
|
/// existing configs that omit the new field keep behaving identically.
|
||||||
|
#[test]
|
||||||
|
fn default_ocr_request_timeout_secs_is_300() {
|
||||||
|
assert_eq!(
|
||||||
|
Config::defaults().image.ocr.request_timeout_secs,
|
||||||
|
300
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn env_overrides_image_ocr_request_timeout_secs() {
|
||||||
|
let mut env = HashMap::new();
|
||||||
|
env.insert(
|
||||||
|
"KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS".to_string(),
|
||||||
|
"900".to_string(),
|
||||||
|
);
|
||||||
|
let c = Config::defaults().apply_env(&env);
|
||||||
|
assert_eq!(c.image.ocr.request_timeout_secs, 900);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// v0.17.2 post-dogfood: a config file written before the OCR
|
||||||
|
/// timeout field existed must still parse and fall back to the
|
||||||
|
/// 300s default — backwards-compat invariant. Reuses the same
|
||||||
|
/// minimal legacy TOML fixture as the LLM-side test.
|
||||||
|
#[test]
|
||||||
|
fn legacy_config_without_ocr_request_timeout_secs_uses_default() {
|
||||||
|
let toml_src = r#"
|
||||||
|
schema_version = 1
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
root = "/tmp/x"
|
||||||
|
exclude = []
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
data_dir = "/tmp/x"
|
||||||
|
sqlite = "/tmp/x/kebab.sqlite"
|
||||||
|
vector_dir = "/tmp/x/lancedb"
|
||||||
|
asset_dir = "/tmp/x/assets"
|
||||||
|
artifact_dir = "/tmp/x/artifacts"
|
||||||
|
model_dir = "/tmp/x/models"
|
||||||
|
runs_dir = "/tmp/x/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"
|
||||||
|
model = "multilingual-e5-large"
|
||||||
|
version = "v1"
|
||||||
|
dimensions = 1024
|
||||||
|
batch_size = 64
|
||||||
|
|
||||||
|
[models.llm]
|
||||||
|
provider = "ollama"
|
||||||
|
model = "gemma3:4b"
|
||||||
|
context_tokens = 4096
|
||||||
|
endpoint = "http://127.0.0.1:11434"
|
||||||
|
temperature = 0.0
|
||||||
|
seed = 0
|
||||||
|
|
||||||
|
[search]
|
||||||
|
default_k = 10
|
||||||
|
hybrid_fusion = "rrf"
|
||||||
|
rrf_k = 60
|
||||||
|
snippet_chars = 220
|
||||||
|
|
||||||
|
[rag]
|
||||||
|
prompt_template_version = "rag-v2"
|
||||||
|
score_gate = 0.3
|
||||||
|
explain_default = false
|
||||||
|
max_context_tokens = 8000
|
||||||
|
|
||||||
|
[image.ocr]
|
||||||
|
enabled = false
|
||||||
|
engine = "ollama-vision"
|
||||||
|
model = "gemma3:4b"
|
||||||
|
languages = ["eng"]
|
||||||
|
max_pixels = 1600
|
||||||
|
|
||||||
|
[image.caption]
|
||||||
|
enabled = false
|
||||||
|
max_pixels = 768
|
||||||
|
prompt_template_version = "caption-v1"
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
theme = "dark"
|
||||||
|
"#;
|
||||||
|
let c: Config = toml::from_str(toml_src).expect("parse legacy config");
|
||||||
|
assert_eq!(c.image.ocr.request_timeout_secs, 300);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_ocr_env_overrides() {
|
fn image_ocr_env_overrides() {
|
||||||
let mut env = HashMap::new();
|
let mut env = HashMap::new();
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ use crate::image_prep;
|
|||||||
/// Engine name written into `OcrText.engine` for the Ollama-vision adapter.
|
/// Engine name written into `OcrText.engine` for the Ollama-vision adapter.
|
||||||
pub const OLLAMA_VISION_ENGINE: &str = "ollama-vision";
|
pub const OLLAMA_VISION_ENGINE: &str = "ollama-vision";
|
||||||
|
|
||||||
/// Hard ceiling on the OCR HTTP exchange. Cold-loading a vision model on
|
|
||||||
/// first call can take ~30s; 5 minutes is generous without being open-ended.
|
|
||||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(300);
|
|
||||||
|
|
||||||
/// Lower bound on `config.image.ocr.max_pixels`. Anything below this is
|
/// Lower bound on `config.image.ocr.max_pixels`. Anything below this is
|
||||||
/// silently bumped to keep the model from receiving an unreadable thumbnail.
|
/// silently bumped to keep the model from receiving an unreadable thumbnail.
|
||||||
const MIN_LONG_EDGE: u32 = 256;
|
const MIN_LONG_EDGE: u32 = 256;
|
||||||
@@ -139,7 +135,13 @@ impl OllamaVisionOcr {
|
|||||||
Some(s) if !s.is_empty() => s.to_string(),
|
Some(s) if !s.is_empty() => s.to_string(),
|
||||||
_ => config.models.llm.endpoint.clone(),
|
_ => config.models.llm.endpoint.clone(),
|
||||||
};
|
};
|
||||||
Self::build(endpoint, ocr.model.clone(), ocr.languages.clone(), ocr.max_pixels)
|
Self::build(
|
||||||
|
endpoint,
|
||||||
|
ocr.model.clone(),
|
||||||
|
ocr.languages.clone(),
|
||||||
|
ocr.max_pixels,
|
||||||
|
ocr.request_timeout_secs,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build directly from explicit fields. Useful for tests that need
|
/// Build directly from explicit fields. Useful for tests that need
|
||||||
@@ -153,8 +155,15 @@ impl OllamaVisionOcr {
|
|||||||
model: impl Into<String>,
|
model: impl Into<String>,
|
||||||
languages: Vec<String>,
|
languages: Vec<String>,
|
||||||
max_pixels: u32,
|
max_pixels: u32,
|
||||||
|
request_timeout_secs: u64,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
Self::build(endpoint.into(), model.into(), languages, max_pixels)
|
Self::build(
|
||||||
|
endpoint.into(),
|
||||||
|
model.into(),
|
||||||
|
languages,
|
||||||
|
max_pixels,
|
||||||
|
request_timeout_secs,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared validation + construction. Centralised so `new` and
|
/// Shared validation + construction. Centralised so `new` and
|
||||||
@@ -164,6 +173,7 @@ impl OllamaVisionOcr {
|
|||||||
model: String,
|
model: String,
|
||||||
languages: Vec<String>,
|
languages: Vec<String>,
|
||||||
requested_max_pixels: u32,
|
requested_max_pixels: u32,
|
||||||
|
request_timeout_secs: u64,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
if endpoint.is_empty() {
|
if endpoint.is_empty() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
@@ -183,7 +193,7 @@ impl OllamaVisionOcr {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let client = reqwest::blocking::Client::builder()
|
let client = reqwest::blocking::Client::builder()
|
||||||
.timeout(REQUEST_TIMEOUT)
|
.timeout(Duration::from_secs(request_timeout_secs))
|
||||||
.build()
|
.build()
|
||||||
.context("building OCR HTTP client")?;
|
.context("building OCR HTTP client")?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -375,6 +385,7 @@ mod tests {
|
|||||||
"m",
|
"m",
|
||||||
vec!["eng".into(), "kor".into()],
|
vec!["eng".into(), "kor".into()],
|
||||||
1024,
|
1024,
|
||||||
|
300,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let p = engine.build_prompt(Some(&Lang("ko".into())));
|
let p = engine.build_prompt(Some(&Lang("ko".into())));
|
||||||
@@ -389,6 +400,7 @@ mod tests {
|
|||||||
"m",
|
"m",
|
||||||
vec!["eng".into()],
|
vec!["eng".into()],
|
||||||
1024,
|
1024,
|
||||||
|
300,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let p = engine.build_prompt(Some(&Lang("und".into())));
|
let p = engine.build_prompt(Some(&Lang("und".into())));
|
||||||
@@ -400,7 +412,7 @@ mod tests {
|
|||||||
/// the constructor cannot drift to "silently accept a bad config".
|
/// the constructor cannot drift to "silently accept a bad config".
|
||||||
#[test]
|
#[test]
|
||||||
fn build_rejects_empty_endpoint() {
|
fn build_rejects_empty_endpoint() {
|
||||||
let r = OllamaVisionOcr::from_parts("", "m", vec![], 1024);
|
let r = OllamaVisionOcr::from_parts("", "m", vec![], 1024, 300);
|
||||||
let err = r.expect_err("empty endpoint must bail").to_string();
|
let err = r.expect_err("empty endpoint must bail").to_string();
|
||||||
assert!(
|
assert!(
|
||||||
err.contains("endpoint is empty"),
|
err.contains("endpoint is empty"),
|
||||||
@@ -413,7 +425,7 @@ mod tests {
|
|||||||
/// so testing `from_parts` covers both.
|
/// so testing `from_parts` covers both.
|
||||||
#[test]
|
#[test]
|
||||||
fn build_rejects_empty_model_after_trim() {
|
fn build_rejects_empty_model_after_trim() {
|
||||||
let r = OllamaVisionOcr::from_parts("http://x", " ", vec![], 1024);
|
let r = OllamaVisionOcr::from_parts("http://x", " ", vec![], 1024, 300);
|
||||||
let err = r.expect_err("empty model must bail").to_string();
|
let err = r.expect_err("empty model must bail").to_string();
|
||||||
assert!(
|
assert!(
|
||||||
err.contains("model is empty"),
|
err.contains("model is empty"),
|
||||||
@@ -428,10 +440,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn build_clamps_max_pixels_outside_legal_range() {
|
fn build_clamps_max_pixels_outside_legal_range() {
|
||||||
let too_small =
|
let too_small =
|
||||||
OllamaVisionOcr::from_parts("http://x", "m", vec![], 1).unwrap();
|
OllamaVisionOcr::from_parts("http://x", "m", vec![], 1, 300).unwrap();
|
||||||
assert_eq!(too_small.max_pixels(), MIN_LONG_EDGE);
|
assert_eq!(too_small.max_pixels(), MIN_LONG_EDGE);
|
||||||
let too_big =
|
let too_big =
|
||||||
OllamaVisionOcr::from_parts("http://x", "m", vec![], u32::MAX).unwrap();
|
OllamaVisionOcr::from_parts("http://x", "m", vec![], u32::MAX, 300).unwrap();
|
||||||
assert_eq!(too_big.max_pixels(), MAX_LONG_EDGE);
|
assert_eq!(too_big.max_pixels(), MAX_LONG_EDGE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,7 +322,8 @@ async fn ocr_downscales_large_image_before_sending() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn from_parts_clamps_max_pixels_into_legal_range() {
|
fn from_parts_clamps_max_pixels_into_legal_range() {
|
||||||
// Below MIN_LONG_EDGE — bumped up to the floor.
|
// Below MIN_LONG_EDGE — bumped up to the floor.
|
||||||
let too_small = OllamaVisionOcr::from_parts("http://x", "m", vec![], 10).unwrap();
|
let too_small =
|
||||||
|
OllamaVisionOcr::from_parts("http://x", "m", vec![], 10, 300).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
too_small.max_pixels(),
|
too_small.max_pixels(),
|
||||||
256,
|
256,
|
||||||
@@ -331,7 +332,7 @@ fn from_parts_clamps_max_pixels_into_legal_range() {
|
|||||||
|
|
||||||
// Above MAX_LONG_EDGE — capped at the ceiling.
|
// Above MAX_LONG_EDGE — capped at the ceiling.
|
||||||
let too_big =
|
let too_big =
|
||||||
OllamaVisionOcr::from_parts("http://x", "m", vec![], 99_999).unwrap();
|
OllamaVisionOcr::from_parts("http://x", "m", vec![], 99_999, 300).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
too_big.max_pixels(),
|
too_big.max_pixels(),
|
||||||
4096,
|
4096,
|
||||||
@@ -339,7 +340,8 @@ fn from_parts_clamps_max_pixels_into_legal_range() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Inside the legal range — pass through untouched.
|
// Inside the legal range — pass through untouched.
|
||||||
let in_range = OllamaVisionOcr::from_parts("http://x", "m", vec![], 1024).unwrap();
|
let in_range =
|
||||||
|
OllamaVisionOcr::from_parts("http://x", "m", vec![], 1024, 300).unwrap();
|
||||||
assert_eq!(in_range.max_pixels(), 1024);
|
assert_eq!(in_range.max_pixels(), 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,14 +25,30 @@ v0.17.0 후속 도그푸딩에서 발견: 사용자가 default `gemma4:e4b` (8B
|
|||||||
- `README.md` 사전 요구 절 + `docs/SMOKE.md` 의 ollama 안내에 권장 모델 (≤ 4B Q4 — `gemma3:4b` / `qwen2.5:3b` / `phi3:mini`) + timeout 노브 anchor 한 줄. 8B+ 시도 시 timeout 패턴 사전 안내.
|
- `README.md` 사전 요구 절 + `docs/SMOKE.md` 의 ollama 안내에 권장 모델 (≤ 4B Q4 — `gemma3:4b` / `qwen2.5:3b` / `phi3:mini`) + timeout 노브 anchor 한 줄. 8B+ 시도 시 timeout 패턴 사전 안내.
|
||||||
- `crates/kebab-config/src/lib.rs::Config::defaults` 의 LlmCfg literal 에 `request_timeout_secs: default_llm_request_timeout_secs()` + comment 한 줄로 CPU only 권장 안내.
|
- `crates/kebab-config/src/lib.rs::Config::defaults` 의 LlmCfg literal 에 `request_timeout_secs: default_llm_request_timeout_secs()` + comment 한 줄로 CPU only 권장 안내.
|
||||||
|
|
||||||
**미진행 (scope 밖)**:
|
**미진행 (scope 밖) — closure 갱신**:
|
||||||
- `crates/kebab-parse-image/src/ocr.rs::REQUEST_TIMEOUT` 도 동일한 hard-coded 300 s — OCR 이 보통 짧아 LLM 만큼 부담 안 되지만, 일관성 측면에서 다음 round 에 같은 노브 (또는 별 노브) 로 재검토.
|
- ~~`crates/kebab-parse-image/src/ocr.rs::REQUEST_TIMEOUT` 도 동일한 hard-coded 300 s — OCR 이 보통 짧아 LLM 만큼 부담 안 되지만, 일관성 측면에서 다음 round 에 같은 노브 (또는 별 노브) 로 재검토.~~ → **closure**: 아래 2026-05-25 v0.17.2 OCR timeout entry 참조 (별 노브 `[image.ocr] request_timeout_secs` 신설).
|
||||||
- `kebab ask --stream` (fb-33) 권장 강조: 5분 cold-start 동안 첫 token 빠르게 surface — UX 개선. README/SKILL.md 추가 한 줄 후속.
|
- ~~`kebab ask --stream` (fb-33) 권장 강조: 5분 cold-start 동안 첫 token 빠르게 surface — UX 개선. README/SKILL.md 추가 한 줄 후속.~~ → **closure**: PR #163 (v0.17.1 cut) 에서 이미 README + SMOKE + SKILL.md 세 곳 모두 추가됨 (`README.md:22` cold start 권장 단락, `docs/SMOKE.md:45/209` 예제, `SKILL.md:114/119` 사용 가이드). 본 entry 의 미진행 표기가 outdated 였음.
|
||||||
|
|
||||||
**후속 도그푸딩 baseline 보존**: `/build/cache/dogfood-v017/` (466 MB workspace + DB + memory.log), `/build/cache/ollama/` (21 GB binary + gemma3:4b/gemma4:e4b 모델). 다음 round 회귀 비교용.
|
**후속 도그푸딩 baseline 보존**: `/build/cache/dogfood-v017/` (466 MB workspace + DB + memory.log), `/build/cache/ollama/` (21 GB binary + gemma3:4b/gemma4:e4b 모델). 다음 round 회귀 비교용.
|
||||||
|
|
||||||
Cross-link: `crates/kebab-config/src/lib.rs::LlmCfg::request_timeout_secs`, `crates/kebab-llm-local/src/ollama.rs::OllamaLanguageModel::new`.
|
Cross-link: `crates/kebab-config/src/lib.rs::LlmCfg::request_timeout_secs`, `crates/kebab-llm-local/src/ollama.rs::OllamaLanguageModel::new`.
|
||||||
|
|
||||||
|
## 2026-05-25 — v0.17.2: `[image.ocr] request_timeout_secs` 노브 (closure of 2026-05-25 v0.17.1 미진행)
|
||||||
|
|
||||||
|
v0.17.1 entry 의 첫 번째 미진행 항목 closure. LLM 쪽이 v0.17.1 에서 `[models.llm] request_timeout_secs` 로 풀려난 패턴을 OCR 어댑터에 동일 적용. 별 노브로 분리한 이유 (사용자 결정): OCR 은 통상 LLM 대비 짧고 cold start 패턴도 다름 — 두 노브를 독립 조절할 수 있어야 16 GB / CPU only 환경에서 vision 모델만 다른 timeout 을 쓰기 편함.
|
||||||
|
|
||||||
|
**변경**:
|
||||||
|
- `crates/kebab-config/src/lib.rs::OcrCfg` 에 `request_timeout_secs: u64` additive 필드 (`#[serde(default = "default_ocr_request_timeout_secs")]`, default `300`). 옛 config 가 필드 누락해도 그대로 파싱 + 동일 동작 (3 신규 unit test 가 default / env override / legacy parse 핀).
|
||||||
|
- env override `KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS`.
|
||||||
|
- `crates/kebab-parse-image/src/ocr.rs` 의 `REQUEST_TIMEOUT` 상수 제거. `OllamaVisionOcr::build` 시그니처가 `request_timeout_secs: u64` 추가, `new(&Config)` 는 `config.image.ocr.request_timeout_secs` 전달. `from_parts` (테스트 전용 surface) 도 동일하게 시그니처 확장 — caller 6 곳 (`crates/kebab-parse-image/src/ocr.rs::tests` 5, `crates/kebab-parse-image/tests/ocr.rs::from_parts_clamps_max_pixels_into_legal_range` 3 site) 모두 `300` 명시 갱신.
|
||||||
|
- `OcrCfg::defaults()` 에 `request_timeout_secs: default_ocr_request_timeout_secs()` 추가. `Config::defaults()` 는 `ImageCfg::defaults()` 경유라 cascade.
|
||||||
|
|
||||||
|
**Edge case 동일**: `0` 은 disable 아닌 "즉시 timeout" (`Duration::from_secs(0)` 의 reqwest 의미). LlmCfg 의 doc comment 와 같은 안내가 OcrCfg field doc 에 명시.
|
||||||
|
|
||||||
|
**사용자 영향**: 기존 v0.17.x KB / config 는 변경 불필요 — 새 필드는 serde default 로 채워지고 동작도 동일 (300s). vision 모델 cold start 가 길면 `KEBAB_IMAGE_OCR_REQUEST_TIMEOUT_SECS=600` 또는 config 에서 `[image.ocr] request_timeout_secs = 600` 설정.
|
||||||
|
|
||||||
|
Cross-link: `crates/kebab-config/src/lib.rs::OcrCfg::request_timeout_secs`, `crates/kebab-parse-image/src/ocr.rs::OllamaVisionOcr::build`.
|
||||||
|
|
||||||
## 2026-05-24 — v0.17.0: 한국어 trigram FTS5 tokenizer 채택 (closure of 2026-05-22 한국어 lexical)
|
## 2026-05-24 — v0.17.0: 한국어 trigram FTS5 tokenizer 채택 (closure of 2026-05-22 한국어 lexical)
|
||||||
|
|
||||||
V007 migration 으로 `chunks_fts` 의 tokenizer 를 `unicode61` → `trigram` 으로 교체. `chunks` 원본 + embedding + vector index 는 그대로, FTS shadow 만 재구축 + 자동 backfill — 사용자는 `kebab ingest` 재실행 불필요 (binary 만 교체하면 다음 open 시 V007 가 즉시 적용). 같은 라운드의 다른 두 follow-up (`code_lang_chunk_breakdown`, C typedef) 은 별 PR (PR-C / PR-B).
|
V007 migration 으로 `chunks_fts` 의 tokenizer 를 `unicode61` → `trigram` 으로 교체. `chunks` 원본 + embedding + vector index 는 그대로, FTS shadow 만 재구축 + 자동 backfill — 사용자는 `kebab ingest` 재실행 불필요 (binary 만 교체하면 다음 open 시 V007 가 즉시 적용). 같은 라운드의 다른 두 follow-up (`code_lang_chunk_breakdown`, C typedef) 은 별 PR (PR-C / PR-B).
|
||||||
|
|||||||
Reference in New Issue
Block a user