Spaces:
Runtime error
Runtime error
| import re | |
| def align_chords_and_lyrics(lyrics_lines: list, chords: list) -> str: | |
| """ | |
| Aligns detected chords with lyrics lines using word-level timestamps. | |
| Returns: A complete ChordPro string with inline chords (e.g., [C]Hosanna [G]in [Am]the [F]highest). | |
| """ | |
| if not lyrics_lines: | |
| return "" | |
| # If no chords were detected, just return the lyrics | |
| if not chords: | |
| return "\n".join([line["text"] for line in lyrics_lines]) | |
| chordpro_lines = [] | |
| chord_idx = 0 | |
| num_chords = len(chords) | |
| # We want to keep track of chords that have already been placed | |
| # to avoid placing duplicate chords or skipping chords | |
| last_placed_chord_time = -1 | |
| for line_idx, line in enumerate(lyrics_lines): | |
| line_text = line["text"] | |
| words = line.get("words", []) | |
| # If the line is empty or has no words, just keep it as is | |
| if not words: | |
| chordpro_lines.append(line_text) | |
| continue | |
| line_start = line["start"] | |
| line_end = line["end"] | |
| # 1. Check if there are any chords that occurred BEFORE this line started, | |
| # but AFTER the previous line ended. These represent intro/bridge chords. | |
| intro_chords = [] | |
| while chord_idx < num_chords and chords[chord_idx]["time"] < line_start: | |
| c_info = chords[chord_idx] | |
| # Avoid duplicate placements | |
| if c_info["time"] > last_placed_chord_time and c_info["chord"] != "N": | |
| intro_chords.append(f"[{c_info['chord']}]") | |
| last_placed_chord_time = c_info["time"] | |
| chord_idx += 1 | |
| if intro_chords: | |
| # Place intro/instrumental chords on their own line | |
| chordpro_lines.append(" ".join(intro_chords)) | |
| # 2. Align chords with words inside the current line | |
| line_buffer = "" | |
| for w_idx, word_info in enumerate(words): | |
| word = word_info["word"] | |
| w_start = word_info["start"] | |
| w_end = word_info["end"] | |
| # Find all chords that fall in this word's timeframe | |
| # Or just before this word (but after the previous word) | |
| word_chords = [] | |
| # Lookahead for chords within this word's window | |
| temp_idx = chord_idx | |
| while temp_idx < num_chords: | |
| c_info = chords[temp_idx] | |
| c_time = c_info["time"] | |
| # If chord falls before the end of this word | |
| if c_time <= w_end: | |
| # And it occurred after the previous word ended (or since last placed) | |
| if c_time > last_placed_chord_time: | |
| if c_info["chord"] != "N": | |
| word_chords.append(c_info["chord"]) | |
| last_placed_chord_time = c_time | |
| temp_idx += 1 | |
| else: | |
| break | |
| # Update the main chord index pointer | |
| chord_idx = max(chord_idx, temp_idx) | |
| # Construct the word with its inline chords | |
| if word_chords: | |
| # Add the chords at the front of the word | |
| chord_prefix = "".join([f"[{c}]" for c in word_chords]) | |
| line_buffer += f"{chord_prefix}{word} " | |
| else: | |
| line_buffer += f"{word} " | |
| chordpro_lines.append(line_buffer.strip()) | |
| # 3. Add any remaining chords that occur after the last line of lyrics | |
| outro_chords = [] | |
| while chord_idx < num_chords: | |
| c_info = chords[chord_idx] | |
| if c_info["time"] > last_placed_chord_time and c_info["chord"] != "N": | |
| outro_chords.append(f"[{c_info['chord']}]") | |
| last_placed_chord_time = c_info["time"] | |
| chord_idx += 1 | |
| if outro_chords: | |
| chordpro_lines.append("") | |
| chordpro_lines.append(" ".join(outro_chords)) | |
| # Clean up multiple spaces and return the joined lines | |
| final_output = "\n".join(chordpro_lines) | |
| return final_output | |
| if __name__ == "__main__": | |
| # Test alignment logic | |
| mock_lyrics = [ | |
| { | |
| "text": "Hosanna, Hosanna", | |
| "start": 1.0, | |
| "end": 3.0, | |
| "words": [ | |
| {"word": "Hosanna,", "start": 1.0, "end": 1.9}, | |
| {"word": "Hosanna", "start": 2.0, "end": 3.0} | |
| ] | |
| }, | |
| { | |
| "text": "Hosanna in the Highest", | |
| "start": 3.5, | |
| "end": 6.0, | |
| "words": [ | |
| {"word": "Hosanna", "start": 3.5, "end": 4.2}, | |
| {"word": "in", "start": 4.3, "end": 4.6}, | |
| {"word": "the", "start": 4.7, "end": 5.0}, | |
| {"word": "Highest", "start": 5.1, "end": 6.0} | |
| ] | |
| } | |
| ] | |
| mock_chords = [ | |
| {"chord": "E/G#", "time": 0.8}, | |
| {"chord": "A", "time": 1.5}, | |
| {"chord": "B", "time": 2.1}, | |
| {"chord": "C#m", "time": 2.8}, | |
| {"chord": "A", "time": 3.6}, | |
| {"chord": "C#m", "time": 4.5}, | |
| {"chord": "B", "time": 5.3} | |
| ] | |
| print(align_chords_and_lyrics(mock_lyrics, mock_chords)) | |