lattifai 1.2.0__py3-none-any.whl → 1.2.2__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 +0 -24
- lattifai/alignment/__init__.py +10 -1
- lattifai/alignment/lattice1_aligner.py +66 -58
- lattifai/alignment/lattice1_worker.py +1 -6
- lattifai/alignment/punctuation.py +38 -0
- lattifai/alignment/segmenter.py +1 -1
- lattifai/alignment/sentence_splitter.py +350 -0
- lattifai/alignment/text_align.py +440 -0
- lattifai/alignment/tokenizer.py +91 -220
- lattifai/caption/__init__.py +82 -6
- lattifai/caption/caption.py +335 -1143
- lattifai/caption/formats/__init__.py +199 -0
- lattifai/caption/formats/base.py +211 -0
- lattifai/caption/formats/gemini.py +722 -0
- lattifai/caption/formats/json.py +194 -0
- lattifai/caption/formats/lrc.py +309 -0
- lattifai/caption/formats/nle/__init__.py +9 -0
- lattifai/caption/formats/nle/audition.py +561 -0
- lattifai/caption/formats/nle/avid.py +423 -0
- lattifai/caption/formats/nle/fcpxml.py +549 -0
- lattifai/caption/formats/nle/premiere.py +589 -0
- lattifai/caption/formats/pysubs2.py +642 -0
- lattifai/caption/formats/sbv.py +147 -0
- lattifai/caption/formats/tabular.py +338 -0
- lattifai/caption/formats/textgrid.py +193 -0
- lattifai/caption/formats/ttml.py +652 -0
- lattifai/caption/formats/vtt.py +469 -0
- lattifai/caption/parsers/__init__.py +9 -0
- lattifai/caption/{text_parser.py → parsers/text_parser.py} +4 -2
- lattifai/caption/standardize.py +636 -0
- lattifai/caption/utils.py +474 -0
- lattifai/cli/__init__.py +2 -1
- lattifai/cli/caption.py +108 -1
- lattifai/cli/transcribe.py +4 -9
- lattifai/cli/youtube.py +4 -1
- lattifai/client.py +48 -84
- lattifai/config/__init__.py +11 -1
- lattifai/config/alignment.py +9 -2
- lattifai/config/caption.py +267 -23
- lattifai/config/media.py +20 -0
- lattifai/diarization/__init__.py +41 -1
- lattifai/mixin.py +36 -18
- lattifai/transcription/base.py +6 -1
- lattifai/transcription/lattifai.py +19 -54
- lattifai/utils.py +81 -13
- lattifai/workflow/__init__.py +28 -4
- lattifai/workflow/file_manager.py +2 -5
- lattifai/youtube/__init__.py +43 -0
- lattifai/youtube/client.py +1170 -0
- lattifai/youtube/types.py +23 -0
- lattifai-1.2.2.dist-info/METADATA +615 -0
- lattifai-1.2.2.dist-info/RECORD +76 -0
- {lattifai-1.2.0.dist-info → lattifai-1.2.2.dist-info}/entry_points.txt +1 -2
- lattifai/caption/gemini_reader.py +0 -371
- lattifai/caption/gemini_writer.py +0 -173
- lattifai/cli/app_installer.py +0 -142
- lattifai/cli/server.py +0 -44
- lattifai/server/app.py +0 -427
- lattifai/workflow/youtube.py +0 -577
- lattifai-1.2.0.dist-info/METADATA +0 -1133
- lattifai-1.2.0.dist-info/RECORD +0 -57
- {lattifai-1.2.0.dist-info → lattifai-1.2.2.dist-info}/WHEEL +0 -0
- {lattifai-1.2.0.dist-info → lattifai-1.2.2.dist-info}/licenses/LICENSE +0 -0
- {lattifai-1.2.0.dist-info → lattifai-1.2.2.dist-info}/top_level.txt +0 -0
lattifai/cli/app_installer.py
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"""CLI tool to install lai-app (frontend web application)."""
|
|
2
|
-
|
|
3
|
-
import platform
|
|
4
|
-
import subprocess
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from lattifai.utils import safe_print
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def check_command_exists(cmd: str) -> bool:
|
|
12
|
-
"""Check if a command exists in PATH."""
|
|
13
|
-
try:
|
|
14
|
-
subprocess.run([cmd, "--version"], check=True, capture_output=True, text=True)
|
|
15
|
-
return True
|
|
16
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
17
|
-
return False
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def install_nodejs():
|
|
21
|
-
"""Install Node.js based on the operating system."""
|
|
22
|
-
system = platform.system().lower()
|
|
23
|
-
|
|
24
|
-
safe_print("📦 Node.js not found. Installing Node.js...\n")
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
if system == "darwin": # macOS
|
|
28
|
-
# Check if Homebrew is installed
|
|
29
|
-
if check_command_exists("brew"):
|
|
30
|
-
safe_print("🍺 Using Homebrew to install Node.js...")
|
|
31
|
-
subprocess.run(["brew", "install", "node"], check=True)
|
|
32
|
-
safe_print("✓ Node.js installed via Homebrew\n")
|
|
33
|
-
else:
|
|
34
|
-
safe_print("❌ Homebrew not found.")
|
|
35
|
-
print(" Please install Homebrew first:")
|
|
36
|
-
print(
|
|
37
|
-
' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
|
38
|
-
)
|
|
39
|
-
print("\n Or install Node.js manually from: https://nodejs.org/")
|
|
40
|
-
sys.exit(1)
|
|
41
|
-
|
|
42
|
-
elif system == "linux":
|
|
43
|
-
# Try common package managers
|
|
44
|
-
if check_command_exists("apt"):
|
|
45
|
-
safe_print("🐧 Using apt to install Node.js...")
|
|
46
|
-
subprocess.run(["sudo", "apt", "update"], check=True)
|
|
47
|
-
subprocess.run(["sudo", "apt", "install", "-y", "nodejs", "npm"], check=True)
|
|
48
|
-
safe_print("✓ Node.js installed via apt\n")
|
|
49
|
-
elif check_command_exists("yum"):
|
|
50
|
-
safe_print("🐧 Using yum to install Node.js...")
|
|
51
|
-
subprocess.run(["sudo", "yum", "install", "-y", "nodejs", "npm"], check=True)
|
|
52
|
-
safe_print("✓ Node.js installed via yum\n")
|
|
53
|
-
elif check_command_exists("dnf"):
|
|
54
|
-
safe_print("🐧 Using dnf to install Node.js...")
|
|
55
|
-
subprocess.run(["sudo", "dnf", "install", "-y", "nodejs", "npm"], check=True)
|
|
56
|
-
safe_print("✓ Node.js installed via dnf\n")
|
|
57
|
-
elif check_command_exists("pacman"):
|
|
58
|
-
safe_print("🐧 Using pacman to install Node.js...")
|
|
59
|
-
subprocess.run(["sudo", "pacman", "-S", "--noconfirm", "nodejs", "npm"], check=True)
|
|
60
|
-
safe_print("✓ Node.js installed via pacman\n")
|
|
61
|
-
else:
|
|
62
|
-
safe_print("❌ No supported package manager found (apt/yum/dnf/pacman).")
|
|
63
|
-
print(" Please install Node.js manually from: https://nodejs.org/")
|
|
64
|
-
sys.exit(1)
|
|
65
|
-
|
|
66
|
-
elif system == "windows":
|
|
67
|
-
safe_print("❌ Automatic installation on Windows is not supported.")
|
|
68
|
-
print(" Please download and install Node.js from: https://nodejs.org/")
|
|
69
|
-
print(" Then run this command again.")
|
|
70
|
-
sys.exit(1)
|
|
71
|
-
|
|
72
|
-
else:
|
|
73
|
-
safe_print(f"❌ Unsupported operating system: {system}")
|
|
74
|
-
print(" Please install Node.js manually from: https://nodejs.org/")
|
|
75
|
-
sys.exit(1)
|
|
76
|
-
|
|
77
|
-
# Verify installation
|
|
78
|
-
if not check_command_exists("npm"):
|
|
79
|
-
safe_print("❌ Node.js installation verification failed.")
|
|
80
|
-
print(" Please restart your terminal and try again.")
|
|
81
|
-
sys.exit(1)
|
|
82
|
-
|
|
83
|
-
except subprocess.CalledProcessError as e:
|
|
84
|
-
safe_print(f"\n❌ Error during Node.js installation: {e}")
|
|
85
|
-
print(" Please install Node.js manually from: https://nodejs.org/")
|
|
86
|
-
sys.exit(1)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def main():
|
|
90
|
-
"""Install lai-app Node.js application."""
|
|
91
|
-
# Get the app directory relative to this package
|
|
92
|
-
app_dir = Path(__file__).parent.parent.parent.parent / "app"
|
|
93
|
-
|
|
94
|
-
if not app_dir.exists():
|
|
95
|
-
safe_print(f"❌ Error: app directory not found at {app_dir}")
|
|
96
|
-
print(" Make sure you're in the lattifai-python repository.")
|
|
97
|
-
sys.exit(1)
|
|
98
|
-
|
|
99
|
-
safe_print("🚀 Installing lai-app (LattifAI Web Application)...\n")
|
|
100
|
-
|
|
101
|
-
# Check if npm is installed, if not, install Node.js
|
|
102
|
-
if not check_command_exists("npm"):
|
|
103
|
-
install_nodejs()
|
|
104
|
-
else:
|
|
105
|
-
npm_version = subprocess.run(["npm", "--version"], capture_output=True, text=True, check=True).stdout.strip()
|
|
106
|
-
safe_print(f"✓ npm is already installed (v{npm_version})\n")
|
|
107
|
-
|
|
108
|
-
# Change to app directory and run installation
|
|
109
|
-
try:
|
|
110
|
-
safe_print(f"📁 Working directory: {app_dir}\n")
|
|
111
|
-
|
|
112
|
-
# Install dependencies
|
|
113
|
-
safe_print("📦 Installing dependencies...")
|
|
114
|
-
subprocess.run(["npm", "install"], cwd=app_dir, check=True)
|
|
115
|
-
safe_print("✓ Dependencies installed\n")
|
|
116
|
-
|
|
117
|
-
# Build the application
|
|
118
|
-
safe_print("🔨 Building application...")
|
|
119
|
-
subprocess.run(["npm", "run", "build"], cwd=app_dir, check=True)
|
|
120
|
-
safe_print("✓ Application built\n")
|
|
121
|
-
|
|
122
|
-
# Link globally
|
|
123
|
-
safe_print("🔗 Linking lai-app command globally...")
|
|
124
|
-
subprocess.run(["npm", "link"], cwd=app_dir, check=True)
|
|
125
|
-
safe_print("✓ lai-app command linked globally\n")
|
|
126
|
-
|
|
127
|
-
safe_print("=" * 60)
|
|
128
|
-
safe_print("✅ lai-app installed successfully!")
|
|
129
|
-
safe_print("=" * 60)
|
|
130
|
-
safe_print("\n🎉 You can now run:")
|
|
131
|
-
print(" lai-app # Start the web application")
|
|
132
|
-
print(" lai-app --help # Show help")
|
|
133
|
-
print(" lai-app --port 8080 # Use custom port")
|
|
134
|
-
safe_print("\n📖 For more information, see app/CLI_USAGE.md\n")
|
|
135
|
-
|
|
136
|
-
except subprocess.CalledProcessError as e:
|
|
137
|
-
safe_print(f"\n❌ Error during installation: {e}")
|
|
138
|
-
sys.exit(1)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if __name__ == "__main__":
|
|
142
|
-
main()
|
lattifai/cli/server.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import os
|
|
3
|
-
|
|
4
|
-
import colorful
|
|
5
|
-
import uvicorn
|
|
6
|
-
|
|
7
|
-
from lattifai.utils import safe_print
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def main():
|
|
11
|
-
"""Launch the LattifAI Web Interface."""
|
|
12
|
-
parser = argparse.ArgumentParser(description="LattifAI Backend Server")
|
|
13
|
-
parser.add_argument(
|
|
14
|
-
"-p",
|
|
15
|
-
"--port",
|
|
16
|
-
type=int,
|
|
17
|
-
default=8001,
|
|
18
|
-
help="Port to run the server on (default: 8001)",
|
|
19
|
-
)
|
|
20
|
-
parser.add_argument(
|
|
21
|
-
"--host",
|
|
22
|
-
type=str,
|
|
23
|
-
default="0.0.0.0",
|
|
24
|
-
help="Host to bind the server to (default: 0.0.0.0)",
|
|
25
|
-
)
|
|
26
|
-
parser.add_argument(
|
|
27
|
-
"--no-reload",
|
|
28
|
-
action="store_true",
|
|
29
|
-
help="Disable auto-reload on code changes",
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
args = parser.parse_args()
|
|
33
|
-
|
|
34
|
-
safe_print(colorful.bold_green("🚀 Launching LattifAI Backend Server..."))
|
|
35
|
-
print(colorful.cyan(f"Server running at http://localhost:{args.port}"))
|
|
36
|
-
print(colorful.yellow(f"Host: {args.host}"))
|
|
37
|
-
print(colorful.yellow(f"Auto-reload: {'disabled' if args.no_reload else 'enabled'}"))
|
|
38
|
-
print()
|
|
39
|
-
|
|
40
|
-
uvicorn.run("lattifai.server.app:app", host=args.host, port=args.port, reload=not args.no_reload, log_level="info")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if __name__ == "__main__":
|
|
44
|
-
main()
|
lattifai/server/app.py
DELETED
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
import tempfile
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
# Load environment variables from .env file
|
|
10
|
-
from dotenv import find_dotenv, load_dotenv
|
|
11
|
-
from fastapi import BackgroundTasks, FastAPI, File, Form, Request, UploadFile
|
|
12
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
13
|
-
from fastapi.responses import JSONResponse
|
|
14
|
-
|
|
15
|
-
# Try to find and load .env file from current directory or parent directories
|
|
16
|
-
load_dotenv(find_dotenv(usecwd=True))
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
app = FastAPI(title="LattifAI Web Interface")
|
|
20
|
-
|
|
21
|
-
print(f"LOADING APP FROM: {__file__}")
|
|
22
|
-
|
|
23
|
-
# Lazy-initialized client - will be created on first use
|
|
24
|
-
_client = None
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def get_client():
|
|
28
|
-
"""Get or create the LattifAI client (lazy initialization)."""
|
|
29
|
-
global _client
|
|
30
|
-
if _client is None:
|
|
31
|
-
from lattifai.client import LattifAI
|
|
32
|
-
|
|
33
|
-
_client = LattifAI()
|
|
34
|
-
return _client
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@app.on_event("startup")
|
|
38
|
-
async def startup_event():
|
|
39
|
-
print("Listing all registered routes:")
|
|
40
|
-
for route in app.routes:
|
|
41
|
-
print(f"Route: {route.path} - {route.name}")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@app.middleware("http")
|
|
45
|
-
async def log_requests(request: Request, call_next):
|
|
46
|
-
print(f"INCOMING REQUEST: {request.method} {request.url}")
|
|
47
|
-
response = await call_next(request)
|
|
48
|
-
print(f"OUTGOING RESPONSE: {response.status_code}")
|
|
49
|
-
return response
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
app.add_middleware(
|
|
53
|
-
CORSMiddleware,
|
|
54
|
-
allow_origins=["*"], # Allow all origins for dev
|
|
55
|
-
allow_credentials=True,
|
|
56
|
-
allow_methods=["*"],
|
|
57
|
-
allow_headers=["*"],
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@app.get("/health")
|
|
62
|
-
async def health_check():
|
|
63
|
-
"""Health check endpoint for server status monitoring."""
|
|
64
|
-
return {"status": "ok", "message": "LattifAI backend server is running"}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def mask_api_key(key: str) -> str:
|
|
68
|
-
"""Mask API key for display, showing only first 6 and last 4 characters."""
|
|
69
|
-
if len(key) <= 10:
|
|
70
|
-
return "*" * len(key)
|
|
71
|
-
return key[:6] + "*" * (len(key) - 10) + key[-4:]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@app.get("/api/keys")
|
|
75
|
-
async def get_api_keys():
|
|
76
|
-
"""Get status of API keys from environment variables."""
|
|
77
|
-
lattifai_key = os.environ.get("LATTIFAI_API_KEY", "")
|
|
78
|
-
gemini_key = os.environ.get("GEMINI_API_KEY", "")
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
"lattifai": {
|
|
82
|
-
"exists": bool(lattifai_key),
|
|
83
|
-
"masked_value": mask_api_key(lattifai_key) if lattifai_key else None,
|
|
84
|
-
"create_url": "https://lattifai.com/dashboard/api-keys",
|
|
85
|
-
},
|
|
86
|
-
"gemini": {
|
|
87
|
-
"exists": bool(gemini_key),
|
|
88
|
-
"masked_value": mask_api_key(gemini_key) if gemini_key else None,
|
|
89
|
-
"create_url": "https://aistudio.google.com/apikey",
|
|
90
|
-
},
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@app.post("/api/keys")
|
|
95
|
-
async def save_api_keys(request: Request):
|
|
96
|
-
"""Save API keys to environment variables and optionally to .env file."""
|
|
97
|
-
try:
|
|
98
|
-
data = await request.json()
|
|
99
|
-
lattifai_key = data.get("lattifai_key", "").strip()
|
|
100
|
-
gemini_key = data.get("gemini_key", "").strip()
|
|
101
|
-
save_to_file = data.get("save_to_file", False) # Optional: save to .env file
|
|
102
|
-
|
|
103
|
-
# Always update environment variables in current process
|
|
104
|
-
if lattifai_key:
|
|
105
|
-
os.environ["LATTIFAI_API_KEY"] = lattifai_key
|
|
106
|
-
if gemini_key:
|
|
107
|
-
os.environ["GEMINI_API_KEY"] = gemini_key
|
|
108
|
-
|
|
109
|
-
# Reset client to force re-initialization with new keys
|
|
110
|
-
global _client
|
|
111
|
-
_client = None
|
|
112
|
-
|
|
113
|
-
result = {
|
|
114
|
-
"status": "success",
|
|
115
|
-
"message": "API keys updated in environment variables",
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
# Optionally save to .env file for persistence
|
|
119
|
-
if save_to_file:
|
|
120
|
-
# Find the .env file path
|
|
121
|
-
env_path = find_dotenv(usecwd=True)
|
|
122
|
-
if not env_path:
|
|
123
|
-
# Create .env in current working directory
|
|
124
|
-
env_path = Path.cwd() / ".env"
|
|
125
|
-
|
|
126
|
-
# Read existing .env content
|
|
127
|
-
env_lines = []
|
|
128
|
-
if Path(env_path).exists():
|
|
129
|
-
with open(env_path, "r") as f:
|
|
130
|
-
env_lines = f.readlines()
|
|
131
|
-
|
|
132
|
-
# Update or add API keys
|
|
133
|
-
updated_lines = []
|
|
134
|
-
lattifai_updated = False
|
|
135
|
-
gemini_updated = False
|
|
136
|
-
|
|
137
|
-
for line in env_lines:
|
|
138
|
-
if line.strip().startswith("LATTIFAI_API_KEY=") or line.strip().startswith("#LATTIFAI_API_KEY="):
|
|
139
|
-
if lattifai_key:
|
|
140
|
-
updated_lines.append(f"LATTIFAI_API_KEY={lattifai_key}\n")
|
|
141
|
-
lattifai_updated = True
|
|
142
|
-
else:
|
|
143
|
-
updated_lines.append(line) # Keep existing or commented out
|
|
144
|
-
elif line.strip().startswith("GEMINI_API_KEY=") or line.strip().startswith("#GEMINI_API_KEY="):
|
|
145
|
-
if gemini_key:
|
|
146
|
-
updated_lines.append(f"GEMINI_API_KEY={gemini_key}\n")
|
|
147
|
-
gemini_updated = True
|
|
148
|
-
else:
|
|
149
|
-
updated_lines.append(line) # Keep existing or commented out
|
|
150
|
-
else:
|
|
151
|
-
updated_lines.append(line)
|
|
152
|
-
|
|
153
|
-
# Add new keys if they weren't in the file
|
|
154
|
-
if lattifai_key and not lattifai_updated:
|
|
155
|
-
updated_lines.append(f"LATTIFAI_API_KEY={lattifai_key}\n")
|
|
156
|
-
if gemini_key and not gemini_updated:
|
|
157
|
-
updated_lines.append(f"GEMINI_API_KEY={gemini_key}\n")
|
|
158
|
-
|
|
159
|
-
# Write back to .env file
|
|
160
|
-
with open(env_path, "w") as f:
|
|
161
|
-
f.writelines(updated_lines)
|
|
162
|
-
|
|
163
|
-
result["message"] = "API keys saved to environment variables and .env file"
|
|
164
|
-
result["env_path"] = str(env_path)
|
|
165
|
-
|
|
166
|
-
return result
|
|
167
|
-
|
|
168
|
-
except Exception as e:
|
|
169
|
-
import traceback
|
|
170
|
-
|
|
171
|
-
traceback.print_exc()
|
|
172
|
-
return JSONResponse(status_code=500, content={"error": str(e), "traceback": traceback.format_exc()})
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
@app.post("/api/utils/select-directory")
|
|
176
|
-
async def select_directory():
|
|
177
|
-
"""
|
|
178
|
-
Open a native directory selection dialog on the server (local machine).
|
|
179
|
-
Returns the selected path.
|
|
180
|
-
"""
|
|
181
|
-
try:
|
|
182
|
-
path = ""
|
|
183
|
-
if sys.platform == "darwin":
|
|
184
|
-
# Use AppleScript for macOS - it's cleaner than Tkinter on Mac
|
|
185
|
-
script = """
|
|
186
|
-
try
|
|
187
|
-
set theFolder to choose folder with prompt "Select Output Directory"
|
|
188
|
-
POSIX path of theFolder
|
|
189
|
-
on error
|
|
190
|
-
return ""
|
|
191
|
-
end try
|
|
192
|
-
"""
|
|
193
|
-
result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True)
|
|
194
|
-
if result.returncode == 0:
|
|
195
|
-
path = result.stdout.strip()
|
|
196
|
-
|
|
197
|
-
# Fallback to Tkinter if path is still empty (e.g. not mac or mac script failed)
|
|
198
|
-
# Note: Tkinter might not be installed or might fail in some environments
|
|
199
|
-
if not path and sys.platform != "darwin":
|
|
200
|
-
try:
|
|
201
|
-
import tkinter
|
|
202
|
-
from tkinter import filedialog
|
|
203
|
-
|
|
204
|
-
root = tkinter.Tk()
|
|
205
|
-
root.withdraw() # Hide main window
|
|
206
|
-
root.wm_attributes("-topmost", 1) # Bring to front
|
|
207
|
-
path = filedialog.askdirectory(title="Select Output Directory")
|
|
208
|
-
root.destroy()
|
|
209
|
-
except ImportError:
|
|
210
|
-
pass
|
|
211
|
-
except Exception as e:
|
|
212
|
-
print(f"Tkinter dialog failed: {e}")
|
|
213
|
-
|
|
214
|
-
return {"path": path}
|
|
215
|
-
except Exception as e:
|
|
216
|
-
# Don't fail the request, just return empty path or error logged
|
|
217
|
-
print(f"Directory selection failed: {e}")
|
|
218
|
-
return {"path": "", "error": str(e)}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
@app.post("/align")
|
|
222
|
-
async def align_files(
|
|
223
|
-
background_tasks: BackgroundTasks,
|
|
224
|
-
media_file: Optional[UploadFile] = File(None),
|
|
225
|
-
caption_file: Optional[UploadFile] = File(None),
|
|
226
|
-
local_media_path: Optional[str] = Form(None),
|
|
227
|
-
local_caption_path: Optional[str] = Form(None),
|
|
228
|
-
local_output_dir: Optional[str] = Form(None),
|
|
229
|
-
youtube_url: Optional[str] = Form(None),
|
|
230
|
-
youtube_output_dir: Optional[str] = Form(None),
|
|
231
|
-
split_sentence: bool = Form(True),
|
|
232
|
-
normalize_text: bool = Form(False),
|
|
233
|
-
output_format: str = Form("srt"),
|
|
234
|
-
transcription_model: str = Form("nvidia/parakeet-tdt-0.6b-v3"),
|
|
235
|
-
alignment_model: str = Form("LattifAI/Lattice-1"),
|
|
236
|
-
):
|
|
237
|
-
# Check if LATTIFAI_API_KEY is set
|
|
238
|
-
if not os.environ.get("LATTIFAI_API_KEY"):
|
|
239
|
-
return JSONResponse(
|
|
240
|
-
status_code=400,
|
|
241
|
-
content={
|
|
242
|
-
"error": "LATTIFAI_API_KEY is not set. Please set the environment variable or add it to your .env file.",
|
|
243
|
-
"help_url": "https://lattifai.com/dashboard/api-keys",
|
|
244
|
-
},
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
if not media_file and not youtube_url and not local_media_path:
|
|
248
|
-
return JSONResponse(
|
|
249
|
-
status_code=400, content={"error": "Either media file, local media path, or YouTube URL must be provided."}
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
# Get lazily initialized client
|
|
253
|
-
client = get_client()
|
|
254
|
-
if not client:
|
|
255
|
-
# This should rarely happen due to lazy init, but just in case
|
|
256
|
-
return JSONResponse(
|
|
257
|
-
status_code=500,
|
|
258
|
-
content={
|
|
259
|
-
"error": "LattifAI client not initialized. Please check API key configuration.",
|
|
260
|
-
},
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
media_path = None
|
|
264
|
-
caption_path = None
|
|
265
|
-
temp_files_to_delete = []
|
|
266
|
-
|
|
267
|
-
try:
|
|
268
|
-
if media_file:
|
|
269
|
-
# Save uploaded media file to a temporary location
|
|
270
|
-
with tempfile.NamedTemporaryFile(delete=False, suffix=Path(media_file.filename).suffix) as tmp_media:
|
|
271
|
-
content = await media_file.read()
|
|
272
|
-
tmp_media.write(content)
|
|
273
|
-
media_path = tmp_media.name
|
|
274
|
-
temp_files_to_delete.append(media_path)
|
|
275
|
-
|
|
276
|
-
if caption_file:
|
|
277
|
-
# Save uploaded caption file to a temporary location
|
|
278
|
-
with tempfile.NamedTemporaryFile(
|
|
279
|
-
delete=False, suffix=Path(caption_file.filename).suffix
|
|
280
|
-
) as tmp_caption:
|
|
281
|
-
content = await caption_file.read()
|
|
282
|
-
tmp_caption.write(content)
|
|
283
|
-
caption_path = tmp_caption.name
|
|
284
|
-
temp_files_to_delete.append(caption_path)
|
|
285
|
-
|
|
286
|
-
elif local_media_path:
|
|
287
|
-
media_path = local_media_path
|
|
288
|
-
if not Path(media_path).exists():
|
|
289
|
-
return JSONResponse(status_code=400, content={"error": f"Local media file not found: {media_path}"})
|
|
290
|
-
|
|
291
|
-
if local_caption_path:
|
|
292
|
-
caption_path = local_caption_path
|
|
293
|
-
if not Path(caption_path).exists():
|
|
294
|
-
return JSONResponse(
|
|
295
|
-
status_code=400, content={"error": f"Local caption file not found: {caption_path}"}
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
# Process in thread pool to not block event loop
|
|
299
|
-
loop = asyncio.get_event_loop()
|
|
300
|
-
result_caption = await loop.run_in_executor(
|
|
301
|
-
None,
|
|
302
|
-
process_alignment,
|
|
303
|
-
media_path,
|
|
304
|
-
youtube_url,
|
|
305
|
-
youtube_output_dir,
|
|
306
|
-
caption_path,
|
|
307
|
-
local_output_dir,
|
|
308
|
-
split_sentence,
|
|
309
|
-
normalize_text,
|
|
310
|
-
transcription_model,
|
|
311
|
-
alignment_model,
|
|
312
|
-
output_format,
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
# Convert result to dict with specified output format
|
|
316
|
-
caption_content = result_caption.to_string(format=output_format)
|
|
317
|
-
|
|
318
|
-
return {
|
|
319
|
-
"status": "success",
|
|
320
|
-
"segments": [
|
|
321
|
-
{
|
|
322
|
-
"start": seg.start,
|
|
323
|
-
"end": seg.end,
|
|
324
|
-
"text": seg.text,
|
|
325
|
-
"speaker": seg.speaker if hasattr(seg, "speaker") else None,
|
|
326
|
-
}
|
|
327
|
-
for seg in result_caption.alignments
|
|
328
|
-
],
|
|
329
|
-
"caption_content": caption_content,
|
|
330
|
-
"output_format": output_format,
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
except Exception as e:
|
|
334
|
-
import traceback
|
|
335
|
-
|
|
336
|
-
traceback.print_exc()
|
|
337
|
-
return JSONResponse(status_code=500, content={"error": str(e), "traceback": traceback.format_exc()})
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def process_alignment(
|
|
341
|
-
media_path,
|
|
342
|
-
youtube_url,
|
|
343
|
-
youtube_output_dir,
|
|
344
|
-
caption_path,
|
|
345
|
-
local_output_dir,
|
|
346
|
-
split_sentence,
|
|
347
|
-
normalize_text,
|
|
348
|
-
transcription_model,
|
|
349
|
-
alignment_model,
|
|
350
|
-
output_format,
|
|
351
|
-
):
|
|
352
|
-
"""
|
|
353
|
-
Wrapper to call LattifAI client.
|
|
354
|
-
Note: Transcription will be automatically triggered when no caption is provided.
|
|
355
|
-
"""
|
|
356
|
-
# Get lazily initialized client
|
|
357
|
-
client = get_client()
|
|
358
|
-
if not client:
|
|
359
|
-
raise RuntimeError("LattifAI client not initialized")
|
|
360
|
-
|
|
361
|
-
# Update caption config
|
|
362
|
-
client.caption_config.normalize_text = normalize_text
|
|
363
|
-
|
|
364
|
-
# Check if alignment model changed - if so, reinitialize aligner
|
|
365
|
-
if client.aligner.config.model_name != alignment_model:
|
|
366
|
-
print(
|
|
367
|
-
f"Alignment model changed from {client.aligner.config.model_name} to {alignment_model}, reinitializing aligner..."
|
|
368
|
-
) # noqa: E501
|
|
369
|
-
from lattifai.alignment import Lattice1Aligner
|
|
370
|
-
|
|
371
|
-
client.aligner.config.model_name = alignment_model
|
|
372
|
-
client.aligner = Lattice1Aligner(config=client.aligner.config)
|
|
373
|
-
|
|
374
|
-
# Check if transcription model changed - if so, reinitialize transcriber
|
|
375
|
-
if transcription_model != client.transcription_config.model_name:
|
|
376
|
-
print(
|
|
377
|
-
f"Transcription model changed from {client.transcription_config.model_name} to {transcription_model}, reinitializing transcriber..."
|
|
378
|
-
) # noqa: E501
|
|
379
|
-
from lattifai.config import TranscriptionConfig
|
|
380
|
-
|
|
381
|
-
client.transcription_config = TranscriptionConfig(model_name=transcription_model)
|
|
382
|
-
client._transcriber = None
|
|
383
|
-
|
|
384
|
-
if youtube_url:
|
|
385
|
-
# If youtube, we use client.youtube
|
|
386
|
-
# Note: client.youtube handles download + alignment
|
|
387
|
-
# Will try to download YT captions first, if not available, will transcribe
|
|
388
|
-
|
|
389
|
-
# Determine output directory
|
|
390
|
-
# Default: ~/Downloads/YYYY-MM-DD
|
|
391
|
-
if not youtube_output_dir or not youtube_output_dir.strip():
|
|
392
|
-
from datetime import datetime
|
|
393
|
-
|
|
394
|
-
today = datetime.now().strftime("%Y-%m-%d")
|
|
395
|
-
youtube_output_dir = f"~/Downloads/{today}"
|
|
396
|
-
|
|
397
|
-
temp_path = Path(youtube_output_dir).expanduser()
|
|
398
|
-
temp_path.mkdir(parents=True, exist_ok=True)
|
|
399
|
-
|
|
400
|
-
result = client.youtube(
|
|
401
|
-
url=youtube_url,
|
|
402
|
-
output_dir=temp_path,
|
|
403
|
-
use_transcription=False, # Try to download captions first
|
|
404
|
-
force_overwrite=True, # No user prompt in server mode
|
|
405
|
-
split_sentence=split_sentence,
|
|
406
|
-
)
|
|
407
|
-
return result
|
|
408
|
-
else:
|
|
409
|
-
# Local file alignment
|
|
410
|
-
output_caption_path = None
|
|
411
|
-
if local_output_dir:
|
|
412
|
-
output_dir = Path(local_output_dir).expanduser()
|
|
413
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
414
|
-
stem = Path(media_path).stem
|
|
415
|
-
# Prevent overwriting input if names clash, use _LattifAI suffix
|
|
416
|
-
output_filename = f"{stem}_LattifAI.{output_format}"
|
|
417
|
-
output_caption_path = output_dir / output_filename
|
|
418
|
-
print(f"Saving alignment result to: {output_caption_path}")
|
|
419
|
-
|
|
420
|
-
# If no caption_path provided, client.alignment will automatically call _transcribe
|
|
421
|
-
return client.alignment(
|
|
422
|
-
input_media=str(media_path),
|
|
423
|
-
input_caption=str(caption_path) if caption_path else None,
|
|
424
|
-
output_caption_path=str(output_caption_path) if output_caption_path else None,
|
|
425
|
-
split_sentence=split_sentence,
|
|
426
|
-
streaming_chunk_secs=None, # Server API default: no streaming
|
|
427
|
-
)
|