datashare-translation-worker 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datashare_translation_worker-0.4.2/.gitignore +21 -0
- datashare_translation_worker-0.4.2/Dockerfile +54 -0
- datashare_translation_worker-0.4.2/PKG-INFO +16 -0
- datashare_translation_worker-0.4.2/README.md +4 -0
- datashare_translation_worker-0.4.2/entrypoints/inference_worker.sh +6 -0
- datashare_translation_worker-0.4.2/entrypoints/io_worker.sh +9 -0
- datashare_translation_worker-0.4.2/pyproject.toml +89 -0
- datashare_translation_worker-0.4.2/translation_worker/__init__.py +0 -0
- datashare_translation_worker-0.4.2/translation_worker/activities.py +438 -0
- datashare_translation_worker-0.4.2/translation_worker/config.py +17 -0
- datashare_translation_worker-0.4.2/translation_worker/constants.py +24 -0
- datashare_translation_worker-0.4.2/translation_worker/core.py +253 -0
- datashare_translation_worker-0.4.2/translation_worker/dependencies.py +16 -0
- datashare_translation_worker-0.4.2/translation_worker/objects.py +46 -0
- datashare_translation_worker-0.4.2/translation_worker/utils.py +19 -0
- datashare_translation_worker-0.4.2/translation_worker/workflows.py +49 -0
- datashare_translation_worker-0.4.2/uv.dist.lock +2158 -0
- datashare_translation_worker-0.4.2/uv.lock +2184 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
FROM python:3.13-slim-trixie AS builder
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
FROM python:3.13-slim-trixie AS translation-worker-builder
|
|
5
|
+
|
|
6
|
+
ENV PYTHONUNBUFFERED=1
|
|
7
|
+
ENV UV_HTTP_TIMEOUT=300
|
|
8
|
+
ENV UV_LINK_MODE=copy
|
|
9
|
+
ENV UV_COMPILE_BYTECODE=1
|
|
10
|
+
ENV UV_NO_DEV=1
|
|
11
|
+
|
|
12
|
+
COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /bin/
|
|
13
|
+
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
FROM translation-worker-builder AS io-worker
|
|
18
|
+
# Install deps first to optimize layer cache
|
|
19
|
+
RUN --mount=type=cache,target=~/.cache/uv \
|
|
20
|
+
--mount=type=bind,source=uv.dist.lock,target=uv.lock \
|
|
21
|
+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
|
22
|
+
uv sync -v --frozen --no-editable --no-install-project
|
|
23
|
+
|
|
24
|
+
# Then copy code
|
|
25
|
+
ADD uv.dist.lock ./uv.lock
|
|
26
|
+
ADD pyproject.toml README.md ./
|
|
27
|
+
ADD translation_worker ./translation_worker/
|
|
28
|
+
ADD entrypoints/io_worker.sh ./entrypoints/io_worker.sh
|
|
29
|
+
|
|
30
|
+
# Then install service
|
|
31
|
+
RUN --mount=type=cache,target=~/.cache/uv uv sync -v --frozen --no-editable
|
|
32
|
+
RUN rm -rf ~/.cache/pip
|
|
33
|
+
|
|
34
|
+
ENTRYPOINT ["entrypoints/io_worker.sh"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
FROM translation-worker-builder AS inference-worker
|
|
38
|
+
# Install deps first to optimize layer cache
|
|
39
|
+
RUN --mount=type=cache,target=~/.cache/uv \
|
|
40
|
+
--mount=type=bind,source=uv.dist.lock,target=uv.lock \
|
|
41
|
+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
|
42
|
+
uv sync -v --frozen --no-editable --no-install-project --extra inference
|
|
43
|
+
|
|
44
|
+
# Then copy code
|
|
45
|
+
ADD uv.dist.lock ./uv.lock
|
|
46
|
+
ADD pyproject.toml README.md ./
|
|
47
|
+
ADD translation_worker ./translation_worker/
|
|
48
|
+
ADD entrypoints/inference_worker.sh ./entrypoints/inference_worker.sh
|
|
49
|
+
|
|
50
|
+
# Then install service
|
|
51
|
+
RUN --mount=type=cache,target=~/.cache/uv uv sync -v --frozen --no-editable --extra inference
|
|
52
|
+
RUN rm -rf ~/.cache/pip
|
|
53
|
+
|
|
54
|
+
ENTRYPOINT ["entrypoints/inference_worker.sh"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datashare-translation-worker
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: Translation worker implementation in Temporal
|
|
5
|
+
Author-email: Clément Doumouro <cdoumouro@icij.org>, Clément Doumouro <clement.doumouro@gmail.com>, Lion Summerbell <lsummerbell@icij.org>
|
|
6
|
+
Requires-Python: <3.13,>=3.11.0
|
|
7
|
+
Requires-Dist: datashare-python~=0.8.4
|
|
8
|
+
Requires-Dist: pydantic-extra-types[pycountry]==2.11.1
|
|
9
|
+
Provides-Extra: inference
|
|
10
|
+
Requires-Dist: argostranslate==1.11.0; extra == 'inference'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Translation worker implementation in Temporal
|
|
14
|
+
|
|
15
|
+
Implements a translation worker for `temporalio` based on the `argostranslate`
|
|
16
|
+
translation library.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "datashare-translation-worker"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Translation worker implementation in Temporal"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Clément Doumouro", email = "cdoumouro@icij.org" },
|
|
7
|
+
{ name = "Clément Doumouro", email = "clement.doumouro@gmail.com" },
|
|
8
|
+
{ name = "Lion Summerbell", email = "lsummerbell@icij.org" }
|
|
9
|
+
]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.11.0, <3.13"
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
"datashare-python~=0.8.4",
|
|
15
|
+
"pydantic-extra-types[pycountry]==2.11.1",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.entry-points."datashare.workflows"]
|
|
19
|
+
workflows = "translation_worker.workflows:WORKFLOWS"
|
|
20
|
+
|
|
21
|
+
[project.entry-points."datashare.activities"]
|
|
22
|
+
activities = "translation_worker.activities:ACTIVITIES"
|
|
23
|
+
|
|
24
|
+
[project.entry-points."datashare.dependencies"]
|
|
25
|
+
dependencies = "translation_worker.dependencies:REGISTRY"
|
|
26
|
+
|
|
27
|
+
[project.entry-points."datashare.worker_config_cls"]
|
|
28
|
+
worker_config_cls = "translation_worker.objects:WORKER_CONFIG_CLS"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
inference = [
|
|
32
|
+
"argostranslate==1.11.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.uv.sources]
|
|
36
|
+
datashare-python = { path = "../../datashare-python", editable = true }
|
|
37
|
+
|
|
38
|
+
[tool.uv]
|
|
39
|
+
package = true
|
|
40
|
+
exclude-dependencies = [
|
|
41
|
+
# TODO: put me back when this is solved: https://github.com/argosopentech/argos-translate/pull/521
|
|
42
|
+
# "torch",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.uv-dynamic-versioning]
|
|
46
|
+
fallback-version = "0.0.0"
|
|
47
|
+
pattern-prefix = "translation-worker-"
|
|
48
|
+
pattern = "default-unprefixed"
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"datashare-python~=0.2",
|
|
53
|
+
"nest-asyncio>=1.6.0",
|
|
54
|
+
"pre-commit>=4.5.1",
|
|
55
|
+
"psutil>=6.1.0",
|
|
56
|
+
"pytest~=8.1",
|
|
57
|
+
"pytest-asyncio~=0.24",
|
|
58
|
+
"pytest-timeout==2.4.0",
|
|
59
|
+
"redis[hiredis]>=5.2.1",
|
|
60
|
+
"ruff==0.15.2",
|
|
61
|
+
"typing-extensions>=4.15.0",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[build-system]
|
|
65
|
+
requires = ["hatchling", "uv-dynamic-versioning"]
|
|
66
|
+
build-backend = "hatchling.build"
|
|
67
|
+
|
|
68
|
+
[tool.hatch.version]
|
|
69
|
+
source = "uv-dynamic-versioning"
|
|
70
|
+
|
|
71
|
+
[tool.hatch.build.targets.wheel]
|
|
72
|
+
packages = ["translation_worker"]
|
|
73
|
+
|
|
74
|
+
[tool.hatch.build.targets.sdist]
|
|
75
|
+
exclude = [
|
|
76
|
+
"tests"
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
asyncio_mode = "auto"
|
|
81
|
+
asyncio_default_fixture_loop_scope = "session"
|
|
82
|
+
markers = [
|
|
83
|
+
"integration",
|
|
84
|
+
"pull",
|
|
85
|
+
]
|
|
86
|
+
log_cli = 1
|
|
87
|
+
log_cli_level = "DEBUG"
|
|
88
|
+
log_file_format = "[%(levelname)s][%(asctime)s.%(msecs)03d][%(name)s]: %(message)s"
|
|
89
|
+
log_file_date_format = "%Y-%m-%d %H:%M:%S"
|
|
File without changes
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Iterable
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from functools import partial
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from aiostream.stream import chain
|
|
10
|
+
from datashare_python.dependencies import lifespan_es_client, lifespan_worker_config
|
|
11
|
+
from datashare_python.objects import Document
|
|
12
|
+
from datashare_python.types_ import ProgressRateHandler
|
|
13
|
+
from datashare_python.utils import ActivityWithProgress, activity_defn, to_raw_progress
|
|
14
|
+
from elasticsearch._async.helpers import async_bulk
|
|
15
|
+
from icij_common.es import (
|
|
16
|
+
BOOL,
|
|
17
|
+
DOC_LANGUAGE,
|
|
18
|
+
HITS,
|
|
19
|
+
ID_,
|
|
20
|
+
QUERY,
|
|
21
|
+
SOURCE,
|
|
22
|
+
TERM,
|
|
23
|
+
ESClient,
|
|
24
|
+
has_id,
|
|
25
|
+
must_not,
|
|
26
|
+
)
|
|
27
|
+
from icij_common.iter_utils import before_and_after, once
|
|
28
|
+
from pydantic_extra_types.language_code import LanguageAlpha2, LanguageName
|
|
29
|
+
|
|
30
|
+
from .constants import BATCHING_DOC_SOURCES, CONTENT_LENGTH, TRANSLATION_DOC_SOURCES
|
|
31
|
+
from .core import (
|
|
32
|
+
Sentencizer,
|
|
33
|
+
TranslationEnsemble,
|
|
34
|
+
get_translation_ensemble,
|
|
35
|
+
has_language,
|
|
36
|
+
translate_as_list,
|
|
37
|
+
)
|
|
38
|
+
from .objects import BatchSentence, TranslationWorkerConfig
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TranslationActivities(ActivityWithProgress):
|
|
44
|
+
@activity_defn(name="translation.worker_config")
|
|
45
|
+
async def translation_worker_config(self) -> TranslationWorkerConfig:
|
|
46
|
+
worker_config = cast(TranslationWorkerConfig, lifespan_worker_config())
|
|
47
|
+
return worker_config
|
|
48
|
+
|
|
49
|
+
@activity_defn(name="translation.create_translation_batches")
|
|
50
|
+
async def create_translation_batches(
|
|
51
|
+
self, project: str, target_language: LanguageAlpha2
|
|
52
|
+
) -> list[tuple[str, list[list[str]]]]:
|
|
53
|
+
es_client = lifespan_es_client()
|
|
54
|
+
worker_config = cast(TranslationWorkerConfig, lifespan_worker_config())
|
|
55
|
+
max_batch_byte_len = worker_config.max_batch_byte_len
|
|
56
|
+
batches = await create_translation_batches(
|
|
57
|
+
project=project,
|
|
58
|
+
target_language=target_language,
|
|
59
|
+
max_batch_byte_len=max_batch_byte_len,
|
|
60
|
+
es_client=es_client,
|
|
61
|
+
)
|
|
62
|
+
return batches
|
|
63
|
+
|
|
64
|
+
@activity_defn(name="translation.translate_docs")
|
|
65
|
+
async def translate_docs(
|
|
66
|
+
self,
|
|
67
|
+
doc_id_batch_with_lang: tuple[str, list[list[str]]],
|
|
68
|
+
target_language: LanguageAlpha2,
|
|
69
|
+
*,
|
|
70
|
+
project: str,
|
|
71
|
+
progress: ProgressRateHandler | None = None,
|
|
72
|
+
) -> int:
|
|
73
|
+
es_client = lifespan_es_client()
|
|
74
|
+
worker_config = cast(TranslationWorkerConfig, lifespan_worker_config())
|
|
75
|
+
n_translated = await translate_docs(
|
|
76
|
+
doc_id_batch_with_lang,
|
|
77
|
+
target_language=target_language,
|
|
78
|
+
project=project,
|
|
79
|
+
es_client=es_client,
|
|
80
|
+
progress=progress,
|
|
81
|
+
worker_config=worker_config,
|
|
82
|
+
)
|
|
83
|
+
return n_translated
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def create_translation_batches(
|
|
87
|
+
*,
|
|
88
|
+
project: str,
|
|
89
|
+
target_language: LanguageAlpha2,
|
|
90
|
+
max_batch_byte_len: int = 1000000,
|
|
91
|
+
es_client: ESClient | None = None,
|
|
92
|
+
) -> list[tuple[str, list[list[str]]]]:
|
|
93
|
+
"""Batch doc ids by language and/or total batch byte length
|
|
94
|
+
|
|
95
|
+
:param project: Project name
|
|
96
|
+
:param target_language: Target language
|
|
97
|
+
:param max_batch_byte_len: Maximum batch byte length
|
|
98
|
+
:param es_client: ES client
|
|
99
|
+
:return: list of batches keyed by language
|
|
100
|
+
"""
|
|
101
|
+
# Retrieve unprocessed docs.
|
|
102
|
+
es_docs = _get_es_docs(
|
|
103
|
+
es_client,
|
|
104
|
+
project,
|
|
105
|
+
target_language_alpha_code=target_language,
|
|
106
|
+
source_includes=BATCHING_DOC_SOURCES,
|
|
107
|
+
)
|
|
108
|
+
all_results = {}
|
|
109
|
+
current_batch = []
|
|
110
|
+
current_batch_byte_len = 0
|
|
111
|
+
|
|
112
|
+
async for es_doc_id_batch in es_docs:
|
|
113
|
+
first_doc = await anext(es_doc_id_batch, None)
|
|
114
|
+
if first_doc is None:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
source_alpha_2 = LanguageName(first_doc[SOURCE][DOC_LANGUAGE].title()).alpha2
|
|
118
|
+
all_results[source_alpha_2] = []
|
|
119
|
+
current_batch.append(first_doc[ID_])
|
|
120
|
+
|
|
121
|
+
async for item in es_doc_id_batch:
|
|
122
|
+
doc_id = item[ID_]
|
|
123
|
+
doc_byte_len = item[SOURCE][CONTENT_LENGTH]
|
|
124
|
+
|
|
125
|
+
if 0 < max_batch_byte_len < current_batch_byte_len + doc_byte_len:
|
|
126
|
+
all_results[source_alpha_2].append(deepcopy(current_batch))
|
|
127
|
+
current_batch = []
|
|
128
|
+
current_batch_byte_len = 0
|
|
129
|
+
|
|
130
|
+
current_batch.append(doc_id)
|
|
131
|
+
current_batch_byte_len += doc_byte_len
|
|
132
|
+
|
|
133
|
+
if len(current_batch) > 0:
|
|
134
|
+
all_results[source_alpha_2].append(deepcopy(current_batch))
|
|
135
|
+
current_batch = []
|
|
136
|
+
current_batch_byte_len = 0
|
|
137
|
+
|
|
138
|
+
return list(all_results.items())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def translate_docs(
|
|
142
|
+
doc_id_batch_with_lang: tuple[str, list[list[str]]],
|
|
143
|
+
target_language: LanguageAlpha2,
|
|
144
|
+
*,
|
|
145
|
+
project: str,
|
|
146
|
+
es_client: ESClient | None = None,
|
|
147
|
+
worker_config: TranslationWorkerConfig | None = None,
|
|
148
|
+
progress: ProgressRateHandler | None = None, # noqa: F821
|
|
149
|
+
) -> int:
|
|
150
|
+
"""Translate sentence batches and reconstruct translations from original
|
|
151
|
+
sentence ordering, inserting them into ES
|
|
152
|
+
|
|
153
|
+
:param doc_id_batch_with_lang: doc_ids keyed by document language alpha code
|
|
154
|
+
:param target_language: Target language alpha2 code
|
|
155
|
+
:param project: Project name
|
|
156
|
+
:param es_client: ES client
|
|
157
|
+
:param progress: ProgressRateHandler
|
|
158
|
+
:param worker_config: worker config
|
|
159
|
+
:return: number of documents translated
|
|
160
|
+
"""
|
|
161
|
+
if worker_config is None:
|
|
162
|
+
worker_config = TranslationWorkerConfig()
|
|
163
|
+
|
|
164
|
+
# TODO: this should not happen
|
|
165
|
+
if not isinstance(worker_config, TranslationWorkerConfig):
|
|
166
|
+
worker_config = TranslationWorkerConfig.model_validate(worker_config)
|
|
167
|
+
|
|
168
|
+
source_language_alpha_code, doc_id_batches = doc_id_batch_with_lang
|
|
169
|
+
|
|
170
|
+
# Get documents
|
|
171
|
+
translation_ensemble = get_translation_ensemble(
|
|
172
|
+
source_language_alpha_code=source_language_alpha_code,
|
|
173
|
+
target_language_alpha_code=target_language,
|
|
174
|
+
device=worker_config.device,
|
|
175
|
+
inter_threads=worker_config.inter_threads,
|
|
176
|
+
intra_threads=worker_config.intra_threads,
|
|
177
|
+
compute_type=worker_config.compute_type,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
all_sentences = []
|
|
181
|
+
all_translations = []
|
|
182
|
+
translation_tasks = []
|
|
183
|
+
|
|
184
|
+
# unit here is a sentence
|
|
185
|
+
seen = 0
|
|
186
|
+
total = 0
|
|
187
|
+
|
|
188
|
+
for doc_id_batch in doc_id_batches:
|
|
189
|
+
sentences_batches = _get_doc_contents_and_split_on_sentences(
|
|
190
|
+
es_client,
|
|
191
|
+
project,
|
|
192
|
+
doc_id_batch,
|
|
193
|
+
translation_ensemble.sentencizer,
|
|
194
|
+
worker_config.batch_size,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Create translation tasks
|
|
198
|
+
async for sentences_batch in sentences_batches:
|
|
199
|
+
n_sentences = len(sentences_batch)
|
|
200
|
+
if not n_sentences:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# Convert the progress to a "raw" progress to update the progress
|
|
204
|
+
# incrementally rather than setting the progress rate
|
|
205
|
+
if progress is not None:
|
|
206
|
+
progress = to_raw_progress(progress, max_progress=n_sentences)
|
|
207
|
+
total += n_sentences
|
|
208
|
+
|
|
209
|
+
# Translate
|
|
210
|
+
translation_tasks.append(
|
|
211
|
+
asyncio.create_task(
|
|
212
|
+
_translate_batch(
|
|
213
|
+
sentences_batch, translation_ensemble, worker_config.beam_size
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
all_sentences += sentences_batch
|
|
219
|
+
|
|
220
|
+
# Run translation tasks
|
|
221
|
+
for task in asyncio.as_completed(translation_tasks):
|
|
222
|
+
translation_batch = await task
|
|
223
|
+
all_translations.extend(translation_batch)
|
|
224
|
+
|
|
225
|
+
seen += len(translation_batch)
|
|
226
|
+
|
|
227
|
+
if progress is not None:
|
|
228
|
+
await progress(int(seen / total))
|
|
229
|
+
|
|
230
|
+
all_translations = await asyncio.gather(*translation_tasks)
|
|
231
|
+
all_translations = [
|
|
232
|
+
translation for batch in all_translations for translation in batch
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
# Reconstruct documents from sentences
|
|
236
|
+
# TODO: separate into a function for testing
|
|
237
|
+
reconstructed_docs = defaultdict(dict)
|
|
238
|
+
|
|
239
|
+
for batch_sentence, translation in zip(
|
|
240
|
+
all_sentences, all_translations, strict=False
|
|
241
|
+
):
|
|
242
|
+
key = batch_sentence.doc_id, batch_sentence.root_document
|
|
243
|
+
reconstructed_docs[key][batch_sentence.sentence_index] = translation
|
|
244
|
+
|
|
245
|
+
# Combine sentences into translations and key with doc_id and root_document
|
|
246
|
+
# for insertion
|
|
247
|
+
translations_with_doc_ids_and_root_doc = []
|
|
248
|
+
|
|
249
|
+
for (doc_id, root_document), sentence_idx_mapping in reconstructed_docs.items():
|
|
250
|
+
ordered_translation = " ".join(
|
|
251
|
+
[translation for (_, translation) in sorted(sentence_idx_mapping.items())]
|
|
252
|
+
)
|
|
253
|
+
seen += len(ordered_translation)
|
|
254
|
+
|
|
255
|
+
translations_with_doc_ids_and_root_doc.append(
|
|
256
|
+
(doc_id, root_document, ordered_translation)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if progress is not None:
|
|
260
|
+
await progress(int(seen / total))
|
|
261
|
+
|
|
262
|
+
await _add_translation(
|
|
263
|
+
es_client,
|
|
264
|
+
translations_with_doc_ids_and_root_doc,
|
|
265
|
+
project,
|
|
266
|
+
target_language_alpha_code=target_language,
|
|
267
|
+
)
|
|
268
|
+
# Return the number of translated documents
|
|
269
|
+
return len(reconstructed_docs)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# async
|
|
273
|
+
async def _get_doc_contents_and_split_on_sentences(
|
|
274
|
+
es_client: ESClient,
|
|
275
|
+
project: str,
|
|
276
|
+
doc_ids: list[str],
|
|
277
|
+
sentencizer: Sentencizer,
|
|
278
|
+
sentence_batch_size: int = 16,
|
|
279
|
+
) -> AsyncGenerator[list[BatchSentence] | None, Any]:
|
|
280
|
+
if len(doc_ids) == 0:
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
batch_gen = _async_query_es(
|
|
284
|
+
es_client,
|
|
285
|
+
project,
|
|
286
|
+
body={QUERY: has_id(doc_ids)},
|
|
287
|
+
source_includes=TRANSLATION_DOC_SOURCES,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
async for batch in _iter_sentences(batch_gen, sentencizer, sentence_batch_size):
|
|
291
|
+
yield batch
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def _iter_sentences(
|
|
295
|
+
doc_iter: AsyncGenerator[dict, None],
|
|
296
|
+
sentencizer: Sentencizer,
|
|
297
|
+
sentence_batch_size: int = 16,
|
|
298
|
+
) -> AsyncGenerator[list[BatchSentence], None]:
|
|
299
|
+
sentence_batch = []
|
|
300
|
+
|
|
301
|
+
async for doc in doc_iter:
|
|
302
|
+
es_doc = Document.from_es(doc)
|
|
303
|
+
sentences = await asyncio.to_thread(sentencizer, es_doc.content)
|
|
304
|
+
for idx, sentence in enumerate(sentences):
|
|
305
|
+
sentence_batch.append(
|
|
306
|
+
BatchSentence(
|
|
307
|
+
doc_id=es_doc.id,
|
|
308
|
+
root_document=es_doc.root_document,
|
|
309
|
+
sentence_index=idx,
|
|
310
|
+
sentence=sentence,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if len(sentence_batch) >= sentence_batch_size:
|
|
315
|
+
yield sentence_batch
|
|
316
|
+
sentence_batch = []
|
|
317
|
+
|
|
318
|
+
if len(sentence_batch) > 0:
|
|
319
|
+
yield sentence_batch
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def _translate_batch(
|
|
323
|
+
sentence_batch: list[BatchSentence],
|
|
324
|
+
translation_ensemble: TranslationEnsemble,
|
|
325
|
+
max_parallel_batches: int = 8,
|
|
326
|
+
beam_size: int = 4,
|
|
327
|
+
) -> list[str]:
|
|
328
|
+
async with asyncio.Semaphore(max_parallel_batches):
|
|
329
|
+
return await asyncio.to_thread(
|
|
330
|
+
translate_as_list, sentence_batch, translation_ensemble, beam_size
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def _get_es_docs(
|
|
335
|
+
es_client: ESClient,
|
|
336
|
+
project: str,
|
|
337
|
+
target_language_alpha_code: str,
|
|
338
|
+
source_includes: list[str],
|
|
339
|
+
) -> AsyncGenerator[AsyncIterator[dict], None]:
|
|
340
|
+
# Get all documents that are not in the target language sorted by language
|
|
341
|
+
docs = _async_query_es(
|
|
342
|
+
es_client,
|
|
343
|
+
project,
|
|
344
|
+
body=_untranslated_query(target_language_alpha_code),
|
|
345
|
+
source_includes=source_includes,
|
|
346
|
+
sort=[f"{DOC_LANGUAGE}:asc", "_doc:asc"],
|
|
347
|
+
)
|
|
348
|
+
while True:
|
|
349
|
+
try:
|
|
350
|
+
next_doc = await anext(aiter(docs))
|
|
351
|
+
except StopAsyncIteration:
|
|
352
|
+
return
|
|
353
|
+
current_language = next_doc[SOURCE][DOC_LANGUAGE]
|
|
354
|
+
|
|
355
|
+
# Consume the iterator until we find a doc with a different language
|
|
356
|
+
language_docs, docs = before_and_after(
|
|
357
|
+
docs, predicate=partial(has_language, language=current_language)
|
|
358
|
+
)
|
|
359
|
+
# Group all docs of same language
|
|
360
|
+
grouped_docs = chain(once(next_doc), language_docs)
|
|
361
|
+
|
|
362
|
+
yield aiter(grouped_docs)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
_SCRIPT_SOURCES = """
|
|
366
|
+
if( !ctx._source.containsKey("content_translated") ) {
|
|
367
|
+
ctx._source.content_translated = new HashMap();
|
|
368
|
+
}
|
|
369
|
+
ctx._source.content_translated[params.language] = params.translation;
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
async def _add_translation(
|
|
374
|
+
es_client: ESClient,
|
|
375
|
+
translations: Iterable[tuple[Document, str]],
|
|
376
|
+
project: str,
|
|
377
|
+
*,
|
|
378
|
+
target_language_alpha_code: str,
|
|
379
|
+
) -> None:
|
|
380
|
+
actions = (
|
|
381
|
+
{
|
|
382
|
+
"_op_type": "update",
|
|
383
|
+
"_index": project,
|
|
384
|
+
"_routing": root_document,
|
|
385
|
+
ID_: doc_id,
|
|
386
|
+
"script": {
|
|
387
|
+
"source": _SCRIPT_SOURCES,
|
|
388
|
+
"lang": "painless",
|
|
389
|
+
"params": {
|
|
390
|
+
"language": target_language_alpha_code,
|
|
391
|
+
"translation": translation,
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
for doc_id, root_document, translation in translations
|
|
396
|
+
)
|
|
397
|
+
await async_bulk(es_client, actions, raise_on_error=True, refresh="wait_for")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _untranslated_query(target_language_alpha_code: str) -> dict:
|
|
401
|
+
query = {
|
|
402
|
+
"query": {
|
|
403
|
+
BOOL: must_not(
|
|
404
|
+
{
|
|
405
|
+
"exists": {
|
|
406
|
+
"field": f"content_translated.{target_language_alpha_code}"
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
{TERM: {DOC_LANGUAGE: target_language_alpha_code}},
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return query
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
async def _async_query_es(
|
|
417
|
+
es_client: ESClient,
|
|
418
|
+
project: str,
|
|
419
|
+
*,
|
|
420
|
+
body: dict,
|
|
421
|
+
source_includes: list[str] = None,
|
|
422
|
+
sort: list[str] = None,
|
|
423
|
+
) -> AsyncGenerator[dict, None]:
|
|
424
|
+
async for res in es_client.poll_search_pages(
|
|
425
|
+
index=project,
|
|
426
|
+
body=body,
|
|
427
|
+
_source_includes=source_includes,
|
|
428
|
+
sort=sort,
|
|
429
|
+
):
|
|
430
|
+
for hit in res[HITS][HITS]:
|
|
431
|
+
yield hit
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
ACTIVITIES = [
|
|
435
|
+
TranslationActivities.translation_worker_config,
|
|
436
|
+
TranslationActivities.create_translation_batches,
|
|
437
|
+
TranslationActivities.translate_docs,
|
|
438
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from datashare_python.config import WorkerConfig
|
|
2
|
+
from pydantic import Field
|
|
3
|
+
|
|
4
|
+
from .constants import TorchDevice
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TranslationWorkerConfig(WorkerConfig):
|
|
8
|
+
device: TorchDevice = Field(default=TorchDevice.CPU, frozen=True)
|
|
9
|
+
|
|
10
|
+
batch_size: int = 16
|
|
11
|
+
max_parallel_batches: int = 8
|
|
12
|
+
max_batch_byte_len: int = 1000000
|
|
13
|
+
# ctranslate2 params
|
|
14
|
+
beam_size: int = 4
|
|
15
|
+
inter_threads: int = 1
|
|
16
|
+
intra_threads: int = 0
|
|
17
|
+
compute_type: str = "auto" # quantization
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
from icij_common.es import DOC_CONTENT, DOC_LANGUAGE, DOC_ROOT_ID
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TaskQueue(StrEnum):
|
|
7
|
+
IO = "translation.io"
|
|
8
|
+
INFERENCE = "translation.inference"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TorchDevice(StrEnum):
|
|
12
|
+
CPU = "cpu"
|
|
13
|
+
GPU = "cuda"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
TRANSLATION_TASK_NAME = "translation"
|
|
17
|
+
|
|
18
|
+
TRANSLATION_WORKER_NAME = "translation-worker"
|
|
19
|
+
TRANSLATION_WORKFLOW_NAME = "translation"
|
|
20
|
+
|
|
21
|
+
CONTENT_LENGTH = "content_length"
|
|
22
|
+
|
|
23
|
+
TRANSLATION_DOC_SOURCES = [DOC_CONTENT, DOC_ROOT_ID, DOC_LANGUAGE]
|
|
24
|
+
BATCHING_DOC_SOURCES = TRANSLATION_DOC_SOURCES[1:] + [CONTENT_LENGTH]
|