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/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
- raise FileNotFoundError(f"Provided caption path does not exist: {caption_path}")
495
-
496
- # Generate transcript file path
497
- transcript_file = output_dir / f"{Path(str(media_file)).stem}_{self.transcriber.file_name}"
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
- # Determine cache directory for this model
116
- cache_dir = Path(HF_HUB_CACHE) / f'models--{model_name_or_path.replace("/", "--")}'
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
- # Check if we have a valid cached version
119
- if _is_cache_valid(cache_dir):
120
- # Return the snapshot path (latest version)
121
- snapshots_dir = cache_dir / "snapshots"
122
- if snapshots_dir.exists():
123
- snapshot_dirs = [d for d in snapshots_dir.iterdir() if d.is_dir()]
124
- if snapshot_dirs:
125
- # Return the most recent snapshot
126
- latest_snapshot = max(snapshot_dirs, key=lambda p: p.stat().st_mtime)
127
- return str(latest_snapshot)
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=model_name_or_path, repo_type="model")
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
@@ -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
- raise RuntimeError(
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
- raise RuntimeError(f"Failed to download transcript: {error_msg}")
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
  """