Mayo commited on
Commit
e4afd8c
·
unverified ·
1 Parent(s): 0928a34

refactor: remove sys-locale

Browse files
Cargo.lock CHANGED
@@ -3993,12 +3993,10 @@ dependencies = [
3993
  "objc2-metal",
3994
  "objc2-metal-performance-shaders",
3995
  "objc2-metal-performance-shaders-graph",
3996
- "once_cell",
3997
  "rustfft",
3998
  "serde",
3999
  "serde_json",
4000
  "strum",
4001
- "sys-locale",
4002
  "tokenizers",
4003
  "tokio",
4004
  "tracing",
@@ -4020,7 +4018,6 @@ dependencies = [
4020
  "serde",
4021
  "serde_bytes",
4022
  "strum",
4023
- "sys-locale",
4024
  "tokio",
4025
  "tracing",
4026
  ]
@@ -4097,7 +4094,6 @@ dependencies = [
4097
  "image",
4098
  "serde",
4099
  "serde_bytes",
4100
- "sys-locale",
4101
  "tokio",
4102
  ]
4103
 
@@ -7345,15 +7341,6 @@ dependencies = [
7345
  "syn 2.0.117",
7346
  ]
7347
 
7348
- [[package]]
7349
- name = "sys-locale"
7350
- version = "0.3.2"
7351
- source = "registry+https://github.com/rust-lang/crates.io-index"
7352
- checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
7353
- dependencies = [
7354
- "libc",
7355
- ]
7356
-
7357
  [[package]]
7358
  name = "sysctl"
7359
  version = "0.6.0"
 
3993
  "objc2-metal",
3994
  "objc2-metal-performance-shaders",
3995
  "objc2-metal-performance-shaders-graph",
 
3996
  "rustfft",
3997
  "serde",
3998
  "serde_json",
3999
  "strum",
 
4000
  "tokenizers",
4001
  "tokio",
4002
  "tracing",
 
4018
  "serde",
4019
  "serde_bytes",
4020
  "strum",
 
4021
  "tokio",
4022
  "tracing",
4023
  ]
 
4094
  "image",
4095
  "serde",
4096
  "serde_bytes",
 
4097
  "tokio",
4098
  ]
4099
 
 
7341
  "syn 2.0.117",
7342
  ]
7343
 
 
 
 
 
 
 
 
 
 
7344
  [[package]]
7345
  name = "sysctl"
7346
  version = "0.6.0"
Cargo.toml CHANGED
@@ -95,7 +95,6 @@ objc2-metal = "0.3"
95
  objc2-metal-performance-shaders = "0.3"
96
  objc2-metal-performance-shaders-graph = "0.3"
97
  objc2-foundation = "0.3"
98
- sys-locale = "0.3"
99
  indicatif = "0.18"
100
  minijinja = "2.16"
101
  windows = { version = "0.62", features = [
 
95
  objc2-metal-performance-shaders = "0.3"
96
  objc2-metal-performance-shaders-graph = "0.3"
97
  objc2-foundation = "0.3"
 
98
  indicatif = "0.18"
99
  minijinja = "2.16"
100
  windows = { version = "0.62", features = [
koharu-mcp/src/lib.rs CHANGED
@@ -112,7 +112,9 @@ impl KoharuMcp {
112
  #[tool(description = "List available LLM translation models with supported languages")]
113
  async fn llm_list(&self) -> Result<String, String> {
114
  let res = self.resources()?;
115
- let models = operations::llm_list(res).await.map_err(|e| e.to_string())?;
 
 
116
  serde_json::to_string_pretty(&models).map_err(|e| e.to_string())
117
  }
118
 
@@ -500,7 +502,12 @@ impl KoharuMcp {
500
  || p.color.is_some()
501
  || p.shader_effect.is_some()
502
  {
503
- let style = block.style.get_or_insert_with(TextStyle::default);
 
 
 
 
 
504
 
505
  if let Some(families) = p.font_families {
506
  style.font_families = families;
 
112
  #[tool(description = "List available LLM translation models with supported languages")]
113
  async fn llm_list(&self) -> Result<String, String> {
114
  let res = self.resources()?;
115
+ let models = operations::llm_list(res, operations::LlmListPayload { language: None })
116
+ .await
117
+ .map_err(|e| e.to_string())?;
118
  serde_json::to_string_pretty(&models).map_err(|e| e.to_string())
119
  }
120
 
 
502
  || p.color.is_some()
503
  || p.shader_effect.is_some()
504
  {
505
+ let style = block.style.get_or_insert_with(|| TextStyle {
506
+ font_families: Vec::new(),
507
+ font_size: None,
508
+ color: [0, 0, 0, 255],
509
+ effect: None,
510
+ });
511
 
512
  if let Some(families) = p.font_families {
513
  style.font_families = families;
koharu-ml/Cargo.toml CHANGED
@@ -29,12 +29,10 @@ tracing = { workspace = true }
29
  tracing-subscriber = { workspace = true }
30
  tokio = { workspace = true }
31
  rustfft = { workspace = true }
32
- once_cell = { workspace = true }
33
  futures = { workspace = true }
34
  num_cpus = { workspace = true }
35
  libloading = { workspace = true }
36
  minijinja = { workspace = true }
37
- sys-locale = { workspace = true }
38
  cudarc = { workspace = true, optional = true }
39
  objc2 = { workspace = true, optional = true }
40
  objc2-metal = { workspace = true, optional = true }
 
29
  tracing-subscriber = { workspace = true }
30
  tokio = { workspace = true }
31
  rustfft = { workspace = true }
 
32
  futures = { workspace = true }
33
  num_cpus = { workspace = true }
34
  libloading = { workspace = true }
35
  minijinja = { workspace = true }
 
36
  cudarc = { workspace = true, optional = true }
37
  objc2 = { workspace = true, optional = true }
38
  objc2-metal = { workspace = true, optional = true }
koharu-ml/bin/llm.rs CHANGED
@@ -1,8 +1,5 @@
1
  use clap::Parser;
2
- use koharu_ml::{
3
- llm::{GenerateOptions, Llm, ModelId},
4
- set_default_locale,
5
- };
6
  use tracing_subscriber::fmt::format::FmtSpan;
7
 
8
  #[derive(Parser, Debug)]
@@ -74,9 +71,8 @@ async fn main() -> anyhow::Result<()> {
74
 
75
  let args = Args::parse();
76
 
77
- set_default_locale(args.locale.clone());
78
-
79
  let mut llm = Llm::load(args.model, args.cpu).await?;
 
80
 
81
  let opts = GenerateOptions {
82
  max_tokens: args.max_tokens,
@@ -89,7 +85,7 @@ async fn main() -> anyhow::Result<()> {
89
  repeat_last_n: args.repeat_last_n,
90
  };
91
 
92
- let out = llm.generate(&args.prompt, &opts)?;
93
 
94
  println!("{}", out);
95
  println!(
 
1
  use clap::Parser;
2
+ use koharu_ml::llm::{GenerateOptions, Llm, ModelId, language_from_tag};
 
 
 
3
  use tracing_subscriber::fmt::format::FmtSpan;
4
 
5
  #[derive(Parser, Debug)]
 
71
 
72
  let args = Args::parse();
73
 
 
 
74
  let mut llm = Llm::load(args.model, args.cpu).await?;
75
+ let target_language = language_from_tag(&args.locale);
76
 
77
  let opts = GenerateOptions {
78
  max_tokens: args.max_tokens,
 
85
  repeat_last_n: args.repeat_last_n,
86
  };
87
 
88
+ let out = llm.generate(&args.prompt, &opts, Some(target_language))?;
89
 
90
  println!("{}", out);
91
  println!(
koharu-ml/src/facade.rs CHANGED
@@ -1,6 +1,6 @@
1
  use anyhow::Result;
2
  use image::{DynamicImage, Rgba};
3
- use koharu_types::{Document, FontPrediction, SerializableDynamicImage, TextBlock, TextStyle};
4
 
5
  use crate::comic_text_detector::{self, ComicTextDetector};
6
  use crate::font_detector::{self, FontDetector};
@@ -126,14 +126,8 @@ impl Model {
126
 
127
  let font_predictions = self.detect_fonts(&images, 1).await?;
128
  for (block, prediction) in doc.text_blocks.iter_mut().zip(font_predictions) {
129
- let color = prediction.text_color;
130
- let font_size = (prediction.font_size_px > 0.0).then_some(prediction.font_size_px);
131
  block.font_prediction = Some(prediction);
132
- block.style = Some(TextStyle {
133
- font_size,
134
- color: [color[0], color[1], color[2], 255],
135
- ..Default::default()
136
- });
137
  }
138
  }
139
 
 
1
  use anyhow::Result;
2
  use image::{DynamicImage, Rgba};
3
+ use koharu_types::{Document, FontPrediction, SerializableDynamicImage, TextBlock};
4
 
5
  use crate::comic_text_detector::{self, ComicTextDetector};
6
  use crate::font_detector::{self, FontDetector};
 
126
 
127
  let font_predictions = self.detect_fonts(&images, 1).await?;
128
  for (block, prediction) in doc.text_blocks.iter_mut().zip(font_predictions) {
 
 
129
  block.font_prediction = Some(prediction);
130
+ block.style = None;
 
 
 
 
131
  }
132
  }
133
 
koharu-ml/src/lib.rs CHANGED
@@ -12,7 +12,7 @@ use candle_core::utils::metal_is_available;
12
 
13
  pub use candle_core::Device;
14
  pub use koharu_http::hf_hub::set_cache_dir;
15
- pub use llm::{language_from_tag, set_default_locale, set_locale, supported_locales};
16
 
17
  pub fn device(cpu: bool) -> Result<Device> {
18
  if cpu {
 
12
 
13
  pub use candle_core::Device;
14
  pub use koharu_http::hf_hub::set_cache_dir;
15
+ pub use llm::{language_from_tag, supported_locales};
16
 
17
  pub fn device(cpu: bool) -> Result<Device> {
18
  if cpu {
koharu-ml/src/llm/facade.rs CHANGED
@@ -149,13 +149,17 @@ impl Model {
149
  matches!(*self.state.read().await, State::Ready(_))
150
  }
151
 
152
- /// Generate text from the loaded model.
153
- pub async fn generate(&self, doc: &mut impl Translatable) -> anyhow::Result<()> {
 
 
 
 
154
  let mut guard = self.state.write().await;
155
  match &mut *guard {
156
  State::Ready(llm) => {
157
  let text = doc.get_source()?;
158
- let response = llm.generate(&text, &GenerateOptions::default())?;
159
  let response = response.trim().to_string();
160
  doc.set_translation(response)
161
  }
 
149
  matches!(*self.state.read().await, State::Ready(_))
150
  }
151
 
152
+ /// Translate text using the loaded model.
153
+ pub async fn translate(
154
+ &self,
155
+ doc: &mut impl Translatable,
156
+ target_language: Option<&str>,
157
+ ) -> anyhow::Result<()> {
158
  let mut guard = self.state.write().await;
159
  match &mut *guard {
160
  State::Ready(llm) => {
161
  let text = doc.get_source()?;
162
+ let response = llm.generate(&text, &GenerateOptions::default(), target_language)?;
163
  let response = response.trim().to_string();
164
  doc.set_translation(response)
165
  }
koharu-ml/src/llm/mod.rs CHANGED
@@ -6,7 +6,7 @@ mod quantized_lfm2;
6
  mod tokenizer;
7
 
8
  pub use model::{GenerateOptions, Llm};
9
- pub use prompt::{ChatMessage, ChatRole, set_default_locale, set_locale};
10
 
11
  macro_rules! define_languages {
12
  ( $( $code:literal => $name:literal ),* $(,)? ) => {
 
6
  mod tokenizer;
7
 
8
  pub use model::{GenerateOptions, Llm};
9
+ pub use prompt::{ChatMessage, ChatRole};
10
 
11
  macro_rules! define_languages {
12
  ( $( $code:literal => $name:literal ),* $(,)? ) => {
koharu-ml/src/llm/model.rs CHANGED
@@ -136,10 +136,15 @@ impl Llm {
136
 
137
  /// Generate up to `max_tokens` following `prompt` using temperature/top-k/p settings.
138
  /// Logs simple performance metrics via `tracing`.
139
- pub fn generate(&mut self, prompt: &str, opts: &GenerateOptions) -> Result<String> {
 
 
 
 
 
140
  let prompt = self
141
  .prompt_renderer
142
- .format_chat_prompt(prompt.to_string())?;
143
  tracing::info!("Generating with prompt:\n{}", prompt);
144
 
145
  // Encode prompt
 
136
 
137
  /// Generate up to `max_tokens` following `prompt` using temperature/top-k/p settings.
138
  /// Logs simple performance metrics via `tracing`.
139
+ pub fn generate(
140
+ &mut self,
141
+ prompt: &str,
142
+ opts: &GenerateOptions,
143
+ target_language: Option<&str>,
144
+ ) -> Result<String> {
145
  let prompt = self
146
  .prompt_renderer
147
+ .format_chat_prompt(prompt.to_string(), target_language)?;
148
  tracing::info!("Generating with prompt:\n{}", prompt);
149
 
150
  // Encode prompt
koharu-ml/src/llm/prompt.rs CHANGED
@@ -1,36 +1,8 @@
1
  use minijinja::{Environment, context};
2
- use once_cell::sync::Lazy;
3
  use serde::Serialize;
4
- use std::sync::RwLock;
5
  use strum::{Display, EnumString};
6
- use sys_locale::get_locale;
7
 
8
- use crate::llm::{ModelId, language_from_tag};
9
-
10
- static LOCALE: Lazy<RwLock<String>> = Lazy::new(|| RwLock::new(system_locale_name()));
11
-
12
- fn system_locale_name() -> String {
13
- get_locale()
14
- .map(|locale| language_from_tag(&locale).to_string())
15
- .unwrap_or_else(|| "English".to_string())
16
- }
17
-
18
- fn get_default_locale() -> String {
19
- LOCALE
20
- .read()
21
- .map(|locale| locale.clone())
22
- .unwrap_or_else(|_| system_locale_name())
23
- }
24
-
25
- pub fn set_default_locale(locale: String) {
26
- set_locale(locale);
27
- }
28
-
29
- pub fn set_locale(locale: String) {
30
- if let Ok(mut guard) = LOCALE.write() {
31
- *guard = locale;
32
- }
33
- }
34
 
35
  #[derive(Debug, Clone, PartialEq, Eq, Display, EnumString)]
36
  #[strum(serialize_all = "lowercase")]
@@ -99,7 +71,7 @@ impl PromptRenderer {
99
  }
100
  }
101
 
102
- fn messages(&self, text: impl Into<String>) -> Vec<ChatMessage> {
103
  match self.model_id {
104
  // refer: https://huggingface.co/lmg-anon/vntl-llama3-8b-v2-gguf#translation-prompt
105
  ModelId::VntlLlama3_8Bv2 => vec![
@@ -124,15 +96,19 @@ impl PromptRenderer {
124
  ChatRole::User,
125
  format!(
126
  "Translate the following light novel dialog into {}, without additional explanation.\n\n{}",
127
- get_default_locale(),
128
  text.into(),
129
  ),
130
  )],
131
  }
132
  }
133
 
134
- pub fn format_chat_prompt(&self, prompt: String) -> anyhow::Result<String> {
135
- let messages = self.messages(prompt);
 
 
 
 
136
  let tmpl = self.env.template_from_str(&self.template)?;
137
 
138
  let prompt = tmpl
@@ -166,7 +142,7 @@ mod tests {
166
  "<|begin_of_text|>".to_string(),
167
  "<|end_of_text|>".to_string(),
168
  );
169
- let formatted = renderer.format_chat_prompt("こんにちは".to_string())?;
170
  let expected = "<|begin_of_text|><|start_header_id|>Metadata<|end_header_id|>\n\n<|eot_id|><|start_header_id|>Japanese<|end_header_id|>\n\nこんにちは<|eot_id|><|start_header_id|>English<|end_header_id|>\n\n";
171
  assert_eq!(formatted, expected);
172
 
@@ -182,7 +158,7 @@ mod tests {
182
  "<|begin_of_text|>".to_string(),
183
  "<|end_of_text|>".to_string(),
184
  );
185
- let formatted = renderer.format_chat_prompt("こんにちは".to_string())?;
186
  let expected = "<|begin_of_text|><|im_start|>system Translate to English, do not add any explanations, do not add or delete line breaks.<|im_end|> <|im_start|>user こんにちは<|im_end|> <|im_start|>assistant ";
187
  assert_eq!(formatted, expected);
188
 
@@ -198,7 +174,7 @@ mod tests {
198
  "<s>".to_string(),
199
  "</s>".to_string(),
200
  );
201
- let formatted = renderer.format_chat_prompt("こんにちは".to_string())?;
202
  let expected = "<|im_start|>system 你是一个视觉小说翻译模型,可以通顺地使用给定的术语表以指定的风格将日文翻译成简体中文,并联系上下文正确使用人称代词,注意不要混淆使役态和被动态的主语和宾语,不要擅自添加原文中没有的特殊符号,也不要擅自增加或减少换行。<|im_end|> <|im_start|>user こんにちは<|im_end|> <|im_start|>assistant ";
203
  assert_eq!(formatted, expected);
204
 
 
1
  use minijinja::{Environment, context};
 
2
  use serde::Serialize;
 
3
  use strum::{Display, EnumString};
 
4
 
5
+ use crate::llm::ModelId;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  #[derive(Debug, Clone, PartialEq, Eq, Display, EnumString)]
8
  #[strum(serialize_all = "lowercase")]
 
71
  }
72
  }
73
 
74
+ fn messages(&self, text: impl Into<String>, target_language: Option<&str>) -> Vec<ChatMessage> {
75
  match self.model_id {
76
  // refer: https://huggingface.co/lmg-anon/vntl-llama3-8b-v2-gguf#translation-prompt
77
  ModelId::VntlLlama3_8Bv2 => vec![
 
96
  ChatRole::User,
97
  format!(
98
  "Translate the following light novel dialog into {}, without additional explanation.\n\n{}",
99
+ target_language.unwrap_or("English"),
100
  text.into(),
101
  ),
102
  )],
103
  }
104
  }
105
 
106
+ pub fn format_chat_prompt(
107
+ &self,
108
+ prompt: String,
109
+ target_language: Option<&str>,
110
+ ) -> anyhow::Result<String> {
111
+ let messages = self.messages(prompt, target_language);
112
  let tmpl = self.env.template_from_str(&self.template)?;
113
 
114
  let prompt = tmpl
 
142
  "<|begin_of_text|>".to_string(),
143
  "<|end_of_text|>".to_string(),
144
  );
145
+ let formatted = renderer.format_chat_prompt("こんにちは".to_string(), None)?;
146
  let expected = "<|begin_of_text|><|start_header_id|>Metadata<|end_header_id|>\n\n<|eot_id|><|start_header_id|>Japanese<|end_header_id|>\n\nこんにちは<|eot_id|><|start_header_id|>English<|end_header_id|>\n\n";
147
  assert_eq!(formatted, expected);
148
 
 
158
  "<|begin_of_text|>".to_string(),
159
  "<|end_of_text|>".to_string(),
160
  );
161
+ let formatted = renderer.format_chat_prompt("こんにちは".to_string(), None)?;
162
  let expected = "<|begin_of_text|><|im_start|>system Translate to English, do not add any explanations, do not add or delete line breaks.<|im_end|> <|im_start|>user こんにちは<|im_end|> <|im_start|>assistant ";
163
  assert_eq!(formatted, expected);
164
 
 
174
  "<s>".to_string(),
175
  "</s>".to_string(),
176
  );
177
+ let formatted = renderer.format_chat_prompt("こんにちは".to_string(), None)?;
178
  let expected = "<|im_start|>system 你是一个视觉小说翻译模型,可以通顺地使用给定的术语表以指定的风格将日文翻译成简体中文,并联系上下文正确使用人称代词,注意不要混淆使役态和被动态的主语和宾语,不要擅自添加原文中没有的特殊符号,也不要擅自增加或减少换行。<|im_end|> <|im_start|>user こんにちは<|im_end|> <|im_start|>assistant ";
179
  assert_eq!(formatted, expected);
180
 
koharu-ml/tests/llm.rs CHANGED
@@ -30,7 +30,7 @@ async fn llm_generates_text_for_all_models() -> anyhow::Result<()> {
30
  repeat_last_n: 64,
31
  };
32
 
33
- let generated = llm.generate(prompt, &opts)?;
34
  assert!(
35
  !generated.trim().is_empty(),
36
  "model {model:?} should return some text"
 
30
  repeat_last_n: 64,
31
  };
32
 
33
+ let generated = llm.generate(prompt, &opts, None)?;
34
  assert!(
35
  !generated.trim().is_empty(),
36
  "model {model:?} should return some text"
koharu-pipeline/Cargo.toml CHANGED
@@ -18,6 +18,5 @@ rayon = { workspace = true }
18
  serde = { workspace = true }
19
  serde_bytes = { workspace = true }
20
  strum = { workspace = true }
21
- sys-locale = { workspace = true }
22
  tokio = { workspace = true }
23
  tracing = { workspace = true }
 
18
  serde = { workspace = true }
19
  serde_bytes = { workspace = true }
20
  strum = { workspace = true }
 
21
  tokio = { workspace = true }
22
  tracing = { workspace = true }
koharu-pipeline/src/operations.rs CHANGED
@@ -6,7 +6,6 @@ use koharu_renderer::renderer::TextShaderEffect;
6
  use rayon::iter::{IntoParallelIterator, ParallelIterator};
7
  use serde::{Deserialize, Serialize};
8
  use strum::IntoEnumIterator;
9
- use sys_locale::get_locale;
10
  use tracing::instrument;
11
 
12
  use koharu_types::{Document, SerializableDynamicImage, TextBlock};
@@ -563,16 +562,24 @@ pub async fn list_font_families(state: AppResources) -> anyhow::Result<Vec<Strin
563
  state.renderer.available_fonts()
564
  }
565
 
566
- pub async fn llm_list(state: AppResources) -> anyhow::Result<Vec<llm::ModelInfo>> {
 
 
 
 
 
 
 
 
 
567
  let mut models: Vec<ModelId> = ModelId::iter().collect();
568
  let cpu_factor = if state.llm.is_cpu() { 10 } else { 1 };
569
- let zh_locale_factor = match get_locale().unwrap_or_default() {
570
- locale if locale.starts_with("zh") => 10,
571
- _ => 1,
572
- };
573
- let non_zh_en_locale_factor = match get_locale().unwrap_or_default() {
574
- locale if locale.starts_with("zh") || locale.starts_with("en") => 1,
575
- _ => 100,
576
  };
577
 
578
  models.sort_by_key(|m| match m {
@@ -627,11 +634,8 @@ pub async fn llm_generate(state: AppResources, payload: LlmGeneratePayload) -> a
627
  .ok_or_else(|| anyhow::anyhow!("Document not found"))?
628
  };
629
 
630
- if let Some(locale) = payload.language.as_ref() {
631
- koharu_ml::set_locale(locale.clone());
632
- }
633
-
634
  let mut updated = snapshot;
 
635
 
636
  match payload.text_block_index {
637
  Some(bi) => {
@@ -639,10 +643,10 @@ pub async fn llm_generate(state: AppResources, payload: LlmGeneratePayload) -> a
639
  .text_blocks
640
  .get_mut(bi)
641
  .ok_or_else(|| anyhow::anyhow!("Text block not found"))?;
642
- state.llm.generate(text_block).await?;
643
  }
644
  None => {
645
- state.llm.generate(&mut updated).await?;
646
  }
647
  }
648
 
 
6
  use rayon::iter::{IntoParallelIterator, ParallelIterator};
7
  use serde::{Deserialize, Serialize};
8
  use strum::IntoEnumIterator;
 
9
  use tracing::instrument;
10
 
11
  use koharu_types::{Document, SerializableDynamicImage, TextBlock};
 
562
  state.renderer.available_fonts()
563
  }
564
 
565
+ #[derive(Debug, Deserialize)]
566
+ #[serde(rename_all = "camelCase")]
567
+ pub struct LlmListPayload {
568
+ pub language: Option<String>,
569
+ }
570
+
571
+ pub async fn llm_list(
572
+ state: AppResources,
573
+ payload: LlmListPayload,
574
+ ) -> anyhow::Result<Vec<llm::ModelInfo>> {
575
  let mut models: Vec<ModelId> = ModelId::iter().collect();
576
  let cpu_factor = if state.llm.is_cpu() { 10 } else { 1 };
577
+ let lang = payload.language.as_deref().unwrap_or("en");
578
+ let zh_locale_factor = if lang.starts_with("zh") { 10 } else { 1 };
579
+ let non_zh_en_locale_factor = if lang.starts_with("zh") || lang.starts_with("en") {
580
+ 1
581
+ } else {
582
+ 100
 
583
  };
584
 
585
  models.sort_by_key(|m| match m {
 
634
  .ok_or_else(|| anyhow::anyhow!("Document not found"))?
635
  };
636
 
 
 
 
 
637
  let mut updated = snapshot;
638
+ let target_language = payload.language.as_deref();
639
 
640
  match payload.text_block_index {
641
  Some(bi) => {
 
643
  .text_blocks
644
  .get_mut(bi)
645
  .ok_or_else(|| anyhow::anyhow!("Text block not found"))?;
646
+ state.llm.translate(text_block, target_language).await?;
647
  }
648
  None => {
649
+ state.llm.translate(&mut updated, target_language).await?;
650
  }
651
  }
652
 
koharu-pipeline/src/pipeline.rs CHANGED
@@ -194,10 +194,6 @@ async fn run_pipeline_inner(
194
  }
195
  }
196
 
197
- if let Some(locale) = req.language.as_ref() {
198
- koharu_ml::set_locale(locale.clone());
199
- }
200
-
201
  let start_index = req.index.unwrap_or(0);
202
  let end_index = req.index.map(|i| i + 1).unwrap_or(total_docs);
203
  let total_steps = PipelineStep::ALL.len();
@@ -237,7 +233,9 @@ async fn run_pipeline_inner(
237
  PipelineStep::Ocr => res.ml.ocr(&mut snapshot).await?,
238
  PipelineStep::Inpaint => res.ml.inpaint(&mut snapshot).await?,
239
  PipelineStep::LlmGenerate => {
240
- res.llm.generate(&mut snapshot).await?;
 
 
241
  }
242
  PipelineStep::Render => {
243
  res.renderer.render(
 
194
  }
195
  }
196
 
 
 
 
 
197
  let start_index = req.index.unwrap_or(0);
198
  let end_index = req.index.map(|i| i + 1).unwrap_or(total_docs);
199
  let total_steps = PipelineStep::ALL.len();
 
233
  PipelineStep::Ocr => res.ml.ocr(&mut snapshot).await?,
234
  PipelineStep::Inpaint => res.ml.inpaint(&mut snapshot).await?,
235
  PipelineStep::LlmGenerate => {
236
+ res.llm
237
+ .translate(&mut snapshot, req.language.as_deref())
238
+ .await?;
239
  }
240
  PipelineStep::Render => {
241
  res.renderer.render(
koharu-renderer/src/facade.rs CHANGED
@@ -101,7 +101,12 @@ impl Renderer {
101
  return Ok(());
102
  };
103
 
104
- let style = text_block.style.clone().unwrap_or_default();
 
 
 
 
 
105
  let font = self.select_font(&style)?;
106
  let block_effect = style.effect.unwrap_or(effect);
107
  let color = text_block
@@ -196,6 +201,63 @@ fn is_latin_only(text: &str) -> bool {
196
  })
197
  }
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  fn center_layout_horizontally(layout: &mut LayoutRun<'_>, container_width: f32) {
200
  if !container_width.is_finite() || container_width <= 0.0 {
201
  return;
 
101
  return Ok(());
102
  };
103
 
104
+ let style = text_block.style.clone().unwrap_or_else(|| TextStyle {
105
+ font_families: fonts_for_script(translation),
106
+ font_size: None,
107
+ color: [0, 0, 0, 255],
108
+ effect: None,
109
+ });
110
  let font = self.select_font(&style)?;
111
  let block_effect = style.effect.unwrap_or(effect);
112
  let color = text_block
 
201
  })
202
  }
203
 
204
+ /// Returns OS-appropriate font family names based on the dominant script in the text.
205
+ fn fonts_for_script(text: &str) -> Vec<String> {
206
+ let script_map = CodePointMapData::<Script>::new();
207
+ let has_cjk = text.chars().any(|c| {
208
+ matches!(
209
+ script_map.get(c),
210
+ Script::Han | Script::Hiragana | Script::Katakana | Script::Hangul | Script::Bopomofo
211
+ )
212
+ });
213
+ let has_arabic = text
214
+ .chars()
215
+ .any(|c| matches!(script_map.get(c), Script::Arabic | Script::Hebrew));
216
+
217
+ let names: &[&str] = if has_cjk {
218
+ #[cfg(target_os = "windows")]
219
+ {
220
+ &["Microsoft YaHei"]
221
+ }
222
+ #[cfg(target_os = "macos")]
223
+ {
224
+ &["PingFang SC"]
225
+ }
226
+ #[cfg(not(any(target_os = "windows", target_os = "macos")))]
227
+ {
228
+ &["Noto Sans CJK SC"]
229
+ }
230
+ } else if has_arabic {
231
+ #[cfg(target_os = "windows")]
232
+ {
233
+ &["Segoe UI"]
234
+ }
235
+ #[cfg(target_os = "macos")]
236
+ {
237
+ &["SF Pro"]
238
+ }
239
+ #[cfg(not(any(target_os = "windows", target_os = "macos")))]
240
+ {
241
+ &["Noto Sans"]
242
+ }
243
+ } else {
244
+ #[cfg(target_os = "windows")]
245
+ {
246
+ &["Segoe UI", "Arial"]
247
+ }
248
+ #[cfg(target_os = "macos")]
249
+ {
250
+ &["SF Pro", "Helvetica"]
251
+ }
252
+ #[cfg(not(any(target_os = "windows", target_os = "macos")))]
253
+ {
254
+ &["Noto Sans", "DejaVu Sans"]
255
+ }
256
+ };
257
+
258
+ names.iter().map(|s| s.to_string()).collect()
259
+ }
260
+
261
  fn center_layout_horizontally(layout: &mut LayoutRun<'_>, container_width: f32) {
262
  if !container_width.is_finite() || container_width <= 0.0 {
263
  return;
koharu-rpc/src/rpc.rs CHANGED
@@ -92,7 +92,7 @@ async fn dispatch(method: &str, params: rmpv::Value, state: AppResources) -> Res
92
  "device" => call0(operations::device, state).await,
93
  "get_documents" => call0(operations::get_documents, state).await,
94
  "list_font_families" => call0(operations::list_font_families, state).await,
95
- "llm_list" => call0(operations::llm_list, state).await,
96
  "llm_ready" => call0(operations::llm_ready, state).await,
97
  "llm_offload" => call0(operations::llm_offload, state).await,
98
  "process_cancel" => call0(operations::process_cancel, state).await,
 
92
  "device" => call0(operations::device, state).await,
93
  "get_documents" => call0(operations::get_documents, state).await,
94
  "list_font_families" => call0(operations::list_font_families, state).await,
95
+ "llm_list" => call(operations::llm_list, state, params).await,
96
  "llm_ready" => call0(operations::llm_ready, state).await,
97
  "llm_offload" => call0(operations::llm_offload, state).await,
98
  "process_cancel" => call0(operations::process_cancel, state).await,
koharu-types/Cargo.toml CHANGED
@@ -17,5 +17,4 @@ blake3 = { workspace = true }
17
  image = { workspace = true }
18
  serde = { workspace = true }
19
  serde_bytes = { workspace = true }
20
- sys-locale = { workspace = true }
21
  tokio = { workspace = true }
 
17
  image = { workspace = true }
18
  serde = { workspace = true }
19
  serde_bytes = { workspace = true }
 
20
  tokio = { workspace = true }
koharu-types/src/lib.rs CHANGED
@@ -6,11 +6,10 @@ pub use effect::TextShaderEffect;
6
  pub use font::{FontPrediction, NamedFontPrediction, TextDirection};
7
  pub use image::SerializableDynamicImage;
8
 
9
- use std::{collections::HashSet, path::PathBuf, sync::Arc};
10
 
11
  use ::image::GenericImageView;
12
  use serde::{Deserialize, Serialize};
13
- use sys_locale::get_locale;
14
  use tokio::sync::RwLock;
15
 
16
  #[derive(Default, Debug, Clone, Serialize, Deserialize)]
@@ -37,133 +36,6 @@ pub struct TextStyle {
37
  pub effect: Option<TextShaderEffect>,
38
  }
39
 
40
- impl Default for TextStyle {
41
- fn default() -> Self {
42
- TextStyle {
43
- font_families: default_font_families(),
44
- font_size: None,
45
- color: [0, 0, 0, 255],
46
- effect: None,
47
- }
48
- }
49
- }
50
-
51
- #[derive(Copy, Clone)]
52
- enum FontCategory {
53
- SimplifiedChinese,
54
- TraditionalChinese,
55
- English,
56
- }
57
-
58
- fn default_font_families() -> Vec<String> {
59
- let locale = get_locale().unwrap_or_default();
60
- let priorities = locale_font_priorities(&locale);
61
-
62
- let fonts: Vec<String> = priorities
63
- .into_iter()
64
- .flat_map(os_font_candidates)
65
- .collect();
66
-
67
- dedup_fonts(fonts)
68
- }
69
-
70
- fn locale_font_priorities(locale: &str) -> Vec<FontCategory> {
71
- let locale = locale.to_ascii_lowercase();
72
- let is_traditional = locale.starts_with("zh")
73
- && (locale.contains("tw")
74
- || locale.contains("hk")
75
- || locale.contains("mo")
76
- || locale.contains("hant"));
77
- let is_simplified = locale.starts_with("zh")
78
- && !is_traditional
79
- && (locale.contains("cn")
80
- || locale.contains("sg")
81
- || locale.contains("my")
82
- || locale.contains("hans")
83
- || locale == "zh");
84
-
85
- if is_traditional {
86
- vec![
87
- FontCategory::TraditionalChinese,
88
- FontCategory::SimplifiedChinese,
89
- FontCategory::English,
90
- ]
91
- } else if is_simplified {
92
- vec![
93
- FontCategory::SimplifiedChinese,
94
- FontCategory::TraditionalChinese,
95
- FontCategory::English,
96
- ]
97
- } else {
98
- vec![
99
- FontCategory::English,
100
- FontCategory::SimplifiedChinese,
101
- FontCategory::TraditionalChinese,
102
- ]
103
- }
104
- }
105
-
106
- fn os_font_candidates(category: FontCategory) -> Vec<String> {
107
- let mut fonts: Vec<&str> = Vec::new();
108
- match category {
109
- FontCategory::SimplifiedChinese => {
110
- #[cfg(target_os = "windows")]
111
- {
112
- fonts.extend(["Microsoft YaHei"]);
113
- }
114
- #[cfg(target_os = "macos")]
115
- {
116
- fonts.extend(["PingFang SC"]);
117
- }
118
- #[cfg(target_os = "linux")]
119
- {
120
- fonts.extend(["Noto Sans CJK SC", "Source Han Sans SC"]);
121
- }
122
- }
123
- FontCategory::TraditionalChinese => {
124
- #[cfg(target_os = "windows")]
125
- {
126
- fonts.extend(["Microsoft JhengHei"]);
127
- }
128
- #[cfg(target_os = "macos")]
129
- {
130
- fonts.extend(["PingFang TC"]);
131
- }
132
- #[cfg(target_os = "linux")]
133
- {
134
- fonts.extend(["Noto Sans CJK TC", "Source Han Sans TC"]);
135
- }
136
- }
137
- FontCategory::English => {
138
- #[cfg(target_os = "windows")]
139
- {
140
- fonts.extend(["Segoe UI", "Arial"]);
141
- }
142
- #[cfg(target_os = "macos")]
143
- {
144
- fonts.extend(["SF Pro", "Helvetica"]);
145
- }
146
- #[cfg(target_os = "linux")]
147
- {
148
- fonts.extend(["Noto Sans", "DejaVu Sans"]);
149
- }
150
- }
151
- }
152
-
153
- fonts.into_iter().map(str::to_string).collect()
154
- }
155
-
156
- fn dedup_fonts(fonts: Vec<String>) -> Vec<String> {
157
- let mut seen = HashSet::new();
158
- let mut unique = Vec::new();
159
- for font in fonts {
160
- if seen.insert(font.to_ascii_lowercase()) {
161
- unique.push(font);
162
- }
163
- }
164
- unique
165
- }
166
-
167
  #[derive(Default, Debug, Clone, Serialize, Deserialize)]
168
  #[serde(rename_all = "camelCase")]
169
  pub struct Document {
 
6
  pub use font::{FontPrediction, NamedFontPrediction, TextDirection};
7
  pub use image::SerializableDynamicImage;
8
 
9
+ use std::{path::PathBuf, sync::Arc};
10
 
11
  use ::image::GenericImageView;
12
  use serde::{Deserialize, Serialize};
 
13
  use tokio::sync::RwLock;
14
 
15
  #[derive(Default, Debug, Clone, Serialize, Deserialize)]
 
36
  pub effect: Option<TextShaderEffect>,
37
  }
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  #[derive(Default, Debug, Clone, Serialize, Deserialize)]
40
  #[serde(rename_all = "camelCase")]
41
  pub struct Document {
ui/lib/rpc-types.ts CHANGED
@@ -68,7 +68,7 @@ export type RpcMethodMap = {
68
  ]
69
  update_text_blocks: [{ index: number; textBlocks: TextBlock[] }, void]
70
  list_font_families: [void, string[]]
71
- llm_list: [void, LlmModelInfo[]]
72
  llm_load: [{ id: string }, void]
73
  llm_offload: [void, void]
74
  llm_ready: [void, boolean]
 
68
  ]
69
  update_text_blocks: [{ index: number; textBlocks: TextBlock[] }, void]
70
  list_font_families: [void, string[]]
71
+ llm_list: [{ language?: string }, LlmModelInfo[]]
72
  llm_load: [{ id: string }, void]
73
  llm_offload: [void, void]
74
  llm_ready: [void, boolean]
ui/lib/store.ts CHANGED
@@ -1,6 +1,7 @@
1
  'use client'
2
 
3
  import { create } from 'zustand'
 
4
  import {
5
  invoke,
6
  subscribeProcessProgress,
@@ -484,7 +485,7 @@ export const useAppStore = create<AppState>((set, get) => {
484
  },
485
  llmList: async () => {
486
  try {
487
- const models = await invoke('llm_list')
488
  set({ llmModels: models })
489
  const currentModel = get().llmSelectedModel
490
  const currentLanguage = get().llmSelectedLanguage
 
1
  'use client'
2
 
3
  import { create } from 'zustand'
4
+ import i18n from '@/lib/i18n'
5
  import {
6
  invoke,
7
  subscribeProcessProgress,
 
485
  },
486
  llmList: async () => {
487
  try {
488
+ const models = await invoke('llm_list', { language: i18n.language })
489
  set({ llmModels: models })
490
  const currentModel = get().llmSelectedModel
491
  const currentLanguage = get().llmSelectedLanguage