datashare-asr-worker 0.6.10__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.
@@ -0,0 +1,21 @@
1
+ .idea/
2
+ # Python
3
+ *.log
4
+ venv
5
+ *.egg-info
6
+ .eggs
7
+ __pycache__
8
+ *.pytest_cache
9
+ *.pyc
10
+ build
11
+ dist
12
+ test/.env
13
+ .cache
14
+ tmp
15
+ ./*.csv
16
+ .DS_Store
17
+
18
+ # VS code
19
+ .vscode
20
+ # Doc
21
+ site
@@ -0,0 +1,83 @@
1
+ FROM python:3.13-slim-trixie AS builder
2
+
3
+ RUN apt update && \
4
+ apt install -y --no-install-recommends build-essential g++ && \
5
+ rm -rf /var/lib/apt/lists/*
6
+
7
+
8
+ FROM python:3.13-slim-trixie AS asr-worker-builder
9
+
10
+ ENV PYTHONUNBUFFERED=1
11
+ ENV UV_HTTP_TIMEOUT=300
12
+ ENV UV_LINK_MODE=copy
13
+ ENV UV_COMPILE_BYTECODE=1
14
+ ENV UV_NO_DEV=1
15
+
16
+ COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /bin/
17
+
18
+ RUN apt-get update && \
19
+ apt-get install -y ffmpeg libavcodec-dev libavformat-dev libavutil-dev
20
+
21
+ WORKDIR /app
22
+
23
+
24
+ FROM asr-worker-builder AS io-worker
25
+ # Install deps first to optimize layer cache
26
+ RUN --mount=type=cache,target=~/.cache/uv \
27
+ --mount=type=bind,source=uv.dist.lock,target=uv.lock \
28
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
29
+ uv sync -v --frozen --no-editable --no-install-project --extra cpu
30
+
31
+ # Then copy code
32
+ ADD uv.dist.lock ./uv.lock
33
+ ADD pyproject.toml README.md ./
34
+ ADD asr_worker ./asr_worker/
35
+ ADD entrypoints/io_worker.sh ./entrypoints/io_worker.sh
36
+
37
+ # Then install service
38
+
39
+ RUN --mount=type=cache,target=~/.cache/uv uv sync -v --frozen --no-editable --extra cpu
40
+ RUN rm -rf ~/.cache/pip
41
+
42
+ ENTRYPOINT ["entrypoints/io_worker.sh"]
43
+
44
+
45
+ FROM asr-worker-builder AS cpu-worker
46
+ # Install deps first to optimize layer cache
47
+ RUN --mount=type=cache,target=~/.cache/uv \
48
+ --mount=type=bind,source=uv.dist.lock,target=uv.lock \
49
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
50
+ uv sync -v --frozen --no-editable --no-install-project --extra preprocessing --extra cpu
51
+
52
+ # Then copy code
53
+ ADD uv.dist.lock ./uv.lock
54
+ ADD pyproject.toml README.md ./
55
+ ADD asr_worker ./asr_worker/
56
+ ADD entrypoints/cpu_worker.sh ./entrypoints/cpu_worker.sh
57
+
58
+ # Then install service
59
+ RUN --mount=type=cache,target=~/.cache/uv uv sync -v --frozen --no-editable --extra preprocessing --extra cpu
60
+ RUN rm -rf ~/.cache/pip
61
+
62
+ ENTRYPOINT ["entrypoints/cpu_worker.sh"]
63
+
64
+
65
+ FROM asr-worker-builder AS inference-gpu-worker
66
+ RUN apt-get update && apt-get install -y build-essential
67
+ # Install deps first to optimize layer cache
68
+ RUN --mount=type=cache,target=~/.cache/uv \
69
+ --mount=type=bind,source=uv.dist.lock,target=uv.lock \
70
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
71
+ uv sync -v --frozen --no-editable --no-install-project --extra inference --extra gpu
72
+
73
+ # Then copy code
74
+ ADD uv.dist.lock ./uv.lock
75
+ ADD pyproject.toml README.md ./
76
+ ADD asr_worker ./asr_worker/
77
+ ADD entrypoints/inference_gpu_worker.sh ./entrypoints/inference_gpu_worker.sh
78
+
79
+ # Then install service
80
+ RUN --mount=type=cache,target=~/.cache/uv uv sync -v --frozen --no-editable --extra inference --extra gpu
81
+ RUN rm -rf ~/.cache/pip
82
+
83
+ ENTRYPOINT ["entrypoints/inference_gpu_worker.sh"]
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: datashare-asr-worker
3
+ Version: 0.6.10
4
+ Summary: ASR Temporal task
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
+ Provides-Extra: cli
9
+ Requires-Dist: caul==0.5.1; extra == 'cli'
10
+ Provides-Extra: cpu
11
+ Requires-Dist: caul==0.5.1; extra == 'cpu'
12
+ Requires-Dist: torch==2.10.0; extra == 'cpu'
13
+ Requires-Dist: torchaudio==2.10.0; extra == 'cpu'
14
+ Requires-Dist: torchcodec==0.10.0; extra == 'cpu'
15
+ Provides-Extra: gpu
16
+ Requires-Dist: cuda-bindings==12.9.4; (sys_platform == 'linux') and extra == 'gpu'
17
+ Requires-Dist: torch==2.10.0+cu129; (sys_platform == 'linux') and extra == 'gpu'
18
+ Requires-Dist: torchaudio==2.10.0+cu129; (sys_platform == 'linux') and extra == 'gpu'
19
+ Requires-Dist: torchcodec==0.10.0+cu129; (sys_platform == 'linux') and extra == 'gpu'
20
+ Provides-Extra: inference
21
+ Requires-Dist: caul[nemo]==0.5.1; extra == 'inference'
22
+ Requires-Dist: kaldialign==0.9.3; extra == 'inference'
23
+ Requires-Dist: ml-dtypes==0.5.4; extra == 'inference'
24
+ Requires-Dist: numpy==2.3.0; extra == 'inference'
25
+ Requires-Dist: pyarrow==20.0.0; extra == 'inference'
26
+ Provides-Extra: preprocessing
27
+ Requires-Dist: caul==0.5.1; extra == 'preprocessing'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ASR Worker for Temporal
31
+
32
+ Module for starting an ASR worker process using Temporal (https://temporal.io)
33
+ task management. Transcription is performed by https://github.com/ICIJ/caul.
34
+ You'll need a Temporal server running somewhere to use the worker, which can
35
+ be passed to `asr_worker.worker.run_asr_worker` through the `target_host`
36
+ parameter.
@@ -0,0 +1,7 @@
1
+ # ASR Worker for Temporal
2
+
3
+ Module for starting an ASR worker process using Temporal (https://temporal.io)
4
+ task management. Transcription is performed by https://github.com/ICIJ/caul.
5
+ You'll need a Temporal server running somewhere to use the worker, which can
6
+ be passed to `asr_worker.worker.run_asr_worker` through the `target_host`
7
+ parameter.
File without changes
@@ -0,0 +1,9 @@
1
+ from .cli import cli_app
2
+
3
+
4
+ def main() -> None:
5
+ cli_app()
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,439 @@
1
+ import asyncio
2
+ import contextlib
3
+ import logging
4
+ import os
5
+ from asyncio import AbstractEventLoop
6
+ from collections.abc import AsyncGenerator, AsyncIterable, Iterable
7
+ from itertools import tee
8
+ from pathlib import Path
9
+ from typing import Any, cast
10
+
11
+ from caul.objects import ASRResult, PreprocessedInput
12
+ from caul.tasks import (
13
+ InferenceRunner,
14
+ ParakeetPostprocessorConfig,
15
+ ParakeetPreprocessorConfig,
16
+ Postprocessor,
17
+ Preprocessor,
18
+ )
19
+ from datashare_python.dependencies import lifespan_worker_config
20
+ from datashare_python.objects import (
21
+ DocArtifact,
22
+ Document,
23
+ DocumentLocation,
24
+ FilesystemDocument,
25
+ )
26
+ from datashare_python.types_ import ProgressRateHandler, RawProgressHandler
27
+ from datashare_python.utils import (
28
+ ActivityWithProgress,
29
+ activity_contextual_id,
30
+ activity_defn,
31
+ debuggable_name,
32
+ safe_dir,
33
+ to_raw_progress,
34
+ write_artifact,
35
+ )
36
+ from icij_common.es import (
37
+ DOC_CONTENT_TYPE,
38
+ DOC_LANGUAGE,
39
+ DOC_METADATA,
40
+ DOC_PATH,
41
+ DOC_ROOT_ID,
42
+ ES_DOCUMENT_TYPE,
43
+ HITS,
44
+ QUERY,
45
+ ESClient,
46
+ and_query,
47
+ has_type,
48
+ )
49
+ from icij_common.iter_utils import async_batches
50
+ from icij_common.pydantic_utils import safe_copy
51
+ from pydantic import TypeAdapter
52
+
53
+ from asr_worker.utils import read_jsonl
54
+
55
+ from .config import ASRWorkerConfig
56
+ from .constants import (
57
+ POSTPROCESS_ACTIVITY,
58
+ PREPROCESS_ACTIVITY,
59
+ RUN_INFERENCE_ACTIVITY,
60
+ SEARCH_AUDIOS_ACTIVITY,
61
+ SUPPORTED_CONTENT_TYPES,
62
+ TRANSCRIPTION_METADATA_KEY,
63
+ TRANSCRIPTION_METADATA_VALUE,
64
+ )
65
+ from .dependencies import lifespan_es_client
66
+ from .objects import InferenceRunnerConfig, Transcription
67
+
68
+ logger = logging.getLogger(__name__)
69
+
70
+ _BASE_WEIGHT = 1.0
71
+ _SEARCH_AUDIOS_WEIGHT = _BASE_WEIGHT * 2
72
+ _PREPROCESS_WEIGHT = 5 * _BASE_WEIGHT
73
+ _INFERENCE_WEIGHT = 10 * _PREPROCESS_WEIGHT
74
+
75
+ _LIST_OF_PATH_ADAPTER = TypeAdapter(list[Path])
76
+ _INFERENCE_CONFIG_TYPE_ADAPTER = TypeAdapter(InferenceRunnerConfig)
77
+
78
+
79
+ class ASRActivities(ActivityWithProgress):
80
+ @activity_defn(name=SEARCH_AUDIOS_ACTIVITY, progress_weight=_SEARCH_AUDIOS_WEIGHT)
81
+ async def search_audio_paths(
82
+ self, project: str, query: dict[str, Any], batch_size: int
83
+ ) -> list[Path]:
84
+ es_client = lifespan_es_client()
85
+ worker_config = cast(ASRWorkerConfig, lifespan_worker_config())
86
+ batch_dir_name = activity_contextual_id()
87
+ workdir = worker_config.workdir
88
+ output_dir = workdir / batch_dir_name
89
+ output_dir.mkdir(parents=True, exist_ok=True)
90
+ batch_paths = [
91
+ p.relative_to(workdir)
92
+ async for p in search_audio_paths_act(
93
+ project,
94
+ es_client,
95
+ query,
96
+ config=worker_config,
97
+ output_dir=output_dir,
98
+ batch_size=batch_size,
99
+ )
100
+ ]
101
+ return batch_paths
102
+
103
+ @activity_defn(name=PREPROCESS_ACTIVITY, progress_weight=_PREPROCESS_WEIGHT)
104
+ def preprocess(
105
+ self, audio_batch: Path, config: ParakeetPreprocessorConfig
106
+ ) -> list[Path]:
107
+ # TODO: this shouldn't be necessary, fix this bug
108
+ worker_config = cast(ASRWorkerConfig, lifespan_worker_config())
109
+ workdir = worker_config.workdir
110
+ # TODO: implement caching
111
+ preprocessor = Preprocessor.from_config(config)
112
+ contextual_id = activity_contextual_id()
113
+ output_dir = workdir / contextual_id
114
+ output_dir.mkdir(parents=True, exist_ok=True)
115
+ audio_batch = workdir / audio_batch
116
+ with preprocessor:
117
+ batch_paths = preprocess_act(
118
+ preprocessor,
119
+ audio_batch,
120
+ worker_config=worker_config,
121
+ output_dir=output_dir,
122
+ )
123
+ batches = [p.relative_to(workdir) for p in batch_paths]
124
+ return batches
125
+
126
+ @activity_defn(name=RUN_INFERENCE_ACTIVITY, progress_weight=_INFERENCE_WEIGHT)
127
+ async def infer(
128
+ self,
129
+ preprocessed_inputs: list[Path],
130
+ project: str,
131
+ config: InferenceRunnerConfig,
132
+ *,
133
+ progress: ProgressRateHandler | None = None,
134
+ ) -> list[Path]:
135
+ # TODO: fix this temporal by, we shouldn't have to reload
136
+ config = _INFERENCE_CONFIG_TYPE_ADAPTER.validate_python(config)
137
+ worker_config = cast(ASRWorkerConfig, lifespan_worker_config())
138
+ workdir = worker_config.workdir
139
+ contextual_id = activity_contextual_id()
140
+ output_dir = workdir / project / contextual_id
141
+ output_dir.mkdir(parents=True, exist_ok=True)
142
+ preprocessed_inputs = _LIST_OF_PATH_ADAPTER.validate_python(preprocessed_inputs)
143
+ preprocessed_inputs = [workdir / p for p in preprocessed_inputs]
144
+ if progress is not None:
145
+ progress = to_raw_progress(progress, max_progress=len(preprocessed_inputs))
146
+ inference_runner = InferenceRunner.from_config(config)
147
+ logger.info("loading model %s", config.model)
148
+ with inference_runner:
149
+ logger.info(
150
+ "model loaded, starting inference on %s audio chunks !",
151
+ len(preprocessed_inputs),
152
+ )
153
+ inference_res = infer_act(
154
+ inference_runner,
155
+ preprocessed_inputs,
156
+ output_dir=output_dir,
157
+ progress=progress,
158
+ )
159
+ inference_res = [p.relative_to(workdir) async for p in inference_res]
160
+ return inference_res
161
+
162
+ @activity_defn(name=POSTPROCESS_ACTIVITY, progress_weight=_BASE_WEIGHT)
163
+ def postprocess(
164
+ self,
165
+ inference_results: list[Path],
166
+ audio_batch: Path,
167
+ config: ParakeetPostprocessorConfig,
168
+ project: str,
169
+ *,
170
+ progress: ProgressRateHandler | None = None,
171
+ ) -> int:
172
+ # TODO: this shouldn't be necessary, fix this bug
173
+ config = ParakeetPostprocessorConfig.model_validate(config)
174
+ worker_config = cast(ASRWorkerConfig, lifespan_worker_config())
175
+ workdir = worker_config.workdir
176
+ audio_batch = workdir / audio_batch
177
+ artifacts_root = worker_config.artifacts_root
178
+ inference_results = _LIST_OF_PATH_ADAPTER.validate_python(inference_results)
179
+ inference_results = (
180
+ ASRResult.model_validate_json((workdir / p).read_text())
181
+ for p in inference_results
182
+ )
183
+ # TODO: implement caching
184
+ postprocessor = Postprocessor.from_config(config)
185
+ with postprocessor:
186
+ docs = (
187
+ FilesystemDocument.model_validate(fs_doc)
188
+ for fs_doc in read_jsonl(audio_batch)
189
+ )
190
+ doc_ids = [doc.id for doc in docs]
191
+ if progress is not None:
192
+ progress = to_raw_progress(progress, max_progress=len(doc_ids))
193
+ return postprocess_act(
194
+ postprocessor,
195
+ inference_results,
196
+ doc_ids,
197
+ project=project,
198
+ artifacts_root=artifacts_root,
199
+ event_loop=self._event_loop,
200
+ progress=progress,
201
+ )
202
+
203
+
204
+ async def search_audio_paths_act(
205
+ project: str,
206
+ es_client: ESClient,
207
+ query: dict[str, Any],
208
+ *,
209
+ config: ASRWorkerConfig,
210
+ output_dir: Path,
211
+ batch_size: int,
212
+ ) -> AsyncIterable[Path]:
213
+ # TODO: supported content types should be args
214
+ docs = _search_audio_paths(
215
+ es_client, project, query, supported_content_types=SUPPORTED_CONTENT_TYPES
216
+ )
217
+ docs = create_symlinks_for_embedded_audios(docs, config)
218
+ async for p in write_audio_batches(docs, output_dir, batch_size):
219
+ yield p
220
+
221
+
222
+ def preprocess_act(
223
+ preprocessor: Preprocessor,
224
+ audio_batch: Path,
225
+ *,
226
+ worker_config: ASRWorkerConfig,
227
+ output_dir: Path,
228
+ ) -> list[Path]:
229
+ docs_root = worker_config.docs_root
230
+ artifacts_root = worker_config.artifacts_root
231
+ workdir = worker_config.workdir
232
+ logger.debug("locating files...")
233
+ audios = (
234
+ FilesystemDocument.model_validate(fs_doc) for fs_doc in read_jsonl(audio_batch)
235
+ )
236
+ audios = (
237
+ fs_doc.locate(
238
+ original_root=docs_root, artifacts_root=artifacts_root, workdir=workdir
239
+ )
240
+ for fs_doc in audios
241
+ )
242
+ audios = (str(a) for a in audios)
243
+ # TODO: implement a caching strategy here, we could avoid processing files
244
+ # which have already been preprocessed
245
+ logger.debug("starting preprocessing...")
246
+ return list(_preprocess(preprocessor, audios, output_dir))
247
+
248
+
249
+ async def infer_act(
250
+ inference_runner: InferenceRunner,
251
+ preprocessed_inputs: list[Path],
252
+ output_dir: Path,
253
+ event_loop: AbstractEventLoop | None = None,
254
+ progress: RawProgressHandler | None = None,
255
+ ) -> AsyncIterable[Path]:
256
+ # Audios paths in the input are relative to the batch file directory
257
+ inputs = (
258
+ [
259
+ _relative_input(PreprocessedInput.model_validate(i), f.parent)
260
+ for i in read_jsonl(f)
261
+ ]
262
+ for f in preprocessed_inputs
263
+ )
264
+ audio_paths, inputs = tee(inputs)
265
+ audio_paths = (i.metadata.preprocessed_file_path for b in audio_paths for i in b)
266
+ # TODO: implement caching
267
+ inference_results = await asyncio.to_thread(
268
+ _transcribe_as_list, inference_runner, list(inputs)
269
+ )
270
+ for res_i, (path, asr_res) in enumerate(
271
+ zip(audio_paths, inference_results, strict=True)
272
+ ):
273
+ filename = f"{debuggable_name(path.name)}-transcript.json"
274
+ transcript_path = output_dir / safe_dir(filename) / filename
275
+ transcript_path.parent.mkdir(parents=True, exist_ok=True)
276
+ logger.debug(
277
+ "run inference for %s, writing result to %s", path, transcript_path
278
+ )
279
+ transcript_path.write_text(asr_res.model_dump_json())
280
+ yield transcript_path
281
+ if progress is not None and event_loop is not None:
282
+ await progress(res_i)
283
+
284
+
285
+ def _transcribe_as_list(
286
+ inference_runner: InferenceRunner, inputs: Iterable[list[PreprocessedInput]]
287
+ ) -> list[ASRResult]:
288
+ return list(inference_runner.process(inputs))
289
+
290
+
291
+ def postprocess_act(
292
+ postprocessor: Postprocessor,
293
+ inference_results: Iterable[ASRResult],
294
+ doc_ids: Iterable[str],
295
+ *,
296
+ artifacts_root: Path,
297
+ project: str,
298
+ event_loop: AbstractEventLoop | None = None,
299
+ progress: ProgressRateHandler | None = None,
300
+ ) -> int:
301
+ transcriptions = postprocessor.process(inference_results)
302
+ # Strict is important here !
303
+ n_docs = 0
304
+ for i, (doc_id, asr_result) in enumerate(zip(doc_ids, transcriptions, strict=True)):
305
+ n_docs += 1
306
+ t_path = write_transcription(
307
+ doc_id, asr_result, artifacts_root=artifacts_root, project=project
308
+ )
309
+ logger.debug("wrote transcription for %s", t_path)
310
+ if progress is not None and event_loop is not None:
311
+ event_loop.run_until_complete(progress(i))
312
+ return n_docs
313
+
314
+
315
+ def _preprocess(
316
+ preprocessor: Preprocessor, audios: Iterable[str], output_dir: Path
317
+ ) -> Iterable[Path]:
318
+ for batch_i, batch in enumerate(
319
+ preprocessor.process(audios, output_dir=output_dir)
320
+ ):
321
+ # TODO: we might to create safe subdirs to avoid creating too many
322
+ # files in the same dir
323
+ batch_file = output_dir / f"{batch_i}.jsonl"
324
+ logger.debug("writing batch to %s", batch_file)
325
+ with batch_file.open("w") as f:
326
+ for processed in batch:
327
+ f.write(processed.model_dump_json() + "\n")
328
+ yield batch_file
329
+
330
+
331
+ def write_transcription(
332
+ doc_id: str, asr_result: ASRResult, *, artifacts_root: Path, project: str
333
+ ) -> Path:
334
+ result = Transcription.from_asr_handler_result(asr_result)
335
+ artifact_bytes = result.model_dump_json().encode()
336
+ artifact = DocArtifact(
337
+ project=project,
338
+ doc_id=doc_id,
339
+ filename=TRANSCRIPTION_METADATA_VALUE,
340
+ metadata_key=TRANSCRIPTION_METADATA_KEY,
341
+ artifact=artifact_bytes,
342
+ )
343
+ # TODO: if transcriptions are too large we could also serialize them
344
+ # as jsonl
345
+ rel_path = write_artifact(artifacts_root, artifact)
346
+ return rel_path
347
+
348
+
349
+ def _relative_input(
350
+ preprocess_input: PreprocessedInput, root: Path
351
+ ) -> PreprocessedInput:
352
+ path = root / preprocess_input.metadata.preprocessed_file_path
353
+ update = {"preprocessed_file_path": path}
354
+ metadata = safe_copy(preprocess_input.metadata, update=update)
355
+ return PreprocessedInput(metadata=metadata) # noqa: F821
356
+
357
+
358
+ async def write_audio_batches(
359
+ docs: AsyncIterable[FilesystemDocument], root: Path, batch_size: int
360
+ ) -> AsyncIterable[Path]:
361
+ batch_id = 0
362
+ async for batch in async_batches(docs, batch_size):
363
+ batch_path = root / f"{batch_id}.txt"
364
+ with batch_path.open("w") as f:
365
+ for fs_doc in batch:
366
+ f.write(f"{fs_doc.model_dump_json()}\n")
367
+ yield batch_path
368
+ batch_id += 1
369
+
370
+
371
+ _DOC_TYPE_QUERY = has_type(type_field="type", type_value=ES_DOCUMENT_TYPE)
372
+ _DOC_CONTENT_SOURCES = [DOC_PATH, DOC_ROOT_ID, DOC_LANGUAGE, DOC_METADATA, DOC_PATH]
373
+
374
+
375
+ async def _search_audio_paths(
376
+ es_client: ESClient,
377
+ project: str,
378
+ query: dict[str, Any],
379
+ supported_content_types: set[str],
380
+ ) -> AsyncGenerator[FilesystemDocument, None]:
381
+ body = _with_audio_content(query, supported_content_types)
382
+ async for page in es_client.poll_search_pages(
383
+ index=project, body=body, sort="_doc:asc", _source_includes=_DOC_CONTENT_SOURCES
384
+ ):
385
+ for hit in page[HITS][HITS]:
386
+ yield Document.from_es(hit).to_filesystem()
387
+
388
+
389
+ def _content_type_query(supported_content_types: set[str]) -> dict[str, Any]:
390
+ content_type_query = {"terms": {DOC_CONTENT_TYPE: sorted(supported_content_types)}}
391
+ doc_type = has_type(type_field="type", type_value=ES_DOCUMENT_TYPE)
392
+ return and_query(content_type_query, doc_type)
393
+
394
+
395
+ def _with_audio_content(
396
+ query: dict[str, Any], supported_content_types: set[str]
397
+ ) -> dict[str, Any]:
398
+ type_query = _content_type_query(supported_content_types)
399
+ if not query:
400
+ return type_query
401
+ return and_query(query, type_query[QUERY])
402
+
403
+
404
+ async def create_symlinks_for_embedded_audios(
405
+ docs: AsyncIterable[FilesystemDocument], config: ASRWorkerConfig
406
+ ) -> AsyncIterable[FilesystemDocument]:
407
+ workdir = config.workdir
408
+ artifacts_root = config.artifacts_root
409
+ symlinks_dir = None
410
+ async for d in docs:
411
+ if d.location == DocumentLocation.ARTIFACTS:
412
+ if symlinks_dir is None:
413
+ symlinks_dir = workdir / d.index / "symlinks"
414
+ symlinks_dir.mkdir(parents=True, exist_ok=True)
415
+ artifact_path = artifacts_root / d.path
416
+ audio_ext = Path(d.resource_name).suffix
417
+ symlink_path = d.path.relative_to(Path(d.index))
418
+ symlink_path = symlinks_dir / f"{symlink_path}{audio_ext}"
419
+ symlink_path.parent.mkdir(parents=True, exist_ok=True)
420
+ with contextlib.suppress(FileExistsError):
421
+ os.symlink(artifact_path, symlink_path)
422
+ symlink = FilesystemDocument(
423
+ path=symlink_path.relative_to(workdir),
424
+ id=d.id,
425
+ location=DocumentLocation.WORKDIR,
426
+ index=d.index,
427
+ resource_name=d.resource_name,
428
+ )
429
+ yield symlink
430
+ else:
431
+ yield d
432
+
433
+
434
+ REGISTRY = [
435
+ ASRActivities.search_audio_paths,
436
+ ASRActivities.preprocess,
437
+ ASRActivities.infer,
438
+ ASRActivities.postprocess,
439
+ ]
@@ -0,0 +1,35 @@
1
+ from typing import Annotated
2
+
3
+ import datashare_python
4
+ import typer
5
+ from datashare_python.cli import pretty_exc_callback, version_callback
6
+ from datashare_python.cli.utils import AsyncTyper
7
+ from icij_common.logging_utils import setup_loggers
8
+
9
+ import asr_worker
10
+
11
+ from .models import models_app
12
+
13
+ cli_app = AsyncTyper(
14
+ context_settings={"help_option_names": ["-h", "--help"]},
15
+ pretty_exceptions_enable=False,
16
+ )
17
+ cli_app.add_typer(models_app)
18
+
19
+
20
+ @cli_app.callback()
21
+ def main(
22
+ version: Annotated[ # noqa: ARG001
23
+ bool | None,
24
+ typer.Option("--version", callback=version_callback, is_eager=True),
25
+ ] = None,
26
+ *,
27
+ pretty_exceptions: Annotated[ # noqa: ARG001
28
+ bool,
29
+ typer.Option(
30
+ "--pretty-exceptions", callback=pretty_exc_callback, is_eager=True
31
+ ),
32
+ ] = False,
33
+ ) -> None:
34
+ """Datashare Python CLI."""
35
+ setup_loggers(["__main__", datashare_python.__name__, asr_worker.__name__])
@@ -0,0 +1,13 @@
1
+ from ..objects import available_models
2
+ from .utils import AsyncTyper
3
+
4
+ _MODELS = "models"
5
+
6
+ _LIST_AVAILABLE_MODELS_HELP = "list available ASR models by language"
7
+
8
+ models_app = AsyncTyper(name=_MODELS)
9
+
10
+
11
+ @models_app.async_command(help=_LIST_AVAILABLE_MODELS_HELP)
12
+ async def list_available() -> None:
13
+ print(available_models().model_dump_json(indent=2))
@@ -0,0 +1,35 @@
1
+ import asyncio
2
+ import concurrent.futures
3
+ import sys
4
+ from collections.abc import Callable
5
+ from functools import wraps
6
+ from typing import Any
7
+
8
+ import typer
9
+
10
+
11
+ class AsyncTyper(typer.Typer):
12
+ def async_command(self, *args, **kwargs) -> Callable[[Callable], Callable]:
13
+ def decorator(async_func: Callable) -> Callable:
14
+ @wraps(async_func)
15
+ def sync_func(*_args, **_kwargs) -> Any:
16
+ res = asyncio.run(async_func(*_args, **_kwargs))
17
+ return res
18
+
19
+ self.command(*args, **kwargs)(sync_func)
20
+ return async_func
21
+
22
+ return decorator
23
+
24
+
25
+ def eprint(*args, **kwargs) -> None:
26
+ print(*args, file=sys.stderr, **kwargs)
27
+
28
+
29
+ def _to_concurrent(
30
+ fut: asyncio.Future, loop: asyncio.AbstractEventLoop
31
+ ) -> concurrent.futures.Future:
32
+ async def wait() -> None:
33
+ await fut
34
+
35
+ return asyncio.run_coroutine_threadsafe(wait(), loop)