lyrics-transcriber 0.17.2__tar.gz → 0.19.0__tar.gz
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-0.17.2 → lyrics_transcriber-0.19.0}/PKG-INFO +2 -2
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/audioshake_transcriber.py +8 -1
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/transcriber.py +41 -22
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/pyproject.toml +3 -4
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/LICENSE +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/README.md +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/__init__.py +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/llm_prompts/README.md +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/llm_prompts/llm_prompt_lyrics_correction_andrew_handwritten_20231118.txt +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/llm_prompts/llm_prompt_lyrics_correction_gpt_optimised_20231119.txt +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/llm_prompts/llm_prompt_lyrics_matching_andrew_handwritten_20231118.txt +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/llm_prompts/promptfooconfig.yaml +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/llm_prompts/test_data/ABBA-UnderAttack-Genius.txt +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/utils/__init__.py +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/utils/ass.py +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/utils/cli.py +0 -0
- {lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/utils/subtitles.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lyrics-transcriber
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.19.0
|
4
4
|
Summary: Automatically create synchronised lyrics files in ASS and MidiCo LRC formats with word-level timestamps, using Whisper and lyrics from Genius and Spotify
|
5
5
|
Home-page: https://github.com/karaokenerds/python-lyrics-transcriber
|
6
6
|
License: MIT
|
@@ -13,8 +13,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
16
17
|
Requires-Dist: Cython (>=0)
|
17
|
-
Requires-Dist: auditok (>=0.2)
|
18
18
|
Requires-Dist: dtw-python (>=1)
|
19
19
|
Requires-Dist: llvmlite (>=0)
|
20
20
|
Requires-Dist: lyricsgenius (>=3)
|
{lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/audioshake_transcriber.py
RENAMED
@@ -5,10 +5,11 @@ import json
|
|
5
5
|
|
6
6
|
|
7
7
|
class AudioShakeTranscriber:
|
8
|
-
def __init__(self, api_token, logger):
|
8
|
+
def __init__(self, api_token, logger, output_prefix):
|
9
9
|
self.api_token = api_token
|
10
10
|
self.base_url = "https://groovy.audioshake.ai"
|
11
11
|
self.logger = logger
|
12
|
+
self.output_prefix = output_prefix
|
12
13
|
|
13
14
|
def transcribe(self, audio_filepath):
|
14
15
|
self.logger.info(f"Transcribing {audio_filepath} using AudioShake API")
|
@@ -103,4 +104,10 @@ class AudioShakeTranscriber:
|
|
103
104
|
if "text" not in segment:
|
104
105
|
segment["text"] = " ".join(word["text"] for word in segment["words"])
|
105
106
|
|
107
|
+
transcription_data["output_filename"] = self.get_output_filename(" (AudioShake)")
|
108
|
+
|
106
109
|
return transcription_data
|
110
|
+
|
111
|
+
def get_output_filename(self, suffix):
|
112
|
+
"""Generate consistent filename with (Purpose) suffix pattern"""
|
113
|
+
return f"{self.output_prefix}{suffix}"
|
@@ -166,6 +166,8 @@ class LyricsTranscriber:
|
|
166
166
|
|
167
167
|
self.create_folders()
|
168
168
|
|
169
|
+
self.output_prefix = f"{artist} - {title}"
|
170
|
+
|
169
171
|
def generate(self):
|
170
172
|
self.logger.debug(f"audio_filepath is set: {self.audio_filepath}, beginning initial whisper transcription")
|
171
173
|
|
@@ -294,7 +296,7 @@ class LyricsTranscriber:
|
|
294
296
|
|
295
297
|
self.logger.debug("write_corrected_lyrics_data_file initiating OpenAI client")
|
296
298
|
|
297
|
-
corrected_lyrics_data_json_cache_filepath = os.path.join(self.cache_dir,
|
299
|
+
corrected_lyrics_data_json_cache_filepath = os.path.join(self.cache_dir, self.get_output_filename(" (Lyrics Corrected).json"))
|
298
300
|
|
299
301
|
if os.path.isfile(corrected_lyrics_data_json_cache_filepath):
|
300
302
|
self.logger.debug(
|
@@ -331,9 +333,7 @@ class LyricsTranscriber:
|
|
331
333
|
# TODO: Possibly add a step after segment-based correct to get the LLM to self-analyse the diff
|
332
334
|
|
333
335
|
self.outputs["llm_transcript"] = ""
|
334
|
-
self.outputs["llm_transcript_filepath"] = os.path.join(
|
335
|
-
self.cache_dir, "lyrics-" + self.get_song_slug() + "-llm-correction-transcript.txt"
|
336
|
-
)
|
336
|
+
self.outputs["llm_transcript_filepath"] = os.path.join(self.cache_dir, self.get_output_filename(" (LLM Transcript).txt"))
|
337
337
|
|
338
338
|
total_segments = len(self.outputs["transcription_data_dict"]["segments"])
|
339
339
|
self.logger.info(f"Beginning correction using LLM, total segments: {total_segments}")
|
@@ -466,7 +466,9 @@ class LyricsTranscriber:
|
|
466
466
|
if self.outputs["corrected_lyrics_data_dict"]:
|
467
467
|
self.logger.debug(f"corrected_lyrics_data_dict exists, writing plain text lyrics file")
|
468
468
|
|
469
|
-
corrected_lyrics_text_filepath = os.path.join(
|
469
|
+
corrected_lyrics_text_filepath = os.path.join(
|
470
|
+
self.cache_dir, self.get_output_filename(" (Lyrics Corrected).txt") # Updated to use consistent naming
|
471
|
+
)
|
470
472
|
self.outputs["corrected_lyrics_text_filepath"] = corrected_lyrics_text_filepath
|
471
473
|
|
472
474
|
self.outputs["corrected_lyrics_text"] = ""
|
@@ -475,7 +477,7 @@ class LyricsTranscriber:
|
|
475
477
|
with open(corrected_lyrics_text_filepath, "w", encoding="utf-8") as f:
|
476
478
|
for corrected_segment in self.outputs["corrected_lyrics_data_dict"]["segments"]:
|
477
479
|
self.outputs["corrected_lyrics_text"] += corrected_segment["text"].strip() + "\n"
|
478
|
-
f.write(corrected_segment["text".strip()
|
480
|
+
f.write(corrected_segment["text"].strip() + "\n")
|
479
481
|
|
480
482
|
def write_spotify_lyrics_data_file(self):
|
481
483
|
if self.spotify_cookie and self.song_known:
|
@@ -484,7 +486,9 @@ class LyricsTranscriber:
|
|
484
486
|
self.logger.warning(f"skipping spotify fetch as not all spotify params were set")
|
485
487
|
return
|
486
488
|
|
487
|
-
spotify_lyrics_data_json_cache_filepath = os.path.join(
|
489
|
+
spotify_lyrics_data_json_cache_filepath = os.path.join(
|
490
|
+
self.cache_dir, self.get_output_filename(" (Lyrics Spotify).json") # Updated to use consistent naming
|
491
|
+
)
|
488
492
|
|
489
493
|
if os.path.isfile(spotify_lyrics_data_json_cache_filepath):
|
490
494
|
self.logger.debug(
|
@@ -531,7 +535,9 @@ class LyricsTranscriber:
|
|
531
535
|
if self.outputs["spotify_lyrics_data_dict"]:
|
532
536
|
self.logger.debug(f"spotify_lyrics data found, checking/writing plain text lyrics file")
|
533
537
|
|
534
|
-
spotify_lyrics_text_filepath = os.path.join(
|
538
|
+
spotify_lyrics_text_filepath = os.path.join(
|
539
|
+
self.cache_dir, self.get_output_filename(" (Lyrics Spotify).txt") # Updated to use consistent naming
|
540
|
+
)
|
535
541
|
self.outputs["spotify_lyrics_text_filepath"] = spotify_lyrics_text_filepath
|
536
542
|
|
537
543
|
lines = self.outputs["spotify_lyrics_data_dict"]["lyrics"]["lines"]
|
@@ -561,7 +567,7 @@ class LyricsTranscriber:
|
|
561
567
|
self.logger.warning(f"skipping genius fetch as not all genius params were set")
|
562
568
|
return
|
563
569
|
|
564
|
-
genius_lyrics_cache_filepath = os.path.join(self.cache_dir,
|
570
|
+
genius_lyrics_cache_filepath = os.path.join(self.cache_dir, self.get_output_filename(" (Lyrics Genius).txt"))
|
565
571
|
|
566
572
|
if os.path.isfile(genius_lyrics_cache_filepath):
|
567
573
|
self.logger.debug(f"found existing file at genius_lyrics_cache_filepath, reading: {genius_lyrics_cache_filepath}")
|
@@ -635,7 +641,9 @@ class LyricsTranscriber:
|
|
635
641
|
# then loops over each word and writes all words with MidiCo segment start/end formatting
|
636
642
|
# and word-level timestamps to a MidiCo-compatible LRC file
|
637
643
|
def write_midico_lrc_file(self):
|
638
|
-
self.outputs["midico_lrc_filepath"] =
|
644
|
+
self.outputs["midico_lrc_filepath"] = os.path.join(
|
645
|
+
self.cache_dir, self.get_output_filename(" (Lyrics Corrected).lrc") # Updated suffix
|
646
|
+
)
|
639
647
|
|
640
648
|
lrc_filename = self.outputs["midico_lrc_filepath"]
|
641
649
|
self.logger.debug(f"writing midico formatted word timestamps to LRC file: {lrc_filename}")
|
@@ -692,9 +700,15 @@ class LyricsTranscriber:
|
|
692
700
|
self.logger.debug("Reset current line")
|
693
701
|
|
694
702
|
current_line_text += (" " if current_line_text else "") + word["text"]
|
703
|
+
|
704
|
+
# fmt: off
|
695
705
|
lyric_segment = subtitles.LyricSegment(
|
696
|
-
text=word["text"],
|
706
|
+
text=word["text"],
|
707
|
+
ts=timedelta(seconds=word["start"]),
|
708
|
+
end_ts=timedelta(seconds=word["end"])
|
697
709
|
)
|
710
|
+
# fmt: on
|
711
|
+
|
698
712
|
current_line.segments.append(lyric_segment)
|
699
713
|
self.logger.debug(f"Added word to current line. Current line: '{current_line_text}'")
|
700
714
|
|
@@ -706,7 +720,7 @@ class LyricsTranscriber:
|
|
706
720
|
return screens
|
707
721
|
|
708
722
|
def write_ass_file(self):
|
709
|
-
self.outputs["ass_subtitles_filepath"] = self.
|
723
|
+
self.outputs["ass_subtitles_filepath"] = os.path.join(self.cache_dir, self.get_output_filename(" (Lyrics Corrected).ass"))
|
710
724
|
|
711
725
|
ass_filepath = self.outputs["ass_subtitles_filepath"]
|
712
726
|
self.logger.debug(f"writing ASS formatted subtitle file: {ass_filepath}")
|
@@ -832,10 +846,10 @@ class LyricsTranscriber:
|
|
832
846
|
|
833
847
|
def write_transcribed_lyrics_plain_text(self):
|
834
848
|
if self.outputs["transcription_data_dict"]:
|
835
|
-
transcription_cache_suffix = "
|
849
|
+
transcription_cache_suffix = " (Lyrics AudioShake).txt" if self.audioshake_api_token else " (Lyrics Whisper).txt"
|
836
850
|
self.logger.debug(f"transcription_cache_suffix: {transcription_cache_suffix}")
|
837
851
|
|
838
|
-
transcribed_lyrics_text_filepath = os.path.join(self.cache_dir,
|
852
|
+
transcribed_lyrics_text_filepath = os.path.join(self.cache_dir, self.get_output_filename(transcription_cache_suffix))
|
839
853
|
self.outputs["transcribed_lyrics_text_filepath"] = transcribed_lyrics_text_filepath
|
840
854
|
|
841
855
|
self.outputs["transcribed_lyrics_text"] = ""
|
@@ -949,8 +963,8 @@ class LyricsTranscriber:
|
|
949
963
|
return new_segments
|
950
964
|
|
951
965
|
def transcribe(self):
|
952
|
-
transcription_cache_suffix = "
|
953
|
-
self.outputs["transcription_data_filepath"] = self.get_cache_filepath(
|
966
|
+
transcription_cache_suffix = " (AudioShake).json" if self.audioshake_api_token else " (Whisper).json"
|
967
|
+
self.outputs["transcription_data_filepath"] = self.get_cache_filepath(transcription_cache_suffix)
|
954
968
|
|
955
969
|
transcription_cache_filepath = self.outputs["transcription_data_filepath"]
|
956
970
|
if os.path.isfile(transcription_cache_filepath):
|
@@ -963,13 +977,16 @@ class LyricsTranscriber:
|
|
963
977
|
self.logger.debug(f"Using AudioShake API for transcription")
|
964
978
|
from .audioshake_transcriber import AudioShakeTranscriber
|
965
979
|
|
966
|
-
audioshake = AudioShakeTranscriber(self.audioshake_api_token, logger=self.logger)
|
980
|
+
audioshake = AudioShakeTranscriber(api_token=self.audioshake_api_token, logger=self.logger, output_prefix=self.output_prefix)
|
967
981
|
transcription_data = audioshake.transcribe(self.audio_filepath)
|
968
982
|
else:
|
969
983
|
self.logger.debug(f"Using Whisper for transcription with model: {self.transcription_model}")
|
970
984
|
audio = whisper.load_audio(self.audio_filepath)
|
971
985
|
model = whisper.load_model(self.transcription_model, device="cpu")
|
972
|
-
transcription_data = whisper.transcribe(model, audio, language="en",
|
986
|
+
transcription_data = whisper.transcribe(model, audio, language="en", beam_size=5, temperature=0.2, best_of=5)
|
987
|
+
|
988
|
+
# auditok is needed for voice activity detection, but it has OS package dependencies that are hard to install on some platforms
|
989
|
+
# transcription_data = whisper.transcribe(model, audio, language="en", vad="auditok", beam_size=5, temperature=0.2, best_of=5)
|
973
990
|
|
974
991
|
# Remove segments with no words, only music
|
975
992
|
transcription_data["segments"] = [segment for segment in transcription_data["segments"] if segment["text"].strip() != "Music"]
|
@@ -987,10 +1004,8 @@ class LyricsTranscriber:
|
|
987
1004
|
self.outputs["transcription_data_dict"] = transcription_data
|
988
1005
|
|
989
1006
|
def get_cache_filepath(self, extension):
|
990
|
-
|
991
|
-
|
992
|
-
hash_value = self.get_file_hash(self.audio_filepath)
|
993
|
-
cache_filepath = os.path.join(self.cache_dir, filename_slug + "_" + hash_value + extension)
|
1007
|
+
# Instead of using slugify and hash, use the consistent naming pattern
|
1008
|
+
cache_filepath = os.path.join(self.cache_dir, self.get_output_filename(extension))
|
994
1009
|
self.logger.debug(f"get_cache_filepath returning cache_filepath: {cache_filepath}")
|
995
1010
|
return cache_filepath
|
996
1011
|
|
@@ -1011,3 +1026,7 @@ class LyricsTranscriber:
|
|
1011
1026
|
|
1012
1027
|
if self.output_dir is not None:
|
1013
1028
|
os.makedirs(self.output_dir, exist_ok=True)
|
1029
|
+
|
1030
|
+
def get_output_filename(self, suffix):
|
1031
|
+
"""Generate consistent filename with (Purpose) suffix pattern"""
|
1032
|
+
return f"{self.output_prefix}{suffix}"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "lyrics-transcriber"
|
3
|
-
version = "0.
|
3
|
+
version = "0.19.0"
|
4
4
|
description = "Automatically create synchronised lyrics files in ASS and MidiCo LRC formats with word-level timestamps, using Whisper and lyrics from Genius and Spotify"
|
5
5
|
authors = ["Andrew Beveridge <andrew@beveridge.uk>"]
|
6
6
|
license = "MIT"
|
@@ -26,13 +26,12 @@ lyricsgenius = ">=3"
|
|
26
26
|
python-slugify = ">=8"
|
27
27
|
syrics = ">=0"
|
28
28
|
openai = "^1"
|
29
|
-
openai-whisper = ">=20231117"
|
30
29
|
transformers = ">=4"
|
31
|
-
auditok = ">=0.2"
|
32
30
|
whisper-timestamped = ">=1"
|
33
31
|
tenacity = ">=8"
|
32
|
+
openai-whisper = ">=20231117"
|
34
33
|
# Note: after adding openai-whisper and whisper-timestamped with poetry lock, I then removed all traces of triton
|
35
|
-
# from poetry.lock before running poetry install, as triton doesn't support macOS but isn't actually needed for whisper
|
34
|
+
# from poetry.lock before running poetry install, as triton doesn't support macOS but isn't actually needed for whisper
|
36
35
|
# This was the only way I was able to get a working cross-platform build published to PyPI.
|
37
36
|
# To update the lockfile and install/upgrade dependencies, modify the dependency list above then run:
|
38
37
|
# poetry lock; patch -p0 poetry.lock <.github/removetriton.patch; poetry install
|
File without changes
|
File without changes
|
File without changes
|
{lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/llm_prompts/README.md
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/utils/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{lyrics_transcriber-0.17.2 → lyrics_transcriber-0.19.0}/lyrics_transcriber/utils/subtitles.py
RENAMED
File without changes
|