fix(wiki): skip vision pipeline entirely when wiki.ocr.enabled is off

This commit is contained in:
matevip 2026-05-02 19:05:05 +08:00
parent f910d762a3
commit 44ea8c784b
2 changed files with 24 additions and 108 deletions

View File

@ -63,54 +63,6 @@ public class PdfImageExtractor {
// a mock or a no-op wrapper.
}
/**
* Quick check: does the PDF contain at least one inline image that
* passes the size threshold? Stops scanning at the first qualifying
* image so it's cheap on large documents.
*
* <p>Used by the upload pipeline to decide whether the absence of
* captions is an "actually nothing to caption" outcome (safe to cache)
* vs a "vision was unavailable" outcome (defer caching until flag
* flips on so the next read retries).
*/
public boolean hasInlineImages(Path pdfPath) {
if (pdfPath == null) {
return false;
}
File pdfFile = pdfPath.toFile();
if (!pdfFile.isFile()) {
return false;
}
try (PDDocument doc = Loader.loadPDF(pdfFile)) {
for (PDPage page : doc.getPages()) {
PDResources resources = page.getResources();
if (resources == null) continue;
for (COSName name : resources.getXObjectNames()) {
PDXObject obj;
try {
obj = resources.getXObject(name);
} catch (IOException ignored) {
continue;
}
if (!(obj instanceof PDImageXObject pdImage)) continue;
BufferedImage bi;
try {
bi = pdImage.getImage();
} catch (IOException | RuntimeException ignored) {
continue;
}
if (bi.getWidth() >= MIN_IMAGE_SIDE_PX
&& bi.getHeight() >= MIN_IMAGE_SIDE_PX) {
return true;
}
}
}
} catch (IOException e) {
log.debug("[PdfImage] hasInlineImages probe failed for {}: {}", pdfPath, e.getMessage());
}
return false;
}
/**
* Walks every page of {@code pdfPath}, captions each qualifying inline
* image, and returns the rendered marker lines in document order.

View File

@ -360,26 +360,20 @@ public class WikiRawMaterialService {
// Append inline-image captions for PDFs so chunk-level search
// hits chart/diagram contents that the text extractor missed.
// Failures are non-fatal: the body text is still returned.
EnrichmentOutcome enrichment = appendPdfImageCaptions(entity, text);
String enriched = appendPdfImageCaptions(entity, text);
if (truncated) {
// 截断的结果不缓存避免永久丢失后半内容返回文本供分块处理使用
// Truncated results are not cached so we don't lose the tail
// permanently we still return the text for chunking use.
log.warn("[Wiki] Extracted text truncated at {} chars for: {} (full document may be larger)",
text.length(), entity.getSourcePath());
} else if (!enrichment.shouldCache()) {
// Vision was unavailable but the PDF has images we'd otherwise
// caption leave extracted_text NULL so the next call retries
// once the operator enables wiki.ocr.enabled.
log.info("[Wiki] PDF id={} captioning deferred (vision disabled or unavailable); "
+ "extracted_text not cached so next read retries", entity.getId());
} else {
// Full extraction: cache to avoid re-extracting on subsequent calls.
updateExtractedText(entity.getId(), enrichment.text());
updateExtractedText(entity.getId(), enriched);
}
log.info("[Wiki] Extracted text from {}: {} chars (text) → {} chars (enriched), method={}, truncated={}, cached={}",
entity.getSourcePath(), text.length(), enrichment.text().length(),
json.getStr("method"), truncated, enrichment.shouldCache() && !truncated);
return enrichment.text();
entity.getSourcePath(), text.length(), enriched.length(),
json.getStr("method"), truncated, !truncated);
return enriched;
}
}
log.warn("[Wiki] Document extraction returned no text for: {}", entity.getSourcePath());
@ -390,75 +384,40 @@ public class WikiRawMaterialService {
return entity.getOriginalContent();
}
/**
* Enrichment outcome: text + a hint about whether the result is final.
*
* <p>{@code shouldCache=false} signals the caller to skip the
* extracted_text cache write so the next call re-runs PDF image
* captioning. This handles the common operator workflow of uploading
* a PDF before flipping {@code wiki.ocr.enabled} on without the
* skip, the partial text-only result would block the eventual
* caption catch-up.
*/
private record EnrichmentOutcome(String text, boolean shouldCache) { }
/**
* For PDF raw materials, walks the inline images and appends a section of
* {@code [ P{n}#{m}]: <caption>} markers so downstream chunking and
* search can index image contents.
*
* <p>Returns an {@link EnrichmentOutcome} that distinguishes three cases:
* <ul>
* <li>Non-PDF / no extractor / no images present text unchanged,
* {@code shouldCache=true}.</li>
* <li>Vision disabled but the PDF has inline images text unchanged,
* {@code shouldCache=false} so the next read retries after the
* operator flips {@code wiki.ocr.enabled} on.</li>
* <li>Vision enabled and at least one image captioned enriched text,
* {@code shouldCache=true}.</li>
* <li>Vision enabled but every image's caption call failed text
* unchanged, {@code shouldCache=true} (transient failures should
* not block the cache; if the model is wedged, retry happens via
* a manual reprocess).</li>
* </ul>
* <p>When {@link #VISION_FLAG_KEY} is off, returns the body unchanged
* without re-parsing the PDF the operator's "off = ignore images"
* intent. Flipping the flag on later requires a manual reprocess for
* existing rows to pick up captions.
*/
private EnrichmentOutcome appendPdfImageCaptions(WikiRawMaterialEntity entity, String body) {
private String appendPdfImageCaptions(WikiRawMaterialEntity entity, String body) {
if (!"pdf".equals(entity.getSourceType()) || pdfImageExtractor == null) {
return new EnrichmentOutcome(body, true);
return body;
}
if (featureFlagService == null || !featureFlagService.isEnabled(VISION_FLAG_KEY)) {
return body;
}
java.nio.file.Path pdfPath = java.nio.file.Paths.get(entity.getSourcePath());
// Vision-disabled path: only refuse the cache when the PDF actually has
// qualifying images. Image-less PDFs can still be cached as text-only.
boolean visionEnabled = featureFlagService != null
&& featureFlagService.isEnabled(VISION_FLAG_KEY);
if (!visionEnabled) {
boolean hasImages = pdfImageExtractor.hasInlineImages(pdfPath);
if (hasImages) {
log.info("[Wiki] PDF id={} has inline images but {} is disabled; "
+ "skipping captioning and deferring extracted_text cache",
entity.getId(), VISION_FLAG_KEY);
return new EnrichmentOutcome(body, false);
}
return new EnrichmentOutcome(body, true);
}
try {
List<String> snippets = pdfImageExtractor.captionInlineImages(pdfPath);
if (snippets.isEmpty()) {
return new EnrichmentOutcome(body, true);
return body;
}
StringBuilder sb = new StringBuilder(body);
sb.append("\n\n--- Inline images ---\n");
for (String snippet : snippets) {
sb.append(snippet).append('\n');
}
return new EnrichmentOutcome(sb.toString(), true);
return sb.toString();
} catch (Exception e) {
log.warn("[Wiki] PDF inline-image captioning failed for id={}: {}",
entity.getId(), e.getMessage());
return new EnrichmentOutcome(body, true);
return body;
}
}
@ -479,6 +438,11 @@ public class WikiRawMaterialService {
log.warn("[Wiki] Image raw material missing sourcePath: id={}", entity.getId());
return "";
}
if (featureFlagService == null || !featureFlagService.isEnabled(VISION_FLAG_KEY)) {
log.info("[Wiki] Image raw id={} skipped: {} is disabled",
entity.getId(), VISION_FLAG_KEY);
return "";
}
byte[] imageBytes;
try {
imageBytes = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(entity.getSourcePath()));