lattifai 1.1.0__py3-none-any.whl → 1.2.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 +11 -12
- lattifai/alignment/lattice1_aligner.py +11 -8
- lattifai/alignment/lattice1_worker.py +125 -151
- lattifai/alignment/tokenizer.py +27 -12
- lattifai/audio2.py +1 -1
- lattifai/cli/diarization.py +3 -1
- lattifai/cli/youtube.py +11 -0
- lattifai/client.py +5 -0
- lattifai/config/client.py +5 -0
- lattifai/mixin.py +7 -4
- lattifai/utils.py +21 -59
- lattifai/workflow/youtube.py +55 -57
- {lattifai-1.1.0.dist-info → lattifai-1.2.0.dist-info}/METADATA +330 -48
- {lattifai-1.1.0.dist-info → lattifai-1.2.0.dist-info}/RECORD +18 -18
- {lattifai-1.1.0.dist-info → lattifai-1.2.0.dist-info}/WHEEL +0 -0
- {lattifai-1.1.0.dist-info → lattifai-1.2.0.dist-info}/entry_points.txt +0 -0
- {lattifai-1.1.0.dist-info → lattifai-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {lattifai-1.1.0.dist-info → lattifai-1.2.0.dist-info}/top_level.txt +0 -0
lattifai/cli/youtube.py
CHANGED
|
@@ -25,6 +25,7 @@ def youtube(
|
|
|
25
25
|
caption: Annotated[Optional[CaptionConfig], run.Config[CaptionConfig]] = None,
|
|
26
26
|
transcription: Annotated[Optional[TranscriptionConfig], run.Config[TranscriptionConfig]] = None,
|
|
27
27
|
diarization: Annotated[Optional[DiarizationConfig], run.Config[DiarizationConfig]] = None,
|
|
28
|
+
use_transcription: bool = False,
|
|
28
29
|
):
|
|
29
30
|
"""
|
|
30
31
|
Download media from YouTube (when needed) and align captions.
|
|
@@ -55,6 +56,11 @@ def youtube(
|
|
|
55
56
|
Fields: gemini_api_key, model_name, language, device
|
|
56
57
|
diarization: Speaker diarization configuration.
|
|
57
58
|
Fields: enabled, num_speakers, min_speakers, max_speakers, device
|
|
59
|
+
use_transcription: If True, skip YouTube caption download and directly use
|
|
60
|
+
transcription.model_name to transcribe. If False (default), first try to
|
|
61
|
+
download YouTube captions; if download fails (no captions available or
|
|
62
|
+
errors like HTTP 429), automatically fallback to transcription if
|
|
63
|
+
transcription.model_name is configured.
|
|
58
64
|
|
|
59
65
|
Examples:
|
|
60
66
|
# Download from YouTube and align (positional argument)
|
|
@@ -108,7 +114,11 @@ def youtube(
|
|
|
108
114
|
transcription_config=transcription,
|
|
109
115
|
diarization_config=diarization,
|
|
110
116
|
)
|
|
117
|
+
|
|
111
118
|
# Call the client's youtube method
|
|
119
|
+
# If use_transcription=True, skip YouTube caption download and use transcription directly.
|
|
120
|
+
# If use_transcription=False (default), try YouTube captions first; on failure,
|
|
121
|
+
# automatically fallback to transcription if transcription.model_name is configured.
|
|
112
122
|
return lattifai_client.youtube(
|
|
113
123
|
url=media_config.input_path,
|
|
114
124
|
output_dir=media_config.output_dir,
|
|
@@ -118,6 +128,7 @@ def youtube(
|
|
|
118
128
|
split_sentence=caption_config.split_sentence,
|
|
119
129
|
channel_selector=media_config.channel_selector,
|
|
120
130
|
streaming_chunk_secs=media_config.streaming_chunk_secs,
|
|
131
|
+
use_transcription=use_transcription,
|
|
121
132
|
)
|
|
122
133
|
|
|
123
134
|
|
lattifai/client.py
CHANGED
|
@@ -56,6 +56,7 @@ class LattifAI(LattifAIClientMixin, SyncAPIClient):
|
|
|
56
56
|
|
|
57
57
|
# Initialize base API client
|
|
58
58
|
super().__init__(config=client_config)
|
|
59
|
+
self.config = client_config
|
|
59
60
|
|
|
60
61
|
# Initialize all configs with defaults
|
|
61
62
|
alignment_config, transcription_config, diarization_config = self._init_configs(
|
|
@@ -269,6 +270,10 @@ class LattifAI(LattifAIClientMixin, SyncAPIClient):
|
|
|
269
270
|
if output_caption_path:
|
|
270
271
|
self._write_caption(caption, output_caption_path)
|
|
271
272
|
|
|
273
|
+
# Profile if enabled
|
|
274
|
+
if self.config.profile:
|
|
275
|
+
self.aligner.profile()
|
|
276
|
+
|
|
272
277
|
except (CaptionProcessingError, LatticeEncodingError, AlignmentError, LatticeDecodingError):
|
|
273
278
|
# Re-raise our specific errors as-is
|
|
274
279
|
raise
|
lattifai/config/client.py
CHANGED
|
@@ -26,6 +26,11 @@ class ClientConfig:
|
|
|
26
26
|
default_headers: Optional[Dict[str, str]] = field(default=None)
|
|
27
27
|
"""Optional static headers to include in all requests."""
|
|
28
28
|
|
|
29
|
+
profile: bool = False
|
|
30
|
+
"""Enable profiling of client operations tasks.
|
|
31
|
+
When True, prints detailed timing information for various stages of the process.
|
|
32
|
+
"""
|
|
33
|
+
|
|
29
34
|
def __post_init__(self):
|
|
30
35
|
"""Validate and auto-populate configuration after initialization."""
|
|
31
36
|
|
lattifai/mixin.py
CHANGED
|
@@ -491,10 +491,13 @@ class LattifAIClientMixin:
|
|
|
491
491
|
safe_print(colorful.green(f"📄 Using provided caption file: {caption_path}"))
|
|
492
492
|
return str(caption_path)
|
|
493
493
|
else:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
494
|
+
safe_print(colorful.red(f"Provided caption path does not exist: {caption_path}, use transcription"))
|
|
495
|
+
use_transcription = True
|
|
496
|
+
transcript_file = caption_path
|
|
497
|
+
caption_path.parent.mkdir(parents=True, exist_ok=True)
|
|
498
|
+
else:
|
|
499
|
+
# Generate transcript file path
|
|
500
|
+
transcript_file = output_dir / f"{Path(str(media_file)).stem}_{self.transcriber.file_name}"
|
|
498
501
|
|
|
499
502
|
if use_transcription:
|
|
500
503
|
# Transcription mode: use Transcriber to transcribe
|
lattifai/utils.py
CHANGED
|
@@ -44,49 +44,6 @@ def safe_print(text: str, **kwargs) -> None:
|
|
|
44
44
|
print(text.encode("utf-8", errors="replace").decode("utf-8"), **kwargs)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def _get_cache_marker_path(cache_dir: Path) -> Path:
|
|
48
|
-
"""Get the path for the cache marker file with current date."""
|
|
49
|
-
today = datetime.now().strftime("%Y%m%d")
|
|
50
|
-
return cache_dir / f".done{today}"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _is_cache_valid(cache_dir: Path) -> bool:
|
|
54
|
-
"""Check if cached model is valid (exists and not older than 1 days)."""
|
|
55
|
-
if not cache_dir.exists():
|
|
56
|
-
return False
|
|
57
|
-
|
|
58
|
-
# Find any .done* marker files
|
|
59
|
-
marker_files = list(cache_dir.glob(".done*"))
|
|
60
|
-
if not marker_files:
|
|
61
|
-
return False
|
|
62
|
-
|
|
63
|
-
# Get the most recent marker file
|
|
64
|
-
latest_marker = max(marker_files, key=lambda p: p.stat().st_mtime)
|
|
65
|
-
|
|
66
|
-
# Extract date from marker filename (format: .doneYYYYMMDD)
|
|
67
|
-
try:
|
|
68
|
-
date_str = latest_marker.name.replace(".done", "")
|
|
69
|
-
marker_date = datetime.strptime(date_str, "%Y%m%d")
|
|
70
|
-
# Check if marker is older than 1 days
|
|
71
|
-
if datetime.now() - marker_date > timedelta(days=1):
|
|
72
|
-
return False
|
|
73
|
-
return True
|
|
74
|
-
except (ValueError, IndexError):
|
|
75
|
-
# Invalid marker file format, treat as invalid cache
|
|
76
|
-
return False
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _create_cache_marker(cache_dir: Path) -> None:
|
|
80
|
-
"""Create a cache marker file with current date and clean old markers."""
|
|
81
|
-
# Remove old marker files
|
|
82
|
-
for old_marker in cache_dir.glob(".done*"):
|
|
83
|
-
old_marker.unlink(missing_ok=True)
|
|
84
|
-
|
|
85
|
-
# Create new marker file
|
|
86
|
-
marker_path = _get_cache_marker_path(cache_dir)
|
|
87
|
-
marker_path.touch()
|
|
88
|
-
|
|
89
|
-
|
|
90
47
|
def _resolve_model_path(model_name_or_path: str, model_hub: str = "huggingface") -> str:
|
|
91
48
|
"""Resolve model path, downloading from the specified model hub when necessary.
|
|
92
49
|
|
|
@@ -108,27 +65,32 @@ def _resolve_model_path(model_name_or_path: str, model_hub: str = "huggingface")
|
|
|
108
65
|
return str(Path(model_name_or_path).expanduser())
|
|
109
66
|
|
|
110
67
|
if hub == "huggingface":
|
|
111
|
-
from huggingface_hub import snapshot_download
|
|
112
|
-
from huggingface_hub.constants import HF_HUB_CACHE
|
|
68
|
+
from huggingface_hub import HfApi, snapshot_download
|
|
113
69
|
from huggingface_hub.errors import LocalEntryNotFoundError
|
|
114
70
|
|
|
115
|
-
#
|
|
116
|
-
|
|
71
|
+
# Support repo_id@revision syntax
|
|
72
|
+
hf_repo_id = model_name_or_path
|
|
73
|
+
revision = None
|
|
74
|
+
if "@" in model_name_or_path:
|
|
75
|
+
hf_repo_id, revision = model_name_or_path.split("@", 1)
|
|
117
76
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
77
|
+
# If no specific revision/commit is provided, try to fetch the real latest SHA
|
|
78
|
+
# to bypass Hugging Face's model_info (metadata) sync lag.
|
|
79
|
+
if not revision:
|
|
80
|
+
try:
|
|
81
|
+
api = HfApi()
|
|
82
|
+
refs = api.list_repo_refs(repo_id=hf_repo_id, repo_type="model")
|
|
83
|
+
# Look for the default branch (usually 'main')
|
|
84
|
+
for branch in refs.branches:
|
|
85
|
+
if branch.name == "main":
|
|
86
|
+
revision = branch.target_commit
|
|
87
|
+
break
|
|
88
|
+
except Exception:
|
|
89
|
+
# Fallback to default behavior if API call fails
|
|
90
|
+
revision = None
|
|
128
91
|
|
|
129
92
|
try:
|
|
130
|
-
downloaded_path = snapshot_download(repo_id=
|
|
131
|
-
_create_cache_marker(cache_dir)
|
|
93
|
+
downloaded_path = snapshot_download(repo_id=hf_repo_id, repo_type="model", revision=revision)
|
|
132
94
|
return downloaded_path
|
|
133
95
|
except LocalEntryNotFoundError:
|
|
134
96
|
# Fall back to modelscope if HF entry not found
|
lattifai/workflow/youtube.py
CHANGED
|
@@ -429,79 +429,77 @@ class YouTubeDownloader:
|
|
|
429
429
|
result = await loop.run_in_executor(
|
|
430
430
|
None, lambda: subprocess.run(ytdlp_options, capture_output=True, text=True, check=True)
|
|
431
431
|
)
|
|
432
|
-
|
|
433
432
|
# Only log success message, not full yt-dlp output
|
|
434
433
|
self.logger.debug(f"yt-dlp output: {result.stdout.strip()}")
|
|
435
|
-
|
|
436
|
-
# Find the downloaded transcript file
|
|
437
|
-
caption_patterns = [
|
|
438
|
-
f"{video_id}.*vtt",
|
|
439
|
-
f"{video_id}.*srt",
|
|
440
|
-
f"{video_id}.*sub",
|
|
441
|
-
f"{video_id}.*sbv",
|
|
442
|
-
f"{video_id}.*ssa",
|
|
443
|
-
f"{video_id}.*ass",
|
|
444
|
-
]
|
|
445
|
-
|
|
446
|
-
caption_files = []
|
|
447
|
-
for pattern in caption_patterns:
|
|
448
|
-
_caption_files = list(target_dir.glob(pattern))
|
|
449
|
-
for caption_file in _caption_files:
|
|
450
|
-
self.logger.info(f"📥 Downloaded caption: {caption_file}")
|
|
451
|
-
caption_files.extend(_caption_files)
|
|
452
|
-
|
|
453
|
-
if not caption_files:
|
|
454
|
-
self.logger.warning("No caption available for this video")
|
|
455
|
-
return None
|
|
456
|
-
|
|
457
|
-
# If only one caption file, return it directly
|
|
458
|
-
if len(caption_files) == 1:
|
|
459
|
-
self.logger.info(f"✅ Using caption: {caption_files[0]}")
|
|
460
|
-
return str(caption_files[0])
|
|
461
|
-
|
|
462
|
-
# Multiple caption files found, let user choose
|
|
463
|
-
if FileExistenceManager.is_interactive_mode():
|
|
464
|
-
self.logger.info(f"📋 Found {len(caption_files)} caption files")
|
|
465
|
-
caption_choice = FileExistenceManager.prompt_file_selection(
|
|
466
|
-
file_type="caption",
|
|
467
|
-
files=[str(f) for f in caption_files],
|
|
468
|
-
operation="use",
|
|
469
|
-
transcriber_name=transcriber_name,
|
|
470
|
-
)
|
|
471
|
-
|
|
472
|
-
if caption_choice == "cancel":
|
|
473
|
-
raise RuntimeError("Caption selection cancelled by user")
|
|
474
|
-
elif caption_choice == TRANSCRIBE_CHOICE:
|
|
475
|
-
return caption_choice
|
|
476
|
-
elif caption_choice:
|
|
477
|
-
self.logger.info(f"✅ Selected caption: {caption_choice}")
|
|
478
|
-
return caption_choice
|
|
479
|
-
else:
|
|
480
|
-
# Fallback to first file
|
|
481
|
-
self.logger.info(f"✅ Using first caption: {caption_files[0]}")
|
|
482
|
-
return str(caption_files[0])
|
|
483
|
-
else:
|
|
484
|
-
# Non-interactive mode: use first file
|
|
485
|
-
self.logger.info(f"✅ Using first caption: {caption_files[0]}")
|
|
486
|
-
return str(caption_files[0])
|
|
487
|
-
|
|
488
434
|
except subprocess.CalledProcessError as e:
|
|
489
435
|
error_msg = e.stderr.strip() if e.stderr else str(e)
|
|
490
436
|
|
|
491
437
|
# Check for specific error conditions
|
|
492
438
|
if "No automatic or manual captions found" in error_msg:
|
|
493
439
|
self.logger.warning("No captions available for this video")
|
|
494
|
-
return None
|
|
495
440
|
elif "HTTP Error 429" in error_msg or "Too Many Requests" in error_msg:
|
|
496
441
|
self.logger.error("YouTube rate limit exceeded. Please try again later or use a different method.")
|
|
497
|
-
|
|
442
|
+
self.logger.error(
|
|
498
443
|
"YouTube rate limit exceeded (HTTP 429). "
|
|
499
444
|
"Try again later or use --cookies option with authenticated cookies. "
|
|
500
445
|
"See: https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp"
|
|
501
446
|
)
|
|
502
447
|
else:
|
|
503
448
|
self.logger.error(f"Failed to download transcript: {error_msg}")
|
|
504
|
-
|
|
449
|
+
|
|
450
|
+
# Find the downloaded transcript file
|
|
451
|
+
caption_patterns = [
|
|
452
|
+
f"{video_id}.*vtt",
|
|
453
|
+
f"{video_id}.*srt",
|
|
454
|
+
f"{video_id}.*sub",
|
|
455
|
+
f"{video_id}.*sbv",
|
|
456
|
+
f"{video_id}.*ssa",
|
|
457
|
+
f"{video_id}.*ass",
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
caption_files = []
|
|
461
|
+
for pattern in caption_patterns:
|
|
462
|
+
_caption_files = list(target_dir.glob(pattern))
|
|
463
|
+
for caption_file in _caption_files:
|
|
464
|
+
self.logger.info(f"📥 Downloaded caption: {caption_file}")
|
|
465
|
+
caption_files.extend(_caption_files)
|
|
466
|
+
|
|
467
|
+
# If only one caption file, return it directly
|
|
468
|
+
if len(caption_files) == 1:
|
|
469
|
+
self.logger.info(f"✅ Using caption: {caption_files[0]}")
|
|
470
|
+
return str(caption_files[0])
|
|
471
|
+
|
|
472
|
+
# Multiple caption files found, let user choose
|
|
473
|
+
if FileExistenceManager.is_interactive_mode():
|
|
474
|
+
self.logger.info(f"📋 Found {len(caption_files)} caption files")
|
|
475
|
+
caption_choice = FileExistenceManager.prompt_file_selection(
|
|
476
|
+
file_type="caption",
|
|
477
|
+
files=[str(f) for f in caption_files],
|
|
478
|
+
operation="use",
|
|
479
|
+
transcriber_name=transcriber_name,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
if caption_choice == "cancel":
|
|
483
|
+
raise RuntimeError("Caption selection cancelled by user")
|
|
484
|
+
elif caption_choice == TRANSCRIBE_CHOICE:
|
|
485
|
+
return caption_choice
|
|
486
|
+
elif caption_choice:
|
|
487
|
+
self.logger.info(f"✅ Selected caption: {caption_choice}")
|
|
488
|
+
return caption_choice
|
|
489
|
+
elif caption_files:
|
|
490
|
+
# Fallback to first file
|
|
491
|
+
self.logger.info(f"✅ Using first caption: {caption_files[0]}")
|
|
492
|
+
return str(caption_files[0])
|
|
493
|
+
else:
|
|
494
|
+
self.logger.warning("No caption files available after download")
|
|
495
|
+
return None
|
|
496
|
+
elif caption_files:
|
|
497
|
+
# Non-interactive mode: use first file
|
|
498
|
+
self.logger.info(f"✅ Using first caption: {caption_files[0]}")
|
|
499
|
+
return str(caption_files[0])
|
|
500
|
+
else:
|
|
501
|
+
self.logger.warning("No caption files available after download")
|
|
502
|
+
return None
|
|
505
503
|
|
|
506
504
|
async def list_available_captions(self, url: str) -> List[Dict[str, Any]]:
|
|
507
505
|
"""
|