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/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'
lattifai/bin/subtitle.py CHANGED
@@ -1,12 +1,16 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+
1
4
  import click
2
5
  from lhotse.utils import Pathlike
3
6
 
4
7
  from lattifai.bin.cli_base import cli
8
+ from lattifai.io import SUBTITLE_FORMATS
5
9
 
6
10
 
7
11
  @cli.group()
8
12
  def subtitle():
9
- """Group of commands used to convert subtitle format."""
13
+ """Commands for subtitle format conversion and management."""
10
14
  pass
11
15
 
12
16
 
@@ -26,7 +30,181 @@ def convert(
26
30
  """
27
31
  Convert subtitle file to another format.
28
32
  """
29
- import pysubs2
33
+ if str(output_subtitle_path).lower().endswith('.TextGrid'.lower()):
34
+ from lattifai.io import SubtitleIO
35
+
36
+ alignments = SubtitleIO.read(input_subtitle_path)
37
+ SubtitleIO.write(alignments, output_subtitle_path)
38
+ else:
39
+ import pysubs2
40
+
41
+ subtitle = pysubs2.load(input_subtitle_path)
42
+
43
+ subtitle.save(output_subtitle_path)
44
+
45
+
46
+ @subtitle.command()
47
+ @click.argument('url', type=str, required=True)
48
+ @click.option(
49
+ '--output-dir',
50
+ '--output_dir',
51
+ '-o',
52
+ type=click.Path(file_okay=False, dir_okay=True),
53
+ default='.',
54
+ help='Output directory for downloaded subtitle files (default: current directory).',
55
+ )
56
+ @click.option(
57
+ '--output-format',
58
+ '--output_format',
59
+ '-f',
60
+ type=click.Choice(SUBTITLE_FORMATS + ['best'], case_sensitive=False),
61
+ default='best',
62
+ help='Preferred subtitle format to download (default: best available).',
63
+ )
64
+ @click.option('--force-overwrite', '-F', is_flag=True, help='Overwrite existing files without prompting.')
65
+ @click.option(
66
+ '--lang',
67
+ '-l',
68
+ '-L',
69
+ '--subtitle-lang',
70
+ '--subtitle_lang',
71
+ type=str,
72
+ help='Specific subtitle language/track to download (e.g., "en").',
73
+ )
74
+ def download(
75
+ url: str,
76
+ output_dir: str,
77
+ output_format: str,
78
+ force_overwrite: bool,
79
+ lang: str,
80
+ ):
81
+ """
82
+ Download subtitles from YouTube URL using yt-dlp.
83
+
84
+ URL should be a valid YouTube URL (e.g., https://www.youtube.com/watch?v=VIDEO_ID).
85
+ """
86
+ # Import here to avoid circular imports and keep startup fast
87
+ from lattifai.workflows.youtube import YouTubeDownloader
88
+
89
+ # Validate URL format
90
+ if not _is_valid_youtube_url(url):
91
+ click.echo(f'Error: Invalid YouTube URL format: {url}', err=True)
92
+ click.echo('Please provide a valid YouTube URL (e.g., https://www.youtube.com/watch?v=VIDEO_ID)', err=True)
93
+ raise click.Abort()
94
+
95
+ # Convert relative path to absolute
96
+ output_path = Path(output_dir).resolve()
97
+
98
+ # Create output directory if it doesn't exist
99
+ output_path.mkdir(parents=True, exist_ok=True)
100
+
101
+ click.echo(f'Downloading subtitles from: {url}')
102
+ click.echo(f' Output directory: {output_path}')
103
+ click.echo(f' Preferred format: {output_format}')
104
+ if lang:
105
+ click.echo(f' Subtitle language: {lang}')
106
+ else:
107
+ click.echo(' Subtitle language: All available')
108
+
109
+ # Initialize downloader and download
110
+ downloader = YouTubeDownloader()
111
+
112
+ async def download_subtitles():
113
+ try:
114
+ result = await downloader.download_subtitles(
115
+ url=url,
116
+ output_dir=str(output_path),
117
+ force_overwrite=force_overwrite,
118
+ subtitle_lang=lang,
119
+ )
120
+
121
+ if result:
122
+ click.echo('✅ Subtitles downloaded successfully!')
123
+ return result
124
+ else:
125
+ click.echo('⚠️ No subtitles available for this video')
126
+ return None
127
+
128
+ except Exception as e:
129
+ click.echo(f'❌ Error downloading subtitles: {str(e)}', err=True)
130
+ raise click.Abort()
131
+
132
+ # Run the async function
133
+ result = asyncio.run(download_subtitles())
134
+
135
+ if result:
136
+ if result == 'gemini':
137
+ click.echo('✨ Gemini transcription selected (use the agent command to transcribe)')
138
+ else:
139
+ click.echo(f'📄 Subtitle file saved to: {result}')
140
+
141
+
142
+ @subtitle.command()
143
+ @click.argument('url', type=str, required=True)
144
+ def list_subs(url: str):
145
+ """
146
+ List available subtitle tracks for a YouTube video.
147
+
148
+ URL should be a valid YouTube URL (e.g., https://www.youtube.com/watch?v=VIDEO_ID)
149
+ """
150
+ # Import here to avoid circular imports and keep startup fast
151
+ from lattifai.workflows.youtube import YouTubeDownloader
152
+
153
+ # Validate URL format
154
+ if not _is_valid_youtube_url(url):
155
+ click.echo(f'Error: Invalid YouTube URL format: {url}', err=True)
156
+ click.echo('Please provide a valid YouTube URL (e.g., https://www.youtube.com/watch?v=VIDEO_ID)', err=True)
157
+ raise click.Abort()
158
+
159
+ click.echo(f'Listing available subtitles for: {url}')
160
+
161
+ # Initialize downloader
162
+ downloader = YouTubeDownloader()
163
+
164
+ async def list_available_subtitles():
165
+ try:
166
+ result = await downloader.list_available_subtitles(url)
167
+
168
+ if result:
169
+ click.echo('📋 Available subtitle tracks:')
170
+ for subtitle_info in result:
171
+ click.echo(f' 🎬 Language: {subtitle_info["language"]} - {subtitle_info["name"]}')
172
+ click.echo(f' 📄 Formats: {", ".join(subtitle_info["formats"])}')
173
+ click.echo()
174
+
175
+ click.echo('💡 To download a specific track, use:')
176
+ click.echo(f' lattifai subtitle download "{url}" --lang <language_code>')
177
+ click.echo(' Example: lattifai subtitle download "{}" --lang en-JkeT_87f4cc'.format(url))
178
+ else:
179
+ click.echo('⚠️ No subtitles available for this video')
180
+
181
+ except Exception as e:
182
+ click.echo(f'❌ Error listing subtitles: {str(e)}', err=True)
183
+ raise click.Abort()
184
+
185
+ # Run the async function
186
+ asyncio.run(list_available_subtitles())
187
+
188
+
189
+ def _is_valid_youtube_url(url: str) -> bool:
190
+ """
191
+ Validate if the URL is a valid YouTube URL format.
192
+
193
+ Supports various YouTube URL formats:
194
+ - https://www.youtube.com/watch?v=VIDEO_ID
195
+ - https://youtu.be/VIDEO_ID
196
+ - https://www.youtube.com/shorts/VIDEO_ID
197
+ - https://m.youtube.com/watch?v=VIDEO_ID
198
+ """
199
+ import re
200
+
201
+ patterns = [
202
+ r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/shorts/)([a-zA-Z0-9_-]{11})',
203
+ r'youtube\.com/embed/([a-zA-Z0-9_-]{11})',
204
+ r'youtube\.com/v/([a-zA-Z0-9_-]{11})',
205
+ ]
30
206
 
31
- subtitle = pysubs2.load(input_subtitle_path)
32
- subtitle.save(output_subtitle_path)
207
+ for pattern in patterns:
208
+ if re.search(pattern, url):
209
+ return True
210
+ return False