Pattern-Based Pre-filtering
The pattern filter module provides a fast pre-processing step that identifies common OCR errors using regular expressions before sending content to LLM analysis.
Core Implementation
defmodule Codexis.QualityChecker.PatternFilter do
@moduledoc """
Pre-filters content using known OCR error patterns before LLM processing
"""
@known_patterns %{
# Character substitutions
~r/\brn/ => "m",
~r/\bcl/ => "d",
~r/\bU\b/ => "ll",
~r/\btl1e\b/ => "the",
~r/\btl1at\b/ => "that",
# Common word errors
~r/\bwith([a-z])/ => "with \\1",
~r/\b([a-z])of\b/ => "\\1 of",
~r/\b([a-z])the\b/ => "\\1 the",
# Punctuation issues
~r/([a-z])\s*,([a-z])/ => "\\1, \\2",
~r/([a-z])\s*\.([a-z])/ => "\\1. \\2"
}
def pre_filter(text) do
potential_errors = find_pattern_matches(text)
%{
text: text,
potential_errors: potential_errors,
needs_llm_check: length(potential_errors) > 0
}
end
defp find_pattern_matches(text) do
Enum.flat_map(@known_patterns, fn {pattern, suggestion} ->
case Regex.scan(pattern, text, return: :index) do
[] -> []
matches ->
Enum.map(matches, fn [{start, length}] ->
%{
position: start,
length: length,
original: String.slice(text, start, length),
suggestion: apply_suggestion(text, start, length, pattern, suggestion),
pattern: inspect(pattern)
}
end)
end
end)
end
defp apply_suggestion(text, start, length, pattern, suggestion) do
matched_text = String.slice(text, start, length)
Regex.replace(pattern, matched_text, suggestion)
end
end
Extended Pattern Library
defmodule Codexis.QualityChecker.ExtendedPatterns do
@moduledoc """
Comprehensive pattern library for OCR error detection
"""
@character_substitutions %{
# Common character confusion
~r/rn/ => "m",
~r/cl/ => "d",
~r/li/ => "h",
~r/vv/ => "w",
~r/ii/ => "u",
# Number-letter confusion
~r/\b0(?=[a-z])/i => "O",
~r/\b(?<=[a-z])0\b/i => "O",
~r/\b1(?=[a-z])/i => "l",
~r/\b(?<=[a-z])1\b/i => "l",
~r/\b5(?=[a-z])/i => "S",
~r/\b(?<=[a-z])5\b/i => "S",
# Special character errors
~r/\-\-/ => "—", # double hyphen to em-dash
~r/``/ => """, # opening quotes
~r/''/ => """ # closing quotes
}
@word_errors %{
# Common word mergers
~r/\bofthe\b/ => "of the",
~r/\bandthe\b/ => "and the",
~r/\binthe\b/ => "in the",
~r/\btothe\b/ => "to the",
~r/\bforthe\b/ => "for the",
# Common splits
~r/\bwith\s+out\b/ => "without",
~r/\bsome\s+thing\b/ => "something",
~r/\bevery\s+thing\b/ => "everything",
~r/\bany\s+thing\b/ => "anything",
# Specific common errors
~r/\btlie\b/ => "the",
~r/\btbe\b/ => "the",
~r/\bwbich\b/ => "which",
~r/\bwbere\b/ => "where",
~r/\bcornmon\b/ => "common",
~r/\brnany\b/ => "many",
~r/\brnight\b/ => "might"
}
@punctuation_errors %{
# Missing spaces after punctuation
~r/([.,:;!?])([A-Z])/ => "\\1 \\2",
# Extra spaces before punctuation
~r/\s+([.,:;!?])/ => "\\1",
# Quote errors
~r/"([^"]+)"/ => ""\\1"",
~r/'([^']+)'/ => "'\\1'",
# Apostrophe errors
~r/\b(\w+)'(\w+)\b/ => "\\1'\\2"
}
def get_all_patterns do
Map.merge(@character_substitutions,
Map.merge(@word_errors, @punctuation_errors))
end
def get_pattern_category(pattern) do
cond do
Map.has_key?(@character_substitutions, pattern) -> :character
Map.has_key?(@word_errors, pattern) -> :word
Map.has_key?(@punctuation_errors, pattern) -> :punctuation
true -> :unknown
end
end
end
Context-Aware Pattern Matching
defmodule Codexis.QualityChecker.ContextAwarePattern do
@moduledoc """
Pattern matching with context awareness
"""
def find_contextual_errors(text) do
words = String.split(text, ~r/\s+/)
words
|> Enum.with_index()
|> Enum.flat_map(fn {word, index} ->
check_word_in_context(word, index, words)
end)
end
defp check_word_in_context(word, index, words) do
prev_word = if index > 0, do: Enum.at(words, index - 1), else: nil
next_word = if index < length(words) - 1, do: Enum.at(words, index + 1), else: nil
errors = []
# Check for article errors based on next word
errors = errors ++ check_article_errors(word, next_word)
# Check for preposition errors
errors = errors ++ check_preposition_errors(word, prev_word, next_word)
# Check for common phrase errors
errors = errors ++ check_phrase_errors(word, prev_word, next_word)
errors
end
defp check_article_errors("a", next_word) when next_word != nil do
if String.match?(next_word, ~r/^[aeiou]/i) do
[%{
type: :article_error,
original: "a",
suggestion: "an",
reason: "Use 'an' before vowel sounds"
}]
else
[]
end
end
defp check_article_errors("an", next_word) when next_word != nil do
if String.match?(next_word, ~r/^[^aeiou]/i) do
[%{
type: :article_error,
original: "an",
suggestion: "a",
reason: "Use 'a' before consonant sounds"
}]
else
[]
end
end
defp check_article_errors(_, _), do: []
defp check_preposition_errors(word, prev_word, next_word) do
case {prev_word, word, next_word} do
{_, "if", "the"} -> []
{_, "if", _} when next_word != nil ->
[%{
type: :preposition_error,
original: "if",
suggestion: "in",
reason: "Possible OCR error: 'if' should be 'in'"
}]
_ -> []
end
end
defp check_phrase_errors(word, prev_word, next_word) do
case {prev_word, word, next_word} do
{"as", "weil", "as"} ->
[%{
type: :phrase_error,
original: "weil",
suggestion: "well",
reason: "Common phrase: 'as well as'"
}]
{"in", "tbe", _} ->
[%{
type: :phrase_error,
original: "tbe",
suggestion: "the",
reason: "Common phrase: 'in the'"
}]
_ -> []
end
end
end
Pattern Learning System
defmodule Codexis.QualityChecker.PatternLearning do
@moduledoc """
Learns new patterns from confirmed corrections
"""
use Agent
def start_link(opts \\ []) do
Agent.start_link(fn -> %{patterns: %{}, statistics: %{}} end, name: __MODULE__)
end
def learn_pattern(original, corrected, context \\ %{}) do
Agent.update(__MODULE__, fn state ->
pattern_key = {original, corrected}
updated_patterns = Map.update(state.patterns, pattern_key,
%{count: 1, contexts: [context]},
fn existing ->
%{
count: existing.count + 1,
contexts: [context | existing.contexts]
}
end
)
%{state | patterns: updated_patterns}
end)
end
def get_learned_patterns(min_occurrences \\ 3) do
Agent.get(__MODULE__, fn state ->
state.patterns
|> Enum.filter(fn {_, data} -> data.count >= min_occurrences end)
|> Enum.map(fn {{original, corrected}, data} ->
%{
original: original,
corrected: corrected,
count: data.count,
confidence: calculate_confidence(data)
}
end)
|> Enum.sort_by(& &1.confidence, :desc)
end)
end
def save_patterns(path) do
patterns = Agent.get(__MODULE__, fn state -> state.patterns end)
content = patterns
|> Enum.map(fn {{orig, corr}, data} ->
"#{orig} => #{corr} (#{data.count} occurrences)"
end)
|> Enum.join("\n")
File.write!(path, content)
end
defp calculate_confidence(data) do
base_confidence = min(1.0, data.count / 10.0)
# Adjust based on context variety
context_variety = length(Enum.uniq(data.contexts))
variety_bonus = min(0.2, context_variety / 20.0)
base_confidence + variety_bonus
end
end
Performance Optimization
defmodule Codexis.QualityChecker.OptimizedPatternFilter do
@moduledoc """
Optimized pattern filtering for large documents
"""
def filter_parallel(text, chunk_size \\ 5000) do
chunks = chunk_text(text, chunk_size)
chunks
|> Task.async_stream(&filter_chunk/1, max_concurrency: System.schedulers_online())
|> Enum.reduce([], fn {:ok, results}, acc ->
acc ++ results
end)
|> deduplicate_results()
end
defp chunk_text(text, size) do
text
|> String.graphemes()
|> Enum.chunk_every(size)
|> Enum.map(&Enum.join/1)
end
defp filter_chunk(chunk) do
Codexis.QualityChecker.PatternFilter.pre_filter(chunk).potential_errors
end
defp deduplicate_results(results) do
results
|> Enum.uniq_by(fn result ->
{result.position, result.original, result.suggestion}
end)
|> Enum.sort_by(& &1.position)
end
def compile_patterns(patterns) do
# Pre-compile regex patterns for better performance
Enum.map(patterns, fn {pattern, replacement} ->
{Regex.compile!(pattern), replacement}
end)
end
def benchmark_patterns(text, patterns) do
Enum.map(patterns, fn {pattern, _} ->
{time, matches} = :timer.tc(fn ->
Regex.scan(pattern, text)
end)
%{
pattern: inspect(pattern),
time_us: time,
matches: length(matches)
}
end)
end
end