Spaces:
Sleeping
Sleeping
File size: 7,269 Bytes
6870c62 1ba66ff 6870c62 1ba66ff 6870c62 1ba66ff |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# app.py
import gradio as gr
from typing import List, Tuple
import re
# ----- Pitch utilities -----
SHARP_NAMES = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]
FLAT_NAMES = ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"]
NAME_TO_PC = {
# naturals
"C":0, "D":2, "E":4, "F":5, "G":7, "A":9, "B":11,
# sharps
"C#":1, "D#":3, "F#":6, "G#":8, "A#":10,
# flats
"Db":1, "Eb":3, "Gb":6, "Ab":8, "Bb":10,
# unicode ♯/♭
"C♯":1, "D♯":3, "F♯":6, "G♯":8, "A♯":10,
"D♭":1, "E♭":3, "G♭":6, "A♭":8, "B♭":10,
}
# Common chord templates: set of intervals from root (0 always present)
CHORD_TEMPLATES = {
(0,4,7): ("major triad", ""),
(0,3,7): ("minor triad", "m"),
(0,3,6): ("diminished triad", "dim"),
(0,4,8): ("augmented triad", "+"),
(0,2,7): ("sus2", "sus2"),
(0,5,7): ("sus4", "sus4"),
(0,4,7,10): ("dominant 7th", "7"),
(0,4,7,11): ("major 7th", "maj7"),
(0,3,7,10): ("minor 7th", "m7"),
(0,3,6,10): ("half-diminished (m7♭5)", "m7b5"),
(0,3,6,9): ("diminished 7th", "dim7"),
(0,3,7,11): ("minor major 7th", "m(maj7)"),
(0,4,7,9): ("major 6th", "6"),
(0,3,7,9): ("minor 6th", "m6"),
}
ADD_TONES = {
2: ("add9", "add9"), # treat 2 as add9
9: ("add9", "add9"),
11: ("add11", "add11"),
6: ("add13", "add13"),
}
SUS_OVERLAPS = {(0,2,7), (0,5,7)}
NOTE_TOKEN_RE = re.compile(r"[A-Ga-g](?:#|b|♯|♭)?")
def pc_name(pc: int, prefer_flats: bool) -> str:
return (FLAT_NAMES if prefer_flats else SHARP_NAMES)[pc % 12]
def parse_notes(user_text: str) -> Tuple[List[int], bool, List[str]]:
"""Return (pitch_classes, prefer_flats, original_tokens)."""
tokens = NOTE_TOKEN_RE.findall(user_text)
tokens = [t.upper().replace("♯", "#").replace("♭", "b") for t in tokens]
prefer_flats = any("B" in t and len(t)>1 for t in tokens) or any("b" in t for t in tokens)
pcs = []
for t in tokens:
if t in NAME_TO_PC:
pcs.append(NAME_TO_PC[t])
# dedupe while preserving order
seen = set()
pcs_unique = []
for p in pcs:
if p not in seen:
pcs_unique.append(p)
seen.add(p)
return pcs_unique, prefer_flats, tokens
def intervals_from_root(pcs: List[int], root: int) -> Tuple[int,...]:
ints = sorted(((p - root) % 12) for p in pcs)
if 0 not in ints:
ints = (0,) + tuple(i for i in ints)
return tuple(sorted(set(ints)))
def describe_chord(pcs: List[int], prefer_flats: bool) -> str:
if len(pcs) < 3:
return "Please provide at least 3 distinct note names (e.g., C E G or C, Eb, G)."
matches = []
for root in pcs: # try each included pitch as potential root
base_ints = intervals_from_root(pcs, root)
# Try exact template match first (triads/sevenths/6ths/etc.)
if base_ints in CHORD_TEMPLATES:
qual_name, suffix = CHORD_TEMPLATES[base_ints]
matches.append((root, qual_name, suffix, []))
continue
# Try triad with added tones (add9/add11/add13)
# Identify a triad subset and extra tones
for triad in [(0,4,7), (0,3,7), (0,3,6), (0,4,8)]:
triad_set = set(triad)
if triad_set.issubset(set(base_ints)):
extras = sorted(set(base_ints) - triad_set)
add_suffixes = []
for e in extras:
if e in ADD_TONES:
add_suffixes.append(ADD_TONES[e][1])
if add_suffixes:
qual_name, suffix = CHORD_TEMPLATES.get(triad, ("triad",""))
matches.append((root, f"{qual_name} with {'/'.join(add_suffixes)}", suffix + ("" if not add_suffixes else ("("+" ".join(add_suffixes)+")")), extras))
# sus chords (if match) possibly with added tones
for sus in [(0,2,7), (0,5,7)]:
if set(sus).issubset(set(base_ints)):
extras = sorted(set(base_ints) - set(sus))
add_suffixes = []
for e in extras:
if e in ADD_TONES:
add_suffixes.append(ADD_TONES[e][1])
qual_name, suffix = CHORD_TEMPLATES[sus]
matches.append((root, qual_name + (" with "+"/".join(add_suffixes) if add_suffixes else ""), suffix + ("" if not add_suffixes else ("("+" ".join(add_suffixes)+")")), extras))
if not matches:
# fallback: show interval set relative to lowest note as a hint
root_guess = min(pcs)
ints = intervals_from_root(pcs, root_guess)
return (
"I couldn't confidently name that chord. Interval set from {}: {}.\n"
"Try removing extensions/duplicates or check note spelling (sharps vs flats)."
).format(pc_name(root_guess, prefer_flats), ",".join(str(i) for i in ints))
# Rank matches: prefer templates with no ambiguous extras, prefer 7th/6th over triad with adds, then by root being lowest provided note
def rank(m):
root, qual_name, suffix, extras = m
score = 0
if "7" in suffix or "6" in suffix:
score += 2
if not extras:
score += 1
if root == min(pcs):
score += 0.5
return -score
matches.sort(key=rank)
# Build a readable response listing top 1-3 candidates
top = matches[:3]
lines = []
for i,(root, qual_name, suffix, extras) in enumerate(top, start=1):
name = pc_name(root, prefer_flats)
symbol = name + (suffix if suffix else "")
spelled = ", ".join(pc_name(p, prefer_flats) for p in sorted(set(pcs)))
lines.append(f"{i}. {symbol} — {qual_name} (notes: {spelled})")
# Inversion hint (best-effort): if lowest note isn't the chosen root, suggest slash chord
chosen_root = top[0][0]
lowest = min(pcs)
if lowest != chosen_root:
lines[0] += f" — likely {pc_name(chosen_root, prefer_flats)}/{pc_name(lowest, prefer_flats)}"
return "\n".join(lines)
def answer(message: str, history: List[Tuple[str,str]]):
pcs, prefer_flats, tokens = parse_notes(message)
if not tokens:
return (
"Tell me 3+ notes (e.g., 'C E G' or 'Db, F, Ab, C'). "
"I support #/b and unicode ♯/♭."
)
return describe_chord(pcs, prefer_flats)
def example_inputs():
return [
"C E G",
"D F# A C",
"C Eb G Bb",
"F A C D",
"G Bb D F",
"Db F Ab C",
"C D G",
"A C E G#",
"E G# B D",
"C Eb Gb A"
]
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# 🎵 Note → Chord Chatbot
Type 3 or more note names and I'll guess the chord (triads, 7ths, 6ths, sus, and common add tones).
- Accepted formats: `C E G`, `Db, F, Ab, C`, `G-B-D-F`, etc.
- Sharps/flats: `#`, `b`, or unicode `♯` `♭`.
""")
chat = gr.ChatInterface(
fn=answer,
examples=[[e] for e in example_inputs()],
title="Chord Identifier",
retry_btn=None,
undo_btn="Delete last turn",
clear_btn="Clear",
textbox=gr.Textbox(placeholder="e.g., C E G or Db, F, Ab", label="Your notes"),
)
if __name__ == "__main__":
demo.launch()
|