karaoke-gen 0.90.1__py3-none-any.whl → 0.99.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.
- backend/.coveragerc +20 -0
- backend/.gitignore +37 -0
- backend/Dockerfile +43 -0
- backend/Dockerfile.base +74 -0
- backend/README.md +242 -0
- backend/__init__.py +0 -0
- backend/api/__init__.py +0 -0
- backend/api/dependencies.py +457 -0
- backend/api/routes/__init__.py +0 -0
- backend/api/routes/admin.py +835 -0
- backend/api/routes/audio_search.py +913 -0
- backend/api/routes/auth.py +348 -0
- backend/api/routes/file_upload.py +2112 -0
- backend/api/routes/health.py +409 -0
- backend/api/routes/internal.py +435 -0
- backend/api/routes/jobs.py +1629 -0
- backend/api/routes/review.py +652 -0
- backend/api/routes/themes.py +162 -0
- backend/api/routes/users.py +1513 -0
- backend/config.py +172 -0
- backend/main.py +157 -0
- backend/middleware/__init__.py +5 -0
- backend/middleware/audit_logging.py +124 -0
- backend/models/__init__.py +0 -0
- backend/models/job.py +519 -0
- backend/models/requests.py +123 -0
- backend/models/theme.py +153 -0
- backend/models/user.py +254 -0
- backend/models/worker_log.py +164 -0
- backend/pyproject.toml +29 -0
- backend/quick-check.sh +93 -0
- backend/requirements.txt +29 -0
- backend/run_tests.sh +60 -0
- backend/services/__init__.py +0 -0
- backend/services/audio_analysis_service.py +243 -0
- backend/services/audio_editing_service.py +278 -0
- backend/services/audio_search_service.py +702 -0
- backend/services/auth_service.py +630 -0
- backend/services/credential_manager.py +792 -0
- backend/services/discord_service.py +172 -0
- backend/services/dropbox_service.py +301 -0
- backend/services/email_service.py +1093 -0
- backend/services/encoding_interface.py +454 -0
- backend/services/encoding_service.py +502 -0
- backend/services/firestore_service.py +512 -0
- backend/services/flacfetch_client.py +573 -0
- backend/services/gce_encoding/README.md +72 -0
- backend/services/gce_encoding/__init__.py +22 -0
- backend/services/gce_encoding/main.py +589 -0
- backend/services/gce_encoding/requirements.txt +16 -0
- backend/services/gdrive_service.py +356 -0
- backend/services/job_logging.py +258 -0
- backend/services/job_manager.py +853 -0
- backend/services/job_notification_service.py +271 -0
- backend/services/langfuse_preloader.py +98 -0
- backend/services/local_encoding_service.py +590 -0
- backend/services/local_preview_encoding_service.py +407 -0
- backend/services/lyrics_cache_service.py +216 -0
- backend/services/metrics.py +413 -0
- backend/services/nltk_preloader.py +122 -0
- backend/services/packaging_service.py +287 -0
- backend/services/rclone_service.py +106 -0
- backend/services/spacy_preloader.py +65 -0
- backend/services/storage_service.py +209 -0
- backend/services/stripe_service.py +371 -0
- backend/services/structured_logging.py +254 -0
- backend/services/template_service.py +330 -0
- backend/services/theme_service.py +469 -0
- backend/services/tracing.py +543 -0
- backend/services/user_service.py +721 -0
- backend/services/worker_service.py +558 -0
- backend/services/youtube_service.py +112 -0
- backend/services/youtube_upload_service.py +445 -0
- backend/tests/__init__.py +4 -0
- backend/tests/conftest.py +224 -0
- backend/tests/emulator/__init__.py +7 -0
- backend/tests/emulator/conftest.py +109 -0
- backend/tests/emulator/test_e2e_cli_backend.py +1053 -0
- backend/tests/emulator/test_emulator_integration.py +356 -0
- backend/tests/emulator/test_style_loading_direct.py +436 -0
- backend/tests/emulator/test_worker_logs_direct.py +229 -0
- backend/tests/emulator/test_worker_logs_subcollection.py +443 -0
- backend/tests/requirements-test.txt +10 -0
- backend/tests/requirements.txt +6 -0
- backend/tests/test_admin_email_endpoints.py +411 -0
- backend/tests/test_api_integration.py +460 -0
- backend/tests/test_api_routes.py +93 -0
- backend/tests/test_audio_analysis_service.py +294 -0
- backend/tests/test_audio_editing_service.py +386 -0
- backend/tests/test_audio_search.py +1398 -0
- backend/tests/test_audio_services.py +378 -0
- backend/tests/test_auth_firestore.py +231 -0
- backend/tests/test_config_extended.py +68 -0
- backend/tests/test_credential_manager.py +377 -0
- backend/tests/test_dependencies.py +54 -0
- backend/tests/test_discord_service.py +244 -0
- backend/tests/test_distribution_services.py +820 -0
- backend/tests/test_dropbox_service.py +472 -0
- backend/tests/test_email_service.py +492 -0
- backend/tests/test_emulator_integration.py +322 -0
- backend/tests/test_encoding_interface.py +412 -0
- backend/tests/test_file_upload.py +1739 -0
- backend/tests/test_flacfetch_client.py +632 -0
- backend/tests/test_gdrive_service.py +524 -0
- backend/tests/test_instrumental_api.py +431 -0
- backend/tests/test_internal_api.py +343 -0
- backend/tests/test_job_creation_regression.py +583 -0
- backend/tests/test_job_manager.py +356 -0
- backend/tests/test_job_manager_notifications.py +329 -0
- backend/tests/test_job_notification_service.py +443 -0
- backend/tests/test_jobs_api.py +283 -0
- backend/tests/test_local_encoding_service.py +423 -0
- backend/tests/test_local_preview_encoding_service.py +567 -0
- backend/tests/test_main.py +87 -0
- backend/tests/test_models.py +918 -0
- backend/tests/test_packaging_service.py +382 -0
- backend/tests/test_requests.py +201 -0
- backend/tests/test_routes_jobs.py +282 -0
- backend/tests/test_routes_review.py +337 -0
- backend/tests/test_services.py +556 -0
- backend/tests/test_services_extended.py +112 -0
- backend/tests/test_spacy_preloader.py +119 -0
- backend/tests/test_storage_service.py +448 -0
- backend/tests/test_style_upload.py +261 -0
- backend/tests/test_template_service.py +295 -0
- backend/tests/test_theme_service.py +516 -0
- backend/tests/test_unicode_sanitization.py +522 -0
- backend/tests/test_upload_api.py +256 -0
- backend/tests/test_validate.py +156 -0
- backend/tests/test_video_worker_orchestrator.py +847 -0
- backend/tests/test_worker_log_subcollection.py +509 -0
- backend/tests/test_worker_logging.py +365 -0
- backend/tests/test_workers.py +1116 -0
- backend/tests/test_workers_extended.py +178 -0
- backend/tests/test_youtube_service.py +247 -0
- backend/tests/test_youtube_upload_service.py +568 -0
- backend/utils/test_data.py +27 -0
- backend/validate.py +173 -0
- backend/version.py +27 -0
- backend/workers/README.md +597 -0
- backend/workers/__init__.py +11 -0
- backend/workers/audio_worker.py +618 -0
- backend/workers/lyrics_worker.py +683 -0
- backend/workers/render_video_worker.py +483 -0
- backend/workers/screens_worker.py +535 -0
- backend/workers/style_helper.py +198 -0
- backend/workers/video_worker.py +1277 -0
- backend/workers/video_worker_orchestrator.py +701 -0
- backend/workers/worker_logging.py +278 -0
- karaoke_gen/instrumental_review/static/index.html +7 -4
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +6 -1
- karaoke_gen/utils/__init__.py +163 -8
- karaoke_gen/video_background_processor.py +9 -4
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/METADATA +1 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/RECORD +196 -46
- lyrics_transcriber/correction/agentic/agent.py +17 -6
- lyrics_transcriber/correction/agentic/providers/config.py +9 -5
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +96 -93
- lyrics_transcriber/correction/agentic/providers/model_factory.py +27 -6
- lyrics_transcriber/correction/anchor_sequence.py +151 -37
- lyrics_transcriber/correction/corrector.py +192 -130
- lyrics_transcriber/correction/handlers/syllables_match.py +44 -2
- lyrics_transcriber/correction/operations.py +24 -9
- lyrics_transcriber/correction/phrase_analyzer.py +18 -0
- lyrics_transcriber/frontend/package-lock.json +2 -2
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +11 -7
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +31 -5
- lyrics_transcriber/frontend/src/components/EditModal.tsx +28 -10
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +123 -27
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +112 -60
- lyrics_transcriber/frontend/src/components/Header.tsx +90 -76
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +53 -31
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +44 -13
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +66 -50
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +124 -30
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +12 -5
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +3 -3
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +11 -7
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +4 -2
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +103 -1
- lyrics_transcriber/frontend/src/theme.ts +42 -15
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/vite.config.js +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-BECn1o8Q.js → index-BSMgOq4Z.js} +6959 -5782
- lyrics_transcriber/frontend/web_assets/assets/index-BSMgOq4Z.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +6 -2
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/output/generator.py +17 -3
- lyrics_transcriber/output/video.py +60 -95
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +0 -1
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.90.1.dist-info → karaoke_gen-0.99.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template service for managing email templates stored in GCS.
|
|
3
|
+
|
|
4
|
+
Templates are stored in GCS bucket and rendered with job-specific variables.
|
|
5
|
+
This allows updating email content without code deployment.
|
|
6
|
+
|
|
7
|
+
Template locations:
|
|
8
|
+
- gs://{bucket}/templates/job-completion.txt - Job completion email (plain text)
|
|
9
|
+
- gs://{bucket}/templates/action-needed-lyrics.txt - Lyrics review reminder
|
|
10
|
+
- gs://{bucket}/templates/action-needed-instrumental.txt - Instrumental selection reminder
|
|
11
|
+
"""
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
from typing import Optional, Dict, Any
|
|
15
|
+
|
|
16
|
+
from backend.config import get_settings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Default templates (fallback if GCS fetch fails)
|
|
23
|
+
DEFAULT_JOB_COMPLETION_TEMPLATE = """Hi {name},
|
|
24
|
+
|
|
25
|
+
Thanks for your order!
|
|
26
|
+
|
|
27
|
+
Here's the link for the karaoke video published to YouTube:
|
|
28
|
+
{youtube_url}
|
|
29
|
+
|
|
30
|
+
Here's the dropbox folder with all the finished files and source files, including:
|
|
31
|
+
- "(Final Karaoke Lossless).mkv": combined karaoke video in 4k H264 with lossless FLAC audio
|
|
32
|
+
- "(Final Karaoke).mp4": combined karaoke video with title/end screen in 4k H264/AAC
|
|
33
|
+
- "(Final Karaoke 720p).mp4": combined karaoke video in 720p H264/AAC (smaller file for older systems)
|
|
34
|
+
- "(With Vocals).mp4": sing along video in 4k H264/AAC with original vocals
|
|
35
|
+
- "(Karaoke).mov": karaoke video output from MidiCo (no title/end screen)
|
|
36
|
+
- "(Title).mov"/"(End).mov": title card and end screen videos
|
|
37
|
+
- "(Final Karaoke CDG).zip": CDG+MP3 format for older/commercial karaoke systems
|
|
38
|
+
- "(Final Karaoke TXT).zip": TXT+MP3 format for Power Karaoke
|
|
39
|
+
- stems/*.flac: various separated instrumental and vocal audio stems in lossless format
|
|
40
|
+
- lyrics/*.txt song lyrics from various sources in plain text format
|
|
41
|
+
|
|
42
|
+
{dropbox_url}
|
|
43
|
+
|
|
44
|
+
Let me know if anything isn't perfect and I'll happily tweak / fix, or if you need it in any other format I can probably convert it for you!
|
|
45
|
+
|
|
46
|
+
If you have a moment, I'd really appreciate your feedback (takes 2 minutes):
|
|
47
|
+
{feedback_url}
|
|
48
|
+
|
|
49
|
+
Thanks again and have a great day!
|
|
50
|
+
-Andrew
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
DEFAULT_ACTION_NEEDED_LYRICS_TEMPLATE = """Hi {name},
|
|
54
|
+
|
|
55
|
+
Your karaoke video for "{artist} - {title}" is ready for lyrics review!
|
|
56
|
+
|
|
57
|
+
Our system has transcribed and synchronized the lyrics, but they may need some corrections. Please review and make any corrections necessary so we can finish generating your video.
|
|
58
|
+
|
|
59
|
+
Review your lyrics here:
|
|
60
|
+
{review_url}
|
|
61
|
+
|
|
62
|
+
This usually takes just a few minutes.
|
|
63
|
+
|
|
64
|
+
Thanks!
|
|
65
|
+
-Andrew
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
DEFAULT_ACTION_NEEDED_INSTRUMENTAL_TEMPLATE = """Hi {name},
|
|
69
|
+
|
|
70
|
+
Your karaoke video for "{artist} - {title}" is almost done!
|
|
71
|
+
|
|
72
|
+
We've separated the audio into different versions. Please select which instrumental track you'd like to use for the final video.
|
|
73
|
+
|
|
74
|
+
Select your instrumental here:
|
|
75
|
+
{instrumental_url}
|
|
76
|
+
|
|
77
|
+
Thanks!
|
|
78
|
+
-Andrew
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TemplateService:
|
|
83
|
+
"""
|
|
84
|
+
Service for fetching and rendering email templates from GCS.
|
|
85
|
+
|
|
86
|
+
Templates support the following variables:
|
|
87
|
+
- {name} - User's display name or "there" if unknown
|
|
88
|
+
- {youtube_url} - YouTube video URL
|
|
89
|
+
- {dropbox_url} - Dropbox folder URL
|
|
90
|
+
- {artist} - Artist name
|
|
91
|
+
- {title} - Song title
|
|
92
|
+
- {job_id} - Job ID
|
|
93
|
+
- {review_url} - Lyrics review URL
|
|
94
|
+
- {instrumental_url} - Instrumental selection URL
|
|
95
|
+
- {feedback_url} - Feedback form URL
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
TEMPLATE_PREFIX = "templates/"
|
|
99
|
+
|
|
100
|
+
def __init__(self):
|
|
101
|
+
"""Initialize template service."""
|
|
102
|
+
self.settings = get_settings()
|
|
103
|
+
self._storage_client = None
|
|
104
|
+
self._bucket = None
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def storage_client(self):
|
|
108
|
+
"""Lazy-initialize storage client."""
|
|
109
|
+
if self._storage_client is None:
|
|
110
|
+
from google.cloud import storage
|
|
111
|
+
self._storage_client = storage.Client(project=self.settings.google_cloud_project)
|
|
112
|
+
return self._storage_client
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def bucket(self):
|
|
116
|
+
"""Get the GCS bucket for templates."""
|
|
117
|
+
if self._bucket is None:
|
|
118
|
+
self._bucket = self.storage_client.bucket(self.settings.gcs_bucket_name)
|
|
119
|
+
return self._bucket
|
|
120
|
+
|
|
121
|
+
def _fetch_template_from_gcs(self, template_name: str) -> Optional[str]:
|
|
122
|
+
"""
|
|
123
|
+
Fetch a template from GCS.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
template_name: Name of template file (e.g., "job-completion.txt")
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Template content as string, or None if not found
|
|
130
|
+
"""
|
|
131
|
+
blob_path = f"{self.TEMPLATE_PREFIX}{template_name}"
|
|
132
|
+
try:
|
|
133
|
+
blob = self.bucket.blob(blob_path)
|
|
134
|
+
if blob.exists():
|
|
135
|
+
content = blob.download_as_text()
|
|
136
|
+
logger.debug(f"Fetched template from GCS: {blob_path}")
|
|
137
|
+
return content
|
|
138
|
+
else:
|
|
139
|
+
logger.warning(f"Template not found in GCS: {blob_path}")
|
|
140
|
+
return None
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Failed to fetch template {blob_path}: {e}")
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def get_job_completion_template(self) -> str:
|
|
146
|
+
"""Get the job completion email template."""
|
|
147
|
+
template = self._fetch_template_from_gcs("job-completion.txt")
|
|
148
|
+
if template is None:
|
|
149
|
+
logger.info("Using default job completion template")
|
|
150
|
+
return DEFAULT_JOB_COMPLETION_TEMPLATE
|
|
151
|
+
return template
|
|
152
|
+
|
|
153
|
+
def get_action_needed_lyrics_template(self) -> str:
|
|
154
|
+
"""Get the lyrics review reminder template."""
|
|
155
|
+
template = self._fetch_template_from_gcs("action-needed-lyrics.txt")
|
|
156
|
+
if template is None:
|
|
157
|
+
logger.info("Using default lyrics reminder template")
|
|
158
|
+
return DEFAULT_ACTION_NEEDED_LYRICS_TEMPLATE
|
|
159
|
+
return template
|
|
160
|
+
|
|
161
|
+
def get_action_needed_instrumental_template(self) -> str:
|
|
162
|
+
"""Get the instrumental selection reminder template."""
|
|
163
|
+
template = self._fetch_template_from_gcs("action-needed-instrumental.txt")
|
|
164
|
+
if template is None:
|
|
165
|
+
logger.info("Using default instrumental reminder template")
|
|
166
|
+
return DEFAULT_ACTION_NEEDED_INSTRUMENTAL_TEMPLATE
|
|
167
|
+
return template
|
|
168
|
+
|
|
169
|
+
def render_template(self, template: str, variables: Dict[str, Any]) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Render a template with the given variables.
|
|
172
|
+
|
|
173
|
+
Missing variables are replaced with empty strings.
|
|
174
|
+
Handles conditional sections like feedback URL.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
template: Template string with {variable} placeholders
|
|
178
|
+
variables: Dictionary of variable values
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Rendered template string
|
|
182
|
+
"""
|
|
183
|
+
result = template
|
|
184
|
+
|
|
185
|
+
# Handle feedback URL section - remove if not provided
|
|
186
|
+
if not variables.get("feedback_url"):
|
|
187
|
+
# Remove feedback section (lines containing feedback_url placeholder and surrounding text)
|
|
188
|
+
result = re.sub(
|
|
189
|
+
r'\n*If you have a moment.*?\{feedback_url\}\n*',
|
|
190
|
+
'\n',
|
|
191
|
+
result,
|
|
192
|
+
flags=re.DOTALL
|
|
193
|
+
)
|
|
194
|
+
variables["feedback_url"] = ""
|
|
195
|
+
|
|
196
|
+
# Replace all variables
|
|
197
|
+
for key, value in variables.items():
|
|
198
|
+
placeholder = "{" + key + "}"
|
|
199
|
+
result = result.replace(placeholder, str(value) if value else "")
|
|
200
|
+
|
|
201
|
+
# Clean up any remaining unreplaced placeholders
|
|
202
|
+
result = re.sub(r'\{[a-z_]+\}', '', result)
|
|
203
|
+
|
|
204
|
+
return result.strip()
|
|
205
|
+
|
|
206
|
+
def render_job_completion(
|
|
207
|
+
self,
|
|
208
|
+
name: Optional[str] = None,
|
|
209
|
+
youtube_url: Optional[str] = None,
|
|
210
|
+
dropbox_url: Optional[str] = None,
|
|
211
|
+
artist: Optional[str] = None,
|
|
212
|
+
title: Optional[str] = None,
|
|
213
|
+
job_id: Optional[str] = None,
|
|
214
|
+
feedback_url: Optional[str] = None,
|
|
215
|
+
) -> str:
|
|
216
|
+
"""
|
|
217
|
+
Render the job completion email template.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
name: User's display name (defaults to "there")
|
|
221
|
+
youtube_url: YouTube video URL
|
|
222
|
+
dropbox_url: Dropbox folder URL
|
|
223
|
+
artist: Artist name
|
|
224
|
+
title: Song title
|
|
225
|
+
job_id: Job ID
|
|
226
|
+
feedback_url: Feedback form URL (optional)
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Rendered email content
|
|
230
|
+
"""
|
|
231
|
+
template = self.get_job_completion_template()
|
|
232
|
+
variables = {
|
|
233
|
+
"name": name or "there",
|
|
234
|
+
"youtube_url": youtube_url or "[YouTube URL not available]",
|
|
235
|
+
"dropbox_url": dropbox_url or "[Dropbox URL not available]",
|
|
236
|
+
"artist": artist or "Unknown Artist",
|
|
237
|
+
"title": title or "Unknown Title",
|
|
238
|
+
"job_id": job_id or "",
|
|
239
|
+
"feedback_url": feedback_url,
|
|
240
|
+
}
|
|
241
|
+
return self.render_template(template, variables)
|
|
242
|
+
|
|
243
|
+
def render_action_needed_lyrics(
|
|
244
|
+
self,
|
|
245
|
+
name: Optional[str] = None,
|
|
246
|
+
artist: Optional[str] = None,
|
|
247
|
+
title: Optional[str] = None,
|
|
248
|
+
review_url: str = "",
|
|
249
|
+
) -> str:
|
|
250
|
+
"""
|
|
251
|
+
Render the lyrics review reminder template.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
name: User's display name
|
|
255
|
+
artist: Artist name
|
|
256
|
+
title: Song title
|
|
257
|
+
review_url: Lyrics review URL
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Rendered email content
|
|
261
|
+
"""
|
|
262
|
+
template = self.get_action_needed_lyrics_template()
|
|
263
|
+
variables = {
|
|
264
|
+
"name": name or "there",
|
|
265
|
+
"artist": artist or "Unknown Artist",
|
|
266
|
+
"title": title or "Unknown Title",
|
|
267
|
+
"review_url": review_url,
|
|
268
|
+
}
|
|
269
|
+
return self.render_template(template, variables)
|
|
270
|
+
|
|
271
|
+
def render_action_needed_instrumental(
|
|
272
|
+
self,
|
|
273
|
+
name: Optional[str] = None,
|
|
274
|
+
artist: Optional[str] = None,
|
|
275
|
+
title: Optional[str] = None,
|
|
276
|
+
instrumental_url: str = "",
|
|
277
|
+
) -> str:
|
|
278
|
+
"""
|
|
279
|
+
Render the instrumental selection reminder template.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
name: User's display name
|
|
283
|
+
artist: Artist name
|
|
284
|
+
title: Song title
|
|
285
|
+
instrumental_url: Instrumental selection URL
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Rendered email content
|
|
289
|
+
"""
|
|
290
|
+
template = self.get_action_needed_instrumental_template()
|
|
291
|
+
variables = {
|
|
292
|
+
"name": name or "there",
|
|
293
|
+
"artist": artist or "Unknown Artist",
|
|
294
|
+
"title": title or "Unknown Title",
|
|
295
|
+
"instrumental_url": instrumental_url,
|
|
296
|
+
}
|
|
297
|
+
return self.render_template(template, variables)
|
|
298
|
+
|
|
299
|
+
def upload_template(self, template_name: str, content: str) -> bool:
|
|
300
|
+
"""
|
|
301
|
+
Upload a template to GCS.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
template_name: Name of template file (e.g., "job-completion.txt")
|
|
305
|
+
content: Template content
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
True if upload successful
|
|
309
|
+
"""
|
|
310
|
+
blob_path = f"{self.TEMPLATE_PREFIX}{template_name}"
|
|
311
|
+
try:
|
|
312
|
+
blob = self.bucket.blob(blob_path)
|
|
313
|
+
blob.upload_from_string(content, content_type="text/plain")
|
|
314
|
+
logger.info(f"Uploaded template to GCS: {blob_path}")
|
|
315
|
+
return True
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.error(f"Failed to upload template {blob_path}: {e}")
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# Global instance
|
|
322
|
+
_template_service: Optional[TemplateService] = None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def get_template_service() -> TemplateService:
|
|
326
|
+
"""Get the global template service instance."""
|
|
327
|
+
global _template_service
|
|
328
|
+
if _template_service is None:
|
|
329
|
+
_template_service = TemplateService()
|
|
330
|
+
return _template_service
|