Skip to content

fix: support large input texts with chunking - #100

Open
RSKKSOFFICIAL wants to merge 18 commits into
nextcloud:mainfrom
RSKKSOFFICIAL:fix/chunk-long-text
Open

RSKKSOFFICIAL wants to merge 18 commits into
nextcloud:mainfrom
RSKKSOFFICIAL:fix/chunk-long-text

Conversation

@RSKKSOFFICIAL

Copy link
Copy Markdown

Fixes #71

What this does

Large input texts (>250 words) were being silently truncated by the model
because the output hit the decoding length limit mid-document. This adds a
chunking layer that splits the input into smaller pieces before translation,
then joins the results back into a single output.

Changes

lib/Service.py

  • Added _chunk_text(): splits input at sentence boundaries into chunks of
    max 80 words. Hard-splits any single sentence that exceeds the limit.
  • Added _join_chunks(): joins translated chunks in document order. Uses an
    empty string separator for no-space languages (zh, ja, th, etc.) and a
    single space for all others. Chunk order is always preserved regardless of
    source/target script direction — each chunk is already translated correctly
    by the model independently.
  • Updated translate(): applies chunking when input exceeds the threshold,
    caps max_decoding_length proportionally per chunk to prevent runaway
    repetition loops, and enforces a minimum repetition_penalty per chunk.

config.json

  • Added chunking section with four configurable parameters:
    • chunk_threshold (250): word count above which input is chunked
    • chunk_size (80): max words per chunk
    • min_repetition_penalty (1.5): lower bound for repetition penalty per chunk, prevents output loops on dense scripts like Devanagari
    • max_decoding_multiplier (3): output token cap as a multiple of input tokens per chunk

Testing

Tested with 350+ word inputs across 7 language pairs:

  • English → German, French, Hindi, Arabic
  • Arabic → English, Persian
  • Persian → English

All pairs now produce complete output covering the full input. Before this
change every pair was truncated at roughly Section 03/04 of a 5-section test
document.

RTL languages (Arabic, Persian) are handled correctly, chunks are always
joined in forward document order since each chunk is translated independently.

Signed-off-by: RSKKSOFFICIAL <rsksofficial02@gmail.com>
Signed-off-by: RSKKSOFFICIAL <rsksofficial02@gmail.com>
Signed-off-by: RSKKSOFFICIAL <rsksofficial02@gmail.com>
Comment thread lib/Service.py Outdated
Comment thread lib/Service.py Outdated
self.config["inference"].get("repetition_penalty", 1.0), min_repetition_penalty
),
}
results = self.translator.translate_batch(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to use one batch for all chunks, instead of running one batch per chunk in sequence

Comment thread lib/Service.py Outdated
logger = logging.getLogger(os.environ["APP_ID"] + __name__)

# Languages that do not use spaces between words — join chunks without a space separator
_NO_SPACE_LANGUAGES = {"zh", "ja", "th", "my", "km", "lo", "bo"}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about yue (Cantonese), dz (Dzongkha — same script as the included bo), shn (Shan)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. yue, dz, and shn also use writing systems where spaces shouldn't be inserted between chunks. I'll add them to _NO_SPACE_LANGUAGES.

btw, are their any more languages which uses no space?

Signed-off-by: RSKKSOFFICIAL <rsksofficial02@gmail.com>
@RSKKSOFFICIAL

Copy link
Copy Markdown
Author

Hi @marcelklehr, I’ve addressed the review comments and tested the changes locally. Everything is working as expected on my side. Could you please take another look and let me know if there’s anything else you’d like me to adjust?

Signed-off-by: RSKKSOFFICIAL <rsksofficial02@gmail.com>
@github-actions

Copy link
Copy Markdown

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

@kyteinsky kyteinsky left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the PR, looks good overall but posted some comments maybe we can discuss about.

did you try out translate_iterable? It was mentioned in the original issue: https://opennmt.net/CTranslate2/python/ctranslate2.Translator.html#ctranslate2.Translator.translate_iterable
it seems to have a few upsides like parallel translation (which we don't do here I think) and parallel prefetching, but should simplify the implementation here at least.

Comment thread config.json Outdated
Comment thread lib/Service.py Outdated
Comment thread lib/Service.py Outdated
Comment thread lib/Service.py Outdated
RSKKSOFFICIAL and others added 3 commits September 21, 2026 22:15
Co-authored-by: Anupam Kumar <kyteinsky@gmail.com>
Signed-off-by: Ravi Shankar Kumar <154051646+RSKKSOFFICIAL@users.noreply.github.com>
Signed-off-by: RSKKSOFFICIAL <rsksofficial02@gmail.com>
@RSKKSOFFICIAL

Copy link
Copy Markdown
Author

Hi @marcelklehr and @kyteinsky,

Thank you both for the helpful feedback and suggestions. I’ve addressed the review comments and pushed the updated changes.

I’ve also tested the updated implementation locally, including long inputs and the relevant language cases, and everything is working as expected on my side.

When you have a chance, could you please take another look at the changes? I’d really appreciate your feedback, and I’d be happy to make any further adjustments if needed.

@kyteinsky kyteinsky left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the fixes, there are a few more things I noticed in the implementation that would be good to discuss about.
feel free to ask questions or push back on the comments.
sorry that the review is taking this much time.

Comment thread lib/Service.py Outdated
Comment thread lib/Service.py Outdated
Comment thread lib/Service.py Outdated
Comment on lines +127 to +132
# Sentence-boundary split: keep the delimiter attached to the preceding sentence.
# For no-space text (CJK etc.) use \s* because sentences run together without
# whitespace. For all other text use \s+ to avoid splitting on abbreviations,
# decimals, URLs, and other mid-word periods (e.g. "Dr.", "3.14", "U.S.A").
if is_no_space:
sentences = re.split(r"(?<=[。!?\u3002\uff01\uff1f])\s*", text) # noqa: RUF001

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Sentence-boundary split: keep the delimiter attached to the preceding sentence.
# For no-space text (CJK etc.) use \s* because sentences run together without
# whitespace. For all other text use \s+ to avoid splitting on abbreviations,
# decimals, URLs, and other mid-word periods (e.g. "Dr.", "3.14", "U.S.A").
if is_no_space:
sentences = re.split(r"(?<=[。!?\u3002\uff01\uff1f])\s*", text) # noqa: RUF001
# Sentence-boundary split: keep the delimiter attached to the preceding sentence.
# For no-space text (CJK etc.) use `\s*` because sentences run together without
# whitespace. For all other text use `\s+`.
if is_no_space:
# split on special sentence boundaries and spaces if present
sentences = re.split(r"(?<=[\u3002\uff01\uff1f])\s*", text)

Comment thread lib/Service.py Outdated
current_count = 0

# If a single sentence is longer than max_words on its own, hard-split it
if unit_count > max_words:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like two terminologies are used here, unit (refers to both words and chars) and words (refers to the same unit meaning but is written as words)
it would be nice to make max_words follow the same terms and be max_units or something so it's easier to reason about the code.

Comment thread lib/Service.py Outdated
continue

# If adding this sentence would overflow the chunk, flush first
if current_count + unit_count > max_words and current_parts:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like this naming is good, current_count and current_parts are related to the sentences loop and are constantly used and recycled, so "current".

Comment thread lib/Service.py Outdated
Comment thread lib/Service.py Outdated
Comment thread lib/Service.py Outdated
Comment on lines +236 to +244
inference_config = {k: v for k, v in self.config["inference"].items()
if k != "max_batch_size"}
max_batch_size = self.config["inference"].get("max_batch_size", 32)

results = list(self.translator.translate_iterable(
all_input_tokens,
max_batch_size=max_batch_size,
batch_type="tokens",
**self.config["inference"],
)
**inference_config,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove max_batch_size and write it separately?
the same could be achieved by checking if the key exists or not and setting a default if not.
also, 32 is small, we should do 256 since batch_type="tokens" so it's 256 tokens at a time.

Comment thread lib/Service.py Outdated
text_size = len(cleaned) if is_no_space_source else len(cleaned.split())
chunks = (
self._chunk_text(cleaned, chunk_size, is_no_space=is_no_space_source)
if text_size > chunk_threshold

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for spaced languages, here we check the word count to be 250 max, which is very far off from the token count which is supposed to be max 256.
each tokenizer is different but generally it's 3-4 characters for 1 token in english and it's less characters for 1 token in other languages like hindi or chinese.

we could do a character count check here and say max 600 chars maybe, for all the languages, but if you're feeling like it, we could use the tokenizer below to estimate the token count of the texts, which should be fast and check that to be more than the threshold to decide if we chunk or not.

RSKKSOFFICIAL and others added 5 commits September 24, 2026 19:37
Co-authored-by: Anupam Kumar <kyteinsky@gmail.com>
Signed-off-by: Ravi Shankar Kumar <154051646+RSKKSOFFICIAL@users.noreply.github.com>
Co-authored-by: Anupam Kumar <kyteinsky@gmail.com>
Signed-off-by: Ravi Shankar Kumar <154051646+RSKKSOFFICIAL@users.noreply.github.com>
Co-authored-by: Anupam Kumar <kyteinsky@gmail.com>
Signed-off-by: Ravi Shankar Kumar <154051646+RSKKSOFFICIAL@users.noreply.github.com>
Co-authored-by: Anupam Kumar <kyteinsky@gmail.com>
Signed-off-by: Ravi Shankar Kumar <154051646+RSKKSOFFICIAL@users.noreply.github.com>
Signed-off-by: RSKKSOFFICIAL <rsksofficial02@gmail.com>
@RSKKSOFFICIAL

Copy link
Copy Markdown
Author

thanks for the fixes, there are a few more things I noticed in the implementation that would be good to discuss about. feel free to ask questions or push back on the comments. sorry that the review is taking this much time.

No worries at all about the review taking some time. I really appreciate the feedback and suggestions from both (@marcelklehr @kyteinsky ) of you. 😊

I’ve addressed the latest comments and pushed the changes. I’ll definitely ask if I have any questions or if there’s anything I’d like to discuss further.

I’m learning a lot from these reviews, not just how to make the code work, but how to write maintainable, clean, and well-integrated code that fits the Nextcloud ecosystem.

Thanks again for your time and guidance!

RSKSOFFICIAL and others added 4 commits September 25, 2026 18:45
Signed-off-by: Ravi Shankar <142860126+RSKSOFFICIAL@users.noreply.github.com>
Signed-off-by: Ravi Shankar <142860126+RSKSOFFICIAL@users.noreply.github.com>
Signed-off-by: Ravi Shankar <142860126+RSKSOFFICIAL@users.noreply.github.com>
Signed-off-by: Ravi Shankar <142860126+RSKSOFFICIAL@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support large input texts with more than 250 words

4 participants