lattifai 0.2.5__py3-none-any.whl → 0.4.1__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 CHANGED
@@ -61,11 +61,16 @@ def __getattr__(name):
61
61
  from .client import LattifAI
62
62
 
63
63
  return LattifAI
64
+ if name == 'AsyncLattifAI':
65
+ from .client import AsyncLattifAI
66
+
67
+ return AsyncLattifAI
64
68
  raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
65
69
 
66
70
 
67
71
  __all__ = [
68
72
  'LattifAI', # noqa: F822
73
+ 'AsyncLattifAI', # noqa: F822
69
74
  'LattifAIError',
70
75
  'AudioProcessingError',
71
76
  'AudioLoadError',
lattifai/base_client.py CHANGED
@@ -113,3 +113,14 @@ class AsyncAPIClient(BaseAPIClient):
113
113
  ) -> httpx.Response:
114
114
  """Make an HTTP request."""
115
115
  return await self._client.request(method=method, url=url, json=json, files=files, **kwargs)
116
+
117
+ async def post(
118
+ self,
119
+ api_endpoint: str,
120
+ *,
121
+ json: Optional[Dict[str, Any]] = None,
122
+ files: Optional[Dict[str, Any]] = None,
123
+ **kwargs,
124
+ ) -> httpx.Response:
125
+ """Make a POST request to the specified API endpoint."""
126
+ return await self._request('POST', api_endpoint, json=json, files=files, **kwargs)
lattifai/bin/__init__.py CHANGED
@@ -1,2 +1,3 @@
1
+ from .agent import * # noqa
1
2
  from .align import * # noqa
2
3
  from .subtitle import * # noqa
lattifai/bin/agent.py ADDED
@@ -0,0 +1,326 @@
1
+ """
2
+ Agent command for YouTube workflow
3
+ """
4
+
5
+ import asyncio
6
+ import os
7
+ import sys
8
+ from typing import List, Optional
9
+
10
+ import click
11
+ import colorful
12
+ from lhotse.utils import Pathlike
13
+
14
+ from lattifai.bin.cli_base import cli
15
+ from lattifai.io import OUTPUT_SUBTITLE_FORMATS
16
+
17
+
18
+ @cli.command()
19
+ @click.option('--youtube', '--yt', is_flag=True, help='Process YouTube URL through agentic workflow.')
20
+ @click.option(
21
+ '--api-key',
22
+ '--api_key',
23
+ type=str,
24
+ help='LattifAI API key for alignment (overrides LATTIFAI_API_KEY env var).',
25
+ )
26
+ @click.option(
27
+ '--gemini-api-key',
28
+ '--gemini_api_key',
29
+ type=str,
30
+ help='Gemini API key for transcription (overrides GEMINI_API_KEY env var).',
31
+ )
32
+ @click.option(
33
+ '-D',
34
+ '--device',
35
+ type=click.Choice(['cpu', 'cuda', 'mps'], case_sensitive=False),
36
+ default='cpu',
37
+ help='Device to use for inference.',
38
+ )
39
+ @click.option(
40
+ '-M',
41
+ '--model-name-or-path',
42
+ '--model_name_or_path',
43
+ type=str,
44
+ default='Lattifai/Lattice-1-Alpha',
45
+ help='Model name or path for alignment.',
46
+ )
47
+ @click.option(
48
+ '--media-format',
49
+ '--media_format',
50
+ type=click.Choice(
51
+ ['mp3', 'wav', 'm4a', 'aac', 'opus', 'mp4', 'webm', 'mkv', 'avi', 'mov', 'flv', 'wmv', 'mpeg', 'mpg', '3gp'],
52
+ case_sensitive=False,
53
+ ),
54
+ default='mp4',
55
+ help='Media format for YouTube download (audio or video).',
56
+ )
57
+ @click.option(
58
+ '--output-format',
59
+ '--output_format',
60
+ type=click.Choice(OUTPUT_SUBTITLE_FORMATS, case_sensitive=False),
61
+ default='srt',
62
+ help='Subtitle output format.',
63
+ )
64
+ @click.option(
65
+ '--output-dir',
66
+ '--output_dir',
67
+ type=click.Path(exists=False, file_okay=False, dir_okay=True),
68
+ help='Output directory for generated files (default: current directory).',
69
+ )
70
+ @click.option(
71
+ '--max-retries',
72
+ '--max_retries',
73
+ type=int,
74
+ default=0,
75
+ help='Maximum number of retries for failed steps.',
76
+ )
77
+ @click.option(
78
+ '-S',
79
+ '--split-sentence',
80
+ '--split_sentence',
81
+ is_flag=True,
82
+ default=False,
83
+ help='Re-segment subtitles by semantics.',
84
+ )
85
+ @click.option(
86
+ '--word-level',
87
+ '--word_level',
88
+ is_flag=True,
89
+ default=False,
90
+ help='Include word-level alignment timestamps in output (for JSON, TextGrid, and subtitle formats).',
91
+ )
92
+ @click.option('--verbose', '-v', is_flag=True, help='Enable verbose logging.')
93
+ @click.option('--force', '-f', is_flag=True, help='Force overwrite existing files without confirmation.')
94
+ @click.argument('url', type=str, required=True)
95
+ def agent(
96
+ youtube: bool,
97
+ url: str,
98
+ api_key: Optional[str] = None,
99
+ gemini_api_key: Optional[str] = None,
100
+ device: str = 'cpu',
101
+ model_name_or_path: str = 'Lattifai/Lattice-1-Alpha',
102
+ media_format: str = 'mp4',
103
+ output_format: str = 'srt',
104
+ output_dir: Optional[str] = None,
105
+ max_retries: int = 0,
106
+ split_sentence: bool = False,
107
+ word_level: bool = False,
108
+ verbose: bool = False,
109
+ force: bool = False,
110
+ ):
111
+ """
112
+ LattifAI Agentic Workflow Agent
113
+
114
+ Process multimedia content through intelligent agent-based pipelines.
115
+
116
+ Example:
117
+ lattifai agent --youtube https://www.youtube.com/watch?v=example
118
+ """
119
+
120
+ if not youtube:
121
+ click.echo(colorful.red('❌ Please specify a workflow type. Use --youtube for YouTube processing.'))
122
+ return
123
+
124
+ # Setup logging
125
+ import logging
126
+
127
+ log_level = logging.DEBUG if verbose else logging.INFO
128
+ logging.basicConfig(level=log_level, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
129
+
130
+ # Set default output directory
131
+ if not output_dir:
132
+ output_dir = os.getcwd()
133
+
134
+ # Get API keys
135
+ lattifai_api_key = api_key or os.getenv('LATTIFAI_API_KEY')
136
+ gemini_key = gemini_api_key or os.getenv('GEMINI_API_KEY')
137
+
138
+ if not gemini_key:
139
+ click.echo(
140
+ colorful.red(
141
+ '❌ Gemini API key is required. Set GEMINI_API_KEY environment variable or use --gemini-api-key option.'
142
+ )
143
+ )
144
+ return
145
+
146
+ try:
147
+ # Run the YouTube workflow
148
+ asyncio.run(
149
+ _run_youtube_workflow(
150
+ url=url,
151
+ lattifai_api_key=lattifai_api_key,
152
+ gemini_api_key=gemini_key,
153
+ device=device,
154
+ model_name_or_path=model_name_or_path,
155
+ media_format=media_format,
156
+ output_format=output_format,
157
+ output_dir=output_dir,
158
+ max_retries=max_retries,
159
+ split_sentence=split_sentence,
160
+ word_level=word_level,
161
+ force_overwrite=force,
162
+ )
163
+ )
164
+
165
+ except KeyboardInterrupt:
166
+ click.echo(colorful.yellow('\n⚠️ Process interrupted by user'))
167
+ sys.exit(1)
168
+ except Exception as e:
169
+ from lattifai.errors import LattifAIError
170
+
171
+ # Extract error message without support info (to avoid duplication)
172
+ if isinstance(e, LattifAIError):
173
+ # Use the get_message() method which includes proper formatting
174
+ click.echo(colorful.red('❌ Workflow failed:'))
175
+ click.echo(e.get_message())
176
+ # Show support info once at the end
177
+ click.echo(e.get_support_info())
178
+ else:
179
+ click.echo(colorful.red(f'❌ Workflow failed: {str(e)}'))
180
+
181
+ if verbose:
182
+ import traceback
183
+
184
+ traceback.print_exc()
185
+ sys.exit(1)
186
+
187
+
188
+ async def _run_youtube_workflow(
189
+ url: str,
190
+ lattifai_api_key: Optional[str],
191
+ gemini_api_key: str,
192
+ device: str,
193
+ model_name_or_path: str,
194
+ media_format: str,
195
+ output_format: str,
196
+ output_dir: str,
197
+ max_retries: int,
198
+ split_sentence: bool = False,
199
+ word_level: bool = False,
200
+ force_overwrite: bool = False,
201
+ ):
202
+ """Run the YouTube processing workflow"""
203
+
204
+ click.echo(colorful.cyan('🚀 LattifAI Agentic Workflow - YouTube Processing'))
205
+ click.echo(f'📺 YouTube URL: {url}')
206
+ click.echo(f'🎬 Media format: {media_format}')
207
+ click.echo(f'📝 Output format: {output_format}')
208
+ click.echo(f'📁 Output directory: {output_dir}')
209
+ click.echo(f'🔄 Max retries: {max_retries}')
210
+ click.echo()
211
+
212
+ # Import workflow components
213
+ from lattifai.client import AsyncLattifAI
214
+ from lattifai.workflows import YouTubeSubtitleAgent
215
+ from lattifai.workflows.gemini import GeminiTranscriber
216
+ from lattifai.workflows.youtube import YouTubeDownloader
217
+
218
+ # Initialize components with their configuration (only persistent config, not runtime params)
219
+ downloader = YouTubeDownloader()
220
+ transcriber = GeminiTranscriber(api_key=gemini_api_key)
221
+ aligner = AsyncLattifAI(model_name_or_path=model_name_or_path, device=device, api_key=lattifai_api_key)
222
+
223
+ # Initialize agent with components
224
+ agent = YouTubeSubtitleAgent(
225
+ downloader=downloader,
226
+ transcriber=transcriber,
227
+ aligner=aligner,
228
+ max_retries=max_retries,
229
+ )
230
+
231
+ # Process the URL
232
+ result = await agent.process_youtube_url(
233
+ url=url,
234
+ output_dir=output_dir,
235
+ media_format=media_format,
236
+ force_overwrite=force_overwrite,
237
+ output_format=output_format,
238
+ split_sentence=split_sentence,
239
+ word_level=word_level,
240
+ )
241
+
242
+ # Display results
243
+ click.echo(colorful.bold_white_on_green('🎉 Workflow completed successfully!'))
244
+ click.echo()
245
+ click.echo(colorful.bold_white_on_green('📊 Results:'))
246
+
247
+ # Show metadata
248
+ metadata = result.get('metadata', {})
249
+ if metadata:
250
+ click.echo(f'🎬 Title: {metadata.get("title", "Unknown")}')
251
+ click.echo(f'👤 Uploader: {metadata.get("uploader", "Unknown").strip()}')
252
+ click.echo(f'⏱️ Duration: {metadata.get("duration", 0)} seconds')
253
+ click.echo()
254
+
255
+ # Show exported files
256
+ exported_files = result.get('exported_files', {})
257
+ if exported_files:
258
+ click.echo(colorful.bold_white_on_green('📄 Generated subtitle files:'))
259
+ for format_name, file_path in exported_files.items():
260
+ click.echo(f' {format_name.upper()}: {file_path}')
261
+ click.echo()
262
+
263
+ # Show subtitle count
264
+ subtitle_count = result.get('subtitle_count', 0)
265
+ click.echo(f'📝 Generated {subtitle_count} subtitle segments')
266
+
267
+ click.echo(colorful.bold_white_on_green('✨ All done! Your aligned subtitles are ready.'))
268
+
269
+
270
+ # Add dependencies check
271
+ def check_dependencies():
272
+ """Check if required dependencies are installed"""
273
+ missing_deps = []
274
+
275
+ try:
276
+ from google import genai
277
+ except ImportError:
278
+ missing_deps.append('google-genai')
279
+
280
+ try:
281
+ import yt_dlp
282
+ except ImportError:
283
+ missing_deps.append('yt-dlp')
284
+
285
+ try:
286
+ from dotenv import load_dotenv
287
+ except ImportError:
288
+ missing_deps.append('python-dotenv')
289
+
290
+ if missing_deps:
291
+ click.echo(colorful.red('❌ Missing required dependencies:'))
292
+ for dep in missing_deps:
293
+ click.echo(f' - {dep}')
294
+ click.echo()
295
+ click.echo('Install them with:')
296
+ click.echo(f' pip install {" ".join(missing_deps)}')
297
+ return False
298
+
299
+ return True
300
+
301
+
302
+ # Check dependencies when module is imported
303
+ if not check_dependencies():
304
+ pass # Don't exit on import, let the command handle it
305
+
306
+
307
+ if __name__ == '__main__':
308
+ import os
309
+
310
+ asyncio.run(
311
+ _run_youtube_workflow(
312
+ # url='https://www.youtube.com/watch?v=7nv1snJRCEI',
313
+ url='https://www.youtube.com/watch?v=DQacCB9tDaw',
314
+ lattifai_api_key=os.getenv('LATTIFAI_API_KEY'),
315
+ gemini_api_key=os.getenv('GEMINI_API_KEY', ''),
316
+ device='mps',
317
+ model_name_or_path='Lattifai/Lattice-1-Alpha',
318
+ media_format='mp3',
319
+ output_format='TextGrid',
320
+ output_dir='~/Downloads/lattifai_youtube',
321
+ max_retries=0,
322
+ split_sentence=True,
323
+ word_level=False,
324
+ force_overwrite=False,
325
+ )
326
+ )
lattifai/bin/align.py CHANGED
@@ -1,17 +1,40 @@
1
+ import asyncio
2
+ import os
3
+ from pathlib import Path
4
+
1
5
  import click
2
6
  import colorful
3
7
  from lhotse.utils import Pathlike
4
8
 
5
9
  from lattifai.bin.cli_base import cli
10
+ from lattifai.client import AsyncLattifAI, LattifAI
11
+ from lattifai.io import INPUT_SUBTITLE_FORMATS, OUTPUT_SUBTITLE_FORMATS
6
12
 
7
13
 
8
14
  @cli.command()
9
15
  @click.option(
10
16
  '-F',
11
17
  '--input_format',
12
- type=click.Choice(['srt', 'vtt', 'ass', 'txt', 'auto'], case_sensitive=False),
18
+ '--input-format',
19
+ type=click.Choice(INPUT_SUBTITLE_FORMATS, case_sensitive=False),
13
20
  default='auto',
14
- help='Input Subtitle format.',
21
+ help='Input subtitle format.',
22
+ )
23
+ @click.option(
24
+ '-S',
25
+ '--split-sentence',
26
+ '--split_sentence',
27
+ is_flag=True,
28
+ default=False,
29
+ help='Re-segment subtitles by semantics.',
30
+ )
31
+ @click.option(
32
+ '-W',
33
+ '--word-level',
34
+ '--word_level',
35
+ is_flag=True,
36
+ default=False,
37
+ help='Include word-level alignment timestamps in output (for JSON, TextGrid, and subtitle formats).',
15
38
  )
16
39
  @click.option(
17
40
  '-D',
@@ -21,17 +44,22 @@ from lattifai.bin.cli_base import cli
21
44
  help='Device to use for inference.',
22
45
  )
23
46
  @click.option(
24
- '-M', '--model_name_or_path', type=str, default='Lattifai/Lattice-1-Alpha', help='Lattifai model name or path'
47
+ '-M',
48
+ '--model-name-or-path',
49
+ '--model_name_or_path',
50
+ type=str,
51
+ default='Lattifai/Lattice-1-Alpha',
52
+ help='Model name or path for alignment.',
25
53
  )
26
54
  @click.option(
27
- '-S',
28
- '--split_sentence',
29
- is_flag=True,
30
- default=False,
31
- help='Re-segment subtitles by semantics.',
55
+ '--api-key',
56
+ '--api_key',
57
+ type=str,
58
+ default=None,
59
+ help='API key for LattifAI.',
32
60
  )
33
61
  @click.argument(
34
- 'input_audio_path',
62
+ 'input_media_path',
35
63
  type=click.Path(exists=True, dir_okay=False),
36
64
  )
37
65
  @click.argument(
@@ -43,24 +71,228 @@ from lattifai.bin.cli_base import cli
43
71
  type=click.Path(allow_dash=True),
44
72
  )
45
73
  def align(
46
- input_audio_path: Pathlike,
74
+ input_media_path: Pathlike,
47
75
  input_subtitle_path: Pathlike,
48
76
  output_subtitle_path: Pathlike,
49
77
  input_format: str = 'auto',
78
+ split_sentence: bool = False,
79
+ word_level: bool = False,
50
80
  device: str = 'cpu',
51
81
  model_name_or_path: str = 'Lattifai/Lattice-1-Alpha',
82
+ api_key: str = None,
83
+ ):
84
+ """
85
+ Command used to align media(audio/video) with subtitles
86
+ """
87
+ try:
88
+ client = LattifAI(model_name_or_path=model_name_or_path, device=device, api_key=api_key)
89
+ client.alignment(
90
+ input_media_path,
91
+ input_subtitle_path,
92
+ format=input_format.lower(),
93
+ split_sentence=split_sentence,
94
+ return_details=word_level,
95
+ output_subtitle_path=output_subtitle_path,
96
+ )
97
+ click.echo(colorful.green(f'✅ Alignment completed successfully: {output_subtitle_path}'))
98
+ except Exception as e:
99
+ from lattifai.errors import LattifAIError
100
+
101
+ # Display error message
102
+ if isinstance(e, LattifAIError):
103
+ click.echo(colorful.red('❌ Alignment failed:'))
104
+ click.echo(e.get_message())
105
+ # Show support info
106
+ click.echo(e.get_support_info())
107
+ else:
108
+ click.echo(colorful.red(f'❌ Alignment failed: {str(e)}'))
109
+
110
+ raise click.ClickException('Alignment failed')
111
+
112
+
113
+ @cli.command()
114
+ @click.option(
115
+ '-M',
116
+ '--media-format',
117
+ '--media_format',
118
+ type=click.Choice(
119
+ [
120
+ # Audio formats
121
+ 'mp3',
122
+ 'wav',
123
+ 'm4a',
124
+ 'aac',
125
+ 'flac',
126
+ 'ogg',
127
+ 'opus',
128
+ 'aiff',
129
+ # Video formats
130
+ 'mp4',
131
+ 'webm',
132
+ 'mkv',
133
+ 'avi',
134
+ 'mov',
135
+ ],
136
+ case_sensitive=False,
137
+ ),
138
+ default='mp3',
139
+ help='Media format for YouTube download (audio or video).',
140
+ )
141
+ @click.option(
142
+ '-S',
143
+ '--split-sentence',
144
+ '--split_sentence',
145
+ is_flag=True,
146
+ default=False,
147
+ help='Re-segment subtitles by semantics.',
148
+ )
149
+ @click.option(
150
+ '-W',
151
+ '--word-level',
152
+ '--word_level',
153
+ is_flag=True,
154
+ default=False,
155
+ help='Include word-level alignment timestamps in output (for JSON, TextGrid, and subtitle formats).',
156
+ )
157
+ @click.option(
158
+ '-O',
159
+ '--output-dir',
160
+ '--output_dir',
161
+ type=click.Path(file_okay=False, dir_okay=True, writable=True),
162
+ default='.',
163
+ help='Output directory (default: current directory).',
164
+ )
165
+ @click.option(
166
+ '-D',
167
+ '--device',
168
+ type=click.Choice(['cpu', 'cuda', 'mps'], case_sensitive=False),
169
+ default='cpu',
170
+ help='Device to use for inference.',
171
+ )
172
+ @click.option(
173
+ '-M',
174
+ '--model-name-or-path',
175
+ '--model_name_or_path',
176
+ type=str,
177
+ default='Lattifai/Lattice-1-Alpha',
178
+ help='Model name or path for alignment.',
179
+ )
180
+ @click.option(
181
+ '--api-key',
182
+ '--api_key',
183
+ type=str,
184
+ default=None,
185
+ help='API key for LattifAI.',
186
+ )
187
+ @click.option(
188
+ '--gemini-api-key',
189
+ '--gemini_api_key',
190
+ type=str,
191
+ default=None,
192
+ help='Gemini API key for transcription fallback when subtitles are unavailable.',
193
+ )
194
+ @click.option(
195
+ '-F',
196
+ '--output-format',
197
+ '--output_format',
198
+ type=click.Choice(OUTPUT_SUBTITLE_FORMATS, case_sensitive=False),
199
+ default='vtt',
200
+ help='Subtitle output format.',
201
+ )
202
+ @click.argument(
203
+ 'yt_url',
204
+ type=str,
205
+ )
206
+ def youtube(
207
+ yt_url: str,
208
+ media_format: str = 'mp3',
52
209
  split_sentence: bool = False,
210
+ word_level: bool = False,
211
+ output_dir: str = '.',
212
+ device: str = 'cpu',
213
+ model_name_or_path: str = 'Lattifai/Lattice-1-Alpha',
214
+ api_key: str = None,
215
+ gemini_api_key: str = None,
216
+ output_format: str = 'vtt',
53
217
  ):
54
218
  """
55
- Command used to align audio with subtitles
219
+ Download media and subtitles from YouTube for further alignment.
56
220
  """
57
- from lattifai import LattifAI
58
-
59
- client = LattifAI(model_name_or_path=model_name_or_path, device=device)
60
- client.alignment(
61
- input_audio_path,
62
- input_subtitle_path,
63
- format=input_format.lower(),
64
- split_sentence=split_sentence,
65
- output_subtitle_path=output_subtitle_path,
66
- )
221
+ from lattifai.workflows.gemini import GeminiTranscriber
222
+ from lattifai.workflows.youtube import YouTubeDownloader, YouTubeSubtitleAgent
223
+
224
+ # Get Gemini API key
225
+ gemini_key = gemini_api_key or os.getenv('GEMINI_API_KEY')
226
+ if not gemini_key:
227
+ click.echo(
228
+ colorful.red(
229
+ '❌ Gemini API key is required. Set GEMINI_API_KEY environment variable or use --gemini-api-key option.'
230
+ )
231
+ )
232
+ raise click.ClickException('Missing Gemini API key')
233
+
234
+ async def _process():
235
+ # Initialize components with their configuration (only config, not runtime params)
236
+ downloader = YouTubeDownloader()
237
+ transcriber = GeminiTranscriber(api_key=gemini_key)
238
+ aligner = AsyncLattifAI(api_key=api_key, model_name_or_path=model_name_or_path, device=device)
239
+
240
+ # Create agent with initialized components
241
+ agent = YouTubeSubtitleAgent(
242
+ downloader=downloader,
243
+ transcriber=transcriber,
244
+ aligner=aligner,
245
+ max_retries=0,
246
+ )
247
+
248
+ result = await agent.process_youtube_url(
249
+ url=yt_url,
250
+ output_dir=output_dir,
251
+ media_format=media_format,
252
+ force_overwrite=False,
253
+ output_format=output_format,
254
+ split_sentence=split_sentence,
255
+ word_level=word_level,
256
+ )
257
+ return result
258
+
259
+ try:
260
+ result = asyncio.run(_process())
261
+
262
+ # Display results
263
+ click.echo(colorful.green('✅ Processing completed!'))
264
+ click.echo()
265
+
266
+ # Show metadata
267
+ metadata = result.get('metadata', {})
268
+ if metadata:
269
+ click.echo(f'🎬 Title: {metadata.get("title", "Unknown")}')
270
+ click.echo(f'⏱️ Duration: {metadata.get("duration", 0)} seconds')
271
+ click.echo()
272
+
273
+ # Show exported files
274
+ exported_files = result.get('exported_files', {})
275
+ if exported_files:
276
+ click.echo(colorful.green('📄 Generated subtitle files:'))
277
+ for format_name, file_path in exported_files.items():
278
+ click.echo(f' {format_name}: {file_path}')
279
+ click.echo()
280
+
281
+ # Show subtitle count
282
+ subtitle_count = result.get('subtitle_count', 0)
283
+ click.echo(f'📝 Generated {subtitle_count} subtitle segments')
284
+
285
+ except Exception as e:
286
+ from lattifai.errors import LattifAIError
287
+
288
+ # Extract error message without support info (to avoid duplication)
289
+ if isinstance(e, LattifAIError):
290
+ # Use the get_message() method which includes proper formatting
291
+ click.echo(colorful.red('❌ Failed to process YouTube URL:'))
292
+ click.echo(e.get_message())
293
+ # Show support info once at the end
294
+ click.echo(e.get_support_info())
295
+ else:
296
+ click.echo(colorful.red(f'❌ Failed to process YouTube URL: {str(e)}'))
297
+
298
+ raise click.ClickException('Processing failed')
lattifai/bin/cli_base.py CHANGED
@@ -17,3 +17,8 @@ def cli():
17
17
  format='%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s',
18
18
  level=logging.INFO,
19
19
  )
20
+
21
+ import os
22
+
23
+ os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
24
+ os.environ['TOKENIZERS_PARALLELISM'] = 'FALSE'