2025-01-21 20:52:28 +08:00
|
|
|
#
|
|
|
|
|
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
|
|
|
#
|
2024-08-15 09:17:36 +08:00
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
#
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
#
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
|
# limitations under the License.
|
|
|
|
|
#
|
2025-01-21 20:52:28 +08:00
|
|
|
|
2025-12-09 13:08:37 +08:00
|
|
|
import asyncio
|
2024-08-15 09:17:36 +08:00
|
|
|
import io
|
2026-06-23 22:02:54 +08:00
|
|
|
import logging
|
|
|
|
|
import os
|
2025-04-28 13:35:34 +08:00
|
|
|
import re
|
2026-06-23 22:02:54 +08:00
|
|
|
import tempfile
|
2024-08-15 09:17:36 +08:00
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
|
|
|
from api.db.services.llm_service import LLMBundle
|
2026-07-09 14:02:08 +08:00
|
|
|
from api.db.joint_services.tenant_model_service import get_tenant_default_model_by_type, get_first_provider_model_name, resolve_model_config, ensure_paddleocr_from_env
|
2025-11-27 10:21:44 +08:00
|
|
|
from common.constants import LLMType
|
2026-06-23 22:02:54 +08:00
|
|
|
from common.parser_config_utils import normalize_layout_recognizer
|
2025-10-28 09:46:32 +08:00
|
|
|
from common.string_utils import clean_markdown_block
|
2025-11-27 10:21:44 +08:00
|
|
|
from deepdoc.vision import OCR
|
|
|
|
|
from rag.nlp import attach_media_context, rag_tokenizer, tokenize
|
2024-08-15 09:17:36 +08:00
|
|
|
|
|
|
|
|
ocr = OCR()
|
|
|
|
|
|
2025-10-20 16:49:47 +08:00
|
|
|
# Gemini supported MIME types
|
2025-10-23 11:13:09 +08:00
|
|
|
VIDEO_EXTS = [".mp4", ".mov", ".avi", ".flv", ".mpeg", ".mpg", ".webm", ".wmv", ".3gp", ".3gpp", ".mkv"]
|
2025-10-20 16:49:47 +08:00
|
|
|
|
2024-08-15 09:17:36 +08:00
|
|
|
|
|
|
|
|
def chunk(filename, binary, tenant_id, lang, callback=None, **kwargs):
|
|
|
|
|
doc = {
|
|
|
|
|
"docnm_kwd": filename,
|
2025-04-28 13:35:34 +08:00
|
|
|
"title_tks": rag_tokenizer.tokenize(re.sub(r"\.[a-zA-Z]+$", "", filename)),
|
2024-08-15 09:17:36 +08:00
|
|
|
}
|
|
|
|
|
eng = lang.lower() == "english"
|
|
|
|
|
|
2025-11-27 10:21:44 +08:00
|
|
|
parser_config = kwargs.get("parser_config", {}) or {}
|
|
|
|
|
image_ctx = max(0, int(parser_config.get("image_context_size", 0) or 0))
|
|
|
|
|
|
2025-10-20 16:49:47 +08:00
|
|
|
if any(filename.lower().endswith(ext) for ext in VIDEO_EXTS):
|
|
|
|
|
try:
|
2025-11-27 10:21:44 +08:00
|
|
|
doc.update(
|
|
|
|
|
{
|
|
|
|
|
"doc_type_kwd": "video",
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-07-13 16:42:55 +08:00
|
|
|
cv_model_config = get_tenant_default_model_by_type(tenant_id, LLMType.VISION)
|
2026-03-05 17:27:17 +08:00
|
|
|
cv_mdl = LLMBundle(tenant_id, model_config=cv_model_config, lang=lang)
|
2026-02-11 09:47:33 +08:00
|
|
|
video_prompt = str(parser_config.get("video_prompt", "") or "")
|
2026-07-03 12:53:39 +08:00
|
|
|
ans = asyncio.run(cv_mdl.async_chat(system="", history=[], gen_conf={}, video_bytes=binary, filename=filename, video_prompt=video_prompt))
|
2025-10-20 16:49:47 +08:00
|
|
|
callback(0.8, "CV LLM respond: %s ..." % ans[:32])
|
|
|
|
|
ans += "\n" + ans
|
feat: add native Dutch language support for BM25 tokenization (#14140)
## Summary
- Add language-aware Snowball stemmer to `RagTokenizer` supporting 16
languages (Dutch, German, French, Spanish, etc.)
- Thread the KB `language` parameter through the full tokenization
pipeline (14 parser modules + task executor)
- Add Dutch to the frontend language lists and cross-language form
## Problem
RAGFlow uses the English Porter stemmer + WordNet lemmatizer for **all**
BM25 tokenization, regardless of the knowledge base language setting.
This produces incorrect stems for non-English text. For example:
| Dutch word | Dutch stemmer | English Porter |
|---|---|---|
| documenten | document | documenten (unchanged!) |
| gebruikers | gebruiker | gebruik (over-stemmed) |
| instellingen | instell | instellingen (unchanged!) |
This degrades BM25 recall for any non-English knowledge base.
## Solution
NLTK already ships Snowball stemmers for 16 languages. This PR:
1. **`rag/nlp/rag_tokenizer.py`**: Overrides `tokenize()` with
`set_language()` and `_normalize_token()` that selects the correct NLTK
Snowball stemmer. Falls back to Porter for unmapped languages (Chinese,
Japanese, Korean, etc. — these use character-based tokenization anyway).
2. **`rag/nlp/__init__.py`** + **14 `rag/app/*.py` parsers** +
**`rag/svr/task_executor.py`**: Threads the `language` parameter through
`tokenize()`, `tokenize_chunks()`, `tokenize_table()`, and all callers.
3. **Frontend**: Adds Dutch (`Nederlands`) to `LanguageList`,
`LanguageMap`, `LanguageAbbreviationMap`, `LanguageTranslationMap`,
cross-language form field, and `en.ts` locale.
## Backward Compatibility
- Default language is `"English"`, preserving existing behavior for all
current users
- Languages without a Snowball stemmer mapping fall back to Porter (no
change)
- No new dependencies — NLTK Snowball is already bundled
2026-07-06 17:39:56 +02:00
|
|
|
tokenize(doc, ans, eng, language=lang)
|
2025-10-20 16:49:47 +08:00
|
|
|
return [doc]
|
|
|
|
|
except Exception as e:
|
|
|
|
|
callback(prog=-1, msg=str(e))
|
|
|
|
|
else:
|
|
|
|
|
img = Image.open(io.BytesIO(binary)).convert("RGB")
|
|
|
|
|
doc.update(
|
|
|
|
|
{
|
|
|
|
|
"image": img,
|
|
|
|
|
"doc_type_kwd": "image",
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-06-23 22:02:54 +08:00
|
|
|
|
|
|
|
|
# Try PaddleOCR if configured as layout_recognize
|
|
|
|
|
txt = _try_paddleocr_image(filename, binary, tenant_id, parser_config, callback)
|
|
|
|
|
|
|
|
|
|
if not txt:
|
|
|
|
|
# Fallback to local deepdoc OCR
|
|
|
|
|
bxs = ocr(np.array(img))
|
|
|
|
|
txt = "\n".join([t[0] for _, t in bxs if t[0]])
|
|
|
|
|
|
2025-10-20 16:49:47 +08:00
|
|
|
callback(0.4, "Finish OCR: (%s ...)" % txt[:12])
|
|
|
|
|
if (eng and len(txt.split()) > 32) or len(txt) > 32:
|
feat: add native Dutch language support for BM25 tokenization (#14140)
## Summary
- Add language-aware Snowball stemmer to `RagTokenizer` supporting 16
languages (Dutch, German, French, Spanish, etc.)
- Thread the KB `language` parameter through the full tokenization
pipeline (14 parser modules + task executor)
- Add Dutch to the frontend language lists and cross-language form
## Problem
RAGFlow uses the English Porter stemmer + WordNet lemmatizer for **all**
BM25 tokenization, regardless of the knowledge base language setting.
This produces incorrect stems for non-English text. For example:
| Dutch word | Dutch stemmer | English Porter |
|---|---|---|
| documenten | document | documenten (unchanged!) |
| gebruikers | gebruiker | gebruik (over-stemmed) |
| instellingen | instell | instellingen (unchanged!) |
This degrades BM25 recall for any non-English knowledge base.
## Solution
NLTK already ships Snowball stemmers for 16 languages. This PR:
1. **`rag/nlp/rag_tokenizer.py`**: Overrides `tokenize()` with
`set_language()` and `_normalize_token()` that selects the correct NLTK
Snowball stemmer. Falls back to Porter for unmapped languages (Chinese,
Japanese, Korean, etc. — these use character-based tokenization anyway).
2. **`rag/nlp/__init__.py`** + **14 `rag/app/*.py` parsers** +
**`rag/svr/task_executor.py`**: Threads the `language` parameter through
`tokenize()`, `tokenize_chunks()`, `tokenize_table()`, and all callers.
3. **Frontend**: Adds Dutch (`Nederlands`) to `LanguageList`,
`LanguageMap`, `LanguageAbbreviationMap`, `LanguageTranslationMap`,
cross-language form field, and `en.ts` locale.
## Backward Compatibility
- Default language is `"English"`, preserving existing behavior for all
current users
- Languages without a Snowball stemmer mapping fall back to Porter (no
change)
- No new dependencies — NLTK Snowball is already bundled
2026-07-06 17:39:56 +02:00
|
|
|
tokenize(doc, txt, eng, language=lang)
|
2025-10-20 16:49:47 +08:00
|
|
|
callback(0.8, "OCR results is too long to use CV LLM.")
|
2025-11-27 10:21:44 +08:00
|
|
|
return attach_media_context([doc], 0, image_ctx)
|
2025-10-20 16:49:47 +08:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
callback(0.4, "Use CV LLM to describe the picture.")
|
2026-07-13 16:42:55 +08:00
|
|
|
cv_model_config = get_tenant_default_model_by_type(tenant_id, LLMType.VISION)
|
2026-03-05 17:27:17 +08:00
|
|
|
cv_mdl = LLMBundle(tenant_id, model_config=cv_model_config, lang=lang)
|
2026-02-06 09:50:53 +08:00
|
|
|
with io.BytesIO() as img_binary:
|
|
|
|
|
img.save(img_binary, format="JPEG")
|
|
|
|
|
img_binary.seek(0)
|
|
|
|
|
ans = cv_mdl.describe(img_binary.read())
|
2025-10-20 16:49:47 +08:00
|
|
|
callback(0.8, "CV LLM respond: %s ..." % ans[:32])
|
|
|
|
|
txt += "\n" + ans
|
feat: add native Dutch language support for BM25 tokenization (#14140)
## Summary
- Add language-aware Snowball stemmer to `RagTokenizer` supporting 16
languages (Dutch, German, French, Spanish, etc.)
- Thread the KB `language` parameter through the full tokenization
pipeline (14 parser modules + task executor)
- Add Dutch to the frontend language lists and cross-language form
## Problem
RAGFlow uses the English Porter stemmer + WordNet lemmatizer for **all**
BM25 tokenization, regardless of the knowledge base language setting.
This produces incorrect stems for non-English text. For example:
| Dutch word | Dutch stemmer | English Porter |
|---|---|---|
| documenten | document | documenten (unchanged!) |
| gebruikers | gebruiker | gebruik (over-stemmed) |
| instellingen | instell | instellingen (unchanged!) |
This degrades BM25 recall for any non-English knowledge base.
## Solution
NLTK already ships Snowball stemmers for 16 languages. This PR:
1. **`rag/nlp/rag_tokenizer.py`**: Overrides `tokenize()` with
`set_language()` and `_normalize_token()` that selects the correct NLTK
Snowball stemmer. Falls back to Porter for unmapped languages (Chinese,
Japanese, Korean, etc. — these use character-based tokenization anyway).
2. **`rag/nlp/__init__.py`** + **14 `rag/app/*.py` parsers** +
**`rag/svr/task_executor.py`**: Threads the `language` parameter through
`tokenize()`, `tokenize_chunks()`, `tokenize_table()`, and all callers.
3. **Frontend**: Adds Dutch (`Nederlands`) to `LanguageList`,
`LanguageMap`, `LanguageAbbreviationMap`, `LanguageTranslationMap`,
cross-language form field, and `en.ts` locale.
## Backward Compatibility
- Default language is `"English"`, preserving existing behavior for all
current users
- Languages without a Snowball stemmer mapping fall back to Porter (no
change)
- No new dependencies — NLTK Snowball is already bundled
2026-07-06 17:39:56 +02:00
|
|
|
tokenize(doc, txt, eng, language=lang)
|
2025-11-27 10:21:44 +08:00
|
|
|
return attach_media_context([doc], 0, image_ctx)
|
2025-10-20 16:49:47 +08:00
|
|
|
except Exception as e:
|
|
|
|
|
callback(prog=-1, msg=str(e))
|
2024-08-15 09:17:36 +08:00
|
|
|
|
|
|
|
|
return []
|
2025-03-18 14:52:20 +08:00
|
|
|
|
|
|
|
|
|
2026-06-23 22:02:54 +08:00
|
|
|
def _try_paddleocr_image(filename, binary, tenant_id, parser_config, callback):
|
|
|
|
|
"""Try to parse image using PaddleOCR if configured. Returns text or empty string."""
|
|
|
|
|
layout_recognize = parser_config.get("layout_recognize", "")
|
|
|
|
|
if not layout_recognize:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
layout_recognizer, parser_model_name = normalize_layout_recognizer(layout_recognize)
|
|
|
|
|
if layout_recognizer != "PaddleOCR":
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
paddleocr_llm_name = parser_model_name
|
|
|
|
|
if not paddleocr_llm_name:
|
|
|
|
|
paddleocr_llm_name = get_first_provider_model_name(tenant_id, "PaddleOCR", LLMType.OCR) or ensure_paddleocr_from_env(tenant_id)
|
|
|
|
|
|
|
|
|
|
if not paddleocr_llm_name:
|
|
|
|
|
return ""
|
|
|
|
|
|
2026-07-09 14:02:08 +08:00
|
|
|
ocr_model_config = resolve_model_config(tenant_id, LLMType.OCR, paddleocr_llm_name)
|
2026-06-23 22:02:54 +08:00
|
|
|
ocr_model = LLMBundle(tenant_id=tenant_id, model_config=ocr_model_config)
|
|
|
|
|
pdf_parser = ocr_model.mdl
|
|
|
|
|
|
|
|
|
|
if not hasattr(pdf_parser, "parse_image"):
|
|
|
|
|
logging.warning("[PaddleOCR] parse_image not available, falling back to local OCR")
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
callback(0.2, "Using PaddleOCR to parse image...")
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=os.path.splitext(filename)[1] or ".png", delete=True) as tmp:
|
|
|
|
|
tmp.write(binary)
|
|
|
|
|
tmp.flush()
|
|
|
|
|
txt = pdf_parser.parse_image(filepath=tmp.name, binary=binary, callback=callback)
|
|
|
|
|
|
|
|
|
|
if txt:
|
|
|
|
|
logging.info(f"[PaddleOCR] image parsed successfully: {len(txt)} chars")
|
|
|
|
|
return txt
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.warning(f"[PaddleOCR] image parsing failed, falling back to local OCR: {e}")
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
2025-03-18 14:52:20 +08:00
|
|
|
def vision_llm_chunk(binary, vision_model, prompt=None, callback=None):
|
|
|
|
|
"""
|
|
|
|
|
A simple wrapper to process image to markdown texts via VLM.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Simple markdown texts generated by VLM.
|
|
|
|
|
"""
|
|
|
|
|
callback = callback or (lambda prog, msg: None)
|
|
|
|
|
|
|
|
|
|
img = binary
|
|
|
|
|
txt = ""
|
|
|
|
|
|
|
|
|
|
try:
|
2026-01-28 14:59:02 +08:00
|
|
|
# Skip tiny crops that fail provider image-size limits.
|
|
|
|
|
if hasattr(img, "size"):
|
|
|
|
|
min_side = 11
|
|
|
|
|
if img.size[0] < min_side or img.size[1] < min_side:
|
|
|
|
|
callback(0.0, f"Skip tiny image for VLM: {img.size[0]}x{img.size[1]}")
|
|
|
|
|
return ""
|
2025-09-02 10:31:37 +08:00
|
|
|
with io.BytesIO() as img_binary:
|
2025-11-07 15:45:10 +08:00
|
|
|
try:
|
|
|
|
|
img.save(img_binary, format="JPEG")
|
|
|
|
|
except Exception:
|
|
|
|
|
img_binary.seek(0)
|
|
|
|
|
img_binary.truncate()
|
|
|
|
|
img.save(img_binary, format="PNG")
|
2025-11-27 10:21:44 +08:00
|
|
|
|
2025-09-02 10:31:37 +08:00
|
|
|
img_binary.seek(0)
|
|
|
|
|
ans = clean_markdown_block(vision_model.describe_with_prompt(img_binary.read(), prompt))
|
|
|
|
|
txt += "\n" + ans
|
|
|
|
|
return txt
|
2025-03-18 14:52:20 +08:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
callback(-1, str(e))
|
|
|
|
|
|
2025-03-20 09:39:32 +08:00
|
|
|
return ""
|