├── .cnb.yml
├── .cnb
└── web_trigger.yml
├── .dockerignore
├── .github
└── workflows
│ └── docker.yml
├── .gitignore
├── .vscode
└── settings.json
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── Dockerfile.enzh
├── LICENSE
├── README.md
├── README_ZH.md
├── compose.yaml
└── src
├── endpoint.rs
├── main.rs
└── translation.rs
/.cnb.yml:
--------------------------------------------------------------------------------
1 | main:
2 | web_trigger_update: &web_trigger_update
3 | - name: Update Docker images
4 | services:
5 | - docker
6 | stages:
7 | - name: download models
8 | script: |
9 | mkdir -p models-enzh/enzh
10 | curl -sL https://github.com/mozilla/firefox-translations-models/raw/refs/heads/main/registry.json > registry.json
11 | files=(model lex trgvocab srcvocab)
12 | for file_type in "${files[@]}"; do
13 | file_name=$(jq -r ".enzh.$file_type.name" registry.json)
14 | if [ -n "$file_name" ] && [ "$file_name" != "null" ]; then
15 | echo "Downloading $file_name for enzh"
16 | download_url="https://github.com/mozilla/firefox-translations-models/raw/refs/heads/main/models/prod/enzh/${file_name}.gz"
17 | curl -L -sL $download_url -o "models-enzh/enzh/${file_name}.gz"
18 | gunzip -f "models-enzh/enzh/${file_name}.gz"
19 | echo "$file_name downloaded and extracted to models-enzh/enzh/$file_name"
20 | else
21 | echo "Failed to find $file_type for enzh in registry.json"
22 | fi
23 | done
24 | echo "Download completed. Model structure:"
25 | pwd
26 | ls -R models-enzh
27 | - name: docker login
28 | script: docker login -u ${CNB_TOKEN_USER_NAME} -p "${CNB_TOKEN}" ${CNB_DOCKER_REGISTRY}
29 | - name: docker build
30 | script: docker build -t ${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}:latest -f Dockerfile.enzh .
31 | - name: docker push
32 | script: docker push ${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}:latest
33 |
--------------------------------------------------------------------------------
/.cnb/web_trigger.yml:
--------------------------------------------------------------------------------
1 | branch:
2 | - buttons:
3 | - name: Update
4 | description: Update Docker Images
5 | event: web_trigger_update
6 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | /target
2 | /models
3 | Dockerfile
4 | compose.yaml
5 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Create and publish a Docker image
2 |
3 | on:
4 | push:
5 | workflow_dispatch:
6 |
7 | env:
8 | REGISTRY: ghcr.io
9 | IMAGE_NAME: ${{ github.repository }}
10 |
11 | jobs:
12 | build-and-push-image:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: read
16 | packages: write
17 | attestations: write
18 | id-token: write
19 | steps:
20 | - name: Checkout repository
21 | uses: actions/checkout@v4
22 | - name: Log in to the Container registry
23 | uses: docker/login-action@v3
24 | with:
25 | registry: ${{ env.REGISTRY }}
26 | username: ${{ github.actor }}
27 | password: ${{ secrets.GITHUB_TOKEN }}
28 | - name: Extract metadata (tags, labels) for Docker
29 | id: meta
30 | uses: docker/metadata-action@v5
31 | with:
32 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
33 | - name: Build and push Docker image
34 | id: push
35 | uses: docker/build-push-action@v6
36 | with:
37 | context: .
38 | push: true
39 | tags: ${{ steps.meta.outputs.tags }}
40 | labels: ${{ steps.meta.outputs.labels }}
41 |
42 | - name: Generate artifact attestation
43 | uses: actions/attest-build-provenance@v2
44 | with:
45 | subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
46 | subject-digest: ${{ steps.push.outputs.digest }}
47 | push-to-registry: true
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /models
3 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "yaml.schemas": {
3 | "https://docs.cnb.cool/conf-schema-zh.json": ".cnb.yml"
4 | }
5 | }
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.24.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler2"
16 | version = "2.0.0"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
19 |
20 | [[package]]
21 | name = "aho-corasick"
22 | version = "1.1.3"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
25 | dependencies = [
26 | "memchr",
27 | ]
28 |
29 | [[package]]
30 | name = "anyhow"
31 | version = "1.0.98"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
34 |
35 | [[package]]
36 | name = "autocfg"
37 | version = "1.4.0"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
40 |
41 | [[package]]
42 | name = "axum"
43 | version = "0.8.3"
44 | source = "registry+https://github.com/rust-lang/crates.io-index"
45 | checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
46 | dependencies = [
47 | "axum-core",
48 | "bytes",
49 | "form_urlencoded",
50 | "futures-util",
51 | "http",
52 | "http-body",
53 | "http-body-util",
54 | "hyper",
55 | "hyper-util",
56 | "itoa",
57 | "matchit",
58 | "memchr",
59 | "mime",
60 | "percent-encoding",
61 | "pin-project-lite",
62 | "rustversion",
63 | "serde",
64 | "serde_json",
65 | "serde_path_to_error",
66 | "serde_urlencoded",
67 | "sync_wrapper",
68 | "tokio",
69 | "tower",
70 | "tower-layer",
71 | "tower-service",
72 | "tracing",
73 | ]
74 |
75 | [[package]]
76 | name = "axum-core"
77 | version = "0.5.2"
78 | source = "registry+https://github.com/rust-lang/crates.io-index"
79 | checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
80 | dependencies = [
81 | "bytes",
82 | "futures-core",
83 | "http",
84 | "http-body",
85 | "http-body-util",
86 | "mime",
87 | "pin-project-lite",
88 | "rustversion",
89 | "sync_wrapper",
90 | "tower-layer",
91 | "tower-service",
92 | "tracing",
93 | ]
94 |
95 | [[package]]
96 | name = "backtrace"
97 | version = "0.3.74"
98 | source = "registry+https://github.com/rust-lang/crates.io-index"
99 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
100 | dependencies = [
101 | "addr2line",
102 | "cfg-if",
103 | "libc",
104 | "miniz_oxide",
105 | "object",
106 | "rustc-demangle",
107 | "windows-targets",
108 | ]
109 |
110 | [[package]]
111 | name = "bitflags"
112 | version = "2.9.0"
113 | source = "registry+https://github.com/rust-lang/crates.io-index"
114 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
115 |
116 | [[package]]
117 | name = "bytes"
118 | version = "1.10.1"
119 | source = "registry+https://github.com/rust-lang/crates.io-index"
120 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
121 |
122 | [[package]]
123 | name = "cc"
124 | version = "1.2.19"
125 | source = "registry+https://github.com/rust-lang/crates.io-index"
126 | checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
127 | dependencies = [
128 | "shlex",
129 | ]
130 |
131 | [[package]]
132 | name = "cfg-if"
133 | version = "1.0.0"
134 | source = "registry+https://github.com/rust-lang/crates.io-index"
135 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
136 |
137 | [[package]]
138 | name = "core-foundation"
139 | version = "0.9.4"
140 | source = "registry+https://github.com/rust-lang/crates.io-index"
141 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
142 | dependencies = [
143 | "core-foundation-sys",
144 | "libc",
145 | ]
146 |
147 | [[package]]
148 | name = "core-foundation-sys"
149 | version = "0.8.7"
150 | source = "registry+https://github.com/rust-lang/crates.io-index"
151 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
152 |
153 | [[package]]
154 | name = "errno"
155 | version = "0.3.11"
156 | source = "registry+https://github.com/rust-lang/crates.io-index"
157 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
158 | dependencies = [
159 | "libc",
160 | "windows-sys 0.59.0",
161 | ]
162 |
163 | [[package]]
164 | name = "fastrand"
165 | version = "2.3.0"
166 | source = "registry+https://github.com/rust-lang/crates.io-index"
167 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
168 |
169 | [[package]]
170 | name = "fnv"
171 | version = "1.0.7"
172 | source = "registry+https://github.com/rust-lang/crates.io-index"
173 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
174 |
175 | [[package]]
176 | name = "foreign-types"
177 | version = "0.3.2"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
180 | dependencies = [
181 | "foreign-types-shared",
182 | ]
183 |
184 | [[package]]
185 | name = "foreign-types-shared"
186 | version = "0.1.1"
187 | source = "registry+https://github.com/rust-lang/crates.io-index"
188 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
189 |
190 | [[package]]
191 | name = "form_urlencoded"
192 | version = "1.2.1"
193 | source = "registry+https://github.com/rust-lang/crates.io-index"
194 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
195 | dependencies = [
196 | "percent-encoding",
197 | ]
198 |
199 | [[package]]
200 | name = "futures-channel"
201 | version = "0.3.31"
202 | source = "registry+https://github.com/rust-lang/crates.io-index"
203 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
204 | dependencies = [
205 | "futures-core",
206 | ]
207 |
208 | [[package]]
209 | name = "futures-core"
210 | version = "0.3.31"
211 | source = "registry+https://github.com/rust-lang/crates.io-index"
212 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
213 |
214 | [[package]]
215 | name = "futures-task"
216 | version = "0.3.31"
217 | source = "registry+https://github.com/rust-lang/crates.io-index"
218 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
219 |
220 | [[package]]
221 | name = "futures-util"
222 | version = "0.3.31"
223 | source = "registry+https://github.com/rust-lang/crates.io-index"
224 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
225 | dependencies = [
226 | "futures-core",
227 | "futures-task",
228 | "pin-project-lite",
229 | "pin-utils",
230 | ]
231 |
232 | [[package]]
233 | name = "getrandom"
234 | version = "0.3.2"
235 | source = "registry+https://github.com/rust-lang/crates.io-index"
236 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
237 | dependencies = [
238 | "cfg-if",
239 | "libc",
240 | "r-efi",
241 | "wasi 0.14.2+wasi-0.2.4",
242 | ]
243 |
244 | [[package]]
245 | name = "gimli"
246 | version = "0.31.1"
247 | source = "registry+https://github.com/rust-lang/crates.io-index"
248 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
249 |
250 | [[package]]
251 | name = "http"
252 | version = "1.3.1"
253 | source = "registry+https://github.com/rust-lang/crates.io-index"
254 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
255 | dependencies = [
256 | "bytes",
257 | "fnv",
258 | "itoa",
259 | ]
260 |
261 | [[package]]
262 | name = "http-body"
263 | version = "1.0.1"
264 | source = "registry+https://github.com/rust-lang/crates.io-index"
265 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
266 | dependencies = [
267 | "bytes",
268 | "http",
269 | ]
270 |
271 | [[package]]
272 | name = "http-body-util"
273 | version = "0.1.3"
274 | source = "registry+https://github.com/rust-lang/crates.io-index"
275 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
276 | dependencies = [
277 | "bytes",
278 | "futures-core",
279 | "http",
280 | "http-body",
281 | "pin-project-lite",
282 | ]
283 |
284 | [[package]]
285 | name = "httparse"
286 | version = "1.10.1"
287 | source = "registry+https://github.com/rust-lang/crates.io-index"
288 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
289 |
290 | [[package]]
291 | name = "httpdate"
292 | version = "1.0.3"
293 | source = "registry+https://github.com/rust-lang/crates.io-index"
294 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
295 |
296 | [[package]]
297 | name = "hyper"
298 | version = "1.6.0"
299 | source = "registry+https://github.com/rust-lang/crates.io-index"
300 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
301 | dependencies = [
302 | "bytes",
303 | "futures-channel",
304 | "futures-util",
305 | "http",
306 | "http-body",
307 | "httparse",
308 | "httpdate",
309 | "itoa",
310 | "pin-project-lite",
311 | "smallvec",
312 | "tokio",
313 | ]
314 |
315 | [[package]]
316 | name = "hyper-util"
317 | version = "0.1.11"
318 | source = "registry+https://github.com/rust-lang/crates.io-index"
319 | checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
320 | dependencies = [
321 | "bytes",
322 | "futures-util",
323 | "http",
324 | "http-body",
325 | "hyper",
326 | "pin-project-lite",
327 | "tokio",
328 | "tower-service",
329 | ]
330 |
331 | [[package]]
332 | name = "intel-mkl-tool"
333 | version = "0.8.1"
334 | source = "registry+https://github.com/rust-lang/crates.io-index"
335 | checksum = "887a16b4537d82227af54d3372971cfa5e0cde53322e60f57584056c16ada1b4"
336 | dependencies = [
337 | "anyhow",
338 | "log",
339 | "walkdir",
340 | ]
341 |
342 | [[package]]
343 | name = "isolang"
344 | version = "2.4.0"
345 | source = "registry+https://github.com/rust-lang/crates.io-index"
346 | checksum = "fe50d48c77760c55188549098b9a7f6e37ae980c586a24693d6b01c3b2010c3c"
347 | dependencies = [
348 | "phf",
349 | ]
350 |
351 | [[package]]
352 | name = "itoa"
353 | version = "1.0.15"
354 | source = "registry+https://github.com/rust-lang/crates.io-index"
355 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
356 |
357 | [[package]]
358 | name = "lazy_static"
359 | version = "1.5.0"
360 | source = "registry+https://github.com/rust-lang/crates.io-index"
361 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
362 |
363 | [[package]]
364 | name = "libc"
365 | version = "0.2.172"
366 | source = "registry+https://github.com/rust-lang/crates.io-index"
367 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
368 |
369 | [[package]]
370 | name = "linguaspark"
371 | version = "0.1.0"
372 | source = "git+https://github.com/LinguaSpark/core.git?branch=main#6a2bc711cce73bc053675c0293dd37c8029e62dd"
373 | dependencies = [
374 | "intel-mkl-tool",
375 | "minreq",
376 | "thiserror",
377 | ]
378 |
379 | [[package]]
380 | name = "linguaspark-server"
381 | version = "0.1.0"
382 | dependencies = [
383 | "anyhow",
384 | "axum",
385 | "isolang",
386 | "linguaspark",
387 | "serde",
388 | "serde_json",
389 | "thiserror",
390 | "tokio",
391 | "tower-http",
392 | "tracing",
393 | "tracing-subscriber",
394 | "whichlang",
395 | ]
396 |
397 | [[package]]
398 | name = "linux-raw-sys"
399 | version = "0.9.4"
400 | source = "registry+https://github.com/rust-lang/crates.io-index"
401 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
402 |
403 | [[package]]
404 | name = "lock_api"
405 | version = "0.4.12"
406 | source = "registry+https://github.com/rust-lang/crates.io-index"
407 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
408 | dependencies = [
409 | "autocfg",
410 | "scopeguard",
411 | ]
412 |
413 | [[package]]
414 | name = "log"
415 | version = "0.4.27"
416 | source = "registry+https://github.com/rust-lang/crates.io-index"
417 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
418 |
419 | [[package]]
420 | name = "matchers"
421 | version = "0.1.0"
422 | source = "registry+https://github.com/rust-lang/crates.io-index"
423 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
424 | dependencies = [
425 | "regex-automata 0.1.10",
426 | ]
427 |
428 | [[package]]
429 | name = "matchit"
430 | version = "0.8.4"
431 | source = "registry+https://github.com/rust-lang/crates.io-index"
432 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
433 |
434 | [[package]]
435 | name = "memchr"
436 | version = "2.7.4"
437 | source = "registry+https://github.com/rust-lang/crates.io-index"
438 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
439 |
440 | [[package]]
441 | name = "mime"
442 | version = "0.3.17"
443 | source = "registry+https://github.com/rust-lang/crates.io-index"
444 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
445 |
446 | [[package]]
447 | name = "miniz_oxide"
448 | version = "0.8.8"
449 | source = "registry+https://github.com/rust-lang/crates.io-index"
450 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
451 | dependencies = [
452 | "adler2",
453 | ]
454 |
455 | [[package]]
456 | name = "minreq"
457 | version = "2.13.4"
458 | source = "registry+https://github.com/rust-lang/crates.io-index"
459 | checksum = "f0d2aaba477837b46ec1289588180fabfccf0c3b1d1a0c6b1866240cd6cd5ce9"
460 | dependencies = [
461 | "log",
462 | "native-tls",
463 | ]
464 |
465 | [[package]]
466 | name = "mio"
467 | version = "1.0.3"
468 | source = "registry+https://github.com/rust-lang/crates.io-index"
469 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
470 | dependencies = [
471 | "libc",
472 | "wasi 0.11.0+wasi-snapshot-preview1",
473 | "windows-sys 0.52.0",
474 | ]
475 |
476 | [[package]]
477 | name = "native-tls"
478 | version = "0.2.14"
479 | source = "registry+https://github.com/rust-lang/crates.io-index"
480 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
481 | dependencies = [
482 | "libc",
483 | "log",
484 | "openssl",
485 | "openssl-probe",
486 | "openssl-sys",
487 | "schannel",
488 | "security-framework",
489 | "security-framework-sys",
490 | "tempfile",
491 | ]
492 |
493 | [[package]]
494 | name = "nu-ansi-term"
495 | version = "0.46.0"
496 | source = "registry+https://github.com/rust-lang/crates.io-index"
497 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
498 | dependencies = [
499 | "overload",
500 | "winapi",
501 | ]
502 |
503 | [[package]]
504 | name = "object"
505 | version = "0.36.7"
506 | source = "registry+https://github.com/rust-lang/crates.io-index"
507 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
508 | dependencies = [
509 | "memchr",
510 | ]
511 |
512 | [[package]]
513 | name = "once_cell"
514 | version = "1.21.3"
515 | source = "registry+https://github.com/rust-lang/crates.io-index"
516 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
517 |
518 | [[package]]
519 | name = "openssl"
520 | version = "0.10.72"
521 | source = "registry+https://github.com/rust-lang/crates.io-index"
522 | checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
523 | dependencies = [
524 | "bitflags",
525 | "cfg-if",
526 | "foreign-types",
527 | "libc",
528 | "once_cell",
529 | "openssl-macros",
530 | "openssl-sys",
531 | ]
532 |
533 | [[package]]
534 | name = "openssl-macros"
535 | version = "0.1.1"
536 | source = "registry+https://github.com/rust-lang/crates.io-index"
537 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
538 | dependencies = [
539 | "proc-macro2",
540 | "quote",
541 | "syn",
542 | ]
543 |
544 | [[package]]
545 | name = "openssl-probe"
546 | version = "0.1.6"
547 | source = "registry+https://github.com/rust-lang/crates.io-index"
548 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
549 |
550 | [[package]]
551 | name = "openssl-sys"
552 | version = "0.9.107"
553 | source = "registry+https://github.com/rust-lang/crates.io-index"
554 | checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
555 | dependencies = [
556 | "cc",
557 | "libc",
558 | "pkg-config",
559 | "vcpkg",
560 | ]
561 |
562 | [[package]]
563 | name = "overload"
564 | version = "0.1.1"
565 | source = "registry+https://github.com/rust-lang/crates.io-index"
566 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
567 |
568 | [[package]]
569 | name = "parking_lot"
570 | version = "0.12.3"
571 | source = "registry+https://github.com/rust-lang/crates.io-index"
572 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
573 | dependencies = [
574 | "lock_api",
575 | "parking_lot_core",
576 | ]
577 |
578 | [[package]]
579 | name = "parking_lot_core"
580 | version = "0.9.10"
581 | source = "registry+https://github.com/rust-lang/crates.io-index"
582 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
583 | dependencies = [
584 | "cfg-if",
585 | "libc",
586 | "redox_syscall",
587 | "smallvec",
588 | "windows-targets",
589 | ]
590 |
591 | [[package]]
592 | name = "percent-encoding"
593 | version = "2.3.1"
594 | source = "registry+https://github.com/rust-lang/crates.io-index"
595 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
596 |
597 | [[package]]
598 | name = "phf"
599 | version = "0.11.3"
600 | source = "registry+https://github.com/rust-lang/crates.io-index"
601 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
602 | dependencies = [
603 | "phf_shared",
604 | ]
605 |
606 | [[package]]
607 | name = "phf_shared"
608 | version = "0.11.3"
609 | source = "registry+https://github.com/rust-lang/crates.io-index"
610 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
611 | dependencies = [
612 | "siphasher",
613 | ]
614 |
615 | [[package]]
616 | name = "pin-project-lite"
617 | version = "0.2.16"
618 | source = "registry+https://github.com/rust-lang/crates.io-index"
619 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
620 |
621 | [[package]]
622 | name = "pin-utils"
623 | version = "0.1.0"
624 | source = "registry+https://github.com/rust-lang/crates.io-index"
625 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
626 |
627 | [[package]]
628 | name = "pkg-config"
629 | version = "0.3.32"
630 | source = "registry+https://github.com/rust-lang/crates.io-index"
631 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
632 |
633 | [[package]]
634 | name = "proc-macro2"
635 | version = "1.0.95"
636 | source = "registry+https://github.com/rust-lang/crates.io-index"
637 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
638 | dependencies = [
639 | "unicode-ident",
640 | ]
641 |
642 | [[package]]
643 | name = "quote"
644 | version = "1.0.40"
645 | source = "registry+https://github.com/rust-lang/crates.io-index"
646 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
647 | dependencies = [
648 | "proc-macro2",
649 | ]
650 |
651 | [[package]]
652 | name = "r-efi"
653 | version = "5.2.0"
654 | source = "registry+https://github.com/rust-lang/crates.io-index"
655 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
656 |
657 | [[package]]
658 | name = "redox_syscall"
659 | version = "0.5.11"
660 | source = "registry+https://github.com/rust-lang/crates.io-index"
661 | checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
662 | dependencies = [
663 | "bitflags",
664 | ]
665 |
666 | [[package]]
667 | name = "regex"
668 | version = "1.11.1"
669 | source = "registry+https://github.com/rust-lang/crates.io-index"
670 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
671 | dependencies = [
672 | "aho-corasick",
673 | "memchr",
674 | "regex-automata 0.4.9",
675 | "regex-syntax 0.8.5",
676 | ]
677 |
678 | [[package]]
679 | name = "regex-automata"
680 | version = "0.1.10"
681 | source = "registry+https://github.com/rust-lang/crates.io-index"
682 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
683 | dependencies = [
684 | "regex-syntax 0.6.29",
685 | ]
686 |
687 | [[package]]
688 | name = "regex-automata"
689 | version = "0.4.9"
690 | source = "registry+https://github.com/rust-lang/crates.io-index"
691 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
692 | dependencies = [
693 | "aho-corasick",
694 | "memchr",
695 | "regex-syntax 0.8.5",
696 | ]
697 |
698 | [[package]]
699 | name = "regex-syntax"
700 | version = "0.6.29"
701 | source = "registry+https://github.com/rust-lang/crates.io-index"
702 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
703 |
704 | [[package]]
705 | name = "regex-syntax"
706 | version = "0.8.5"
707 | source = "registry+https://github.com/rust-lang/crates.io-index"
708 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
709 |
710 | [[package]]
711 | name = "rustc-demangle"
712 | version = "0.1.24"
713 | source = "registry+https://github.com/rust-lang/crates.io-index"
714 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
715 |
716 | [[package]]
717 | name = "rustix"
718 | version = "1.0.5"
719 | source = "registry+https://github.com/rust-lang/crates.io-index"
720 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
721 | dependencies = [
722 | "bitflags",
723 | "errno",
724 | "libc",
725 | "linux-raw-sys",
726 | "windows-sys 0.59.0",
727 | ]
728 |
729 | [[package]]
730 | name = "rustversion"
731 | version = "1.0.20"
732 | source = "registry+https://github.com/rust-lang/crates.io-index"
733 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
734 |
735 | [[package]]
736 | name = "ryu"
737 | version = "1.0.20"
738 | source = "registry+https://github.com/rust-lang/crates.io-index"
739 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
740 |
741 | [[package]]
742 | name = "same-file"
743 | version = "1.0.6"
744 | source = "registry+https://github.com/rust-lang/crates.io-index"
745 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
746 | dependencies = [
747 | "winapi-util",
748 | ]
749 |
750 | [[package]]
751 | name = "schannel"
752 | version = "0.1.27"
753 | source = "registry+https://github.com/rust-lang/crates.io-index"
754 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
755 | dependencies = [
756 | "windows-sys 0.59.0",
757 | ]
758 |
759 | [[package]]
760 | name = "scopeguard"
761 | version = "1.2.0"
762 | source = "registry+https://github.com/rust-lang/crates.io-index"
763 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
764 |
765 | [[package]]
766 | name = "security-framework"
767 | version = "2.11.1"
768 | source = "registry+https://github.com/rust-lang/crates.io-index"
769 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
770 | dependencies = [
771 | "bitflags",
772 | "core-foundation",
773 | "core-foundation-sys",
774 | "libc",
775 | "security-framework-sys",
776 | ]
777 |
778 | [[package]]
779 | name = "security-framework-sys"
780 | version = "2.14.0"
781 | source = "registry+https://github.com/rust-lang/crates.io-index"
782 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
783 | dependencies = [
784 | "core-foundation-sys",
785 | "libc",
786 | ]
787 |
788 | [[package]]
789 | name = "serde"
790 | version = "1.0.219"
791 | source = "registry+https://github.com/rust-lang/crates.io-index"
792 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
793 | dependencies = [
794 | "serde_derive",
795 | ]
796 |
797 | [[package]]
798 | name = "serde_derive"
799 | version = "1.0.219"
800 | source = "registry+https://github.com/rust-lang/crates.io-index"
801 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
802 | dependencies = [
803 | "proc-macro2",
804 | "quote",
805 | "syn",
806 | ]
807 |
808 | [[package]]
809 | name = "serde_json"
810 | version = "1.0.140"
811 | source = "registry+https://github.com/rust-lang/crates.io-index"
812 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
813 | dependencies = [
814 | "itoa",
815 | "memchr",
816 | "ryu",
817 | "serde",
818 | ]
819 |
820 | [[package]]
821 | name = "serde_path_to_error"
822 | version = "0.1.17"
823 | source = "registry+https://github.com/rust-lang/crates.io-index"
824 | checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
825 | dependencies = [
826 | "itoa",
827 | "serde",
828 | ]
829 |
830 | [[package]]
831 | name = "serde_urlencoded"
832 | version = "0.7.1"
833 | source = "registry+https://github.com/rust-lang/crates.io-index"
834 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
835 | dependencies = [
836 | "form_urlencoded",
837 | "itoa",
838 | "ryu",
839 | "serde",
840 | ]
841 |
842 | [[package]]
843 | name = "sharded-slab"
844 | version = "0.1.7"
845 | source = "registry+https://github.com/rust-lang/crates.io-index"
846 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
847 | dependencies = [
848 | "lazy_static",
849 | ]
850 |
851 | [[package]]
852 | name = "shlex"
853 | version = "1.3.0"
854 | source = "registry+https://github.com/rust-lang/crates.io-index"
855 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
856 |
857 | [[package]]
858 | name = "signal-hook-registry"
859 | version = "1.4.5"
860 | source = "registry+https://github.com/rust-lang/crates.io-index"
861 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
862 | dependencies = [
863 | "libc",
864 | ]
865 |
866 | [[package]]
867 | name = "siphasher"
868 | version = "1.0.1"
869 | source = "registry+https://github.com/rust-lang/crates.io-index"
870 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
871 |
872 | [[package]]
873 | name = "smallvec"
874 | version = "1.15.0"
875 | source = "registry+https://github.com/rust-lang/crates.io-index"
876 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
877 |
878 | [[package]]
879 | name = "socket2"
880 | version = "0.5.9"
881 | source = "registry+https://github.com/rust-lang/crates.io-index"
882 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
883 | dependencies = [
884 | "libc",
885 | "windows-sys 0.52.0",
886 | ]
887 |
888 | [[package]]
889 | name = "syn"
890 | version = "2.0.100"
891 | source = "registry+https://github.com/rust-lang/crates.io-index"
892 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
893 | dependencies = [
894 | "proc-macro2",
895 | "quote",
896 | "unicode-ident",
897 | ]
898 |
899 | [[package]]
900 | name = "sync_wrapper"
901 | version = "1.0.2"
902 | source = "registry+https://github.com/rust-lang/crates.io-index"
903 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
904 |
905 | [[package]]
906 | name = "tempfile"
907 | version = "3.19.1"
908 | source = "registry+https://github.com/rust-lang/crates.io-index"
909 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
910 | dependencies = [
911 | "fastrand",
912 | "getrandom",
913 | "once_cell",
914 | "rustix",
915 | "windows-sys 0.59.0",
916 | ]
917 |
918 | [[package]]
919 | name = "thiserror"
920 | version = "2.0.12"
921 | source = "registry+https://github.com/rust-lang/crates.io-index"
922 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
923 | dependencies = [
924 | "thiserror-impl",
925 | ]
926 |
927 | [[package]]
928 | name = "thiserror-impl"
929 | version = "2.0.12"
930 | source = "registry+https://github.com/rust-lang/crates.io-index"
931 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
932 | dependencies = [
933 | "proc-macro2",
934 | "quote",
935 | "syn",
936 | ]
937 |
938 | [[package]]
939 | name = "thread_local"
940 | version = "1.1.8"
941 | source = "registry+https://github.com/rust-lang/crates.io-index"
942 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
943 | dependencies = [
944 | "cfg-if",
945 | "once_cell",
946 | ]
947 |
948 | [[package]]
949 | name = "tokio"
950 | version = "1.44.2"
951 | source = "registry+https://github.com/rust-lang/crates.io-index"
952 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
953 | dependencies = [
954 | "backtrace",
955 | "bytes",
956 | "libc",
957 | "mio",
958 | "parking_lot",
959 | "pin-project-lite",
960 | "signal-hook-registry",
961 | "socket2",
962 | "tokio-macros",
963 | "windows-sys 0.52.0",
964 | ]
965 |
966 | [[package]]
967 | name = "tokio-macros"
968 | version = "2.5.0"
969 | source = "registry+https://github.com/rust-lang/crates.io-index"
970 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
971 | dependencies = [
972 | "proc-macro2",
973 | "quote",
974 | "syn",
975 | ]
976 |
977 | [[package]]
978 | name = "tower"
979 | version = "0.5.2"
980 | source = "registry+https://github.com/rust-lang/crates.io-index"
981 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
982 | dependencies = [
983 | "futures-core",
984 | "futures-util",
985 | "pin-project-lite",
986 | "sync_wrapper",
987 | "tokio",
988 | "tower-layer",
989 | "tower-service",
990 | "tracing",
991 | ]
992 |
993 | [[package]]
994 | name = "tower-http"
995 | version = "0.6.2"
996 | source = "registry+https://github.com/rust-lang/crates.io-index"
997 | checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
998 | dependencies = [
999 | "bitflags",
1000 | "bytes",
1001 | "http",
1002 | "http-body",
1003 | "pin-project-lite",
1004 | "tower-layer",
1005 | "tower-service",
1006 | "tracing",
1007 | ]
1008 |
1009 | [[package]]
1010 | name = "tower-layer"
1011 | version = "0.3.3"
1012 | source = "registry+https://github.com/rust-lang/crates.io-index"
1013 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
1014 |
1015 | [[package]]
1016 | name = "tower-service"
1017 | version = "0.3.3"
1018 | source = "registry+https://github.com/rust-lang/crates.io-index"
1019 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
1020 |
1021 | [[package]]
1022 | name = "tracing"
1023 | version = "0.1.41"
1024 | source = "registry+https://github.com/rust-lang/crates.io-index"
1025 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
1026 | dependencies = [
1027 | "log",
1028 | "pin-project-lite",
1029 | "tracing-attributes",
1030 | "tracing-core",
1031 | ]
1032 |
1033 | [[package]]
1034 | name = "tracing-attributes"
1035 | version = "0.1.28"
1036 | source = "registry+https://github.com/rust-lang/crates.io-index"
1037 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
1038 | dependencies = [
1039 | "proc-macro2",
1040 | "quote",
1041 | "syn",
1042 | ]
1043 |
1044 | [[package]]
1045 | name = "tracing-core"
1046 | version = "0.1.33"
1047 | source = "registry+https://github.com/rust-lang/crates.io-index"
1048 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
1049 | dependencies = [
1050 | "once_cell",
1051 | "valuable",
1052 | ]
1053 |
1054 | [[package]]
1055 | name = "tracing-log"
1056 | version = "0.2.0"
1057 | source = "registry+https://github.com/rust-lang/crates.io-index"
1058 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
1059 | dependencies = [
1060 | "log",
1061 | "once_cell",
1062 | "tracing-core",
1063 | ]
1064 |
1065 | [[package]]
1066 | name = "tracing-subscriber"
1067 | version = "0.3.19"
1068 | source = "registry+https://github.com/rust-lang/crates.io-index"
1069 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
1070 | dependencies = [
1071 | "matchers",
1072 | "nu-ansi-term",
1073 | "once_cell",
1074 | "regex",
1075 | "sharded-slab",
1076 | "smallvec",
1077 | "thread_local",
1078 | "tracing",
1079 | "tracing-core",
1080 | "tracing-log",
1081 | ]
1082 |
1083 | [[package]]
1084 | name = "unicode-ident"
1085 | version = "1.0.18"
1086 | source = "registry+https://github.com/rust-lang/crates.io-index"
1087 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
1088 |
1089 | [[package]]
1090 | name = "valuable"
1091 | version = "0.1.1"
1092 | source = "registry+https://github.com/rust-lang/crates.io-index"
1093 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
1094 |
1095 | [[package]]
1096 | name = "vcpkg"
1097 | version = "0.2.15"
1098 | source = "registry+https://github.com/rust-lang/crates.io-index"
1099 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
1100 |
1101 | [[package]]
1102 | name = "walkdir"
1103 | version = "2.5.0"
1104 | source = "registry+https://github.com/rust-lang/crates.io-index"
1105 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
1106 | dependencies = [
1107 | "same-file",
1108 | "winapi-util",
1109 | ]
1110 |
1111 | [[package]]
1112 | name = "wasi"
1113 | version = "0.11.0+wasi-snapshot-preview1"
1114 | source = "registry+https://github.com/rust-lang/crates.io-index"
1115 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1116 |
1117 | [[package]]
1118 | name = "wasi"
1119 | version = "0.14.2+wasi-0.2.4"
1120 | source = "registry+https://github.com/rust-lang/crates.io-index"
1121 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
1122 | dependencies = [
1123 | "wit-bindgen-rt",
1124 | ]
1125 |
1126 | [[package]]
1127 | name = "whichlang"
1128 | version = "0.1.1"
1129 | source = "registry+https://github.com/rust-lang/crates.io-index"
1130 | checksum = "0b9aa3ad29c3d08283ac6b769e3ec15ad1ddb88af7d2e9bc402c574973b937e7"
1131 |
1132 | [[package]]
1133 | name = "winapi"
1134 | version = "0.3.9"
1135 | source = "registry+https://github.com/rust-lang/crates.io-index"
1136 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1137 | dependencies = [
1138 | "winapi-i686-pc-windows-gnu",
1139 | "winapi-x86_64-pc-windows-gnu",
1140 | ]
1141 |
1142 | [[package]]
1143 | name = "winapi-i686-pc-windows-gnu"
1144 | version = "0.4.0"
1145 | source = "registry+https://github.com/rust-lang/crates.io-index"
1146 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1147 |
1148 | [[package]]
1149 | name = "winapi-util"
1150 | version = "0.1.9"
1151 | source = "registry+https://github.com/rust-lang/crates.io-index"
1152 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
1153 | dependencies = [
1154 | "windows-sys 0.59.0",
1155 | ]
1156 |
1157 | [[package]]
1158 | name = "winapi-x86_64-pc-windows-gnu"
1159 | version = "0.4.0"
1160 | source = "registry+https://github.com/rust-lang/crates.io-index"
1161 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1162 |
1163 | [[package]]
1164 | name = "windows-sys"
1165 | version = "0.52.0"
1166 | source = "registry+https://github.com/rust-lang/crates.io-index"
1167 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
1168 | dependencies = [
1169 | "windows-targets",
1170 | ]
1171 |
1172 | [[package]]
1173 | name = "windows-sys"
1174 | version = "0.59.0"
1175 | source = "registry+https://github.com/rust-lang/crates.io-index"
1176 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
1177 | dependencies = [
1178 | "windows-targets",
1179 | ]
1180 |
1181 | [[package]]
1182 | name = "windows-targets"
1183 | version = "0.52.6"
1184 | source = "registry+https://github.com/rust-lang/crates.io-index"
1185 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1186 | dependencies = [
1187 | "windows_aarch64_gnullvm",
1188 | "windows_aarch64_msvc",
1189 | "windows_i686_gnu",
1190 | "windows_i686_gnullvm",
1191 | "windows_i686_msvc",
1192 | "windows_x86_64_gnu",
1193 | "windows_x86_64_gnullvm",
1194 | "windows_x86_64_msvc",
1195 | ]
1196 |
1197 | [[package]]
1198 | name = "windows_aarch64_gnullvm"
1199 | version = "0.52.6"
1200 | source = "registry+https://github.com/rust-lang/crates.io-index"
1201 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1202 |
1203 | [[package]]
1204 | name = "windows_aarch64_msvc"
1205 | version = "0.52.6"
1206 | source = "registry+https://github.com/rust-lang/crates.io-index"
1207 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1208 |
1209 | [[package]]
1210 | name = "windows_i686_gnu"
1211 | version = "0.52.6"
1212 | source = "registry+https://github.com/rust-lang/crates.io-index"
1213 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1214 |
1215 | [[package]]
1216 | name = "windows_i686_gnullvm"
1217 | version = "0.52.6"
1218 | source = "registry+https://github.com/rust-lang/crates.io-index"
1219 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1220 |
1221 | [[package]]
1222 | name = "windows_i686_msvc"
1223 | version = "0.52.6"
1224 | source = "registry+https://github.com/rust-lang/crates.io-index"
1225 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1226 |
1227 | [[package]]
1228 | name = "windows_x86_64_gnu"
1229 | version = "0.52.6"
1230 | source = "registry+https://github.com/rust-lang/crates.io-index"
1231 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1232 |
1233 | [[package]]
1234 | name = "windows_x86_64_gnullvm"
1235 | version = "0.52.6"
1236 | source = "registry+https://github.com/rust-lang/crates.io-index"
1237 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1238 |
1239 | [[package]]
1240 | name = "windows_x86_64_msvc"
1241 | version = "0.52.6"
1242 | source = "registry+https://github.com/rust-lang/crates.io-index"
1243 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1244 |
1245 | [[package]]
1246 | name = "wit-bindgen-rt"
1247 | version = "0.39.0"
1248 | source = "registry+https://github.com/rust-lang/crates.io-index"
1249 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
1250 | dependencies = [
1251 | "bitflags",
1252 | ]
1253 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "linguaspark-server"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | anyhow = "1"
8 | axum = "0.8"
9 | isolang = "2"
10 | serde = { version = "1", features = ["derive"] }
11 | serde_json = "1"
12 | thiserror = "2"
13 | tokio = { version = "1", features = ["full"] }
14 | tower-http = { version = "0.6", features = ["cors", "trace"] }
15 | tracing = "0.1"
16 | tracing-subscriber = { version = "0.3", features = ["env-filter"] }
17 | whichlang = "0.1"
18 |
19 | linguaspark = { git = "https://github.com/LinguaSpark/core.git", branch = "main" }
20 |
21 | [profile.release]
22 | strip = true
23 | opt-level = "z"
24 | lto = true
25 | codegen-units = 1
26 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM rust:bookworm AS builder
2 |
3 | WORKDIR /app
4 | COPY . .
5 |
6 | RUN cargo build --release
7 |
8 | RUN mkdir -p /app/lib && \
9 | find /app/target/release/build -name "linguaspark-*" -type d | xargs -I {} find {} -path "*/out/*.so" -type f | xargs -I {} cp {} /app/lib/ && \
10 | ls -l /app/lib
11 |
12 | FROM debian:bookworm-slim
13 |
14 | WORKDIR /app
15 | COPY --from=builder /app/target/release/linguaspark-server /app/linguaspark-server
16 | COPY --from=builder /app/lib/*.so /lib/x86_64-linux-gnu/
17 |
18 | ENV MODELS_DIR=/app/models
19 | ENV NUM_WORKERS=1
20 | ENV IP=0.0.0.0
21 | ENV PORT=3000
22 | # ENV ENV_API_KEY=
23 | ENV RUST_LOG=info
24 |
25 | EXPOSE 3000
26 |
27 | ENTRYPOINT ["/app/linguaspark-server"]
28 |
--------------------------------------------------------------------------------
/Dockerfile.enzh:
--------------------------------------------------------------------------------
1 | FROM ghcr.io/linguaspark/server:main
2 |
3 | COPY ./models-enzh /app/models
4 |
5 | ENV MODELS_DIR=/app/models
6 | ENV NUM_WORKERS=1
7 | ENV IP=0.0.0.0
8 | ENV PORT=3000
9 | # ENV ENV_API_KEY=
10 | ENV RUST_LOG=info
11 |
12 | EXPOSE 3000
13 |
14 | ENTRYPOINT ["/app/linguaspark-server"]
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published by
637 | the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mtranservercore-rs
2 |
3 | ## BergaRust - Translation Service
4 |
5 | [](https://github.com/aalivexy/translation-service)
6 | [](https://github.com/aalivexy/translation-service/pkgs/container/translation-service)
7 |
8 | A lightweight multilingual translation service based on Rust and Bergamot translation engine, compatible with multiple translation frontend APIs.
9 |
10 | [简体中文](README_ZH.md)
11 |
12 | ## Project Background
13 |
14 | This project originated when I discovered the [MTranServer](https://github.com/xxnuo/MTranServer/) repository, which uses [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/) for machine translation and is compatible with APIs like Immersive Translate and Kiss Translator, but found that it wasn't open-sourced yet.
15 |
16 | While searching for similar projects, I found Mozilla's [translation-service](https://github.com/mozilla/translation-service/), which works but hasn't been updated for a year and isn't compatible with Immersive Translate or Kiss Translator APIs. Since that project is written in C++ and I'm not very familiar with C++, I rewrote this project in Rust.
17 |
18 | ## Features
19 |
20 | - 💪 Written in Rust for excellent performance and low memory footprint
21 | - 🔄 Based on [Bergamot Translator](https://github.com/browsermt/bergamot-translator) engine used in Firefox
22 | - 🧠 Compatible with [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/)
23 | - 🔍 Built-in language detection with automatic source language identification
24 | - 🔌 Supports multiple translation API formats:
25 | - Native API
26 | - [Immersive Translate](https://immersivetranslate.com/) API
27 | - [Kiss Translator](https://www.kis-translator.com/) API
28 | - [HCFY](https://hcfy.app/) API
29 | - [DeepLX](https://github.com/OwO-Network/DeepLX) API
30 | - 🔑 API key protection support
31 | - 🐳 Docker deployment ready
32 |
33 | ## Tech Stack
34 |
35 | - **Web Framework**: [Axum](https://github.com/tokio-rs/axum)
36 | - **Translation Engine**: [Bergamot Translator](https://github.com/browsermt/bergamot-translator)
37 | - **Translation Models**: [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/)
38 | - **Language Detection**: [Whichlang](https://github.com/quickwit-oss/whichlang)
39 |
40 | ## Deployment
41 |
42 | Docker is the **only recommended** deployment method for this service.
43 |
44 | ### Option 1: Using pre-built image (with your own translation models)
45 |
46 | ```bash
47 | # Create models directory
48 | mkdir -p models
49 | # Download your models here
50 | # Pull and start container
51 | docker run -d --name translation-service \
52 | -p 3000:3000 \
53 | -v "$(pwd)/models:/app/models" \
54 | ghcr.io/aalivexy/translation-service:main
55 | ```
56 |
57 | ### Option 2: Using pre-built image with English-Chinese model (China mirror)
58 |
59 | ```bash
60 | docker run -d --name translation-service \
61 | -p 3000:3000 \
62 | docker.cnb.cool/aalivexy/translation-service:latest
63 | ```
64 |
65 | > Note: The English-Chinese model image is about 70MiB, and each worker uses approximately 300MiB+ of memory with low translation latency.
66 |
67 | ### Docker Compose Deployment
68 |
69 | Create a `compose.yaml` file:
70 |
71 | ```yaml
72 | services:
73 | translation-service:
74 | image: ghcr.io/aalivexy/translation-service:main
75 | ports:
76 | - "3000:3000"
77 | volumes:
78 | - ./models:/app/models
79 | environment:
80 | API_KEY: "your_api_key" # Optional, leave empty to disable API key protection
81 | restart: unless-stopped
82 | healthcheck:
83 | test: ["CMD", "/bin/sh", "-c", "echo -e 'GET /health HTTP/1.1\r\nHost: localhost:3000\r\n\r\n' | timeout 5 bash -c 'cat > /dev/tcp/localhost/3000' && echo 'Health check passed'"]
84 | interval: 30s
85 | timeout: 10s
86 | retries: 3
87 | ```
88 |
89 | Start the service:
90 |
91 | ```bash
92 | docker compose up -d
93 | ```
94 |
95 | ### Custom Image for Specific Language Pairs
96 |
97 | If you need to create a custom image with specific language pairs, use this Dockerfile template:
98 |
99 | ```dockerfile
100 | FROM ghcr.io/aalivexy/translation-service:main
101 |
102 | COPY ./your-models-directory /app/models
103 |
104 | ENV MODELS_DIR=/app/models
105 | ENV NUM_WORKERS=1
106 | ENV IP=0.0.0.0
107 | ENV PORT=3000
108 | ENV RUST_LOG=info
109 |
110 | EXPOSE 3000
111 |
112 | ENTRYPOINT ["/app/server"]
113 | ```
114 |
115 | ## Translation Models
116 |
117 | ### Getting Models
118 |
119 | 1. Download pre-trained models from [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/)
120 | 2. Place them in the models directory with the following structure:
121 |
122 | ```
123 | models/
124 | ├── enzh/ # Language pair directory name format: "[source language code][target language code]"
125 | │ ├── model.intgemm8.bin # Translation model
126 | │ ├── model.s2t.bin # Shortlist file
127 | │ ├── srcvocab.spm # Source language vocabulary
128 | │ └── trgvocab.spm # Target language vocabulary
129 | └── zhen/ # Another language pair
130 | └── ...
131 | ```
132 |
133 | ### Language Pair Support
134 |
135 | The translation service will automatically scan all language pair directories under the `models` directory and load them. Directory names should follow the `[source language][target language]` format using [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language codes.
136 |
137 | ## Environment Variables
138 |
139 | | Variable Name | Description | Default Value |
140 | |---------------|-------------|---------------|
141 | | `MODELS_DIR` | Path to models directory | `/app/models` |
142 | | `NUM_WORKERS` | Number of translation worker threads | `1` |
143 | | `IP` | IP address for the service to listen on | `127.0.0.1` |
144 | | `PORT` | Port for the service to listen on | `3000` |
145 | | `API_KEY` | API key (leave empty to disable) | `""` |
146 | | `RUST_LOG` | Log level | `info` |
147 |
148 | ## API Endpoints
149 |
150 | ### Native API
151 |
152 | #### Translate
153 |
154 | ```
155 | POST /translate
156 | ```
157 |
158 | Request body:
159 | ```json
160 | {
161 | "text": "Hello world",
162 | "from": "en", // Optional, omit to auto-detect
163 | "to": "zh"
164 | }
165 | ```
166 |
167 | Response:
168 | ```json
169 | {
170 | "text": "你好世界",
171 | "from": "en",
172 | "to": "zh"
173 | }
174 | ```
175 |
176 | #### Language Detection
177 |
178 | ```
179 | POST /detect
180 | ```
181 |
182 | Request body:
183 | ```json
184 | {
185 | "text": "Hello world"
186 | }
187 | ```
188 |
189 | Response:
190 | ```json
191 | {
192 | "language": "en"
193 | }
194 | ```
195 |
196 | ### Compatible APIs
197 |
198 | #### Immersive Translate API
199 |
200 | ```
201 | POST /imme
202 | ```
203 |
204 | Request body:
205 | ```json
206 | {
207 | "source_lang": "auto", // Optional, omit to auto-detect
208 | "target_lang": "zh",
209 | "text_list": ["Hello world", "How are you?"]
210 | }
211 | ```
212 |
213 | Response:
214 | ```json
215 | {
216 | "translations": [
217 | {
218 | "detected_source_lang": "en",
219 | "text": "你好世界"
220 | },
221 | {
222 | "detected_source_lang": "en",
223 | "text": "你好吗?"
224 | }
225 | ]
226 | }
227 | ```
228 |
229 | #### Kiss Translator API
230 |
231 | ```
232 | POST /kiss
233 | ```
234 |
235 | Request body:
236 | ```json
237 | {
238 | "text": "Hello world",
239 | "from": "en", // Optional, omit to auto-detect
240 | "to": "zh"
241 | }
242 | ```
243 |
244 | Response:
245 | ```json
246 | {
247 | "text": "你好世界",
248 | "from": "en",
249 | "to": "zh"
250 | }
251 | ```
252 |
253 | #### HCFY API
254 |
255 | ```
256 | POST /hcfy
257 | ```
258 |
259 | Request body:
260 | ```json
261 | {
262 | "text": "Hello world",
263 | "source": "英语", // Optional, omit to auto-detect
264 | "destination": ["中文(简体)"]
265 | }
266 | ```
267 |
268 | Response:
269 | ```json
270 | {
271 | "text": "Hello world",
272 | "from": "英语",
273 | "to": "中文(简体)",
274 | "result": ["你好世界"]
275 | }
276 | ```
277 |
278 | #### DeepLX API
279 |
280 | ```
281 | POST /translate
282 | ```
283 |
284 | Request body:
285 | ```json
286 | {
287 | "text": "Hello world",
288 | "source_lang": "EN",
289 | "target_lang": "ZH"
290 | }
291 | ```
292 |
293 | Response:
294 | ```json
295 | {
296 | "code": 200,
297 | "id": 1744646400,
298 | "data": "你好世界",
299 | "alternatives": [],
300 | "source_lang": "EN",
301 | "target_lang": "ZH"
302 | }
303 | ```
304 |
305 | ### Health Check
306 |
307 | ```
308 | GET /health
309 | ```
310 |
311 | Response:
312 | ```json
313 | {
314 | "status": "ok"
315 | }
316 | ```
317 |
318 | ## Authentication
319 |
320 | If the `API_KEY` environment variable is set, all API requests must provide authentication credentials using one of the following methods:
321 |
322 | 1. Authorization header: `Authorization: Bearer your_api_key`
323 | 2. Query parameter: `?token=your_api_key`
324 |
325 | ## License
326 |
327 | This project is open-sourced under the AGPL-3.0 license.
328 |
329 | ## Acknowledgements
330 |
331 | - [Bergamot Translator](https://github.com/browsermt/bergamot-translator) - Translation engine
332 | - [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/) - Translation models
333 | - [MTranServer](https://github.com/xxnuo/MTranServer/) - Inspiration
334 | - [Mozilla Translation Service](https://github.com/mozilla/translation-service/) - Reference implementation
335 |
--------------------------------------------------------------------------------
/README_ZH.md:
--------------------------------------------------------------------------------
1 | # BergaRust - Translation Service
2 |
3 | [](https://github.com/aalivexy/translation-service)
4 | [](https://github.com/aalivexy/translation-service/pkgs/container/translation-service)
5 |
6 | 一个基于 Rust 和 Bergamot 翻译引擎的轻量级多语言翻译服务,兼容多种翻译前端 API。
7 |
8 | [English](README.md)
9 |
10 | ## 项目背景
11 |
12 | 这个项目的起源是我看到了 [MTranServer](https://github.com/xxnuo/MTranServer/) 这个仓库,它使用了 [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/) 进行机器翻译,并且兼容了沉浸式翻译、简约翻译等 API,但发现它目前还没开源。
13 |
14 | 在寻找类似项目时,我发现了 Mozilla 的 [translation-service](https://github.com/mozilla/translation-service/),虽然能用但有一年没更新了,也不兼容沉浸式翻译、简约翻译的 API。由于该项目是 C++ 编写的,而我对 C++ 不太熟悉,所以我使用 Rust 重新编写了这个项目。
15 |
16 | ## 功能特性
17 |
18 | - 💪 使用 Rust 编写,性能优异,内存占用低
19 | - 🔄 基于 Firefox 同款的 [Bergamot Translator](https://github.com/browsermt/bergamot-translator) 引擎
20 | - 🧠 兼容 [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/)
21 | - 🔍 内置语言检测,支持自动识别源语言
22 | - 🔌 支持多种翻译前端 API 格式:
23 | - 原生 API
24 | - [沉浸式翻译 (Immersive Translate)](https://immersivetranslate.com/) API
25 | - [简约翻译 (Kiss Translator)](https://www.kis-translator.com/) API
26 | - [划词翻译 (HCFY)](https://hcfy.app/) API
27 | - [DeepLX](https://github.com/OwO-Network/DeepLX) API
28 | - 🔑 支持 API 密钥保护
29 | - 🐳 提供 Docker 镜像,便于部署
30 |
31 | ## 技术栈
32 |
33 | - **Web 框架**: [Axum](https://github.com/tokio-rs/axum)
34 | - **翻译引擎**: [Bergamot Translator](https://github.com/browsermt/bergamot-translator)
35 | - **翻译模型**: [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/)
36 | - **语言检测**: [Whichlang](https://github.com/quickwit-oss/whichlang)
37 |
38 | ## 部署
39 |
40 | Docker 是本服务**唯一推荐**的部署方式。
41 |
42 | ### 方式一:使用自带英译中模型的镜像(国内托管,推荐,速度快)
43 |
44 | ```bash
45 | docker run -d --name translation-service \
46 | -p 3000:3000 \
47 | docker.cnb.cool/aalivexy/translation-service:latest
48 | ```
49 |
50 | > 注意:自带英译中模型的镜像大小约 70MiB,启动后单 worker 大约占用内存 300MiB+,且翻译延迟较低。
51 |
52 | ### 方式二:使用预构建镜像(不含翻译模型)
53 |
54 | ```bash
55 | # 创建模型目录
56 | mkdir -p models
57 | # 下载你的模型到目录里
58 | # 拉取并启动容器
59 | docker run -d --name translation-service \
60 | -p 3000:3000 \
61 | -v "$(pwd)/models:/app/models" \
62 | ghcr.io/aalivexy/translation-service:main
63 | ```
64 |
65 | ### Docker Compose 部署
66 |
67 | 创建 `compose.yaml` 文件:
68 |
69 | ```yaml
70 | services:
71 | translation-service:
72 | image: docker.cnb.cool/aalivexy/translation-service:latest
73 | ports:
74 | - "3000:3000"
75 | environment:
76 | API_KEY: "" # 可选,设置为空字符串则不启用 API 密钥保护
77 | restart: unless-stopped
78 | healthcheck:
79 | test: ["CMD", "/bin/sh", "-c", "echo -e 'GET /health HTTP/1.1\r\nHost: localhost:3000\r\n\r\n' | timeout 5 bash -c 'cat > /dev/tcp/localhost/3000' && echo 'Health check passed'"]
80 | interval: 30s
81 | timeout: 10s
82 | retries: 3
83 | ```
84 |
85 | 启动服务:
86 |
87 | ```bash
88 | docker compose up -d
89 | ```
90 |
91 | ### 自定义特定语言对的镜像
92 |
93 | 如果需要创建包含特定语言对的自定义镜像,可以使用以下 Dockerfile 模板:
94 |
95 | ```dockerfile
96 | FROM ghcr.io/aalivexy/translation-service:main
97 |
98 | COPY ./your-models-directory /app/models
99 |
100 | ENV MODELS_DIR=/app/models
101 | ENV NUM_WORKERS=1
102 | ENV IP=0.0.0.0
103 | ENV PORT=3000
104 | ENV RUST_LOG=info
105 |
106 | EXPOSE 3000
107 |
108 | ENTRYPOINT ["/app/server"]
109 | ```
110 |
111 | ## 翻译模型
112 |
113 | ### 获取模型
114 |
115 | 1. 从 [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/) 下载预训练模型
116 | 2. 模型放置结构应为:
117 |
118 | ```
119 | models/
120 | ├── enzh/ # 语言对目录名格式为 "[源语言代码][目标语言代码]"
121 | │ ├── model.intgemm8.bin # 翻译模型
122 | │ ├── model.s2t.bin # shortlist 文件
123 | │ ├── srcvocab.spm # 源语言词表
124 | │ └── trgvocab.spm # 目标语言词表
125 | └── zhen/ # 另一个语言对
126 | └── ...
127 | ```
128 |
129 | ### 语言对支持
130 |
131 | 翻译服务会自动扫描 `models` 目录下的所有语言对目录,并加载它们。目录名应遵循 `[源语言][目标语言]` 的格式,使用 [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) 语言代码。
132 |
133 | ## 环境变量
134 |
135 | | 变量名 | 描述 | 默认值 |
136 | |--------|------|--------|
137 | | `MODELS_DIR` | 模型目录路径 | `./models` |
138 | | `NUM_WORKERS` | 翻译工作线程数 | `1` |
139 | | `IP` | 服务监听的 IP 地址 | `127.0.0.1` |
140 | | `PORT` | 服务监听的端口 | `3000` |
141 | | `API_KEY` | API 密钥(留空则不启用) | `""` |
142 | | `RUST_LOG` | 日志级别 | `info` |
143 |
144 | ## API 端点
145 |
146 | ### 原生 API
147 |
148 | #### 翻译
149 |
150 | ```
151 | POST /translate
152 | ```
153 |
154 | 请求体:
155 | ```json
156 | {
157 | "text": "Hello world",
158 | "from": "en", // 可选,省略则自动检测
159 | "to": "zh"
160 | }
161 | ```
162 |
163 | 响应:
164 | ```json
165 | {
166 | "text": "你好世界",
167 | "from": "en",
168 | "to": "zh"
169 | }
170 | ```
171 |
172 | #### 语言检测
173 |
174 | ```
175 | POST /detect
176 | ```
177 |
178 | 请求体:
179 | ```json
180 | {
181 | "text": "Hello world"
182 | }
183 | ```
184 |
185 | 响应:
186 | ```json
187 | {
188 | "language": "en"
189 | }
190 | ```
191 |
192 | ### 兼容 API
193 |
194 | #### 沉浸式翻译 API
195 |
196 | ```
197 | POST /imme
198 | ```
199 |
200 | 请求体:
201 | ```json
202 | {
203 | "source_lang": "auto", // 可选,省略则自动检测
204 | "target_lang": "zh",
205 | "text_list": ["Hello world", "How are you?"]
206 | }
207 | ```
208 |
209 | 响应:
210 | ```json
211 | {
212 | "translations": [
213 | {
214 | "detected_source_lang": "en",
215 | "text": "你好世界"
216 | },
217 | {
218 | "detected_source_lang": "en",
219 | "text": "你好吗?"
220 | }
221 | ]
222 | }
223 | ```
224 |
225 | #### 简约翻译 API
226 |
227 | ```
228 | POST /kiss
229 | ```
230 |
231 | 请求体:
232 | ```json
233 | {
234 | "text": "Hello world",
235 | "from": "en", // 可选,省略则自动检测
236 | "to": "zh"
237 | }
238 | ```
239 |
240 | 响应:
241 | ```json
242 | {
243 | "text": "你好世界",
244 | "from": "en",
245 | "to": "zh"
246 | }
247 | ```
248 |
249 | #### 划词翻译 API
250 |
251 | ```
252 | POST /hcfy
253 | ```
254 |
255 | 请求体:
256 | ```json
257 | {
258 | "text": "Hello world",
259 | "source": "英语", // 可选,省略则自动检测
260 | "destination": ["中文(简体)"]
261 | }
262 | ```
263 |
264 | 响应:
265 | ```json
266 | {
267 | "text": "Hello world",
268 | "from": "英语",
269 | "to": "中文(简体)",
270 | "result": ["你好世界"]
271 | }
272 | ```
273 |
274 | #### DeepLX API
275 |
276 | ```
277 | POST /translate
278 | ```
279 |
280 | 请求体:
281 | ```json
282 | {
283 | "text": "Hello world",
284 | "source_lang": "EN", // 可选,省略则自动检测
285 | "target_lang": "ZH"
286 | }
287 | ```
288 |
289 | 响应:
290 | ```json
291 | {
292 | "code": 200,
293 | "data": "你好世界",
294 | "alternatives": []
295 | }
296 | ```
297 |
298 | ### 健康检查
299 |
300 | ```
301 | GET /health
302 | ```
303 |
304 | 响应:
305 | ```json
306 | {
307 | "status": "ok"
308 | }
309 | ```
310 |
311 | ## 认证
312 |
313 | 如果设置了 `API_KEY` 环境变量,所有 API 请求都需要提供认证凭据,支持两种方式:
314 |
315 | 1. Authorization 头: `Authorization: Bearer your_api_key`
316 | 2. 查询参数: `?token=your_api_key`
317 |
318 | ## 许可证
319 |
320 | 本项目基于 AGPL-3.0 许可证开源。
321 |
322 | ## 致谢
323 |
324 | - [Bergamot Translator](https://github.com/browsermt/bergamot-translator) - 提供翻译引擎
325 | - [Firefox Translations Models](https://github.com/mozilla/firefox-translations-models/) - 提供翻译模型
326 | - [MTranServer](https://github.com/xxnuo/MTranServer/) - 提供灵感来源
327 | - [Mozilla Translation Service](https://github.com/mozilla/translation-service/) - 提供参考实现
328 |
--------------------------------------------------------------------------------
/compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | linguaspark:
3 | build:
4 | context: .
5 | dockerfile: Dockerfile
6 | image: linguaspark:latest
7 | ports:
8 | - "3000:3000"
9 | volumes:
10 | - ./models:/app/models
11 | environment:
12 | API_KEY: ""
13 | restart: unless-stopped
14 | healthcheck:
15 | test: ["CMD", "/bin/sh", "-c", "echo -e 'GET /health HTTP/1.1\r\nHost: localhost:3000\r\n\r\n' | timeout 5 bash -c 'cat > /dev/tcp/localhost/3000' && echo 'Health check passed'"]
16 | interval: 30s
17 | timeout: 10s
18 | retries: 3
19 |
--------------------------------------------------------------------------------
/src/endpoint.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | AppError, AppState,
3 | translation::{detect_language_code, perform_translation},
4 | };
5 | use axum::{Json, extract::State};
6 | use serde::{Deserialize, Serialize};
7 | use std::{sync::Arc, time::SystemTime};
8 |
9 | #[derive(Debug, Deserialize)]
10 | pub struct DetectLanguageRequest {
11 | text: String,
12 | }
13 |
14 | #[derive(Debug, Serialize)]
15 | pub struct DetectLanguageResponse {
16 | language: String,
17 | }
18 |
19 | pub async fn detect_language(
20 | Json(request): Json,
21 | ) -> Result, AppError> {
22 | Ok(Json(DetectLanguageResponse {
23 | language: detect_language_code(&request.text)?.to_owned(),
24 | }))
25 | }
26 |
27 | #[derive(Debug, Deserialize)]
28 | pub struct TranslationRequest {
29 | text: String,
30 | from: Option,
31 | to: String,
32 | }
33 |
34 | #[derive(Debug, Serialize)]
35 | pub struct TranslationResponse {
36 | text: String,
37 | from: String,
38 | to: String,
39 | }
40 |
41 | pub async fn translate(
42 | State(state): State>,
43 | Json(request): Json,
44 | ) -> Result, AppError> {
45 | let (text, from_lang, to_lang) =
46 | perform_translation(&state, &request.text, request.from, &request.to).await?;
47 |
48 | Ok(Json(TranslationResponse {
49 | text,
50 | from: from_lang,
51 | to: to_lang,
52 | }))
53 | }
54 |
55 | #[derive(Debug, Deserialize)]
56 | pub struct KissTranslationRequest {
57 | text: String,
58 | from: Option,
59 | to: String,
60 | }
61 |
62 | #[derive(Debug, Serialize)]
63 | pub struct KissTranslationResponse {
64 | text: String,
65 | from: String,
66 | to: String,
67 | }
68 |
69 | pub async fn translate_kiss(
70 | State(state): State>,
71 | Json(request): Json,
72 | ) -> Result, AppError> {
73 | let (text, from_lang, to_lang) =
74 | perform_translation(&state, &request.text, request.from, &request.to).await?;
75 |
76 | Ok(Json(KissTranslationResponse {
77 | text,
78 | from: from_lang,
79 | to: to_lang,
80 | }))
81 | }
82 |
83 | #[derive(Debug, Deserialize)]
84 | pub struct ImmersiveTranslationRequest {
85 | source_lang: Option,
86 | target_lang: String,
87 | text_list: Vec,
88 | }
89 |
90 | #[derive(Debug, Serialize)]
91 | pub struct ImmersiveTranslationItem {
92 | detected_source_lang: String,
93 | text: String,
94 | }
95 |
96 | #[derive(Debug, Serialize)]
97 | pub struct ImmersiveTranslationResponse {
98 | translations: Vec,
99 | }
100 |
101 | pub async fn translate_immersive(
102 | State(state): State>,
103 | Json(request): Json,
104 | ) -> Result, AppError> {
105 | let mut translations = Vec::with_capacity(request.text_list.len());
106 |
107 | for text in request.text_list {
108 | let (translated_text, from_lang, _) = perform_translation(
109 | &state,
110 | &text,
111 | request.source_lang.clone(),
112 | &request.target_lang,
113 | )
114 | .await?;
115 |
116 | translations.push(ImmersiveTranslationItem {
117 | detected_source_lang: from_lang,
118 | text: translated_text,
119 | });
120 | }
121 |
122 | Ok(Json(ImmersiveTranslationResponse { translations }))
123 | }
124 |
125 | #[derive(Debug, Deserialize)]
126 | pub struct HcfyTranslationRequest {
127 | text: String,
128 | source: Option,
129 | destination: Vec,
130 | }
131 |
132 | #[derive(Debug, Serialize)]
133 | pub struct HcfyTranslationResponse {
134 | text: String,
135 | from: String,
136 | to: String,
137 | result: Vec,
138 | }
139 |
140 | pub async fn translate_hcfy(
141 | State(state): State>,
142 | Json(request): Json,
143 | ) -> Result, AppError> {
144 | const LANGUAGE_CODE_MAP: &[(&str, &str)] =
145 | &[("中文(简体)", "zh"), ("英语", "en"), ("日语", "jp")];
146 |
147 | fn convert_language_name(lang: &str) -> String {
148 | LANGUAGE_CODE_MAP
149 | .iter()
150 | .find(|&&(name, _)| name == lang)
151 | .map(|&(_, code)| code)
152 | .unwrap_or_else(|| lang)
153 | .to_string()
154 | }
155 |
156 | fn get_language_name(code: &str) -> String {
157 | LANGUAGE_CODE_MAP
158 | .iter()
159 | .find(|&&(_, c)| c == code)
160 | .map(|&(name, _)| name)
161 | .unwrap_or(code)
162 | .to_string()
163 | }
164 |
165 | let source_lang = request.source.as_deref().map(convert_language_name);
166 |
167 | let target_lang = match (
168 | request.destination.first(),
169 | source_lang.as_deref(),
170 | request.destination.get(1),
171 | ) {
172 | (None, _, _) => "en".to_string(),
173 | (Some(first), Some(src), Some(second)) if convert_language_name(first) == src => {
174 | convert_language_name(second)
175 | }
176 | (Some(first), _, _) => convert_language_name(first),
177 | };
178 |
179 | let (translated_text, detected_source, _) =
180 | perform_translation(&state, &request.text, source_lang, &target_lang).await?;
181 |
182 | Ok(Json(HcfyTranslationResponse {
183 | text: request.text,
184 | from: get_language_name(&detected_source),
185 | to: get_language_name(&target_lang),
186 | result: vec![translated_text],
187 | }))
188 | }
189 |
190 | #[derive(Debug, Deserialize)]
191 | pub struct DeeplxTranslationRequest {
192 | text: String,
193 | source_lang: String,
194 | target_lang: String,
195 | }
196 |
197 | #[derive(Debug, Serialize)]
198 | pub struct DeeplxTranslationResponse {
199 | code: u32,
200 | id: u128,
201 | data: String,
202 | alternatives: Vec,
203 | source_lang: String,
204 | target_lang: String,
205 | method: String,
206 | }
207 |
208 | pub async fn translate_deeplx(
209 | State(state): State>,
210 | Json(request): Json,
211 | ) -> Result, AppError> {
212 | let (text, from_lang, to_lang) = perform_translation(
213 | &state,
214 | &request.text,
215 | Some(request.source_lang.to_lowercase()),
216 | &request.target_lang.to_lowercase(),
217 | )
218 | .await?;
219 |
220 | Ok(Json(DeeplxTranslationResponse {
221 | code: 200,
222 | id: SystemTime::now()
223 | .duration_since(SystemTime::UNIX_EPOCH)
224 | .unwrap()
225 | .as_millis(),
226 | data: text,
227 | alternatives: vec![],
228 | source_lang: from_lang.to_uppercase(),
229 | target_lang: to_lang.to_uppercase(),
230 | method: "Free".to_owned(),
231 | }))
232 | }
233 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Context;
2 | use axum::{
3 | Router,
4 | extract::Json,
5 | http::{HeaderMap, StatusCode},
6 | middleware::{self, Next},
7 | response::{IntoResponse, Response},
8 | routing::{get, post},
9 | };
10 | use isolang::Language;
11 | use linguaspark::Translator;
12 | use std::{fs, io, net::SocketAddr, path::PathBuf, sync::Arc};
13 | use tokio::net::TcpListener;
14 | use tower_http::{
15 | cors::{Any, CorsLayer},
16 | trace::TraceLayer,
17 | };
18 | use tracing::{debug, error, info};
19 |
20 | mod endpoint;
21 | mod translation;
22 |
23 | const ENV_MODELS_PATH: &str = "MODELS_DIR";
24 | const ENV_NUM_WORKERS: &str = "NUM_WORKERS";
25 | const ENV_SERVER_IP: &str = "IP";
26 | const ENV_SERVER_PORT: &str = "PORT";
27 | const ENV_API_KEY: &str = "API_KEY";
28 | const ENV_LOG_LEVEL: &str = "RUST_LOG";
29 |
30 | #[derive(Debug, thiserror::Error)]
31 | enum AppError {
32 | #[error("Translation error: {0}")]
33 | TranslationError(String),
34 |
35 | #[error("IO error: {0}")]
36 | IoError(#[from] io::Error),
37 |
38 | #[error("Unauthorized")]
39 | Unauthorized,
40 |
41 | #[error("Translator error: {0}")]
42 | TranslatorError(#[from] linguaspark::TranslatorError),
43 |
44 | #[error("Configuration error: {0}")]
45 | ConfigError(String),
46 | }
47 |
48 | impl IntoResponse for AppError {
49 | fn into_response(self) -> Response {
50 | let (status, message) = match self {
51 | AppError::TranslationError(msg) => (StatusCode::BAD_REQUEST, msg),
52 | AppError::IoError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
53 | AppError::Unauthorized => (
54 | StatusCode::UNAUTHORIZED,
55 | "Invalid or missing API key".to_string(),
56 | ),
57 | AppError::TranslatorError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
58 | AppError::ConfigError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
59 | };
60 |
61 | (status, Json(serde_json::json!({ "error": message }))).into_response()
62 | }
63 | }
64 |
65 | struct AppState {
66 | translator: Translator,
67 | models: Vec<(Language, Language)>,
68 | }
69 |
70 | async fn auth_middleware(
71 | headers: HeaderMap,
72 | request: axum::extract::Request,
73 | next: Next,
74 | ) -> Result {
75 | let expected_key = std::env::var(ENV_API_KEY).unwrap_or_default();
76 |
77 | if !expected_key.is_empty() {
78 | let header_key = headers
79 | .get("Authorization")
80 | .and_then(|header| header.to_str().ok())
81 | .and_then(|auth| auth.strip_prefix("Bearer "));
82 |
83 | let query_key = request.uri().query().and_then(|query| {
84 | query.split('&').find_map(|pair| {
85 | let mut parts = pair.split('=');
86 | if let Some("token") = parts.next() {
87 | parts.next()
88 | } else {
89 | None
90 | }
91 | })
92 | });
93 |
94 | if header_key != Some(&expected_key) && query_key != Some(&expected_key) {
95 | debug!("Invalid API key");
96 | return Err(AppError::Unauthorized);
97 | }
98 | }
99 | Ok(next.run(request).await)
100 | }
101 |
102 | fn load_models_manually(
103 | translator: &Translator,
104 | models_dir: &PathBuf,
105 | ) -> Result, AppError> {
106 | let mut models = Vec::new();
107 |
108 | for entry in fs::read_dir(models_dir)? {
109 | let entry = entry?;
110 | let model_dir_path = entry.path();
111 | let language_pair = entry.file_name().to_string_lossy().into_owned();
112 |
113 | info!("Looking for models in {}", model_dir_path.display());
114 | translator.load_model(&language_pair, model_dir_path)?;
115 |
116 | if language_pair.len() >= 4 {
117 | let from_lang = translation::parse_language_code(&language_pair[0..2])?;
118 | let to_lang = translation::parse_language_code(&language_pair[2..4])?;
119 | models.push((from_lang, to_lang));
120 | } else {
121 | return Err(AppError::ConfigError(format!(
122 | "Invalid language pair format: '{}'. Expected format like 'enzh', 'jpen'",
123 | language_pair
124 | )));
125 | }
126 |
127 | info!("Loaded model for language pair '{}'", language_pair);
128 | }
129 |
130 | Ok(models)
131 | }
132 |
133 | #[tokio::main]
134 | async fn main() -> anyhow::Result<()> {
135 | if std::env::var(ENV_LOG_LEVEL).is_err() {
136 | tracing_subscriber::fmt()
137 | .with_max_level(tracing::Level::INFO)
138 | .init();
139 | } else {
140 | tracing_subscriber::fmt::init();
141 | }
142 |
143 | let models_dir = std::env::var(ENV_MODELS_PATH)
144 | .map(PathBuf::from)
145 | .context(format!(
146 | "Failed to get environment variable {}",
147 | ENV_MODELS_PATH
148 | ))
149 | .unwrap_or_else(|_| {
150 | let default_dir = PathBuf::from("models");
151 | if !default_dir.exists() {
152 | fs::create_dir_all(&default_dir)
153 | .expect("Failed to create default models directory");
154 | }
155 | default_dir
156 | });
157 |
158 | let num_workers = std::env::var(ENV_NUM_WORKERS)
159 | .ok()
160 | .and_then(|s| s.parse::().ok())
161 | .unwrap_or(1);
162 |
163 | let server_ip = std::env::var(ENV_SERVER_IP).unwrap_or_else(|_| "127.0.0.1".to_string());
164 | let server_port = std::env::var(ENV_SERVER_PORT)
165 | .ok()
166 | .and_then(|s| s.parse::().ok())
167 | .unwrap_or(3000);
168 |
169 | let server_address = format!("{}:{}", server_ip, server_port);
170 |
171 | info!("Initializing translator with {} workers", num_workers);
172 | let translator = Translator::new(num_workers).context("Failed to initialize translator")?;
173 |
174 | info!("Loading translation models from {}", models_dir.display());
175 | let models = load_models_manually(&translator, &models_dir)
176 | .context("Failed to load translation models")?;
177 |
178 | let app_state = Arc::new(AppState { translator, models });
179 |
180 | let cors = CorsLayer::new()
181 | .allow_origin(Any)
182 | .allow_methods(Any)
183 | .allow_headers(Any);
184 |
185 | let app = Router::new()
186 | .route("/translate", post(endpoint::translate))
187 | .route("/kiss", post(endpoint::translate_kiss))
188 | .route("/imme", post(endpoint::translate_immersive))
189 | .route("/hcfy", post(endpoint::translate_hcfy))
190 | .route("/deeplx", post(endpoint::translate_deeplx))
191 | .route("/detect", post(endpoint::detect_language))
192 | .route(
193 | "/health",
194 | get(async || {
195 | Json(serde_json::json!({
196 | "status": "ok",
197 | }))
198 | }),
199 | )
200 | .route_layer(middleware::from_fn(auth_middleware))
201 | .layer(TraceLayer::new_for_http())
202 | .layer(cors)
203 | .with_state(app_state);
204 |
205 | let addr: SocketAddr = server_address.parse().context(format!(
206 | "Failed to parse server address: {}",
207 | server_address
208 | ))?;
209 | info!(
210 | "Starting server on {} (IP: {}, Port: {})",
211 | addr, server_ip, server_port
212 | );
213 | let listener = TcpListener::bind(addr)
214 | .await
215 | .context(format!("Failed to bind to address: {}", addr))?;
216 |
217 | axum::serve(listener, app).await.context("Server error")?;
218 |
219 | Ok(())
220 | }
221 |
--------------------------------------------------------------------------------
/src/translation.rs:
--------------------------------------------------------------------------------
1 | use crate::{AppError, AppState};
2 | use isolang::Language;
3 | use std::sync::Arc;
4 |
5 | pub fn parse_language_code(code: &str) -> Result {
6 | Language::from_639_1(code.split('-').next().unwrap_or(code)).ok_or_else(|| {
7 | AppError::TranslationError(format!(
8 | "Invalid language code: '{}'. Please use ISO 639-1 format.",
9 | code
10 | ))
11 | })
12 | }
13 |
14 | fn get_iso_code(lang: &Language) -> Result<&'static str, AppError> {
15 | lang.to_639_1().ok_or_else(|| {
16 | AppError::TranslationError(format!(
17 | "Language '{}' doesn't have an ISO 639-1 code",
18 | lang
19 | ))
20 | })
21 | }
22 |
23 | pub fn detect_language_code(text: &str) -> Result<&'static str, AppError> {
24 | get_iso_code(
25 | &Language::from_639_3(whichlang::detect_language(text).three_letter_code()).ok_or_else(
26 | || {
27 | AppError::TranslationError(format!(
28 | "Failed to identify language for text: '{}'",
29 | text
30 | ))
31 | },
32 | )?,
33 | )
34 | }
35 |
36 | pub async fn perform_translation(
37 | state: &Arc,
38 | text: &str,
39 | from_lang: Option,
40 | to_lang: &str,
41 | ) -> Result<(String, String, String), AppError> {
42 | let source_lang = match from_lang.as_deref() {
43 | None | Some("") | Some("auto") => {
44 | if state.models.len() == 1 {
45 | // If there's only one model, use it as the source language
46 | state
47 | .models
48 | .first()
49 | .map(|model| model.0)
50 | .unwrap_or(Language::Eng)
51 | } else {
52 | Language::from_639_3(whichlang::detect_language(text).three_letter_code())
53 | .ok_or_else(|| {
54 | AppError::TranslationError(format!(
55 | "Failed to detect language for text: '{}'",
56 | text
57 | ))
58 | })?
59 | }
60 | }
61 | Some(code) => parse_language_code(code)?,
62 | };
63 |
64 | let target_lang = parse_language_code(to_lang)?;
65 |
66 | let from_code = get_iso_code(&source_lang)?;
67 | let to_code = get_iso_code(&target_lang)?;
68 |
69 | if !state.translator.is_supported(from_code, to_code)? {
70 | return Err(AppError::TranslationError(format!(
71 | "Translation from '{}' to '{}' is not supported",
72 | from_code, to_code
73 | )));
74 | }
75 |
76 | let translated_text = state.translator.translate(from_code, to_code, text)?;
77 |
78 | Ok((translated_text, from_code.to_string(), to_code.to_string()))
79 | }
80 |
--------------------------------------------------------------------------------