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/client.py CHANGED
@@ -1,18 +1,26 @@
1
1
  """LattifAI client implementation."""
2
2
 
3
- import logging
3
+ import asyncio
4
4
  import os
5
- from pathlib import Path
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, LattifAIError, SyncAPIClient
13
- from lattifai.io import SubtitleFormat, SubtitleIO
14
- from lattifai.tokenizer import LatticeTokenizer
15
- from lattifai.workers import Lattice1AlphaWorker
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 LattifAIError(
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
- # Initialize components
56
- if not Path(model_name_or_path).exists():
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
- try:
61
- model_path = snapshot_download(repo_id=model_name_or_path, repo_type='model')
62
- except LocalEntryNotFoundError:
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
- ) -> str:
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
- export_format: Output format (srt, vtt, ass, txt)
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
- Aligned subtitles in specified format
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
- # step1: parse text or subtitles
104
- print(colorful.cyan(f'📖 Step 1: Reading subtitle file from {subtitle}'))
105
- supervisions = SubtitleIO.read(subtitle, format=format)
106
- print(colorful.green(f' ✓ Parsed {len(supervisions)} subtitle segments'))
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
- # step2: make lattice by call Lattifai API
109
- print(colorful.cyan('🔗 Step 2: Creating lattice graph from text'))
110
- lattice_id, lattice_graph = self.tokenizer.tokenize(supervisions, split_sentence=split_sentence)
111
- print(colorful.green(f' ✓ Generated lattice graph with ID: {lattice_id}'))
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
- # step3: align audio with text
114
- print(colorful.cyan(f'🎵 Step 3: Performing alignment on audio file: {audio}'))
115
- lattice_results = self.worker.alignment(audio, lattice_graph)
116
- print(colorful.green(' ✓ Alignment completed successfully'))
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
- # step5: export alignments to target format
124
- if output_subtitle_path:
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
- return output_subtitle_path or alignments
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) == 4:
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(audio, subtitle, output_subtitle_path=output, split_sentence=True)
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__ = ['SubtitleReader', 'SubtitleWriter', 'SubtitleIO', 'Supervision']
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: