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/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
|
-
|
|
18
|
+
'--input-format',
|
|
19
|
+
type=click.Choice(INPUT_SUBTITLE_FORMATS, case_sensitive=False),
|
|
13
20
|
default='auto',
|
|
14
|
-
help='Input
|
|
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',
|
|
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
|
-
'-
|
|
28
|
-
'--
|
|
29
|
-
|
|
30
|
-
default=
|
|
31
|
-
help='
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
219
|
+
Download media and subtitles from YouTube for further alignment.
|
|
56
220
|
"""
|
|
57
|
-
from lattifai import
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
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
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
207
|
+
for pattern in patterns:
|
|
208
|
+
if re.search(pattern, url):
|
|
209
|
+
return True
|
|
210
|
+
return False
|