How book-to-skill Estimates Tokens for CJK Characters: A Heuristic Deep Dive

book-to-skill uses a deterministic heuristic in estimate_tokens() that applies different counting ratios to Latin words and CJK characters (1.5 CJK characters per token), avoiding whitespace-dependent splitting that would otherwise underestimate East Asian text by orders of magnitude.

The open-source repository virgiliojr94/book-to-skill implements a lightweight, reproducible token estimator that operates without external tokenizer libraries. Understanding how book-to-skill estimates tokens for CJK characters reveals a language-aware algorithm that separately processes ideographic scripts from whitespace-delimited Latin text, ensuring accurate LLM cost predictions across multilingual documents.

Why CJK Characters Require Special Token Estimation Logic

Traditional word-counting methods fail for CJK (Chinese, Japanese, Korean) scripts because these writing systems rarely use spaces between words. If book-to-skill applied the standard Latin approach—splitting on whitespace and dividing by WORDS_PER_TOKEN—a dense block of Chinese characters would register as a single "word," causing token estimates to be off by roughly 1000×.

The tool solves this by treating CJK codepoints as individual units with their own token density ratio, while preserving the word-based calculation for Latin script.

Core Configuration Constants

WORDS_PER_TOKEN and CJK_CHARS_PER_TOKEN in config.py

As defined in book_to_skill/config.py, the algorithm relies on two key constants:

  • WORDS_PER_TOKEN – The standard ratio for whitespace-delimited languages (typically around 0.75 words per token or 1.33 tokens per word, depending on configuration).
  • CJK_CHARS_PER_TOKEN – Defaults to 1.5, approximating the compression ratio observed in the cl100k_base tokenizer used by modern LLMs for CJK codepoints.

These constants allow estimate_tokens() to weight each script type appropriately without invoking heavy neural tokenizers.

CJK Detection Using _CJK_RE in utils.py

Unicode Ranges Covered by the Regex Pattern

The book_to_skill/utils.py file defines _CJK_RE, a compiled regular expression that identifies CJK codepoints across multiple Unicode blocks:

  • CJK Symbols and Punctuation (U+3000–U+303F)
  • Hiragana and Katakana (U+3040–U+30FF)
  • CJK Unified Ideographs (BMP: U+4E00–U+9FFF; Supplementary Planes: U+20000–U+3FFFF)
  • Hangul Syllables (U+AC00–U+D7A3)
  • Full-width forms and compatibility symbols

This comprehensive regex ensures that supplementary-plane characters (rare historical variants) are counted alongside common Modern Chinese and Japanese glyphs.

The estimate_tokens() Algorithm Implementation

Step-by-Step Token Estimation Process

The estimate_tokens() function in book_to_skill/utils.py implements a four-step hybrid counting strategy:

def estimate_tokens(text: str) -> int:
    if not text:
        return 0
    cjk = len(_CJK_RE.findall(text))               # count of CJK codepoints

    if not cjk:
        # Pure Latin/whitespace‑delimited text

        return int(len(text.split()) / WORDS_PER_TOKEN)

    # Replace CJK chars with a space, then split → count Latin words

    latin_words = len(_CJK_RE.sub(" ", text).split())
    # Combine the two branches

    return int(latin_words / WORDS_PER_TOKEN + cjk / CJK_CHARS_PER_TOKEN)

Step 1: The function scans the input using _CJK_RE.findall() to enumerate all CJK codepoints.

Step 2: If no CJK characters exist, the function falls back to the Latin path: splitting on whitespace and dividing the word count by WORDS_PER_TOKEN.

Step 3: For mixed or pure CJK text, the algorithm substitutes each CJK character with a space, then splits on whitespace to isolate and count the remaining Latin words.

Step 4: The final token count combines both contributions: Latin words divided by WORDS_PER_TOKEN plus CJK characters divided by CJK_CHARS_PER_TOKEN (default 1.5), truncated to an integer.

Practical Examples of CJK Token Estimation

The following examples demonstrate how book-to-skill handles different script combinations:

from book_to_skill.utils import estimate_tokens

# Pure Latin text – falls back to the word‑based branch

print(estimate_tokens("The quick brown fox jumps over the lazy dog."))

# → 1  (≈ 9 words / 8 words‑per‑token)

# Mixed Latin + a few Chinese characters

mixed = "The book contains Chinese characters: 文本 and more."
print(estimate_tokens(mixed))

# → 2  (≈ 7 Latin words / 8 + 2 CJK chars / 1.5)

# Pure CJK – counts each ideograph directly

cjk_only = "第一章 緒論\n第二章 架構\n第三章 方法"
print(estimate_tokens(cjk_only))

# → 3  (≈ 9 CJK chars / 1.5)

These examples show the algorithm correctly weighting dense ideographic text (where each character consumes roughly 0.66 tokens) against sparse Latin prose.

Unit Testing CJK Supplementary Plane Support

The repository includes targeted tests to validate CJK handling:

  • tests/test_cjk_supplementary_plane.py – Confirms that characters in supplementary planes (e.g., rare CJK Extension B characters) are correctly identified and counted by _CJK_RE.
  • tests/test_book_to_skill.py – Exercises estimate_tokens() against real book excerpts containing mixed Japanese/English and Chinese/English content, verifying end-to-end accuracy.

These tests ensure the heuristic remains deterministic across edge cases in the Unicode standard.

Summary

  • book-to-skill estimates CJK tokens using a hybrid heuristic in book_to_skill/utils.py that processes Latin words and CJK characters through separate mathematical paths.
  • The _CJK_RE regex covers BMP and supplementary-plane Unicode blocks for Chinese, Japanese, and Korean scripts.
  • The default ratio of 1.5 CJK characters per token mirrors modern tokenizer behavior without requiring external dependencies.
  • Mixed-script text is handled by sanitizing CJK characters to whitespace, allowing the Latin word counter to function normally on the remaining text.
  • All constants are configurable in book_to_skill/config.py, making the estimator adaptable to different LLM tokenizers.

Frequently Asked Questions

How accurate is the CJK_CHARS_PER_TOKEN ratio compared to real tokenizers?

The default value of 1.5 mirrors the average compression ratio observed in OpenAI's cl100k_base tokenizer for CJK content. While actual tokenizers use Byte Pair Encoding (BPE) and may split characters differently based on context, the 1.5 ratio provides a deterministic, reproducible estimate within 10-15% of actual token counts for most standard CJK text.

Why doesn't book-to-skill use tiktoken or the actual OpenAI tokenizer?

As implemented in virgiliojr94/book-to-skill, the tool prioritizes lightweight reproducibility and zero external tokenizer dependencies. By using a heuristic approach, the estimator avoids importing heavy neural tokenization libraries, ensuring the tool runs quickly in CI/CD pipelines and resource-constrained environments without downloading tokenizer vocabularies.

Which CJK scripts and Unicode planes are supported?

The _CJK_RE pattern in book_to_skill/utils.py supports Modern Chinese (Unified Ideographs), Japanese (Hiragana, Katakana, and Kanji), and Korean (Hangul syllables). It explicitly includes supplementary planes (U+20000–U+3FFFF) for rare and historical CJK variants, ensuring compatibility with classical texts and extended character sets.

Does the algorithm handle mixed scripts efficiently?

Yes. The algorithm processes mixed Latin-CJK text in O(n) time complexity, where n is the string length. By using regex substitution to normalize CJK characters into whitespace, it avoids nested loops or recursive tokenization, making it suitable for batch-processing entire books while maintaining deterministic output across Python environments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →