Skip to content

Instantly share code, notes, and snippets.

@Zerodya
Last active February 26, 2025 13:58
Show Gist options
  • Save Zerodya/301ade170c94861268b9bffef385b096 to your computer and use it in GitHub Desktop.
Save Zerodya/301ade170c94861268b9bffef385b096 to your computer and use it in GitHub Desktop.
requests
mutagen
langdetect

Quick script to translate the lyrics in your flac files using the free DeepL API.

  • Detects the language of the lyrics in every flac file in your music library, and translates it if it doesn't match your target language
  • Keeps track of already processed files in translations_store.json which is stored in the script's directory

Tested on a ~1400 songs library.

Python dependencies

  • requests
  • mutagen
  • langdetect

Usage

$ python3 ./translate-lyrics.py -h

usage: translate-lyrics.py [-h] [-n] [-f] [-d DIRECTORY]

Translate lyrics in flac files using DeepL API. Keeps track of processed files inside `translations_store.json` which is stored in the script's
directory.

options:
  -h, --help            show this help message and exit
  -n, --dry-run         show what would be translated without modifying files.
  -f, --force           force rechecking of all files even if processed before.
  -d DIRECTORY, --directory DIRECTORY
                        directory containing the flac files.

Change these:

MUSIC_DIR = "/my/music/library" # Directory containing the FLAC files
DEEPL_AUTH_KEY = "mysecretkey" # Your DeepL API key (https://www.deepl.com/en/your-account/keys)
TARGET_LANG = "EN"
import os
import json
import argparse
import requests
from mutagen.flac import FLAC
from langdetect import detect, LangDetectException
from datetime import datetime
# Change these
MUSIC_DIR = "/my/music/library" # Directory containing the FLAC files
DEEPL_AUTH_KEY = "mysecretkey" # Your DeepL API key (https://www.deepl.com/en/your-account/keys)
TARGET_LANG = "EN"
DEEPL_API_URL = "https://api-free.deepl.com/v2/translate"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
STORE_FILE = os.path.join(SCRIPT_DIR, "translations_store.json")
def load_store():
"""Load processed files store from JSON file."""
if os.path.exists(STORE_FILE):
with open(STORE_FILE, "r") as f:
try:
store = json.load(f)
except json.JSONDecodeError:
store = {}
else:
store = {}
return store
def save_store(store):
"""Save the processed files store to JSON file."""
with open(STORE_FILE, "w") as f:
json.dump(store, f, indent=4)
def translate_text(text, target_lang=TARGET_LANG):
"""Translate given text using DeepL API."""
payload = {
'auth_key': DEEPL_AUTH_KEY,
'text': text,
'target_lang': target_lang,
}
response = requests.post(DEEPL_API_URL, data=payload)
if response.status_code == 200:
json_data = response.json()
translated_text = json_data['translations'][0]['text']
return translated_text
else:
print("lyrics: error from deepl api for: unknown file")
return None
def process_flac_file(file_path, dry_run=False, force=False, store=None):
"""Process a single FLAC file: check lyrics language and translate if necessary."""
abs_path = os.path.abspath(file_path)
try:
audio = FLAC(file_path)
except Exception as e:
print(f"lyrics: error opening file: {file_path}")
return
# Check for the lyrics tag (assumed to be "LYRICS")
if "LYRICS" in audio:
original_lyrics = audio["LYRICS"][0]
try:
detected_lang = detect(original_lyrics)
except LangDetectException as e:
print(f"lyrics: could not detect language: {file_path}")
store[abs_path] = {"status": "detection_failed", "timestamp": datetime.now().isoformat()}
return
if detected_lang.lower() != "en":
print(f"lyrics: translating file (detected language {detected_lang.lower()}): {file_path}")
translated = translate_text(original_lyrics)
if translated:
if dry_run:
print(f"lyrics: dry run translation preview: {file_path}")
print(f"lyrics: {translated.lower()}: {file_path}")
else:
audio["LYRICS"] = [translated]
try:
audio.save()
print(f"lyrics: updated with translated lyrics: {file_path}")
except Exception as save_error:
print(f"lyrics: error saving file: {file_path}")
store[abs_path] = {"status": "save_error", "timestamp": datetime.now().isoformat()}
return
store[abs_path] = {"status": "translated", "timestamp": datetime.now().isoformat()}
else:
print(f"lyrics: translation failed for: {file_path}")
store[abs_path] = {"status": "translation_failed", "timestamp": datetime.now().isoformat()}
else:
print(f"lyrics: lyrics already in english: {file_path}")
store[abs_path] = {"status": "english", "timestamp": datetime.now().isoformat()}
else:
print(f"lyrics: no lyrics tag found: {file_path}")
store[abs_path] = {"status": "no_lyrics", "timestamp": datetime.now().isoformat()}
def iterate_albums(directory, dry_run=False, force=False):
"""Walk through the directory recursively and process each FLAC file."""
store = load_store() if not force else {}
skipped_count = 0
for root, dirs, files in os.walk(directory):
for file in files:
if file.lower().endswith(".flac"):
file_path = os.path.join(root, file)
abs_path = os.path.abspath(file_path)
if not force and abs_path in store:
skipped_count += 1
continue
process_flac_file(file_path, dry_run=dry_run, force=force, store=store)
if skipped_count > 0:
print(f"Skipped {skipped_count} paths.")
save_store(store)
def main():
parser = argparse.ArgumentParser(
description="Translate lyrics in flac files using DeepL API.\n"
"Keeps track of processed files inside `translations_store.json` which is stored in the script's directory."
)
parser.add_argument("-n", "--dry-run", action="store_true", help="show what would be translated without modifying files.")
parser.add_argument("-f", "--force", action="store_true", help="force rechecking of all files even if processed before.")
parser.add_argument("-d", "--directory", type=str, default=MUSIC_DIR, help="directory containing the flac files.")
args = parser.parse_args()
iterate_albums(directory=args.directory, dry_run=args.dry_run, force=args.force)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment