lattifai 0.4.1__py3-none-any.whl → 0.4.3__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/agent.py CHANGED
@@ -18,12 +18,15 @@ from lattifai.io import OUTPUT_SUBTITLE_FORMATS
18
18
  @cli.command()
19
19
  @click.option('--youtube', '--yt', is_flag=True, help='Process YouTube URL through agentic workflow.')
20
20
  @click.option(
21
+ '-K',
22
+ '-L',
21
23
  '--api-key',
22
24
  '--api_key',
23
25
  type=str,
24
26
  help='LattifAI API key for alignment (overrides LATTIFAI_API_KEY env var).',
25
27
  )
26
28
  @click.option(
29
+ '-G',
27
30
  '--gemini-api-key',
28
31
  '--gemini_api_key',
29
32
  type=str,
@@ -135,14 +138,6 @@ def agent(
135
138
  lattifai_api_key = api_key or os.getenv('LATTIFAI_API_KEY')
136
139
  gemini_key = gemini_api_key or os.getenv('GEMINI_API_KEY')
137
140
 
138
- if not gemini_key:
139
- click.echo(
140
- colorful.red(
141
- '❌ Gemini API key is required. Set GEMINI_API_KEY environment variable or use --gemini-api-key option.'
142
- )
143
- )
144
- return
145
-
146
141
  try:
147
142
  # Run the YouTube workflow
148
143
  asyncio.run(
lattifai/bin/align.py CHANGED
@@ -52,6 +52,8 @@ from lattifai.io import INPUT_SUBTITLE_FORMATS, OUTPUT_SUBTITLE_FORMATS
52
52
  help='Model name or path for alignment.',
53
53
  )
54
54
  @click.option(
55
+ '-K',
56
+ '-L',
55
57
  '--api-key',
56
58
  '--api_key',
57
59
  type=str,
@@ -178,6 +180,8 @@ def align(
178
180
  help='Model name or path for alignment.',
179
181
  )
180
182
  @click.option(
183
+ '-K',
184
+ '-L',
181
185
  '--api-key',
182
186
  '--api_key',
183
187
  type=str,
@@ -185,6 +189,7 @@ def align(
185
189
  help='API key for LattifAI.',
186
190
  )
187
191
  @click.option(
192
+ '-G',
188
193
  '--gemini-api-key',
189
194
  '--gemini_api_key',
190
195
  type=str,
@@ -223,13 +228,6 @@ def youtube(
223
228
 
224
229
  # Get Gemini API key
225
230
  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
231
 
234
232
  async def _process():
235
233
  # Initialize components with their configuration (only config, not runtime params)
lattifai/bin/cli_base.py CHANGED
@@ -9,9 +9,10 @@ def cli():
9
9
  The shell entry point to Lattifai, a tool for audio data manipulation.
10
10
  """
11
11
  # Load environment variables from .env file
12
- from dotenv import load_dotenv
12
+ from dotenv import find_dotenv, load_dotenv
13
13
 
14
- load_dotenv()
14
+ # Try to find and load .env file from current directory or parent directories
15
+ load_dotenv(find_dotenv(usecwd=True))
15
16
 
16
17
  logging.basicConfig(
17
18
  format='%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s',
lattifai/client.py CHANGED
@@ -5,7 +5,6 @@ import os
5
5
  from typing import Dict, List, Optional, Tuple, Union
6
6
 
7
7
  import colorful
8
- from dotenv import load_dotenv
9
8
  from lhotse.utils import Pathlike
10
9
 
11
10
  from lattifai.base_client import AsyncAPIClient, SyncAPIClient
@@ -22,8 +21,6 @@ from lattifai.io import SubtitleFormat, SubtitleIO, Supervision
22
21
  from lattifai.tokenizer import AsyncLatticeTokenizer
23
22
  from lattifai.utils import _load_tokenizer, _load_worker, _resolve_model_path, _select_device
24
23
 
25
- load_dotenv()
26
-
27
24
 
28
25
  class LattifAI(SyncAPIClient):
29
26
  """Synchronous LattifAI client."""
lattifai/io/reader.py CHANGED
@@ -4,8 +4,8 @@ from typing import List, Literal, Optional, Union
4
4
 
5
5
  from lhotse.utils import Pathlike
6
6
 
7
- from .parser import parse_speaker_text
8
7
  from .supervision import Supervision
8
+ from .text_parser import parse_speaker_text
9
9
 
10
10
  SubtitleFormat = Literal['txt', 'srt', 'vtt', 'ass', 'auto']
11
11
 
@@ -231,7 +231,7 @@ class LatticeTokenizer:
231
231
  remainder = ''
232
232
  # Detect and split special sentence types: e.g., '[APPLAUSE] >> MIRA MURATI:' -> ['[APPLAUSE]', '>> MIRA MURATI:'] # noqa: E501
233
233
  resplit_parts = self._resplit_special_sentence_types(_sentence)
234
- if any(resplit_parts[-1].endswith(sp) for sp in [':', ':', ']']):
234
+ if any(resplit_parts[-1].endswith(sp) for sp in [':', ':']):
235
235
  if s < len(_sentences) - 1:
236
236
  _sentences[s + 1] = resplit_parts[-1] + ' ' + _sentences[s + 1]
237
237
  else: # last part
@@ -0,0 +1,34 @@
1
+ """
2
+ LattifAI Agentic Workflows
3
+
4
+ This module provides agentic workflow capabilities for automated processing
5
+ of multimedia content through intelligent agent-based pipelines.
6
+ """
7
+
8
+ # Import transcript processing functionality
9
+ from lattifai.io import (
10
+ ALL_SUBTITLE_FORMATS,
11
+ INPUT_SUBTITLE_FORMATS,
12
+ OUTPUT_SUBTITLE_FORMATS,
13
+ SUBTITLE_FORMATS,
14
+ GeminiReader,
15
+ GeminiWriter,
16
+ )
17
+
18
+ from .agents import YouTubeSubtitleAgent
19
+ from .base import WorkflowAgent, WorkflowResult, WorkflowStep
20
+ from .file_manager import FileExistenceManager
21
+
22
+ __all__ = [
23
+ 'WorkflowAgent',
24
+ 'WorkflowStep',
25
+ 'WorkflowResult',
26
+ 'YouTubeSubtitleAgent',
27
+ 'FileExistenceManager',
28
+ 'GeminiReader',
29
+ 'GeminiWriter',
30
+ 'SUBTITLE_FORMATS',
31
+ 'INPUT_SUBTITLE_FORMATS',
32
+ 'OUTPUT_SUBTITLE_FORMATS',
33
+ 'ALL_SUBTITLE_FORMATS',
34
+ ]
@@ -0,0 +1,10 @@
1
+ """
2
+ Subtitle Agents
3
+
4
+ An agentic workflow for processing YouTube(or more) videos through:
5
+ 1. URL processing and audio download
6
+ 2. Gemini 2.5 Pro transcription
7
+ 3. LattifAI alignment
8
+ """
9
+
10
+ from .youtube import YouTubeSubtitleAgent
@@ -0,0 +1,192 @@
1
+ """
2
+ Base classes for agentic workflows
3
+ """
4
+
5
+ import abc
6
+ import logging
7
+ import time
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Any, Dict, List, Optional, Union
11
+
12
+ import colorful
13
+
14
+
15
+ def setup_workflow_logger(name: str) -> logging.Logger:
16
+ """Setup a logger with consistent formatting for workflow modules"""
17
+ logger = logging.getLogger(f'workflows.{name}')
18
+
19
+ # Only add handler if it doesn't exist
20
+ if not logger.handlers:
21
+ handler = logging.StreamHandler()
22
+ formatter = logging.Formatter(
23
+ '%(asctime)s - %(name)+17s.py:%(lineno)-4d - %(levelname)-8s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S'
24
+ )
25
+ handler.setFormatter(formatter)
26
+ logger.addHandler(handler)
27
+ logger.setLevel(logging.INFO)
28
+ logger.propagate = False
29
+
30
+ return logger
31
+
32
+
33
+ logger = setup_workflow_logger('base')
34
+
35
+
36
+ class WorkflowStatus(Enum):
37
+ """Workflow execution status"""
38
+
39
+ PENDING = 'pending'
40
+ RUNNING = 'running'
41
+ COMPLETED = 'completed'
42
+ FAILED = 'failed'
43
+ RETRYING = 'retrying'
44
+
45
+
46
+ @dataclass
47
+ class WorkflowResult:
48
+ """Result of a workflow execution"""
49
+
50
+ status: WorkflowStatus
51
+ data: Optional[Any] = None
52
+ error: Optional[str] = None
53
+ exception: Optional[Exception] = None # Store the original exception object
54
+ execution_time: Optional[float] = None
55
+ step_results: Optional[List[Dict[str, Any]]] = None
56
+
57
+ @property
58
+ def is_success(self) -> bool:
59
+ return self.status == WorkflowStatus.COMPLETED
60
+
61
+ @property
62
+ def is_error(self) -> bool:
63
+ return self.status == WorkflowStatus.FAILED
64
+
65
+
66
+ @dataclass
67
+ class WorkflowStep:
68
+ """Individual step in a workflow"""
69
+
70
+ name: str
71
+ description: str
72
+ required: bool = True
73
+ retry_count: int = 0
74
+ max_retries: int = 1
75
+
76
+ def should_retry(self) -> bool:
77
+ return self.retry_count < self.max_retries
78
+
79
+
80
+ class WorkflowAgent(abc.ABC):
81
+ """Base class for agentic workflows"""
82
+
83
+ def __init__(self, name: str, max_retries: int = 0):
84
+ self.name = name
85
+ self.max_retries = max_retries
86
+ self.steps: List[WorkflowStep] = []
87
+ self.logger = setup_workflow_logger('agent')
88
+
89
+ @abc.abstractmethod
90
+ def define_steps(self) -> List[WorkflowStep]:
91
+ """Define the workflow steps"""
92
+ pass
93
+
94
+ @abc.abstractmethod
95
+ async def execute_step(self, step: WorkflowStep, context: Dict[str, Any]) -> Any:
96
+ """Execute a single workflow step"""
97
+ pass
98
+
99
+ def setup(self):
100
+ """Setup the workflow"""
101
+ self.steps = self.define_steps()
102
+ for step in self.steps:
103
+ step.max_retries = self.max_retries
104
+
105
+ async def execute(self, **kwargs) -> WorkflowResult:
106
+ """Execute the complete workflow"""
107
+ if not self.steps:
108
+ self.setup()
109
+
110
+ start_time = time.time()
111
+ context = kwargs.copy()
112
+ step_results = []
113
+
114
+ self.logger.info(colorful.bold_white_on_green(f'🚀 Starting workflow: {self.name}'))
115
+
116
+ try:
117
+ for i, step in enumerate(self.steps):
118
+ step_info = f'📋 Step {i + 1}/{len(self.steps)}: {step.name}'
119
+ self.logger.info(colorful.bold_white_on_green(step_info))
120
+
121
+ step_start = time.time()
122
+ step_result = await self._execute_step_with_retry(step, context)
123
+ step_duration = time.time() - step_start
124
+
125
+ step_results.append(
126
+ {'step_name': step.name, 'status': 'completed', 'duration': step_duration, 'result': step_result}
127
+ )
128
+
129
+ # Update context with step result
130
+ context[f'step_{i}_result'] = step_result
131
+ context[f'{step.name.lower().replace(" ", "_")}_result'] = step_result
132
+
133
+ self.logger.info(f'✅ Step {i + 1} completed in {step_duration:.2f}s')
134
+
135
+ execution_time = time.time() - start_time
136
+ self.logger.info(f'🎉 Workflow completed in {execution_time:.2f}s')
137
+
138
+ return WorkflowResult(
139
+ status=WorkflowStatus.COMPLETED, data=context, execution_time=execution_time, step_results=step_results
140
+ )
141
+
142
+ except Exception as e:
143
+ execution_time = time.time() - start_time
144
+ # For LattifAI errors, just log the error code and basic message
145
+ from lattifai.errors import LattifAIError
146
+
147
+ if isinstance(e, LattifAIError):
148
+ self.logger.error(f'❌ Workflow failed after {execution_time:.2f}s: [{e.error_code}] {e.message}')
149
+ else:
150
+ self.logger.error(f'❌ Workflow failed after {execution_time:.2f}s: {str(e)}')
151
+
152
+ return WorkflowResult(
153
+ status=WorkflowStatus.FAILED,
154
+ error=str(e),
155
+ exception=e, # Store the original exception
156
+ execution_time=execution_time,
157
+ step_results=step_results,
158
+ )
159
+
160
+ async def _execute_step_with_retry(self, step: WorkflowStep, context: Dict[str, Any]) -> Any:
161
+ """Execute a step with retry logic"""
162
+ last_error = None
163
+
164
+ for attempt in range(step.max_retries + 1):
165
+ try:
166
+ if attempt > 0:
167
+ self.logger.info(f'🔄 Retrying step {step.name} (attempt {attempt + 1}/{step.max_retries + 1})')
168
+
169
+ result = await self.execute_step(step, context)
170
+ return result
171
+
172
+ except Exception as e:
173
+ last_error = e
174
+ step.retry_count += 1
175
+
176
+ # For LattifAI errors, show simplified message in logs
177
+ from lattifai.errors import LattifAIError
178
+
179
+ error_summary = f'[{e.error_code}]' if isinstance(e, LattifAIError) else str(e)[:100]
180
+
181
+ if step.should_retry():
182
+ self.logger.warning(f'⚠️ Step {step.name} failed: {error_summary}. Retrying...')
183
+ continue
184
+ else:
185
+ self.logger.error(
186
+ f'❌ Step {step.name} failed after {step.max_retries + 1} attempts: {error_summary}'
187
+ )
188
+ raise e
189
+
190
+ # This should never be reached, but just in case
191
+ if last_error:
192
+ raise last_error