Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data/app.drey.Dialect.gschema.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
<key type="s" name="instance-url">
<default>""</default>
</key>
<key type="s" name="engine-name">
<default>""</default>
</key>
<key type="s" name="api-key">
<default>""</default>
</key>
Expand Down
6 changes: 5 additions & 1 deletion dialect/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ def _provider_has_settings(self, name: str):
if not name:
return False

if ProviderFeature.INSTANCES in MODULES[name].features or ProviderFeature.API_KEY in MODULES[name].features:
if (
ProviderFeature.INSTANCES in MODULES[name].features
or ProviderFeature.API_KEY in MODULES[name].features
or ProviderFeature.ENGINES in MODULES[name].features
):
return True

return False
Expand Down
51 changes: 51 additions & 0 deletions dialect/providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class ProviderFeature(Flag):
""" Provider has no features """
INSTANCES = auto()
""" If it supports changing the instance url """
ENGINES = auto()
""" If it supports changing the translation engine model """
API_KEY = auto()
""" If the api key is supported but not necessary """
API_KEY_REQUIRED = auto()
Expand All @@ -40,6 +42,8 @@ class ProviderFeature(Flag):
""" If it supports showing translation pronunciation """
SUGGESTIONS = auto()
""" If it supports sending translation suggestions to the service """
STREAMING = auto()
""" If it supports streaming translation tokens progressively """


class ProviderLangModel(Enum):
Expand Down Expand Up @@ -106,6 +110,7 @@ class BaseProvider:

defaults: ProviderDefaults = {
"instance_url": "",
"engine_name": "",
"api_key": "",
"src_langs": ["en", "fr", "es", "de"],
"dest_langs": ["fr", "es", "de", "en"],
Expand Down Expand Up @@ -149,6 +154,18 @@ async def validate_instance(self, url: str) -> bool:
"""
raise NotImplementedError()

async def validate_engine(self, name: str) -> bool:
"""
Validate a translation engine model name.

Args:
name: The engine/model name to validate.

Returns:
If the engine name is valid and available.
"""
raise NotImplementedError()

async def validate_api_key(self, key: str) -> bool:
"""
Validate an API key.
Expand Down Expand Up @@ -184,6 +201,23 @@ async def translate(self, request: TranslationRequest) -> Translation:
"""
raise NotImplementedError()

async def stream_translate(self, request: TranslationRequest):
"""
Streams translation tokens progressively (async generator).

Only available when ``ProviderFeature.STREAMING`` is in features.

Args:
request: The translation request.

Yields:
str tokens as they arrive.
"""
raise NotImplementedError()
# Make this an async generator
return
yield # noqa

async def suggest(self, text: str, src: str, dest: str, suggestion: str) -> bool:
"""
Sends a translation suggestion to the provider.
Expand Down Expand Up @@ -295,6 +329,10 @@ def lang_aliases(self) -> dict[str, str]:
def supports_instances(self) -> bool:
return ProviderFeature.INSTANCES in self.features

@property
def supports_engines(self) -> bool:
return ProviderFeature.ENGINES in self.features

@property
def supports_api_key(self) -> bool:
return ProviderFeature.API_KEY in self.features
Expand Down Expand Up @@ -340,6 +378,19 @@ def reset_instance_url(self):
"""Resets saved instance url"""
self.instance_url = ""

@property
def engine(self) -> str:
"""Translation engine model name saved on settings"""
return self.settings.engine

@engine.setter
def engine(self, name: str):
self.settings.engine = name

def reset_engine(self):
"""Resets saved translation engine model name"""
self.engine = ""

@property
def api_key(self) -> str:
"""API key saved on settings"""
Expand Down
1 change: 1 addition & 0 deletions dialect/providers/modules/bing.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Provider(SoupProvider):

defaults = {
"instance_url": "",
"engine_name": "",
"api_key": "",
"src_langs": ["en", "fr", "es", "de"],
"dest_langs": ["fr", "es", "de", "en"],
Expand Down
1 change: 1 addition & 0 deletions dialect/providers/modules/deepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Provider(SoupProvider):

defaults = {
"instance_url": "",
"engine_name": "",
"api_key": "",
"src_langs": ["en", "fr", "es", "de"],
"dest_langs": ["fr", "es", "de", "en-US"],
Expand Down
1 change: 1 addition & 0 deletions dialect/providers/modules/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ class Provider(LocalProvider, SoupProvider):

defaults = {
"instance_url": "",
"engine_name": "",
"api_key": "",
"src_langs": ["en", "fr", "es", "de"],
"dest_langs": ["fr", "es", "de", "en"],
Expand Down
1 change: 1 addition & 0 deletions dialect/providers/modules/kagi.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Provider(SoupProvider):

defaults = {
"instance_url": "",
"engine_name": "",
"api_key": "",
"src_langs": ["en", "fr", "es", "de", "ja", "zh"],
"dest_langs": ["fr", "es", "de", "en", "ja", "zh"],
Expand Down
1 change: 1 addition & 0 deletions dialect/providers/modules/libretrans.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Provider(SoupProvider):

defaults = {
"instance_url": "lt.dialectapp.org",
"engine_name": "",
"api_key": "",
"src_langs": ["en", "fr", "es", "de"],
"dest_langs": ["fr", "es", "de", "en"],
Expand Down
1 change: 1 addition & 0 deletions dialect/providers/modules/lingva.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Provider(SoupProvider):

defaults = {
"instance_url": "lingva.dialectapp.org",
"engine_name": "",
"api_key": "",
"src_langs": ["en", "fr", "es", "de"],
"dest_langs": ["fr", "es", "de", "en"],
Expand Down
173 changes: 173 additions & 0 deletions dialect/providers/modules/ollama.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Copyright 2026 Sunur Efe Vural
# SPDX-License-Identifier: GPL-3.0-or-later

import json

from dialect.define import LANGUAGES as ALL_LANGUAGES
from dialect.providers.base import (
ProviderCapability,
ProviderFeature,
TranslationRequest,
)
from dialect.providers.errors import RequestError
from dialect.providers.soup import SoupProvider
from dialect.session import Session

AUTO_PROMPT = (
"You are a professional {dest_lang} ({dest_code}) translator. Your goal is to"
" accurately convey the meaning and nuances of the original text while adhering"
" to {dest_lang} grammar, vocabulary, and cultural sensitivities.\n"
"Produce only the {dest_lang} translation, without any additional explanations or"
" commentary. Please translate the following text into {dest_lang}:\n\n\n"
)

TRANSLATE_PROMPT = (
"You are a professional {src_lang} ({src_code}) to {dest_lang} ({dest_code})"
" translator. Your goal is to accurately convey the meaning and nuances of the"
" original {src_lang} text while adhering to {dest_lang} grammar, vocabulary,"
" and cultural sensitivities.\n"
"Produce only the {dest_lang} translation, without any additional explanations or"
" commentary. Please translate the following {src_lang} text into {dest_lang}:\n\n\n"
)

SUPPORTED_LANGS = [
"zh",
"en",
"fr",
"pt",
"es",
"ja",
"tr",
"ru",
"ar",
"ko",
"th",
"it",
"de",
"vi",
"ms",
"id",
"tl",
"hi",
"pl",
"cs",
"nl",
"km",
"my",
"fa",
"gu",
"ur",
"te",
"mr",
"he",
"bn",
"ta",
"uk",
"bo",
"kk",
"mn",
"ug",
"yue",
]


class Provider(SoupProvider):
name = "ollama"
prettyname = "Ollama"

capabilities = ProviderCapability.TRANSLATION
features = (
ProviderFeature.INSTANCES
| ProviderFeature.ENGINES
| ProviderFeature.DETECTION
| ProviderFeature.API_KEY
| ProviderFeature.STREAMING
)

defaults = {
"instance_url": "localhost:11434/api",
"engine_name": "translategemma",
"api_key": "",
"src_langs": [],
"dest_langs": ["en"],
}

def __init__(self, **kwargs):
super().__init__(**kwargs)

@property
def generate_url(self):
return self.format_url(self.instance_url, "/generate")

@property
def headers(self) -> dict:
if self.api_key:
return {"Authorization": f"Bearer {self.api_key}"}
return {}

async def _fetch_model_names(self, url) -> list[str]:
response = await self.get(self.format_url(url, "/tags"), self.headers, check_common=False)
return [m["name"] for m in response.get("models", [])]

async def validate_instance(self, url):
try:
return bool(await self._fetch_model_names(url))
except Exception:
return False

async def validate_engine(self, name):
try:
available = await self._fetch_model_names(self.instance_url)
engine = name if ":" in name else name + ":latest"
return engine in available
except Exception:
return False

async def init_trans(self):
available = await self._fetch_model_names(self.instance_url)
if not available:
raise RequestError("Ollama instance not reachable or has no models")
engine = self.engine if ":" in self.engine else self.engine + ":latest"
if engine not in available:
raise RequestError(f'Model "{self.engine}" is not available on this Ollama instance')
for code in SUPPORTED_LANGS:
self.add_lang(code, ALL_LANGUAGES.get(code))

def _build_prompt(self, request: TranslationRequest) -> str:
dest_lang = self._languages_names.get(request.dest, request.dest)

if request.src == "auto":
return AUTO_PROMPT.format(dest_lang=dest_lang, dest_code=request.dest) + request.text
else:
src_lang = self._languages_names.get(request.src, request.src)
prompt = TRANSLATE_PROMPT.format(
src_lang=src_lang,
src_code=request.src,
dest_lang=dest_lang,
dest_code=request.dest,
)
return prompt + request.text

async def stream_translate(self, request: TranslationRequest):
from gi.repository import Gio

prompt = self._build_prompt(request)
data = {"model": self.engine, "prompt": prompt, "stream": True}
message = self.create_message("POST", self.generate_url, data, self.headers)

try:
stream = Gio.DataInputStream.new(await Session.get().send_async(message, 0, None))
while True:
line, _ = await stream.read_line_async(0, None)
if not line:
break
obj = json.loads(line)
if token := obj.get("response"):
yield token
if obj.get("done", False):
return
except Exception as exc:
raise RequestError(str(exc)) from exc

def check_known_errors(self, status, data):
pass
1 change: 1 addition & 0 deletions dialect/providers/modules/yandex.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Provider(SoupProvider):

defaults = {
"instance_url": "",
"engine_name": "",
"api_key": "",
"src_langs": ["en", "fr", "es", "de"],
"dest_langs": ["fr", "es", "de", "en"],
Expand Down
10 changes: 10 additions & 0 deletions dialect/providers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

class ProviderDefaults(TypedDict):
instance_url: str
engine_name: str
api_key: str
src_langs: list[str]
dest_langs: list[str]
Expand Down Expand Up @@ -47,6 +48,15 @@ def instance_url(self) -> str:
def instance_url(self, url: str):
self.set_string("instance-url", url)

@property
def engine(self) -> str:
"""Translation engine model name."""
return self.get_string("engine-name") or self.defaults["engine_name"]

@engine.setter
def engine(self, name: str):
self.set_string("engine-name", name)

@property
def api_key(self) -> str:
"""API key."""
Expand Down
Loading