Mayo commited on
refactor: remove sys-locale
Browse files- Cargo.lock +0 -13
- Cargo.toml +0 -1
- koharu-mcp/src/lib.rs +9 -2
- koharu-ml/Cargo.toml +0 -2
- koharu-ml/bin/llm.rs +3 -7
- koharu-ml/src/facade.rs +2 -8
- koharu-ml/src/lib.rs +1 -1
- koharu-ml/src/llm/facade.rs +7 -3
- koharu-ml/src/llm/mod.rs +1 -1
- koharu-ml/src/llm/model.rs +7 -2
- koharu-ml/src/llm/prompt.rs +12 -36
- koharu-ml/tests/llm.rs +1 -1
- koharu-pipeline/Cargo.toml +0 -1
- koharu-pipeline/src/operations.rs +19 -15
- koharu-pipeline/src/pipeline.rs +3 -5
- koharu-renderer/src/facade.rs +63 -1
- koharu-rpc/src/rpc.rs +1 -1
- koharu-types/Cargo.toml +0 -1
- koharu-types/src/lib.rs +1 -129
- ui/lib/rpc-types.ts +1 -1
- ui/lib/store.ts +2 -1
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
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 =
|
| 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,
|
| 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 |
-
///
|
| 153 |
-
pub async fn
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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::
|
| 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 |
-
|
| 128 |
text.into(),
|
| 129 |
),
|
| 130 |
)],
|
| 131 |
}
|
| 132 |
}
|
| 133 |
|
| 134 |
-
pub fn format_chat_prompt(
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
let mut models: Vec<ModelId> = ModelId::iter().collect();
|
| 568 |
let cpu_factor = if state.llm.is_cpu() { 10 } else { 1 };
|
| 569 |
-
let
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 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.
|
| 643 |
}
|
| 644 |
None => {
|
| 645 |
-
state.llm.
|
| 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
|
|
|
|
|
|
|
| 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().
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" =>
|
| 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::{
|
| 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: [
|
| 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
|