Mayo commited on
Commit
d1aa81e
·
unverified ·
1 Parent(s): 8105b1d

refactor: split modules

Browse files
Files changed (41) hide show
  1. Cargo.lock +58 -115
  2. Cargo.toml +8 -2
  3. {koharu-core → koharu-http}/Cargo.toml +1 -1
  4. {koharu-core → koharu-http}/README.md +0 -0
  5. {koharu-core → koharu-http}/src/download.rs +0 -0
  6. {koharu-core → koharu-http}/src/hf_hub.rs +0 -0
  7. {koharu-core → koharu-http}/src/http.rs +0 -0
  8. {koharu-core → koharu-http}/src/lib.rs +0 -0
  9. {koharu-core → koharu-http}/src/progress.rs +0 -0
  10. koharu-ml/Cargo.toml +2 -1
  11. koharu/src/ml.rs → koharu-ml/src/facade.rs +8 -13
  12. koharu-ml/src/font_detector/mod.rs +2 -47
  13. koharu-ml/src/hf_hub.rs +3 -3
  14. koharu-ml/src/lib.rs +2 -1
  15. koharu/src/llm.rs → koharu-ml/src/llm/facade.rs +6 -6
  16. koharu-ml/src/llm/mod.rs +1 -0
  17. koharu-pipeline/Cargo.toml +23 -0
  18. koharu-pipeline/src/lib.rs +20 -0
  19. {koharu → koharu-pipeline}/src/operations.rs +11 -45
  20. {koharu → koharu-pipeline}/src/pipeline.rs +1 -1
  21. koharu-renderer/Cargo.toml +2 -0
  22. koharu/src/renderer.rs → koharu-renderer/src/facade.rs +5 -7
  23. koharu-renderer/src/lib.rs +1 -0
  24. koharu-renderer/src/renderer.rs +1 -23
  25. koharu-rpc/Cargo.toml +19 -0
  26. koharu-rpc/src/lib.rs +4 -0
  27. koharu-rpc/src/rpc.rs +226 -0
  28. {koharu → koharu-rpc}/src/server.rs +14 -6
  29. koharu-runtime/Cargo.toml +1 -1
  30. koharu-runtime/src/dylib.rs +2 -2
  31. koharu-runtime/src/zip.rs +1 -1
  32. koharu-types/Cargo.toml +21 -0
  33. koharu-types/src/effect.rs +25 -0
  34. koharu-types/src/font.rs +45 -0
  35. {koharu → koharu-types}/src/image.rs +2 -2
  36. koharu/src/state.rs → koharu-types/src/lib.rs +11 -17
  37. koharu/Cargo.toml +10 -26
  38. koharu/src/app.rs +18 -23
  39. koharu/src/khr.rs +0 -144
  40. koharu/src/lib.rs +0 -10
  41. koharu/src/rpc.rs +0 -569
Cargo.lock CHANGED
@@ -423,15 +423,6 @@ dependencies = [
423
  "system-deps",
424
  ]
425
 
426
- [[package]]
427
- name = "atomic-polyfill"
428
- version = "1.0.3"
429
- source = "registry+https://github.com/rust-lang/crates.io-index"
430
- checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
431
- dependencies = [
432
- "critical-section",
433
- ]
434
-
435
  [[package]]
436
  name = "atomic-waker"
437
  version = "1.1.2"
@@ -1067,15 +1058,6 @@ dependencies = [
1067
  "cc",
1068
  ]
1069
 
1070
- [[package]]
1071
- name = "cobs"
1072
- version = "0.3.0"
1073
- source = "registry+https://github.com/rust-lang/crates.io-index"
1074
- checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
1075
- dependencies = [
1076
- "thiserror 2.0.18",
1077
- ]
1078
-
1079
  [[package]]
1080
  name = "codespan-reporting"
1081
  version = "0.12.0"
@@ -1300,12 +1282,6 @@ dependencies = [
1300
  "itertools 0.13.0",
1301
  ]
1302
 
1303
- [[package]]
1304
- name = "critical-section"
1305
- version = "1.2.0"
1306
- source = "registry+https://github.com/rust-lang/crates.io-index"
1307
- checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
1308
-
1309
  [[package]]
1310
  name = "crossbeam-channel"
1311
  version = "0.5.15"
@@ -1770,18 +1746,6 @@ version = "1.2.2"
1770
  source = "registry+https://github.com/rust-lang/crates.io-index"
1771
  checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
1772
 
1773
- [[package]]
1774
- name = "embedded-io"
1775
- version = "0.4.0"
1776
- source = "registry+https://github.com/rust-lang/crates.io-index"
1777
- checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
1778
-
1779
- [[package]]
1780
- name = "embedded-io"
1781
- version = "0.6.1"
1782
- source = "registry+https://github.com/rust-lang/crates.io-index"
1783
- checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
1784
-
1785
  [[package]]
1786
  name = "encode_unicode"
1787
  version = "1.0.0"
@@ -1815,18 +1779,6 @@ dependencies = [
1815
  "syn 2.0.114",
1816
  ]
1817
 
1818
- [[package]]
1819
- name = "enum_dispatch"
1820
- version = "0.3.13"
1821
- source = "registry+https://github.com/rust-lang/crates.io-index"
1822
- checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
1823
- dependencies = [
1824
- "once_cell",
1825
- "proc-macro2",
1826
- "quote",
1827
- "syn 2.0.114",
1828
- ]
1829
-
1830
  [[package]]
1831
  name = "enumflags2"
1832
  version = "0.7.12"
@@ -3001,15 +2953,6 @@ dependencies = [
3001
  "smallvec",
3002
  ]
3003
 
3004
- [[package]]
3005
- name = "hash32"
3006
- version = "0.2.1"
3007
- source = "registry+https://github.com/rust-lang/crates.io-index"
3008
- checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
3009
- dependencies = [
3010
- "byteorder",
3011
- ]
3012
-
3013
  [[package]]
3014
  name = "hashbrown"
3015
  version = "0.12.3"
@@ -3040,20 +2983,6 @@ dependencies = [
3040
  "serde_core",
3041
  ]
3042
 
3043
- [[package]]
3044
- name = "heapless"
3045
- version = "0.7.17"
3046
- source = "registry+https://github.com/rust-lang/crates.io-index"
3047
- checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
3048
- dependencies = [
3049
- "atomic-polyfill",
3050
- "hash32",
3051
- "rustc_version",
3052
- "serde",
3053
- "spin",
3054
- "stable_deref_trait",
3055
- ]
3056
-
3057
  [[package]]
3058
  name = "heck"
3059
  version = "0.4.1"
@@ -4021,38 +3950,22 @@ name = "koharu"
4021
  version = "0.32.8"
4022
  dependencies = [
4023
  "anyhow",
4024
- "async-trait",
4025
- "axum",
4026
- "blake3",
4027
  "clap",
4028
  "dirs",
4029
- "enum_dispatch",
4030
- "futures",
4031
  "git-version",
4032
- "icu",
4033
- "image",
4034
- "koharu-core",
4035
  "koharu-ml",
 
4036
  "koharu-renderer",
 
4037
  "koharu-runtime",
 
4038
  "once_cell",
4039
- "open",
4040
- "postcard",
4041
- "rayon",
4042
  "rfd",
4043
- "rmp-serde",
4044
- "rmpv",
4045
- "serde",
4046
- "serde_bytes",
4047
  "serde_json",
4048
- "strum",
4049
- "sys-locale",
4050
  "tauri",
4051
  "tauri-build",
4052
  "tauri-plugin-updater",
4053
- "thiserror 2.0.18",
4054
  "tokio",
4055
- "tower",
4056
  "tracing",
4057
  "tracing-subscriber",
4058
  "windows 0.62.2",
@@ -4061,7 +3974,7 @@ dependencies = [
4061
  ]
4062
 
4063
  [[package]]
4064
- name = "koharu-core"
4065
  version = "0.32.8"
4066
  dependencies = [
4067
  "anyhow",
@@ -4092,7 +4005,8 @@ dependencies = [
4092
  "futures",
4093
  "image",
4094
  "imageproc",
4095
- "koharu-core",
 
4096
  "libloading 0.9.0",
4097
  "minijinja",
4098
  "num_cpus",
@@ -4113,6 +4027,26 @@ dependencies = [
4113
  "tracing-subscriber",
4114
  ]
4115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4116
  [[package]]
4117
  name = "koharu-renderer"
4118
  version = "0.32.8"
@@ -4127,7 +4061,9 @@ dependencies = [
4127
  "harfrust",
4128
  "icu",
4129
  "image",
 
4130
  "once_cell",
 
4131
  "serde",
4132
  "skrifa",
4133
  "tokio",
@@ -4135,6 +4071,22 @@ dependencies = [
4135
  "wgpu",
4136
  ]
4137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4138
  [[package]]
4139
  name = "koharu-runtime"
4140
  version = "0.32.8"
@@ -4143,7 +4095,7 @@ dependencies = [
4143
  "csv",
4144
  "flate2",
4145
  "futures",
4146
- "koharu-core",
4147
  "libloading 0.9.0",
4148
  "num_cpus",
4149
  "once_cell",
@@ -4156,6 +4108,19 @@ dependencies = [
4156
  "zip 7.4.0",
4157
  ]
4158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4159
  [[package]]
4160
  name = "kuchikiki"
4161
  version = "0.8.8-speedreader"
@@ -5630,19 +5595,6 @@ dependencies = [
5630
  "portable-atomic",
5631
  ]
5632
 
5633
- [[package]]
5634
- name = "postcard"
5635
- version = "1.1.3"
5636
- source = "registry+https://github.com/rust-lang/crates.io-index"
5637
- checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
5638
- dependencies = [
5639
- "cobs",
5640
- "embedded-io 0.4.0",
5641
- "embedded-io 0.6.1",
5642
- "heapless",
5643
- "serde",
5644
- ]
5645
-
5646
  [[package]]
5647
  name = "potential_utf"
5648
  version = "0.1.4"
@@ -7128,15 +7080,6 @@ dependencies = [
7128
  "system-deps",
7129
  ]
7130
 
7131
- [[package]]
7132
- name = "spin"
7133
- version = "0.9.8"
7134
- source = "registry+https://github.com/rust-lang/crates.io-index"
7135
- checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
7136
- dependencies = [
7137
- "lock_api",
7138
- ]
7139
-
7140
  [[package]]
7141
  name = "spirv"
7142
  version = "0.3.0+sdk-1.3.268.0"
 
423
  "system-deps",
424
  ]
425
 
 
 
 
 
 
 
 
 
 
426
  [[package]]
427
  name = "atomic-waker"
428
  version = "1.1.2"
 
1058
  "cc",
1059
  ]
1060
 
 
 
 
 
 
 
 
 
 
1061
  [[package]]
1062
  name = "codespan-reporting"
1063
  version = "0.12.0"
 
1282
  "itertools 0.13.0",
1283
  ]
1284
 
 
 
 
 
 
 
1285
  [[package]]
1286
  name = "crossbeam-channel"
1287
  version = "0.5.15"
 
1746
  source = "registry+https://github.com/rust-lang/crates.io-index"
1747
  checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
1748
 
 
 
 
 
 
 
 
 
 
 
 
 
1749
  [[package]]
1750
  name = "encode_unicode"
1751
  version = "1.0.0"
 
1779
  "syn 2.0.114",
1780
  ]
1781
 
 
 
 
 
 
 
 
 
 
 
 
 
1782
  [[package]]
1783
  name = "enumflags2"
1784
  version = "0.7.12"
 
2953
  "smallvec",
2954
  ]
2955
 
 
 
 
 
 
 
 
 
 
2956
  [[package]]
2957
  name = "hashbrown"
2958
  version = "0.12.3"
 
2983
  "serde_core",
2984
  ]
2985
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2986
  [[package]]
2987
  name = "heck"
2988
  version = "0.4.1"
 
3950
  version = "0.32.8"
3951
  dependencies = [
3952
  "anyhow",
 
 
 
3953
  "clap",
3954
  "dirs",
 
 
3955
  "git-version",
 
 
 
3956
  "koharu-ml",
3957
+ "koharu-pipeline",
3958
  "koharu-renderer",
3959
+ "koharu-rpc",
3960
  "koharu-runtime",
3961
+ "koharu-types",
3962
  "once_cell",
 
 
 
3963
  "rfd",
 
 
 
 
3964
  "serde_json",
 
 
3965
  "tauri",
3966
  "tauri-build",
3967
  "tauri-plugin-updater",
 
3968
  "tokio",
 
3969
  "tracing",
3970
  "tracing-subscriber",
3971
  "windows 0.62.2",
 
3974
  ]
3975
 
3976
  [[package]]
3977
+ name = "koharu-http"
3978
  version = "0.32.8"
3979
  dependencies = [
3980
  "anyhow",
 
4005
  "futures",
4006
  "image",
4007
  "imageproc",
4008
+ "koharu-http",
4009
+ "koharu-types",
4010
  "libloading 0.9.0",
4011
  "minijinja",
4012
  "num_cpus",
 
4027
  "tracing-subscriber",
4028
  ]
4029
 
4030
+ [[package]]
4031
+ name = "koharu-pipeline"
4032
+ version = "0.32.8"
4033
+ dependencies = [
4034
+ "anyhow",
4035
+ "image",
4036
+ "koharu-ml",
4037
+ "koharu-renderer",
4038
+ "koharu-types",
4039
+ "once_cell",
4040
+ "open",
4041
+ "rayon",
4042
+ "serde",
4043
+ "serde_bytes",
4044
+ "strum",
4045
+ "sys-locale",
4046
+ "tokio",
4047
+ "tracing",
4048
+ ]
4049
+
4050
  [[package]]
4051
  name = "koharu-renderer"
4052
  version = "0.32.8"
 
4061
  "harfrust",
4062
  "icu",
4063
  "image",
4064
+ "koharu-types",
4065
  "once_cell",
4066
+ "rayon",
4067
  "serde",
4068
  "skrifa",
4069
  "tokio",
 
4071
  "wgpu",
4072
  ]
4073
 
4074
+ [[package]]
4075
+ name = "koharu-rpc"
4076
+ version = "0.32.8"
4077
+ dependencies = [
4078
+ "anyhow",
4079
+ "axum",
4080
+ "futures",
4081
+ "koharu-http",
4082
+ "koharu-pipeline",
4083
+ "rmp-serde",
4084
+ "rmpv",
4085
+ "serde",
4086
+ "tokio",
4087
+ "tracing",
4088
+ ]
4089
+
4090
  [[package]]
4091
  name = "koharu-runtime"
4092
  version = "0.32.8"
 
4095
  "csv",
4096
  "flate2",
4097
  "futures",
4098
+ "koharu-http",
4099
  "libloading 0.9.0",
4100
  "num_cpus",
4101
  "once_cell",
 
4108
  "zip 7.4.0",
4109
  ]
4110
 
4111
+ [[package]]
4112
+ name = "koharu-types"
4113
+ version = "0.32.8"
4114
+ dependencies = [
4115
+ "anyhow",
4116
+ "blake3",
4117
+ "image",
4118
+ "serde",
4119
+ "serde_bytes",
4120
+ "sys-locale",
4121
+ "tokio",
4122
+ ]
4123
+
4124
  [[package]]
4125
  name = "kuchikiki"
4126
  version = "0.8.8-speedreader"
 
5595
  "portable-atomic",
5596
  ]
5597
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5598
  [[package]]
5599
  name = "potential_utf"
5600
  version = "0.1.4"
 
7080
  "system-deps",
7081
  ]
7082
 
 
 
 
 
 
 
 
 
 
7083
  [[package]]
7084
  name = "spirv"
7085
  version = "0.3.0+sdk-1.3.268.0"
Cargo.toml CHANGED
@@ -1,10 +1,13 @@
1
  [workspace]
2
  members = [
3
  "koharu",
4
- "koharu-core",
5
  "koharu-ml",
6
  "koharu-runtime",
7
  "koharu-renderer",
 
 
 
8
  ]
9
  default-members = ["koharu"]
10
  resolver = "3"
@@ -25,10 +28,13 @@ keywords = ["manga", "ocr", "translation", "deep_learning", "computer_vision"]
25
  publish = false
26
 
27
  [workspace.dependencies]
28
- koharu-core = { path = "koharu-core", default-features = false }
29
  koharu-runtime = { path = "koharu-runtime", default-features = false }
30
  koharu-ml = { path = "koharu-ml", default-features = false }
31
  koharu-renderer = { path = "koharu-renderer", default-features = false }
 
 
 
32
  candle-transformers = "=0.9.2"
33
  candle-core = "=0.9.2"
34
  candle-nn = "=0.9.2"
 
1
  [workspace]
2
  members = [
3
  "koharu",
4
+ "koharu-http",
5
  "koharu-ml",
6
  "koharu-runtime",
7
  "koharu-renderer",
8
+ "koharu-pipeline",
9
+ "koharu-rpc",
10
+ "koharu-types",
11
  ]
12
  default-members = ["koharu"]
13
  resolver = "3"
 
28
  publish = false
29
 
30
  [workspace.dependencies]
31
+ koharu-http = { path = "koharu-http", default-features = false }
32
  koharu-runtime = { path = "koharu-runtime", default-features = false }
33
  koharu-ml = { path = "koharu-ml", default-features = false }
34
  koharu-renderer = { path = "koharu-renderer", default-features = false }
35
+ koharu-pipeline = { path = "koharu-pipeline", default-features = false }
36
+ koharu-rpc = { path = "koharu-rpc", default-features = false }
37
+ koharu-types = { path = "koharu-types", default-features = false }
38
  candle-transformers = "=0.9.2"
39
  candle-core = "=0.9.2"
40
  candle-nn = "=0.9.2"
{koharu-core → koharu-http}/Cargo.toml RENAMED
@@ -1,5 +1,5 @@
1
  [package]
2
- name = "koharu-core"
3
  version.workspace = true
4
  edition.workspace = true
5
  description.workspace = true
 
1
  [package]
2
+ name = "koharu-http"
3
  version.workspace = true
4
  edition.workspace = true
5
  description.workspace = true
{koharu-core → koharu-http}/README.md RENAMED
File without changes
{koharu-core → koharu-http}/src/download.rs RENAMED
File without changes
{koharu-core → koharu-http}/src/hf_hub.rs RENAMED
File without changes
{koharu-core → koharu-http}/src/http.rs RENAMED
File without changes
{koharu-core → koharu-http}/src/lib.rs RENAMED
File without changes
{koharu-core → koharu-http}/src/progress.rs RENAMED
File without changes
koharu-ml/Cargo.toml CHANGED
@@ -12,7 +12,8 @@ keywords.workspace = true
12
  publish.workspace = true
13
 
14
  [dependencies]
15
- koharu-core = { workspace = true }
 
16
  anyhow = { workspace = true }
17
  clap = { workspace = true }
18
  image = { workspace = true }
 
12
  publish.workspace = true
13
 
14
  [dependencies]
15
+ koharu-http = { workspace = true }
16
+ koharu-types = { workspace = true }
17
  anyhow = { workspace = true }
18
  clap = { workspace = true }
19
  image = { workspace = true }
koharu/src/ml.rs → koharu-ml/src/facade.rs RENAMED
@@ -1,12 +1,11 @@
1
  use anyhow::Result;
2
  use image::{DynamicImage, Rgba};
3
- use koharu_ml::comic_text_detector::{self, ComicTextDetector};
4
- use koharu_ml::font_detector::{self, FontDetector};
5
- use koharu_ml::lama::{self, Lama};
6
- use koharu_ml::manga_ocr::{self, MangaOcr};
7
 
8
- use crate::image::SerializableDynamicImage;
9
- use crate::state::{Document, TextBlock, TextStyle};
 
 
10
 
11
  const NEAR_BLACK_THRESHOLD: u8 = 12;
12
  const GRAY_NEAR_BLACK_THRESHOLD: u8 = 60;
@@ -56,7 +55,7 @@ fn colors_similar(a: [u8; 3], b: [u8; 3]) -> bool {
56
  && a[2].abs_diff(b[2]) <= SIMILAR_COLOR_MAX_DIFF
57
  }
58
 
59
- fn normalize_font_prediction(prediction: &mut font_detector::FontPrediction) {
60
  prediction.text_color = clamp_near_white(clamp_near_black(prediction.text_color));
61
  prediction.stroke_color = clamp_near_white(clamp_near_black(prediction.stroke_color));
62
 
@@ -213,11 +212,7 @@ impl Model {
213
  Ok(result.into())
214
  }
215
 
216
- pub async fn detect_font(
217
- &self,
218
- image: &DynamicImage,
219
- top_k: usize,
220
- ) -> Result<font_detector::FontPrediction> {
221
  let mut results = self
222
  .detect_fonts(std::slice::from_ref(image), top_k)
223
  .await?;
@@ -228,7 +223,7 @@ impl Model {
228
  &self,
229
  images: &[DynamicImage],
230
  top_k: usize,
231
- ) -> Result<Vec<font_detector::FontPrediction>> {
232
  if images.is_empty() {
233
  return Ok(Vec::new());
234
  }
 
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};
7
+ use crate::lama::{self, Lama};
8
+ use crate::manga_ocr::{self, MangaOcr};
9
 
10
  const NEAR_BLACK_THRESHOLD: u8 = 12;
11
  const GRAY_NEAR_BLACK_THRESHOLD: u8 = 60;
 
55
  && a[2].abs_diff(b[2]) <= SIMILAR_COLOR_MAX_DIFF
56
  }
57
 
58
+ fn normalize_font_prediction(prediction: &mut FontPrediction) {
59
  prediction.text_color = clamp_near_white(clamp_near_black(prediction.text_color));
60
  prediction.stroke_color = clamp_near_white(clamp_near_black(prediction.stroke_color));
61
 
 
212
  Ok(result.into())
213
  }
214
 
215
+ pub async fn detect_font(&self, image: &DynamicImage, top_k: usize) -> Result<FontPrediction> {
 
 
 
 
216
  let mut results = self
217
  .detect_fonts(std::slice::from_ref(image), top_k)
218
  .await?;
 
223
  &self,
224
  images: &[DynamicImage],
225
  top_k: usize,
226
+ ) -> Result<Vec<FontPrediction>> {
227
  if images.is_empty() {
228
  return Ok(Vec::new());
229
  }
koharu-ml/src/font_detector/mod.rs CHANGED
@@ -1,5 +1,6 @@
1
  use std::{fs, path::PathBuf};
2
 
 
3
  use anyhow::{Context, Result};
4
  use candle_core::{DType, Device, IndexOp, Tensor};
5
  use candle_nn::{
@@ -7,9 +8,6 @@ use candle_nn::{
7
  ops::{sigmoid, softmax},
8
  };
9
  use image::{DynamicImage, GenericImageView, imageops::FilterType};
10
- use serde::{Deserialize, Serialize};
11
-
12
- use crate::{define_models, device};
13
 
14
  mod models;
15
  pub use models::ModelKind;
@@ -23,50 +21,7 @@ define_models! {
23
  FontNames => ("fffonion/yuzumarker-font-detection", "font-labels-ex.json"),
24
  }
25
 
26
- #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
27
- pub enum TextDirection {
28
- Horizontal,
29
- Vertical,
30
- }
31
-
32
- #[derive(Debug, Clone, Serialize, Deserialize)]
33
- pub struct NamedFontPrediction {
34
- pub index: usize,
35
- pub name: String,
36
- pub language: Option<String>,
37
- pub probability: f32,
38
- pub serif: bool,
39
- }
40
-
41
- #[derive(Debug, Clone, Serialize, Deserialize)]
42
- pub struct FontPrediction {
43
- pub top_fonts: Vec<(usize, f32)>,
44
- pub named_fonts: Vec<NamedFontPrediction>,
45
- pub direction: TextDirection,
46
- pub text_color: [u8; 3],
47
- pub stroke_color: [u8; 3],
48
- pub font_size_px: f32,
49
- pub stroke_width_px: f32,
50
- pub line_height: f32,
51
- pub angle_deg: f32,
52
- }
53
-
54
- // implement Default for FontPrediction
55
- impl Default for FontPrediction {
56
- fn default() -> Self {
57
- Self {
58
- top_fonts: Vec::new(),
59
- named_fonts: Vec::new(),
60
- direction: TextDirection::Horizontal,
61
- text_color: [0, 0, 0],
62
- stroke_color: [0, 0, 0],
63
- font_size_px: 0.0,
64
- stroke_width_px: 0.0,
65
- line_height: 1.0,
66
- angle_deg: 0.0,
67
- }
68
- }
69
- }
70
 
71
  pub struct FontDetector {
72
  model: models::Model,
 
1
  use std::{fs, path::PathBuf};
2
 
3
+ use crate::{define_models, device};
4
  use anyhow::{Context, Result};
5
  use candle_core::{DType, Device, IndexOp, Tensor};
6
  use candle_nn::{
 
8
  ops::{sigmoid, softmax},
9
  };
10
  use image::{DynamicImage, GenericImageView, imageops::FilterType};
 
 
 
11
 
12
  mod models;
13
  pub use models::ModelKind;
 
21
  FontNames => ("fffonion/yuzumarker-font-detection", "font-labels-ex.json"),
22
  }
23
 
24
+ pub use koharu_types::{FontPrediction, NamedFontPrediction, TextDirection};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  pub struct FontDetector {
27
  model: models::Model,
koharu-ml/src/hf_hub.rs CHANGED
@@ -1,7 +1,7 @@
1
  #[allow(unused_imports)]
2
- pub use koharu_core::download::model;
3
  #[allow(unused_imports)]
4
- pub use koharu_core::hf_hub::set_cache_dir;
5
 
6
  #[macro_export]
7
  macro_rules! define_models {
@@ -19,7 +19,7 @@ macro_rules! define_models {
19
  use strum::EnumProperty;
20
  let repo = self.get_str("repo").expect("repo property");
21
  let filename = self.get_str("filename").expect("filename property");
22
- koharu_core::download::model(repo, filename).await
23
  }
24
  }
25
 
 
1
  #[allow(unused_imports)]
2
+ pub use koharu_http::download::model;
3
  #[allow(unused_imports)]
4
+ pub use koharu_http::hf_hub::set_cache_dir;
5
 
6
  #[macro_export]
7
  macro_rules! define_models {
 
19
  use strum::EnumProperty;
20
  let repo = self.get_str("repo").expect("repo property");
21
  let filename = self.get_str("filename").expect("filename property");
22
+ koharu_http::download::model(repo, filename).await
23
  }
24
  }
25
 
koharu-ml/src/lib.rs CHANGED
@@ -1,6 +1,7 @@
1
  mod hf_hub;
2
 
3
  pub mod comic_text_detector;
 
4
  pub mod font_detector;
5
  pub mod lama;
6
  pub mod llm;
@@ -9,7 +10,7 @@ pub mod manga_ocr;
9
  use anyhow::Result;
10
  use candle_core::{Device, utils::metal_is_available};
11
 
12
- pub use koharu_core::hf_hub::set_cache_dir;
13
  pub use llm::{language_from_tag, set_default_locale, set_locale, supported_locales};
14
 
15
  /// Name of the compute device being used.
 
1
  mod hf_hub;
2
 
3
  pub mod comic_text_detector;
4
+ pub mod facade;
5
  pub mod font_detector;
6
  pub mod lama;
7
  pub mod llm;
 
10
  use anyhow::Result;
11
  use candle_core::{Device, utils::metal_is_available};
12
 
13
+ pub use koharu_http::hf_hub::set_cache_dir;
14
  pub use llm::{language_from_tag, set_default_locale, set_locale, supported_locales};
15
 
16
  /// Name of the compute device being used.
koharu/src/llm.rs → koharu-ml/src/llm/facade.rs RENAMED
@@ -1,12 +1,14 @@
1
- use koharu_ml::llm::{GenerateOptions, Llm, ModelId};
2
- use serde::Serialize;
3
  use std::sync::Arc;
 
 
4
  use strum::Display;
5
  use tokio::sync::RwLock;
6
 
7
- use crate::state::{Document, TextBlock};
 
 
8
 
9
- pub use koharu_ml::llm::prefetch;
10
 
11
  #[derive(Debug, Clone, Serialize)]
12
  #[serde(rename_all = "camelCase")]
@@ -128,13 +130,11 @@ impl Model {
128
  }
129
 
130
  /// Returns a read guard to the internal state.
131
- /// Callers can inspect `State` directly while holding the guard.
132
  pub async fn get(&self) -> tokio::sync::RwLockReadGuard<'_, State> {
133
  self.state.read().await
134
  }
135
 
136
  /// Returns a write guard to the internal state.
137
- /// Needed for operations that mutate the loaded LLM instance.
138
  pub async fn get_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, State> {
139
  self.state.write().await
140
  }
 
 
 
1
  use std::sync::Arc;
2
+
3
+ use serde::Serialize;
4
  use strum::Display;
5
  use tokio::sync::RwLock;
6
 
7
+ use koharu_types::{Document, TextBlock};
8
+
9
+ use super::{GenerateOptions, Llm, ModelId};
10
 
11
+ pub use super::prefetch;
12
 
13
  #[derive(Debug, Clone, Serialize)]
14
  #[serde(rename_all = "camelCase")]
 
130
  }
131
 
132
  /// Returns a read guard to the internal state.
 
133
  pub async fn get(&self) -> tokio::sync::RwLockReadGuard<'_, State> {
134
  self.state.read().await
135
  }
136
 
137
  /// Returns a write guard to the internal state.
 
138
  pub async fn get_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, State> {
139
  self.state.write().await
140
  }
koharu-ml/src/llm/mod.rs CHANGED
@@ -1,3 +1,4 @@
 
1
  mod model;
2
  pub mod prompt;
3
  mod quantized_hunyuan_dense;
 
1
+ pub mod facade;
2
  mod model;
3
  pub mod prompt;
4
  mod quantized_hunyuan_dense;
koharu-pipeline/Cargo.toml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "koharu-pipeline"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ description.workspace = true
6
+ authors.workspace = true
7
+ publish.workspace = true
8
+
9
+ [dependencies]
10
+ koharu-types = { workspace = true }
11
+ koharu-ml = { workspace = true }
12
+ koharu-renderer = { workspace = true }
13
+ anyhow = { workspace = true }
14
+ image = { workspace = true }
15
+ once_cell = { workspace = true }
16
+ open = { workspace = true }
17
+ 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 }
koharu-pipeline/src/lib.rs ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pub mod operations;
2
+ pub mod pipeline;
3
+
4
+ use std::sync::Arc;
5
+
6
+ use koharu_ml::DeviceName;
7
+ use koharu_renderer::facade::Renderer;
8
+ use koharu_types::AppState;
9
+ use tokio::sync::RwLock;
10
+
11
+ #[derive(Clone)]
12
+ pub struct AppResources {
13
+ pub state: AppState,
14
+ pub ml: Arc<koharu_ml::facade::Model>,
15
+ pub llm: Arc<koharu_ml::llm::facade::Model>,
16
+ pub renderer: Arc<Renderer>,
17
+ pub ml_device: DeviceName,
18
+ pub pipeline: Arc<RwLock<Option<pipeline::PipelineHandle>>>,
19
+ pub version: &'static str,
20
+ }
{koharu → koharu-pipeline}/src/operations.rs RENAMED
@@ -9,17 +9,14 @@ use strum::IntoEnumIterator;
9
  use sys_locale::get_locale;
10
  use tracing::instrument;
11
 
12
- use crate::{
13
- app::AppResources,
14
- image::SerializableDynamicImage,
15
- khr::{deserialize_khr, has_khr_magic, serialize_khr},
16
- llm,
17
- state::{Document, TextBlock},
18
- version,
19
- };
20
 
21
- pub async fn app_version(_state: AppResources) -> anyhow::Result<String> {
22
- Ok(version::current().to_string())
 
 
 
 
23
  }
24
 
25
  #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -42,7 +39,10 @@ pub struct OpenExternalPayload {
42
  pub url: String,
43
  }
44
 
45
- pub fn open_external(payload: OpenExternalPayload) -> anyhow::Result<()> {
 
 
 
46
  open::that(&payload.url)?;
47
  Ok(())
48
  }
@@ -142,33 +142,6 @@ pub struct FileResult {
142
  pub content_type: String,
143
  }
144
 
145
- pub async fn save_documents(state: AppResources) -> anyhow::Result<FileResult> {
146
- let guard = state.state.read().await;
147
- if guard.documents.is_empty() {
148
- anyhow::bail!("No documents to save");
149
- }
150
-
151
- let filename = if guard.documents.len() == 1 {
152
- let stem = guard.documents[0]
153
- .path
154
- .file_stem()
155
- .and_then(|s| s.to_str())
156
- .unwrap_or("project");
157
- format!("{}.khr", stem)
158
- } else {
159
- "project.khr".to_string()
160
- };
161
-
162
- let bytes = serialize_khr(&guard.documents)?;
163
- drop(guard);
164
-
165
- Ok(FileResult {
166
- filename,
167
- data: bytes,
168
- content_type: "application/octet-stream".to_string(),
169
- })
170
- }
171
-
172
  pub async fn export_document(
173
  state: AppResources,
174
  payload: IndexPayload,
@@ -766,13 +739,6 @@ pub fn load_documents(inputs: Vec<(PathBuf, Vec<u8>)>) -> anyhow::Result<Vec<Doc
766
  return Ok(vec![]);
767
  }
768
 
769
- if inputs.len() == 1 {
770
- let (_, ref bytes) = inputs[0];
771
- if has_khr_magic(bytes) {
772
- return deserialize_khr(bytes);
773
- }
774
- }
775
-
776
  let mut documents: Vec<_> = inputs
777
  .into_par_iter()
778
  .filter_map(|(path, bytes)| match Document::from_bytes(path, bytes) {
 
9
  use sys_locale::get_locale;
10
  use tracing::instrument;
11
 
12
+ use koharu_types::{Document, SerializableDynamicImage, TextBlock};
 
 
 
 
 
 
 
13
 
14
+ use koharu_ml::llm::facade as llm;
15
+
16
+ use crate::AppResources;
17
+
18
+ pub async fn app_version(state: AppResources) -> anyhow::Result<String> {
19
+ Ok(state.version.to_string())
20
  }
21
 
22
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
39
  pub url: String,
40
  }
41
 
42
+ pub async fn open_external(
43
+ _state: AppResources,
44
+ payload: OpenExternalPayload,
45
+ ) -> anyhow::Result<()> {
46
  open::that(&payload.url)?;
47
  Ok(())
48
  }
 
142
  pub content_type: String,
143
  }
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  pub async fn export_document(
146
  state: AppResources,
147
  payload: IndexPayload,
 
739
  return Ok(vec![]);
740
  }
741
 
 
 
 
 
 
 
 
742
  let mut documents: Vec<_> = inputs
743
  .into_par_iter()
744
  .filter_map(|(path, bytes)| match Document::from_bytes(path, bytes) {
{koharu → koharu-pipeline}/src/pipeline.rs RENAMED
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
11
  use std::str::FromStr;
12
  use tokio::sync::broadcast;
13
 
14
- use crate::app::AppResources;
15
 
16
  /// Steps in the processing pipeline.
17
  #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
 
11
  use std::str::FromStr;
12
  use tokio::sync::broadcast;
13
 
14
+ use crate::AppResources;
15
 
16
  /// Steps in the processing pipeline.
17
  #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
koharu-renderer/Cargo.toml CHANGED
@@ -12,6 +12,7 @@ keywords.workspace = true
12
  publish.workspace = true
13
 
14
  [dependencies]
 
15
  anyhow = { workspace = true }
16
  image = { workspace = true }
17
  once_cell = { workspace = true }
@@ -26,6 +27,7 @@ wgpu = { workspace = true }
26
  etagere = { workspace = true }
27
  bytemuck = { workspace = true }
28
  tracing = { workspace = true }
 
29
 
30
  [dev-dependencies]
31
  tokio = { workspace = true }
 
12
  publish.workspace = true
13
 
14
  [dependencies]
15
+ koharu-types = { workspace = true }
16
  anyhow = { workspace = true }
17
  image = { workspace = true }
18
  once_cell = { workspace = true }
 
27
  etagere = { workspace = true }
28
  bytemuck = { workspace = true }
29
  tracing = { workspace = true }
30
+ rayon = { workspace = true }
31
 
32
  [dev-dependencies]
33
  tokio = { workspace = true }
koharu/src/renderer.rs → koharu-renderer/src/facade.rs RENAMED
@@ -3,16 +3,14 @@ use std::sync::{Arc, Mutex};
3
  use anyhow::Result;
4
  use icu::properties::{CodePointMapData, props::Script};
5
  use image::{DynamicImage, imageops};
6
- use koharu_renderer::{
7
- font::{FamilyName, Font, FontBook, Properties},
8
- layout::{LayoutRun, TextLayout, WritingMode},
9
- renderer::{RenderOptions, TextShaderEffect, WgpuDeviceInfo, WgpuRenderer},
10
- };
11
  use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
12
 
 
 
13
  use crate::{
14
- image::SerializableDynamicImage,
15
- state::{Document, TextBlock, TextStyle},
 
16
  };
17
 
18
  pub struct Renderer {
 
3
  use anyhow::Result;
4
  use icu::properties::{CodePointMapData, props::Script};
5
  use image::{DynamicImage, imageops};
 
 
 
 
 
6
  use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
7
 
8
+ use koharu_types::{Document, SerializableDynamicImage, TextBlock, TextShaderEffect, TextStyle};
9
+
10
  use crate::{
11
+ font::{FamilyName, Font, FontBook, Properties},
12
+ layout::{LayoutRun, TextLayout, WritingMode},
13
+ renderer::{RenderOptions, WgpuDeviceInfo, WgpuRenderer},
14
  };
15
 
16
  pub struct Renderer {
koharu-renderer/src/lib.rs CHANGED
@@ -1,3 +1,4 @@
 
1
  pub mod font;
2
  pub mod layout;
3
  pub mod renderer;
 
1
+ pub mod facade;
2
  pub mod font;
3
  pub mod layout;
4
  pub mod renderer;
koharu-renderer/src/renderer.rs CHANGED
@@ -11,29 +11,7 @@ use wgpu::util::DeviceExt;
11
  use crate::font::Font;
12
  use crate::layout::{LayoutRun, PositionedGlyph, WritingMode};
13
 
14
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15
- #[serde(rename_all = "camelCase")]
16
- #[derive(Default)]
17
- pub enum TextShaderEffect {
18
- #[default]
19
- Normal,
20
- Antique,
21
- Metal,
22
- Manga,
23
- MotionBlur,
24
- }
25
-
26
- impl TextShaderEffect {
27
- fn id(self) -> f32 {
28
- match self {
29
- Self::Normal => 0.0,
30
- Self::Antique => 1.0,
31
- Self::Metal => 2.0,
32
- Self::Manga => 3.0,
33
- Self::MotionBlur => 4.0,
34
- }
35
- }
36
- }
37
 
38
  /// Options for rendering text.
39
  #[derive(Debug, Clone)]
 
11
  use crate::font::Font;
12
  use crate::layout::{LayoutRun, PositionedGlyph, WritingMode};
13
 
14
+ pub use koharu_types::TextShaderEffect;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  /// Options for rendering text.
17
  #[derive(Debug, Clone)]
koharu-rpc/Cargo.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "koharu-rpc"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ description.workspace = true
6
+ authors.workspace = true
7
+ publish.workspace = true
8
+
9
+ [dependencies]
10
+ koharu-http = { workspace = true }
11
+ koharu-pipeline = { workspace = true }
12
+ anyhow = { workspace = true }
13
+ axum = { workspace = true }
14
+ futures = { workspace = true }
15
+ rmp-serde = { workspace = true }
16
+ rmpv = { workspace = true }
17
+ serde = { workspace = true }
18
+ tokio = { workspace = true }
19
+ tracing = { workspace = true }
koharu-rpc/src/lib.rs ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ pub mod rpc;
2
+ pub mod server;
3
+
4
+ pub use rpc::{SharedResources, WsState};
koharu-rpc/src/rpc.rs ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::future::Future;
2
+ use std::sync::Arc;
3
+ use std::time::Duration;
4
+
5
+ use anyhow::Result;
6
+ use axum::{
7
+ extract::{
8
+ State,
9
+ ws::{Message, WebSocket, WebSocketUpgrade},
10
+ },
11
+ response::IntoResponse,
12
+ };
13
+ use futures::{SinkExt, StreamExt};
14
+ use serde::{Deserialize, Serialize, de::DeserializeOwned};
15
+ use tokio::sync::{broadcast, mpsc};
16
+
17
+ use koharu_pipeline::AppResources;
18
+ use koharu_pipeline::operations;
19
+
20
+ pub type SharedResources = Arc<tokio::sync::OnceCell<AppResources>>;
21
+
22
+ #[derive(Debug, Deserialize)]
23
+ struct RawIncoming {
24
+ id: u32,
25
+ method: String,
26
+ params: Option<rmpv::Value>,
27
+ }
28
+
29
+ #[derive(Debug, Serialize)]
30
+ #[serde(tag = "type")]
31
+ pub enum OutgoingMessage {
32
+ #[serde(rename = "res")]
33
+ Response {
34
+ id: u32,
35
+ #[serde(skip_serializing_if = "Option::is_none")]
36
+ result: Option<rmpv::Value>,
37
+ #[serde(skip_serializing_if = "Option::is_none")]
38
+ error: Option<String>,
39
+ },
40
+ #[serde(rename = "ntf")]
41
+ Notification { method: String, params: rmpv::Value },
42
+ }
43
+
44
+ fn ok_response(id: u32, result: rmpv::Value) -> OutgoingMessage {
45
+ OutgoingMessage::Response {
46
+ id,
47
+ result: Some(result),
48
+ error: None,
49
+ }
50
+ }
51
+
52
+ fn err_response(id: u32, msg: &str) -> OutgoingMessage {
53
+ OutgoingMessage::Response {
54
+ id,
55
+ result: None,
56
+ error: Some(msg.to_string()),
57
+ }
58
+ }
59
+
60
+ fn to_value<T: Serialize>(val: &T) -> Result<rmpv::Value> {
61
+ let bytes = rmp_serde::to_vec_named(val)?;
62
+ Ok(rmp_serde::from_slice(&bytes)?)
63
+ }
64
+
65
+ fn from_value<T: DeserializeOwned>(val: rmpv::Value) -> Result<T> {
66
+ let bytes = rmp_serde::to_vec_named(&val)?;
67
+ Ok(rmp_serde::from_slice(&bytes)?)
68
+ }
69
+
70
+ async fn call<F, Fut, P, T>(f: F, state: AppResources, params: rmpv::Value) -> Result<rmpv::Value>
71
+ where
72
+ F: FnOnce(AppResources, P) -> Fut,
73
+ Fut: Future<Output = Result<T>>,
74
+ P: DeserializeOwned,
75
+ T: Serialize,
76
+ {
77
+ to_value(&f(state, from_value(params)?).await?)
78
+ }
79
+
80
+ async fn call0<F, Fut, T>(f: F, state: AppResources) -> Result<rmpv::Value>
81
+ where
82
+ F: FnOnce(AppResources) -> Fut,
83
+ Fut: Future<Output = Result<T>>,
84
+ T: Serialize,
85
+ {
86
+ to_value(&f(state).await?)
87
+ }
88
+
89
+ async fn dispatch(method: &str, params: rmpv::Value, state: AppResources) -> Result<rmpv::Value> {
90
+ match method {
91
+ "app_version" => call0(operations::app_version, 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" => 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,
99
+ "get_document" => call(operations::get_document, state, params).await,
100
+ "get_thumbnail" => call(operations::get_thumbnail, state, params).await,
101
+ "export_document" => call(operations::export_document, state, params).await,
102
+ "open_documents" => call(operations::open_documents, state, params).await,
103
+ "open_external" => call(operations::open_external, state, params).await,
104
+ "detect" => call(operations::detect, state, params).await,
105
+ "ocr" => call(operations::ocr, state, params).await,
106
+ "inpaint" => call(operations::inpaint, state, params).await,
107
+ "update_inpaint_mask" => call(operations::update_inpaint_mask, state, params).await,
108
+ "update_brush_layer" => call(operations::update_brush_layer, state, params).await,
109
+ "inpaint_partial" => call(operations::inpaint_partial, state, params).await,
110
+ "render" => call(operations::render, state, params).await,
111
+ "update_text_blocks" => call(operations::update_text_blocks, state, params).await,
112
+ "llm_load" => call(operations::llm_load, state, params).await,
113
+ "llm_generate" => call(operations::llm_generate, state, params).await,
114
+ "process" => call(operations::process, state, params).await,
115
+ _ => Err(anyhow::anyhow!("Unknown method: {}", method)),
116
+ }
117
+ }
118
+
119
+ #[derive(Clone)]
120
+ pub struct WsState {
121
+ pub resources: SharedResources,
122
+ }
123
+
124
+ pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State<WsState>) -> impl IntoResponse {
125
+ ws.max_message_size(1024 * 1024 * 1024)
126
+ .on_upgrade(|socket| handle_socket(socket, state))
127
+ }
128
+
129
+ async fn handle_socket(socket: WebSocket, state: WsState) {
130
+ let (mut ws_sender, mut ws_receiver) = socket.split();
131
+ let (tx, mut send_rx) = mpsc::channel::<OutgoingMessage>(256);
132
+
133
+ // Notification forwarders
134
+ spawn_notification_forwarder(
135
+ "download_progress",
136
+ koharu_http::download::subscribe(),
137
+ tx.clone(),
138
+ );
139
+ spawn_notification_forwarder(
140
+ "process_progress",
141
+ koharu_pipeline::pipeline::subscribe(),
142
+ tx.clone(),
143
+ );
144
+
145
+ // Sender task: drain mpsc → WebSocket binary frames
146
+ let send_task = tokio::spawn(async move {
147
+ while let Some(msg) = send_rx.recv().await {
148
+ let Ok(bytes) = rmp_serde::to_vec_named(&msg) else {
149
+ continue;
150
+ };
151
+ if ws_sender.send(Message::Binary(bytes.into())).await.is_err() {
152
+ break;
153
+ }
154
+ }
155
+ });
156
+
157
+ // Receive loop
158
+ while let Some(Ok(msg)) = ws_receiver.next().await {
159
+ let data = match msg {
160
+ Message::Binary(data) => data,
161
+ Message::Close(_) => break,
162
+ _ => continue,
163
+ };
164
+
165
+ let raw: RawIncoming = match rmp_serde::from_slice(&data) {
166
+ Ok(r) => r,
167
+ Err(e) => {
168
+ let _ = tx
169
+ .send(err_response(0, &format!("Decode error: {e}")))
170
+ .await;
171
+ continue;
172
+ }
173
+ };
174
+
175
+ let id = raw.id;
176
+ let tx = tx.clone();
177
+ let resources = state.resources.clone();
178
+
179
+ tokio::spawn(async move {
180
+ let response = match resources.get() {
181
+ Some(res) => {
182
+ let params = raw.params.unwrap_or(rmpv::Value::Nil);
183
+ match tokio::time::timeout(
184
+ Duration::from_secs(300),
185
+ dispatch(&raw.method, params, res.clone()),
186
+ )
187
+ .await
188
+ {
189
+ Ok(Ok(result)) => ok_response(id, result),
190
+ Ok(Err(e)) => err_response(id, &format!("{e:#}")),
191
+ Err(_) => err_response(id, "Request timed out"),
192
+ }
193
+ }
194
+ None => err_response(id, "Resources not initialized"),
195
+ };
196
+ let _ = tx.send(response).await;
197
+ });
198
+ }
199
+
200
+ send_task.abort();
201
+ }
202
+
203
+ fn spawn_notification_forwarder<T: Serialize + Clone + Send + 'static>(
204
+ method: &'static str,
205
+ mut rx: broadcast::Receiver<T>,
206
+ tx: mpsc::Sender<OutgoingMessage>,
207
+ ) {
208
+ tokio::spawn(async move {
209
+ loop {
210
+ match rx.recv().await {
211
+ Ok(payload) => {
212
+ let params = to_value(&payload).unwrap_or(rmpv::Value::Nil);
213
+ let msg = OutgoingMessage::Notification {
214
+ method: method.to_string(),
215
+ params,
216
+ };
217
+ if tx.send(msg).await.is_err() {
218
+ break;
219
+ }
220
+ }
221
+ Err(broadcast::error::RecvError::Lagged(_)) => continue,
222
+ Err(broadcast::error::RecvError::Closed) => break,
223
+ }
224
+ }
225
+ });
226
+ }
{koharu → koharu-rpc}/src/server.rs RENAMED
@@ -8,12 +8,20 @@ use axum::{
8
  response::{IntoResponse, Response},
9
  routing::get,
10
  };
11
- use tauri::AssetResolver;
12
  use tokio::net::TcpListener;
13
 
14
  use crate::rpc::{self, SharedResources, WsState};
15
 
16
- fn build_router(shared: SharedResources, resolver: Arc<AssetResolver<tauri::Wry>>) -> Router {
 
 
 
 
 
 
 
 
 
17
  let ws_state = WsState { resources: shared };
18
 
19
  Router::new()
@@ -25,7 +33,7 @@ fn build_router(shared: SharedResources, resolver: Arc<AssetResolver<tauri::Wry>
25
  })
26
  }
27
 
28
- fn serve_asset(resolver: &AssetResolver<tauri::Wry>, uri: Uri) -> Response {
29
  let path = uri.path();
30
  let target = if path == "/" {
31
  "index.html"
@@ -38,8 +46,8 @@ fn serve_asset(resolver: &AssetResolver<tauri::Wry>, uri: Uri) -> Response {
38
  .unwrap_or_else(|| (StatusCode::NOT_FOUND, "Not Found").into_response())
39
  }
40
 
41
- fn resolve_asset(resolver: &AssetResolver<tauri::Wry>, path: &str) -> Option<Response> {
42
- let asset = resolver.get(path.to_string())?;
43
  let mut response = Response::new(Body::from(asset.bytes));
44
  if let Ok(ct) = HeaderValue::from_str(&asset.mime_type) {
45
  response.headers_mut().insert(header::CONTENT_TYPE, ct);
@@ -50,7 +58,7 @@ fn resolve_asset(resolver: &AssetResolver<tauri::Wry>, path: &str) -> Option<Res
50
  pub async fn serve_with_listener(
51
  listener: TcpListener,
52
  shared: SharedResources,
53
- resolver: Arc<AssetResolver<tauri::Wry>>,
54
  ) -> Result<()> {
55
  let router = build_router(shared, resolver);
56
  tracing::info!("HTTP server listening on http://{}", listener.local_addr()?);
 
8
  response::{IntoResponse, Response},
9
  routing::get,
10
  };
 
11
  use tokio::net::TcpListener;
12
 
13
  use crate::rpc::{self, SharedResources, WsState};
14
 
15
+ /// An asset returned by the resolver: raw bytes + MIME type.
16
+ pub struct Asset {
17
+ pub bytes: Vec<u8>,
18
+ pub mime_type: String,
19
+ }
20
+
21
+ /// A function that resolves a path to an asset.
22
+ pub type SharedAssetResolver = Arc<dyn Fn(&str) -> Option<Asset> + Send + Sync>;
23
+
24
+ fn build_router(shared: SharedResources, resolver: SharedAssetResolver) -> Router {
25
  let ws_state = WsState { resources: shared };
26
 
27
  Router::new()
 
33
  })
34
  }
35
 
36
+ fn serve_asset(resolver: &SharedAssetResolver, uri: Uri) -> Response {
37
  let path = uri.path();
38
  let target = if path == "/" {
39
  "index.html"
 
46
  .unwrap_or_else(|| (StatusCode::NOT_FOUND, "Not Found").into_response())
47
  }
48
 
49
+ fn resolve_asset(resolver: &SharedAssetResolver, path: &str) -> Option<Response> {
50
+ let asset = resolver(path)?;
51
  let mut response = Response::new(Body::from(asset.bytes));
52
  if let Ok(ct) = HeaderValue::from_str(&asset.mime_type) {
53
  response.headers_mut().insert(header::CONTENT_TYPE, ct);
 
58
  pub async fn serve_with_listener(
59
  listener: TcpListener,
60
  shared: SharedResources,
61
+ resolver: SharedAssetResolver,
62
  ) -> Result<()> {
63
  let router = build_router(shared, resolver);
64
  tracing::info!("HTTP server listening on http://{}", listener.local_addr()?);
koharu-runtime/Cargo.toml CHANGED
@@ -12,7 +12,7 @@ keywords.workspace = true
12
  publish.workspace = true
13
 
14
  [dependencies]
15
- koharu-core = { workspace = true }
16
  anyhow = { workspace = true }
17
  serde_json = { workspace = true }
18
  zip = { workspace = true }
 
12
  publish.workspace = true
13
 
14
  [dependencies]
15
+ koharu-http = { workspace = true }
16
  anyhow = { workspace = true }
17
  serde_json = { workspace = true }
18
  zip = { workspace = true }
koharu-runtime/src/dylib.rs CHANGED
@@ -7,8 +7,8 @@ use std::{
7
 
8
  use anyhow::{Context, Result};
9
  use futures::stream::{self, StreamExt, TryStreamExt};
10
- use koharu_core::download;
11
- use koharu_core::http::http_client;
12
  use once_cell::sync::OnceCell;
13
  use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
14
  use tokio::task;
 
7
 
8
  use anyhow::{Context, Result};
9
  use futures::stream::{self, StreamExt, TryStreamExt};
10
+ use koharu_http::download;
11
+ use koharu_http::http::http_client;
12
  use once_cell::sync::OnceCell;
13
  use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
14
  use tokio::task;
koharu-runtime/src/zip.rs CHANGED
@@ -2,7 +2,7 @@ use std::io;
2
 
3
  use anyhow::Result;
4
  use flate2::read::DeflateDecoder;
5
- use koharu_core::http::http_client;
6
 
7
  // ZIP signatures and constants
8
  const SIG_EOCD: [u8; 4] = [0x50, 0x4b, 0x05, 0x06]; // End Of Central Directory
 
2
 
3
  use anyhow::Result;
4
  use flate2::read::DeflateDecoder;
5
+ use koharu_http::http::http_client;
6
 
7
  // ZIP signatures and constants
8
  const SIG_EOCD: [u8; 4] = [0x50, 0x4b, 0x05, 0x06]; // End Of Central Directory
koharu-types/Cargo.toml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "koharu-types"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ description.workspace = true
6
+ license.workspace = true
7
+ authors.workspace = true
8
+ readme.workspace = true
9
+ homepage.workspace = true
10
+ repository.workspace = true
11
+ keywords.workspace = true
12
+ publish.workspace = true
13
+
14
+ [dependencies]
15
+ anyhow = { workspace = true }
16
+ 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 }
koharu-types/src/effect.rs ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4
+ #[serde(rename_all = "camelCase")]
5
+ #[derive(Default)]
6
+ pub enum TextShaderEffect {
7
+ #[default]
8
+ Normal,
9
+ Antique,
10
+ Metal,
11
+ Manga,
12
+ MotionBlur,
13
+ }
14
+
15
+ impl TextShaderEffect {
16
+ pub fn id(self) -> f32 {
17
+ match self {
18
+ Self::Normal => 0.0,
19
+ Self::Antique => 1.0,
20
+ Self::Metal => 2.0,
21
+ Self::Manga => 3.0,
22
+ Self::MotionBlur => 4.0,
23
+ }
24
+ }
25
+ }
koharu-types/src/font.rs ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
4
+ pub enum TextDirection {
5
+ Horizontal,
6
+ Vertical,
7
+ }
8
+
9
+ #[derive(Debug, Clone, Serialize, Deserialize)]
10
+ pub struct NamedFontPrediction {
11
+ pub index: usize,
12
+ pub name: String,
13
+ pub language: Option<String>,
14
+ pub probability: f32,
15
+ pub serif: bool,
16
+ }
17
+
18
+ #[derive(Debug, Clone, Serialize, Deserialize)]
19
+ pub struct FontPrediction {
20
+ pub top_fonts: Vec<(usize, f32)>,
21
+ pub named_fonts: Vec<NamedFontPrediction>,
22
+ pub direction: TextDirection,
23
+ pub text_color: [u8; 3],
24
+ pub stroke_color: [u8; 3],
25
+ pub font_size_px: f32,
26
+ pub stroke_width_px: f32,
27
+ pub line_height: f32,
28
+ pub angle_deg: f32,
29
+ }
30
+
31
+ impl Default for FontPrediction {
32
+ fn default() -> Self {
33
+ Self {
34
+ top_fonts: Vec::new(),
35
+ named_fonts: Vec::new(),
36
+ direction: TextDirection::Horizontal,
37
+ text_color: [0, 0, 0],
38
+ stroke_color: [0, 0, 0],
39
+ font_size_px: 0.0,
40
+ stroke_width_px: 0.0,
41
+ line_height: 1.0,
42
+ angle_deg: 0.0,
43
+ }
44
+ }
45
+ }
{koharu → koharu-types}/src/image.rs RENAMED
@@ -1,6 +1,6 @@
1
  use std::ops::Deref;
2
 
3
- use image::{ColorType, DynamicImage, codecs::webp::WebPEncoder};
4
  use serde::{Deserialize, Serialize, Serializer};
5
 
6
  #[derive(Debug, Default, Clone)]
@@ -30,7 +30,7 @@ impl<'de> Deserialize<'de> for SerializableDynamicImage {
30
  D: serde::Deserializer<'de>,
31
  {
32
  let bytes: Vec<u8> = serde_bytes::deserialize(deserializer)?;
33
- let img = image::load_from_memory(&bytes).map_err(serde::de::Error::custom)?;
34
  Ok(SerializableDynamicImage(img))
35
  }
36
  }
 
1
  use std::ops::Deref;
2
 
3
+ use ::image::{ColorType, DynamicImage, codecs::webp::WebPEncoder};
4
  use serde::{Deserialize, Serialize, Serializer};
5
 
6
  #[derive(Debug, Default, Clone)]
 
30
  D: serde::Deserializer<'de>,
31
  {
32
  let bytes: Vec<u8> = serde_bytes::deserialize(deserializer)?;
33
+ let img = ::image::load_from_memory(&bytes).map_err(serde::de::Error::custom)?;
34
  Ok(SerializableDynamicImage(img))
35
  }
36
  }
koharu/src/state.rs → koharu-types/src/lib.rs RENAMED
@@ -1,18 +1,18 @@
 
 
 
 
 
 
 
 
1
  use std::{collections::HashSet, path::PathBuf, sync::Arc};
2
 
3
- use anyhow::anyhow;
4
- use image::GenericImageView;
5
- use koharu_ml::font_detector::FontPrediction;
6
- use koharu_renderer::renderer::TextShaderEffect;
7
  use serde::{Deserialize, Serialize};
8
  use sys_locale::get_locale;
9
  use tokio::sync::RwLock;
10
 
11
- use crate::{
12
- image::SerializableDynamicImage,
13
- khr::{deserialize_khr, has_khr_magic},
14
- };
15
-
16
  #[derive(Default, Debug, Clone, Serialize, Deserialize)]
17
  #[serde(rename_all = "camelCase")]
18
  pub struct TextBlock {
@@ -188,22 +188,16 @@ impl Document {
188
  documents
189
  .into_iter()
190
  .next()
191
- .ok_or_else(|| anyhow!("No document found in file"))
192
  }
193
 
194
  pub fn from_bytes(path: impl Into<PathBuf>, bytes: Vec<u8>) -> anyhow::Result<Vec<Self>> {
195
  let path = path.into();
196
-
197
- if has_khr_magic(&bytes) {
198
- return deserialize_khr(&bytes)
199
- .map_err(|err| anyhow!("Failed to load documents: {err}"));
200
- }
201
-
202
  Ok(vec![Self::image(path, bytes)?])
203
  }
204
 
205
  fn image(path: PathBuf, bytes: Vec<u8>) -> anyhow::Result<Self> {
206
- let img = image::load_from_memory(&bytes)?;
207
  let (width, height) = img.dimensions();
208
  let id = blake3::hash(&bytes).to_hex().to_string();
209
  let name = path
 
1
+ mod effect;
2
+ mod font;
3
+ mod image;
4
+
5
+ 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)]
17
  #[serde(rename_all = "camelCase")]
18
  pub struct TextBlock {
 
188
  documents
189
  .into_iter()
190
  .next()
191
+ .ok_or_else(|| anyhow::anyhow!("No document found in file"))
192
  }
193
 
194
  pub fn from_bytes(path: impl Into<PathBuf>, bytes: Vec<u8>) -> anyhow::Result<Vec<Self>> {
195
  let path = path.into();
 
 
 
 
 
 
196
  Ok(vec![Self::image(path, bytes)?])
197
  }
198
 
199
  fn image(path: PathBuf, bytes: Vec<u8>) -> anyhow::Result<Self> {
200
+ let img = ::image::load_from_memory(&bytes)?;
201
  let (width, height) = img.dimensions();
202
  let id = blake3::hash(&bytes).to_hex().to_string();
203
  let name = path
koharu/Cargo.toml CHANGED
@@ -22,40 +22,24 @@ include = [
22
  ]
23
 
24
  [dependencies]
25
- koharu-core = { workspace = true }
26
  koharu-ml = { workspace = true }
 
27
  koharu-renderer = { workspace = true }
 
28
  koharu-runtime = { workspace = true }
 
29
  anyhow = { workspace = true }
30
- futures = { workspace = true }
 
 
 
 
31
  rfd = { workspace = true }
32
- open = { workspace = true }
33
- serde = { workspace = true }
34
- serde_bytes = { workspace = true }
35
- image = { workspace = true }
36
- tracing = { workspace = true }
37
- tracing-subscriber = { workspace = true }
38
- postcard = { workspace = true }
39
- thiserror = { workspace = true }
40
  tauri = { workspace = true }
41
  tauri-plugin-updater = { workspace = true }
42
- blake3 = { workspace = true }
43
  tokio = { workspace = true }
44
- rayon = { workspace = true }
45
- strum = { workspace = true }
46
- dirs = { workspace = true }
47
- clap = { workspace = true }
48
- once_cell = { workspace = true }
49
- sys-locale = { workspace = true }
50
- icu = { workspace = true }
51
- axum = { workspace = true }
52
- tower = { workspace = true }
53
- rmp-serde = { workspace = true }
54
- rmpv = { workspace = true }
55
- enum_dispatch = { workspace = true }
56
- async-trait = { workspace = true }
57
- git-version = { workspace = true }
58
- serde_json = { workspace = true }
59
 
60
  [target.'cfg(target_os = "windows")'.dependencies]
61
  windows = { workspace = true }
 
22
  ]
23
 
24
  [dependencies]
 
25
  koharu-ml = { workspace = true }
26
+ koharu-pipeline = { workspace = true }
27
  koharu-renderer = { workspace = true }
28
+ koharu-rpc = { workspace = true }
29
  koharu-runtime = { workspace = true }
30
+ koharu-types = { workspace = true }
31
  anyhow = { workspace = true }
32
+ clap = { workspace = true }
33
+ dirs = { workspace = true }
34
+ git-version = { workspace = true }
35
+ serde_json = { workspace = true }
36
+ once_cell = { workspace = true }
37
  rfd = { workspace = true }
 
 
 
 
 
 
 
 
38
  tauri = { workspace = true }
39
  tauri-plugin-updater = { workspace = true }
 
40
  tokio = { workspace = true }
41
+ tracing = { workspace = true }
42
+ tracing-subscriber = { workspace = true }
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  [target.'cfg(target_os = "windows")'.dependencies]
45
  windows = { workspace = true }
koharu/src/app.rs CHANGED
@@ -2,21 +2,18 @@ use std::{path::PathBuf, sync::Arc};
2
 
3
  use anyhow::{Context, Result};
4
  use clap::Parser;
5
- use koharu_ml::{DeviceName, cuda_is_available, device_name};
6
- use koharu_runtime::{ensure_dylibs, preload_dylibs};
7
  use once_cell::sync::Lazy;
8
  use rfd::MessageDialog;
9
  use tauri::Manager;
10
  use tokio::{net::TcpListener, sync::RwLock};
11
  use tracing_subscriber::fmt::format::FmtSpan;
12
 
13
- use crate::{
14
- llm, ml,
15
- renderer::Renderer,
16
- rpc::SharedResources,
17
- server,
18
- state::{AppState, State},
19
- };
20
 
21
  static APP_ROOT: Lazy<PathBuf> = Lazy::new(|| {
22
  dirs::data_local_dir()
@@ -26,16 +23,6 @@ static APP_ROOT: Lazy<PathBuf> = Lazy::new(|| {
26
  static LIB_ROOT: Lazy<PathBuf> = Lazy::new(|| APP_ROOT.join("libs"));
27
  static MODEL_ROOT: Lazy<PathBuf> = Lazy::new(|| APP_ROOT.join("models"));
28
 
29
- #[derive(Clone)]
30
- pub struct AppResources {
31
- pub state: AppState,
32
- pub ml: Arc<ml::Model>,
33
- pub llm: Arc<llm::Model>,
34
- pub renderer: Arc<Renderer>,
35
- pub ml_device: DeviceName,
36
- pub pipeline: Arc<RwLock<Option<crate::pipeline::PipelineHandle>>>,
37
- }
38
-
39
  #[derive(Parser)]
40
  #[command(version = crate::version::APP_VERSION, about)]
41
  struct Cli {
@@ -117,7 +104,7 @@ fn initialize(headless: bool, _debug: bool) -> Result<()> {
117
 
118
  async fn prefetch() -> Result<()> {
119
  ensure_dylibs(LIB_ROOT.to_path_buf()).await?;
120
- ml::prefetch().await?;
121
 
122
  Ok(())
123
  }
@@ -146,11 +133,11 @@ async fn build_resources(cpu: bool) -> Result<AppResources> {
146
 
147
  let ml_device = device_name(cpu);
148
  let ml = Arc::new(
149
- ml::Model::new(cpu)
150
  .await
151
  .context("Failed to initialize ML model")?,
152
  );
153
- let llm = Arc::new(llm::Model::new(cpu));
154
  let renderer = Arc::new(Renderer::new().context("Failed to initialize renderer")?);
155
  let state = Arc::new(RwLock::new(State::default()));
156
 
@@ -161,6 +148,7 @@ async fn build_resources(cpu: bool) -> Result<AppResources> {
161
  renderer,
162
  ml_device,
163
  pipeline: Arc::new(RwLock::new(None)),
 
164
  })
165
  }
166
 
@@ -188,7 +176,14 @@ pub async fn run() -> Result<()> {
188
  .append_invoke_initialization_script(format!("window.__KOHARU_WS_PORT__ = {};", ws_port))
189
  .build(tauri::generate_context!())?;
190
 
191
- let resolver = Arc::new(app.asset_resolver());
 
 
 
 
 
 
 
192
  tokio::spawn({
193
  let shared = shared.clone();
194
  async move {
 
2
 
3
  use anyhow::{Context, Result};
4
  use clap::Parser;
 
 
5
  use once_cell::sync::Lazy;
6
  use rfd::MessageDialog;
7
  use tauri::Manager;
8
  use tokio::{net::TcpListener, sync::RwLock};
9
  use tracing_subscriber::fmt::format::FmtSpan;
10
 
11
+ use koharu_ml::{cuda_is_available, device_name};
12
+ use koharu_pipeline::AppResources;
13
+ use koharu_renderer::facade::Renderer;
14
+ use koharu_rpc::{SharedResources, server};
15
+ use koharu_runtime::{ensure_dylibs, preload_dylibs};
16
+ use koharu_types::State;
 
17
 
18
  static APP_ROOT: Lazy<PathBuf> = Lazy::new(|| {
19
  dirs::data_local_dir()
 
23
  static LIB_ROOT: Lazy<PathBuf> = Lazy::new(|| APP_ROOT.join("libs"));
24
  static MODEL_ROOT: Lazy<PathBuf> = Lazy::new(|| APP_ROOT.join("models"));
25
 
 
 
 
 
 
 
 
 
 
 
26
  #[derive(Parser)]
27
  #[command(version = crate::version::APP_VERSION, about)]
28
  struct Cli {
 
104
 
105
  async fn prefetch() -> Result<()> {
106
  ensure_dylibs(LIB_ROOT.to_path_buf()).await?;
107
+ koharu_ml::facade::prefetch().await?;
108
 
109
  Ok(())
110
  }
 
133
 
134
  let ml_device = device_name(cpu);
135
  let ml = Arc::new(
136
+ koharu_ml::facade::Model::new(cpu)
137
  .await
138
  .context("Failed to initialize ML model")?,
139
  );
140
+ let llm = Arc::new(koharu_ml::llm::facade::Model::new(cpu));
141
  let renderer = Arc::new(Renderer::new().context("Failed to initialize renderer")?);
142
  let state = Arc::new(RwLock::new(State::default()));
143
 
 
148
  renderer,
149
  ml_device,
150
  pipeline: Arc::new(RwLock::new(None)),
151
+ version: crate::version::current(),
152
  })
153
  }
154
 
 
176
  .append_invoke_initialization_script(format!("window.__KOHARU_WS_PORT__ = {};", ws_port))
177
  .build(tauri::generate_context!())?;
178
 
179
+ let tauri_resolver = Arc::new(app.asset_resolver());
180
+ let resolver: server::SharedAssetResolver = Arc::new(move |path: &str| {
181
+ let asset = tauri_resolver.get(path.to_string())?;
182
+ Some(server::Asset {
183
+ bytes: asset.bytes.to_vec(),
184
+ mime_type: asset.mime_type.clone(),
185
+ })
186
+ });
187
  tokio::spawn({
188
  let shared = shared.clone();
189
  async move {
koharu/src/khr.rs DELETED
@@ -1,144 +0,0 @@
1
- use std::io::Cursor;
2
-
3
- use anyhow::bail;
4
- use image::{DynamicImage, GenericImageView, ImageFormat, RgbaImage, imageops};
5
- use once_cell::sync::Lazy;
6
-
7
- use crate::state::Document;
8
-
9
- pub const KHR_MAGIC: &[u8; 4] = b"khr!";
10
- const KHR_FOOTER_LEN: usize = KHR_MAGIC.len() + std::mem::size_of::<u64>();
11
- const THUMBNAIL_HEIGHT: u32 = 300;
12
- const THUMBNAIL_WIDTH: u32 = THUMBNAIL_HEIGHT * 4 / 3; // 4:3 aspect for contact sheet
13
- const ICON_BYTES: &[u8] = include_bytes!("../icons/Square142x142Logo.png");
14
-
15
- static ICON_IMAGE: Lazy<RgbaImage> = Lazy::new(|| {
16
- image::load_from_memory(ICON_BYTES)
17
- .expect("failed to decode embedded icon")
18
- .to_rgba8()
19
- });
20
-
21
- pub fn has_khr_magic(bytes: &[u8]) -> bool {
22
- bytes.len() >= KHR_MAGIC.len() && &bytes[bytes.len() - KHR_MAGIC.len()..] == KHR_MAGIC
23
- }
24
-
25
- pub fn serialize_khr(documents: &[Document]) -> anyhow::Result<Vec<u8>> {
26
- let thumbnail = thumbnail_contact_sheet(documents);
27
- let mut thumbnail_bytes = Vec::new();
28
- thumbnail.write_to(&mut Cursor::new(&mut thumbnail_bytes), ImageFormat::Jpeg)?;
29
-
30
- let khr_bytes = postcard::to_allocvec(documents)?;
31
- let khr_offset = thumbnail_bytes.len() as u64;
32
-
33
- let mut output = thumbnail_bytes;
34
- output.extend_from_slice(&khr_bytes);
35
- output.extend_from_slice(&khr_offset.to_le_bytes());
36
- output.extend_from_slice(KHR_MAGIC);
37
-
38
- Ok(output)
39
- }
40
-
41
- pub fn deserialize_khr(bytes: &[u8]) -> anyhow::Result<Vec<Document>> {
42
- if bytes.len() >= KHR_FOOTER_LEN && has_khr_magic(bytes) {
43
- let offset_start = bytes.len() - KHR_FOOTER_LEN;
44
- let offset_bytes: [u8; 8] = bytes[offset_start..offset_start + 8]
45
- .try_into()
46
- .expect("slice with exact length");
47
- let khr_offset = u64::from_le_bytes(offset_bytes) as usize;
48
- let khr_end = bytes.len() - KHR_FOOTER_LEN;
49
-
50
- if khr_offset > khr_end {
51
- bail!("Invalid KHR offset in file");
52
- }
53
-
54
- let khr_bytes = &bytes[khr_offset..khr_end];
55
- return decode_postcard(khr_bytes);
56
- }
57
-
58
- // fallback to legacy format without footer/signature
59
- decode_postcard(bytes)
60
- }
61
-
62
- fn decode_postcard(bytes: &[u8]) -> anyhow::Result<Vec<Document>> {
63
- if let Ok(documents) = postcard::from_bytes(bytes) {
64
- return Ok(documents);
65
- }
66
-
67
- let document: Document = postcard::from_bytes(bytes)?;
68
- Ok(vec![document])
69
- }
70
-
71
- fn thumbnail_contact_sheet(documents: &[Document]) -> DynamicImage {
72
- if documents.is_empty() {
73
- return DynamicImage::new_rgba8(1, 1);
74
- }
75
-
76
- let mut canvas = RgbaImage::from_pixel(
77
- THUMBNAIL_WIDTH,
78
- THUMBNAIL_HEIGHT,
79
- image::Rgba([255, 255, 255, 255]),
80
- );
81
-
82
- // If there's only one document, fill the entire canvas with it.
83
- if documents.len() == 1 {
84
- let thumb = documents[0]
85
- .image
86
- .thumbnail(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
87
- let (thumb_w, thumb_h) = thumb.dimensions();
88
- let x = ((THUMBNAIL_WIDTH - thumb_w) / 2) as i64;
89
- let y = ((THUMBNAIL_HEIGHT - thumb_h) / 2) as i64;
90
- imageops::overlay(&mut canvas, &thumb.to_rgba8(), x, y);
91
- } else {
92
- // First image takes the left 1/3 of the canvas.
93
- let left_width = THUMBNAIL_WIDTH / 3;
94
- let first_thumb = documents[0].image.thumbnail(left_width, THUMBNAIL_HEIGHT);
95
- let (first_w, first_h) = first_thumb.dimensions();
96
- let first_x = ((left_width - first_w) / 2) as i64;
97
- let first_y = ((THUMBNAIL_HEIGHT - first_h) / 2) as i64;
98
- imageops::overlay(&mut canvas, &first_thumb.to_rgba8(), first_x, first_y);
99
-
100
- // Remaining images are packed into the right 2/3 area.
101
- let remaining = &documents[1..];
102
- if !remaining.is_empty() {
103
- let area_width = THUMBNAIL_WIDTH - left_width;
104
- let area_height = THUMBNAIL_HEIGHT;
105
-
106
- let cols = ((remaining.len() as f64).sqrt().ceil() as u32).max(1);
107
- let rows = (remaining.len() as u32).div_ceil(cols);
108
-
109
- let cell_w = (area_width / cols).max(1);
110
- let cell_h = (area_height / rows).max(1);
111
-
112
- for (idx, document) in remaining.iter().enumerate() {
113
- let thumb = document.image.thumbnail(cell_w, cell_h);
114
- let (thumb_w, thumb_h) = thumb.dimensions();
115
-
116
- let col = (idx as u32) % cols;
117
- let row = (idx as u32) / cols;
118
-
119
- let base_x = left_width as i64 + (col * cell_w) as i64;
120
- let base_y = (row * cell_h) as i64;
121
- let x = base_x + ((cell_w as i64 - thumb_w as i64) / 2);
122
- let y = base_y + ((cell_h as i64 - thumb_h as i64) / 2);
123
-
124
- imageops::overlay(&mut canvas, &thumb.to_rgba8(), x, y);
125
- }
126
- }
127
- }
128
-
129
- // overlay small app icon on the bottom-left of the composite thumbnail
130
- let icon = &*ICON_IMAGE;
131
- let icon_w = icon.width() as i64;
132
- let icon_h = icon.height() as i64;
133
- let canvas_w = canvas.width() as i64;
134
- let canvas_h = canvas.height() as i64;
135
-
136
- if canvas_w >= icon_w && canvas_h >= icon_h {
137
- let padding = 8i64;
138
- let x = padding;
139
- let y = (canvas_h - icon_h).max(0);
140
- imageops::overlay(&mut canvas, icon, x, y);
141
- }
142
-
143
- DynamicImage::ImageRgba8(canvas)
144
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
koharu/src/lib.rs CHANGED
@@ -1,14 +1,4 @@
1
  pub mod app;
2
- pub mod image;
3
- pub mod khr;
4
- pub mod llm;
5
- pub mod ml;
6
- pub mod operations;
7
- pub mod pipeline;
8
- pub mod renderer;
9
- pub mod rpc;
10
- pub mod server;
11
- pub mod state;
12
  pub mod version;
13
  #[cfg(target_os = "windows")]
14
  pub mod windows;
 
1
  pub mod app;
 
 
 
 
 
 
 
 
 
 
2
  pub mod version;
3
  #[cfg(target_os = "windows")]
4
  pub mod windows;
koharu/src/rpc.rs DELETED
@@ -1,569 +0,0 @@
1
- use std::sync::Arc;
2
- use std::time::Duration;
3
-
4
- use anyhow::Result;
5
- use async_trait::async_trait;
6
- use axum::{
7
- extract::{
8
- State,
9
- ws::{Message, WebSocket, WebSocketUpgrade},
10
- },
11
- response::IntoResponse,
12
- };
13
- use futures::{SinkExt, StreamExt};
14
- use serde::{Deserialize, Serialize};
15
- use tokio::sync::{broadcast, mpsc};
16
- use tower::{Service, ServiceBuilder, ServiceExt, service_fn, timeout::TimeoutLayer};
17
-
18
- use crate::app::AppResources;
19
- use crate::operations;
20
-
21
- // --- Shared Resources (lazy init) ---
22
-
23
- pub type SharedResources = Arc<tokio::sync::OnceCell<AppResources>>;
24
-
25
- // --- Wire Protocol ---
26
-
27
- #[derive(Debug, Deserialize)]
28
- struct RawIncoming {
29
- id: u32,
30
- method: String,
31
- params: Option<rmpv::Value>,
32
- }
33
-
34
- #[derive(Debug, Serialize)]
35
- #[serde(tag = "type")]
36
- pub enum OutgoingMessage {
37
- #[serde(rename = "res")]
38
- Response {
39
- id: u32,
40
- #[serde(skip_serializing_if = "Option::is_none")]
41
- result: Option<rmpv::Value>,
42
- #[serde(skip_serializing_if = "Option::is_none")]
43
- error: Option<String>,
44
- },
45
- #[serde(rename = "ntf")]
46
- Notification { method: String, params: rmpv::Value },
47
- }
48
-
49
- fn ok_response(id: u32, result: rmpv::Value) -> OutgoingMessage {
50
- OutgoingMessage::Response {
51
- id,
52
- result: Some(result),
53
- error: None,
54
- }
55
- }
56
-
57
- fn err_response(id: u32, msg: &str) -> OutgoingMessage {
58
- OutgoingMessage::Response {
59
- id,
60
- result: None,
61
- error: Some(msg.to_string()),
62
- }
63
- }
64
-
65
- // --- Value conversion helpers ---
66
-
67
- fn to_value<T: Serialize>(val: &T) -> Result<rmpv::Value> {
68
- let bytes = rmp_serde::to_vec_named(val)?;
69
- Ok(rmp_serde::from_slice(&bytes)?)
70
- }
71
-
72
- fn from_value<T: serde::de::DeserializeOwned>(val: rmpv::Value) -> Result<T> {
73
- let bytes = rmp_serde::to_vec_named(&val)?;
74
- Ok(rmp_serde::from_slice(&bytes)?)
75
- }
76
-
77
- // --- Handler Trait ---
78
-
79
- #[async_trait]
80
- #[enum_dispatch::enum_dispatch]
81
- pub trait RpcCall {
82
- async fn call(self, res: AppResources) -> Result<rmpv::Value>;
83
- }
84
-
85
- // --- Handler Structs ---
86
-
87
- // No-param handlers
88
-
89
- #[derive(Deserialize)]
90
- pub struct AppVersionHandler;
91
-
92
- #[async_trait]
93
- impl RpcCall for AppVersionHandler {
94
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
95
- let v = operations::app_version(state).await?;
96
- to_value(&v)
97
- }
98
- }
99
-
100
- #[derive(Deserialize)]
101
- pub struct DeviceHandler;
102
-
103
- #[async_trait]
104
- impl RpcCall for DeviceHandler {
105
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
106
- let v = operations::device(state).await?;
107
- to_value(&v)
108
- }
109
- }
110
-
111
- #[derive(Deserialize)]
112
- pub struct GetDocumentsHandler;
113
-
114
- #[async_trait]
115
- impl RpcCall for GetDocumentsHandler {
116
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
117
- let v = operations::get_documents(state).await?;
118
- to_value(&v)
119
- }
120
- }
121
-
122
- #[derive(Deserialize)]
123
- pub struct ListFontFamiliesHandler;
124
-
125
- #[async_trait]
126
- impl RpcCall for ListFontFamiliesHandler {
127
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
128
- let v = operations::list_font_families(state).await?;
129
- to_value(&v)
130
- }
131
- }
132
-
133
- #[derive(Deserialize)]
134
- pub struct LlmListHandler;
135
-
136
- #[async_trait]
137
- impl RpcCall for LlmListHandler {
138
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
139
- let v = operations::llm_list(state).await?;
140
- to_value(&v)
141
- }
142
- }
143
-
144
- #[derive(Deserialize)]
145
- pub struct LlmOffloadHandler;
146
-
147
- #[async_trait]
148
- impl RpcCall for LlmOffloadHandler {
149
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
150
- operations::llm_offload(state).await?;
151
- Ok(rmpv::Value::Nil)
152
- }
153
- }
154
-
155
- #[derive(Deserialize)]
156
- pub struct LlmReadyHandler;
157
-
158
- #[async_trait]
159
- impl RpcCall for LlmReadyHandler {
160
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
161
- let v = operations::llm_ready(state).await?;
162
- to_value(&v)
163
- }
164
- }
165
-
166
- #[derive(Deserialize)]
167
- pub struct SaveDocumentsHandler;
168
-
169
- #[async_trait]
170
- impl RpcCall for SaveDocumentsHandler {
171
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
172
- let v = operations::save_documents(state).await?;
173
- to_value(&v)
174
- }
175
- }
176
-
177
- #[derive(Deserialize)]
178
- pub struct ProcessCancelHandler;
179
-
180
- #[async_trait]
181
- impl RpcCall for ProcessCancelHandler {
182
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
183
- operations::process_cancel(state).await?;
184
- Ok(rmpv::Value::Nil)
185
- }
186
- }
187
-
188
- // Index-based handlers (wrapping IndexPayload)
189
-
190
- #[derive(Deserialize)]
191
- #[serde(transparent)]
192
- pub struct GetDocumentHandler(operations::IndexPayload);
193
-
194
- #[async_trait]
195
- impl RpcCall for GetDocumentHandler {
196
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
197
- let v = operations::get_document(state, self.0).await?;
198
- to_value(&v)
199
- }
200
- }
201
-
202
- #[derive(Deserialize)]
203
- #[serde(transparent)]
204
- pub struct GetThumbnailHandler(operations::IndexPayload);
205
-
206
- #[async_trait]
207
- impl RpcCall for GetThumbnailHandler {
208
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
209
- let v = operations::get_thumbnail(state, self.0).await?;
210
- to_value(&v)
211
- }
212
- }
213
-
214
- #[derive(Deserialize)]
215
- #[serde(transparent)]
216
- pub struct ExportDocumentHandler(operations::IndexPayload);
217
-
218
- #[async_trait]
219
- impl RpcCall for ExportDocumentHandler {
220
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
221
- let v = operations::export_document(state, self.0).await?;
222
- to_value(&v)
223
- }
224
- }
225
-
226
- #[derive(Deserialize)]
227
- #[serde(transparent)]
228
- pub struct DetectHandler(operations::IndexPayload);
229
-
230
- #[async_trait]
231
- impl RpcCall for DetectHandler {
232
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
233
- operations::detect(state, self.0).await?;
234
- Ok(rmpv::Value::Nil)
235
- }
236
- }
237
-
238
- #[derive(Deserialize)]
239
- #[serde(transparent)]
240
- pub struct OcrHandler(operations::IndexPayload);
241
-
242
- #[async_trait]
243
- impl RpcCall for OcrHandler {
244
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
245
- operations::ocr(state, self.0).await?;
246
- Ok(rmpv::Value::Nil)
247
- }
248
- }
249
-
250
- #[derive(Deserialize)]
251
- #[serde(transparent)]
252
- pub struct InpaintHandler(operations::IndexPayload);
253
-
254
- #[async_trait]
255
- impl RpcCall for InpaintHandler {
256
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
257
- operations::inpaint(state, self.0).await?;
258
- Ok(rmpv::Value::Nil)
259
- }
260
- }
261
-
262
- // Complex payload handlers
263
-
264
- #[derive(Deserialize)]
265
- #[serde(transparent)]
266
- pub struct OpenExternalHandler(operations::OpenExternalPayload);
267
-
268
- #[async_trait]
269
- impl RpcCall for OpenExternalHandler {
270
- async fn call(self, _state: AppResources) -> Result<rmpv::Value> {
271
- operations::open_external(self.0)?;
272
- Ok(rmpv::Value::Nil)
273
- }
274
- }
275
-
276
- #[derive(Deserialize)]
277
- #[serde(transparent)]
278
- pub struct OpenDocumentsHandler(operations::OpenDocumentsPayload);
279
-
280
- #[async_trait]
281
- impl RpcCall for OpenDocumentsHandler {
282
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
283
- let v = operations::open_documents(state, self.0).await?;
284
- to_value(&v)
285
- }
286
- }
287
-
288
- #[derive(Deserialize)]
289
- #[serde(transparent)]
290
- pub struct UpdateInpaintMaskHandler(operations::UpdateInpaintMaskPayload);
291
-
292
- #[async_trait]
293
- impl RpcCall for UpdateInpaintMaskHandler {
294
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
295
- operations::update_inpaint_mask(state, self.0).await?;
296
- Ok(rmpv::Value::Nil)
297
- }
298
- }
299
-
300
- #[derive(Deserialize)]
301
- #[serde(transparent)]
302
- pub struct UpdateBrushLayerHandler(operations::UpdateBrushLayerPayload);
303
-
304
- #[async_trait]
305
- impl RpcCall for UpdateBrushLayerHandler {
306
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
307
- operations::update_brush_layer(state, self.0).await?;
308
- Ok(rmpv::Value::Nil)
309
- }
310
- }
311
-
312
- #[derive(Deserialize)]
313
- #[serde(transparent)]
314
- pub struct InpaintPartialHandler(operations::InpaintPartialPayload);
315
-
316
- #[async_trait]
317
- impl RpcCall for InpaintPartialHandler {
318
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
319
- operations::inpaint_partial(state, self.0).await?;
320
- Ok(rmpv::Value::Nil)
321
- }
322
- }
323
-
324
- #[derive(Deserialize)]
325
- #[serde(transparent)]
326
- pub struct RenderHandler(operations::RenderPayload);
327
-
328
- #[async_trait]
329
- impl RpcCall for RenderHandler {
330
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
331
- operations::render(state, self.0).await?;
332
- Ok(rmpv::Value::Nil)
333
- }
334
- }
335
-
336
- #[derive(Deserialize)]
337
- #[serde(transparent)]
338
- pub struct UpdateTextBlocksHandler(operations::UpdateTextBlocksPayload);
339
-
340
- #[async_trait]
341
- impl RpcCall for UpdateTextBlocksHandler {
342
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
343
- operations::update_text_blocks(state, self.0).await?;
344
- Ok(rmpv::Value::Nil)
345
- }
346
- }
347
-
348
- #[derive(Deserialize)]
349
- #[serde(transparent)]
350
- pub struct LlmLoadHandler(operations::LlmLoadPayload);
351
-
352
- #[async_trait]
353
- impl RpcCall for LlmLoadHandler {
354
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
355
- operations::llm_load(state, self.0).await?;
356
- Ok(rmpv::Value::Nil)
357
- }
358
- }
359
-
360
- #[derive(Deserialize)]
361
- #[serde(transparent)]
362
- pub struct LlmGenerateHandler(operations::LlmGeneratePayload);
363
-
364
- #[async_trait]
365
- impl RpcCall for LlmGenerateHandler {
366
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
367
- operations::llm_generate(state, self.0).await?;
368
- Ok(rmpv::Value::Nil)
369
- }
370
- }
371
-
372
- #[derive(Deserialize)]
373
- #[serde(transparent)]
374
- pub struct ProcessHandler(crate::pipeline::ProcessRequest);
375
-
376
- #[async_trait]
377
- impl RpcCall for ProcessHandler {
378
- async fn call(self, state: AppResources) -> Result<rmpv::Value> {
379
- operations::process(state, self.0).await?;
380
- Ok(rmpv::Value::Nil)
381
- }
382
- }
383
-
384
- // --- Method Dispatch Enum ---
385
-
386
- #[derive(Deserialize)]
387
- #[serde(tag = "method", content = "params")]
388
- #[enum_dispatch::enum_dispatch(RpcCall)]
389
- pub enum Method {
390
- #[serde(rename = "app_version")]
391
- AppVersion(AppVersionHandler),
392
- #[serde(rename = "device")]
393
- Device(DeviceHandler),
394
- #[serde(rename = "open_external")]
395
- OpenExternal(OpenExternalHandler),
396
- #[serde(rename = "get_documents")]
397
- GetDocuments(GetDocumentsHandler),
398
- #[serde(rename = "get_document")]
399
- GetDocument(GetDocumentHandler),
400
- #[serde(rename = "get_thumbnail")]
401
- GetThumbnail(GetThumbnailHandler),
402
- #[serde(rename = "open_documents")]
403
- OpenDocuments(OpenDocumentsHandler),
404
- #[serde(rename = "save_documents")]
405
- SaveDocuments(SaveDocumentsHandler),
406
- #[serde(rename = "export_document")]
407
- ExportDocument(ExportDocumentHandler),
408
- #[serde(rename = "detect")]
409
- Detect(DetectHandler),
410
- #[serde(rename = "ocr")]
411
- Ocr(OcrHandler),
412
- #[serde(rename = "inpaint")]
413
- Inpaint(InpaintHandler),
414
- #[serde(rename = "update_inpaint_mask")]
415
- UpdateInpaintMask(UpdateInpaintMaskHandler),
416
- #[serde(rename = "update_brush_layer")]
417
- UpdateBrushLayer(UpdateBrushLayerHandler),
418
- #[serde(rename = "inpaint_partial")]
419
- InpaintPartial(InpaintPartialHandler),
420
- #[serde(rename = "render")]
421
- Render(RenderHandler),
422
- #[serde(rename = "update_text_blocks")]
423
- UpdateTextBlocks(UpdateTextBlocksHandler),
424
- #[serde(rename = "list_font_families")]
425
- ListFontFamilies(ListFontFamiliesHandler),
426
- #[serde(rename = "llm_list")]
427
- LlmList(LlmListHandler),
428
- #[serde(rename = "llm_load")]
429
- LlmLoad(LlmLoadHandler),
430
- #[serde(rename = "llm_offload")]
431
- LlmOffload(LlmOffloadHandler),
432
- #[serde(rename = "llm_ready")]
433
- LlmReady(LlmReadyHandler),
434
- #[serde(rename = "llm_generate")]
435
- LlmGenerate(LlmGenerateHandler),
436
- #[serde(rename = "process")]
437
- Process(ProcessHandler),
438
- #[serde(rename = "process_cancel")]
439
- ProcessCancel(ProcessCancelHandler),
440
- }
441
-
442
- /// Parse method name + params into a Method enum using serde adjacently-tagged deserialization.
443
- fn parse_call(method: &str, params: rmpv::Value) -> Result<Method> {
444
- let tagged = rmpv::Value::Map(vec![
445
- (
446
- rmpv::Value::String("method".into()),
447
- rmpv::Value::String(method.into()),
448
- ),
449
- (rmpv::Value::String("params".into()), params),
450
- ]);
451
- from_value(tagged)
452
- }
453
-
454
- // --- WebSocket State ---
455
-
456
- #[derive(Clone)]
457
- pub struct WsState {
458
- pub resources: SharedResources,
459
- }
460
-
461
- // --- WebSocket Handler ---
462
-
463
- pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State<WsState>) -> impl IntoResponse {
464
- ws.max_message_size(1024 * 1024 * 1024)
465
- .on_upgrade(|socket| handle_socket(socket, state))
466
- }
467
-
468
- async fn handle_socket(socket: WebSocket, state: WsState) {
469
- let (mut ws_sender, mut ws_receiver) = socket.split();
470
- let (tx, mut send_rx) = mpsc::channel::<OutgoingMessage>(256);
471
-
472
- // Notification forwarders
473
- spawn_notification_forwarder(
474
- "download_progress",
475
- koharu_core::download::subscribe(),
476
- tx.clone(),
477
- );
478
- spawn_notification_forwarder("process_progress", crate::pipeline::subscribe(), tx.clone());
479
-
480
- // Sender task: drain mpsc → WebSocket binary frames
481
- let send_task = tokio::spawn(async move {
482
- while let Some(msg) = send_rx.recv().await {
483
- let Ok(bytes) = rmp_serde::to_vec_named(&msg) else {
484
- continue;
485
- };
486
- if ws_sender.send(Message::Binary(bytes.into())).await.is_err() {
487
- break;
488
- }
489
- }
490
- });
491
-
492
- // Tower dispatch service (cloned per request)
493
- let dispatch_svc = ServiceBuilder::new()
494
- .layer(TimeoutLayer::new(Duration::from_secs(300)))
495
- .service(service_fn(
496
- |(method, res): (Method, AppResources)| async move { method.call(res).await },
497
- ));
498
-
499
- // Receive loop
500
- while let Some(Ok(msg)) = ws_receiver.next().await {
501
- let data = match msg {
502
- Message::Binary(data) => data,
503
- Message::Close(_) => break,
504
- _ => continue,
505
- };
506
-
507
- let raw: RawIncoming = match rmp_serde::from_slice(&data) {
508
- Ok(r) => r,
509
- Err(e) => {
510
- let _ = tx
511
- .send(err_response(0, &format!("Decode error: {e}")))
512
- .await;
513
- continue;
514
- }
515
- };
516
-
517
- let id = raw.id;
518
- let tx = tx.clone();
519
- let resources = state.resources.clone();
520
- let mut svc = dispatch_svc.clone();
521
-
522
- tokio::spawn(async move {
523
- let response = match resources.get() {
524
- Some(res) => {
525
- let params = raw.params.unwrap_or(rmpv::Value::Nil);
526
- match parse_call(&raw.method, params) {
527
- Ok(method) => match svc.ready().await {
528
- Ok(ready) => match ready.call((method, res.clone())).await {
529
- Ok(result) => ok_response(id, result),
530
- Err(e) => err_response(id, &format!("{e:#}")),
531
- },
532
- Err(e) => err_response(id, &format!("{e}")),
533
- },
534
- Err(e) => err_response(id, &format!("{e:#}")),
535
- }
536
- }
537
- None => err_response(id, "Resources not initialized"),
538
- };
539
- let _ = tx.send(response).await;
540
- });
541
- }
542
-
543
- send_task.abort();
544
- }
545
-
546
- fn spawn_notification_forwarder<T: Serialize + Clone + Send + 'static>(
547
- method: &'static str,
548
- mut rx: broadcast::Receiver<T>,
549
- tx: mpsc::Sender<OutgoingMessage>,
550
- ) {
551
- tokio::spawn(async move {
552
- loop {
553
- match rx.recv().await {
554
- Ok(payload) => {
555
- let params = to_value(&payload).unwrap_or(rmpv::Value::Nil);
556
- let msg = OutgoingMessage::Notification {
557
- method: method.to_string(),
558
- params,
559
- };
560
- if tx.send(msg).await.is_err() {
561
- break;
562
- }
563
- }
564
- Err(broadcast::error::RecvError::Lagged(_)) => continue,
565
- Err(broadcast::error::RecvError::Closed) => break,
566
- }
567
- }
568
- });
569
- }