Further enhance and add in more advanced stuff for accuracy with depths, and also make it convert into real-notes with sheets and turned into a pdf file.

```
import pretty_midi
from music21 import stream, note, tempo, meter, key, environment, dynamics, articulations
import argparse
import os
import sys

def get_dynamic(velocity):
    """Map MIDI velocity to dynamic markings."""
    if velocity >= 100:
        return "f"  # forte
    elif velocity >= 80:
        return "mf"  # mezzo-forte
    elif velocity >= 60:
        return "mp"  # mezzo-piano
    elif velocity >= 40:
        return "p"  # piano
    elif velocity >= 20:
        return "pp"  # pianissimo
    else:
        return "ppp"  # very soft

def get_articulation(attack_mean):
    """Determine articulation based on note attack properties."""
    # Placeholder for articulation logic; this can be expanded
    if attack_mean > 0.7:
        return "staccato"
    elif attack_mean < 0.3:
        return "legato"
    else:
        return ""

def midi_to_text_piano_sheet_enhanced(midi_path, output_path, note_detail=True):
    """
    Converts a MIDI file with piano tracks into an enhanced text-based piano sheet.

    Parameters:
    - midi_path: Path to the input MIDI file.
    - output_path: Path to save the output text sheet.
    - note_detail: Whether to include detailed note information (dynamics, articulations).
    """
    try:
        # Load MIDI file
        midi_data = pretty_midi.PrettyMIDI(midi_path)
    except Exception as e:
        print(f"Error loading MIDI file: {e}")
        return

    # Initialize music21 stream
    piano_stream = stream.Stream()

    # Set tempo
    tempos, tempo_times = midi_data.get_tempo_changes()
    if len(tempos) > 0:
        try:
            bpm = tempos[0]
            piano_stream.append(tempo.MetronomeMark(number=bpm))
        except:
            piano_stream.append(tempo.MetronomeMark(number=120))  # Default BPM
    else:
        piano_stream.append(tempo.MetronomeMark(number=120))  # Default BPM

    # Set key signature
    try:
        key_signature = midi_data.key_signature_changes[0]
        # pretty_midi does not provide direct key signature names; using C major as default
        # Advanced implementation would require key estimation
        piano_stream.append(key.KeySignature(0))
    except IndexError:
        piano_stream.append(key.KeySignature(0))  # Default to C major

    # Set time signature
    try:
        ts = midi_data.time_signature_changes[0]
        time_signature = meter.TimeSignature(f"{ts.numerator}/{ts.denominator}")
        piano_stream.append(time_signature)
    except IndexError:
        piano_stream.append(meter.TimeSignature("4/4"))  # Default time signature

    # Extract piano instruments (General MIDI program numbers 0-7 for piano)
    piano_instruments = [inst for inst in midi_data.instruments if not inst.is_drum and 0 <= inst.program <= 7]

    if not piano_instruments:
        print("No piano tracks found in the MIDI file.")
        return

    # Separate into left and right hands based on pitch ranges
    left_hand_notes = []
    right_hand_notes = []

    for inst in piano_instruments:
        for pm_note in inst.notes:
            if pm_note.pitch < 60:  # Middle C is 60 in MIDI
                left_hand_notes.append(pm_note)
            else:
                right_hand_notes.append(pm_note)

    # Sort notes by start time
    left_hand_notes.sort(key=lambda x: x.start)
    right_hand_notes.sort(key=lambda x: x.start)

    # Create separate streams for left and right hands
    left_stream = stream.Part(id='Left Hand')
    right_stream = stream.Part(id='Right Hand')

    # Function to convert PrettyMIDI notes to music21 notes with details
    def convert_notes(pm_notes, part_stream):
        for pm_note in pm_notes:
            m21_note = note.Note(pm_note.pitch)
            m21_note.quarterLength = pm_note.end - pm_note.start
            m21_note.offset = pm_note.start

            if note_detail:
                # Dynamics
                dyn = get_dynamic(pm_note.velocity)
                dyn_obj = dynamics.Dynamic(dyn)
                m21_note.expressions.append(dyn_obj)

                # Articulations (simplified)
                articulation = ""  # Placeholder for future articulation logic
                art_obj = articulations.Staccato() if dyn == "ppp" else None
                if art_obj:
                    m21_note.articulations.append(art_obj)

            part_stream.append(m21_note)

    convert_notes(left_hand_notes, left_stream)
    convert_notes(right_hand_notes, right_stream)

    # Add parts to the main stream
    piano_stream.insert(0, left_stream)
    piano_stream.insert(0, right_stream)

    # Add measures
    piano_stream.makeMeasures()

    # Prepare environment
    env = environment.Environment()
    env['musicxmlPath'] = ''
    env['musescoreDirectPNGPath'] = ''

    # Get text representation
    try:
        text_representation = piano_stream.show('text')
    except Exception as e:
        print(f"Error generating text representation: {e}")
        return

    # Save to file
    try:
        with open(output_path, 'w') as f:
            f.write(text_representation)
        print(f"Enhanced text piano sheet saved to {output_path}")
    except Exception as e:
        print(f"Error writing to file: {e}")

def main():
    parser = argparse.ArgumentParser(description="Convert MIDI files with piano tracks into enhanced text-based piano sheets.")
    parser.add_argument('midi_file', type=str, help='Path to the input MIDI file.')
    parser.add_argument('-o', '--output', type=str, default='enhanced_piano_sheet.txt', help='Output text file for the piano sheet.')
    parser.add_argument('--no-details', action='store_true', help='Exclude note details like dynamics and articulations.')

    args = parser.parse_args()

    if not os.path.isfile(args.midi_file):
        print(f"Error: The file {args.midi_file} does not exist.")
        sys.exit(1)

    midi_to_text_piano_sheet_enhanced(args.midi_file, args.output, note_detail=not args.no_details)

if __name__ == "__main__":
    main()