|
import random |
|
import os |
|
import tempfile |
|
import pretty_midi |
|
|
|
def pitch_to_name(pitch): |
|
notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] |
|
return notes[pitch % 12] |
|
|
|
def create_ytpmv(midi_file, video_files): |
|
""" |
|
Generate an advanced YTPMV: Parse MIDI for BPM, key, notes. Sync video clip lengths to beats. |
|
Randomly cut clips from videos, concatenate, trim to MIDI length. |
|
""" |
|
if not video_files: |
|
return None, "Please upload at least one video file.", "No analysis available." |
|
|
|
analysis = "No MIDI uploaded. Using default BPM: 120, total length: 30s." |
|
bpm = 120 |
|
total_length = 30 |
|
beat_duration = 60 / bpm |
|
|
|
if midi_file: |
|
try: |
|
pm = pretty_midi.PrettyMIDI(midi_file.name) |
|
bpm = pm.estimate_tempo() |
|
beat_duration = 60 / bpm |
|
total_length = pm.get_end_time() |
|
|
|
|
|
notes_freq = {i: 0 for i in range(12)} |
|
all_notes = [] |
|
for inst in pm.instruments: |
|
for note in inst.notes: |
|
pc = note.pitch % 12 |
|
notes_freq[pc] += 1 |
|
all_notes.append(note.pitch) |
|
|
|
|
|
tonic = max(notes_freq, key=notes_freq.get) |
|
tonic_name = pitch_to_name(tonic) |
|
|
|
major_profile = [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88] |
|
minor_profile = [6.33, 2.68, 3.52, 3.52, 2.54, 4.38, 3.25, 5.37, 2.86, 3.02, 2.71, 3.53] |
|
|
|
def correlate(profile, freqs): |
|
return sum(freqs.get((tonic + i) % 12, 0) * profile[i] for i in range(12)) |
|
|
|
major_corr = correlate(major_profile, notes_freq) |
|
minor_corr = correlate(minor_profile, notes_freq) |
|
key_type = 'major' if major_corr > minor_corr else 'minor' |
|
|
|
unique_notes = sorted(set(pitch_to_name(n) for n in all_notes)) |
|
analysis = f"BPM: {bpm:.2f}\nKey: {tonic_name} {key_type}\nNotes: {', '.join(unique_notes)}" |
|
except Exception as e: |
|
print(f"MIDI analysis error: {e}") |
|
analysis = f"MIDI analysis failed: {e}. Using defaults." |
|
|
|
clips = [] |
|
for video_path in video_files: |
|
try: |
|
clip = VideoFileClip(video_path.name) |
|
if clip.duration > 0: |
|
|
|
length = random.randint(1, 8) * beat_duration |
|
if length > clip.duration: |
|
length = clip.duration |
|
start = random.uniform(0, clip.duration - length) |
|
end = start + length |
|
short_clip = clip.subclip(start, end) |
|
clips.append(short_clip) |
|
except Exception as e: |
|
print(f"Error processing video {video_path.name}: {e}") |
|
continue |
|
|
|
if not clips: |
|
return None, "No valid video clips could be processed.", analysis |
|
|
|
|
|
final_clip = concatenate_videoclips(clips[:20], method="compose") |
|
if final_clip.duration > total_length: |
|
final_clip = final_clip.subclip(0, total_length) |
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp: |
|
final_clip.write_videofile(tmp.name, codec="libx264", audio_codec="aac", verbose=False, logger=None) |
|
output_path = tmp.name |
|
|
|
final_clip.close() |
|
for c in clips: |
|
c.close() |
|
|
|
|
|
|
|
status = f"Generated YTPMV ({total_length:.2f}s) from {len(clips)} beat-synced clips." |
|
return output_path, status, analysis |
|
|
|
down("- Deploy on Hugging Face Spaces with `requirements.txt` (includes gradio, moviepy, pretty_midi).") |
|
|
|
|
|
if __name__ == "__main__": |
|
demo.launch(server_name="0.0.0.0", server_port=7860, share=True) |