lattifai 0.2.4__py3-none-any.whl → 0.4.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.
- lattifai/__init__.py +32 -1
- lattifai/base_client.py +14 -6
- lattifai/bin/__init__.py +1 -0
- lattifai/bin/agent.py +325 -0
- lattifai/bin/align.py +253 -21
- lattifai/bin/cli_base.py +5 -0
- lattifai/bin/subtitle.py +182 -4
- lattifai/client.py +236 -63
- lattifai/errors.py +257 -0
- lattifai/io/__init__.py +21 -1
- lattifai/io/gemini_reader.py +371 -0
- lattifai/io/gemini_writer.py +173 -0
- lattifai/io/reader.py +21 -9
- lattifai/io/supervision.py +16 -0
- lattifai/io/utils.py +15 -0
- lattifai/io/writer.py +58 -17
- lattifai/tokenizer/__init__.py +2 -2
- lattifai/tokenizer/tokenizer.py +221 -40
- lattifai/utils.py +133 -0
- lattifai/workers/lattice1_alpha.py +130 -66
- lattifai-0.4.0.dist-info/METADATA +811 -0
- lattifai-0.4.0.dist-info/RECORD +28 -0
- lattifai-0.4.0.dist-info/entry_points.txt +3 -0
- lattifai-0.2.4.dist-info/METADATA +0 -334
- lattifai-0.2.4.dist-info/RECORD +0 -22
- lattifai-0.2.4.dist-info/entry_points.txt +0 -4
- {lattifai-0.2.4.dist-info → lattifai-0.4.0.dist-info}/WHEEL +0 -0
- {lattifai-0.2.4.dist-info → lattifai-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {lattifai-0.2.4.dist-info → lattifai-0.4.0.dist-info}/top_level.txt +0 -0
lattifai/client.py
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
"""LattifAI client implementation."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import asyncio
|
|
4
4
|
import os
|
|
5
|
-
from
|
|
6
|
-
from typing import Any, Awaitable, BinaryIO, Callable, Dict, Optional, Union
|
|
5
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
7
6
|
|
|
8
7
|
import colorful
|
|
9
8
|
from dotenv import load_dotenv
|
|
10
9
|
from lhotse.utils import Pathlike
|
|
11
10
|
|
|
12
|
-
from lattifai.base_client import AsyncAPIClient,
|
|
13
|
-
from lattifai.
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
from lattifai.base_client import AsyncAPIClient, SyncAPIClient
|
|
12
|
+
from lattifai.errors import (
|
|
13
|
+
AlignmentError,
|
|
14
|
+
ConfigurationError,
|
|
15
|
+
LatticeDecodingError,
|
|
16
|
+
LatticeEncodingError,
|
|
17
|
+
LattifAIError,
|
|
18
|
+
SubtitleProcessingError,
|
|
19
|
+
handle_exception,
|
|
20
|
+
)
|
|
21
|
+
from lattifai.io import SubtitleFormat, SubtitleIO, Supervision
|
|
22
|
+
from lattifai.tokenizer import AsyncLatticeTokenizer
|
|
23
|
+
from lattifai.utils import _load_tokenizer, _load_worker, _resolve_model_path, _select_device
|
|
16
24
|
|
|
17
25
|
load_dotenv()
|
|
18
26
|
|
|
@@ -34,7 +42,7 @@ class LattifAI(SyncAPIClient):
|
|
|
34
42
|
if api_key is None:
|
|
35
43
|
api_key = os.environ.get('LATTIFAI_API_KEY')
|
|
36
44
|
if api_key is None:
|
|
37
|
-
raise
|
|
45
|
+
raise ConfigurationError(
|
|
38
46
|
'The api_key client option must be set either by passing api_key to the client '
|
|
39
47
|
'or by setting the LATTIFAI_API_KEY environment variable'
|
|
40
48
|
)
|
|
@@ -52,35 +60,12 @@ class LattifAI(SyncAPIClient):
|
|
|
52
60
|
default_headers=default_headers,
|
|
53
61
|
)
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
from huggingface_hub import snapshot_download
|
|
58
|
-
from huggingface_hub.errors import LocalEntryNotFoundError
|
|
63
|
+
model_path = _resolve_model_path(model_name_or_path)
|
|
64
|
+
device = _select_device(device)
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
|
|
64
|
-
model_path = snapshot_download(repo_id=model_name_or_path, repo_type='model')
|
|
65
|
-
else:
|
|
66
|
-
model_path = model_name_or_path
|
|
67
|
-
|
|
68
|
-
# device setup
|
|
69
|
-
if device is None:
|
|
70
|
-
import torch
|
|
71
|
-
|
|
72
|
-
device = 'cpu'
|
|
73
|
-
if torch.backends.mps.is_available():
|
|
74
|
-
device = 'mps'
|
|
75
|
-
elif torch.cuda.is_available():
|
|
76
|
-
device = 'cuda'
|
|
77
|
-
|
|
78
|
-
self.tokenizer = LatticeTokenizer.from_pretrained(
|
|
79
|
-
client_wrapper=self,
|
|
80
|
-
model_path=model_path,
|
|
81
|
-
device=device,
|
|
82
|
-
)
|
|
83
|
-
self.worker = Lattice1AlphaWorker(model_path, device=device, num_threads=8)
|
|
66
|
+
self.tokenizer = _load_tokenizer(self, model_path, device)
|
|
67
|
+
self.worker = _load_worker(model_path, device)
|
|
68
|
+
self.device = device
|
|
84
69
|
|
|
85
70
|
def alignment(
|
|
86
71
|
self,
|
|
@@ -88,55 +73,243 @@ class LattifAI(SyncAPIClient):
|
|
|
88
73
|
subtitle: Pathlike,
|
|
89
74
|
format: Optional[SubtitleFormat] = None,
|
|
90
75
|
split_sentence: bool = False,
|
|
76
|
+
return_details: bool = False,
|
|
91
77
|
output_subtitle_path: Optional[Pathlike] = None,
|
|
92
|
-
) ->
|
|
78
|
+
) -> Tuple[List[Supervision], Optional[Pathlike]]:
|
|
93
79
|
"""Perform alignment on audio and subtitle/text.
|
|
94
80
|
|
|
95
81
|
Args:
|
|
96
82
|
audio: Audio file path
|
|
97
83
|
subtitle: Subtitle/Text to align with audio
|
|
98
|
-
|
|
84
|
+
format: Input subtitle format (srt, vtt, ass, txt). Auto-detected if None
|
|
85
|
+
split_sentence: Enable intelligent sentence re-splitting based on punctuation semantics
|
|
86
|
+
return_details: Return word-level alignment details in Supervision.alignment field
|
|
87
|
+
output_subtitle_path: Output path for aligned subtitle (optional)
|
|
99
88
|
|
|
100
89
|
Returns:
|
|
101
|
-
|
|
90
|
+
Tuple containing:
|
|
91
|
+
- List of aligned Supervision objects with timing information
|
|
92
|
+
- Output subtitle path (if output_subtitle_path was provided)
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
SubtitleProcessingError: If subtitle file cannot be parsed
|
|
96
|
+
LatticeEncodingError: If lattice graph generation fails
|
|
97
|
+
AlignmentError: If audio alignment fails
|
|
98
|
+
LatticeDecodingError: If lattice decoding fails
|
|
102
99
|
"""
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
try:
|
|
101
|
+
# step1: parse text or subtitles
|
|
102
|
+
print(colorful.cyan(f'📖 Step 1: Reading subtitle file from {subtitle}'))
|
|
103
|
+
try:
|
|
104
|
+
supervisions = SubtitleIO.read(subtitle, format=format)
|
|
105
|
+
print(colorful.green(f' ✓ Parsed {len(supervisions)} subtitle segments'))
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise SubtitleProcessingError(
|
|
108
|
+
f'Failed to parse subtitle file: {subtitle}',
|
|
109
|
+
subtitle_path=str(subtitle),
|
|
110
|
+
context={'original_error': str(e)},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# step2: make lattice by call Lattifai API
|
|
114
|
+
print(colorful.cyan('🔗 Step 2: Creating lattice graph from segments'))
|
|
115
|
+
try:
|
|
116
|
+
supervisions, lattice_id, lattice_graph = self.tokenizer.tokenize(
|
|
117
|
+
supervisions, split_sentence=split_sentence
|
|
118
|
+
)
|
|
119
|
+
print(colorful.green(f' ✓ Generated lattice graph with ID: {lattice_id}'))
|
|
120
|
+
except Exception as e:
|
|
121
|
+
text_content = ' '.join([sup.text for sup in supervisions]) if supervisions else ''
|
|
122
|
+
raise LatticeEncodingError(text_content, original_error=e)
|
|
123
|
+
|
|
124
|
+
# step3: search lattice graph with audio
|
|
125
|
+
print(colorful.cyan(f'🔍 Step 3: Searching lattice graph with audio: {audio}'))
|
|
126
|
+
try:
|
|
127
|
+
lattice_results = self.worker.alignment(audio, lattice_graph)
|
|
128
|
+
print(colorful.green(' ✓ Lattice search completed'))
|
|
129
|
+
except Exception as e:
|
|
130
|
+
raise AlignmentError(
|
|
131
|
+
f'Audio alignment failed for {audio}',
|
|
132
|
+
audio_path=str(audio),
|
|
133
|
+
subtitle_path=str(subtitle),
|
|
134
|
+
context={'original_error': str(e)},
|
|
135
|
+
)
|
|
107
136
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
# step4: decode lattice results to aligned segments
|
|
138
|
+
print(colorful.cyan('🎯 Step 4: Decoding lattice results to aligned segments'))
|
|
139
|
+
try:
|
|
140
|
+
alignments = self.tokenizer.detokenize(
|
|
141
|
+
lattice_id, lattice_results, supervisions=supervisions, return_details=return_details
|
|
142
|
+
)
|
|
143
|
+
print(colorful.green(f' ✓ Successfully aligned {len(alignments)} segments'))
|
|
144
|
+
except LatticeDecodingError as e:
|
|
145
|
+
print(colorful.red(' x Failed to decode lattice alignment results'))
|
|
146
|
+
raise e
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(colorful.red(' x Failed to decode lattice alignment results'))
|
|
149
|
+
raise LatticeDecodingError(lattice_id, original_error=e)
|
|
150
|
+
|
|
151
|
+
# step5: export alignments to target format
|
|
152
|
+
if output_subtitle_path:
|
|
153
|
+
try:
|
|
154
|
+
SubtitleIO.write(alignments, output_path=output_subtitle_path)
|
|
155
|
+
print(colorful.green(f'🎉🎉🎉🎉🎉 Subtitle file written to: {output_subtitle_path}'))
|
|
156
|
+
except Exception as e:
|
|
157
|
+
raise SubtitleProcessingError(
|
|
158
|
+
f'Failed to write output file: {output_subtitle_path}',
|
|
159
|
+
subtitle_path=str(output_subtitle_path),
|
|
160
|
+
context={'original_error': str(e)},
|
|
161
|
+
)
|
|
162
|
+
return (alignments, output_subtitle_path)
|
|
112
163
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
164
|
+
except (SubtitleProcessingError, LatticeEncodingError, AlignmentError, LatticeDecodingError):
|
|
165
|
+
# Re-raise our specific errors as-is
|
|
166
|
+
raise
|
|
167
|
+
except Exception as e:
|
|
168
|
+
# Catch any unexpected errors and wrap them
|
|
169
|
+
raise AlignmentError(
|
|
170
|
+
'Unexpected error during alignment process',
|
|
171
|
+
audio_path=str(audio),
|
|
172
|
+
subtitle_path=str(subtitle),
|
|
173
|
+
context={'original_error': str(e), 'error_type': e.__class__.__name__},
|
|
174
|
+
)
|
|
117
175
|
|
|
118
|
-
# step4: decode the lattice paths
|
|
119
|
-
print(colorful.cyan('🔍 Step 4: Decoding lattice paths to final alignments'))
|
|
120
|
-
alignments = self.tokenizer.detokenize(lattice_id, lattice_results)
|
|
121
|
-
print(colorful.green(f' ✓ Decoded {len(alignments)} aligned segments'))
|
|
122
176
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
SubtitleIO.write(alignments, output_path=output_subtitle_path)
|
|
126
|
-
print(colorful.green(f'🎉🎉🎉🎉🎉 Subtitle file written to: {output_subtitle_path}'))
|
|
177
|
+
class AsyncLattifAI(AsyncAPIClient):
|
|
178
|
+
"""Asynchronous LattifAI client."""
|
|
127
179
|
|
|
128
|
-
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
api_key: Optional[str] = None,
|
|
184
|
+
model_name_or_path: str = 'Lattifai/Lattice-1-Alpha',
|
|
185
|
+
device: Optional[str] = None,
|
|
186
|
+
base_url: Optional[str] = None,
|
|
187
|
+
timeout: Union[float, int] = 120.0,
|
|
188
|
+
max_retries: int = 2,
|
|
189
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
if api_key is None:
|
|
192
|
+
api_key = os.environ.get('LATTIFAI_API_KEY')
|
|
193
|
+
if api_key is None:
|
|
194
|
+
raise ConfigurationError(
|
|
195
|
+
'The api_key client option must be set either by passing api_key to the client '
|
|
196
|
+
'or by setting the LATTIFAI_API_KEY environment variable'
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if base_url is None:
|
|
200
|
+
base_url = os.environ.get('LATTIFAI_BASE_URL')
|
|
201
|
+
if not base_url:
|
|
202
|
+
base_url = 'https://api.lattifai.com/v1'
|
|
203
|
+
|
|
204
|
+
super().__init__(
|
|
205
|
+
api_key=api_key,
|
|
206
|
+
base_url=base_url,
|
|
207
|
+
timeout=timeout,
|
|
208
|
+
max_retries=max_retries,
|
|
209
|
+
default_headers=default_headers,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
model_path = _resolve_model_path(model_name_or_path)
|
|
213
|
+
device = _select_device(device)
|
|
214
|
+
|
|
215
|
+
self.tokenizer = _load_tokenizer(self, model_path, device, tokenizer_cls=AsyncLatticeTokenizer)
|
|
216
|
+
self.worker = _load_worker(model_path, device)
|
|
217
|
+
self.device = device
|
|
218
|
+
|
|
219
|
+
async def alignment(
|
|
220
|
+
self,
|
|
221
|
+
audio: Pathlike,
|
|
222
|
+
subtitle: Pathlike,
|
|
223
|
+
format: Optional[SubtitleFormat] = None,
|
|
224
|
+
split_sentence: bool = False,
|
|
225
|
+
return_details: bool = False,
|
|
226
|
+
output_subtitle_path: Optional[Pathlike] = None,
|
|
227
|
+
) -> Tuple[List[Supervision], Optional[Pathlike]]:
|
|
228
|
+
try:
|
|
229
|
+
print(colorful.cyan(f'📖 Step 1: Reading subtitle file from {subtitle}'))
|
|
230
|
+
try:
|
|
231
|
+
supervisions = await asyncio.to_thread(SubtitleIO.read, subtitle, format=format)
|
|
232
|
+
print(colorful.green(f' ✓ Parsed {len(supervisions)} subtitle segments'))
|
|
233
|
+
except Exception as e:
|
|
234
|
+
raise SubtitleProcessingError(
|
|
235
|
+
f'Failed to parse subtitle file: {subtitle}',
|
|
236
|
+
subtitle_path=str(subtitle),
|
|
237
|
+
context={'original_error': str(e)},
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
print(colorful.cyan('🔗 Step 2: Creating lattice graph from segments'))
|
|
241
|
+
try:
|
|
242
|
+
supervisions, lattice_id, lattice_graph = await self.tokenizer.tokenize(
|
|
243
|
+
supervisions,
|
|
244
|
+
split_sentence=split_sentence,
|
|
245
|
+
)
|
|
246
|
+
print(colorful.green(f' ✓ Generated lattice graph with ID: {lattice_id}'))
|
|
247
|
+
except Exception as e:
|
|
248
|
+
text_content = ' '.join([sup.text for sup in supervisions]) if supervisions else ''
|
|
249
|
+
raise LatticeEncodingError(text_content, original_error=e)
|
|
250
|
+
|
|
251
|
+
print(colorful.cyan(f'🔍 Step 3: Searching lattice graph with audio: {audio}'))
|
|
252
|
+
try:
|
|
253
|
+
lattice_results = await asyncio.to_thread(self.worker.alignment, audio, lattice_graph)
|
|
254
|
+
print(colorful.green(' ✓ Lattice search completed'))
|
|
255
|
+
except Exception as e:
|
|
256
|
+
raise AlignmentError(
|
|
257
|
+
f'Audio alignment failed for {audio}',
|
|
258
|
+
audio_path=str(audio),
|
|
259
|
+
subtitle_path=str(subtitle),
|
|
260
|
+
context={'original_error': str(e)},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
print(colorful.cyan('🎯 Step 4: Decoding lattice results to aligned segments'))
|
|
264
|
+
try:
|
|
265
|
+
alignments = await self.tokenizer.detokenize(
|
|
266
|
+
lattice_id, lattice_results, supervisions=supervisions, return_details=return_details
|
|
267
|
+
)
|
|
268
|
+
print(colorful.green(f' ✓ Successfully aligned {len(alignments)} segments'))
|
|
269
|
+
except LatticeDecodingError as e:
|
|
270
|
+
print(colorful.red(' x Failed to decode lattice alignment results'))
|
|
271
|
+
raise e
|
|
272
|
+
except Exception as e:
|
|
273
|
+
print(colorful.red(' x Failed to decode lattice alignment results'))
|
|
274
|
+
raise LatticeDecodingError(lattice_id, original_error=e)
|
|
275
|
+
|
|
276
|
+
if output_subtitle_path:
|
|
277
|
+
try:
|
|
278
|
+
await asyncio.to_thread(SubtitleIO.write, alignments, output_subtitle_path)
|
|
279
|
+
print(colorful.green(f'🎉🎉🎉🎉🎉 Subtitle file written to: {output_subtitle_path}'))
|
|
280
|
+
except Exception as e:
|
|
281
|
+
raise SubtitleProcessingError(
|
|
282
|
+
f'Failed to write output file: {output_subtitle_path}',
|
|
283
|
+
subtitle_path=str(output_subtitle_path),
|
|
284
|
+
context={'original_error': str(e)},
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return (alignments, output_subtitle_path)
|
|
288
|
+
|
|
289
|
+
except (SubtitleProcessingError, LatticeEncodingError, AlignmentError, LatticeDecodingError):
|
|
290
|
+
raise
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise AlignmentError(
|
|
293
|
+
'Unexpected error during alignment process',
|
|
294
|
+
audio_path=str(audio),
|
|
295
|
+
subtitle_path=str(subtitle),
|
|
296
|
+
context={'original_error': str(e), 'error_type': e.__class__.__name__},
|
|
297
|
+
)
|
|
129
298
|
|
|
130
299
|
|
|
131
300
|
if __name__ == '__main__':
|
|
132
301
|
client = LattifAI()
|
|
133
302
|
import sys
|
|
134
303
|
|
|
135
|
-
if len(sys.argv) ==
|
|
136
|
-
audio, subtitle, output = sys.argv[1:]
|
|
304
|
+
if len(sys.argv) == 5:
|
|
305
|
+
audio, subtitle, output, split_sentence = sys.argv[1:]
|
|
306
|
+
split_sentence = split_sentence.lower() in ('true', '1', 'yes')
|
|
137
307
|
else:
|
|
138
308
|
audio = 'tests/data/SA1.wav'
|
|
139
309
|
subtitle = 'tests/data/SA1.TXT'
|
|
140
310
|
output = None
|
|
311
|
+
split_sentence = False
|
|
141
312
|
|
|
142
|
-
alignments = client.alignment(
|
|
313
|
+
(alignments, output_subtitle_path) = client.alignment(
|
|
314
|
+
audio, subtitle, output_subtitle_path=output, split_sentence=split_sentence, return_details=True
|
|
315
|
+
)
|
lattifai/errors.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Error handling and exception classes for LattifAI SDK."""
|
|
2
|
+
|
|
3
|
+
import traceback
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import colorful
|
|
7
|
+
|
|
8
|
+
# Error help messages
|
|
9
|
+
LATTICE_DECODING_FAILURE_HELP = (
|
|
10
|
+
'Failed to decode lattice alignment. Possible reasons:\n\n'
|
|
11
|
+
'1) Audio and text content mismatch:\n'
|
|
12
|
+
' - The transcript/subtitle does not accurately match the audio content\n'
|
|
13
|
+
' - Text may be from a different version or section of the audio\n'
|
|
14
|
+
' ⚠️ Note: Gemini transcription may occasionally skip large segments of audio, causing alignment failures.\n'
|
|
15
|
+
' We will detect and fix this issue in the next version.\n\n'
|
|
16
|
+
'2) Unsupported audio type:\n'
|
|
17
|
+
' - Singing is not yet supported, this will be optimized in future versions\n\n'
|
|
18
|
+
'💡 Troubleshooting tips:\n'
|
|
19
|
+
' • Verify the transcript matches the audio by listening to a few segments\n'
|
|
20
|
+
' • For YouTube videos, manually check if auto-generated transcript are accurate\n'
|
|
21
|
+
' • Consider using a different transcription source if Gemini results are incomplete'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LattifAIError(Exception):
|
|
26
|
+
"""Base exception for LattifAI errors."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str, error_code: Optional[str] = None, context: Optional[Dict[str, Any]] = None):
|
|
29
|
+
"""Initialize LattifAI error.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
message: Error message
|
|
33
|
+
error_code: Optional error code for categorization
|
|
34
|
+
context: Optional context information about the error
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.message = message
|
|
38
|
+
self.error_code = error_code or self.__class__.__name__
|
|
39
|
+
self.context = context or {}
|
|
40
|
+
|
|
41
|
+
def get_support_info(self) -> str:
|
|
42
|
+
"""Get support information for users."""
|
|
43
|
+
return (
|
|
44
|
+
f'\n{colorful.green("🔧 Need help? Here are two ways to get support:")}\n'
|
|
45
|
+
f' 1. 📝 Create a GitHub issue: {colorful.green("https://github.com/lattifai/lattifai-python/issues")}\n'
|
|
46
|
+
' Please include:\n'
|
|
47
|
+
' - Your audio file format and duration\n'
|
|
48
|
+
" - The text/subtitle content you're trying to align\n"
|
|
49
|
+
' - This error message and stack trace\n'
|
|
50
|
+
f' 2. 💬 Join our Discord community: {colorful.green("https://discord.gg/vzmTzzZgNu")}\n'
|
|
51
|
+
' Our team and community can help you troubleshoot\n'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def get_message(self) -> str:
|
|
55
|
+
"""Return formatted error message without support information."""
|
|
56
|
+
base_message = f'{colorful.red(f"[{self.error_code}] {self.message}")}'
|
|
57
|
+
if self.context:
|
|
58
|
+
context_str = f'\n{colorful.yellow("Context:")} ' + ', '.join(f'{k}={v}' for k, v in self.context.items())
|
|
59
|
+
base_message += context_str
|
|
60
|
+
return base_message
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str:
|
|
63
|
+
"""Return formatted error message without support information.
|
|
64
|
+
|
|
65
|
+
Note: Support info should be displayed explicitly at the CLI level,
|
|
66
|
+
not automatically appended to avoid duplication when errors are re-raised.
|
|
67
|
+
"""
|
|
68
|
+
return self.get_message()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AudioProcessingError(LattifAIError):
|
|
72
|
+
"""Error during audio processing operations."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, message: str, audio_path: Optional[str] = None, **kwargs):
|
|
75
|
+
context = kwargs.get('context', {})
|
|
76
|
+
if audio_path:
|
|
77
|
+
context['audio_path'] = audio_path
|
|
78
|
+
kwargs['context'] = context
|
|
79
|
+
super().__init__(message, **kwargs)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AudioLoadError(AudioProcessingError):
|
|
83
|
+
"""Error loading or reading audio file."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, audio_path: str, original_error: Optional[Exception] = None, **kwargs):
|
|
86
|
+
message = f'Failed to load audio file: {colorful.red(audio_path)}'
|
|
87
|
+
if original_error:
|
|
88
|
+
message += f' - {colorful.red(str(original_error))}'
|
|
89
|
+
|
|
90
|
+
context = kwargs.get('context', {})
|
|
91
|
+
context.update({'audio_path': audio_path, 'original_error': str(original_error) if original_error else None})
|
|
92
|
+
kwargs['context'] = context
|
|
93
|
+
|
|
94
|
+
super().__init__(message, audio_path=audio_path, **kwargs)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AudioFormatError(AudioProcessingError):
|
|
98
|
+
"""Error with audio format or codec."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, audio_path: str, format_issue: str, **kwargs):
|
|
101
|
+
message = f'Audio format error for {colorful.red(audio_path)}: {colorful.red(format_issue)}'
|
|
102
|
+
context = kwargs.get('context', {})
|
|
103
|
+
context.update({'audio_path': audio_path, 'format_issue': format_issue})
|
|
104
|
+
kwargs['context'] = context
|
|
105
|
+
super().__init__(message, audio_path=audio_path, **kwargs)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SubtitleProcessingError(LattifAIError):
|
|
109
|
+
"""Error during subtitle/text processing operations."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, message: str, subtitle_path: Optional[str] = None, **kwargs):
|
|
112
|
+
context = kwargs.get('context', {})
|
|
113
|
+
if subtitle_path:
|
|
114
|
+
context['subtitle_path'] = subtitle_path
|
|
115
|
+
kwargs['context'] = context
|
|
116
|
+
super().__init__(message, **kwargs)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class SubtitleParseError(SubtitleProcessingError):
|
|
120
|
+
"""Error parsing subtitle or text file."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, subtitle_path: str, parse_issue: str, **kwargs):
|
|
123
|
+
message = f'Failed to parse subtitle file {subtitle_path}: {parse_issue}'
|
|
124
|
+
context = kwargs.get('context', {})
|
|
125
|
+
context.update({'subtitle_path': subtitle_path, 'parse_issue': parse_issue})
|
|
126
|
+
kwargs['context'] = context
|
|
127
|
+
super().__init__(message, subtitle_path=subtitle_path, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class AlignmentError(LattifAIError):
|
|
131
|
+
"""Error during audio-text alignment process."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, message: str, audio_path: Optional[str] = None, subtitle_path: Optional[str] = None, **kwargs):
|
|
134
|
+
context = kwargs.get('context', {})
|
|
135
|
+
if audio_path:
|
|
136
|
+
context['audio_path'] = audio_path
|
|
137
|
+
if subtitle_path:
|
|
138
|
+
context['subtitle_path'] = subtitle_path
|
|
139
|
+
kwargs['context'] = context
|
|
140
|
+
super().__init__(message, **kwargs)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class LatticeEncodingError(AlignmentError):
|
|
144
|
+
"""Error generating lattice graph from text."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, text_content: str, original_error: Optional[Exception] = None, **kwargs):
|
|
147
|
+
message = 'Failed to generate lattice graph from text'
|
|
148
|
+
if original_error:
|
|
149
|
+
message += f': {colorful.red(str(original_error))}'
|
|
150
|
+
|
|
151
|
+
context = kwargs.get('context', {})
|
|
152
|
+
context.update(
|
|
153
|
+
{
|
|
154
|
+
'text_content_length': len(text_content),
|
|
155
|
+
'text_preview': text_content[:100] + '...' if len(text_content) > 100 else text_content,
|
|
156
|
+
'original_error': str(original_error) if original_error else None,
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
kwargs['context'] = context
|
|
160
|
+
super().__init__(message, **kwargs)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class LatticeDecodingError(AlignmentError):
|
|
164
|
+
"""Error decoding lattice alignment results."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, lattice_id: str, original_error: Optional[Exception] = None, **kwargs):
|
|
167
|
+
message = f'Failed to decode lattice alignment results for lattice ID: {colorful.red(lattice_id)}'
|
|
168
|
+
|
|
169
|
+
# Don't duplicate the help message if it's already in original_error
|
|
170
|
+
if original_error and str(original_error) != LATTICE_DECODING_FAILURE_HELP:
|
|
171
|
+
message += f' - {colorful.red(str(original_error))}'
|
|
172
|
+
|
|
173
|
+
context = kwargs.get('context', {})
|
|
174
|
+
# Don't store the entire help message in context to avoid duplication
|
|
175
|
+
if original_error and str(original_error) != LATTICE_DECODING_FAILURE_HELP:
|
|
176
|
+
context['original_error'] = str(original_error)
|
|
177
|
+
context['lattice_id'] = lattice_id
|
|
178
|
+
kwargs['context'] = context
|
|
179
|
+
super().__init__(message, **kwargs)
|
|
180
|
+
|
|
181
|
+
def get_message(self) -> str:
|
|
182
|
+
"""Return formatted error message with help text."""
|
|
183
|
+
base_message = f'{colorful.red(f"[{self.error_code}]")} {self.message}'
|
|
184
|
+
if self.context and self.context.get('lattice_id'):
|
|
185
|
+
# Only show essential context (lattice_id), not the duplicated help message
|
|
186
|
+
base_message += f'\n{colorful.yellow("Lattice ID:")} {self.context["lattice_id"]}'
|
|
187
|
+
# Append help message once at the end
|
|
188
|
+
base_message += f'\n\n{colorful.yellow(LATTICE_DECODING_FAILURE_HELP)}'
|
|
189
|
+
return base_message
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ModelLoadError(LattifAIError):
|
|
193
|
+
"""Error loading AI model."""
|
|
194
|
+
|
|
195
|
+
def __init__(self, model_name: str, original_error: Optional[Exception] = None, **kwargs):
|
|
196
|
+
message = f'Failed to load model: {colorful.red(model_name)}'
|
|
197
|
+
if original_error:
|
|
198
|
+
message += f' - {colorful.red(str(original_error))}'
|
|
199
|
+
|
|
200
|
+
context = kwargs.get('context', {})
|
|
201
|
+
context.update({'model_name': model_name, 'original_error': str(original_error) if original_error else None})
|
|
202
|
+
kwargs['context'] = context
|
|
203
|
+
super().__init__(message, **kwargs)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class DependencyError(LattifAIError):
|
|
207
|
+
"""Error with required dependencies."""
|
|
208
|
+
|
|
209
|
+
def __init__(self, dependency_name: str, install_command: Optional[str] = None, **kwargs):
|
|
210
|
+
message = f'Missing required dependency: {colorful.red(dependency_name)}'
|
|
211
|
+
if install_command:
|
|
212
|
+
message += f'\nPlease install it using: {colorful.yellow(install_command)}'
|
|
213
|
+
|
|
214
|
+
context = kwargs.get('context', {})
|
|
215
|
+
context.update({'dependency_name': dependency_name, 'install_command': install_command})
|
|
216
|
+
kwargs['context'] = context
|
|
217
|
+
super().__init__(message, **kwargs)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class APIError(LattifAIError):
|
|
221
|
+
"""Error communicating with LattifAI API."""
|
|
222
|
+
|
|
223
|
+
def __init__(self, message: str, status_code: Optional[int] = None, response_text: Optional[str] = None, **kwargs):
|
|
224
|
+
context = kwargs.get('context', {})
|
|
225
|
+
context.update({'status_code': status_code, 'response_text': response_text})
|
|
226
|
+
kwargs['context'] = context
|
|
227
|
+
super().__init__(message, **kwargs)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class ConfigurationError(LattifAIError):
|
|
231
|
+
"""Error with client configuration."""
|
|
232
|
+
|
|
233
|
+
def __init__(self, config_issue: str, **kwargs):
|
|
234
|
+
message = f'Configuration error: {config_issue}'
|
|
235
|
+
super().__init__(message, **kwargs)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def handle_exception(func):
|
|
239
|
+
"""Decorator to handle exceptions and convert them to LattifAI errors."""
|
|
240
|
+
|
|
241
|
+
def wrapper(*args, **kwargs):
|
|
242
|
+
try:
|
|
243
|
+
return func(*args, **kwargs)
|
|
244
|
+
except LattifAIError:
|
|
245
|
+
# Re-raise LattifAI errors as-is
|
|
246
|
+
raise
|
|
247
|
+
except Exception as e:
|
|
248
|
+
# Convert other exceptions to LattifAI errors
|
|
249
|
+
error_msg = f'Unexpected error in {func.__name__}: {str(e)}'
|
|
250
|
+
context = {
|
|
251
|
+
'function': func.__name__,
|
|
252
|
+
'original_exception': e.__class__.__name__,
|
|
253
|
+
'traceback': traceback.format_exc(),
|
|
254
|
+
}
|
|
255
|
+
raise LattifAIError(error_msg, context=context) from e
|
|
256
|
+
|
|
257
|
+
return wrapper
|
lattifai/io/__init__.py
CHANGED
|
@@ -2,11 +2,31 @@ from typing import List, Optional
|
|
|
2
2
|
|
|
3
3
|
from lhotse.utils import Pathlike
|
|
4
4
|
|
|
5
|
+
from .gemini_reader import GeminiReader, GeminiSegment
|
|
6
|
+
from .gemini_writer import GeminiWriter
|
|
5
7
|
from .reader import SubtitleFormat, SubtitleReader
|
|
6
8
|
from .supervision import Supervision
|
|
9
|
+
from .utils import (
|
|
10
|
+
ALL_SUBTITLE_FORMATS,
|
|
11
|
+
INPUT_SUBTITLE_FORMATS,
|
|
12
|
+
OUTPUT_SUBTITLE_FORMATS,
|
|
13
|
+
SUBTITLE_FORMATS,
|
|
14
|
+
)
|
|
7
15
|
from .writer import SubtitleWriter
|
|
8
16
|
|
|
9
|
-
__all__ = [
|
|
17
|
+
__all__ = [
|
|
18
|
+
'SubtitleReader',
|
|
19
|
+
'SubtitleWriter',
|
|
20
|
+
'SubtitleIO',
|
|
21
|
+
'Supervision',
|
|
22
|
+
'GeminiReader',
|
|
23
|
+
'GeminiWriter',
|
|
24
|
+
'GeminiSegment',
|
|
25
|
+
'SUBTITLE_FORMATS',
|
|
26
|
+
'INPUT_SUBTITLE_FORMATS',
|
|
27
|
+
'OUTPUT_SUBTITLE_FORMATS',
|
|
28
|
+
'ALL_SUBTITLE_FORMATS',
|
|
29
|
+
]
|
|
10
30
|
|
|
11
31
|
|
|
12
32
|
class SubtitleIO:
|