lattifai 0.4.5__py3-none-any.whl → 0.4.6__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 +26 -27
- lattifai/base_client.py +7 -7
- lattifai/bin/agent.py +90 -91
- lattifai/bin/align.py +110 -111
- lattifai/bin/cli_base.py +3 -3
- lattifai/bin/subtitle.py +45 -45
- lattifai/client.py +56 -56
- lattifai/errors.py +73 -73
- lattifai/io/__init__.py +12 -11
- lattifai/io/gemini_reader.py +30 -30
- lattifai/io/gemini_writer.py +17 -17
- lattifai/io/reader.py +13 -12
- lattifai/io/supervision.py +3 -3
- lattifai/io/text_parser.py +43 -16
- lattifai/io/utils.py +4 -4
- lattifai/io/writer.py +31 -19
- lattifai/tokenizer/__init__.py +1 -1
- lattifai/tokenizer/phonemizer.py +3 -3
- lattifai/tokenizer/tokenizer.py +83 -82
- lattifai/utils.py +15 -15
- lattifai/workers/__init__.py +1 -1
- lattifai/workers/lattice1_alpha.py +46 -46
- lattifai/workflows/__init__.py +11 -11
- lattifai/workflows/agents.py +2 -0
- lattifai/workflows/base.py +22 -22
- lattifai/workflows/file_manager.py +182 -182
- lattifai/workflows/gemini.py +29 -29
- lattifai/workflows/prompts/__init__.py +4 -4
- lattifai/workflows/youtube.py +233 -233
- {lattifai-0.4.5.dist-info → lattifai-0.4.6.dist-info}/METADATA +7 -9
- lattifai-0.4.6.dist-info/RECORD +39 -0
- {lattifai-0.4.5.dist-info → lattifai-0.4.6.dist-info}/licenses/LICENSE +1 -1
- lattifai-0.4.5.dist-info/RECORD +0 -39
- {lattifai-0.4.5.dist-info → lattifai-0.4.6.dist-info}/WHEEL +0 -0
- {lattifai-0.4.5.dist-info → lattifai-0.4.6.dist-info}/entry_points.txt +0 -0
- {lattifai-0.4.5.dist-info → lattifai-0.4.6.dist-info}/top_level.txt +0 -0
lattifai/bin/align.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
|
|
5
4
|
import click
|
|
6
5
|
import colorful
|
|
@@ -13,74 +12,74 @@ from lattifai.io import INPUT_SUBTITLE_FORMATS, OUTPUT_SUBTITLE_FORMATS
|
|
|
13
12
|
|
|
14
13
|
@cli.command()
|
|
15
14
|
@click.option(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
"-F",
|
|
16
|
+
"--input_format",
|
|
17
|
+
"--input-format",
|
|
19
18
|
type=click.Choice(INPUT_SUBTITLE_FORMATS, case_sensitive=False),
|
|
20
|
-
default=
|
|
21
|
-
help=
|
|
19
|
+
default="auto",
|
|
20
|
+
help="Input subtitle format.",
|
|
22
21
|
)
|
|
23
22
|
@click.option(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
"-S",
|
|
24
|
+
"--split-sentence",
|
|
25
|
+
"--split_sentence",
|
|
27
26
|
is_flag=True,
|
|
28
27
|
default=False,
|
|
29
|
-
help=
|
|
28
|
+
help="Re-segment subtitles by semantics.",
|
|
30
29
|
)
|
|
31
30
|
@click.option(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
"-W",
|
|
32
|
+
"--word-level",
|
|
33
|
+
"--word_level",
|
|
35
34
|
is_flag=True,
|
|
36
35
|
default=False,
|
|
37
|
-
help=
|
|
36
|
+
help="Include word-level alignment timestamps in output (for JSON, TextGrid, and subtitle formats).",
|
|
38
37
|
)
|
|
39
38
|
@click.option(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
type=click.Choice([
|
|
43
|
-
default=
|
|
44
|
-
help=
|
|
39
|
+
"-D",
|
|
40
|
+
"--device",
|
|
41
|
+
type=click.Choice(["cpu", "cuda", "mps"], case_sensitive=False),
|
|
42
|
+
default="cpu",
|
|
43
|
+
help="Device to use for inference.",
|
|
45
44
|
)
|
|
46
45
|
@click.option(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
"-M",
|
|
47
|
+
"--model-name-or-path",
|
|
48
|
+
"--model_name_or_path",
|
|
50
49
|
type=str,
|
|
51
|
-
default=
|
|
52
|
-
help=
|
|
50
|
+
default="Lattifai/Lattice-1-Alpha",
|
|
51
|
+
help="Model name or path for alignment.",
|
|
53
52
|
)
|
|
54
53
|
@click.option(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
"-K",
|
|
55
|
+
"-L",
|
|
56
|
+
"--api-key",
|
|
57
|
+
"--api_key",
|
|
59
58
|
type=str,
|
|
60
59
|
default=None,
|
|
61
|
-
help=
|
|
60
|
+
help="API key for LattifAI.",
|
|
62
61
|
)
|
|
63
62
|
@click.argument(
|
|
64
|
-
|
|
63
|
+
"input_media_path",
|
|
65
64
|
type=click.Path(exists=True, dir_okay=False),
|
|
66
65
|
)
|
|
67
66
|
@click.argument(
|
|
68
|
-
|
|
67
|
+
"input_subtitle_path",
|
|
69
68
|
type=click.Path(exists=True, dir_okay=False),
|
|
70
69
|
)
|
|
71
70
|
@click.argument(
|
|
72
|
-
|
|
71
|
+
"output_subtitle_path",
|
|
73
72
|
type=click.Path(allow_dash=True),
|
|
74
73
|
)
|
|
75
74
|
def align(
|
|
76
75
|
input_media_path: Pathlike,
|
|
77
76
|
input_subtitle_path: Pathlike,
|
|
78
77
|
output_subtitle_path: Pathlike,
|
|
79
|
-
input_format: str =
|
|
78
|
+
input_format: str = "auto",
|
|
80
79
|
split_sentence: bool = False,
|
|
81
80
|
word_level: bool = False,
|
|
82
|
-
device: str =
|
|
83
|
-
model_name_or_path: str =
|
|
81
|
+
device: str = "cpu",
|
|
82
|
+
model_name_or_path: str = "Lattifai/Lattice-1-Alpha",
|
|
84
83
|
api_key: str = None,
|
|
85
84
|
):
|
|
86
85
|
"""
|
|
@@ -96,129 +95,129 @@ def align(
|
|
|
96
95
|
return_details=word_level,
|
|
97
96
|
output_subtitle_path=output_subtitle_path,
|
|
98
97
|
)
|
|
99
|
-
click.echo(colorful.green(f
|
|
98
|
+
click.echo(colorful.green(f"✅ Alignment completed successfully: {output_subtitle_path}"))
|
|
100
99
|
except Exception as e:
|
|
101
100
|
from lattifai.errors import LattifAIError
|
|
102
101
|
|
|
103
102
|
# Display error message
|
|
104
103
|
if isinstance(e, LattifAIError):
|
|
105
|
-
click.echo(colorful.red(
|
|
104
|
+
click.echo(colorful.red("❌ Alignment failed:"))
|
|
106
105
|
click.echo(e.get_message())
|
|
107
106
|
# Show support info
|
|
108
107
|
click.echo(e.get_support_info())
|
|
109
108
|
else:
|
|
110
|
-
click.echo(colorful.red(f
|
|
109
|
+
click.echo(colorful.red(f"❌ Alignment failed: {str(e)}"))
|
|
111
110
|
|
|
112
|
-
raise click.ClickException(
|
|
111
|
+
raise click.ClickException("Alignment failed")
|
|
113
112
|
|
|
114
113
|
|
|
115
114
|
@cli.command()
|
|
116
115
|
@click.option(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
"-M",
|
|
117
|
+
"--media-format",
|
|
118
|
+
"--media_format",
|
|
120
119
|
type=click.Choice(
|
|
121
120
|
[
|
|
122
121
|
# Audio formats
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
"mp3",
|
|
123
|
+
"wav",
|
|
124
|
+
"m4a",
|
|
125
|
+
"aac",
|
|
126
|
+
"flac",
|
|
127
|
+
"ogg",
|
|
128
|
+
"opus",
|
|
129
|
+
"aiff",
|
|
131
130
|
# Video formats
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
"mp4",
|
|
132
|
+
"webm",
|
|
133
|
+
"mkv",
|
|
134
|
+
"avi",
|
|
135
|
+
"mov",
|
|
137
136
|
],
|
|
138
137
|
case_sensitive=False,
|
|
139
138
|
),
|
|
140
|
-
default=
|
|
141
|
-
help=
|
|
139
|
+
default="mp3",
|
|
140
|
+
help="Media format for YouTube download (audio or video).",
|
|
142
141
|
)
|
|
143
142
|
@click.option(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
"-S",
|
|
144
|
+
"--split-sentence",
|
|
145
|
+
"--split_sentence",
|
|
147
146
|
is_flag=True,
|
|
148
147
|
default=False,
|
|
149
|
-
help=
|
|
148
|
+
help="Re-segment subtitles by semantics.",
|
|
150
149
|
)
|
|
151
150
|
@click.option(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
"-W",
|
|
152
|
+
"--word-level",
|
|
153
|
+
"--word_level",
|
|
155
154
|
is_flag=True,
|
|
156
155
|
default=False,
|
|
157
|
-
help=
|
|
156
|
+
help="Include word-level alignment timestamps in output (for JSON, TextGrid, and subtitle formats).",
|
|
158
157
|
)
|
|
159
158
|
@click.option(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
"-O",
|
|
160
|
+
"--output-dir",
|
|
161
|
+
"--output_dir",
|
|
163
162
|
type=click.Path(file_okay=False, dir_okay=True, writable=True),
|
|
164
|
-
default=
|
|
165
|
-
help=
|
|
163
|
+
default=".",
|
|
164
|
+
help="Output directory (default: current directory).",
|
|
166
165
|
)
|
|
167
166
|
@click.option(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
type=click.Choice([
|
|
171
|
-
default=
|
|
172
|
-
help=
|
|
167
|
+
"-D",
|
|
168
|
+
"--device",
|
|
169
|
+
type=click.Choice(["cpu", "cuda", "mps"], case_sensitive=False),
|
|
170
|
+
default="cpu",
|
|
171
|
+
help="Device to use for inference.",
|
|
173
172
|
)
|
|
174
173
|
@click.option(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
"-M",
|
|
175
|
+
"--model-name-or-path",
|
|
176
|
+
"--model_name_or_path",
|
|
178
177
|
type=str,
|
|
179
|
-
default=
|
|
180
|
-
help=
|
|
178
|
+
default="Lattifai/Lattice-1-Alpha",
|
|
179
|
+
help="Model name or path for alignment.",
|
|
181
180
|
)
|
|
182
181
|
@click.option(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
"-K",
|
|
183
|
+
"-L",
|
|
184
|
+
"--api-key",
|
|
185
|
+
"--api_key",
|
|
187
186
|
type=str,
|
|
188
187
|
default=None,
|
|
189
|
-
help=
|
|
188
|
+
help="API key for LattifAI.",
|
|
190
189
|
)
|
|
191
190
|
@click.option(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
"-G",
|
|
192
|
+
"--gemini-api-key",
|
|
193
|
+
"--gemini_api_key",
|
|
195
194
|
type=str,
|
|
196
195
|
default=None,
|
|
197
|
-
help=
|
|
196
|
+
help="Gemini API key for transcription fallback when subtitles are unavailable.",
|
|
198
197
|
)
|
|
199
198
|
@click.option(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
"-F",
|
|
200
|
+
"--output-format",
|
|
201
|
+
"--output_format",
|
|
203
202
|
type=click.Choice(OUTPUT_SUBTITLE_FORMATS, case_sensitive=False),
|
|
204
|
-
default=
|
|
205
|
-
help=
|
|
203
|
+
default="vtt",
|
|
204
|
+
help="Subtitle output format.",
|
|
206
205
|
)
|
|
207
206
|
@click.argument(
|
|
208
|
-
|
|
207
|
+
"yt_url",
|
|
209
208
|
type=str,
|
|
210
209
|
)
|
|
211
210
|
def youtube(
|
|
212
211
|
yt_url: str,
|
|
213
|
-
media_format: str =
|
|
212
|
+
media_format: str = "mp3",
|
|
214
213
|
split_sentence: bool = False,
|
|
215
214
|
word_level: bool = False,
|
|
216
|
-
output_dir: str =
|
|
217
|
-
device: str =
|
|
218
|
-
model_name_or_path: str =
|
|
215
|
+
output_dir: str = ".",
|
|
216
|
+
device: str = "cpu",
|
|
217
|
+
model_name_or_path: str = "Lattifai/Lattice-1-Alpha",
|
|
219
218
|
api_key: str = None,
|
|
220
219
|
gemini_api_key: str = None,
|
|
221
|
-
output_format: str =
|
|
220
|
+
output_format: str = "vtt",
|
|
222
221
|
):
|
|
223
222
|
"""
|
|
224
223
|
Download media and subtitles from YouTube for further alignment.
|
|
@@ -227,7 +226,7 @@ def youtube(
|
|
|
227
226
|
from lattifai.workflows.youtube import YouTubeDownloader, YouTubeSubtitleAgent
|
|
228
227
|
|
|
229
228
|
# Get Gemini API key
|
|
230
|
-
gemini_key = gemini_api_key or os.getenv(
|
|
229
|
+
gemini_key = gemini_api_key or os.getenv("GEMINI_API_KEY")
|
|
231
230
|
|
|
232
231
|
async def _process():
|
|
233
232
|
# Initialize components with their configuration (only config, not runtime params)
|
|
@@ -258,27 +257,27 @@ def youtube(
|
|
|
258
257
|
result = asyncio.run(_process())
|
|
259
258
|
|
|
260
259
|
# Display results
|
|
261
|
-
click.echo(colorful.green(
|
|
260
|
+
click.echo(colorful.green("✅ Processing completed!"))
|
|
262
261
|
click.echo()
|
|
263
262
|
|
|
264
263
|
# Show metadata
|
|
265
|
-
metadata = result.get(
|
|
264
|
+
metadata = result.get("metadata", {})
|
|
266
265
|
if metadata:
|
|
267
266
|
click.echo(f'🎬 Title: {metadata.get("title", "Unknown")}')
|
|
268
267
|
click.echo(f'⏱️ Duration: {metadata.get("duration", 0)} seconds')
|
|
269
268
|
click.echo()
|
|
270
269
|
|
|
271
270
|
# Show exported files
|
|
272
|
-
exported_files = result.get(
|
|
271
|
+
exported_files = result.get("exported_files", {})
|
|
273
272
|
if exported_files:
|
|
274
|
-
click.echo(colorful.green(
|
|
273
|
+
click.echo(colorful.green("📄 Generated subtitle files:"))
|
|
275
274
|
for format_name, file_path in exported_files.items():
|
|
276
|
-
click.echo(f
|
|
275
|
+
click.echo(f" {format_name}: {file_path}")
|
|
277
276
|
click.echo()
|
|
278
277
|
|
|
279
278
|
# Show subtitle count
|
|
280
|
-
subtitle_count = result.get(
|
|
281
|
-
click.echo(f
|
|
279
|
+
subtitle_count = result.get("subtitle_count", 0)
|
|
280
|
+
click.echo(f"📝 Generated {subtitle_count} subtitle segments")
|
|
282
281
|
|
|
283
282
|
except Exception as e:
|
|
284
283
|
from lattifai.errors import LattifAIError
|
|
@@ -286,11 +285,11 @@ def youtube(
|
|
|
286
285
|
# Extract error message without support info (to avoid duplication)
|
|
287
286
|
if isinstance(e, LattifAIError):
|
|
288
287
|
# Use the get_message() method which includes proper formatting
|
|
289
|
-
click.echo(colorful.red(
|
|
288
|
+
click.echo(colorful.red("❌ Failed to process YouTube URL:"))
|
|
290
289
|
click.echo(e.get_message())
|
|
291
290
|
# Show support info once at the end
|
|
292
291
|
click.echo(e.get_support_info())
|
|
293
292
|
else:
|
|
294
|
-
click.echo(colorful.red(f
|
|
293
|
+
click.echo(colorful.red(f"❌ Failed to process YouTube URL: {str(e)}"))
|
|
295
294
|
|
|
296
|
-
raise click.ClickException(
|
|
295
|
+
raise click.ClickException("Processing failed")
|
lattifai/bin/cli_base.py
CHANGED
|
@@ -15,11 +15,11 @@ def cli():
|
|
|
15
15
|
load_dotenv(find_dotenv(usecwd=True))
|
|
16
16
|
|
|
17
17
|
logging.basicConfig(
|
|
18
|
-
format=
|
|
18
|
+
format="%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s",
|
|
19
19
|
level=logging.INFO,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
import os
|
|
23
23
|
|
|
24
|
-
os.environ[
|
|
25
|
-
os.environ[
|
|
24
|
+
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
|
25
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "FALSE"
|
lattifai/bin/subtitle.py
CHANGED
|
@@ -16,11 +16,11 @@ def subtitle():
|
|
|
16
16
|
|
|
17
17
|
@subtitle.command()
|
|
18
18
|
@click.argument(
|
|
19
|
-
|
|
19
|
+
"input_subtitle_path",
|
|
20
20
|
type=click.Path(exists=True, dir_okay=False),
|
|
21
21
|
)
|
|
22
22
|
@click.argument(
|
|
23
|
-
|
|
23
|
+
"output_subtitle_path",
|
|
24
24
|
type=click.Path(allow_dash=True),
|
|
25
25
|
)
|
|
26
26
|
def convert(
|
|
@@ -30,7 +30,7 @@ def convert(
|
|
|
30
30
|
"""
|
|
31
31
|
Convert subtitle file to another format.
|
|
32
32
|
"""
|
|
33
|
-
if str(output_subtitle_path).lower().endswith(
|
|
33
|
+
if str(output_subtitle_path).lower().endswith(".TextGrid".lower()):
|
|
34
34
|
from lattifai.io import SubtitleIO
|
|
35
35
|
|
|
36
36
|
alignments = SubtitleIO.read(input_subtitle_path)
|
|
@@ -44,30 +44,30 @@ def convert(
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
@subtitle.command()
|
|
47
|
-
@click.argument(
|
|
47
|
+
@click.argument("url", type=str, required=True)
|
|
48
48
|
@click.option(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
"--output-dir",
|
|
50
|
+
"--output_dir",
|
|
51
|
+
"-o",
|
|
52
52
|
type=click.Path(file_okay=False, dir_okay=True),
|
|
53
|
-
default=
|
|
54
|
-
help=
|
|
53
|
+
default=".",
|
|
54
|
+
help="Output directory for downloaded subtitle files (default: current directory).",
|
|
55
55
|
)
|
|
56
56
|
@click.option(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
type=click.Choice(SUBTITLE_FORMATS + [
|
|
61
|
-
default=
|
|
62
|
-
help=
|
|
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
63
|
)
|
|
64
|
-
@click.option(
|
|
64
|
+
@click.option("--force-overwrite", "-F", is_flag=True, help="Overwrite existing files without prompting.")
|
|
65
65
|
@click.option(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
"--lang",
|
|
67
|
+
"-l",
|
|
68
|
+
"-L",
|
|
69
|
+
"--subtitle-lang",
|
|
70
|
+
"--subtitle_lang",
|
|
71
71
|
type=str,
|
|
72
72
|
help='Specific subtitle language/track to download (e.g., "en").',
|
|
73
73
|
)
|
|
@@ -88,8 +88,8 @@ def download(
|
|
|
88
88
|
|
|
89
89
|
# Validate URL format
|
|
90
90
|
if not _is_valid_youtube_url(url):
|
|
91
|
-
click.echo(f
|
|
92
|
-
click.echo(
|
|
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
93
|
raise click.Abort()
|
|
94
94
|
|
|
95
95
|
# Convert relative path to absolute
|
|
@@ -98,13 +98,13 @@ def download(
|
|
|
98
98
|
# Create output directory if it doesn't exist
|
|
99
99
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
100
100
|
|
|
101
|
-
click.echo(f
|
|
102
|
-
click.echo(f
|
|
103
|
-
click.echo(f
|
|
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
104
|
if lang:
|
|
105
|
-
click.echo(f
|
|
105
|
+
click.echo(f" Subtitle language: {lang}")
|
|
106
106
|
else:
|
|
107
|
-
click.echo(
|
|
107
|
+
click.echo(" Subtitle language: All available")
|
|
108
108
|
|
|
109
109
|
# Initialize downloader and download
|
|
110
110
|
downloader = YouTubeDownloader()
|
|
@@ -119,28 +119,28 @@ def download(
|
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
if result:
|
|
122
|
-
click.echo(
|
|
122
|
+
click.echo("✅ Subtitles downloaded successfully!")
|
|
123
123
|
return result
|
|
124
124
|
else:
|
|
125
|
-
click.echo(
|
|
125
|
+
click.echo("⚠️ No subtitles available for this video")
|
|
126
126
|
return None
|
|
127
127
|
|
|
128
128
|
except Exception as e:
|
|
129
|
-
click.echo(f
|
|
129
|
+
click.echo(f"❌ Error downloading subtitles: {str(e)}", err=True)
|
|
130
130
|
raise click.Abort()
|
|
131
131
|
|
|
132
132
|
# Run the async function
|
|
133
133
|
result = asyncio.run(download_subtitles())
|
|
134
134
|
|
|
135
135
|
if result:
|
|
136
|
-
if result ==
|
|
137
|
-
click.echo(
|
|
136
|
+
if result == "gemini":
|
|
137
|
+
click.echo("✨ Gemini transcription selected (use the agent command to transcribe)")
|
|
138
138
|
else:
|
|
139
|
-
click.echo(f
|
|
139
|
+
click.echo(f"📄 Subtitle file saved to: {result}")
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
@subtitle.command()
|
|
143
|
-
@click.argument(
|
|
143
|
+
@click.argument("url", type=str, required=True)
|
|
144
144
|
def list_subs(url: str):
|
|
145
145
|
"""
|
|
146
146
|
List available subtitle tracks for a YouTube video.
|
|
@@ -152,11 +152,11 @@ def list_subs(url: str):
|
|
|
152
152
|
|
|
153
153
|
# Validate URL format
|
|
154
154
|
if not _is_valid_youtube_url(url):
|
|
155
|
-
click.echo(f
|
|
156
|
-
click.echo(
|
|
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
157
|
raise click.Abort()
|
|
158
158
|
|
|
159
|
-
click.echo(f
|
|
159
|
+
click.echo(f"Listing available subtitles for: {url}")
|
|
160
160
|
|
|
161
161
|
# Initialize downloader
|
|
162
162
|
downloader = YouTubeDownloader()
|
|
@@ -166,20 +166,20 @@ def list_subs(url: str):
|
|
|
166
166
|
result = await downloader.list_available_subtitles(url)
|
|
167
167
|
|
|
168
168
|
if result:
|
|
169
|
-
click.echo(
|
|
169
|
+
click.echo("📋 Available subtitle tracks:")
|
|
170
170
|
for subtitle_info in result:
|
|
171
171
|
click.echo(f' 🎬 Language: {subtitle_info["language"]} - {subtitle_info["name"]}')
|
|
172
172
|
click.echo(f' 📄 Formats: {", ".join(subtitle_info["formats"])}')
|
|
173
173
|
click.echo()
|
|
174
174
|
|
|
175
|
-
click.echo(
|
|
175
|
+
click.echo("💡 To download a specific track, use:")
|
|
176
176
|
click.echo(f' lattifai subtitle download "{url}" --lang <language_code>')
|
|
177
177
|
click.echo(' Example: lattifai subtitle download "{}" --lang en-JkeT_87f4cc'.format(url))
|
|
178
178
|
else:
|
|
179
|
-
click.echo(
|
|
179
|
+
click.echo("⚠️ No subtitles available for this video")
|
|
180
180
|
|
|
181
181
|
except Exception as e:
|
|
182
|
-
click.echo(f
|
|
182
|
+
click.echo(f"❌ Error listing subtitles: {str(e)}", err=True)
|
|
183
183
|
raise click.Abort()
|
|
184
184
|
|
|
185
185
|
# Run the async function
|
|
@@ -199,9 +199,9 @@ def _is_valid_youtube_url(url: str) -> bool:
|
|
|
199
199
|
import re
|
|
200
200
|
|
|
201
201
|
patterns = [
|
|
202
|
-
r
|
|
203
|
-
r
|
|
204
|
-
r
|
|
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
205
|
]
|
|
206
206
|
|
|
207
207
|
for pattern in patterns:
|