Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = [".", "oar-ocr-derive", "oar-ocr-core", "oar-ocr-vl"]
resolver = "2"

[workspace.package]
version = "0.8.0"
version = "0.8.1"
edition = "2024"
rust-version = "1.95"
license = "Apache-2.0"
Expand All @@ -12,8 +12,8 @@ repository = "https://github.com/greatv/oar-ocr"
homepage = "https://github.com/greatv/oar-ocr"

[workspace.dependencies]
oar-ocr-core = { version = "0.8.0", path = "oar-ocr-core", default-features = false }
oar-ocr-derive = { version = "0.8.0", path = "oar-ocr-derive", default-features = false }
oar-ocr-core = { version = "0.8.1", path = "oar-ocr-core", default-features = false }
oar-ocr-derive = { version = "0.8.1", path = "oar-ocr-derive", default-features = false }

# Shared third-party dependencies (kept in sync via workspace inheritance).
clap = { version = "4.5.42", features = ["derive"] }
Expand Down
1 change: 1 addition & 0 deletions oar-ocr-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ thiserror = "2.0"
tokenizers.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
unicode-bidi = "0.3.18"
Comment thread
GreatV marked this conversation as resolved.
Comment thread
GreatV marked this conversation as resolved.
ureq = { version = "3.0", default-features = false, features = ["rustls", "platform-verifier"], optional = true }
wide = { version = "1.5", optional = true }

Expand Down
21 changes: 16 additions & 5 deletions oar-ocr-core/src/domain/adapters/text_recognition_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ use crate::core::traits::{
adapter::{AdapterInfo, ModelAdapter},
task::Task,
};
use crate::domain::tasks::{TextRecognitionConfig, TextRecognitionOutput, TextRecognitionTask};
use crate::domain::tasks::{
TextRecognitionConfig, TextRecognitionOutput, TextRecognitionTask,
postprocess_text_direction_with_order_change,
};
use crate::impl_adapter_builder;
use crate::models::recognition::crnn::{CRNNModel, CRNNModelBuilder, CRNNPreprocessConfig};

Expand Down Expand Up @@ -83,11 +86,18 @@ impl ModelAdapter for TextRecognitionAdapter {
)
{
if score >= effective_config.score_threshold {
result_texts.push(text);
let (text, changed_order) = postprocess_text_direction_with_order_change(text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not reorder recognizers that already emit logical RTL

In environments where the recognition model already returns Unicode logical RTL strings, such as مرحبا rather than the visual CTC order ابحرم, this unconditional post-processing still runs every above-threshold result through reorder_line; that converts logical storage to display order, so pure RTL text is reversed back to visual order and changed_order also clears word-box metadata. Please keep a no-reorder/default path or apply this only to recognizers configured as emitting visual RTL output.

Useful? React with 👍 / 👎.

result_scores.push(score);
result_positions.push(positions);
result_col_indices.push(col_indices);
result_seq_lengths.push(seq_len);
result_texts.push(text);
if changed_order {
result_positions.push(Vec::new());
result_col_indices.push(Vec::new());
result_seq_lengths.push(0);
} else {
result_positions.push(positions);
result_col_indices.push(col_indices);
result_seq_lengths.push(seq_len);
}
} else {
// Keep entry to preserve index correspondence, but mark as filtered
result_texts.push(String::new());
Expand Down Expand Up @@ -179,6 +189,7 @@ impl_adapter_builder! {
self.return_word_box = enable;
self
}

}

build: |builder: TextRecognitionAdapterBuilder, model_source: crate::core::ModelSource| -> Result<TextRecognitionAdapter, OCRError> {
Expand Down
5 changes: 4 additions & 1 deletion oar-ocr-core/src/domain/tasks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ pub use text_detection::{Detection, TextDetectionConfig, TextDetectionOutput, Te
pub use text_line_orientation::{
TextLineOrientationConfig, TextLineOrientationOutput, TextLineOrientationTask,
};
pub use text_recognition::{TextRecognitionConfig, TextRecognitionOutput, TextRecognitionTask};
pub use text_recognition::{
TextRecognitionConfig, TextRecognitionOutput, TextRecognitionTask, postprocess_text_direction,
postprocess_text_direction_with_order_change,
};
62 changes: 62 additions & 0 deletions oar-ocr-core/src/domain/tasks/text_recognition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::core::traits::TaskDefinition;
use crate::core::traits::task::{ImageTaskInput, Task, TaskSchema, TaskType};
use crate::utils::ScoreValidator;
use serde::{Deserialize, Serialize};
use unicode_bidi::BidiInfo;

/// Configuration for text recognition task.
///
Expand All @@ -28,6 +29,47 @@ impl Default for TextRecognitionConfig {
}
}

fn reorder_bidi_line(line: &str) -> String {
let bidi_info = BidiInfo::new(line, None);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Choose the bidi base from the visual RTL end

The fresh evidence after the earlier auto-direction fixes is this new None paragraph level: for visual-order RTL CTC output whose logical line ends with an LTR token, e.g. abc 123 ابحرم for مرحبا abc 123, the first strong character is the visual-left a, so unicode-bidi resolves the paragraph as LTR and reorder_line only flips the Arabic run, leaving abc 123 مرحبا instead of moving the LTR token after the RTL base. Determine the base from the visual right edge, or force RTL when appropriate, before reordering.

Useful? React with 👍 / 👎.

let Some(para) = bidi_info.paragraphs.first() else {
return line.to_string();
};

bidi_info
.reorder_line(para, para.range.clone())
.into_owned()
}

fn reorder_bidi_text(text: &str) -> (String, bool) {
let mut out = String::with_capacity(text.len());
let mut changed_order = false;

for segment in text.split_inclusive('\n') {
if let Some(line) = segment.strip_suffix('\n') {
let converted = reorder_bidi_line(line);
changed_order |= converted != line;
out.push_str(&converted);
out.push('\n');
} else {
let converted = reorder_bidi_line(segment);
changed_order |= converted != segment;
out.push_str(&converted);
}
}

(out, changed_order)
}

/// Applies Unicode bidi post-processing to decoded OCR text.
pub fn postprocess_text_direction(text: String) -> String {
postprocess_text_direction_with_order_change(text).0
}

/// Applies Unicode bidi post-processing and reports whether character order changed.
pub fn postprocess_text_direction_with_order_change(text: String) -> (String, bool) {
reorder_bidi_text(&text)
}

/// Output from text recognition task.
#[derive(Debug, Clone)]
pub struct TextRecognitionOutput {
Expand Down Expand Up @@ -185,4 +227,24 @@ mod tests {
assert!(schema.input_types.contains(&"text_boxes".to_string()));
assert!(schema.output_types.contains(&"text_strings".to_string()));
}

#[test]
fn bidi_postprocess_leaves_ltr_text_unchanged() {
assert_eq!(
postprocess_text_direction("hello 123".to_string()),
"hello 123"
);
}

#[test]
fn bidi_postprocess_reports_order_changes() {
assert_eq!(
postprocess_text_direction_with_order_change("hello 123".to_string()),
("hello 123".to_string(), false)
);
assert_eq!(
postprocess_text_direction_with_order_change("ابحرم".to_string()),
("مرحبا".to_string(), true)
);
}
}
Loading