accentuk 0.1.0__py3-none-any.whl
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.
- accentuk/__init__.py +23 -0
- accentuk/assets/NOTICE.md +17 -0
- accentuk/assets/releases.json +128 -0
- accentuk/assets.py +218 -0
- accentuk/cli.py +104 -0
- accentuk/combiner.py +291 -0
- accentuk/engine.py +718 -0
- accentuk/errors.py +27 -0
- accentuk/export.py +403 -0
- accentuk/frontend.py +178 -0
- accentuk/inference.py +344 -0
- accentuk/lexicon.py +110 -0
- accentuk/morphology.py +260 -0
- accentuk/normalizer.py +121 -0
- accentuk/route.py +35 -0
- accentuk/segment.py +166 -0
- accentuk/text.py +158 -0
- accentuk/token_resolver.py +335 -0
- accentuk/triage.py +47 -0
- accentuk/verbalize.py +211 -0
- accentuk-0.1.0.dist-info/METADATA +201 -0
- accentuk-0.1.0.dist-info/RECORD +26 -0
- accentuk-0.1.0.dist-info/WHEEL +4 -0
- accentuk-0.1.0.dist-info/entry_points.txt +2 -0
- accentuk-0.1.0.dist-info/licenses/LICENSE +201 -0
- accentuk-0.1.0.dist-info/licenses/src/accentuk/assets/NOTICE.md +17 -0
accentuk/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""accentuk: Offline Ukrainian stress marking and optional TTS verbalization."""
|
|
2
|
+
|
|
3
|
+
from .engine import Accentor, StressResult
|
|
4
|
+
from .errors import AccentukError, AssetError, NormalizationError, ServingError
|
|
5
|
+
from .frontend import Frontend, FrontendResult
|
|
6
|
+
from .lexicon import Lexicon
|
|
7
|
+
from .text import StressToken
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Accentor",
|
|
12
|
+
"AccentukError",
|
|
13
|
+
"AssetError",
|
|
14
|
+
"Frontend",
|
|
15
|
+
"FrontendResult",
|
|
16
|
+
"Lexicon",
|
|
17
|
+
"NormalizationError",
|
|
18
|
+
"ServingError",
|
|
19
|
+
"StressResult",
|
|
20
|
+
"StressToken",
|
|
21
|
+
"__version__",
|
|
22
|
+
]
|
|
23
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Notices and Attributions
|
|
2
|
+
|
|
3
|
+
`accentuk` incorporates, derives from, or provides access to the following works and datasets:
|
|
4
|
+
|
|
5
|
+
## Models and Checkpoints
|
|
6
|
+
|
|
7
|
+
- **uk-stress-crossencoder**: Developed by aloudreader / vacuum-in under Apache-2.0.
|
|
8
|
+
- **uk-stress-token-classifier**: Developed by aloudreader / vacuum-in under Apache-2.0.
|
|
9
|
+
- **uk-stress-combiner**: Developed by aloudreader / vacuum-in under Apache-2.0.
|
|
10
|
+
- **marian-uk-verbalizer**: Developed by aloudreader / vacuum-in under Apache-2.0.
|
|
11
|
+
- **uk_core_news_trf (3.8.0)**: Trained and published by Explosion AI under the MIT license, based on Ukrainian UD treebanks.
|
|
12
|
+
|
|
13
|
+
## Lexicon and Dictionaries
|
|
14
|
+
|
|
15
|
+
- **ukrainian-word-stress**: Copyright (c) 2018-2024 Bohdan Moseiko, licensed under the MIT License.
|
|
16
|
+
- **Ukrainian Wiktionary**: Data derived from Wikimedia Foundation under CC BY-SA 3.0 / 4.0.
|
|
17
|
+
- **VESUM / pymorphy3-dicts-uk**: Ukrainian morphological dictionary, licensed under GPL / MIT / CC BY-SA as published by the lang-uk project.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"release_version": "0.1.0",
|
|
3
|
+
"dataset_id": 2,
|
|
4
|
+
"inventory_hash": "759baefd94a6d8bc865f0a93ff378542b7a2b65dd58617c3194bdf85746c2769",
|
|
5
|
+
"normalization_version": "ukstress-nfd-v1",
|
|
6
|
+
"schema_version": 1,
|
|
7
|
+
"data_release": {
|
|
8
|
+
"url_base": "https://github.com/vacuum-in/accentuk-api/releases/download/data-v2",
|
|
9
|
+
"files": {
|
|
10
|
+
"lexicon-serving.tar": {
|
|
11
|
+
"sha256": "b94dcdf833bf40938a1fa4c4110964227995a7c4e3818918c2ea15c1dd22d891"
|
|
12
|
+
},
|
|
13
|
+
"lexicon-assets.tar.gz": {
|
|
14
|
+
"sha256": "e65def53d4fc95d95bbd8c18142bd1f58b7af0eba2db3eef79794907cdf18cdc"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"models": {
|
|
19
|
+
"crossencoder": {
|
|
20
|
+
"repo": "aloudreader/uk-stress-crossencoder",
|
|
21
|
+
"revision": "6760529e714ffac7f89f33b8f7001b885a5ebc8f",
|
|
22
|
+
"files": {
|
|
23
|
+
"checkpoint/model.safetensors": "35cb3190904f352680850a507f45540df0ea8544653821b4aee459c693ce23c5",
|
|
24
|
+
"checkpoint/config.json": "fa756aaec2d8c2fc215e1d22070554ed834a8fbda9789a6fcfd961635ec62b49",
|
|
25
|
+
"checkpoint/tokenizer.json": "013ee4d5884a2e49945b7181b918285367b7e2ebcba843106ce22e2635bb4a8a",
|
|
26
|
+
"checkpoint/tokenizer_config.json": "5f3587fd1486c98a01f9c67bff3748fb150d7a3862289769833cb1c6309d76ca",
|
|
27
|
+
"checkpoint/special_tokens_map.json": "38d989b0fdad0fec0c67c14b1f3c8b68184022cf6d4adc5444526ced8653f738",
|
|
28
|
+
"serving_manifest.json": "39593fc90af96c0a48f24586c280b74152a54847af8500d3f7543753874d3684"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"token_classifier": {
|
|
32
|
+
"repo": "aloudreader/uk-stress-token-classifier",
|
|
33
|
+
"revision": "d2795e0c32154ba284248729f35362de135b0d72",
|
|
34
|
+
"files": {
|
|
35
|
+
"model.safetensors": "28607d6d0c99fe184b54f7540ae6abe6d04e6923e06abfacf7d11681f6cea2b6",
|
|
36
|
+
"config.json": "5a8242cc875e21f364b29cedc5d10d6dea6c9adc023acfee64bd34ac50b00088",
|
|
37
|
+
"head.pt": "5dfbb68e4e0caca8a66e853b0b6ab5dc34f7106f3e958b5393febfaba201730c",
|
|
38
|
+
"resolver.json": "4599961ae8f299ed0458a665d8d009d961a35e8c2b0a6c8ed570030e582c7e5f",
|
|
39
|
+
"coverage.json": "a24de1bf1f815a402116fb328d171508676b89d227573d698429387ff657fe41"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"combiner": {
|
|
43
|
+
"repo": "aloudreader/uk-stress-combiner",
|
|
44
|
+
"revision": "f54a4f2f15615b201e26a284882d5ef65c7821c0",
|
|
45
|
+
"files": {
|
|
46
|
+
"model.pt": "ca630edde162a81afcb9d70c5d5a52203758887f0b24a264dc47bb7503a30840",
|
|
47
|
+
"audio_prior.json": "e14cf2c5be46c9d6d4cb7d3d89a9692c96cbe9968318e1ba0ea520221b837727",
|
|
48
|
+
"classifier_profile.json": "b311120d232858d6e93a2189e2e84830bd94d8c8d49db6d037027f9694409a43",
|
|
49
|
+
"readings.jsonl": "0ad426e929e56000c49a294091305e6b2e25bb696b076e92d9899f965cc04d7c"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"spacy_trf": {
|
|
53
|
+
"url": "https://github.com/explosion/spacy-models/releases/download/uk_core_news_trf-3.8.0/uk_core_news_trf-3.8.0-py3-none-any.whl",
|
|
54
|
+
"version": "3.8.0"
|
|
55
|
+
},
|
|
56
|
+
"verbalizer": {
|
|
57
|
+
"repo": "aloudreader/marian-uk-verbalizer",
|
|
58
|
+
"revision": "7726f30947caf3b2882c8e753805f930e67ec858",
|
|
59
|
+
"files": {
|
|
60
|
+
"pytorch_model.bin": "d221528b5a001bec01834ae053da5b9327d2812edb36497185ab56d12849b06c",
|
|
61
|
+
"config.json": "d83a1a7a284a4391fdf9d359be5191e01be04007461a54cea2321eca6921344b",
|
|
62
|
+
"source.spm": "de8d503a8c321617ea0ba65b82539e50fd255dda0e5e6e5fc57bed5a55dfbd58",
|
|
63
|
+
"target.spm": "de8d503a8c321617ea0ba65b82539e50fd255dda0e5e6e5fc57bed5a55dfbd58",
|
|
64
|
+
"vocab.json": "0f11e1d7404edd5176fe1aa82561207fcbc489adac41b7f5b957feecbc1c9cc2"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"lexicon_bundle": {
|
|
69
|
+
"url": "https://github.com/vacuum-in/accentuk/releases/download/lexicon-v2/accentuk-lexicon-v2.tar.gz",
|
|
70
|
+
"sha256": "73b068ab20d2884cbe81a479ec62a2a98d093b026bda5e4a61e3605816004220"
|
|
71
|
+
},
|
|
72
|
+
"hub_models": {
|
|
73
|
+
"crossencoder_ct2": {
|
|
74
|
+
"repo": "aloudreader/uk-stress-crossencoder-ct2-int8",
|
|
75
|
+
"revision": "cf9133781f0f74c142a7d589a7af8ace1d8e5573",
|
|
76
|
+
"target": "ct2_models/stress-model-ct2-int8",
|
|
77
|
+
"set": "light"
|
|
78
|
+
},
|
|
79
|
+
"token_classifier_ct2": {
|
|
80
|
+
"repo": "aloudreader/uk-stress-token-classifier-ct2-int8",
|
|
81
|
+
"revision": "0d85c73a7565a5b1c4e2a5593d529b50c1ea9a30",
|
|
82
|
+
"target": "ct2_models/tok-v5-ct2-int8",
|
|
83
|
+
"set": "light"
|
|
84
|
+
},
|
|
85
|
+
"spacy_sm": {
|
|
86
|
+
"repo": "spacy/uk_core_news_sm",
|
|
87
|
+
"revision": "ca2310baff047c262a04258e0d7f6a8721a06bd1",
|
|
88
|
+
"target": "spacy-uk_core_news_sm",
|
|
89
|
+
"set": "light"
|
|
90
|
+
},
|
|
91
|
+
"verbalizer_ct2": {
|
|
92
|
+
"repo": "aloudreader/marian-uk-verbalizer-ct2-int8",
|
|
93
|
+
"revision": "483bd81b6ad3bc1a9eaaf6d5835ced755774e95e",
|
|
94
|
+
"target": "ct2_models/verbalizer-ct2-int8",
|
|
95
|
+
"set": "light-tts"
|
|
96
|
+
},
|
|
97
|
+
"crossencoder": {
|
|
98
|
+
"repo": "aloudreader/uk-stress-crossencoder",
|
|
99
|
+
"revision": "6760529e714ffac7f89f33b8f7001b885a5ebc8f",
|
|
100
|
+
"target": "stress-model",
|
|
101
|
+
"set": "full"
|
|
102
|
+
},
|
|
103
|
+
"token_classifier": {
|
|
104
|
+
"repo": "aloudreader/uk-stress-token-classifier",
|
|
105
|
+
"revision": "d2795e0c32154ba284248729f35362de135b0d72",
|
|
106
|
+
"target": "tok-v5",
|
|
107
|
+
"set": "full"
|
|
108
|
+
},
|
|
109
|
+
"combiner": {
|
|
110
|
+
"repo": "aloudreader/uk-stress-combiner",
|
|
111
|
+
"revision": "f54a4f2f15615b201e26a284882d5ef65c7821c0",
|
|
112
|
+
"target": "combiner-v1",
|
|
113
|
+
"set": "full"
|
|
114
|
+
},
|
|
115
|
+
"spacy_trf": {
|
|
116
|
+
"repo": "spacy/uk_core_news_trf",
|
|
117
|
+
"revision": "695c843e091e2659b1ee747af0b564ce54c8aff5",
|
|
118
|
+
"target": "spacy-uk_core_news_trf",
|
|
119
|
+
"set": "full"
|
|
120
|
+
},
|
|
121
|
+
"verbalizer": {
|
|
122
|
+
"repo": "aloudreader/marian-uk-verbalizer",
|
|
123
|
+
"revision": "7726f30947caf3b2882c8e753805f930e67ec858",
|
|
124
|
+
"target": "verbalizer",
|
|
125
|
+
"set": "full-tts"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
accentuk/assets.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Asset bundle verification, atomic download, and cache management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import urllib.request
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .errors import AssetError
|
|
15
|
+
from .export import build_bundle, sha256_file
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger("accentuk.assets")
|
|
18
|
+
|
|
19
|
+
DEFAULT_CACHE_DIR = Path.home() / ".cache" / "accentuk"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_releases_descriptor() -> dict[str, Any]:
|
|
23
|
+
"""Load bundled releases.json descriptor."""
|
|
24
|
+
desc_file = Path(__file__).parent / "assets" / "releases.json"
|
|
25
|
+
if not desc_file.is_file():
|
|
26
|
+
raise AssetError("releases.json descriptor is missing.")
|
|
27
|
+
return json.loads(desc_file.read_text(encoding="utf-8"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AssetBundle:
|
|
31
|
+
"""Represents an active, verified accentuk asset bundle."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, path: Path, manifest: dict[str, Any]) -> None:
|
|
34
|
+
self.path = path
|
|
35
|
+
self.manifest = manifest
|
|
36
|
+
self.schema_version = manifest.get("schema_version", 1)
|
|
37
|
+
self.normalization_version = manifest.get("normalization_version", "ukstress-nfd-v1")
|
|
38
|
+
self.dataset_id = manifest.get("dataset_id", 2)
|
|
39
|
+
self.inventory_hash = manifest.get("inventory_hash", "")
|
|
40
|
+
self.model_version = manifest.get("model_version", "")
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def lexicon_path(self) -> Path:
|
|
44
|
+
return self.path / "lexicon.marisa"
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def open(cls, directory: Path | str) -> AssetBundle:
|
|
48
|
+
"""Open and validate an existing asset bundle directory."""
|
|
49
|
+
dir_path = Path(directory)
|
|
50
|
+
manifest_file = dir_path / "manifest.json"
|
|
51
|
+
if not manifest_file.is_file():
|
|
52
|
+
raise AssetError(
|
|
53
|
+
f"Asset bundle manifest missing at {manifest_file}.",
|
|
54
|
+
"Run 'accentuk download' to fetch or repair the bundle.",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
manifest = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
59
|
+
except Exception as err:
|
|
60
|
+
raise AssetError(
|
|
61
|
+
f"Corrupt bundle manifest at {manifest_file}: {err}",
|
|
62
|
+
"Run 'accentuk download' to re-download the bundle.",
|
|
63
|
+
) from err
|
|
64
|
+
|
|
65
|
+
if manifest.get("schema_version") != 1:
|
|
66
|
+
raise AssetError(
|
|
67
|
+
f"Incompatible bundle schema version: {manifest.get('schema_version')}, expected 1.",
|
|
68
|
+
"Upgrade accentuk or download a compatible bundle.",
|
|
69
|
+
)
|
|
70
|
+
if manifest.get("normalization_version") != "ukstress-nfd-v1":
|
|
71
|
+
raise AssetError(
|
|
72
|
+
f"Incompatible normalization version: {manifest.get('normalization_version')}, expected 'ukstress-nfd-v1'.",
|
|
73
|
+
"Rebuild or re-download the bundle.",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Verify bundled file checksums
|
|
77
|
+
files = manifest.get("files", {})
|
|
78
|
+
for filename, expected_sha in files.items():
|
|
79
|
+
fpath = dir_path / filename
|
|
80
|
+
if not fpath.is_file():
|
|
81
|
+
raise AssetError(
|
|
82
|
+
f"Required bundle file missing: {fpath}.",
|
|
83
|
+
"Run 'accentuk download' to repair the bundle.",
|
|
84
|
+
)
|
|
85
|
+
actual_sha = sha256_file(fpath)
|
|
86
|
+
if actual_sha.lower() != expected_sha.lower():
|
|
87
|
+
raise AssetError(
|
|
88
|
+
f"Corrupted bundle file {filename}: expected {expected_sha}, got {actual_sha}.",
|
|
89
|
+
"Run 'accentuk download' to re-download and repair.",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return cls(dir_path, manifest)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _download_file(url: str, dest: Path, expected_sha: str | None = None) -> None:
|
|
96
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
temp_dest = dest.with_suffix(dest.suffix + ".part")
|
|
98
|
+
try:
|
|
99
|
+
req = urllib.request.Request(url, headers={"User-Agent": "accentuk/0.1.0"})
|
|
100
|
+
with urllib.request.urlopen(req) as resp, temp_dest.open("wb") as out:
|
|
101
|
+
shutil.copyfileobj(resp, out)
|
|
102
|
+
|
|
103
|
+
if expected_sha:
|
|
104
|
+
actual = sha256_file(temp_dest)
|
|
105
|
+
if actual.lower() != expected_sha.lower():
|
|
106
|
+
raise AssetError(
|
|
107
|
+
f"Checksum mismatch for {dest.name}: expected {expected_sha}, got {actual}."
|
|
108
|
+
)
|
|
109
|
+
temp_dest.replace(dest)
|
|
110
|
+
finally:
|
|
111
|
+
if temp_dest.is_file():
|
|
112
|
+
temp_dest.unlink(missing_ok=True)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def download_assets(
|
|
116
|
+
target_dir: Path | str | None = None,
|
|
117
|
+
*,
|
|
118
|
+
include_tts: bool = False,
|
|
119
|
+
allow_local_fixtures: bool = True,
|
|
120
|
+
light: bool = False,
|
|
121
|
+
full: bool = False,
|
|
122
|
+
) -> Path:
|
|
123
|
+
"""Atomically download or build and verify the stress assets bundle."""
|
|
124
|
+
target_path = Path(target_dir) if target_dir else DEFAULT_CACHE_DIR
|
|
125
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
desc = get_releases_descriptor()
|
|
128
|
+
staging_dir = target_path.parent / f".staging-{os.getpid()}-{target_path.name}"
|
|
129
|
+
if staging_dir.is_dir():
|
|
130
|
+
shutil.rmtree(staging_dir)
|
|
131
|
+
staging_dir.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Optional local sources instead of downloads (maintainers): ACCENTUK_LOCAL_DATA holds serving/ and
|
|
135
|
+
# lexicon-assets.tar.gz, ACCENTUK_LOCAL_MODELS the model directories, ACCENTUK_LOCAL_CT2 the CT2 ones.
|
|
136
|
+
local_data = Path(os.environ.get("ACCENTUK_LOCAL_DATA", "/nonexistent"))
|
|
137
|
+
local_archive = local_data / "serving"
|
|
138
|
+
local_assets_tar = local_data / "lexicon-assets.tar.gz"
|
|
139
|
+
local_models = Path(os.environ.get("ACCENTUK_LOCAL_MODELS", "/nonexistent"))
|
|
140
|
+
|
|
141
|
+
if allow_local_fixtures and local_archive.is_dir() and (local_models / "stress-model").is_dir():
|
|
142
|
+
log.info("Using local archive fixtures from %s...", local_archive)
|
|
143
|
+
build_bundle(
|
|
144
|
+
archive=local_archive,
|
|
145
|
+
model_manifest=local_models / "stress-model" / "serving_manifest.json",
|
|
146
|
+
output=staging_dir,
|
|
147
|
+
assets_dir=local_assets_tar if local_assets_tar.is_file() else local_models / "lexicon",
|
|
148
|
+
)
|
|
149
|
+
# Copy CT2 light models if present
|
|
150
|
+
ct2_source = Path(os.environ.get("ACCENTUK_LOCAL_CT2", "/nonexistent"))
|
|
151
|
+
if ct2_source.is_dir():
|
|
152
|
+
for ct2_name in ("stress-model-ct2-int8", "tok-v5-ct2-int8", "verbalizer-ct2-int8"):
|
|
153
|
+
src_ct2 = ct2_source / ct2_name
|
|
154
|
+
if src_ct2.is_dir():
|
|
155
|
+
shutil.copytree(src_ct2, staging_dir / ct2_name, dirs_exist_ok=True)
|
|
156
|
+
|
|
157
|
+
# Copy full models unless light-only was explicitly requested
|
|
158
|
+
if not light or full:
|
|
159
|
+
for model_name in ("stress-model", "tok-v5", "combiner-v1", "spacy-uk_core_news_trf_380"):
|
|
160
|
+
src_m = local_models / model_name
|
|
161
|
+
if src_m.is_dir():
|
|
162
|
+
shutil.copytree(src_m, staging_dir / model_name, dirs_exist_ok=True)
|
|
163
|
+
|
|
164
|
+
if include_tts:
|
|
165
|
+
src_verb = local_models / "verbalizer"
|
|
166
|
+
if src_verb.is_dir():
|
|
167
|
+
shutil.copytree(src_verb, staging_dir / "verbalizer", dirs_exist_ok=True)
|
|
168
|
+
else:
|
|
169
|
+
# The lexicon comes prebuilt (building it needs ukrainian-word-stress for the readings),
|
|
170
|
+
# the models from the Hub at the commits pinned in releases.json.
|
|
171
|
+
lex = desc["lexicon_bundle"]
|
|
172
|
+
downloads_dir = staging_dir / "downloads"
|
|
173
|
+
downloads_dir.mkdir(exist_ok=True)
|
|
174
|
+
lex_tar = downloads_dir / "lexicon.tar.gz"
|
|
175
|
+
_download_file(lex["url"], lex_tar, lex["sha256"])
|
|
176
|
+
import tarfile
|
|
177
|
+
with tarfile.open(lex_tar) as tf:
|
|
178
|
+
tf.extractall(staging_dir, filter="data")
|
|
179
|
+
shutil.rmtree(downloads_dir)
|
|
180
|
+
|
|
181
|
+
from huggingface_hub import snapshot_download
|
|
182
|
+
sets = {"full"} if full and not light else {"light"}
|
|
183
|
+
if include_tts:
|
|
184
|
+
sets |= {f"{x}-tts" for x in sets}
|
|
185
|
+
for name, m in desc["hub_models"].items():
|
|
186
|
+
if m["set"] not in sets:
|
|
187
|
+
continue
|
|
188
|
+
log.info("Downloading %s (%s@%s)...", name, m["repo"], m["revision"][:12])
|
|
189
|
+
snapshot_download(m["repo"], revision=m["revision"], local_dir=staging_dir / m["target"],
|
|
190
|
+
ignore_patterns=["*.whl", ".gitattributes", "README.md"])
|
|
191
|
+
# Both cross-encoders read the serving manifest from the bundle root.
|
|
192
|
+
for cand in ("ct2_models/stress-model-ct2-int8", "stress-model"):
|
|
193
|
+
if (staging_dir / cand / "serving_manifest.json").is_file():
|
|
194
|
+
shutil.copy(staging_dir / cand / "serving_manifest.json", staging_dir / "serving_manifest.json")
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
# Validate staging before activating
|
|
198
|
+
AssetBundle.open(staging_dir)
|
|
199
|
+
|
|
200
|
+
# If target_path already exists and we're just adding --tts:
|
|
201
|
+
if (target_path / "lexicon.marisa").is_file() and include_tts and (staging_dir / "verbalizer").is_dir():
|
|
202
|
+
shutil.copytree(staging_dir / "verbalizer", target_path / "verbalizer", dirs_exist_ok=True)
|
|
203
|
+
else:
|
|
204
|
+
# Atomic copy/replace
|
|
205
|
+
for item in staging_dir.iterdir():
|
|
206
|
+
dest = target_path / item.name
|
|
207
|
+
if item.is_dir():
|
|
208
|
+
if dest.is_dir():
|
|
209
|
+
shutil.rmtree(dest)
|
|
210
|
+
shutil.copytree(item, dest)
|
|
211
|
+
else:
|
|
212
|
+
shutil.copy2(item, dest)
|
|
213
|
+
|
|
214
|
+
log.info("Assets successfully installed and verified at %s", target_path)
|
|
215
|
+
return target_path
|
|
216
|
+
finally:
|
|
217
|
+
if staging_dir.is_dir():
|
|
218
|
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
accentuk/cli.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Command-line interface for accentuk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .assets import download_assets
|
|
10
|
+
from .engine import Accentor
|
|
11
|
+
from .errors import AccentukError
|
|
12
|
+
from .export import build_bundle
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(args: list[str] | None = None) -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="accentuk",
|
|
18
|
+
description="Offline Ukrainian stress marking and optional TTS preparation",
|
|
19
|
+
)
|
|
20
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
21
|
+
|
|
22
|
+
# download
|
|
23
|
+
p_dl = subparsers.add_parser("download", help="Download and verify required stress models/lexicon")
|
|
24
|
+
p_dl.add_argument("--light", action="store_true", help="Download light bundle (CTranslate2 INT8 + Lexicon, ~450 MB)")
|
|
25
|
+
p_dl.add_argument("--full", action="store_true", help="Download full bundle (PyTorch weights + spaCy TRF, ~2.2 GB)")
|
|
26
|
+
p_dl.add_argument("--tts", action="store_true", help="Also download optional TTS verbalizer model")
|
|
27
|
+
p_dl.add_argument("--directory", type=Path, default=None, help="Target cache directory")
|
|
28
|
+
|
|
29
|
+
# export
|
|
30
|
+
p_exp = subparsers.add_parser("export", help="Export serving archive into MARISA bundle")
|
|
31
|
+
p_exp.add_argument("--archive", type=Path, required=True, help="Path to serving archive or directory")
|
|
32
|
+
p_exp.add_argument("--manifest", type=Path, required=True, help="Path to serving_manifest.json")
|
|
33
|
+
p_exp.add_argument("--output", type=Path, required=True, help="Output bundle directory")
|
|
34
|
+
p_exp.add_argument("--assets", type=Path, default=None, help="Path to lexicon-assets.tar.gz or dir")
|
|
35
|
+
|
|
36
|
+
# stress
|
|
37
|
+
p_stress = subparsers.add_parser("stress", help="Mark stress on Ukrainian text")
|
|
38
|
+
p_stress.add_argument("text", help="Ukrainian input text")
|
|
39
|
+
p_stress.add_argument("--mode", choices=["auto", "light", "full"], default="auto", help="Inference mode: light (CT2 INT8) or full (PyTorch/spaCy)")
|
|
40
|
+
p_stress.add_argument("--ambiguity", choices=["default", "preserve"], default="default", help="Handling of ambiguous words")
|
|
41
|
+
p_stress.add_argument("--combiner", action="store_true", help="Enable learned combiner")
|
|
42
|
+
p_stress.add_argument("--assets", type=Path, default=None, help="Path to asset bundle directory")
|
|
43
|
+
|
|
44
|
+
# prepare
|
|
45
|
+
p_prep = subparsers.add_parser("prepare", help="Prepare text for TTS (verbalize and stress mark)")
|
|
46
|
+
p_prep.add_argument("text", help="Input text with numbers/dates/symbols")
|
|
47
|
+
p_prep.add_argument("--mode", choices=["auto", "light", "full"], default="auto", help="Inference mode: light (CT2 INT8) or full (PyTorch/spaCy)")
|
|
48
|
+
p_prep.add_argument("--ambiguity", choices=["default", "preserve"], default="default", help="Handling of ambiguous words")
|
|
49
|
+
p_prep.add_argument("--combiner", action="store_true", help="Enable learned combiner")
|
|
50
|
+
p_prep.add_argument("--assets", type=Path, default=None, help="Path to asset bundle directory")
|
|
51
|
+
|
|
52
|
+
parsed = parser.parse_args(args)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
if parsed.command == "download":
|
|
56
|
+
download_assets(
|
|
57
|
+
target_dir=parsed.directory,
|
|
58
|
+
include_tts=parsed.tts,
|
|
59
|
+
light=parsed.light,
|
|
60
|
+
full=parsed.full,
|
|
61
|
+
)
|
|
62
|
+
print("Download completed successfully.")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
elif parsed.command == "export":
|
|
66
|
+
build_bundle(
|
|
67
|
+
archive=parsed.archive,
|
|
68
|
+
model_manifest=parsed.manifest,
|
|
69
|
+
output=parsed.output,
|
|
70
|
+
assets_dir=parsed.assets,
|
|
71
|
+
)
|
|
72
|
+
print(f"Export completed successfully to {parsed.output}.")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
elif parsed.command == "stress":
|
|
76
|
+
with Accentor(asset_dir=parsed.assets, mode=parsed.mode, enable_combiner=parsed.combiner) as engine:
|
|
77
|
+
res = engine.stress(parsed.text, on_ambiguity=parsed.ambiguity, combiner=parsed.combiner)
|
|
78
|
+
print(res.text)
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
elif parsed.command == "prepare":
|
|
82
|
+
try:
|
|
83
|
+
from .frontend import Frontend
|
|
84
|
+
|
|
85
|
+
with Frontend(asset_dir=parsed.assets, mode=parsed.mode) as frontend:
|
|
86
|
+
res = frontend.prepare(parsed.text, on_ambiguity=parsed.ambiguity, combiner=parsed.combiner)
|
|
87
|
+
print(res.text)
|
|
88
|
+
return 0
|
|
89
|
+
except ImportError as err:
|
|
90
|
+
print(f"TTS extra not available: {err}. Install with 'accentuk[tts]'.", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
except AccentukError as err:
|
|
94
|
+
print(f"Error: {err}", file=sys.stderr)
|
|
95
|
+
return 1
|
|
96
|
+
except Exception as err:
|
|
97
|
+
print(f"Unexpected error: {err}", file=sys.stderr)
|
|
98
|
+
return 1
|
|
99
|
+
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
sys.exit(main())
|