forked from mirrors/LibreTranslate
Compare commits
130 commits
Author | SHA1 | Date | |
---|---|---|---|
753a33e7c5 | |||
e559f899b8 | |||
01b892b96a | |||
282737519c | |||
5a2e0fe656 | |||
11324ab1f5 | |||
91ae57ad6c | |||
72aaa41d8f | |||
b407ffee3c | |||
f2d6800f4c | |||
959f4638cc | |||
5285697203 | |||
10f82d9a3e | |||
c4d1b05b27 | |||
034407ee15 | |||
b55d150f9c | |||
cdfeb6d8fc | |||
34fceaacc7 | |||
c9ccbf6a25 | |||
9a3b92caf1 | |||
5974c022b6 | |||
c21fedc6bb | |||
9629cb8888 | |||
b9293e911a | |||
4ab7e7d2f5 | |||
9deefbbc84 | |||
8be739a783 | |||
2b0074b94f | |||
0875fdc433 | |||
6621c51b52 | |||
e79089b5c1 | |||
0478d4ee2c | |||
b87b210bfc | |||
30b3382af8 | |||
7c5d798fd5 | |||
ee638b407d | |||
1c5b006c87 | |||
488aa6db97 | |||
b59e8ffcab | |||
7c753a33d4 | |||
f96e77df2b | |||
65e6a7f262 | |||
968793d36e | |||
8e2c6a9405 | |||
dc7b1367f6 | |||
a6a061d439 | |||
db71da493b | |||
6cf1a931fa | |||
aed684b666 | |||
07fafa2f4b | |||
99672ef528 | |||
69762ce4f4 | |||
85da156c39 | |||
70c4571067 | |||
bc8b331fe9 | |||
f18dfcd60d | |||
0b34a4545c | |||
ea3979ab0c | |||
0e3b9a9ebd | |||
e68ec8411d | |||
b833608f5c | |||
eb0df4d0df | |||
54079b0276 | |||
9e8bcf6040 | |||
eee2cf2f33 | |||
3072219085 | |||
5d8e513d45 | |||
7c37681afc | |||
99b6a1a2f2 | |||
b90d996965 | |||
36e05596aa | |||
057e4b01c9 | |||
052f0ae1cd | |||
eb5aec7bdb | |||
bb6536d786 | |||
c66a519751 | |||
077d6e9efa | |||
5aa3f1eee8 | |||
13d792b6b6 | |||
7b7cd7768b | |||
cabb887212 | |||
9d6d06ceeb | |||
81f667b214 | |||
2a5e84730a | |||
299e5c3aa0 | |||
8f8473ed4a | |||
6cc4f94da2 | |||
6415ea98f2 | |||
d11e2ea165 | |||
be03358e83 | |||
d8bf8af169 | |||
d7a536fdd0 | |||
95405726a2 | |||
cebeca43cb | |||
dd4b2cb0b9 | |||
e0c4810246 | |||
85c569e124 | |||
982c412169 | |||
54bdc77bdc | |||
b2a4dd0a9f | |||
c36c9ade62 | |||
537430f55d | |||
d1d8fbab89 | |||
b6219cb605 | |||
ed764ce81b | |||
da1c9af97d | |||
be88f289c7 | |||
1dddcea794 | |||
e4fe0e8919 | |||
752d2aae2c | |||
393eebe235 | |||
482a1c65c6 | |||
727b9192e8 | |||
6e2df73a7f | |||
c2b82e9303 | |||
79b9af9071 | |||
78d2bc7be4 | |||
47fc85fdec | |||
ed68f1bcf9 | |||
8f8087f8ae | |||
647379aea5 | |||
3c1be4e731 | |||
89dde2d468 | |||
8247444fb2 | |||
9670777484 | |||
ef8ccc231c | |||
9831ba88a6 | |||
d5f6276e7a | |||
e81d119603 | |||
d6ef04ba3e |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
custom: ['https://portal.libretranslate.com']
|
2
.github/workflows/publish-docker.yml
vendored
2
.github/workflows/publish-docker.yml
vendored
|
@ -42,7 +42,7 @@ jobs:
|
|||
- name: Build and push Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
${{ steps.get-variables.outputs.gh-username-lower }}/libretranslate:${{ env.TAG }},
|
||||
ghcr.io/${{ steps.get-variables.outputs.gh-username-lower }}/libretranslate:${{ env.TAG }}
|
||||
|
|
47
Dockerfile
47
Dockerfile
|
@ -1,34 +1,43 @@
|
|||
FROM python:3.8.12-slim-bullseye
|
||||
|
||||
ARG with_models=false
|
||||
ARG models=
|
||||
FROM python:3.8.14-slim-bullseye as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get -qqq install --no-install-recommends -y libicu-dev pkg-config gcc g++ \
|
||||
&& apt-get -qqq install --no-install-recommends -y pkg-config gcc g++ \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt
|
||||
|
||||
RUN pip install --upgrade pip
|
||||
RUN apt-get update && apt-get upgrade --assume-yes
|
||||
|
||||
RUN python -mvenv venv && ./venv/bin/pip install --upgrade pip
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install package from source code
|
||||
RUN ./venv/bin/pip install . \
|
||||
&& ./venv/bin/pip cache purge
|
||||
|
||||
|
||||
FROM python:3.8.14-slim-bullseye
|
||||
|
||||
ARG with_models=false
|
||||
ARG models=
|
||||
|
||||
RUN addgroup --system --gid 1032 libretranslate && adduser --system --uid 1032 libretranslate && mkdir -p /home/libretranslate/.local && chown -R libretranslate:libretranslate /home/libretranslate/.local
|
||||
USER libretranslate
|
||||
|
||||
COPY --from=builder --chown=1032:1032 /app /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN if [ "$with_models" = "true" ]; then \
|
||||
# install only the dependencies first
|
||||
pip install -e .; \
|
||||
# initialize the language models
|
||||
if [ ! -z "$models" ]; then \
|
||||
./install_models.py --load_only_lang_codes "$models"; \
|
||||
else \
|
||||
./install_models.py; \
|
||||
fi \
|
||||
fi
|
||||
# Install package from source code
|
||||
RUN pip install . \
|
||||
&& pip cache purge
|
||||
# initialize the language models
|
||||
if [ ! -z "$models" ]; then \
|
||||
./venv/bin/python install_models.py --load_only_lang_codes "$models"; \
|
||||
else \
|
||||
./venv/bin/python install_models.py; \
|
||||
fi \
|
||||
fi
|
||||
|
||||
EXPOSE 5000
|
||||
ENTRYPOINT [ "libretranslate", "--host", "0.0.0.0" ]
|
||||
ENTRYPOINT [ "./venv/bin/libretranslate", "--host", "0.0.0.0" ]
|
||||
|
|
197
README.md
197
README.md
|
@ -110,12 +110,8 @@ libretranslate [args]
|
|||
|
||||
Then open a web browser to http://localhost:5000
|
||||
|
||||
If you're on Windows, we recommend you [Run with Docker](#run-with-docker) instead.
|
||||
|
||||
On Ubuntu 20.04 you can also use the install script available at https://github.com/argosopentech/LibreTranslate-init
|
||||
|
||||
If you would rather run it natively, you can follow the guide [here](https://github.com/nuttolum/LibreOnWindows).
|
||||
|
||||
## Build and Run
|
||||
|
||||
If you want to make changes to the code, you can build from source, and run the API:
|
||||
|
@ -134,14 +130,12 @@ Then open a web browser to http://localhost:5000
|
|||
|
||||
### Run with Docker
|
||||
|
||||
Simply run:
|
||||
|
||||
```bash
|
||||
docker run -ti --rm -p 5000:5000 libretranslate/libretranslate
|
||||
```
|
||||
Linux/MacOS: `./run.sh [args]`
|
||||
Windows: `run.bat [args]`
|
||||
|
||||
Then open a web browser to http://localhost:5000
|
||||
|
||||
|
||||
### Build with Docker
|
||||
|
||||
```bash
|
||||
|
@ -164,6 +158,18 @@ docker-compose up -d --build
|
|||
|
||||
> Feel free to change the [`docker-compose.yml`](https://github.com/LibreTranslate/LibreTranslate/blob/main/docker-compose.yml) file to adapt it to your deployment needs, or use an extra `docker-compose.prod.yml` file for your deployment configuration.
|
||||
|
||||
> The models are stored inside the container under `/home/libretranslate/.local/share` and `/home/libretranslate/.local/cache`. Feel free to use volumes if you do not want to redownload the models when the container is destroyed. To update the models, use the `--update-models` argument.
|
||||
|
||||
### CUDA
|
||||
|
||||
You can use hardware acceleration to speed up translations on a GPU machine with CUDA 11.2 and [nvidia-docker](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) installed.
|
||||
|
||||
Run this version with:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.cuda.yml up -d --build
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
| Argument | Description | Default | Env. name |
|
||||
|
@ -172,6 +178,7 @@ docker-compose up -d --build
|
|||
| --port | Set port to bind the server to | `5000` | LT_PORT |
|
||||
| --char-limit | Set character limit | `No limit` | LT_CHAR_LIMIT |
|
||||
| --req-limit | Set maximum number of requests per minute per client | `No limit` | LT_REQ_LIMIT |
|
||||
| --req-limit-storage | Storage URI to use for request limit data storage. See [Flask Limiter](https://flask-limiter.readthedocs.io/en/stable/configuration.html) | `memory://` | LT_REQ_LIMIT_STORAGE |
|
||||
| --batch-limit | Set maximum number of texts to translate in a batch request | `No limit` | LT_BATCH_LIMIT |
|
||||
| --ga-id | Enable Google Analytics on the API client page by providing an ID | `No tracking` | LT_GA_ID |
|
||||
| --debug | Enable debug environment | `False` | LT_DEBUG |
|
||||
|
@ -180,13 +187,36 @@ docker-compose up -d --build
|
|||
| --frontend-language-target | Set frontend default language - target | `es` | LT_FRONTEND_LANGUAGE_TARGET |
|
||||
| --frontend-timeout | Set frontend translation timeout | `500` | LT_FRONTEND_TIMEOUT |
|
||||
| --api-keys | Enable API keys database for per-user rate limits lookup | `Don't use API keys` | LT_API_KEYS |
|
||||
| --api-keys-db-path | Use a specific path inside the container for the local database. Can be absolute or relative | `db/api_keys.db` | LT_API_KEYS_DB_PATH |
|
||||
| --api-keys-remote | Use this remote endpoint to query for valid API keys instead of using the local database | `Use local API key database` | LT_API_KEYS_REMOTE |
|
||||
| --get-api-key-link | Show a link in the UI where to direct users to get an API key | `Don't show a link` | LT_GET_API_KEY_LINK |
|
||||
| --require-api-key-origin | Require use of an API key for programmatic access to the API, unless the request origin matches this domain | `No restrictions on domain origin` | LT_REQUIRE_API_KEY_ORIGIN |
|
||||
| --load-only | Set available languages | `all from argostranslate` | LT_LOAD_ONLY |
|
||||
| --threads | Set number of threads | `4` | LT_THREADS |
|
||||
| --suggestions | Allow user suggestions | `false` | LT_SUGGESTIONS |
|
||||
| --disable-files-translation | Disable files translation | `false` | LT_DISABLE_FILES_TRANSLATION |
|
||||
| --disable-web-ui | Disable web ui | `false` | LT_DISABLE_WEB_UI |
|
||||
| --update-models | Update language models at startup | `false` | LT_UPDATE_MODELS |
|
||||
|
||||
Note that each argument has an equivalent environment variable that can be used instead. The env. variables overwrite the default values but have lower priority than the command aguments and are particularly useful if used with Docker. The environment variable names are the upper-snake-case of the equivalent command argument's name with a `LT` prefix.
|
||||
Note that each argument has an equivalent environment variable that can be used instead. The env. variables overwrite the default values but have lower priority than the command arguments and are particularly useful if used with Docker. The environment variable names are the upper-snake-case of the equivalent command argument's name with a `LT` prefix.
|
||||
|
||||
## Update
|
||||
|
||||
### Software
|
||||
|
||||
If you installed with pip:
|
||||
|
||||
`pip install -U libretranslate`
|
||||
|
||||
If you're using docker:
|
||||
|
||||
`docker pull libretranslate/libretranslate`
|
||||
|
||||
### Language Models
|
||||
|
||||
Start the program with the `--update-models` argument. For example: `libretranslate --update-models` or `./run.sh --update-models`.
|
||||
|
||||
Alternatively you can also run the `install_models.py` script.
|
||||
|
||||
## Run with WSGI and Gunicorn
|
||||
|
||||
|
@ -210,7 +240,7 @@ See ["LibreTranslate: your own translation service on Kubernetes" by JM Robles](
|
|||
|
||||
LibreTranslate supports per-user limit quotas, e.g. you can issue API keys to users so that they can enjoy higher requests limits per minute (if you also set `--req-limit`). By default all users are rate-limited based on `--req-limit`, but passing an optional `api_key` parameter to the REST endpoints allows a user to enjoy higher request limits.
|
||||
|
||||
To use API keys simply start LibreTranslate with the `--api-keys` option.
|
||||
To use API keys simply start LibreTranslate with the `--api-keys` option. If you modified the API keys database path with the option `--api-keys-db-path`, you must specify the path with the same argument flag when using the `ltmanage keys` command.
|
||||
|
||||
### Add New Keys
|
||||
|
||||
|
@ -220,6 +250,11 @@ To issue a new API key with 120 requests per minute limits:
|
|||
ltmanage keys add 120
|
||||
```
|
||||
|
||||
If you changed the API keys database path:
|
||||
```bash
|
||||
ltmanage keys --api-keys-db-path path/to/db/dbName.db add 120
|
||||
```
|
||||
|
||||
### Remove Keys
|
||||
|
||||
```bash
|
||||
|
@ -243,8 +278,11 @@ You can use the LibreTranslate API using the following bindings:
|
|||
- Python: https://github.com/argosopentech/LibreTranslate-py
|
||||
- PHP: https://github.com/jefs42/libretranslate
|
||||
- C++: https://github.com/argosopentech/LibreTranslate-cpp
|
||||
- Swift: https://github.com/wacumov/libretranslate
|
||||
- Unix: https://github.com/argosopentech/LibreTranslate-sh
|
||||
|
||||
- Shell: https://github.com/Hayao0819/Hayao-Tools/tree/master/libretranslate-sh
|
||||
- Java: https://github.com/suuft/libretranslate-java
|
||||
-
|
||||
## Discourse Plugin
|
||||
|
||||
You can use this [discourse translator plugin](https://github.com/LibreTranslate/discourse-translator) to translate [Discourse](https://discourse.org) topics. To install it simply modify `/var/discourse/containers/app.yml`:
|
||||
|
@ -267,6 +305,7 @@ Then issue `./launcher rebuild app`. From the Discourse's admin panel then selec
|
|||
## Mobile Apps
|
||||
|
||||
- [LibreTranslater](https://gitlab.com/BeowuIf/libretranslater) is an Android app [available on the Play Store](https://play.google.com/store/apps/details?id=de.beowulf.libretranslater) and [in the F-Droid store](https://f-droid.org/packages/de.beowulf.libretranslater/) that uses the LibreTranslate API.
|
||||
- [LiTranslate](https://github.com/viktorkalyniuk/LiTranslate-iOS) is an iOS app [available on the App Store](https://apps.apple.com/us/app/litranslate/id1644385339) that uses the LibreTranslate API.
|
||||
|
||||
## Web browser
|
||||
- [minbrowser](https://minbrowser.org/) is a web browser with [integrated LibreTranslate support](https://github.com/argosopentech/argos-translate/discussions/158#discussioncomment-1141551).
|
||||
|
@ -276,15 +315,23 @@ Then issue `./launcher rebuild app`. From the Discourse's admin panel then selec
|
|||
|
||||
This is a list of public LibreTranslate instances, some require an API key. If you want to add a new URL, please open a pull request.
|
||||
|
||||
URL |API Key Required | Links
|
||||
--- | --- | ---
|
||||
[libretranslate.com](https://libretranslate.com)|:heavy_check_mark:|[Get API Key](https://portal.libretranslate.com)
|
||||
[libretranslate.de](https://libretranslate.de)|-
|
||||
[translate.argosopentech.com](https://translate.argosopentech.com/)|-
|
||||
[translate.api.skitzen.com](https://translate.api.skitzen.com/)|-
|
||||
[translate.fortytwo-it.com](https://translate.fortytwo-it.com/)|-
|
||||
[translate.terraprint.co](https://translate.terraprint.co/)|-
|
||||
[lt.vern.cc](https://lt.vern.cc)|-
|
||||
|
||||
## TOR/i2p Mirrors
|
||||
|
||||
URL |API Key Required|Payment Link|Cost
|
||||
--- | --- | --- | ---
|
||||
[libretranslate.com](https://libretranslate.com)|:heavy_check_mark:|[Buy](https://buy.stripe.com/3cs4j3a4u4c8d3i289)| [$19 / month](https://buy.stripe.com/3cs4j3a4u4c8d3i289), 80 requests / minute limit
|
||||
[libretranslate.de](https://libretranslate.de)|-|-
|
||||
[translate.argosopentech.com](https://translate.argosopentech.com/)|-|-
|
||||
[translate.api.skitzen.com](https://translate.api.skitzen.com/)|-|-
|
||||
[libretranslate.pussthecat.org](https://libretranslate.pussthecat.org/)|-|-
|
||||
[translate.fortytwo-it.com](https://translate.fortytwo-it.com/)|-|-
|
||||
[translate.terraprint.co](https://translate.terraprint.co/)|-|-
|
||||
[lt.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://lt.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/)|-|-
|
||||
[lt.vern.i2p](http://vernf45n7mxwqnp5riaax7p67pwcl7wcefdcnqqvim7ckdx4264a.b32.i2p/)|-|-
|
||||
|
||||
|
||||
## Adding New Languages
|
||||
|
||||
|
@ -306,11 +353,11 @@ Help us by opening a pull request!
|
|||
|
||||
### Can I use your API server at libretranslate.com for my application in production?
|
||||
|
||||
In short, no. [You need to buy an API key](https://buy.stripe.com/3cs4j3a4u4c8d3i289). You can always run LibreTranslate for free on your own server of course.
|
||||
In short, no. [You need to buy an API key](https://portal.libretranslate.com). You can always run LibreTranslate for free on your own server of course.
|
||||
|
||||
### Can I use LibreTranslate behind a reverse proxy, like Apache2?
|
||||
### Can I use LibreTranslate behind a reverse proxy, like Apache2 or Caddy?
|
||||
|
||||
Yes, here is an example Apache2 config that redirects a subdomain (with HTTPS certificate) to LibreTranslate running on a docker at localhost.
|
||||
Yes, here are config examples for Apache2 and Caddy that redirect a subdomain (with HTTPS certificate) to LibreTranslate running on a docker at localhost.
|
||||
```
|
||||
sudo docker run -ti --rm -p 127.0.0.1:5000:5000 libretranslate/libretranslate
|
||||
```
|
||||
|
@ -360,6 +407,112 @@ Add this to an existing site config, or a new file in `/etc/apache2/sites-availa
|
|||
To get a HTTPS subdomain certificate, install `certbot` (snap), run `sudo certbot certonly --manual --preferred-challenges dns` and enter your information (with `subdomain.domain.tld` as the domain). Add a DNS TXT record with your domain registrar when asked. This will save your certificate and key to `/etc/letsencrypt/live/{subdomain.domain.tld}/`. Alternatively, comment the SSL lines out if you don't want to use HTTPS.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Caddy config</summary>
|
||||
<br>
|
||||
|
||||
Replace [YOUR_DOMAIN] with your full domain; for example, `translate.domain.tld` or `libretranslate.domain.tld`.
|
||||
|
||||
```Caddyfile
|
||||
#Libretranslate
|
||||
[YOUR_DOMAIN] {
|
||||
reverse_proxy localhost:5000
|
||||
}
|
||||
```
|
||||
|
||||
Add this to an existing Caddyfile or save it as `Caddyfile` in any directory and run `sudo caddy reload` in that same directory.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>NGINX config</summary>
|
||||
<br>
|
||||
|
||||
Replace [YOUR_DOMAIN] with your full domain; for example, `translate.domain.tld` or `libretranslate.domain.tld`.
|
||||
|
||||
Remove `#` on the `access_log` and `error_log` lines to disable logging.
|
||||
|
||||
```NginxConf
|
||||
server {
|
||||
listen 80;
|
||||
server_name [YOUR_DOMAIN];
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 http2 ssl;
|
||||
server_name [YOUR_DOMAIN];
|
||||
|
||||
#access_log off;
|
||||
#error_log off;
|
||||
|
||||
# SSL Section
|
||||
ssl_certificate /etc/letsencrypt/live/[YOUR_DOMAIN]/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/[YOUR_DOMAIN]/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
# Using the recommended cipher suite from: https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
||||
|
||||
ssl_session_timeout 10m;
|
||||
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||
ssl_session_tickets off;
|
||||
|
||||
# Specifies a curve for ECDHE ciphers.
|
||||
ssl_ecdh_curve prime256v1;
|
||||
# Server should determine the ciphers, not the client
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
|
||||
# Header section
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
add_header Referrer-Policy "strict-origin" always;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Download-Options "noopen" always;
|
||||
add_header X-Robots-Tag "none" always;
|
||||
|
||||
add_header Feature-Policy "microphone 'none'; camera 'none'; geolocation 'none';" always;
|
||||
# Newer header but not everywhere supported
|
||||
add_header Permissions-Policy "microphone=(), camera=(), geolocation=()" always;
|
||||
|
||||
# Remove X-Powered-By, which is an information leak
|
||||
fastcgi_hide_header X-Powered-By;
|
||||
|
||||
# Do not send nginx server header
|
||||
server_tokens off;
|
||||
|
||||
# GZIP Section
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_types text/xml text/javascript font/ttf font/eot font/otf application/x-javascript application/atom+xml application/javascript application/json application/manifest+json application/rss+xml application/x-web-app-manifest+json application/xhtml+xml application/xml image/svg+xml image/x-icon text/css text/plain;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000/;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
client_max_body_size 0;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Add this to an existing NGINX config or save it as `libretranslate` in the `/etc/nginx/site-enabled` directory and run `sudo nginx -s reload`.
|
||||
|
||||
</details>
|
||||
|
||||
## Credits
|
||||
|
||||
This work is largely possible thanks to [Argos Translate](https://github.com/argosopentech/argos-translate), which powers the translation engine.
|
||||
|
|
|
@ -16,7 +16,8 @@ If you want to report misuse of an LibreTranslate trademark, please contact us v
|
|||
|
||||
## When do I need specific permission to use an LibreTranslate trademark?
|
||||
|
||||
You may do the following without receiving specific permission from LibreTranslate (or its affiliates):
|
||||
Noting that LibreTranslate software combined with, or integrated into, any other software program, including but not limited to automation software for offering LibreTranslate as a cloud service or orchestration software for offering LibreTranslate in containers is considered "modified" LibreTranslate software, you may do the following without receiving specific permission from LibreTranslate (or its affiliates):
|
||||
|
||||
* Use LibreTranslate wordmarks and/or logos in unmodified versions of LibreTranslate programs, products, services and technologies.
|
||||
* Use LibreTranslate wordmarks in text to truthfully refer to and/or link to unmodified LibreTranslate programs, products, services and technologies.
|
||||
* Use LibreTranslate logos in visuals to truthfully refer to and/or to link to the applicable programs, products, services and technologies hosted on LibreTranslate servers.
|
||||
|
|
|
@ -1,13 +1,3 @@
|
|||
import os
|
||||
|
||||
# override polyglot path
|
||||
import polyglot
|
||||
from appdirs import user_data_dir
|
||||
|
||||
polyglot.polyglot_path = os.path.join(
|
||||
user_data_dir(appname="LibreTranslate", appauthor="uav4geo"), "polyglot_data"
|
||||
)
|
||||
|
||||
|
||||
from .main import main
|
||||
from .manage import manage
|
||||
|
|
|
@ -1,13 +1,26 @@
|
|||
import os
|
||||
import sqlite3
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
from expiringdict import ExpiringDict
|
||||
from app.default_values import DEFAULT_ARGUMENTS as DEFARGS
|
||||
|
||||
DEFAULT_DB_PATH = "api_keys.db"
|
||||
DEFAULT_DB_PATH = DEFARGS['API_KEYS_DB_PATH']
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, db_path=DEFAULT_DB_PATH, max_cache_len=1000, max_cache_age=30):
|
||||
# Legacy check - this can be removed at some point in the near future
|
||||
if os.path.isfile("api_keys.db") and not os.path.isfile("db/api_keys.db"):
|
||||
print("Migrating %s to %s" % ("api_keys.db", "db/api_keys.db"))
|
||||
try:
|
||||
os.rename("api_keys.db", "db/api_keys.db")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
db_dir = os.path.dirname(db_path)
|
||||
if not db_dir == "" and not os.path.exists(db_dir):
|
||||
os.makedirs(db_dir)
|
||||
self.db_path = db_path
|
||||
self.cache = ExpiringDict(max_len=max_cache_len, max_age_seconds=max_cache_age)
|
||||
|
||||
|
@ -61,3 +74,27 @@ class Database:
|
|||
def all(self):
|
||||
row = self.c.execute("SELECT api_key, req_limit FROM api_keys")
|
||||
return row.fetchall()
|
||||
|
||||
|
||||
class RemoteDatabase:
|
||||
def __init__(self, url, max_cache_len=1000, max_cache_age=600):
|
||||
self.url = url
|
||||
self.cache = ExpiringDict(max_len=max_cache_len, max_age_seconds=max_cache_age)
|
||||
|
||||
def lookup(self, api_key):
|
||||
req_limit = self.cache.get(api_key)
|
||||
if req_limit is None:
|
||||
try:
|
||||
r = requests.post(self.url, data={'api_key': api_key})
|
||||
res = r.json()
|
||||
except Exception as e:
|
||||
print("Cannot authenticate API key: " + str(e))
|
||||
return None
|
||||
|
||||
if res.get('error', None) is None:
|
||||
req_limit = res.get('req_limit', None)
|
||||
else:
|
||||
req_limit = None
|
||||
self.cache[api_key] = req_limit
|
||||
|
||||
return req_limit
|
||||
|
|
77
app/app.py
77
app/app.py
|
@ -15,9 +15,9 @@ from translatehtml import translate_html
|
|||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app import flood, remove_translated_files, security
|
||||
from app.language import detect_languages, transliterate
|
||||
from app.language import detect_languages, improve_translation_formatting
|
||||
|
||||
from .api_keys import Database
|
||||
from .api_keys import Database, RemoteDatabase
|
||||
from .suggestions import Database as SuggestionsDatabase
|
||||
|
||||
|
||||
|
@ -100,7 +100,7 @@ def get_routes_limits(default_req_limit, daily_req_limit, api_keys_db):
|
|||
def create_app(args):
|
||||
from app.init import boot
|
||||
|
||||
boot(args.load_only)
|
||||
boot(args.load_only, args.update_models)
|
||||
|
||||
from app.language import load_languages
|
||||
|
||||
|
@ -112,6 +112,9 @@ def create_app(args):
|
|||
if not args.disable_files_translation:
|
||||
remove_translated_files.setup(get_upload_dir())
|
||||
languages = load_languages()
|
||||
language_pairs = {}
|
||||
for lang in languages:
|
||||
language_pairs[lang.code] = sorted([l.to_lang.code for l in lang.translations_from])
|
||||
|
||||
# Map userdefined frontend languages to argos language object.
|
||||
if args.frontend_language_source == "auto":
|
||||
|
@ -146,7 +149,12 @@ def create_app(args):
|
|||
api_keys_db = None
|
||||
|
||||
if args.req_limit > 0 or args.api_keys or args.daily_req_limit > 0:
|
||||
api_keys_db = Database() if args.api_keys else None
|
||||
api_keys_db = None
|
||||
if args.api_keys:
|
||||
if args.api_keys_remote:
|
||||
api_keys_db = RemoteDatabase(args.api_keys_remote)
|
||||
else:
|
||||
api_keys_db = Database(args.api_keys_db_path)
|
||||
|
||||
from flask_limiter import Limiter
|
||||
|
||||
|
@ -156,6 +164,7 @@ def create_app(args):
|
|||
default_limits=get_routes_limits(
|
||||
args.req_limit, args.daily_req_limit, api_keys_db
|
||||
),
|
||||
storage_uri=args.req_limit_storage,
|
||||
)
|
||||
else:
|
||||
from .no_limiter import Limiter
|
||||
|
@ -172,9 +181,6 @@ def create_app(args):
|
|||
|
||||
if flood.is_banned(ip):
|
||||
abort(403, description="Too many request limits violations")
|
||||
else:
|
||||
if flood.has_violation(ip):
|
||||
flood.decrease(ip)
|
||||
|
||||
if args.api_keys:
|
||||
ak = get_req_api_key()
|
||||
|
@ -190,9 +196,12 @@ def create_app(args):
|
|||
and api_keys_db.lookup(ak) is None
|
||||
and request.headers.get("Origin") != args.require_api_key_origin
|
||||
):
|
||||
description = "Please contact the server operator to get an API key"
|
||||
if args.get_api_key_link:
|
||||
description = "Visit %s to get an API key" % args.get_api_key_link
|
||||
abort(
|
||||
403,
|
||||
description="Please contact the server operator to obtain an API key",
|
||||
description=description,
|
||||
)
|
||||
|
||||
return f(*a, **kw)
|
||||
|
@ -227,6 +236,7 @@ def create_app(args):
|
|||
gaId=args.ga_id,
|
||||
frontendTimeout=args.frontend_timeout,
|
||||
api_keys=args.api_keys,
|
||||
get_api_key_link=args.get_api_key_link,
|
||||
web_version=os.environ.get("LT_WEB") is not None,
|
||||
version=get_version()
|
||||
)
|
||||
|
@ -262,17 +272,13 @@ def create_app(args):
|
|||
name:
|
||||
type: string
|
||||
description: Human-readable language name (in English)
|
||||
429:
|
||||
description: Slow down
|
||||
schema:
|
||||
id: error-slow-down
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Reason for slow down
|
||||
targets:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Supported target language codes
|
||||
"""
|
||||
return jsonify([{"code": l.code, "name": l.name} for l in languages])
|
||||
return jsonify([{"code": l.code, "name": l.name, "targets": language_pairs.get(l.code, [])} for l in languages])
|
||||
|
||||
# Add cors
|
||||
@app.after_request
|
||||
|
@ -474,32 +480,18 @@ def create_app(args):
|
|||
if text_format not in ["text", "html"]:
|
||||
abort(400, description="%s format is not supported" % text_format)
|
||||
|
||||
def improve_translation(source, translation):
|
||||
if source.islower():
|
||||
return translation.lower()
|
||||
|
||||
if source.isupper():
|
||||
return translation.upper()
|
||||
|
||||
if source[0].islower():
|
||||
return translation[0].lower() + translation[1:]
|
||||
|
||||
if source[0].isupper():
|
||||
return translation[0].upper() + translation[1:]
|
||||
|
||||
return translation
|
||||
|
||||
try:
|
||||
if batch:
|
||||
results = []
|
||||
for idx, text in enumerate(q):
|
||||
translator = src_langs[idx].get_translation(tgt_lang)
|
||||
if translator is None:
|
||||
abort(400, description="%s (%s) is not available as a target language from %s (%s)" % (tgt_lang.name, tgt_lang.code, src_langs[idx].name, src_langs[idx].code))
|
||||
|
||||
if text_format == "html":
|
||||
translated_text = str(translate_html(translator, text))
|
||||
else:
|
||||
translated_text = improve_translation(text, translator.translate(
|
||||
transliterate(text, target_lang=source_langs[idx]["language"])))
|
||||
translated_text = improve_translation_formatting(text, translator.translate(text))
|
||||
|
||||
results.append(unescape(translated_text))
|
||||
if source_lang == "auto":
|
||||
|
@ -511,24 +503,25 @@ def create_app(args):
|
|||
)
|
||||
else:
|
||||
return jsonify(
|
||||
{
|
||||
{
|
||||
"translatedText": results
|
||||
}
|
||||
}
|
||||
)
|
||||
else:
|
||||
translator = src_langs[0].get_translation(tgt_lang)
|
||||
if translator is None:
|
||||
abort(400, description="%s (%s) is not available as a target language from %s (%s)" % (tgt_lang.name, tgt_lang.code, src_langs[0].name, src_langs[0].code))
|
||||
|
||||
if text_format == "html":
|
||||
translated_text = str(translate_html(translator, q))
|
||||
else:
|
||||
translated_text = improve_translation(q, translator.translate(
|
||||
transliterate(q, target_lang=source_langs[0]["language"])))
|
||||
translated_text = improve_translation_formatting(q, translator.translate(q))
|
||||
|
||||
if source_lang == "auto":
|
||||
return jsonify(
|
||||
{
|
||||
"translatedText": unescape(translated_text),
|
||||
"detectedLanguage": source_langs[0]
|
||||
"detectedLanguage": source_langs[0]
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
@ -581,7 +574,7 @@ def create_app(args):
|
|||
200:
|
||||
description: Translated file
|
||||
schema:
|
||||
id: translate
|
||||
id: translate-file
|
||||
type: object
|
||||
properties:
|
||||
translatedFileUrl:
|
||||
|
@ -951,7 +944,7 @@ def create_app(args):
|
|||
return jsonify({"success": True})
|
||||
|
||||
swag = swagger(app)
|
||||
swag["info"]["version"] = "1.3.0"
|
||||
swag["info"]["version"] = "1.3.1"
|
||||
swag["info"]["title"] = "LibreTranslate"
|
||||
|
||||
@app.route("/spec")
|
||||
|
|
|
@ -56,6 +56,11 @@ _default_options_objects = [
|
|||
'default_value': -1,
|
||||
'value_type': 'int'
|
||||
},
|
||||
{
|
||||
'name': 'REQ_LIMIT_STORAGE',
|
||||
'default_value': 'memory://',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'DAILY_REQ_LIMIT',
|
||||
'default_value': -1,
|
||||
|
@ -106,6 +111,21 @@ _default_options_objects = [
|
|||
'default_value': False,
|
||||
'value_type': 'bool'
|
||||
},
|
||||
{
|
||||
'name': 'API_KEYS_DB_PATH',
|
||||
'default_value': 'db/api_keys.db',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'API_KEYS_REMOTE',
|
||||
'default_value': '',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'GET_API_KEY_LINK',
|
||||
'default_value': '',
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'REQUIRE_API_KEY_ORIGIN',
|
||||
'default_value': '',
|
||||
|
@ -116,6 +136,11 @@ _default_options_objects = [
|
|||
'default_value': None,
|
||||
'value_type': 'str'
|
||||
},
|
||||
{
|
||||
'name': 'THREADS',
|
||||
'default_value': 4,
|
||||
'value_type': 'int'
|
||||
},
|
||||
{
|
||||
'name': 'SUGGESTIONS',
|
||||
'default_value': False,
|
||||
|
@ -131,6 +156,11 @@ _default_options_objects = [
|
|||
'default_value': False,
|
||||
'value_type': 'bool'
|
||||
},
|
||||
{
|
||||
'name': 'UPDATE_MODELS',
|
||||
'default_value': False,
|
||||
'value_type': 'bool'
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
|
72
app/detect.py
Normal file
72
app/detect.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
# Originally adapted from https://github.com/aboSamoor/polyglot/blob/master/polyglot/base.py
|
||||
|
||||
import pycld2 as cld2
|
||||
|
||||
class UnknownLanguage(Exception):
|
||||
pass
|
||||
|
||||
class Language(object):
|
||||
def __init__(self, choice):
|
||||
name, code, confidence, bytesize = choice
|
||||
self.code = code
|
||||
self.name = name
|
||||
self.confidence = float(confidence)
|
||||
self.read_bytes = int(bytesize)
|
||||
|
||||
def __str__(self):
|
||||
return ("name: {:<12}code: {:<9}confidence: {:>5.1f} "
|
||||
"read bytes:{:>6}".format(self.name, self.code,
|
||||
self.confidence, self.read_bytes))
|
||||
|
||||
@staticmethod
|
||||
def from_code(code):
|
||||
return Language(("", code, 100, 0))
|
||||
|
||||
|
||||
class Detector(object):
|
||||
""" Detect the language used in a snippet of text."""
|
||||
|
||||
def __init__(self, text, quiet=False):
|
||||
""" Detector of the language used in `text`.
|
||||
Args:
|
||||
text (string): unicode string.
|
||||
"""
|
||||
self.__text = text
|
||||
self.reliable = True
|
||||
"""False if the detector used Best Effort strategy in detection."""
|
||||
self.quiet = quiet
|
||||
"""If true, exceptions will be silenced."""
|
||||
self.detect(text)
|
||||
|
||||
@staticmethod
|
||||
def supported_languages():
|
||||
"""Returns a list of the languages that can be detected by pycld2."""
|
||||
return [name.capitalize() for name,code in cld2.LANGUAGES if not name.startswith("X_")]
|
||||
|
||||
def detect(self, text):
|
||||
"""Decide which language is used to write the text.
|
||||
The method tries first to detect the language with high reliability. If
|
||||
that is not possible, the method switches to best effort strategy.
|
||||
Args:
|
||||
text (string): A snippet of text, the longer it is the more reliable we
|
||||
can detect the language used to write the text.
|
||||
"""
|
||||
reliable, index, top_3_choices = cld2.detect(text, bestEffort=False)
|
||||
|
||||
if not reliable:
|
||||
self.reliable = False
|
||||
reliable, index, top_3_choices = cld2.detect(text, bestEffort=True)
|
||||
|
||||
if not self.quiet:
|
||||
if not reliable:
|
||||
raise UnknownLanguage("Try passing a longer snippet of text")
|
||||
|
||||
self.languages = [Language(x) for x in top_3_choices]
|
||||
self.language = self.languages[0]
|
||||
return self.language
|
||||
|
||||
def __str__(self):
|
||||
text = "Prediction is reliable: {}\n".format(self.reliable)
|
||||
text += u"\n".join(["Language {}: {}".format(i+1, str(l))
|
||||
for i,l in enumerate(self.languages)])
|
||||
return text
|
|
@ -31,7 +31,7 @@ def setup(violations_threshold=100):
|
|||
threshold = violations_threshold
|
||||
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.add_job(func=forgive_banned, trigger="interval", minutes=480)
|
||||
scheduler.add_job(func=forgive_banned, trigger="interval", minutes=30)
|
||||
scheduler.start()
|
||||
|
||||
# Shut down the scheduler when exiting the app
|
||||
|
|
46
app/init.py
46
app/init.py
|
@ -1,15 +1,13 @@
|
|||
from pathlib import Path
|
||||
|
||||
import polyglot
|
||||
from argostranslate import package, translate
|
||||
|
||||
import app.language
|
||||
|
||||
|
||||
def boot(load_only=None):
|
||||
def boot(load_only=None, update_models=False):
|
||||
try:
|
||||
check_and_install_models(load_only_lang_codes=load_only)
|
||||
check_and_install_transliteration()
|
||||
check_and_install_models(force=update_models, load_only_lang_codes=load_only)
|
||||
except Exception as e:
|
||||
print("Cannot update models (normal if you're offline): %s" % str(e))
|
||||
|
||||
|
@ -21,7 +19,7 @@ def check_and_install_models(force=False, load_only_lang_codes=None):
|
|||
package.update_package_index()
|
||||
|
||||
# Load available packages from local package index
|
||||
available_packages = package.load_available_packages()
|
||||
available_packages = package.get_available_packages()
|
||||
print("Found %s models" % len(available_packages))
|
||||
|
||||
if load_only_lang_codes is not None:
|
||||
|
@ -55,40 +53,8 @@ def check_and_install_models(force=False, load_only_lang_codes=None):
|
|||
package.install_from_path(download_path)
|
||||
|
||||
# reload installed languages
|
||||
app.language.languages = translate.load_installed_languages()
|
||||
app.language.languages = translate.get_installed_languages()
|
||||
print(
|
||||
"Loaded support for %s languages (%s models total)!"
|
||||
% (len(translate.load_installed_languages()), len(available_packages))
|
||||
)
|
||||
|
||||
|
||||
def check_and_install_transliteration(force=False):
|
||||
# 'en' is not a supported transliteration language
|
||||
transliteration_languages = [
|
||||
l.code for l in app.language.languages if l.code != "en"
|
||||
]
|
||||
|
||||
# check installed
|
||||
install_needed = []
|
||||
if not force:
|
||||
t_packages_path = Path(polyglot.polyglot_path) / "transliteration2"
|
||||
for lang in transliteration_languages:
|
||||
if not (
|
||||
t_packages_path / lang / f"transliteration.{lang}.tar.bz2"
|
||||
).exists():
|
||||
install_needed.append(lang)
|
||||
else:
|
||||
install_needed = transliteration_languages
|
||||
|
||||
# install the needed transliteration packages
|
||||
if install_needed:
|
||||
print(
|
||||
f"Installing transliteration models for the following languages: {', '.join(install_needed)}"
|
||||
)
|
||||
|
||||
from polyglot.downloader import Downloader
|
||||
|
||||
downloader = Downloader()
|
||||
|
||||
for lang in install_needed:
|
||||
downloader.download(f"transliteration2.{lang}")
|
||||
% (len(translate.get_installed_languages()), len(available_packages))
|
||||
)
|
|
@ -1,8 +1,7 @@
|
|||
import string
|
||||
|
||||
from argostranslate import translate
|
||||
from polyglot.detect.base import Detector, UnknownLanguage
|
||||
from polyglot.transliteration.base import Transliterator
|
||||
from app.detect import Detector, UnknownLanguage
|
||||
|
||||
__languages = None
|
||||
|
||||
|
@ -79,49 +78,40 @@ def detect_languages(text):
|
|||
return [{"confidence": l.confidence, "language": l.code} for l in candidate_langs]
|
||||
|
||||
|
||||
def __transliterate_line(transliterator, line_text):
|
||||
new_text = []
|
||||
def improve_translation_formatting(source, translation, improve_punctuation=True):
|
||||
source = source.strip()
|
||||
|
||||
# transliteration is done word by word
|
||||
for orig_word in line_text.split(" "):
|
||||
# remove any punctuation on the right side
|
||||
r_word = orig_word.rstrip(string.punctuation)
|
||||
r_diff = set(char for char in orig_word) - set(char for char in r_word)
|
||||
# and on the left side
|
||||
l_word = orig_word.lstrip(string.punctuation)
|
||||
l_diff = set(char for char in orig_word) - set(char for char in l_word)
|
||||
if not len(source):
|
||||
return ""
|
||||
|
||||
if not len(translation):
|
||||
return source
|
||||
|
||||
if improve_punctuation:
|
||||
source_last_char = source[len(source) - 1]
|
||||
translation_last_char = translation[len(translation) - 1]
|
||||
|
||||
# the actual transliteration of the word
|
||||
t_word = transliterator.transliterate(orig_word.strip(string.punctuation))
|
||||
punctuation_chars = ['!', '?', '.', ',', ';']
|
||||
if source_last_char in punctuation_chars:
|
||||
if translation_last_char != source_last_char:
|
||||
if translation_last_char in punctuation_chars:
|
||||
translation = translation[:-1]
|
||||
|
||||
# if transliteration fails, default back to the original word
|
||||
if not t_word:
|
||||
t_word = orig_word
|
||||
else:
|
||||
# add back any stripped punctuation
|
||||
if r_diff:
|
||||
t_word = t_word + "".join(r_diff)
|
||||
if l_diff:
|
||||
t_word = "".join(l_diff) + t_word
|
||||
translation += source_last_char
|
||||
elif translation_last_char in punctuation_chars:
|
||||
translation = translation[:-1]
|
||||
|
||||
new_text.append(t_word)
|
||||
if source.islower():
|
||||
return translation.lower()
|
||||
|
||||
# rebuild the text
|
||||
return " ".join(new_text)
|
||||
if source.isupper():
|
||||
return translation.upper()
|
||||
|
||||
if source[0].islower():
|
||||
return translation[0].lower() + translation[1:]
|
||||
|
||||
def transliterate(text, target_lang="en"):
|
||||
# initialize the transliterator from polyglot
|
||||
transliterator = Transliterator(target_lang=target_lang)
|
||||
if source[0].isupper():
|
||||
return translation[0].upper() + translation[1:]
|
||||
|
||||
# check for multiline string
|
||||
if "\n" in text:
|
||||
lines = []
|
||||
# process each line separate
|
||||
for line in text.split("\n"):
|
||||
lines.append(__transliterate_line(transliterator, line))
|
||||
return translation
|
||||
|
||||
# rejoin multiline string
|
||||
return "\n".join(lines)
|
||||
else:
|
||||
return __transliterate_line(transliterator, text)
|
||||
|
|
42
app/main.py
42
app/main.py
|
@ -28,6 +28,13 @@ def get_args():
|
|||
metavar="<number>",
|
||||
help="Set the default maximum number of requests per minute per client (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--req-limit-storage",
|
||||
default=DEFARGS['REQ_LIMIT_STORAGE'],
|
||||
type=str,
|
||||
metavar="<Storage URI>",
|
||||
help="Storage URI to use for request limit data storage. See https://flask-limiter.readthedocs.io/en/stable/configuration.html. (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--daily-req-limit",
|
||||
default=DEFARGS['DAILY_REQ_LIMIT'],
|
||||
|
@ -89,6 +96,24 @@ def get_args():
|
|||
action="store_true",
|
||||
help="Enable API keys database for per-user rate limits lookup",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-keys-db-path",
|
||||
default=DEFARGS['API_KEYS_DB_PATH'],
|
||||
type=str,
|
||||
help="Use a specific path inside the container for the local database. Can be absolute or relative (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-keys-remote",
|
||||
default=DEFARGS['API_KEYS_REMOTE'],
|
||||
type=str,
|
||||
help="Use this remote endpoint to query for valid API keys instead of using the local database",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--get-api-key-link",
|
||||
default=DEFARGS['GET_API_KEY_LINK'],
|
||||
type=str,
|
||||
help="Show a link in the UI where to direct users to get an API key",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--require-api-key-origin",
|
||||
type=str,
|
||||
|
@ -102,6 +127,13 @@ def get_args():
|
|||
metavar="<comma-separated language codes>",
|
||||
help="Set available languages (ar,de,en,es,fr,ga,hi,it,ja,ko,pt,ru,zh)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--threads",
|
||||
default=DEFARGS['THREADS'],
|
||||
type=int,
|
||||
metavar="<number of threads>",
|
||||
help="Set number of threads (%(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--suggestions", default=DEFARGS['SUGGESTIONS'], action="store_true", help="Allow user suggestions"
|
||||
)
|
||||
|
@ -112,7 +144,9 @@ def get_args():
|
|||
parser.add_argument(
|
||||
"--disable-web-ui", default=DEFARGS['DISABLE_WEB_UI'], action="store_true", help="Disable web ui"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--update-models", default=DEFARGS['UPDATE_MODELS'], action="store_true", help="Update language models at startup"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
|
@ -128,11 +162,15 @@ def main():
|
|||
else:
|
||||
from waitress import serve
|
||||
|
||||
url_scheme = "https" if args.ssl else "http"
|
||||
print("Running on %s://%s:%s" % (url_scheme, args.host, args.port))
|
||||
|
||||
serve(
|
||||
app,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
url_scheme="https" if args.ssl else "http",
|
||||
url_scheme=url_scheme,
|
||||
threads=args.threads
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import argparse
|
||||
import os
|
||||
|
||||
from app.api_keys import Database
|
||||
from app.default_values import DEFAULT_ARGUMENTS as DEFARGS
|
||||
|
||||
|
||||
def manage():
|
||||
|
@ -10,6 +12,12 @@ def manage():
|
|||
)
|
||||
|
||||
keys_parser = subparsers.add_parser("keys", help="Manage API keys database")
|
||||
keys_parser.add_argument(
|
||||
"--api-keys-db-path",
|
||||
default=DEFARGS['API_KEYS_DB_PATH'],
|
||||
type=str,
|
||||
help="Use a specific path inside the container for the local database",
|
||||
)
|
||||
keys_subparser = keys_parser.add_subparsers(
|
||||
help="", dest="sub_command", title="Command List"
|
||||
)
|
||||
|
@ -30,7 +38,10 @@ def manage():
|
|||
args = parser.parse_args()
|
||||
|
||||
if args.command == "keys":
|
||||
db = Database()
|
||||
if not os.path.exists(args.api_keys_db_path):
|
||||
print("No such database: %s" % args.api_keys_db_path)
|
||||
exit(1)
|
||||
db = Database(args.api_keys_db_path)
|
||||
if args.sub_command is None:
|
||||
# Print keys
|
||||
keys = db.all()
|
||||
|
|
|
@ -3,11 +3,15 @@
|
|||
background-color: #111 !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.blue.darken-3 {
|
||||
background-color: #1E5DA6 !important;
|
||||
}
|
||||
|
||||
|
||||
/* like in btn-delete-text */
|
||||
.btn-flat {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-switch-type {
|
||||
background-color: #333;
|
||||
|
@ -17,7 +21,6 @@
|
|||
background-color: #444 !important;
|
||||
color: #5CA8FF;
|
||||
}
|
||||
|
||||
.btn-switch-type.active {
|
||||
background-color: #3392FF !important;
|
||||
color: #fff;
|
||||
|
@ -26,19 +29,49 @@
|
|||
background-color: #5CA8FF !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
select {
|
||||
|
||||
.btn-switch-language {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.language-select:after {
|
||||
border: solid #fff;
|
||||
border-width: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* like in textarea */
|
||||
.card-content {
|
||||
border: 1px solid #444 !important;
|
||||
background-color: #222 !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.file-dropzone {
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
select {
|
||||
color: #fff;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
option {
|
||||
color: #fff;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: 1px solid #444 !important;
|
||||
background-color: #222 !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* like in file dropzone */
|
||||
.textarea-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
border: 1px solid #444;
|
||||
background: #222;
|
||||
|
@ -48,14 +81,13 @@
|
|||
color: #fff;
|
||||
text-shadow: 0 1px #000;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #40b5e8;
|
||||
}
|
||||
|
||||
|
||||
.language-css .token.string,
|
||||
.style .token.string,
|
||||
.token.entity,
|
||||
|
@ -64,7 +96,7 @@
|
|||
color: #eecfab;
|
||||
background: hsla(0,0%,15%,.5);
|
||||
}
|
||||
|
||||
|
||||
.token.attr-name,
|
||||
.token.builtin,
|
||||
.token.char,
|
||||
|
@ -73,7 +105,7 @@
|
|||
.token.string {
|
||||
color: #acd25f;
|
||||
}
|
||||
|
||||
|
||||
.token.boolean,
|
||||
.token.constant,
|
||||
.token.deleted,
|
||||
|
@ -83,7 +115,7 @@
|
|||
.token.tag {
|
||||
color: #ff8bcc;
|
||||
}
|
||||
|
||||
|
||||
.token.class-name, .token.function {
|
||||
color: #ff7994;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ select {
|
|||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#app {
|
||||
|
@ -61,14 +61,6 @@ h3.header {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
.language-select select {
|
||||
-moz-appearance: none;
|
||||
text-indent: -2px;
|
||||
margin-right: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.language-select:after {
|
||||
content: "";
|
||||
width: 0.5em;
|
||||
|
|
|
@ -25,6 +25,8 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
translatedText: "",
|
||||
output: "",
|
||||
charactersLimit: -1,
|
||||
|
||||
detectedLangText: "",
|
||||
|
||||
copyTextLabel: "Copy text",
|
||||
|
||||
|
@ -39,14 +41,17 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
filesTranslation: true,
|
||||
frontendTimeout: 500
|
||||
},
|
||||
mounted: function(){
|
||||
var self = this;
|
||||
var requestSettings = new XMLHttpRequest();
|
||||
requestSettings.open('GET', BaseUrl + '/frontend/settings', true);
|
||||
mounted: function() {
|
||||
const self = this;
|
||||
|
||||
requestSettings.onload = function() {
|
||||
const settingsRequest = new XMLHttpRequest();
|
||||
settingsRequest.open("GET", BaseUrl + "/frontend/settings", true);
|
||||
|
||||
const langsRequest = new XMLHttpRequest();
|
||||
langsRequest.open("GET", BaseUrl + "/languages", true);
|
||||
|
||||
settingsRequest.onload = function() {
|
||||
if (this.status >= 200 && this.status < 400) {
|
||||
// Success!
|
||||
self.settings = JSON.parse(this.response);
|
||||
self.sourceLang = self.settings.language.source.code;
|
||||
self.targetLang = self.settings.language.target.code;
|
||||
|
@ -55,74 +60,46 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
self.supportedFilesFormat = self.settings.supportedFilesFormat;
|
||||
self.filesTranslation = self.settings.filesTranslation;
|
||||
self.frontendTimeout = self.settings.frontendTimeout;
|
||||
}else {
|
||||
|
||||
if (langsRequest.response) {
|
||||
handleLangsResponse(self, langsRequest);
|
||||
} else {
|
||||
langsRequest.onload = function() {
|
||||
handleLangsResponse(self, this);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.error = "Cannot load /frontend/settings";
|
||||
self.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
requestSettings.onerror = function() {
|
||||
settingsRequest.onerror = function() {
|
||||
self.error = "Error while calling /frontend/settings";
|
||||
self.loading = false;
|
||||
};
|
||||
|
||||
requestSettings.send();
|
||||
|
||||
var requestLanguages = new XMLHttpRequest();
|
||||
requestLanguages.open('GET', BaseUrl + '/languages', true);
|
||||
|
||||
requestLanguages.onload = function() {
|
||||
if (this.status >= 200 && this.status < 400) {
|
||||
// Success!
|
||||
self.langs = JSON.parse(this.response);
|
||||
self.langs.push({ name: 'Auto Detect (Experimental)', code: 'auto' })
|
||||
if (self.langs.length === 0){
|
||||
self.loading = false;
|
||||
self.error = "No languages available. Did you install the models correctly?"
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceLanguage = self.langs.find(l => l.code === self.getQueryParam('source'))
|
||||
const isSourceAuto = !sourceLanguage && self.getQueryParam('source') === "auto"
|
||||
const targetLanguage = self.langs.find(l => l.code === self.getQueryParam('target'))
|
||||
|
||||
if (sourceLanguage || isSourceAuto) {
|
||||
self.sourceLang = isSourceAuto ? "auto" : sourceLanguage.code
|
||||
}
|
||||
|
||||
if (targetLanguage) {
|
||||
self.targetLang = targetLanguage.code
|
||||
}
|
||||
|
||||
const defaultText = self.getQueryParam('q')
|
||||
|
||||
if(defaultText) {
|
||||
self.inputText = decodeURI(defaultText)
|
||||
}
|
||||
|
||||
self.loading = false;
|
||||
} else {
|
||||
self.error = "Cannot load /languages";
|
||||
self.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
requestLanguages.onerror = function() {
|
||||
langsRequest.onerror = function() {
|
||||
self.error = "Error while calling /languages";
|
||||
self.loading = false;
|
||||
};
|
||||
|
||||
requestLanguages.send();
|
||||
settingsRequest.send();
|
||||
langsRequest.send();
|
||||
},
|
||||
updated: function(){
|
||||
if (this.isSuggesting) return;
|
||||
|
||||
M.FormSelect.init(this.$refs.sourceLangDropdown);
|
||||
M.FormSelect.init(this.$refs.targetLangDropdown);
|
||||
|
||||
|
||||
if (this.$refs.inputTextarea){
|
||||
this.$refs.inputTextarea.focus()
|
||||
|
||||
if (this.inputText === ""){
|
||||
this.$refs.inputTextarea.style.height = this.inputTextareaHeight + "px";
|
||||
this.$refs.translatedTextarea.style.height = this.inputTextareaHeight + "px";
|
||||
}else{
|
||||
} else{
|
||||
this.$refs.inputTextarea.style.height = this.$refs.translatedTextarea.style.height = "1px";
|
||||
this.$refs.inputTextarea.style.height = Math.max(this.inputTextareaHeight, this.$refs.inputTextarea.scrollHeight + 32) + "px";
|
||||
this.$refs.translatedTextarea.style.height = Math.max(this.inputTextareaHeight, this.$refs.translatedTextarea.scrollHeight + 32) + "px";
|
||||
|
@ -136,28 +113,12 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
// Update "selected" attribute (to overcome a vue.js limitation)
|
||||
// but properly display checkmarks on supported browsers.
|
||||
// Also change the <select> width value depending on the <option> length
|
||||
if (this.$refs.sourceLangDropdown){
|
||||
for (var i = 0; i < this.$refs.sourceLangDropdown.children.length; i++){
|
||||
var el = this.$refs.sourceLangDropdown.children[i];
|
||||
if (el.value === this.sourceLang){
|
||||
el.setAttribute('selected', '');
|
||||
this.$refs.sourceLangDropdown.style.width = getTextWidth(el.text) + 24 + 'px';
|
||||
}else{
|
||||
el.removeAttribute('selected');
|
||||
}
|
||||
}
|
||||
if (this.$refs.sourceLangDropdown) {
|
||||
updateSelectedAttribute(this.$refs.sourceLangDropdown, this.sourceLang);
|
||||
}
|
||||
|
||||
if (this.$refs.targetLangDropdown){
|
||||
for (var i = 0; i < this.$refs.targetLangDropdown.children.length; i++){
|
||||
var el = this.$refs.targetLangDropdown.children[i];
|
||||
if (el.value === this.targetLang){
|
||||
el.setAttribute('selected', '');
|
||||
this.$refs.targetLangDropdown.style.width = getTextWidth(el.text) + 24 + 'px';
|
||||
}else{
|
||||
el.removeAttribute('selected');
|
||||
}
|
||||
}
|
||||
if (this.$refs.targetLangDropdown) {
|
||||
updateSelectedAttribute(this.$refs.targetLangDropdown, this.targetLang);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -168,7 +129,8 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
' q: ' + this.$options.filters.escape(this.inputText) + ',',
|
||||
' source: ' + this.$options.filters.escape(this.sourceLang) + ',',
|
||||
' target: ' + this.$options.filters.escape(this.targetLang) + ',',
|
||||
' format: "' + (this.isHtml ? "html" : "text") + '"',
|
||||
' format: "' + (this.isHtml ? "html" : "text") + '",',
|
||||
' api_key: "' + (localStorage.getItem("api_key") || "") + '"',
|
||||
' }),',
|
||||
' headers: { "Content-Type": "application/json" }',
|
||||
'});',
|
||||
|
@ -181,8 +143,16 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
isHtml: function(){
|
||||
return htmlRegex.test(this.inputText);
|
||||
},
|
||||
canSendSuggestion() {
|
||||
canSendSuggestion: function(){
|
||||
return this.translatedText.trim() !== "" && this.translatedText !== this.savedTanslatedText;
|
||||
},
|
||||
targetLangs: function(){
|
||||
if (!this.sourceLang) return this.langs;
|
||||
else{
|
||||
var lang = this.langs.find(l => l.code === this.sourceLang);
|
||||
if (!lang) return this.langs;
|
||||
return lang.targets.map(t => this.langs.find(l => l.code === t));
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
|
@ -201,14 +171,20 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
}
|
||||
},
|
||||
swapLangs: function(e){
|
||||
this.closeSuggestTranslation(e)
|
||||
this.closeSuggestTranslation(e);
|
||||
|
||||
// Make sure that we can swap
|
||||
// by checking that the current target language
|
||||
// has source language as target
|
||||
var tgtLang = this.langs.find(l => l.code === this.targetLang);
|
||||
if (tgtLang.targets.indexOf(this.sourceLang) === -1) return; // Not supported
|
||||
|
||||
var t = this.sourceLang;
|
||||
this.sourceLang = this.targetLang;
|
||||
this.targetLang = t;
|
||||
this.inputText = this.translatedText;
|
||||
this.translatedText = "";
|
||||
this.handleInput();
|
||||
this.handleInput(e);
|
||||
},
|
||||
dismissError: function(){
|
||||
this.error = '';
|
||||
|
@ -233,6 +209,8 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
if (this.timeout) clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
|
||||
this.detectedLangText = "";
|
||||
|
||||
if (this.inputText === ""){
|
||||
this.translatedText = "";
|
||||
this.output = "";
|
||||
|
@ -267,10 +245,14 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
self.translatedText = res.translatedText;
|
||||
self.loadingTranslation = false;
|
||||
self.output = JSON.stringify(res, null, 4);
|
||||
}else{
|
||||
if(self.sourceLang == "auto" && res.detectedLanguage !== undefined){
|
||||
let lang = self.langs.find(l => l.code === res.detectedLanguage.language)
|
||||
self.detectedLangText = ": " + (lang !== undefined ? lang.name : res.detectedLanguage.language) + " (" + res.detectedLanguage.confidence + "%)";
|
||||
}
|
||||
} else{
|
||||
throw new Error(res.error || "Unknown error");
|
||||
}
|
||||
}catch(e){
|
||||
} catch (e) {
|
||||
self.error = e.message;
|
||||
self.loadingTranslation = false;
|
||||
}
|
||||
|
@ -303,11 +285,15 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
this.savedTanslatedText = this.translatedText
|
||||
|
||||
this.isSuggesting = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.translatedTextarea.focus();
|
||||
});
|
||||
},
|
||||
closeSuggestTranslation: function(e) {
|
||||
this.translatedText = this.savedTanslatedText
|
||||
|
||||
e.preventDefault();
|
||||
if(this.isSuggesting) {
|
||||
e.preventDefault();
|
||||
// this.translatedText = this.savedTanslatedText
|
||||
}
|
||||
|
||||
this.isSuggesting = false;
|
||||
},
|
||||
|
@ -331,7 +317,7 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
try{
|
||||
var res = JSON.parse(this.response);
|
||||
if (res.success){
|
||||
M.toast({html: 'Thanks for your correction.'})
|
||||
M.toast({html: 'Thanks for your correction. Note the suggestion will not take effect right away.'})
|
||||
self.closeSuggestTranslation(e)
|
||||
}else{
|
||||
throw new Error(res.error || "Unknown error");
|
||||
|
@ -422,9 +408,63 @@ document.addEventListener('DOMContentLoaded', function(){
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {object} self
|
||||
* @param {XMLHttpRequest} response
|
||||
*/
|
||||
function handleLangsResponse(self, response) {
|
||||
if (response.status >= 200 && response.status < 400) {
|
||||
self.langs = JSON.parse(response.response);
|
||||
|
||||
if (self.langs.length === 0){
|
||||
self.loading = false;
|
||||
self.error = "No languages available. Did you install the models correctly?"
|
||||
return;
|
||||
}
|
||||
|
||||
self.langs.push({ name: "Auto Detect", code: "auto", targets: self.langs.map(l => l.code)})
|
||||
|
||||
const sourceLanguage = self.langs.find(l => l.code === self.getQueryParam("source"))
|
||||
const targetLanguage = self.langs.find(l => l.code === self.getQueryParam("target"))
|
||||
|
||||
if (sourceLanguage) {
|
||||
self.sourceLang = sourceLanguage.code
|
||||
}
|
||||
|
||||
if (targetLanguage) {
|
||||
self.targetLang = targetLanguage.code
|
||||
}
|
||||
|
||||
const defaultText = self.getQueryParam("q")
|
||||
|
||||
if (defaultText) {
|
||||
self.inputText = decodeURI(defaultText)
|
||||
self.handleInput(new Event('none'))
|
||||
}
|
||||
} else {
|
||||
self.error = "Cannot load /languages";
|
||||
}
|
||||
|
||||
self.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} langDropdown
|
||||
* @param {string} lang
|
||||
*/
|
||||
function updateSelectedAttribute(langDropdown, lang) {
|
||||
for (const child of langDropdown.children) {
|
||||
if (child.value === lang){
|
||||
child.setAttribute('selected', '');
|
||||
langDropdown.style.width = getTextWidth(child.text) + 24 + 'px';
|
||||
} else{
|
||||
child.removeAttribute('selected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTextWidth(text) {
|
||||
var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
@ -436,7 +476,9 @@ function getTextWidth(text) {
|
|||
function setApiKey(){
|
||||
var prevKey = localStorage.getItem("api_key") || "";
|
||||
var newKey = "";
|
||||
newKey = window.prompt("Type in your API Key. If you need an API key, contact the server operator.", prevKey);
|
||||
var instructions = "contact the server operator.";
|
||||
if (window.getApiKeyLink) instructions = "press the \"Get API Key\" link."
|
||||
newKey = window.prompt("Type in your API Key. If you need an API key, " + instructions, prevKey);
|
||||
if (newKey === null) newKey = "";
|
||||
|
||||
localStorage.setItem("api_key", newKey);
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
import sqlite3
|
||||
import os
|
||||
|
||||
from expiringdict import ExpiringDict
|
||||
|
||||
DEFAULT_DB_PATH = "suggestions.db"
|
||||
DEFAULT_DB_PATH = "db/suggestions.db"
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, db_path=DEFAULT_DB_PATH, max_cache_len=1000, max_cache_age=30):
|
||||
# Legacy check - this can be removed at some point in the near future
|
||||
if os.path.isfile("suggestions.db") and not os.path.isfile("db/suggestions.db"):
|
||||
print("Migrating %s to %s" % ("suggestions.db", "db/suggestions.db"))
|
||||
try:
|
||||
os.rename("suggestions.db", "db/suggestions.db")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
self.db_path = db_path
|
||||
self.cache = ExpiringDict(max_len=max_cache_len, max_age_seconds=max_cache_age)
|
||||
|
||||
|
|
|
@ -5,33 +5,47 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LibreTranslate - Free and Open Source Machine Translation API</title>
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<meta name="description" content="Free and Open Source Machine Translation API. 100% self-hosted, offline capable and easy to setup. Run your own API server in just a few minutes.">
|
||||
<meta name="keywords" content="translation,api">
|
||||
<meta name="description" content="Free and Open Source Machine Translation API. 100% self-hosted, offline capable and easy to setup. Run your own API server in just a few minutes.">
|
||||
<meta name="keywords" content="translation,api">
|
||||
|
||||
<meta property="og:title" content="LibreTranslate - Free and Open Source Machine Translation API" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://libretranslate.com" />
|
||||
<meta property="og:image" content="https://user-images.githubusercontent.com/1951843/102724116-32a6df00-42db-11eb-8cc0-129ab39cdfb5.png" />
|
||||
<meta property="og:description" name="description" class="swiftype" content="Free and Open Source Machine Translation API. 100% self-hosted, no limits, no ties to proprietary services. Run your own API server in just a few minutes."/>
|
||||
<link rel="preload" href="{{ url_for('static', filename='icon.svg') }}" as="image" />
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/vue@2.js') }}" as="script">
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/materialize.min.js') }}" as="script">
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/prism.min.js') }}" as="script">
|
||||
<link rel="preload" href="{{ url_for('static', filename='js/app.js') }}?v={{ version }}" as="script">
|
||||
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/materialize.min.css') }}" as="style"/>
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/material-icons.css') }}" as="style"/>
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/prism.min.css') }}" as="style"/>
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/main.css') }}?v={{ version }}" as="style"/>
|
||||
<link rel="preload" href="{{ url_for('static', filename='css/dark-theme.css') }}" as="style"/>
|
||||
|
||||
<meta property="og:title" content="LibreTranslate - Free and Open Source Machine Translation API" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://libretranslate.com" />
|
||||
<meta property="og:image" content="https://user-images.githubusercontent.com/1951843/102724116-32a6df00-42db-11eb-8cc0-129ab39cdfb5.png" />
|
||||
<meta property="og:description" name="description" class="swiftype" content="Free and Open Source Machine Translation API. 100% self-hosted, no limits, no ties to proprietary services. Run your own API server in just a few minutes."/>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/vue@2.js') }}"></script>
|
||||
<!-- Compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/materialize.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/material-icons.css') }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/prism.min.css') }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}?v={{ version }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/dark-theme.css') }}" />
|
||||
|
||||
|
||||
{% if gaId %}
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ gaId }}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '{{ gaId }}');
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '{{ gaId }}');
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<!-- Compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/materialize.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/material-icons.css') }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/prism.min.css') }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}?v={{ version }}" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/dark-theme.css') }}" />
|
||||
</head>
|
||||
|
||||
<body class="white">
|
||||
|
@ -41,10 +55,14 @@
|
|||
<button data-target="nav-mobile" class="sidenav-trigger"><i class="material-icons">menu</i></button>
|
||||
<a id="logo-container" href="/" class="brand-logo">
|
||||
<img src="{{ url_for('static', filename='icon.svg') }}" alt="Logo for LibreTranslate" class="logo">
|
||||
<span>LibreTranslate</span>
|
||||
<span>LibreTranslate</span>
|
||||
</a>
|
||||
<ul class="right hide-on-med-and-down">
|
||||
<li><a href="/docs">API Docs</a></li>
|
||||
{% if get_api_key_link %}
|
||||
<li><a href="{{ get_api_key_link }}">Get API Key</a></li>
|
||||
<script>window.getApiKeyLink = "{{ get_api_key_link }}";</script>
|
||||
{% endif %}
|
||||
<li><a href="https://github.com/LibreTranslate/LibreTranslate" rel="noopener noreferrer">GitHub</a></li>
|
||||
{% if api_keys %}
|
||||
<li><a href="javascript:setApiKey()" title="Set API Key"><i class="material-icons">vpn_key</i></a></li>
|
||||
|
@ -53,6 +71,9 @@
|
|||
|
||||
<ul id="nav-mobile" class="sidenav">
|
||||
<li><a href="/docs">API Docs</a></li>
|
||||
{% if get_api_key_link %}
|
||||
<li><a href="{{ get_api_key_link }}">Get API Key</a></li>
|
||||
{% endif %}
|
||||
<li><a href="https://github.com/LibreTranslate/LibreTranslate" rel="noopener noreferrer">GitHub</a></li>
|
||||
{% if api_keys %}
|
||||
<li><a href="javascript:setApiKey()" title="Set API Key"><i class="material-icons">vpn_key</i></a></li>
|
||||
|
@ -107,20 +128,21 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<h3 class="header center">Translation API</h3>
|
||||
<div id="translation-type-btns" class="s12 center" v-if="filesTranslation === true">
|
||||
<button type="button" class="btn btn-switch-type" @click="switchType('text')" :class="{'active': translationType === 'text'}">
|
||||
<i class="material-icons">title</i>
|
||||
<span class="btn-text">Translate Text</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-switch-type" @click="switchType('files')" :class="{'active': translationType === 'files'}">
|
||||
<i class="material-icons">description</i>
|
||||
<span class="btn-text">Translate Files</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="translation-type-btns" class="s12 center" v-if="filesTranslation === true">
|
||||
<button type="button" class="btn btn-switch-type" @click="switchType('text')" :class="{'active': translationType === 'text'}">
|
||||
<i class="material-icons">title</i>
|
||||
<span class="btn-text">Translate Text</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-switch-type" @click="switchType('files')" :class="{'active': translationType === 'files'}">
|
||||
<i class="material-icons">description</i>
|
||||
<span class="btn-text">Translate Files</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="translation-form" class="col s12">
|
||||
<div class="row mb-0">
|
||||
<div class="col s6 language-select">
|
||||
<span>Translate from</span>
|
||||
<span v-if="detectedLangText !== ''">[[ detectedLangText ]]</span>
|
||||
<select class="browser-default" v-model="sourceLang" ref="sourceLangDropdown" @change="handleInput">
|
||||
<template v-for="option in langs">
|
||||
<option :value="option.code">[[ option.name ]]</option>
|
||||
|
@ -134,7 +156,7 @@
|
|||
</a>
|
||||
<span>Translate into</span>
|
||||
<select class="browser-default" v-model="targetLang" ref="targetLangDropdown" @change="handleInput">
|
||||
<template v-for="option in langs">
|
||||
<template v-for="option in targetLangs">
|
||||
<option v-if="option.code !== 'auto'" :value="option.code">[[ option.name ]]</option>
|
||||
</template>
|
||||
</select>
|
||||
|
@ -159,20 +181,20 @@
|
|||
Translated text
|
||||
</label>
|
||||
<textarea id="textarea2" v-model="translatedText" ref="translatedTextarea" dir="auto" v-bind:readonly="suggestions && !isSuggesting"></textarea>
|
||||
<div class="actions">
|
||||
<button v-if="suggestions && !loadingTranslation && inputText.length && !isSuggesting" class="btn-action" @click="suggestTranslation">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>
|
||||
<button v-if="suggestions && !loadingTranslation && inputText.length && isSuggesting" class="btn-action btn-blue" @click="closeSuggestTranslation">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
<button v-if="suggestions && !loadingTranslation && inputText.length && isSuggesting" :disabled="!canSendSuggestion" class="btn-action btn-blue" @click="sendSuggestion">
|
||||
<span>Send</span>
|
||||
</button>
|
||||
<button v-if="!isSuggesting" class="btn-action btn-copy-translated" @click="copyText">
|
||||
<span>[[ copyTextLabel ]]</span> <i class="material-icons">content_copy</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button v-if="suggestions && !loadingTranslation && inputText.length && !isSuggesting" class="btn-action" @click="suggestTranslation">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>
|
||||
<button v-if="suggestions && !loadingTranslation && inputText.length && isSuggesting" class="btn-action btn-blue" @click="closeSuggestTranslation">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
<button v-if="suggestions && !loadingTranslation && inputText.length && isSuggesting" :disabled="!canSendSuggestion" class="btn-action btn-blue" @click="sendSuggestion">
|
||||
<span>Send</span>
|
||||
</button>
|
||||
<button v-if="!isSuggesting" class="btn-action btn-copy-translated" @click="copyText">
|
||||
<span>[[ copyTextLabel ]]</span> <i class="material-icons">content_copy</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="position-relative">
|
||||
<div class="progress translate" v-if="loadingTranslation">
|
||||
<div class="indeterminate"></div>
|
||||
|
@ -180,43 +202,43 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="translationType === 'files'">
|
||||
<div class="file-dropzone">
|
||||
<div v-if="inputFile === false" class="dropzone-content">
|
||||
<span>Supported file formats: [[ supportedFilesFormatFormatted ]]</span>
|
||||
<form action="#">
|
||||
<div class="file-field input-field">
|
||||
<div class="btn">
|
||||
<span>File</span>
|
||||
<input type="file" :accept="supportedFilesFormatFormatted" @change="handleInputFile" ref="fileInputRef">
|
||||
</div>
|
||||
<div class="file-path-wrapper hidden">
|
||||
<input class="file-path validate" type="text">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="inputFile !== false" class="dropzone-content">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="row mb-0">
|
||||
<div class="col s12">
|
||||
[[ inputFile.name ]]
|
||||
<button v-if="loadingFileTranslation !== true" @click="removeFile" class="btn-flat">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="translateFile" v-if="translatedFileUrl === false && loadingFileTranslation === false" class="btn">Translate</button>
|
||||
<a v-if="translatedFileUrl !== false" :href="translatedFileUrl" class="btn">Download</a>
|
||||
<div class="progress" v-if="loadingFileTranslation">
|
||||
<div class="indeterminate"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="translationType === 'files'">
|
||||
<div class="file-dropzone">
|
||||
<div v-if="inputFile === false" class="dropzone-content">
|
||||
<span>Supported file formats: [[ supportedFilesFormatFormatted ]]</span>
|
||||
<form action="#">
|
||||
<div class="file-field input-field">
|
||||
<div class="btn">
|
||||
<span>File</span>
|
||||
<input type="file" :accept="supportedFilesFormatFormatted" @change="handleInputFile" ref="fileInputRef">
|
||||
</div>
|
||||
<div class="file-path-wrapper hidden">
|
||||
<input class="file-path validate" type="text">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="inputFile !== false" class="dropzone-content">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="row mb-0">
|
||||
<div class="col s12">
|
||||
[[ inputFile.name ]]
|
||||
<button v-if="loadingFileTranslation !== true" @click="removeFile" class="btn-flat">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="translateFile" v-if="translatedFileUrl === false && loadingFileTranslation === false" class="btn">Translate</button>
|
||||
<a v-if="translatedFileUrl !== false" :href="translatedFileUrl" class="btn">Download</a>
|
||||
<div class="progress" v-if="loadingFileTranslation">
|
||||
<div class="indeterminate"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -244,24 +266,24 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if web_version %}
|
||||
{% if web_version %}
|
||||
<div class="section no-pad-bot">
|
||||
<div class="container">
|
||||
<div class="row center">
|
||||
<div class="col s12 m12">
|
||||
<h3 class="header">Open Source Machine Translation API</h3>
|
||||
<h4 class="header">100% Self-Hosted. Offline Capable. Easy to Setup.</h4>
|
||||
<div id="download-btn-wrapper">
|
||||
<a id="download-btn" class="waves-effect waves-light btn btn-large teal darken-2" href="https://github.com/LibreTranslate/LibreTranslate" rel="noopener noreferrer">
|
||||
<i class="material-icons">cloud_download</i>
|
||||
<span class="btn-text">Download</span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="download-btn-wrapper">
|
||||
<a id="download-btn" class="waves-effect waves-light btn btn-large teal darken-2" href="https://github.com/LibreTranslate/LibreTranslate" rel="noopener noreferrer">
|
||||
<i class="material-icons">cloud_download</i>
|
||||
<span class="btn-text">Download</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
@ -275,7 +297,7 @@
|
|||
<p><a class="grey-text text-lighten-4" href="/javascript-licenses" rel="jslicense">JavaScript license information</a></p>
|
||||
{% if web_version %}
|
||||
<p>
|
||||
This public API should be used for testing, personal or infrequent use. If you're going to run an application in production, please <a href="https://github.com/LibreTranslate/LibreTranslate" class="grey-text text-lighten-4" rel="noopener noreferrer">host your own server</a> or <a class="grey-text text-lighten-4" href="https://github.com/LibreTranslate/LibreTranslate#mirrors" rel="noopener noreferrer">get an API key</a>.
|
||||
This public API should be used for testing, personal or infrequent use. If you're going to run an application in production, please <a href="https://github.com/LibreTranslate/LibreTranslate" class="grey-text text-lighten-4" rel="noopener noreferrer">host your own server</a> or <a class="grey-text text-lighten-4" href="{{ get_api_key_link if get_api_key_link else 'https://github.com/LibreTranslate/LibreTranslate#mirrors' }}" rel="noopener noreferrer">get an API key</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -295,7 +317,7 @@
|
|||
window.Prism.manual = true;
|
||||
// @license-end
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/prism.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}?v={{ version }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/prism.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}?v={{ version }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
0
db/.gitignore
vendored
Normal file
0
db/.gitignore
vendored
Normal file
18
docker-compose.cuda.yml
Normal file
18
docker-compose.cuda.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
libretranslate-cuda:
|
||||
container_name: libretranslate-cuda
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.cuda
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 5000:5000
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
|
@ -8,4 +8,12 @@ services:
|
|||
ports:
|
||||
- 5000:5000
|
||||
## Uncomment above command and define your args if necessary
|
||||
# command: --ssl --ga-id MY-GA-ID --req-limit 100 --char-limit 500
|
||||
# command: --ssl --ga-id MY-GA-ID --req-limit 100 --char-limit 500
|
||||
## Uncomment this section and the `volumes` section if you want to backup your API keys
|
||||
# environment:
|
||||
# - LT_API_KEYS_DB_PATH=/app/db/api_keys.db # Same result as `db/api_keys.db` or `./db/api_keys.db`
|
||||
# volumes:
|
||||
# - libretranslate_api_keys:/app/db/api_keys.db
|
||||
|
||||
# volumes:
|
||||
# libretranslate_api_keys:
|
45
docker/Dockerfile.cuda
Normal file
45
docker/Dockerfile.cuda
Normal file
|
@ -0,0 +1,45 @@
|
|||
FROM nvidia/cuda:11.2.2-devel-ubuntu20.04
|
||||
|
||||
ENV ARGOS_DEVICE_TYPE cuda
|
||||
ARG with_models=true
|
||||
ARG models=
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get -qqq install --no-install-recommends -y libicu-dev libaspell-dev libcairo2 libcairo2-dev pkg-config gcc g++ python3.8-dev python3-pip libpython3.8-dev\
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt
|
||||
|
||||
RUN apt-get update && apt-get upgrade --assume-yes
|
||||
|
||||
RUN pip3 install --upgrade pip && apt-get remove python3-pip --assume-yes
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN ln -s /usr/bin/python3 /usr/bin/python
|
||||
|
||||
RUN pip3 install torch==1.12.0+cu116 -f https://download.pytorch.org/whl/torch_stable.html
|
||||
|
||||
RUN if [ "$with_models" = "true" ]; then \
|
||||
# install only the dependencies first
|
||||
pip3 install -e .; \
|
||||
# initialize the language models
|
||||
if [ ! -z "$models" ]; then \
|
||||
./install_models.py --load_only_lang_codes "$models"; \
|
||||
else \
|
||||
./install_models.py; \
|
||||
fi \
|
||||
fi
|
||||
|
||||
# Install package from source code
|
||||
RUN pip3 install . \
|
||||
&& pip3 cache purge
|
||||
|
||||
# Depending on your cuda install you may need to uncomment this line to allow the container to access the cuda libraries
|
||||
# See: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#post-installation-actions
|
||||
# ENV LD_LIBRARY_PATH=/usr/local/cuda/lib:/usr/local/cuda/lib64
|
||||
|
||||
EXPOSE 5000
|
||||
ENTRYPOINT [ "libretranslate", "--host", "0.0.0.0" ]
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
import argparse
|
||||
from app.init import check_and_install_models, check_and_install_transliteration
|
||||
from app.init import check_and_install_models
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
|
@ -10,4 +10,3 @@ if __name__ == "__main__":
|
|||
if len(lang_codes) == 0 or lang_codes[0] == '':
|
||||
lang_codes = None
|
||||
check_and_install_models(force=True, load_only_lang_codes=lang_codes)
|
||||
check_and_install_transliteration(force=True)
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
argostranslate==1.7.0
|
||||
Flask==2.1.2
|
||||
Flask==2.2.2
|
||||
flask-swagger==0.2.14
|
||||
flask-swagger-ui==3.36.0
|
||||
Flask-Limiter==2.4.5.1
|
||||
waitress==2.1.1
|
||||
expiringdict==1.2.1
|
||||
pyicu>=2.8
|
||||
pycld2==0.41
|
||||
flask-swagger-ui==4.11.1
|
||||
Flask-Limiter==2.6.3
|
||||
waitress==2.1.2
|
||||
expiringdict==1.2.2
|
||||
LTpycld2==0.42
|
||||
morfessor==2.0.6
|
||||
polyglot==16.7.4
|
||||
appdirs==1.4.4
|
||||
APScheduler==3.9.1
|
||||
translatehtml==1.5.2
|
||||
argos-translate-files==1.0.5
|
||||
argos-translate-files==1.1.0
|
||||
itsdangerous==2.1.2
|
||||
Werkzeug==2.1.2
|
||||
Werkzeug==2.2.2
|
||||
requests==2.28.1
|
||||
redis==4.3.4
|
||||
|
|
41
run.bat
Normal file
41
run.bat
Normal file
|
@ -0,0 +1,41 @@
|
|||
@ECHO OFF
|
||||
|
||||
SETLOCAL
|
||||
|
||||
SET LT_PORT=5000
|
||||
|
||||
:loop
|
||||
IF NOT "%1"=="" (
|
||||
IF "%1"=="--port" (
|
||||
SET LT_PORT=%2
|
||||
SHIFT
|
||||
)
|
||||
IF "%1"=="--help" (
|
||||
echo Usage: run.bat [--port N]
|
||||
echo:
|
||||
echo Run LibreTranslate using docker.
|
||||
echo:
|
||||
GOTO :done
|
||||
)
|
||||
IF "%1"=="--api-keys" (
|
||||
SET DB_VOLUME=-v lt-db:/app/db
|
||||
SHIFT
|
||||
)
|
||||
SHIFT
|
||||
GOTO :loop
|
||||
)
|
||||
|
||||
WHERE /Q docker
|
||||
IF %ERRORLEVEL% NEQ 0 GOTO :install_docker
|
||||
|
||||
docker run -ti --rm -p %LT_PORT%:%LT_PORT% %DB_VOLUME% -v lt-local:/home/libretranslate/.local libretranslate/libretranslate %*
|
||||
|
||||
GOTO :done
|
||||
|
||||
:install_docker
|
||||
ECHO Cannot find docker! Go to https://docs.docker.com/desktop/install/windows-install/ and install docker before running this script (pressing Enter will open the page)
|
||||
pause
|
||||
start "" https://docs.docker.com/desktop/install/windows-install/
|
||||
GOTO :done
|
||||
|
||||
:done
|
89
run.sh
Executable file
89
run.sh
Executable file
|
@ -0,0 +1,89 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
__dirname=$(cd "$(dirname "$0")"; pwd -P)
|
||||
cd "${__dirname}"
|
||||
|
||||
platform="Linux" # Assumed
|
||||
uname=$(uname)
|
||||
case $uname in
|
||||
"Darwin")
|
||||
platform="MacOS / OSX"
|
||||
;;
|
||||
MINGW*)
|
||||
platform="Windows"
|
||||
;;
|
||||
esac
|
||||
|
||||
usage(){
|
||||
echo "Usage: $0 [--port N]"
|
||||
echo
|
||||
echo "Run LibreTranslate using docker."
|
||||
echo
|
||||
exit
|
||||
}
|
||||
|
||||
export LT_PORT=5000
|
||||
|
||||
# Parse args for overrides
|
||||
ARGS=()
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
key="$1"
|
||||
case $key in
|
||||
--port)
|
||||
export LT_PORT="$2"
|
||||
ARGS+=("$1")
|
||||
ARGS+=("$2") # save it in an array for later
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
--debug)
|
||||
export LT_DEBUG=YES
|
||||
ARGS+=("$1")
|
||||
shift # past argument
|
||||
;;
|
||||
--api-keys)
|
||||
export DB_VOLUME="-v lt-db:/app/db"
|
||||
ARGS+=("$1")
|
||||
shift # past argument
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
;;
|
||||
*) # unknown option
|
||||
ARGS+=("$1")
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# $1 = command | $2 = help_text | $3 = install_command (optional)
|
||||
check_command(){
|
||||
hash "$1" 2>/dev/null || not_found=true
|
||||
if [[ $not_found ]]; then
|
||||
check_msg_prefix="Checking for $1... "
|
||||
|
||||
# Can we attempt to install it?
|
||||
if [[ -n "$3" ]]; then
|
||||
echo -e "$check_msg_prefix \033[93mnot found, we'll attempt to install\033[39m"
|
||||
$3 || sudo $3
|
||||
|
||||
# Recurse, but don't pass the install command
|
||||
check_command "$1" "$2"
|
||||
else
|
||||
check_msg_result="\033[91m can't find $1! Check that the program is installed and that you have added the proper path to the program to your PATH environment variable before launching WebODM. If you change your PATH environment variable, remember to close and reopen your terminal. $2\033[39m"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "$check_msg_prefix $check_msg_result"
|
||||
if [[ $not_found ]]; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
environment_check(){
|
||||
check_command "docker" "https://www.docker.com/"
|
||||
}
|
||||
|
||||
environment_check
|
||||
docker run -ti --rm -p $LT_PORT:$LT_PORT $DB_VOLUME -v lt-local:/home/libretranslate/.local libretranslate/libretranslate ${ARGS[@]}
|
46
suggestions-to-jsonl.py
Executable file
46
suggestions-to-jsonl.py
Executable file
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import time
|
||||
import sqlite3
|
||||
import json
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Program to generate JSONL files from a LibreTranslate's suggestions.db")
|
||||
parser.add_argument(
|
||||
"--db",
|
||||
type=str,
|
||||
nargs=1,
|
||||
help="Path to suggestions.db file",
|
||||
default='suggestions.db'
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clear",
|
||||
action='store_true',
|
||||
help="Clear suggestions.db after generation",
|
||||
default=False
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
output_file = str(int(time.time())) + ".jsonl"
|
||||
|
||||
con = sqlite3.connect(args.db, check_same_thread=False)
|
||||
cur = con.cursor()
|
||||
|
||||
with open(output_file, 'w', encoding="utf-8") as f:
|
||||
for row in cur.execute('SELECT q, s, source, target FROM suggestions WHERE source != "auto" ORDER BY source'):
|
||||
q, s, source, target = row
|
||||
obj = {
|
||||
'q': q,
|
||||
's': s,
|
||||
'source': source,
|
||||
'target': target
|
||||
}
|
||||
json.dump(obj, f, ensure_ascii=False)
|
||||
f.write('\n')
|
||||
|
||||
print("Wrote %s" % output_file)
|
||||
|
||||
if args.clear:
|
||||
cur.execute("DELETE FROM suggestions")
|
||||
con.commit()
|
||||
print("Cleared " + args.db)
|
Loading…
Reference in a new issue