lyrics-transcriber 0.30.0__py3-none-any.whl → 0.32.1__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.
- lyrics_transcriber/__init__.py +2 -1
- lyrics_transcriber/cli/{main.py → cli_main.py} +47 -14
- lyrics_transcriber/core/config.py +35 -0
- lyrics_transcriber/core/controller.py +164 -166
- lyrics_transcriber/correction/anchor_sequence.py +471 -0
- lyrics_transcriber/correction/corrector.py +256 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +30 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +91 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +147 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +98 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +55 -0
- lyrics_transcriber/correction/handlers/repeat.py +71 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +223 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +182 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +54 -0
- lyrics_transcriber/correction/handlers/word_operations.py +135 -0
- lyrics_transcriber/correction/phrase_analyzer.py +426 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +125 -0
- lyrics_transcriber/lyrics/genius.py +73 -0
- lyrics_transcriber/lyrics/spotify.py +82 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/{ass.py → ass/ass.py} +150 -690
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +37 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +219 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +503 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +1919 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +140 -171
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +91 -0
- lyrics_transcriber/output/segment_resizer.py +416 -0
- lyrics_transcriber/output/subtitles.py +328 -302
- lyrics_transcriber/output/video.py +219 -0
- lyrics_transcriber/review/__init__.py +1 -0
- lyrics_transcriber/review/server.py +138 -0
- lyrics_transcriber/storage/dropbox.py +110 -134
- lyrics_transcriber/transcribers/audioshake.py +171 -105
- lyrics_transcriber/transcribers/base_transcriber.py +149 -0
- lyrics_transcriber/transcribers/whisper.py +267 -133
- lyrics_transcriber/types.py +454 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/METADATA +14 -3
- lyrics_transcriber-0.32.1.dist-info/RECORD +86 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/WHEEL +1 -1
- lyrics_transcriber-0.32.1.dist-info/entry_points.txt +4 -0
- lyrics_transcriber/core/corrector.py +0 -56
- lyrics_transcriber/core/fetcher.py +0 -143
- lyrics_transcriber/storage/tokens.py +0 -116
- lyrics_transcriber/transcribers/base.py +0 -31
- lyrics_transcriber-0.30.0.dist-info/RECORD +0 -22
- lyrics_transcriber-0.30.0.dist-info/entry_points.txt +0 -3
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/LICENSE +0 -0
@@ -1,134 +1,61 @@
|
|
1
1
|
#! /usr/bin/env python3
|
2
|
+
from dataclasses import dataclass
|
2
3
|
import os
|
3
|
-
import sys
|
4
4
|
import json
|
5
5
|
import requests
|
6
6
|
import hashlib
|
7
7
|
import tempfile
|
8
|
-
|
8
|
+
import time
|
9
|
+
from typing import Optional, Dict, Any, Protocol, Union
|
10
|
+
from pathlib import Path
|
9
11
|
from pydub import AudioSegment
|
10
|
-
from .
|
11
|
-
from
|
12
|
+
from lyrics_transcriber.types import TranscriptionData, LyricsSegment, Word
|
13
|
+
from lyrics_transcriber.transcribers.base_transcriber import BaseTranscriber, TranscriptionError
|
12
14
|
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
+
@dataclass
|
17
|
+
class WhisperConfig:
|
18
|
+
"""Configuration for Whisper transcription service."""
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
dropbox_app_secret=None,
|
24
|
-
dropbox_refresh_token=None,
|
25
|
-
dropbox_access_token=None,
|
26
|
-
):
|
27
|
-
super().__init__(logger)
|
28
|
-
self.runpod_api_key = runpod_api_key or os.getenv("RUNPOD_API_KEY")
|
29
|
-
self.endpoint_id = endpoint_id or os.getenv("WHISPER_RUNPOD_ID")
|
30
|
-
|
31
|
-
if not self.runpod_api_key or not self.endpoint_id:
|
32
|
-
raise ValueError("RunPod API key and endpoint ID must be provided either directly or via environment variables")
|
33
|
-
|
34
|
-
self.dbx = DropboxHandler(
|
35
|
-
app_key=dropbox_app_key or os.getenv("WHISPER_DROPBOX_APP_KEY"),
|
36
|
-
app_secret=dropbox_app_secret or os.getenv("WHISPER_DROPBOX_APP_SECRET"),
|
37
|
-
refresh_token=dropbox_refresh_token or os.getenv("WHISPER_DROPBOX_REFRESH_TOKEN"),
|
38
|
-
access_token=dropbox_access_token or os.getenv("WHISPER_DROPBOX_ACCESS_TOKEN"),
|
39
|
-
)
|
20
|
+
runpod_api_key: Optional[str] = None
|
21
|
+
endpoint_id: Optional[str] = None
|
22
|
+
dropbox_app_key: Optional[str] = None
|
23
|
+
dropbox_app_secret: Optional[str] = None
|
24
|
+
dropbox_refresh_token: Optional[str] = None
|
25
|
+
timeout_minutes: int = 10
|
40
26
|
|
41
|
-
def get_name(self) -> str:
|
42
|
-
return "Whisper"
|
43
27
|
|
44
|
-
|
45
|
-
|
46
|
-
Transcribe an audio file using Whisper API via RunPod.
|
28
|
+
class FileStorageProtocol(Protocol):
|
29
|
+
"""Protocol for file storage operations."""
|
47
30
|
|
48
|
-
|
49
|
-
|
31
|
+
def file_exists(self, path: str) -> bool: ... # pragma: no cover
|
32
|
+
def upload_with_retry(self, file: Any, path: str) -> None: ... # pragma: no cover
|
33
|
+
def create_or_get_shared_link(self, path: str) -> str: ... # pragma: no cover
|
50
34
|
|
51
|
-
Returns:
|
52
|
-
Dict containing:
|
53
|
-
- segments: List of segments with start/end times and word-level data
|
54
|
-
- text: Full text transcription
|
55
|
-
- metadata: Dict of additional info
|
56
|
-
"""
|
57
|
-
self.logger.info(f"Starting transcription for {audio_filepath} using Whisper API")
|
58
35
|
|
59
|
-
|
60
|
-
|
61
|
-
processed_filepath = self._convert_to_flac(audio_filepath)
|
36
|
+
class RunPodWhisperAPI:
|
37
|
+
"""Handles interactions with RunPod API."""
|
62
38
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
# Get transcription from API
|
69
|
-
result = self._run_transcription(audio_url)
|
70
|
-
|
71
|
-
# Add metadata
|
72
|
-
result["metadata"] = {
|
73
|
-
"service": self.get_name(),
|
74
|
-
"model": "large-v2",
|
75
|
-
"language": "en",
|
76
|
-
}
|
39
|
+
def __init__(self, config: WhisperConfig, logger):
|
40
|
+
self.config = config
|
41
|
+
self.logger = logger
|
42
|
+
self._validate_config()
|
77
43
|
|
78
|
-
|
44
|
+
def _validate_config(self) -> None:
|
45
|
+
"""Validate API configuration."""
|
46
|
+
if not self.config.runpod_api_key or not self.config.endpoint_id:
|
47
|
+
raise ValueError("RunPod API key and endpoint ID must be provided")
|
79
48
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
os.unlink(processed_filepath)
|
85
|
-
|
86
|
-
def _convert_to_flac(self, filepath: str) -> str:
|
87
|
-
"""Convert WAV to FLAC if needed for faster upload."""
|
88
|
-
if not filepath.lower().endswith(".wav"):
|
89
|
-
return filepath
|
90
|
-
|
91
|
-
self.logger.info("Converting WAV to FLAC for faster upload...")
|
92
|
-
audio = AudioSegment.from_wav(filepath)
|
93
|
-
|
94
|
-
with tempfile.NamedTemporaryFile(suffix=".flac", delete=False) as temp_flac:
|
95
|
-
flac_path = temp_flac.name
|
96
|
-
audio.export(flac_path, format="flac")
|
97
|
-
|
98
|
-
return flac_path
|
99
|
-
|
100
|
-
def _get_file_md5(self, filepath: str) -> str:
|
101
|
-
"""Calculate MD5 hash of a file."""
|
102
|
-
md5_hash = hashlib.md5()
|
103
|
-
with open(filepath, "rb") as f:
|
104
|
-
for chunk in iter(lambda: f.read(4096), b""):
|
105
|
-
md5_hash.update(chunk)
|
106
|
-
return md5_hash.hexdigest()
|
107
|
-
|
108
|
-
def _upload_and_get_link(self, filepath: str, dropbox_path: str) -> str:
|
109
|
-
"""Upload file to Dropbox and return shared link."""
|
110
|
-
if not self.dbx.file_exists(dropbox_path):
|
111
|
-
self.logger.info("Uploading file to Dropbox...")
|
112
|
-
with open(filepath, "rb") as f:
|
113
|
-
self.dbx.upload_with_retry(f, dropbox_path)
|
114
|
-
else:
|
115
|
-
self.logger.info("File already exists in Dropbox, skipping upload...")
|
116
|
-
|
117
|
-
audio_url = self.dbx.create_or_get_shared_link(dropbox_path)
|
118
|
-
self.logger.debug(f"Using shared link: {audio_url}")
|
119
|
-
return audio_url
|
120
|
-
|
121
|
-
def _run_transcription(self, audio_url: str) -> dict:
|
122
|
-
"""Submit transcription job to RunPod and get results."""
|
123
|
-
run_url = f"https://api.runpod.ai/v2/{self.endpoint_id}/run"
|
124
|
-
status_url = f"https://api.runpod.ai/v2/{self.endpoint_id}/status"
|
125
|
-
headers = {"Authorization": f"Bearer {self.runpod_api_key}"}
|
49
|
+
def submit_job(self, audio_url: str) -> str:
|
50
|
+
"""Submit transcription job and return job ID."""
|
51
|
+
run_url = f"https://api.runpod.ai/v2/{self.config.endpoint_id}/run"
|
52
|
+
headers = {"Authorization": f"Bearer {self.config.runpod_api_key}"}
|
126
53
|
|
127
54
|
payload = {
|
128
55
|
"input": {
|
129
56
|
"audio": audio_url,
|
130
57
|
"word_timestamps": True,
|
131
|
-
"model": "
|
58
|
+
"model": "medium",
|
132
59
|
"temperature": 0.2,
|
133
60
|
"best_of": 5,
|
134
61
|
"compression_ratio_threshold": 2.8,
|
@@ -138,49 +65,256 @@ class WhisperTranscriber(BaseTranscriber):
|
|
138
65
|
}
|
139
66
|
}
|
140
67
|
|
141
|
-
# Submit job
|
142
68
|
self.logger.info("Submitting transcription job...")
|
143
69
|
response = requests.post(run_url, json=payload, headers=headers)
|
144
70
|
|
145
71
|
self.logger.debug(f"Response status code: {response.status_code}")
|
72
|
+
|
73
|
+
# Try to parse and log the JSON response
|
146
74
|
try:
|
147
|
-
|
148
|
-
|
75
|
+
response_json = response.json()
|
76
|
+
self.logger.debug(f"Response content: {json.dumps(response_json, indent=2)}")
|
77
|
+
except ValueError:
|
149
78
|
self.logger.debug(f"Raw response content: {response.text}")
|
79
|
+
# Re-raise if we can't parse the response at all
|
80
|
+
raise TranscriptionError(f"Invalid JSON response: {response.text}")
|
81
|
+
|
82
|
+
response.raise_for_status()
|
83
|
+
return response_json["id"]
|
84
|
+
|
85
|
+
def get_job_status(self, job_id: str) -> Dict[str, Any]:
|
86
|
+
"""Get job status and results."""
|
87
|
+
status_url = f"https://api.runpod.ai/v2/{self.config.endpoint_id}/status/{job_id}"
|
88
|
+
headers = {"Authorization": f"Bearer {self.config.runpod_api_key}"}
|
150
89
|
|
90
|
+
response = requests.get(status_url, headers=headers)
|
151
91
|
response.raise_for_status()
|
152
|
-
|
92
|
+
return response.json()
|
93
|
+
|
94
|
+
def cancel_job(self, job_id: str) -> None:
|
95
|
+
"""Cancel a running job."""
|
96
|
+
cancel_url = f"https://api.runpod.ai/v2/{self.config.endpoint_id}/cancel/{job_id}"
|
97
|
+
headers = {"Authorization": f"Bearer {self.config.runpod_api_key}"}
|
98
|
+
|
99
|
+
try:
|
100
|
+
response = requests.post(cancel_url, headers=headers)
|
101
|
+
response.raise_for_status()
|
102
|
+
except Exception as e:
|
103
|
+
self.logger.warning(f"Failed to cancel job {job_id}: {e}")
|
104
|
+
|
105
|
+
def wait_for_job_result(self, job_id: str) -> Dict[str, Any]:
|
106
|
+
"""Poll for job completion and return results."""
|
107
|
+
self.logger.info(f"Getting job result for job {job_id}")
|
108
|
+
|
109
|
+
start_time = time.time()
|
110
|
+
last_status_log = start_time
|
111
|
+
timeout_seconds = self.config.timeout_minutes * 60
|
153
112
|
|
154
|
-
# Poll for results
|
155
|
-
self.logger.info("Waiting for results...")
|
156
113
|
while True:
|
157
|
-
|
158
|
-
|
159
|
-
|
114
|
+
current_time = time.time()
|
115
|
+
elapsed_time = current_time - start_time
|
116
|
+
|
117
|
+
if elapsed_time > timeout_seconds:
|
118
|
+
self.cancel_job(job_id)
|
119
|
+
raise TranscriptionError(f"Transcription timed out after {self.config.timeout_minutes} minutes")
|
120
|
+
|
121
|
+
# Log status periodically
|
122
|
+
if current_time - last_status_log >= 60:
|
123
|
+
self.logger.info(f"Still waiting for transcription... Elapsed time: {int(elapsed_time/60)} minutes")
|
124
|
+
last_status_log = current_time
|
125
|
+
|
126
|
+
status_data = self.get_job_status(job_id)
|
160
127
|
|
161
128
|
if status_data["status"] == "COMPLETED":
|
162
129
|
return status_data["output"]
|
163
130
|
elif status_data["status"] == "FAILED":
|
164
|
-
|
131
|
+
error_msg = status_data.get("error", "Unknown error")
|
132
|
+
self.logger.error(f"Job failed with error: {error_msg}")
|
133
|
+
raise TranscriptionError(f"Transcription failed: {error_msg}")
|
134
|
+
|
135
|
+
time.sleep(5)
|
136
|
+
|
137
|
+
|
138
|
+
class AudioProcessor:
|
139
|
+
"""Handles audio file processing."""
|
140
|
+
|
141
|
+
def __init__(self, logger):
|
142
|
+
self.logger = logger
|
143
|
+
|
144
|
+
def get_file_md5(self, filepath: str) -> str:
|
145
|
+
"""Calculate MD5 hash of a file."""
|
146
|
+
md5_hash = hashlib.md5()
|
147
|
+
with open(filepath, "rb") as f:
|
148
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
149
|
+
md5_hash.update(chunk)
|
150
|
+
return md5_hash.hexdigest()
|
151
|
+
|
152
|
+
def convert_to_flac(self, filepath: str) -> str:
|
153
|
+
"""Convert WAV to FLAC if needed for faster upload."""
|
154
|
+
if not filepath.lower().endswith(".wav"):
|
155
|
+
return filepath
|
156
|
+
|
157
|
+
self.logger.info("Converting WAV to FLAC for faster upload...")
|
158
|
+
audio = AudioSegment.from_wav(filepath)
|
159
|
+
|
160
|
+
with tempfile.NamedTemporaryFile(suffix=".flac", delete=False) as temp_flac:
|
161
|
+
flac_path = temp_flac.name
|
162
|
+
audio.export(flac_path, format="flac")
|
163
|
+
|
164
|
+
return flac_path
|
165
|
+
|
166
|
+
|
167
|
+
class WhisperTranscriber(BaseTranscriber):
|
168
|
+
"""Transcription service using Whisper API via RunPod."""
|
169
|
+
|
170
|
+
def __init__(
|
171
|
+
self,
|
172
|
+
cache_dir: Union[str, Path],
|
173
|
+
config: Optional[WhisperConfig] = None,
|
174
|
+
logger: Optional[Any] = None,
|
175
|
+
runpod_client: Optional[RunPodWhisperAPI] = None,
|
176
|
+
storage_client: Optional[FileStorageProtocol] = None,
|
177
|
+
audio_processor: Optional[AudioProcessor] = None,
|
178
|
+
):
|
179
|
+
"""Initialize Whisper transcriber."""
|
180
|
+
super().__init__(cache_dir=cache_dir, logger=logger)
|
181
|
+
|
182
|
+
# Initialize configuration
|
183
|
+
self.config = config or WhisperConfig(
|
184
|
+
runpod_api_key=os.getenv("RUNPOD_API_KEY"),
|
185
|
+
endpoint_id=os.getenv("WHISPER_RUNPOD_ID"),
|
186
|
+
dropbox_app_key=os.getenv("WHISPER_DROPBOX_APP_KEY"),
|
187
|
+
dropbox_app_secret=os.getenv("WHISPER_DROPBOX_APP_SECRET"),
|
188
|
+
dropbox_refresh_token=os.getenv("WHISPER_DROPBOX_REFRESH_TOKEN"),
|
189
|
+
)
|
190
|
+
|
191
|
+
# Initialize components (with dependency injection)
|
192
|
+
self.runpod = runpod_client or RunPodWhisperAPI(self.config, self.logger)
|
193
|
+
self.storage = storage_client or self._initialize_storage()
|
194
|
+
self.audio_processor = audio_processor or AudioProcessor(self.logger)
|
195
|
+
|
196
|
+
def _initialize_storage(self) -> FileStorageProtocol:
|
197
|
+
"""Initialize storage client."""
|
198
|
+
from lyrics_transcriber.storage.dropbox import DropboxHandler, DropboxConfig
|
165
199
|
|
166
|
-
|
200
|
+
# Create config using os.getenv directly
|
201
|
+
config = DropboxConfig(
|
202
|
+
app_key=os.getenv("WHISPER_DROPBOX_APP_KEY"),
|
203
|
+
app_secret=os.getenv("WHISPER_DROPBOX_APP_SECRET"),
|
204
|
+
refresh_token=os.getenv("WHISPER_DROPBOX_REFRESH_TOKEN"),
|
205
|
+
)
|
206
|
+
|
207
|
+
# Log the actual config values being used
|
208
|
+
self.logger.debug("Initializing DropboxHandler with config")
|
209
|
+
return DropboxHandler(config=config)
|
210
|
+
|
211
|
+
def get_name(self) -> str:
|
212
|
+
return "Whisper"
|
167
213
|
|
214
|
+
def _perform_transcription(self, audio_filepath: str) -> TranscriptionData:
|
215
|
+
"""Actually perform the whisper transcription using Whisper API."""
|
216
|
+
self.logger.info(f"Starting transcription for {audio_filepath}")
|
168
217
|
|
169
|
-
|
170
|
-
|
171
|
-
|
218
|
+
# Start transcription and get results
|
219
|
+
job_id = self.start_transcription(audio_filepath)
|
220
|
+
result = self.get_transcription_result(job_id)
|
221
|
+
return result
|
172
222
|
|
173
|
-
|
223
|
+
def start_transcription(self, audio_filepath: str) -> str:
|
224
|
+
"""Prepare audio and start whisper transcription job."""
|
225
|
+
audio_url, temp_filepath = self._prepare_audio_url(audio_filepath)
|
226
|
+
try:
|
227
|
+
return self.runpod.submit_job(audio_url)
|
228
|
+
except Exception as e:
|
229
|
+
if temp_filepath:
|
230
|
+
self._cleanup_temporary_files(temp_filepath)
|
231
|
+
raise TranscriptionError(f"Failed to submit job: {str(e)}") from e
|
232
|
+
|
233
|
+
def _prepare_audio_url(self, audio_filepath: str) -> tuple[str, Optional[str]]:
|
234
|
+
"""Process audio file and return URL for API and path to any temporary files."""
|
235
|
+
if audio_filepath.startswith(("http://", "https://")):
|
236
|
+
return audio_filepath, None
|
237
|
+
|
238
|
+
file_hash = self.audio_processor.get_file_md5(audio_filepath)
|
239
|
+
temp_flac_filepath = self.audio_processor.convert_to_flac(audio_filepath)
|
240
|
+
|
241
|
+
# Upload and get URL
|
242
|
+
dropbox_path = f"/transcription_temp/{file_hash}{os.path.splitext(temp_flac_filepath)[1]}"
|
243
|
+
url = self._upload_and_get_link(temp_flac_filepath, dropbox_path)
|
244
|
+
return url, temp_flac_filepath
|
245
|
+
|
246
|
+
def get_transcription_result(self, job_id: str) -> Dict[str, Any]:
|
247
|
+
"""Poll for whisper job completion and return raw results."""
|
248
|
+
raw_data = self.runpod.wait_for_job_result(job_id)
|
249
|
+
|
250
|
+
# Add job_id to raw data for later use
|
251
|
+
raw_data["job_id"] = job_id
|
252
|
+
|
253
|
+
return raw_data
|
254
|
+
|
255
|
+
def _convert_result_format(self, raw_data: Dict[str, Any]) -> TranscriptionData:
|
256
|
+
"""Convert Whisper API response to standard format."""
|
257
|
+
self._validate_response(raw_data)
|
258
|
+
|
259
|
+
job_id = raw_data.get("job_id")
|
260
|
+
all_words = []
|
261
|
+
|
262
|
+
# First collect all words from word_timestamps
|
263
|
+
word_list = [
|
264
|
+
Word(
|
265
|
+
text=word["word"].strip(),
|
266
|
+
start_time=word["start"],
|
267
|
+
end_time=word["end"],
|
268
|
+
confidence=word.get("probability"), # Only set if provided
|
269
|
+
)
|
270
|
+
for word in raw_data.get("word_timestamps", [])
|
271
|
+
]
|
272
|
+
all_words.extend(word_list)
|
273
|
+
|
274
|
+
# Then create segments, using the words that fall within each segment's time range
|
275
|
+
segments = []
|
276
|
+
for seg in raw_data["segments"]:
|
277
|
+
segment_words = [word for word in word_list if seg["start"] <= word.start_time < seg["end"]]
|
278
|
+
segments.append(LyricsSegment(text=seg["text"].strip(), words=segment_words, start_time=seg["start"], end_time=seg["end"]))
|
279
|
+
|
280
|
+
return TranscriptionData(
|
281
|
+
segments=segments,
|
282
|
+
words=all_words,
|
283
|
+
text=raw_data["transcription"],
|
284
|
+
source=self.get_name(),
|
285
|
+
metadata={
|
286
|
+
"language": raw_data.get("detected_language", "en"),
|
287
|
+
"model": raw_data.get("model"),
|
288
|
+
"job_id": job_id,
|
289
|
+
},
|
290
|
+
)
|
174
291
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
292
|
+
def _upload_and_get_link(self, filepath: str, dropbox_path: str) -> str:
|
293
|
+
"""Upload file to storage and return shared link."""
|
294
|
+
if not self.storage.file_exists(dropbox_path):
|
295
|
+
self.logger.info("Uploading file to storage...")
|
296
|
+
with open(filepath, "rb") as f:
|
297
|
+
self.storage.upload_with_retry(f, dropbox_path)
|
298
|
+
else:
|
299
|
+
self.logger.info("File already exists in storage, skipping upload...")
|
179
300
|
|
180
|
-
|
181
|
-
|
301
|
+
audio_url = self.storage.create_or_get_shared_link(dropbox_path)
|
302
|
+
self.logger.debug(f"Using shared link: {audio_url}")
|
303
|
+
return audio_url
|
182
304
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
305
|
+
def _cleanup_temporary_files(self, *filepaths: Optional[str]) -> None:
|
306
|
+
"""Clean up any temporary files that were created during transcription."""
|
307
|
+
for filepath in filepaths:
|
308
|
+
if filepath and os.path.exists(filepath):
|
309
|
+
try:
|
310
|
+
os.remove(filepath)
|
311
|
+
self.logger.debug(f"Cleaned up temporary file: {filepath}")
|
312
|
+
except Exception as e:
|
313
|
+
self.logger.warning(f"Failed to clean up temporary file {filepath}: {e}")
|
314
|
+
|
315
|
+
def _validate_response(self, raw_data: Dict[str, Any]) -> None:
|
316
|
+
"""Validate the response contains required fields."""
|
317
|
+
if "segments" not in raw_data:
|
318
|
+
raise TranscriptionError("Response missing required 'segments' field")
|
319
|
+
if "transcription" not in raw_data:
|
320
|
+
raise TranscriptionError("Response missing required 'transcription' field")
|