karaoke-gen 0.76.20__py3-none-any.whl → 0.81.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.
Files changed (26) hide show
  1. karaoke_gen/instrumental_review/static/index.html +179 -16
  2. karaoke_gen/karaoke_gen.py +5 -4
  3. karaoke_gen/lyrics_processor.py +25 -6
  4. {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.81.1.dist-info}/METADATA +79 -3
  5. {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.81.1.dist-info}/RECORD +26 -23
  6. lyrics_transcriber/core/config.py +8 -0
  7. lyrics_transcriber/core/controller.py +43 -1
  8. lyrics_transcriber/correction/agentic/providers/config.py +6 -0
  9. lyrics_transcriber/correction/agentic/providers/model_factory.py +24 -1
  10. lyrics_transcriber/correction/agentic/router.py +17 -13
  11. lyrics_transcriber/frontend/.gitignore +1 -0
  12. lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts +207 -0
  13. lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json +226 -0
  14. lyrics_transcriber/frontend/package.json +4 -1
  15. lyrics_transcriber/frontend/playwright.config.ts +1 -1
  16. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +34 -30
  17. lyrics_transcriber/frontend/src/components/Header.tsx +141 -34
  18. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +120 -3
  19. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +11 -1
  20. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +122 -35
  21. lyrics_transcriber/frontend/src/components/shared/types.ts +6 -0
  22. lyrics_transcriber/output/generator.py +50 -3
  23. lyrics_transcriber/transcribers/local_whisper.py +260 -0
  24. {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.81.1.dist-info}/WHEEL +0 -0
  25. {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.81.1.dist-info}/entry_points.txt +0 -0
  26. {karaoke_gen-0.76.20.dist-info → karaoke_gen-0.81.1.dist-info}/licenses/LICENSE +0 -0
@@ -60,8 +60,16 @@
60
60
  .logo {
61
61
  font-size: 1.25rem;
62
62
  font-weight: 600;
63
+ display: inline-flex;
64
+ align-items: center;
65
+ gap: 8px;
63
66
  }
64
-
67
+
68
+ .logo-img {
69
+ height: 40px;
70
+ width: auto;
71
+ }
72
+
65
73
  .track-info {
66
74
  font-size: 0.9rem;
67
75
  color: var(--text-muted);
@@ -568,6 +576,143 @@
568
576
  font-size: 1.5rem;
569
577
  color: var(--success);
570
578
  }
579
+
580
+ /* Mobile responsiveness */
581
+ @media (max-width: 768px) {
582
+ .app {
583
+ padding: 12px;
584
+ gap: 8px;
585
+ height: auto;
586
+ min-height: 100vh;
587
+ overflow-y: auto;
588
+ }
589
+
590
+ body {
591
+ overflow: auto;
592
+ }
593
+
594
+ .header {
595
+ flex-direction: column;
596
+ align-items: flex-start;
597
+ gap: 8px;
598
+ }
599
+
600
+ .header-left {
601
+ width: 100%;
602
+ }
603
+
604
+ .header-right {
605
+ width: 100%;
606
+ justify-content: flex-start;
607
+ flex-wrap: wrap;
608
+ }
609
+
610
+ .logo {
611
+ font-size: 1rem;
612
+ }
613
+
614
+ .logo-img {
615
+ height: 32px;
616
+ }
617
+
618
+ .waveform-player {
619
+ flex: none;
620
+ min-height: 200px;
621
+ }
622
+
623
+ .waveform-toolbar {
624
+ flex-wrap: wrap;
625
+ padding: 8px 12px;
626
+ gap: 8px;
627
+ }
628
+
629
+ .toolbar-left,
630
+ .toolbar-center,
631
+ .toolbar-right {
632
+ flex-wrap: wrap;
633
+ }
634
+
635
+ .audio-toggle-group {
636
+ order: 10;
637
+ width: 100%;
638
+ justify-content: center;
639
+ }
640
+
641
+ .bottom-section {
642
+ flex-direction: column;
643
+ }
644
+
645
+ .mute-panel {
646
+ max-height: none;
647
+ }
648
+
649
+ .selection-panel {
650
+ width: 100%;
651
+ }
652
+
653
+ .selection-option {
654
+ padding: 12px;
655
+ }
656
+
657
+ .btn {
658
+ min-height: 44px;
659
+ padding: 8px 12px;
660
+ }
661
+
662
+ .btn-icon {
663
+ width: 44px;
664
+ height: 44px;
665
+ }
666
+
667
+ .audio-toggle {
668
+ padding: 8px 12px;
669
+ min-height: 40px;
670
+ }
671
+
672
+ .zoom-btn {
673
+ width: 40px;
674
+ height: 40px;
675
+ }
676
+
677
+ .time-display {
678
+ font-size: 0.9rem;
679
+ }
680
+ }
681
+
682
+ @media (max-width: 480px) {
683
+ .app {
684
+ padding: 8px;
685
+ }
686
+
687
+ .header-left {
688
+ flex-wrap: wrap;
689
+ }
690
+
691
+ .track-info {
692
+ width: 100%;
693
+ margin-top: 4px;
694
+ }
695
+
696
+ .waveform-toolbar {
697
+ padding: 6px 8px;
698
+ }
699
+
700
+ .toolbar-center {
701
+ width: 100%;
702
+ justify-content: center;
703
+ order: -1;
704
+ }
705
+
706
+ .toolbar-left {
707
+ order: 1;
708
+ }
709
+
710
+ .toolbar-right {
711
+ order: 2;
712
+ width: 100%;
713
+ justify-content: space-between;
714
+ }
715
+ }
571
716
  </style>
572
717
  </head>
573
718
  <body>
@@ -641,7 +786,8 @@
641
786
 
642
787
  if (waveformRes.ok) {
643
788
  waveformData = await waveformRes.json();
644
- duration = waveformData.duration;
789
+ // API returns duration_seconds, not duration
790
+ duration = waveformData.duration_seconds || 0;
645
791
  }
646
792
 
647
793
  // Set initial selection based on recommendation
@@ -679,7 +825,7 @@
679
825
  app.innerHTML = `
680
826
  <div class="header">
681
827
  <div class="header-left">
682
- <span class="logo">🎤 Instrumental Review</span>
828
+ <span class="logo"><img src="https://gen.nomadkaraoke.com/nomad-karaoke-logo.svg" alt="Nomad Karaoke" class="logo-img" onerror="this.style.display='none'"> Instrumental Review</span>
683
829
  <span class="track-info">${escapeHtml(analysisData.artist) || ''} ${analysisData.artist && analysisData.title ? '–' : ''} ${escapeHtml(analysisData.title) || ''}</span>
684
830
  </div>
685
831
  <div class="header-right">
@@ -969,8 +1115,14 @@
969
1115
  canvas.onmousedown = (e) => {
970
1116
  const rect = canvas.getBoundingClientRect();
971
1117
  const x = e.clientX - rect.left;
1118
+
1119
+ // Guard against invalid duration
1120
+ if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(rect.width) || rect.width <= 0) {
1121
+ return;
1122
+ }
1123
+
972
1124
  const time = (x / rect.width) * duration;
973
-
1125
+
974
1126
  // Shift+drag to select mute region
975
1127
  if (e.shiftKey) {
976
1128
  isDragging = true;
@@ -993,18 +1145,26 @@
993
1145
 
994
1146
  const endDrag = (e) => {
995
1147
  if (!isDragging) return;
996
-
1148
+
997
1149
  const rect = canvas.getBoundingClientRect();
998
1150
  const x = e.clientX - rect.left;
1151
+
1152
+ // Guard against invalid duration
1153
+ if (!Number.isFinite(duration) || duration <= 0 || !Number.isFinite(rect.width) || rect.width <= 0) {
1154
+ isDragging = false;
1155
+ showSelectionOverlay(false);
1156
+ return;
1157
+ }
1158
+
999
1159
  const time = (x / rect.width) * duration;
1000
-
1160
+
1001
1161
  const start = Math.min(dragStartTime, time);
1002
1162
  const end = Math.max(dragStartTime, time);
1003
-
1163
+
1004
1164
  if (end - start > 0.5) {
1005
1165
  addRegion(start, end);
1006
1166
  }
1007
-
1167
+
1008
1168
  isDragging = false;
1009
1169
  showSelectionOverlay(false);
1010
1170
  };
@@ -1090,14 +1250,15 @@
1090
1250
 
1091
1251
  function seekTo(time, autoPlay = true) {
1092
1252
  const audio = document.getElementById('audio-player');
1093
- if (audio) {
1094
- audio.currentTime = time;
1095
- currentTime = time;
1096
- updatePlayhead();
1097
- // Auto-play when seeking via click (if not already playing)
1098
- if (autoPlay && !isPlaying) {
1099
- audio.play();
1100
- }
1253
+ // Guard against non-finite time values (NaN, Infinity)
1254
+ if (!audio || !Number.isFinite(time)) return;
1255
+
1256
+ audio.currentTime = time;
1257
+ currentTime = time;
1258
+ updatePlayhead();
1259
+ // Auto-play when seeking via click (if not already playing)
1260
+ if (autoPlay && !isPlaying) {
1261
+ audio.play();
1101
1262
  }
1102
1263
  }
1103
1264
 
@@ -1155,6 +1316,8 @@
1155
1316
  }
1156
1317
 
1157
1318
  function formatTime(seconds) {
1319
+ // Guard against NaN/Infinity
1320
+ if (!Number.isFinite(seconds)) return '0:00';
1158
1321
  const mins = Math.floor(seconds / 60);
1159
1322
  const secs = Math.floor(seconds % 60);
1160
1323
  return `${mins}:${secs.toString().padStart(2, '0')}`;
@@ -796,21 +796,22 @@ class KaraokePrep:
796
796
 
797
797
  outputs = output_generator.generate_outputs(
798
798
  transcription_corrected=correction_result,
799
+ lyrics_results={}, # Lyrics already written during transcription phase
799
800
  audio_filepath=audio_path,
800
801
  output_prefix=output_prefix,
801
802
  )
802
803
 
803
804
  # Copy video to expected location in parent directory
804
- if outputs and outputs.get("video_filepath"):
805
- source_video = outputs["video_filepath"]
805
+ if outputs and outputs.video:
806
+ source_video = outputs.video
806
807
  dest_video = os.path.join(track_output_dir, f"{artist_title} (With Vocals).mkv")
807
808
  shutil.copy2(source_video, dest_video)
808
809
  self.logger.info(f"Video rendered successfully: {dest_video}")
809
810
  processed_track["with_vocals_video"] = dest_video
810
811
 
811
812
  # Update ASS filepath for video background processing
812
- if outputs.get("ass_filepath"):
813
- processed_track["ass_filepath"] = outputs["ass_filepath"]
813
+ if outputs.ass:
814
+ processed_track["ass_filepath"] = outputs.ass
814
815
  else:
815
816
  self.logger.warning("Video rendering did not produce expected output")
816
817
  else:
@@ -170,15 +170,15 @@ class LyricsProcessor:
170
170
  def _check_transcription_providers(self) -> dict:
171
171
  """
172
172
  Check which transcription providers are configured and return their status.
173
-
173
+
174
174
  Returns:
175
175
  dict with 'configured' (list of provider names) and 'missing' (list of missing configs)
176
176
  """
177
177
  load_dotenv()
178
-
178
+
179
179
  configured = []
180
180
  missing = []
181
-
181
+
182
182
  # Check AudioShake
183
183
  audioshake_token = os.getenv("AUDIOSHAKE_API_TOKEN")
184
184
  if audioshake_token:
@@ -187,7 +187,7 @@ class LyricsProcessor:
187
187
  else:
188
188
  missing.append("AudioShake (AUDIOSHAKE_API_TOKEN)")
189
189
  self.logger.debug("AudioShake transcription provider: not configured (missing AUDIOSHAKE_API_TOKEN)")
190
-
190
+
191
191
  # Check Whisper via RunPod
192
192
  runpod_key = os.getenv("RUNPOD_API_KEY")
193
193
  whisper_id = os.getenv("WHISPER_RUNPOD_ID")
@@ -203,7 +203,16 @@ class LyricsProcessor:
203
203
  else:
204
204
  missing.append("Whisper (RUNPOD_API_KEY + WHISPER_RUNPOD_ID)")
205
205
  self.logger.debug("Whisper transcription provider: not configured")
206
-
206
+
207
+ # Check Local Whisper (whisper-timestamped)
208
+ try:
209
+ import whisper_timestamped
210
+ configured.append("Local Whisper")
211
+ self.logger.debug("Local Whisper transcription provider: configured (whisper-timestamped installed)")
212
+ except ImportError:
213
+ missing.append("Local Whisper (pip install karaoke-gen[local-whisper])")
214
+ self.logger.debug("Local Whisper transcription provider: not configured (whisper-timestamped not installed)")
215
+
207
216
  return {"configured": configured, "missing": missing}
208
217
 
209
218
  def _build_transcription_provider_error_message(self, missing_providers: list) -> str:
@@ -221,12 +230,18 @@ class LyricsProcessor:
221
230
  " - Set environment variable: AUDIOSHAKE_API_TOKEN=your_token\n"
222
231
  " - Get an API key at: https://www.audioshake.ai/\n"
223
232
  "\n"
224
- "2. Whisper via RunPod (Open-source alternative)\n"
233
+ "2. Whisper via RunPod (Cloud-based open-source)\n"
225
234
  " - Set environment variables:\n"
226
235
  " RUNPOD_API_KEY=your_key\n"
227
236
  " WHISPER_RUNPOD_ID=your_endpoint_id\n"
228
237
  " - Set up a Whisper endpoint at: https://www.runpod.io/\n"
229
238
  "\n"
239
+ "3. Local Whisper (No cloud required - runs on your machine)\n"
240
+ " - Install with: pip install karaoke-gen[local-whisper]\n"
241
+ " - For CPU-only: pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu\n"
242
+ " pip install karaoke-gen[local-whisper]\n"
243
+ " - Requires 2-10GB RAM depending on model size\n"
244
+ "\n"
230
245
  "ALTERNATIVES:\n"
231
246
  "\n"
232
247
  "- Use --skip-lyrics flag to generate instrumental-only karaoke (no synchronized lyrics)\n"
@@ -348,6 +363,10 @@ class LyricsProcessor:
348
363
  # Create config objects for LyricsTranscriber
349
364
  transcriber_config = TranscriberConfig(
350
365
  audioshake_api_token=env_config.get("audioshake_api_token"),
366
+ runpod_api_key=env_config.get("runpod_api_key"),
367
+ whisper_runpod_id=env_config.get("whisper_runpod_id"),
368
+ # Local Whisper is enabled by default as a fallback when no cloud providers are configured
369
+ enable_local_whisper=True,
351
370
  )
352
371
 
353
372
  lyrics_config = LyricsConfig(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karaoke-gen
3
- Version: 0.76.20
3
+ Version: 0.81.1
4
4
  Summary: Generate karaoke videos with synchronized lyrics. Handles the entire process from downloading audio and lyrics to creating the final video with title screens.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -13,12 +13,14 @@ Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Provides-Extra: local-whisper
16
17
  Requires-Dist: argparse (>=1.4.0)
17
18
  Requires-Dist: attrs (>=24.2.0)
18
19
  Requires-Dist: audio-separator[cpu] (>=0.34.0)
19
20
  Requires-Dist: beautifulsoup4 (>=4)
20
21
  Requires-Dist: cattrs (>=24.1.2)
21
22
  Requires-Dist: dropbox (>=12)
23
+ Requires-Dist: email-validator (>=2.0.0)
22
24
  Requires-Dist: fastapi (>=0.104.0)
23
25
  Requires-Dist: fetch-lyrics-from-genius (>=0.1)
24
26
  Requires-Dist: ffmpeg-python (>=0.2.0,<0.3.0)
@@ -40,6 +42,7 @@ Requires-Dist: kbputils (>=0.0.16,<0.0.17)
40
42
  Requires-Dist: langchain (>=0.3.0)
41
43
  Requires-Dist: langchain-anthropic (>=0.2.0)
42
44
  Requires-Dist: langchain-core (>=0.3.0)
45
+ Requires-Dist: langchain-google-vertexai (>=2.0.0)
43
46
  Requires-Dist: langchain-ollama (>=0.2.0)
44
47
  Requires-Dist: langchain-openai (>=0.2.0)
45
48
  Requires-Dist: langfuse (>=3.0.0)
@@ -74,10 +77,12 @@ Requires-Dist: python-levenshtein (>=0.26)
74
77
  Requires-Dist: python-multipart (>=0.0.20,<0.0.21)
75
78
  Requires-Dist: python-slugify (>=8)
76
79
  Requires-Dist: requests (>=2)
80
+ Requires-Dist: sendgrid (>=6.10.0)
77
81
  Requires-Dist: shortuuid (>=1.0.13)
78
82
  Requires-Dist: spacy (>=3.8.7)
79
83
  Requires-Dist: spacy-syllables (>=3)
80
84
  Requires-Dist: srsly (>=2.5.1)
85
+ Requires-Dist: stripe (>=7.0.0)
81
86
  Requires-Dist: syllables (>=1)
82
87
  Requires-Dist: syrics (>=0)
83
88
  Requires-Dist: thefuzz (>=0.22)
@@ -86,6 +91,7 @@ Requires-Dist: torch (>=2.7)
86
91
  Requires-Dist: tqdm (>=4.67)
87
92
  Requires-Dist: transformers (>=4.47)
88
93
  Requires-Dist: uvicorn[standard] (>=0.24.0)
94
+ Requires-Dist: whisper-timestamped (>=1.15.0) ; extra == "local-whisper"
89
95
  Requires-Dist: yt-dlp (>=2024.0.0)
90
96
  Project-URL: Documentation, https://github.com/nomadkaraoke/karaoke-gen/blob/main/README.md
91
97
  Project-URL: Homepage, https://github.com/nomadkaraoke/karaoke-gen
@@ -165,8 +171,40 @@ export AUDIOSHAKE_API_TOKEN="your_audioshake_token"
165
171
 
166
172
  Get an API key at [https://www.audioshake.ai/](https://www.audioshake.ai/) - business only, at time of writing this.
167
173
 
168
- #### Option 2: Whisper via RunPod
169
- Open-source alternative using OpenAI's Whisper model on RunPod infrastructure.
174
+ #### Option 2: Local Whisper (No Cloud Required)
175
+ Run Whisper directly on your local machine using whisper-timestamped. Works on CPU, NVIDIA GPU (CUDA), or Apple Silicon.
176
+
177
+ ```bash
178
+ # Install with local Whisper support
179
+ pip install "karaoke-gen[local-whisper]"
180
+
181
+ # Optional: Configure model size (tiny, base, small, medium, large)
182
+ export WHISPER_MODEL_SIZE="medium"
183
+
184
+ # Optional: Force specific device (cpu, cuda, mps)
185
+ export WHISPER_DEVICE="cpu"
186
+ ```
187
+
188
+ **Model Size Guide:**
189
+ | Model | VRAM | Speed | Quality |
190
+ |-------|------|-------|---------|
191
+ | tiny | ~1GB | Fast | Lower |
192
+ | base | ~1GB | Fast | Basic |
193
+ | small | ~2GB | Medium | Good |
194
+ | medium | ~5GB | Slower | Better |
195
+ | large | ~10GB | Slowest | Best |
196
+
197
+ **CPU-Only Installation** (no GPU required):
198
+ ```bash
199
+ # Pre-install CPU-only PyTorch first
200
+ pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu
201
+ pip install "karaoke-gen[local-whisper]"
202
+ ```
203
+
204
+ Local Whisper runs automatically as a fallback when no cloud transcription services are configured.
205
+
206
+ #### Option 3: Whisper via RunPod
207
+ Cloud-based alternative using OpenAI's Whisper model on RunPod infrastructure.
170
208
 
171
209
  ```bash
172
210
  export RUNPOD_API_KEY="your_runpod_key"
@@ -668,6 +706,44 @@ If the output video has quality problems:
668
706
  - Check available codecs: `ffmpeg -codecs`
669
707
  - For 4K output, ensure sufficient disk space (10GB+ per track)
670
708
 
709
+ ### Local Whisper Issues
710
+
711
+ #### GPU Out of Memory
712
+ If you get CUDA out of memory errors:
713
+ ```bash
714
+ # Use a smaller model
715
+ export WHISPER_MODEL_SIZE="small" # or "tiny"
716
+
717
+ # Or force CPU mode
718
+ export WHISPER_DEVICE="cpu"
719
+ ```
720
+
721
+ #### Slow Transcription on CPU
722
+ CPU transcription is significantly slower than GPU. For faster processing:
723
+ - Use a smaller model (`tiny` or `base`)
724
+ - Consider using cloud transcription (AudioShake or RunPod)
725
+ - On Apple Silicon, the `small` model offers good speed/quality balance
726
+
727
+ #### Model Download Issues
728
+ Whisper models are downloaded on first use (~1-3GB depending on size). If downloads fail:
729
+ - Check your internet connection
730
+ - Set a custom cache directory: `export WHISPER_CACHE_DIR="/path/with/space"`
731
+ - Models are cached in `~/.cache/whisper/` by default
732
+
733
+ #### whisper-timestamped Not Found
734
+ If you get "whisper-timestamped is not installed":
735
+ ```bash
736
+ pip install "karaoke-gen[local-whisper]"
737
+ # Or install directly:
738
+ pip install whisper-timestamped
739
+ ```
740
+
741
+ #### Disabling Local Whisper
742
+ If you want to disable local Whisper (e.g., to force cloud transcription):
743
+ ```bash
744
+ export ENABLE_LOCAL_WHISPER="false"
745
+ ```
746
+
671
747
  ---
672
748
 
673
749
  ## 🧪 Development
@@ -8,12 +8,12 @@ karaoke_gen/instrumental_review/analyzer.py,sha256=Heg8TbrwM4g5IV7bavmO6EfVD4M0U
8
8
  karaoke_gen/instrumental_review/editor.py,sha256=_DGTjKMk5WhoGtLGtTvHzU522LJyQQ_DSY1r8fULuiA,11568
9
9
  karaoke_gen/instrumental_review/models.py,sha256=cUSb_JheJK0cGdKx9f59-9sRvRrhrgdTdKBzQN3lHto,5226
10
10
  karaoke_gen/instrumental_review/server.py,sha256=Ick90X77t2EeMRwtx2U08sSybadQyWH7G0tDG-4JqP4,19377
11
- karaoke_gen/instrumental_review/static/index.html,sha256=EjMFxCQJOUSrsgwIXAW3R4bN6hYxDLmL4MHzoXxI4f0,59362
11
+ karaoke_gen/instrumental_review/static/index.html,sha256=1lzo_W5B4HxNStWPiVaP4I6ctqDkXAABJkQmojvBDqc,63235
12
12
  karaoke_gen/instrumental_review/waveform.py,sha256=Q6LBPZrJAD6mzZ7TmRf3Tf4gwYhUYTHumJKytLs3hSg,12940
13
13
  karaoke_gen/karaoke_finalise/__init__.py,sha256=HqZ7TIhgt_tYZ-nb_NNCaejWAcF_aK-7wJY5TaW_keM,46
14
14
  karaoke_gen/karaoke_finalise/karaoke_finalise.py,sha256=Wn1KcdRyINT63UxKUPT9uB-bsrFVih0Im_cjXtequS0,93534
15
- karaoke_gen/karaoke_gen.py,sha256=-kmv26iqF36OXHoKAdFCXqaLPhrqk-aH958v-cPbTWM,65694
16
- karaoke_gen/lyrics_processor.py,sha256=IzwscxBtDe2l7NhmWY8PdglYeMfIXhh2AWHQMY-ro1M,22829
15
+ karaoke_gen/karaoke_gen.py,sha256=84n2SE0MixJr01_btLmm5cVdf35hJvp7W638b8TKR-Q,65734
16
+ karaoke_gen/lyrics_processor.py,sha256=9BtL2uJa4Ekrodj2w_SXSeOraVKCB2kzYuHcGHTFpo8,23979
17
17
  karaoke_gen/metadata.py,sha256=SZW6TuUpkGGU98gRdjPfrR8F4vWXjnfCSGry2XD5_A4,6689
18
18
  karaoke_gen/pipeline/__init__.py,sha256=-MZnba4qobr1qGDamG9CieLl2pWCZMEB5_Yur62RKeM,2106
19
19
  karaoke_gen/pipeline/base.py,sha256=yg4LIm7Mc9ER0zCmZcUv4huEkotSSXK_0OAFio-TSNI,6235
@@ -44,8 +44,8 @@ lyrics_transcriber/__init__.py,sha256=g9ZbJg9U1qo7XzrC25J3bTKcNzzwUJWDVdi_7-hjcM
44
44
  lyrics_transcriber/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  lyrics_transcriber/cli/cli_main.py,sha256=F72ENLTj934bXjHAUbRm0toCK73qnuJhwEm9agBVKHQ,11596
46
46
  lyrics_transcriber/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
- lyrics_transcriber/core/config.py,sha256=AM6RZKll8tzdZtMLgvHRQb1SxiXVPek0q4vmSWVUvEo,1368
48
- lyrics_transcriber/core/controller.py,sha256=laeUakqT-0CoIyoBWYvv7kWxX-_wefWRwg2xrz84gRg,29432
47
+ lyrics_transcriber/core/config.py,sha256=_X_d1wSYTJjSquqbODYCwPdOYpnSR9KERwvr_jkdYls,2056
48
+ lyrics_transcriber/core/controller.py,sha256=dUJvnehr9_Mv3Syj_TWZQsQVsDD1w8AdF5_1xISA2cw,31661
49
49
  lyrics_transcriber/correction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  lyrics_transcriber/correction/agentic/__init__.py,sha256=p7PHiebuvRs8RDlPDs-9gLZKzXG5KfWg3fFCdDhY6pE,222
51
51
  lyrics_transcriber/correction/agentic/adapter.py,sha256=Z0JBTAA7xlSdctCHqO9nBMl78C4XmqsLKKtS6BvNZNI,2912
@@ -83,15 +83,15 @@ lyrics_transcriber/correction/agentic/prompts/classifier.py,sha256=pKbL4Cyj0-c_O
83
83
  lyrics_transcriber/correction/agentic/providers/__init__.py,sha256=PS7C4sKDfa6S9lSo33GXIRamCLsv0Jn7u0GtXuhiRD4,95
84
84
  lyrics_transcriber/correction/agentic/providers/base.py,sha256=bExuntMLLInMmWWNzN81_ScWQJhNYbtlF3wZYhlX-qw,1059
85
85
  lyrics_transcriber/correction/agentic/providers/circuit_breaker.py,sha256=D3Jg4YHqvy4gzlxfkALa7PztyYQpJb8NwJAonMS0TSI,4694
86
- lyrics_transcriber/correction/agentic/providers/config.py,sha256=FsX1xIF1UG_Kbxp4tlEnZc68AsRTL4Q4XenP_VuRi6o,2937
86
+ lyrics_transcriber/correction/agentic/providers/config.py,sha256=2dy9zynj8hU3LdRkb2RmKSOztsX4_Ay23EU-RfUGCrM,3206
87
87
  lyrics_transcriber/correction/agentic/providers/constants.py,sha256=aDIEsDvNQLEGlGk8klAaRxJmdldGBDFqwYLuCmlYoNM,692
88
88
  lyrics_transcriber/correction/agentic/providers/health.py,sha256=F8pHY5BQYvylGRDGXUHplcAJooAyiqVLRhBl4kHC1H8,710
89
89
  lyrics_transcriber/correction/agentic/providers/langchain_bridge.py,sha256=hderNRLrSZn49LrGBrgdCvBP5E7tPAugjaw7TFbb0JY,7957
90
- lyrics_transcriber/correction/agentic/providers/model_factory.py,sha256=ITPc7BLIhtHKzobERl0P7YsmunOquqbaJ_M5tinztx4,7667
90
+ lyrics_transcriber/correction/agentic/providers/model_factory.py,sha256=iKbpMEeTyhPN8n9abVf645TfovnFEz3ia1g6XLHqp4s,8613
91
91
  lyrics_transcriber/correction/agentic/providers/response_cache.py,sha256=Byr7fQJsgUMFlsvHeVCxTiFjjnbsg3KIlEmEEtAo-Gw,7047
92
92
  lyrics_transcriber/correction/agentic/providers/response_parser.py,sha256=a8pdUYKBS5X72gck3u1ndFYB__UN0UijAdxNhbHp8ZQ,3809
93
93
  lyrics_transcriber/correction/agentic/providers/retry_executor.py,sha256=hX21Zwy2cSECAw7k13ndEinWRqwjo4xYoSCQ2B2CUf0,3912
94
- lyrics_transcriber/correction/agentic/router.py,sha256=pk4xeS-BZLGJMdFj7Q7MjNaqYJF_glI590z9Alg15co,1229
94
+ lyrics_transcriber/correction/agentic/router.py,sha256=_JtnXgcIdui6qeN9x0EawThDGZavAwfpbtEJAYVlQTY,1334
95
95
  lyrics_transcriber/correction/agentic/workflows/__init__.py,sha256=OsBExAbIIKxJgX6FKXFOgcUjIG9AWJQV_fESZVdO8mo,77
96
96
  lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py,sha256=gMuLTUxkgYaciMsI4yrZSC3wi--7V_PgaDNE-Vd6FE8,575
97
97
  lyrics_transcriber/correction/agentic/workflows/correction_graph.py,sha256=kgZKnz0h9cG1EfhW7BSSl-kSpQtJrRM_S86kAniXfE4,1815
@@ -117,17 +117,19 @@ lyrics_transcriber/correction/handlers/word_operations.py,sha256=410xhyO9tiqezV5
117
117
  lyrics_transcriber/correction/operations.py,sha256=k5N8w_8BeR7CXiclaJ3zuu_g2KLoWSnnuD4OAmY3kJs,14010
118
118
  lyrics_transcriber/correction/phrase_analyzer.py,sha256=dtO_2LjxnPdHJM7De40mYIdHCkozwhizVVQp5XGO7x0,16962
119
119
  lyrics_transcriber/correction/text_utils.py,sha256=7QHK6-PY7Rx1G1E31sWiLBw00mHorRDo-M44KMHFaZs,833
120
- lyrics_transcriber/frontend/.gitignore,sha256=lgGIPiVpFVUNSZl9oNQLelLOWUzpF7sikLW8xmsrrqI,248
120
+ lyrics_transcriber/frontend/.gitignore,sha256=cR2ofyyWArkna_jByfaWi8gTeMhsKTSoK128PmIw218,262
121
121
  lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs,sha256=KTYy2KCV2OpHhussV5jIPDdUSr7RftMRhqPsRUmgfAY,2765465
122
122
  lyrics_transcriber/frontend/.yarnrc.yml,sha256=0hZQ1OTcPqTUNBqQeme4VFkIzrsabHNzLtc_M-wSgIM,66
123
123
  lyrics_transcriber/frontend/README.md,sha256=-D6CAfKTT7Y0V3EjlZ2fMy7fyctFQ4x2TJ9vx6xtccM,1607
124
124
  lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md,sha256=iRZbicW5satHel9gbG-uLyZ7oq3xdp87KQlJEL1ZhK8,8384
125
125
  lyrics_transcriber/frontend/__init__.py,sha256=nW8acRSWTjXoRwGqcTU4w-__X7tMAE0iXL0uihBN3CU,836
126
+ lyrics_transcriber/frontend/e2e/agentic-corrections.spec.ts,sha256=yNynyV8dUfwRJ1a0Cdr6o2SZEMFiuGAQG1ZM0Ro8q9o,7359
127
+ lyrics_transcriber/frontend/e2e/fixtures/agentic-correction-data.json,sha256=_h-nI76gPXuqWErpTBrZaTgcc8LNLi6j81t3Wtt--ac,8184
126
128
  lyrics_transcriber/frontend/eslint.config.js,sha256=3ADH23ANA4NNBKFy6nCVk65e8bx1DrVd_FIaYNnhuqA,734
127
129
  lyrics_transcriber/frontend/index.html,sha256=hcVQvxU1yITMrMS4vVLwn4YwvnlXsfl4XY9UNtXvWAw,1135
128
130
  lyrics_transcriber/frontend/package-lock.json,sha256=gQekpsz4CAKMJ8Fi331Q3Pv5yqhZlQ-nbGoDNnF35WE,159262
129
- lyrics_transcriber/frontend/package.json,sha256=ttlZ0EqPSz0HIkjyCt_3szk1CsFrLR-uNtyhdxsc30w,1278
130
- lyrics_transcriber/frontend/playwright.config.ts,sha256=7AtyWuAP193WVbFDqzqKMYZwE1dZ7TZ_QKzNfP8hIB0,1520
131
+ lyrics_transcriber/frontend/package.json,sha256=qujjeqPUSJizfHxK_2egicJYea8fziJO4O6u2A6N9Xw,1395
132
+ lyrics_transcriber/frontend/playwright.config.ts,sha256=l5aoc_rEbrYxIipTAVbpRER0FL5bAevYtRTT-chGUqA,1523
131
133
  lyrics_transcriber/frontend/public/android-chrome-192x192.png,sha256=lg-6aPF5mGLiuG7LyftZk_0RI41srmpA8wj-NkaaQms,17632
132
134
  lyrics_transcriber/frontend/public/android-chrome-512x512.png,sha256=x-zuKT3NYsTqAWzhKRTZeD4-0uYoUjqMPZpKTChqNJ8,123447
133
135
  lyrics_transcriber/frontend/public/apple-touch-icon.png,sha256=6y5vGra54w5oc8VP6sn2JjoQtN9hWTKn0YPhmdlmfU0,16188
@@ -143,7 +145,7 @@ lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx,sha256=ubJwQewryjU
143
145
  lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx,sha256=Yg6FG0LtrneRfAYeBu3crt_RdN-_o7FojtYhDMDKi0o,8595
144
146
  lyrics_transcriber/frontend/src/components/AppHeader.tsx,sha256=5KUVADDv9cAs2WNX9M31utQIYHKMi81unzrCW3j1fl0,2396
145
147
  lyrics_transcriber/frontend/src/components/AudioPlayer.tsx,sha256=XOCz0VtGiAIBs1qnCwrAixwfgHbTSGpjEb1jQg8wqzc,5441
146
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx,sha256=lcOQhSQBuTBv-adGB4uOb66qR-uX4OuN5P0CfFMxmHo,4941
148
+ lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx,sha256=Z5i0MMaFC1dbafUsZVsNEMkdoqBLjkA6yCWtjoMmqi8,5207
147
149
  lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx,sha256=XtF5XNLL2ztm714tXql7rKi2BX4k_bsizpZ_ZCvpu8s,13368
148
150
  lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx,sha256=Hp-i1iSB3pzrpPH2wIREtEHHaReimBaYi8vcSUUArlg,9512
149
151
  lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx,sha256=CoTZS9Z3pf4lfPrzpQ2hZvLqFvt-IarSGBSCxFxD-y4,6274
@@ -154,8 +156,8 @@ lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx,sha256=VQy5fp
154
156
  lyrics_transcriber/frontend/src/components/EditWordList.tsx,sha256=XN59JnNPYNI4KrSenW7cYC6zUIlK7GvlyRbj9eg-Eac,13716
155
157
  lyrics_transcriber/frontend/src/components/FileUpload.tsx,sha256=fwn2rMWtMLPTZLREMb3ps4prSf9nzxGwnjmeC6KYsJA,2383
156
158
  lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx,sha256=U7duKns4IqNXwbWFbQfdyaswnvkSRpfsU0UG__-Serc,20192
157
- lyrics_transcriber/frontend/src/components/Header.tsx,sha256=UmQzQpsdLrtzi9U5HFDtvtr6vq9qZu3cnPDnT_T3QuI,18151
158
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx,sha256=TFo2HCr6k9bENsQm-AfT797ZyFLqvKDa8g5W-pw1v24,54256
159
+ lyrics_transcriber/frontend/src/components/Header.tsx,sha256=_lIo1ZsObF1lygXYX865Rhj053KPWvPewi7e0p11xuA,23429
160
+ lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx,sha256=9UZbTAXXTcYgFWwr030z-vrd8vklYrmgDCxCrPifho8,59256
159
161
  lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx,sha256=j4rQjBQVbaPsp1ra_rvEoCqmX3JFJdfNnFvj3BvfsgQ,6069
160
162
  lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx,sha256=DuQBAdF8bYcAYKNEWVklXSAlEykhIDQKbELq_4SEPCg,27415
161
163
  lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx,sha256=BXkEeo5yMgHkeOCBcZKqxMb1rspjXH-X5_6X9Hl7z3E,2588
@@ -171,16 +173,16 @@ lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx,sha256=VQg_gBF
171
173
  lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx,sha256=6ME02FkFwCgDAxW49yW260N4vbr80eAJ332Ex811GOo,1643
172
174
  lyrics_transcriber/frontend/src/components/TimelineEditor.tsx,sha256=gJRCxdmJo80g0h5hq5AtDHK-HbOoYhMaQYvP2WgOuRI,13201
173
175
  lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx,sha256=aivGi6ehI6cDqwtoKBb6Eif8gpPqi0t3mJT8i5Feu7Q,4803
174
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx,sha256=SK5nVjey4Eg2_JiVAnJbOtNwDEmrKYlNb529F7Q0nLY,11337
176
+ lyrics_transcriber/frontend/src/components/TranscriptionView.tsx,sha256=6FqgDS6NBbdeGMANKQyF1rjxN0CZy6tYAheOq_FTYEE,11827
175
177
  lyrics_transcriber/frontend/src/components/WordDivider.tsx,sha256=ynib_j0w0q4iOYAk7D4IyZJCq71LykX7SaD9haGlZeI,6695
176
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx,sha256=ZVZ61rwrKLdlmhlNuSNR7-sgdl7JdBxVTeSeKO6K-3w,16321
178
+ lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx,sha256=0WFhL8BlcIrjw1BTakP-UgG0j2pripyqG5LK66a1IOE,21333
177
179
  lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx,sha256=FpMn-0i1NFxdIHZN0dxHkWzIIsOEi9CJc_rQMpLyxVw,2031
178
180
  lyrics_transcriber/frontend/src/components/shared/components/Word.tsx,sha256=CXzepxI3Negx2cqdfqLNGgesNbbIcczomXed71FupNw,2676
179
181
  lyrics_transcriber/frontend/src/components/shared/constants.ts,sha256=GByG5KFLJOX0iCs80_PLXxZKQ5FBX5Qw0Mg05Xf8Faw,1142
180
182
  lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts,sha256=eEgBHiKIWOzFK8eBzBgcQRv7StKpaPchqh3k2kFlwgY,6253
181
183
  lyrics_transcriber/frontend/src/components/shared/styles.ts,sha256=J1jCSuRqpk1mOFYAqJudhxeozH-q1bi-dsOibLukBJU,411
182
184
  lyrics_transcriber/frontend/src/components/shared/types.js,sha256=1DqoH1vIn6o1ng-XyBS6JRVVkf8Hj7ub_UD4x8loMjA,77
183
- lyrics_transcriber/frontend/src/components/shared/types.ts,sha256=22gpjxcUglmykSimUu6l_6XzgIga1fX8QIM1Mr0CpiI,3973
185
+ lyrics_transcriber/frontend/src/components/shared/types.ts,sha256=mA7YELw4x0TI3PG2-6EuB1WmGnG36h_C4iwTOvmIoaY,4249
184
186
  lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts,sha256=Yh5c_kOdOjE84FtKVB4BBC8QIgkFk5tO0ZJa9oJqqqU,5870
185
187
  lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts,sha256=jpLT65Rk_toaB-8X2lRGyYZ9EoMQDI45GviUT7N9Bp0,3240
186
188
  lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts,sha256=TJ2oHDitFFVxm83eFEhdlwvhx--mIt3054YbET2RiXs,2575
@@ -266,7 +268,7 @@ lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf,sha256=WNG5LOQ-uGUF_WWT5aQ
266
268
  lyrics_transcriber/output/fonts/arial.ttf,sha256=NcDzVZ2NtWnjbDEJW4pg1EFkPZX1kTneQOI_ragZuDM,275572
267
269
  lyrics_transcriber/output/fonts/georgia.ttf,sha256=fQuyDGMrtZ6BoIhfVzvSFz9x9zIE3pBY_raM4DIicHI,142964
268
270
  lyrics_transcriber/output/fonts/verdana.ttf,sha256=lu0UlJyktzks_yNbnEHVXBJTgqu-DA08K53WaJfK4Ms,139640
269
- lyrics_transcriber/output/generator.py,sha256=jruQv0FQHlVkrLML-rcJqWCR7n5YT1QRE9yDBjqQJy0,11990
271
+ lyrics_transcriber/output/generator.py,sha256=eblMtME-OBTbf1awd9BvlTLyUer_XcIXfIo0L-l38b4,13774
270
272
  lyrics_transcriber/output/lrc_to_cdg.py,sha256=2pi5tvreD_ADAR4RF5yVwj7OJ4Pf5Zo_EJ7rt4iH3k0,2063
271
273
  lyrics_transcriber/output/lyrics_file.py,sha256=_KQyQjCOMIwQdQ0115uEAUIjQWTRmShkSfQuINPKxaw,3741
272
274
  lyrics_transcriber/output/plain_text.py,sha256=XARaWcy6MeQeQCUoz0PV_bHoBw5dba-u79bjS7XucnE,3867
@@ -280,12 +282,13 @@ lyrics_transcriber/storage/dropbox.py,sha256=Dyam1ULTkoxD1X5trkZ5dGp5XhBGCn998mo
280
282
  lyrics_transcriber/transcribers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
281
283
  lyrics_transcriber/transcribers/audioshake.py,sha256=RihuLKzhhHfX7m5cjKISwIuTQkGWapCS29D6Qk3hR4U,15869
282
284
  lyrics_transcriber/transcribers/base_transcriber.py,sha256=T3m4ZCwZ9Bpv6Jvb2hNcnllk-lmeNmADDJlSySBtP1Q,6480
285
+ lyrics_transcriber/transcribers/local_whisper.py,sha256=oT-MsKdkHMgRpuCdYL4o8vtCZ4Uhls7BzkRDf-QHMHM,9926
283
286
  lyrics_transcriber/transcribers/whisper.py,sha256=YcCB1ic9H6zL1GS0jD0emu8-qlcH0QVEjjjYB4aLlIQ,13260
284
287
  lyrics_transcriber/types.py,sha256=UJjaxhVd2o14AG4G8ToU598p0JeYdiTFjpG38jGCoYQ,27917
285
288
  lyrics_transcriber/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
286
289
  lyrics_transcriber/utils/word_utils.py,sha256=-cMGpj9UV4F6IsoDKAV2i1aiqSO8eI91HMAm_igtVMk,958
287
- karaoke_gen-0.76.20.dist-info/METADATA,sha256=LuFlyEoDv5uHvTPpoUcAQChjNC1QdHnRBQzmdZRUPIw,20667
288
- karaoke_gen-0.76.20.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
289
- karaoke_gen-0.76.20.dist-info/entry_points.txt,sha256=xIyLe7K84ZyjO8L0_AmNectz93QjGSs5AkApMtlAd4g,160
290
- karaoke_gen-0.76.20.dist-info/licenses/LICENSE,sha256=81R_4XwMZDODHD7JcZeUR8IiCU8AD7Ajl6bmwR9tYDk,1074
291
- karaoke_gen-0.76.20.dist-info/RECORD,,
290
+ karaoke_gen-0.81.1.dist-info/METADATA,sha256=zA3O3rRKeXu_LX28aeN3knlhx3WcpXD8Ozf4s_LT2C0,23077
291
+ karaoke_gen-0.81.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
292
+ karaoke_gen-0.81.1.dist-info/entry_points.txt,sha256=xIyLe7K84ZyjO8L0_AmNectz93QjGSs5AkApMtlAd4g,160
293
+ karaoke_gen-0.81.1.dist-info/licenses/LICENSE,sha256=81R_4XwMZDODHD7JcZeUR8IiCU8AD7Ajl6bmwR9tYDk,1074
294
+ karaoke_gen-0.81.1.dist-info/RECORD,,
@@ -11,6 +11,14 @@ class TranscriberConfig:
11
11
  runpod_api_key: Optional[str] = None
12
12
  whisper_runpod_id: Optional[str] = None
13
13
 
14
+ # Local Whisper configuration - reads from environment variables with sensible defaults
15
+ # Environment variables: WHISPER_MODEL_SIZE, WHISPER_DEVICE, WHISPER_CACHE_DIR, WHISPER_LANGUAGE
16
+ enable_local_whisper: bool = True # Enabled by default as fallback
17
+ local_whisper_model_size: str = field(default_factory=lambda: os.getenv("WHISPER_MODEL_SIZE", "medium"))
18
+ local_whisper_device: Optional[str] = field(default_factory=lambda: os.getenv("WHISPER_DEVICE"))
19
+ local_whisper_cache_dir: Optional[str] = field(default_factory=lambda: os.getenv("WHISPER_CACHE_DIR"))
20
+ local_whisper_language: Optional[str] = field(default_factory=lambda: os.getenv("WHISPER_LANGUAGE"))
21
+
14
22
 
15
23
  @dataclass
16
24
  class LyricsConfig: