domenica 21 marzo 2010

Festival, ma non per cantare

Chi mi segue anche sulle "Pagine Oscure" avrà notato che, da qualche giorno, in capo ad ogni articolo è comparsa la voce "Ascolta", basata sul servizio di sintesi vocale messo a disposizione dal sito ReadSpeaker.
Ora, senza nulla voler togliere a una società commerciale che offre un servizio di sintesi vocale di certo migliore di quello che potremmo fare in piccolo e con software open source... ma io ho una risposta a questo servizio.
Prima parentesi: festival è il nome di un programma di sintesi vocale molto in voga nell'ambito Linux.
E io in questi giorni mi sono chiesto "ma non si potrebbe molto semplicemente implementare un motore basato su festival per avere la sintesi vocale sul proprio sito web?".
La risposta è molto semplice: "Sì, è possibilissimo!".
Ragioniamo. Ragioniamo secondo uno schema a blocchi.
Io ho un sito web, per esempio un blog, oppure il sito del ponte radio Pappagallo Mannaro. Quello che ho, in pratica è una pagina (o una serie di pagine, ma nello specifico voglio analizzare una singola pagina alla volta) che contiene una serie di cose: qualche elemento grafico di presentazione, un'intestazione, un fondo pagina e un contenuto di base (per esempio, nel caso di un Blog, un singolo articolo).
Per esempio se ho una singola pagina di cui voglio che una parte venga letta a voce alta, mi serve che un server si preoccupi di prendere quella pagina, tagliare le parti che non servono, dare il resto in pasto a festival e fornirmi in output il flusso multimediale che contiene la lettura in TTS.
Per tradurlo in una specie di diagramma:
  1. Prendere il documento HTML che mi interessa leggere;
  2. Identificare la parte di testo che voglio sia elaborata, e tagliare le parti non necessarie;
  3. Identificare la lingua del documento, o applicare la lingua di default (normalmente l'italiano);
  4. Dare in pasto il testo a Festival;
  5. Anziché lasciare che l'output di Festival vada alla scheda sonora del server, lo salvo da qualche parte e ce l'ho a disposizione;
Ora, naturalmente l'avvento di Flash ha permesso di inserire facilmente contenuti multimediali nelle pagine web (pensate solo a Youtube, per esempio!), e se quello che io voglio aggiungere è un bel fondo musicale, esistono decine di player multimediali per pagine web basati sulla tecnologia Flash. Tutto sta a capire che anziché un sottofondo musicale, io a una pagina voglio aggiungere materialmente la voce che ne legge il testo. Ma il risultato è lo stesso! Mi basta un semplice brano mp3 che contiene il testo del sito e voilà! Il gioco è fatto.

Che significa? Che devo creare decine di brani mp3 con tutte le registrazioni delle pagine del mio sito? Che devo rifare un sacco di lavoro ogni volta che cambio una virgola sul sito? Nooo!

La potenza di un server web sta nella sua capacità di gestire automazione (scripting) e flussi di dati (stream). Ecco che cosa mi serve!

Quello che presento è un progetto, realizzato al volo usando Festival (e in particolare lo script "text2wave", lame mp3 encoder (ma i puristi dello GNU possono usare qualunque altro encoder in grado di convertire dal wave PCM a mp3), qualche sacra riga di perl e, naturalmente, un buon mp3 player in flash, come l'eccellente MP3 Player di neolao, che permette di essere personalizzato all'estremo.
Siamo pronti? Cominciamo!
Anzitutto devo dire che non sono un gran programmatore in perl, anche se riesco a cavarmela abbastanza bene con questi script da due righe; comunque bando alle ciance e mettiamoci d'impegno.

Per prima cosa devo identificare in ogni pagina qual è la parte di testo che mi interessa leggere. Per esempio posso fare in modo di intervenire sul codice sorgente della pagina: dove comincia la parte che deve essere letta, ad esempio, pongo un commento in HTML del tipo <!-- INIZIO_TTS -->, e al termine di quella parte, pongo un commento del tipo <!-- FINE_TTS -->; pulito, nella pagina non si vede nulla, è tutto nel sorgente, esempio:
file: /pagina/da/leggere.html (prima versione)
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="it">
<head>
 <meta http-equiv="content-type"
  content="application/xhtml+xml; charset=utf-8" />
 <meta name="author" content="Grizzly aka Mirko Tuccitto" />
 <meta name="Description" content="Un esempio di text-to-speech" />
 <title>Esempio di text-to-speech via Festival</title>
</head>
<body>
 <div id="intestazione">
  <h1 id="titolo">Titolo della pagina</h1>
  <h2 id="sottotitolo">Questa parte non sar&agrave; letta</h2>
 </div>
 <hr class="separatoreNV" />

 <div id="contenuto">
  <p>Questo &egrave; un insieme di paragrafi che dovranno essere letti dal
   software di sintesi vocale.</p>
  <p>La vispa Teresa avea tra l'erbetta appena afferrato la pia
   farfalletta.</p>
  <p>La veloce volpe argentata con un balzo scavalc&ograve; l'astuto
   cane di spalle.</p>
  <p>Qualche Budda mangia spesso verza fritta,</p>
  <p>Questa &egrave; l'ultima frase che sar&agrave; letta dal sistema di
   sintesi vocale, perch&eacute; dopo questo DIV c'&egrave; il commento
   di fine tts.</p>
 </div>

 <div id="fondo pagina">
  <p>Fesserie in fondo al documento, non saranno lette</p>
 </div>
</body>
</html>
A questo punto comincio con il ragionamento: devo inserire da qualche parte l'oggetto "lettore mp3", e dargli in pasto, anziché un semplice brano mp3, un programma CGI che mi lanci uno stream di Festival. Metto dei parametri standard per creare un piccolo lettore con i pulsanti Play, Stop e controllo volume.
file: /pagina/da/leggere.html (seconda versione, aggiunte in grassetto)
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="it">
<head>
 <meta http-equiv="content-type"
  content="application/xhtml+xml; charset=utf-8" />
 <meta name="author" content="Grizzly aka Mirko Tuccitto" />
 <meta name="Description" content="Un esempio di text-to-speech" />
 <title>Esempio di text-to-speech via Festival</title>
</head>
<body>
 <div id="intestazione">
  <h1 id="titolo">Titolo della pagina</h1>
  <h2 id="sottotitolo">Questa parte non sar&agrave; letta</h2>
 </div>
 <hr class="separatoreNV" />
 <div id="letturaTTS">
  <object type="application/x-shockwave-flash" data="http://flash-mp3-player.net/medias/player_mp3_maxi.swf" width="75" height="25">
    <param name="movie" value="http://flash-mp3-player.net/medias/player_mp3_maxi.swf" />
    <param name="bgcolor" value="#ffffff" />
    <param name="FlashVars"
     value="mp3=http%3A//miosito.ext/cgi-bin/nph-mp3read.pl&amp;width=75&amp;height=25&amp;showstop=1&amp;showvolume=1&amp;buttonwidth=24&amp;volumewidth=28&amp;volumeheight=14" />
  </object>
 </div>
 <!-- INIZIO_TTS -->
 <div id="contenuto">
  <p>Questo &egrave; un insieme di paragrafi che dovranno essere letti dal
   software di sintesi vocale.</p>
  <p>La vispa Teresa avea tra l'erbetta appena afferrato la pia
   farfalletta.</p>
  <p>La veloce volpe argentata con un balzo scavalc&ograve; l'astuto
   cane di spalle.</p>
  <p>Qualche Budda mangia spesso verza fritta,</p>
  <p>Questa &egrave; l'ultima frase che sar&agrave; letta dal sistema di
   sintesi vocale, perch&eacute; dopo questo DIV c'&egrave; il commento
   di fine tts.</p>
 </div>
 <!-- FINE_TTS -->

 <div id="fondo pagina">
  <p>Fesserie in fondo al documento, non saranno lette</p>
 </div>
</body>
</html>
Vi prego peraltro di notare la parte scritta in rosso: http://miosito.ext/cgi-bin/nph-mp3read.pl è il nome del file CGI che si occupa di leggere il contenuto del sito (si può utilizzare in maniera tale che si colleghi alla pagina "referer" come testo di origine) e fornirne il resoconto in formato mp3 direttamente al lettore. Vi prego inoltre di notare che, dato che l'operazione può impiegare qualche istante di troppo, ho appositamente chiamato il programma che fa questa operazione con la sigla iniziale "nph-", che per Apache significa "manda subito lo stream senza aspettare che si concluda il contenuto".
A questo punto non resta altro che analizzare il file nph-mp3read.pl, che deve essere presente sul server web del quale non solo potete operare sulla cartella cgi-bin, ma anche deve disporre del permesso, per l'utente che esegue il server web, di lanciare Festival e lame. Il codice è sporco, lo ammetto, ma ci vuole poco per ripulirlo e sistemarlo un po':
File: nph-mp3read.pl (nella cgi-bin di un server che dispone di Festival; permessi: 750 o 755);
#!/usr/bin/perl -w
##################
# Esempio di conversione da documento html a stream mp3 della
#  sintesi vocale.
# Copyright (C) 2010 by Grizzly - Please refer to GNU/GPL

# Escamotage necessario
print "content-type: audio/mpeg3\r\n\r\n";


use LWP::UserAgent;
use HTML::FormatText;
use HTML::TreeBuilder;

# Eventualmente letta dal referer http
my $url = 'http://miosito.ext/pagina/da/leggere.html';
# Eventualmente confrontato con una whitelist di referer

# Crea un browser virtuale e lo usa per caricare il documento in
#  una variabile
my $ua = LWP::UserAgent->new();
my $doc = $ua->get($url);
$documento = $doc->content;
die "Impossibile aprire il file: $!" unless defined $documento;

# Toglie dal documento le righe vuote
$documento =~ s/\n//g;
$documento =~ s/\r//g;

# Prende solo quello che serve
$documento =~ s/^.*<!-- INIZIO_TTS -->//g;
$documento =~ s/<!-- FINE_TTS -->.*$//g;

# Costruisce un albero HTML per il parser di testo
$albero = HTML::TreeBuilder->new->parse($documento);
$formatter = HTML::FormatText->new(leftmargin => 0, rightmargin => 72);

# Formatta l'albero HTML del contenuto da leggere in testo semplice
$testo = $formatter->format($albero);

# Il doppio stream in entrata e uscita da un errore in stderr, ma
#  funziona. Tuttavia si potrebbe utilizzare qualche soluzione migliore

# Apre uno stream su cui scrivere il testo, per poi trasformarlo
#  in wav e quindi MP3
open MP3CONT, "| text2wave | lame -V2 - - |";

# Da il risultante MP3 in output
print MP3CONT $testo;
while (<MP3CONT>) {
  print;
}

# Finito. Senza bisogno di scrivere files sull'hard disk! (-:
Sono tantissime le modifiche che si possono pensare in questa situazione: ad esempio lo script può essere chiamato con un token quale parametro. In esecuzione aggiunge al nome del file del referrer una password segreta e ne calcola la firma MD5, se corrisponde al token lo stream richiesto è valido, altrimenti è qualcuno che cerca di sfruttare il nostro server.
E ancora: crea un token in base alla risorsa che deve tradurre, e cerca in una cartella di servizio se esiste un file .mp3 con il nome ${token_calcolato}.mp3 che dovrebbe contenere il text-to-speech generato in passato.
Se c'e', e il file mp3 ha una data di creazione superiore a quella di modifica del documento (ammesso che documento, script e files mp3 siano sullo stesso server) allora da direttamente il file mp3 senza fare altro, altrimenti prima di tutto salva lo stream come ${token_md5}.mp3 e poi lo da in stream (un passaggio molto cervellotico, ma che consiste nel creare una "cache" dei documenti in sintesi vocale senza dover far frullare il server ogni volta che qualcuno clicka su "leggimi 'stu documento a voce alta").
Il sistema si presta a una lunghissima serie di modifiche, per esempio con determinati parametri allo script si può chiedere la lingua o la voce da utilizzare. Inoltre con un po' di lavoro certosino si può istruire il parser html=>txt per pronunciare dopo ogni link la sua destinazione, per segnalare la presenza di link con un suono qualsiasi, o alla follia fargli interpretare per una pagina che dovrebbe leggere, se esiste il foglio di stile per il media @aural e cercare di ottenere la lettura il più possibile conforme a quel foglio di stile.
Le possibilità sono infinite, e questo è semplicemente il risultato di un paio d'ore di sperimentazione sul mio server aziendale in una domenica pomeriggio d'inizio primavera (-:

0 commenti: