2. MIDI Metadata Normalizer
2. MIDI Metadata Normalizer
Transforming raw MIDI metadata into a clean, consistent, human‑editable dataset
After extracting the raw metadata from your MIDI library, the next step is to convert that unfiltered snapshot into a normalized, machine‑clean, human‑friendly spreadsheet. The Metadata Normalizer script takes the raw dump and produces a structured, consistent, encoding‑safe metadata file that is ready for human review.
This step is crucial because raw metadata often contains:
- mojibake (UTF‑8/CP1252 corruption)
- inconsistent punctuation
- multiple composers or ambiguous composer fields
- missing or duplicated titles
- stray comments or text events
- inconsistent capitalization
- whitespace artifacts
The normalizer script does not attempt to interpret the music — that’s the human’s job in Step 3. Instead, it ensures that every row is clean, consistent, and ready for curation.
What This Script Does
✔ Repairs encoding issues
The script attempts to reverse common CP1252/UTF‑8 mojibake patterns so titles and composers become readable again.
✔ Normalizes whitespace and punctuation
Extra spaces, stray underscores, and odd punctuation are cleaned up.
✔ Extracts the primary composer
If multiple composers are listed (e.g., “Smith / Jones”), the script selects the first one as the canonical composer.
✔ Ensures consistent column structure
Every row contains the same fields, even if some are empty.
✔ Writes a clean, normalized XLSX and CSV
The output file is named something like:
normalized_metadata.xlsx
normalized_metadata.csv
This file becomes the authoritative source for human review in Step 3.
Why This Step Matters
- It separates mechanical cleanup from human interpretation.
- It ensures the renamer script receives clean, predictable input.
- It prevents encoding issues from propagating into filenames.
- It makes the human review step dramatically easier.
- It creates a stable, reproducible metadata foundation for the entire library.
✅ FULL METADATA NORMALIZER SCRIPT
import csv
import re
from pathlib import Path
from openpyxl import Workbook, load_workbook
RAW_METADATA_FILE = "raw_metadata" # base name only; script auto-detects CSV or XLSX
OUTPUT_XLSX = "normalized_metadata.xlsx"
OUTPUT_CSV = "normalized_metadata.csv"
# ------------------------------------------------------------
# Encoding cleanup
# ------------------------------------------------------------
def fix_encoding(s):
"""Attempt to repair common CP1252/UTF‑8 mojibake."""
if not isinstance(s, str):
return ""
try:
return s.encode("cp1252").decode("utf-8")
except:
return s
def clean_text(s):
"""Normalize whitespace and punctuation."""
s = fix_encoding(s)
s = s.replace("\u2018", "'").replace("\u2019", "'")
s = s.replace("\u201C", '"').replace("\u201D", '"')
s = s.replace("\u2013", "-").replace("\u2014", "-")
s = s.replace("\u2026", "...")
s = re.sub(r"\s+", " ", s)
return s.strip()
# ------------------------------------------------------------
# Composer normalization
# ------------------------------------------------------------
def extract_primary_composer(composer):
"""Use only the first composer if multiple are listed."""
composer = clean_text(composer)
parts = re.split(r"[\/,&]| and ", composer, flags=re.IGNORECASE)
return parts[0].strip()
# ------------------------------------------------------------
# Load raw metadata
# ------------------------------------------------------------
def load_raw_metadata(base_name):
csv_path = Path(base_name + ".csv")
xlsx_path = Path(base_name + ".xlsx")
if csv_path.exists():
with open(csv_path, newline="", encoding="utf-8") as f:
return list(csv.DictReader(f))
if xlsx_path.exists():
wb = load_workbook(xlsx_path)
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
headers = rows[0]
return [dict(zip(headers, row)) for row in rows[1:]]
raise FileNotFoundError("No raw metadata file found.")
# ------------------------------------------------------------
# Write output files
# ------------------------------------------------------------
def write_xlsx(rows, path):
wb = Workbook()
ws = wb.active
ws.append(list(rows[0].keys()))
for row in rows:
ws.append(list(row.values()))
wb.save(path)
def write_csv(rows, path):
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
# ------------------------------------------------------------
# Main normalization logic
# ------------------------------------------------------------
def main():
rows = load_raw_metadata(RAW_METADATA_FILE)
normalized = []
for row in rows:
filename = row.get("filename", "").strip()
title_raw = row.get("title_raw", "")
composer_raw = row.get("composer_raw", "")
comments_raw = row.get("comments_raw", "")
tempo = row.get("tempo", "")
time_sig = row.get("time_signature", "")
key_sig = row.get("key_signature", "")
title = clean_text(title_raw)
composer = clean_text(composer_raw)
comments = clean_text(comments_raw)
primary_composer = extract_primary_composer(composer)
normalized.append({
"filename": filename,
"title": title,
"composer": composer,
"primary_composer": primary_composer,
"comments": comments,
"tempo": tempo,
"time_signature": time_sig,
"key_signature": key_sig,
})
write_xlsx(normalized, OUTPUT_XLSX)
write_csv(normalized, OUTPUT_CSV)
print("\nNormalized metadata written to:")
print(f" {OUTPUT_XLSX}")
print(f" {OUTPUT_CSV}")
print("\nNormalization complete.")
if __name__ == "__main__":
main()
Summary
The Metadata Normalizer is the bridge between raw extraction and human curation.
It produces a clean, consistent, encoding‑safe metadata file that is easy to review and ready for the renaming stage. This step ensures that the entire workflow remains deterministic, reversible, and archivally sound.