Visualize
main.py
# ------------------- Standard tuning ------------------- STD_TUNING = { 'E': 40, 'A': 45, 'D': 50, 'G': 55, 'B': 59, 'e': 64 } NST_TUNING = { 6: 36, 5: 43, 4: 50, 3: 57, 2: 64, 1: 67 } STRINGS = ['E', 'A', 'D', 'G', 'B', 'e'] NST_TO_STD = { 6: 'E', 5: 'A', 4: 'D', 3: 'G', 2: 'B', 1: 'e' } # ------------------- pitch ------------------- def pitch_from_std(string, fret): return STD_TUNING[string] + fret def nst_candidates(pitch): out = [] for s in range(1, 7): fret = pitch - NST_TUNING[s] if 0 <= fret <= 24: out.append((s, fret)) return out # ------------------- parsing ------------------- def parse_tab(tab): lines = [l for l in tab.split("\n") if "|" in l] parsed = {} for l in lines: parsed[l[0]] = l.split("|")[1] width = max(len(v) for v in parsed.values()) columns = [] i = 0 while i < width: notes = [] for s in STRINGS: line = parsed[s] if i >= len(line): continue ch = line[i] if ch.isdigit(): num = ch j = i + 1 while j < len(line) and line[j].isdigit(): num += line[j] j += 1 notes.append((s, int(num))) if notes: columns.append(notes) i += 1 return columns # ------------------- position clustering ------------------- def get_position(state): if not state: return 0 return sum(f for _, f in state) / len(state) # ------------------- DP optimizer ------------------- def optimize(columns): from itertools import product dp = [{} for _ in range(len(columns) + 1)] dp[0][()] = (0, None) for i, col in enumerate(columns): for state, (cost, _) in dp[i].items(): options = [] valid = True for (s_std, fret_std) in col: pitch = pitch_from_std(s_std, fret_std) cand = nst_candidates(pitch) if not cand: valid = False break options.append(cand) if not valid: continue prev_pos = get_position(state) for combo in product(*options): new_cost = cost for j, (s, f) in enumerate(combo): new_cost += f * 0.1 if state: prev = state[min(j, len(state) - 1)] new_cost += abs(prev[1] - f) * 0.2 new_cost += abs(prev[0] - s) * 0.3 combo_pos = sum(f for _, f in combo) / len(combo) new_cost += abs(combo_pos - prev_pos) * 1 combo_state = tuple(combo) if combo_state not in dp[i + 1] or new_cost < dp[i + 1][combo_state][0]: dp[i + 1][combo_state] = (new_cost, state) dp[i + 1] = dict(sorted(dp[i + 1].items(), key=lambda x: x[1][0])[:300]) if not dp[-1]: return [] best = min(dp[-1], key=lambda s: dp[-1][s][0]) result = [] cur = best for i in range(len(columns), 0, -1): result.append(cur) cur = dp[i][cur][1] result.reverse() return result # ------------------- rendering ------------------- def render_solution(columns, solution): if not solution: return "NO SOLUTION FOUND" COL_WIDTH = 4 grid = {s: ['-' * COL_WIDTH for _ in range(len(solution))] for s in STRINGS} for i, state in enumerate(solution): for (nst_string, fret) in state: s = NST_TO_STD[nst_string] grid[s][i] = str(fret).center(COL_WIDTH, '-') display_order = ['e', 'B', 'G', 'D', 'A', 'E'] return "\n".join(f"{s}|{''.join(grid[s])}|" for s in display_order) # ------------------- pipeline ------------------- def convert(tab): cols = parse_tab(tab) sol = optimize(cols) return render_solution(cols, sol) # ------------------- INPUT HANDLING (FIXED) ------------------- if __name__ == "__main__": print("Paste 6-string tab (press Enter twice to finish):\n") lines = [] while True: try: line = input() # allow blank line to terminate if line.strip() == "": if lines: break else: continue lines.append(line) except EOFError: break print("\n--- OUTPUT ---\n") print(convert("\n".join(lines)))
Output