MD2Video: Genera video da post Hugo in .MD (con qualche compromesso)

App md2video
MD2Video App
Matteo 5 min

Vi è mai capitato di guardare un post del blog e pensare “sarebbe bello poterlo ascoltare mentre faccio altro”? Beh, è esattamente quello che mi è passato per la testa qualche giorno fa. E come ogni sviluppatore che si rispetti, invece di cercare una soluzione esistente, ho pensato “hey, potrei scriverlo io!”. Perché sì, a volte reinventare la ruota può essere divertente (e istruttivo).

L’Architettura del Progetto

Il progetto è strutturato seguendo il principio della separazione delle responsabilità, con tre componenti principali che gestiscono rispettivamente l’input dei post, la generazione degli script e la creazione dei video.

Pipeline di Elaborazione

Post Markdown ->   Script XML    -> Video MP4
     ↓                  ↓               ↓
BlogProcessor -> ScriptProcessor -> VideoProcessor

Ogni step della pipeline è indipendente e può essere testato e modificato separatamente. Questo approccio modulare permette di:

  • Sostituire facilmente i componenti
  • Aggiungere nuove funzionalità senza modificare il codice esistente
  • Testare ogni componente in isolamento

Il Cuore del Sistema: I Processor

BlogProcessor

Si occupa di leggere i file markdown e estrarre:

  • Metadati (frontmatter)
  • Struttura del contenuto (headings, paragrafi)
  • Elementi speciali (liste, citazioni)
def _parse_content(self, content: str) -> List[Dict]:
    """Parse il contenuto markdown in sezioni strutturate"""
    sections = []
    current_section = {"level": 0, "title": "", "content": []}

    for line in content.split('\n'):
        # Matching dei titoli
        heading_match = re.match(r'^(#{1,6})\s+(.+)$', line)
        
        if heading_match:
            if current_section["content"]:
                sections.append(current_section)
            
            level = len(heading_match.group(1))
            title = heading_match.group(2).strip()
            current_section = {
                "level": level,
                "title": title,
                "content": []
            }

ScriptProcessor

Genera uno script XML intermedio che definisce:

  • La struttura del video
  • Il testo da narrare
  • Le transizioni e gli effetti
<?xml version="1.0" encoding="UTF-8"?>
<script version="1.0">
    <metadata>
        <title>Titolo Post</title>
        <date>2024-11-25</date>
    </metadata>
    <content>
        <section level="1" type="intro" animation="fade">
            <heading>Introduzione</heading>
            <speech pause="0.5">Testo da narrare</speech>
        </section>
    </content>
</script>

In realtà questo passaggio non è necesario. E’ possibile infatti generare un file XML manualmente nella dir degli script.

Inoltre è possibile aggiungere degli sfondi custom al posto di quelli default generati dallo script, tramite il parametro background di section:

<?xml version="1.0" encoding="UTF-8"?>
<script version="1.0">
    <metadata>
        <title>Titolo Post 2</title>
        <date>2024-11-25</date>
    </metadata>
    <content>
        <section level="1" type="intro" background="intro_bg.png" animation="zoom">
            <heading>Introduzione</heading>
            <speech pause="0.5">Testo da narrare</speech>
            <speech pause="0.7">Ulteriore testo da narrare</speech>
        </section>
    </content>
</script>

VideoProcessor

Il componente più complesso, che si occupa di:

  1. Generare le slide con PIL
  2. Creare l’audio con gTTS
  3. Applicare effetti e transizioni
  4. Assemblare il video finale con MoviePy
def _create_slide(self, text: str, output_path: Path):
    """Crea una slide con testo"""
    image = Image.new('RGB', 
                     (self.config.VIDEO_WIDTH, 
                      self.config.VIDEO_HEIGHT))
    draw = ImageDraw.Draw(image)
    
    # Configurazione font
    font = ImageFont.truetype(self.config.FONT_PATH, 
                             self.config.FONT_SIZE)
    
    # Layout del testo
    lines = self._wrap_text(text, font, 
                           self.config.MAX_WIDTH)
    y_position = self._calculate_vertical_position(lines)
    
    # Rendering del testo
    for line in lines:
        x = self._center_text(line, font)
        draw.text((x, y_position), line, 
                 font=font, 
                 fill=self.config.TEXT_COLOR)
        y_position += self.config.LINE_HEIGHT

Sfide Tecniche e Soluzioni

1. Sincronizzazione Audio-Video

Una delle sfide principali è stata sincronizzare perfettamente l’audio con le slide. La soluzione? Utilizzare i metadati dell’audio generato:

def _sync_audio_video(self, audio_clip, video_clip):
    """Sincronizza audio e video considerando le pause"""
    audio_duration = audio_clip.duration
    video_duration = audio_duration + self.config.PAUSE_DURATION
    
    # Estendi il video per coprire l'audio più la pausa
    extended_video = video_clip.set_duration(video_duration)
    
    # Applica l'audio
    final_clip = extended_video.set_audio(audio_clip)
    
    return final_clip

2. Gestione della Memoria

La generazione di video può essere memory-intensive. Ho implementato un sistema di pulizia progressiva:

def cleanup(self):
    """Pulisce i file temporanei durante l'elaborazione"""
    temp_dir = Path(self.config.TEMP_DIR)
    if temp_dir.exists():
        for file in temp_dir.glob('*'):
            try:
                file.unlink()
            except Exception as e:
                self.logger.error(f"Errore pulizia: {str(e)}")

3. Text-to-Speech: Il Punto Dolente

Attualmente utilizzo gTTS per la sintesi vocale. Non è la soluzione ideale per diversi motivi:

  • Qualità audio non eccezionale
  • Necessità di connessione internet
  • Limiti di utilizzo
  • Poca naturalezza nella prosodia

Ho valutato alternative come:

  • Amazon Polly (costoso per uso personale)
  • Mozilla TTS (complesso da configurare)
  • Coqui TTS (promettente ma ancora acerbo)

Per ora gTTS rimane la scelta più pratica per un progetto open source, ma sono aperto a suggerimenti per alternative migliori.

Testing e Qualità del Codice

Ho implementato una suite di test completa utilizzando pytest:

@pytest.fixture
def video_processor(tmp_path):
    """Crea un VideoProcessor configurato per i test"""
    processor = VideoProcessor()
    processor.config.TEMP_DIR = tmp_path / "temp"
    processor.config.OUTPUT_DIR = tmp_path / "output"
    return processor

def test_video_generation(video_processor):
    """Verifica la generazione completa del video"""
    script_path = "test_script.xml"
    output_file = video_processor.process(script_path)
    
    assert Path(output_file).exists()
    assert Path(output_file).stat().st_size > 0

Work in Progress

Il progetto è ancora in fase di sviluppo attivo e ci sono diverse aree di miglioramento:

  1. Performance

    • Parallelizzazione della generazione delle slide
    • Caching dei font e degli elementi grafici comuni
    • Ottimizzazione della pipeline video
  2. Features Pianificate

    • Supporto per temi personalizzabili
    • Più effetti di transizione
    • Miglior gestione del layout per codice e tabelle
  3. Documentazione

    • Guida completa all’installazione e all’utilizzo
    • API reference
    • Tutorial per l’estensione

Contribuire al Progetto

Il codice è disponibile su GitHub e sono ben accetti contributi di ogni tipo:

  • Bug fix
  • Nuove feature
  • Miglioramenti alla documentazione
  • Suggerimenti per alternative a gTTS
  • Report di bug e feedback

Per contribuire:

  1. Forka il repository
  2. Crea un branch per la tua feature
  3. Implementa le modifiche
  4. Aggiungi o aggiorna i test
  5. Invia una pull request

Conclusioni

MD2Video è nato come esperimento personale ma ha il potenziale per diventare uno strumento utile per chi vuole rendere i propri contenuti più accessibili. Nonostante le limitazioni attuali (soprattutto legate al text-to-speech), il progetto fornisce una base solida per generare video da contenuti markdown.

Se sei interessato a contribuire o hai suggerimenti, special modo riguardo alternative a gTTS, ogni contributo è benvenuto!

content_copy Copiato