Create ai-gen_v2.py
This commit is contained in:
562
ai-gen_v2.py
Normal file
562
ai-gen_v2.py
Normal file
@@ -0,0 +1,562 @@
|
||||
from typing import List, Dict, Any, Tuple
|
||||
|
||||
# ============================================================
|
||||
# CONFIG / CONSTANTS (keep most variables here)
|
||||
# ============================================================
|
||||
|
||||
GLOBAL_KEY = "G" # global major key center
|
||||
DEBUG = 0 # 0..5 (0 = no debug, 5 = max)
|
||||
|
||||
NOTE_TO_PC = {
|
||||
"C": 0, "B#": 0,
|
||||
"C#": 1, "Db": 1,
|
||||
"D": 2,
|
||||
"D#": 3, "Eb": 3,
|
||||
"E": 4, "Fb": 4,
|
||||
"F": 5, "E#": 5,
|
||||
"F#": 6, "Gb": 6,
|
||||
"G": 7,
|
||||
"G#": 8, "Ab": 8,
|
||||
"A": 9,
|
||||
"A#": 10, "Bb": 10,
|
||||
"B": 11, "Cb": 11,
|
||||
}
|
||||
|
||||
MAJOR_INTERVALS = [0, 2, 4, 5, 7, 9, 11] # 7-note set
|
||||
|
||||
DEGREE_LABELS = {
|
||||
0: "1",
|
||||
1: "b2",
|
||||
2: "2",
|
||||
3: "b3",
|
||||
4: "3",
|
||||
5: "4",
|
||||
6: "b5",
|
||||
7: "5",
|
||||
8: "#5",
|
||||
9: "6",
|
||||
10: "b7",
|
||||
11: "7",
|
||||
}
|
||||
|
||||
ROMANS = {1: "I", 2: "II", 3: "III", 4: "IV", 5: "V", 6: "VI", 7: "VII"}
|
||||
|
||||
# 21-mode catalog: each is a 7-note set described by intervals from its root.
|
||||
# Ordering matters for tie-break: Major-family first, then Melodic-minor, then Harmonic-minor.
|
||||
|
||||
MODE_CATALOG: List[Dict[str, Any]] = []
|
||||
|
||||
def _build_catalog():
|
||||
# Major family (7)
|
||||
major_modes = [
|
||||
("Ionian (major)", [0, 2, 4, 5, 7, 9, 11]),
|
||||
("Dorian", [0, 2, 3, 5, 7, 9, 10]),
|
||||
("Phrygian", [0, 1, 3, 5, 7, 8, 10]),
|
||||
("Lydian", [0, 2, 4, 6, 7, 9, 11]),
|
||||
("Mixolydian", [0, 2, 4, 5, 7, 9, 10]),
|
||||
("Aeolian (natural minor)", [0, 2, 3, 5, 7, 8, 10]),
|
||||
("Locrian", [0, 1, 3, 5, 6, 8, 10]),
|
||||
]
|
||||
|
||||
# Melodic minor family (7) (ascending melodic minor)
|
||||
melodic_minor_modes = [
|
||||
("Melodic minor", [0, 2, 3, 5, 7, 9, 11]),
|
||||
("Dorian b2", [0, 1, 3, 5, 7, 9, 10]),
|
||||
("Lydian augmented", [0, 2, 4, 6, 8, 9, 11]),
|
||||
("Lydian dominant", [0, 2, 4, 6, 7, 9, 10]),
|
||||
("Mixolydian b6", [0, 2, 4, 5, 7, 8, 10]),
|
||||
("Locrian #2", [0, 2, 3, 5, 6, 8, 10]),
|
||||
("Altered (super locrian)", [0, 1, 3, 4, 6, 8, 10]),
|
||||
]
|
||||
|
||||
# Harmonic minor family (7)
|
||||
harmonic_minor_modes = [
|
||||
("Harmonic minor", [0, 2, 3, 5, 7, 8, 11]),
|
||||
("Locrian #6", [0, 1, 3, 5, 6, 9, 10]),
|
||||
("Ionian #5", [0, 2, 4, 5, 8, 9, 11]),
|
||||
("Dorian #4", [0, 2, 3, 6, 7, 9, 10]),
|
||||
("Phrygian dominant", [0, 1, 4, 5, 7, 8, 10]),
|
||||
("Lydian #2", [0, 3, 4, 6, 7, 9, 11]),
|
||||
("Altered diminished", [0, 1, 3, 4, 6, 7, 9]), # sometimes called "Ultralocrian"
|
||||
]
|
||||
|
||||
# store
|
||||
for fam_i, (family, modes) in enumerate([
|
||||
("major", major_modes),
|
||||
("melodic_minor", melodic_minor_modes),
|
||||
("harmonic_minor", harmonic_minor_modes),
|
||||
]):
|
||||
for mode_i, (name, intervals) in enumerate(modes):
|
||||
MODE_CATALOG.append({
|
||||
"family": family,
|
||||
"family_rank": fam_i, # tie-break: smaller is preferred
|
||||
"mode_rank": mode_i, # within family
|
||||
"name": name,
|
||||
"intervals": intervals
|
||||
})
|
||||
|
||||
_build_catalog()
|
||||
|
||||
# ============================================================
|
||||
# BASIC HELPERS
|
||||
# ============================================================
|
||||
|
||||
def dprint(level: int, msg: str) -> None:
|
||||
if DEBUG >= level:
|
||||
print(msg)
|
||||
|
||||
def normalize_note(name: str) -> str:
|
||||
s = name.strip().replace("♭", "b").replace("♯", "#")
|
||||
if not s:
|
||||
return s
|
||||
return s[0].upper() + s[1:]
|
||||
|
||||
def note_to_pc(name: str) -> int:
|
||||
name = normalize_note(name)
|
||||
if name not in NOTE_TO_PC:
|
||||
raise ValueError(f"Unknown note name: {name}")
|
||||
return NOTE_TO_PC[name]
|
||||
|
||||
def build_scale_from_intervals(root_pc: int, intervals: List[int]) -> List[int]:
|
||||
return [(root_pc + i) % 12 for i in intervals]
|
||||
|
||||
def build_major_scale_pcs(tonic: str) -> List[int]:
|
||||
t_pc = note_to_pc(tonic)
|
||||
return [(t_pc + i) % 12 for i in MAJOR_INTERVALS]
|
||||
|
||||
def pitch_to_degree_label(note_pc: int, root_pc: int) -> str:
|
||||
return DEGREE_LABELS[(note_pc - root_pc) % 12]
|
||||
|
||||
# ============================================================
|
||||
# CHORD PARSING + REQUIRED TONES
|
||||
# ============================================================
|
||||
|
||||
def parse_chord_symbol(symbol: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parses roots like D, Db, F#
|
||||
Parses qualities: maj7 / M7, m / m7, dominant by presence of 7/9/13 without maj/min marker.
|
||||
Parses alterations: b9, #9, b13, #11, b5, #5
|
||||
Also recognizes plain extensions: 9, 11, 13 (as unaltered).
|
||||
"""
|
||||
s = symbol.strip().replace("♭", "b").replace("♯", "#")
|
||||
if not s:
|
||||
raise ValueError("Empty chord symbol.")
|
||||
|
||||
# root letter + optional accidental
|
||||
if len(s) >= 2 and s[1] in ("b", "#"):
|
||||
root, rest = s[:2], s[2:]
|
||||
else:
|
||||
root, rest = s[0], s[1:]
|
||||
|
||||
root = normalize_note(root)
|
||||
r = rest.lower()
|
||||
|
||||
# Quality
|
||||
quality = "maj"
|
||||
if "maj" in r or "ma7" in r or "maj7" in r or "∆" in r:
|
||||
quality = "maj"
|
||||
elif r.startswith("m") or "min" in r:
|
||||
quality = "min"
|
||||
|
||||
# Dominant (7/9/13) when not maj/min explicitly
|
||||
dominant = False
|
||||
if any(x in r for x in ["7", "9", "11", "13"]) and ("maj" not in r and not (r.startswith("m") or "min" in r)):
|
||||
quality = "dom"
|
||||
dominant = True
|
||||
|
||||
# Extract alterations and extensions
|
||||
# We treat "b9" etc as explicit, and "9"/"11"/"13" as plain extensions if present.
|
||||
alts = []
|
||||
for a in ["b9", "#9", "b13", "#11", "b5", "#5"]:
|
||||
if a in r:
|
||||
alts.append(a)
|
||||
|
||||
exts = []
|
||||
for e in ["13", "11", "9", "7"]: # check longer first
|
||||
if e in r:
|
||||
exts.append(e)
|
||||
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"root": root,
|
||||
"quality": quality, # 'maj', 'min', 'dom'
|
||||
"dominant": dominant,
|
||||
"alts": alts,
|
||||
"exts": exts,
|
||||
}
|
||||
|
||||
def chord_required_semitones(ch: Dict[str, Any]) -> Tuple[List[int], List[int]]:
|
||||
"""
|
||||
Returns (core_tones, extra_tones) as semitone offsets from chord root.
|
||||
core_tones: identity tones (triad + 7 if present/assumed by quality)
|
||||
extra_tones: extensions/alterations (9/11/13, b9, #9, #11, b13, etc)
|
||||
"""
|
||||
q = ch["quality"]
|
||||
exts = ch["exts"]
|
||||
alts = set(ch["alts"])
|
||||
|
||||
# core: 1 + 3/b3 + 5 + 7/b7 depending on chord quality
|
||||
if q == "maj":
|
||||
core = [0, 4, 7] + ([11] if ("7" in exts or "maj7" in ch["symbol"].lower() or "ma7" in ch["symbol"].lower()) else [])
|
||||
elif q == "min":
|
||||
core = [0, 3, 7] + ([10] if ("7" in exts or "m7" in ch["symbol"].lower()) else [])
|
||||
else: # dom
|
||||
core = [0, 4, 7, 10] # dominant implies b7
|
||||
|
||||
extra: List[int] = []
|
||||
|
||||
# plain extensions (if written)
|
||||
if "9" in exts:
|
||||
extra.append(2)
|
||||
if "11" in exts:
|
||||
extra.append(5)
|
||||
if "13" in exts:
|
||||
extra.append(9)
|
||||
|
||||
# alterations override / add
|
||||
if "b9" in alts:
|
||||
extra.append(1)
|
||||
if "#9" in alts:
|
||||
extra.append(3)
|
||||
if "#11" in alts:
|
||||
extra.append(6)
|
||||
if "b13" in alts:
|
||||
extra.append(8)
|
||||
if "b5" in alts:
|
||||
extra.append(6)
|
||||
if "#5" in alts:
|
||||
extra.append(8)
|
||||
|
||||
# remove duplicates while preserving order
|
||||
def uniq(xs: List[int]) -> List[int]:
|
||||
out = []
|
||||
seen = set()
|
||||
for x in xs:
|
||||
if x not in seen:
|
||||
out.append(x)
|
||||
seen.add(x)
|
||||
return out
|
||||
|
||||
return uniq(core), uniq(extra)
|
||||
|
||||
# ============================================================
|
||||
# ROMAN NUMERAL IN GLOBAL MAJOR KEY
|
||||
# ============================================================
|
||||
|
||||
def degree_in_global_major(chord_root: str, global_key: str) -> int:
|
||||
scale = build_major_scale_pcs(global_key)
|
||||
r_pc = note_to_pc(chord_root)
|
||||
return scale.index(r_pc) + 1 if r_pc in scale else 0
|
||||
|
||||
def roman_for_degree(deg: int, quality: str) -> str:
|
||||
if deg == 0:
|
||||
return "N/A"
|
||||
base = ROMANS[deg]
|
||||
return base.lower() if quality == "min" else base
|
||||
|
||||
# ============================================================
|
||||
# 21-MODE MATCHING ENGINE
|
||||
# ============================================================
|
||||
|
||||
def mode_match_rank(
|
||||
root_pc: int,
|
||||
core: List[int],
|
||||
extra: List[int],
|
||||
melody_pcs: List[int],
|
||||
mode: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Scoring idea:
|
||||
- core tones should strongly be present in mode (big penalty if missing)
|
||||
- extra (extensions/alterations) should be present if possible (smaller penalty)
|
||||
- melody notes in mode add points, weighted by order (earlier notes count slightly more)
|
||||
"""
|
||||
mode_pcs = set(build_scale_from_intervals(root_pc, mode["intervals"]))
|
||||
|
||||
core_pcs = {(root_pc + s) % 12 for s in core}
|
||||
extra_pcs = {(root_pc + s) % 12 for s in extra}
|
||||
|
||||
missing_core = [pc for pc in core_pcs if pc not in mode_pcs]
|
||||
missing_extra = [pc for pc in extra_pcs if pc not in mode_pcs]
|
||||
|
||||
# melody match with order weighting
|
||||
n = max(len(melody_pcs), 1)
|
||||
weights = [(n - i) / n for i in range(n)] # first notes slightly heavier
|
||||
melody_hits = []
|
||||
melody_score = 0.0
|
||||
for i, pc in enumerate(melody_pcs):
|
||||
hit = (pc in mode_pcs)
|
||||
melody_hits.append(hit)
|
||||
if hit:
|
||||
melody_score += 1.0 * weights[i]
|
||||
|
||||
# penalties / bonuses
|
||||
score = 0.0
|
||||
score += 4.0 * (len(core_pcs) - len(missing_core))
|
||||
score -= 10.0 * len(missing_core)
|
||||
score += 1.5 * (len(extra_pcs) - len(missing_extra))
|
||||
score -= 2.5 * len(missing_extra)
|
||||
score += melody_score
|
||||
|
||||
return {
|
||||
"mode": mode,
|
||||
"score": score,
|
||||
"missing_core": missing_core,
|
||||
"missing_extra": missing_extra,
|
||||
"melody_score": melody_score,
|
||||
"melody_hits": melody_hits,
|
||||
}
|
||||
|
||||
def choose_modes_21(
|
||||
chord_info: Dict[str, Any],
|
||||
melody_pcs: List[int],
|
||||
) -> Dict[str, Any]:
|
||||
root_pc = note_to_pc(chord_info["root"])
|
||||
core, extra = chord_required_semitones(chord_info)
|
||||
|
||||
dprint(4, f"[debug] chord root pc={root_pc}, core semitones={core}, extra semitones={extra}")
|
||||
|
||||
ranked = []
|
||||
for mode in MODE_CATALOG:
|
||||
r = mode_match_rank(root_pc, core, extra, melody_pcs, mode)
|
||||
ranked.append(r)
|
||||
|
||||
# Sort:
|
||||
# 1) higher score
|
||||
# 2) prefer major-family first (family_rank)
|
||||
# 3) prefer earlier mode_rank
|
||||
ranked.sort(key=lambda x: (-x["score"], x["mode"]["family_rank"], x["mode"]["mode_rank"]))
|
||||
|
||||
if DEBUG >= 5:
|
||||
for k in range(min(10, len(ranked))):
|
||||
m = ranked[k]
|
||||
dprint(5, f"[debug] rank {k+1}: {m['mode']['family']}::{m['mode']['name']} score={m['score']:.3f} "
|
||||
f"missing_core={len(m['missing_core'])} missing_extra={len(m['missing_extra'])} "
|
||||
f"melody_score={m['melody_score']:.3f}")
|
||||
|
||||
best = ranked[0]
|
||||
# include some alternatives that are close
|
||||
alternatives = ranked[1:6]
|
||||
|
||||
return {
|
||||
"best": best,
|
||||
"alternatives": alternatives,
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# SIMPLE MELODY FUNCTION TAGS (per chord)
|
||||
# ============================================================
|
||||
|
||||
def analyze_melody_functions(melody_pcs: List[int], root_pc: int, quality: str) -> Dict[int, List[str]]:
|
||||
"""
|
||||
Heuristics, minimal and local:
|
||||
- chord_tone: chord triad + 7 (based on quality)
|
||||
- guide_tone: the 3rd and 7th relative to root (major 3rd + maj7 assumed for maj7-ish,
|
||||
minor 3rd + b7 for minor7-ish, major3 + b7 for dominant)
|
||||
- scale_line: 3 stepwise notes
|
||||
- neighbor: a == c and b step away
|
||||
- passing / chromatic passing: between two chord tones
|
||||
"""
|
||||
n = len(melody_pcs)
|
||||
labels: Dict[int, List[str]] = {i: [] for i in range(n)}
|
||||
|
||||
if quality == "maj":
|
||||
chord_tones = {(root_pc + s) % 12 for s in [0, 4, 7, 11]}
|
||||
guide_tones = {(root_pc + 4) % 12, (root_pc + 11) % 12}
|
||||
elif quality == "min":
|
||||
chord_tones = {(root_pc + s) % 12 for s in [0, 3, 7, 10]}
|
||||
guide_tones = {(root_pc + 3) % 12, (root_pc + 10) % 12}
|
||||
else:
|
||||
chord_tones = {(root_pc + s) % 12 for s in [0, 4, 7, 10]}
|
||||
guide_tones = {(root_pc + 4) % 12, (root_pc + 10) % 12}
|
||||
|
||||
for i, pc in enumerate(melody_pcs):
|
||||
if pc in guide_tones:
|
||||
labels[i].append("guide_tone")
|
||||
if pc in chord_tones:
|
||||
labels[i].append("chord_tone")
|
||||
|
||||
# scale line
|
||||
for i in range(1, n - 1):
|
||||
a, b, c = melody_pcs[i - 1], melody_pcs[i], melody_pcs[i + 1]
|
||||
d1 = (b - a) % 12
|
||||
d2 = (c - b) % 12
|
||||
if d1 in (1, 2, 10, 11) and d2 == d1:
|
||||
labels[i - 1].append("scale_line")
|
||||
labels[i].append("scale_line")
|
||||
labels[i + 1].append("scale_line")
|
||||
|
||||
# neighbors
|
||||
for i in range(1, n - 1):
|
||||
a, b, c = melody_pcs[i - 1], melody_pcs[i], melody_pcs[i + 1]
|
||||
if a == c and a in chord_tones:
|
||||
step = (b - a) % 12
|
||||
if step in (1, 2):
|
||||
labels[i].append("upper_neighbor")
|
||||
elif step in (10, 11):
|
||||
labels[i].append("lower_neighbor")
|
||||
|
||||
# passing / chromatic passing (no scale context; purely chord-tone bridge)
|
||||
for i in range(1, n - 1):
|
||||
a, b, c = melody_pcs[i - 1], melody_pcs[i], melody_pcs[i + 1]
|
||||
if a in chord_tones and c in chord_tones and b not in chord_tones:
|
||||
up = (b - a) % 12
|
||||
down = (c - b) % 12
|
||||
if up in (1, 2, 10, 11) and down in (1, 2, 10, 11):
|
||||
# if b is diatonic to either end? we don't know scale here, so treat semitone as chromatic
|
||||
if up == 1 or down == 1 or up == 11 or down == 11:
|
||||
labels[i].append("chromatic_passing_tone")
|
||||
else:
|
||||
labels[i].append("passing_tone")
|
||||
|
||||
return labels
|
||||
|
||||
# ============================================================
|
||||
# MAIN ANALYSIS
|
||||
# ============================================================
|
||||
|
||||
def analyze_chord_and_melody(chord_symbol: str, melody_notes: List[str], global_key: str) -> Dict[str, Any]:
|
||||
chord = parse_chord_symbol(chord_symbol)
|
||||
melody_pcs = [note_to_pc(n) for n in melody_notes]
|
||||
|
||||
deg = degree_in_global_major(chord["root"], global_key)
|
||||
roman = roman_for_degree(deg, chord["quality"])
|
||||
|
||||
dprint(3, f"[debug] parsed chord={chord} deg={deg} roman={roman}")
|
||||
|
||||
mode_choice = choose_modes_21(chord, melody_pcs)
|
||||
best = mode_choice["best"]
|
||||
best_mode = best["mode"]
|
||||
|
||||
root_pc = note_to_pc(chord["root"])
|
||||
melody_degrees = [pitch_to_degree_label(pc, root_pc) for pc in melody_pcs]
|
||||
|
||||
functions = analyze_melody_functions(melody_pcs, root_pc, chord["quality"])
|
||||
|
||||
return {
|
||||
"global_key": global_key,
|
||||
"chord": chord,
|
||||
"degree_in_key": deg,
|
||||
"roman": roman,
|
||||
"best_mode": {
|
||||
"family": best_mode["family"],
|
||||
"name": best_mode["name"],
|
||||
"score": best["score"],
|
||||
"melody_score": best["melody_score"],
|
||||
"missing_core": best["missing_core"],
|
||||
"missing_extra": best["missing_extra"],
|
||||
},
|
||||
"alternatives": [
|
||||
{
|
||||
"family": a["mode"]["family"],
|
||||
"name": a["mode"]["name"],
|
||||
"score": a["score"],
|
||||
"melody_score": a["melody_score"],
|
||||
"missing_core": a["missing_core"],
|
||||
"missing_extra": a["missing_extra"],
|
||||
}
|
||||
for a in mode_choice["alternatives"]
|
||||
],
|
||||
"melody_notes": [normalize_note(n) for n in melody_notes],
|
||||
"melody_degrees": melody_degrees,
|
||||
"functions": functions,
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# CLI
|
||||
# ============================================================
|
||||
|
||||
def parse_cli_line(line: str) -> Tuple[str, List[str]]:
|
||||
"""
|
||||
Expects: [chord]: note1, note2, ...
|
||||
Allows commas or whitespace between notes.
|
||||
"""
|
||||
if ":" not in line:
|
||||
raise ValueError("Input must contain ':' like D7b9b13: F#, A, C, Eb, Bb")
|
||||
|
||||
chord_part, melody_part = line.split(":", 1)
|
||||
chord = chord_part.strip()
|
||||
if not chord:
|
||||
raise ValueError("Missing chord before ':'.")
|
||||
|
||||
melody_raw = melody_part.strip()
|
||||
if not melody_raw:
|
||||
raise ValueError("Missing melody notes after ':'.")
|
||||
|
||||
if "," in melody_raw:
|
||||
tokens = [t.strip() for t in melody_raw.split(",")]
|
||||
else:
|
||||
tokens = [t.strip() for t in melody_raw.split()]
|
||||
|
||||
notes = [t for t in tokens if t]
|
||||
if not notes:
|
||||
raise ValueError("No valid note names parsed.")
|
||||
|
||||
return chord, notes
|
||||
|
||||
def main():
|
||||
global DEBUG
|
||||
|
||||
print("Interactive chord/melody analyzer (21-mode matching)")
|
||||
print(f"Global key: {GLOBAL_KEY} major")
|
||||
print(f"Debug level: {DEBUG} (0..5)")
|
||||
print("Input format: [chord]: note1, note2, note3 ...")
|
||||
print('Commands: "quit", or "debug=N" to set debug level.\n')
|
||||
|
||||
while True:
|
||||
s = input("Enter > ").strip()
|
||||
if not s:
|
||||
continue
|
||||
if s.lower() == "quit":
|
||||
print("Goodbye.")
|
||||
break
|
||||
if s.lower().startswith("debug="):
|
||||
try:
|
||||
DEBUG = int(s.split("=", 1)[1].strip())
|
||||
if DEBUG < 0 or DEBUG > 5:
|
||||
raise ValueError
|
||||
print(f"Debug set to {DEBUG}.")
|
||||
except ValueError:
|
||||
print("Error: debug must be an integer 0..5.")
|
||||
continue
|
||||
|
||||
try:
|
||||
chord, notes = parse_cli_line(s)
|
||||
result = analyze_chord_and_melody(chord, notes, GLOBAL_KEY)
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}")
|
||||
print("Please type again.")
|
||||
continue
|
||||
|
||||
print("\n====================================")
|
||||
print(f"Chord: {result['chord']['symbol']} (root {result['chord']['root']}, quality {result['chord']['quality']})")
|
||||
print(f"Global key: {result['global_key']} major")
|
||||
print(f"Roman numeral (by root in global key): {result['roman']} (degree {result['degree_in_key']})")
|
||||
|
||||
bm = result["best_mode"]
|
||||
print("\nBest mode match (rooted on chord root):")
|
||||
print(f" {bm['family']} :: {bm['name']}")
|
||||
print(f" score={bm['score']:.3f} (melody_score={bm['melody_score']:.3f})")
|
||||
if bm["missing_core"]:
|
||||
print(f" WARNING missing core chord tones (pcs): {bm['missing_core']}")
|
||||
if bm["missing_extra"]:
|
||||
print(f" missing extension/alteration tones (pcs): {bm['missing_extra']}")
|
||||
|
||||
print("\nAlternatives:")
|
||||
for a in result["alternatives"]:
|
||||
print(f" - {a['family']} :: {a['name']} score={a['score']:.3f} melody_score={a['melody_score']:.3f}"
|
||||
f" missing_core={len(a['missing_core'])} missing_extra={len(a['missing_extra'])}")
|
||||
|
||||
print("\nMelody numeral labels (relative to chord root):")
|
||||
for i, (n, deg) in enumerate(zip(result["melody_notes"], result["melody_degrees"])):
|
||||
print(f" [{i}] {n:3} -> {deg}")
|
||||
|
||||
print("\nPer-note function tags:")
|
||||
for i, labs in result["functions"].items():
|
||||
if labs:
|
||||
print(f" index {i}, note {result['melody_notes'][i]}: {', '.join(sorted(set(labs)))}")
|
||||
|
||||
print("====================================\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user