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 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())