[Python] Traduttore automatico di sottotitoli

La mia smodata passione per le serie tv made in USA mi ha iniziato al binge watching molto prima che ne venisse coniato il termine per diventare una moda. Ricordo ancora il week end di una decina di anni fa, passato a ‘recuperare’ su internet gli episodi delle prime tre stagioni di «Battlestar Galactica» poco prima della trasmissione in America della quarta stagione. Di lì ho scoperto che non mi piaceva attendere qualche anno prima di poter guardare una serie tv che mi interessava e che internet da questo punto di vista era una notevole risorsa se si era disposti ad abituare l’occhio a non perdersi nessuna scena sbirciando contemporaneamente i sottotitoli sullo schermo (ancora tante grazie Italiansubs!).


Ora che i sottotitoli in italiano si fa più fatica a trovarli, un po’ per gioco e un po’ per utilità, ho pensato di creare uno script in Python che traduce un file di sottotitoli in inglese nel corrispettivo in italiano utilizzando la libreria googletrans (link) che sfrutta l’API di Google Translate. Il risultato non sarà dei più corretti ma IMHO abbastanza accettabile per godersi un episodio della propria serie tv preferita.


Ingredienti:

    • python 3
    • libreria googletrans (per la traduzione)
    • librerie sys, os (per la gestione dei file)
    • libreria timeit (per computare le prestazioni)

Per installare la libreria avendo a disposizione l’utility PIP è sufficiente digitare il seguente comando:

pip install googletrans

Analizziamo le parti principali del sorgente:

Innanzitutto importiamo le librerie che ci necessitano:

import sys, os
from googletrans import Translator
from timeit import default_timer as timer

Memorizziamo in una variabile il tempo iniziale che ci servirà per calcolare successivamente le prestazioni dello script:

start = timer()

E inizializziamo il traduttore sull’API pubblica di Google Translate:

translator = Translator(service_urls=[
      'translate.google.com',
      'translate.google.it',
    ])

Il progetto prevede di dare in pasto al traduttore le frasi contenute in un file in formato .srt contenente i sottotitoli in inglese, strutturalmente fatto in questo modo:

Per quanto riguarda il nome del file da tradurre lo passeremo come argomento nell’istruzione di esecuzione del programma. Il nome del file prodotto con i sottotitoli tradotti in italiano sarà generato conseguentemente aggiungendo il suffisso “_tradotto”. Viene anche controllata l’esistenza di un eventuale precedente file di traduzione avente lo stesso nome di quello in produzione e nel caso verrà cancellato.

try:
sourcefile = sys.argv[1]
resultfile = sys.argv[1].replace('.srt', '')+'_tradotto.srt'
except:
sourcefile = "sottotitolo.srt"
resultfile = sourcefile.replace('.srt', '')+'_tradotto.srt'

if os.path.isfile(resultfile) == True:
os.remove(resultfile)

Nessun problema ad aprire il file in questione e scomporre le righe di testo in elementi di una lista. Inoltre già che ci siamo cancelliamo gli eventuali inutili spazi vuoti in testa e in coda a ciascuna riga con il metodo strip():

with open(sourcefile) as f:
    content = f.readlines()
content = [x.strip() for x in content]

Ovviamente al nostro traduttore dovremo passare ‘solo’ i testi e non gli altri elementi utili alla renderizzazione a video dei sottotitoli. Inoltre al traduttore dobbiamo passare delle frasi intere, anche se nel file dovessero risultare ‘spezzate’ su più righe.

Processiamo ciascuna riga della lista creata e scartiamo quelle righe (senza eliminarle perché ci occorreranno poi per scrivere correttamente il file .srt dei sottotitoli tradotti) contenenti un numero intero (cioè il numero del sottotitolo) piuttosto che la durata del sottotitolo (che Python vede comunque come una stringa di testo da qui la trovata di pulire la riga dal segno di separazione (“:”) e di considerarne solo i primi 5 caratteri, in modo che ad esempio la riga “00:00:03,886 –> 00:00:07,961” venga computata come “00000” che Python riconosce giustamente come numero e quindi ce la fa scartare:

for item in content:
    count += 1
    if item.isdigit() == False:
        try:
            control = item.replace(":","")
            part = control[0:4].isdigit()
        except:
            part = False

Per risolvere il problema delle frasi spezzate su più righe come abbiamo ad esempio in questo caso:

Dove per ottenere una traduzione corretta occorre passare al traduttore una corretta frase interaBut you won’t fix the real problem” e non due righe separate come si presentano nel file. Si andrà quindi a tradurre quanto memorizzato in una lista ‘volatile’ solo dopo aver incontrato un salto di riga vuoto come questo:

Di seguito la mia soluzione che memorizza il testo da tradurre nella stringa “datradurre” che viene passata al traduttore istanziando l’oggetto translations da cui ricaviamo il testo tradotto quando la variabile trigger ‘accio’ processa una riga vuota. Ovviamente occorre tenere conto che sebbene al traduttore la frase di due righe verrà fornita in una riga sola, al momento della ricostruzione del sottotitolo tradotto la frase dovrà essere nuovamente divisa se troppo lunga per non perdere in leggibilità del sottotitolo: la frase sarà ‘spezzata in due righe dovesse essere composta da più di 5 parole e comunque da più di 40 caratteri (esclusi i segni ‘speciali di renderizzazione). Dopo aver passato la stringa al traduttore questa viene azzerata, pronta per le righe successive. Il tutto viene memorizzato in una lista denominata ‘traduzione’ a cui si aggiungeranno man mano tutte le righe processate, comprendenti il numero del sottotitolo, i tempi di inizio e fine visualizzazione e la traduzione ottenuta (o il testo originale se il programma non è riuscito a tradurre).

       if part != True:
            datradurre = datradurre +' '+item
            datrad_non.append(datradurre)
            accio = ("_______"+str(item)+"________________>")
            if accio == "_______________________>":
                try:
                    translations = translator.translate(datradurre, src='en', dest='it')
                    trad = translations.text
                    parole = 0
                    a = trad.split(" ")
                    for i in a:
                        if (i!=" " and i!="<i>" and i!="</" and i!="i>" and i!="-" and i!="..."):
                            parole=parole+1
                    if parole >5 and len(trad) >40:
                        frase = range(6,parole)
                        primariga = str(a[0])+' '+str(a[1])+' '+str(a[2])+' '+str(a[3])+' '+str(a[4])+' '+str(a[5])
                        secondariga = ''
                        for x in frase:
                            secondariga = secondariga+' '+str(a[x])
                        traduzione.append(primariga)
                        traduzione.append(secondariga[1:])
                        traduzione.append(item)
                        TRcount += 1
                    else:
                        traduzione.append(trad)
                        traduzione.append(item)
                        TRcount += 1
                except:
                    for x in datrad_non:
                        traduzione.append(x)
                    traduzione.append(item)
                    NOcount += 1
                datradurre = ""
                datrad_non.clear()
        else:
            traduzione.append(item)

Dopo aver processato tutte le righe si può procedere a generare scrivendo riga per riga il file .srt con i sottotitoli tradotti:

with open(resultfile, 'a') as f:
    for item in traduzione:
        f.write(item+'\n')

Possiamo calcolare a questo punto il tempo di esecuzione in secondi del programma:

end = timer()
tempo = "{0:.2f}".format((end - start))

E infine visualizzare le statistiche raccolte con i vari contatori alimentati durante il processo di traduzione:

print('\n')
print ('Totale righe _________ '+str(count))
print ('Totale traduzioni ____ '+str(TRcount))
print ('Problemi traduzioni __ '+str(NOcount))
print ('Totale tempo _________ '+tempo+'s')
print('\n')

Non sarà lo script meglio scritto, ma in un tempo ragionevole (circa 300 secondi per processare un episodio di tre quarti d’ora) riesce a sfornare dei sottotitoli in italiano discretamente accettabili.

Codice sorgente completo disponibile su GITHUB (link).

Il mio script python per scaricare automaticamente via Raspberry i sottotitoli che mi interessano da Subspedia

Da grande estimatore delle serie tv made USA mi trovo spesso a reperire le puntate dei miei telefilm preferiti via .torrent (della cosa si occupa egregiamente il mio Raspberry Pi B+ via Transmission, il tutto automaticamente grazie ai feed RSS di Karmorra che vengono macinati da Flexget), e per quanto riguarda i sottotitoli faccio riferimento alle diverse piattaforme (ItaSA, Subspedia, Subfactory, The Legion Sub e Traduttori Anonimi) che fanno tutte più che egregiamente e gratuitamente il lavoro di fornire agli appassionato dei sottotitoli di qualità.telefilm-awards

Volendo automatizzare anche la ricerca dei sottotitoli in maniera tale da ritrovarmi la mia puntata bell’è pronta mi sono armato di pazienza e ho creato uno script in python con il quale sfruttando principalmente la libreria Beautifulsoup per il parsing della home page di Subspedia, il mio Raspberry Pi va a scaricare il sottotitolo che mi occorre non appena viene messo a disposizione dalla crew di traduttori. Non contento il valente tuttofare Raspberry Pi mi comunica via Telegram dell’avvenuto ritrovamento. Lo script ‘gira’ automaticamente ogni 30 minuti ed è in grado di evitare di riprocessare le puntate già trattate.

Ecco come ho realizzato lo script…

senzanomeInnanzitutto scorrendo il codice sorgente della home page di Subspedia, mi sono accorto della facilità con cui potevo realizzare la ‘chiamata’ al download del sottotitolo della puntata di mio interesse. Ovviamente chiedo scusa al webmaster di Subspedia se la mia curiosità non era cosa gradita. Dal canto mio lo ringrazio dal profondo del cuore per come ha realizzato l’intera piattaforma che è all’altezza dell’ottimo servizio di tutti coloro che dedicano tempo e fatica per fare felici tutti noi.senzanome

Comunque procedendo nell’analisi del sito ho visto che riguardo ad ogni sottotitolo pubblicato nella home page è presente una chiamata a uno script che si occupa dell’operazione di download:

istantanea_2017-01-15_23-14-07

istantanea_2017-01-15_23-14-27

La prima operazione è identificare le puntate presenti sul mio dispositivo in maniera tale da fornire allo script ciò che stiamo cercando… In pratica visto che Transmission salva le puntate in una directory con il nome della serie mi basta creare leggere il contenuto del dispositivo di archiviazione per dare a python ciò che deve cercare.

In realtà a questo punto c’è una precisazione che devo fare: mi sono accorto che alcune serie tv vengono salvate con un nome da Transmission e indicate da Subspedia con un altro ( si tratta di piccole differenze, ma per noi sostanziali), quindi ho risolto il problema con un file esterno (che ho chiamato except) in cui elenco queste eccezioni e faccio agire lo script di conseguenza.

Ad esempio la serie che si trova nella directory “The Librarians Us” sarà cercata nel sito di Subspedia come “The Librarians”:istantanea_2017-01-15_23-29-04

opengraph-icon-200x200Per leggere adeguatamente la pagina di Subspedia mi sono affidato alla potente libreria BeautifulSoup. Si è trattato di isolare l’oggetto che ci interessa e quindi ricavare i parametri che occorrono (link, nome della serie, stagione e numero della puntata) per costruire l’url a cui il sito di Sbspedia risponde facendo partire il download dei sottotitolo che ci occorre.

Una volta scaricato il sottotitolo, provvedo a rinominare la directory che contiene la puntata (in maniera da rendere più visibile il fatto che è disponibile il sottotitolo ed evitare che lo script la rilavori successivamente), prendo nota dell’operazione effettuata in un file di log per un successivo controllo e invio un avviso via Telegram.

Ed ecco il risultato:istantanea_2017-01-15_23-40-51

senzanome

#!/usr/bin/python
'''
Programma per scarico sottotitoli da Subspedia
ver. 1.2 (13/01/17)
By Zirconet - 07-01-2017
'''

import subprocess
import time
import os, sys
import urllib
import requests
from bs4 import BeautifulSoup

url = "http://www.subspedia.tv/index.php"
r = requests.get(url)
soup = BeautifulSoup(r.content, "lxml")
g_data = soup.find_all("table", {"class": "subHomeContainer"})

path = '/mnt/usb/0tv/'
subdirectories =  os.listdir(path)

for item in subdirectories:
    seriedacercare = item
    dirname = seriedacercare

    f = open('/mnt/usb/except','r')
    xxx = [x for x in f.readlines()]  
    f.close()
    for x in xxx:
     eccept = x.split(";")
     if eccept[0] == seriedacercare:
        seriedacercare = eccept[1]
   
    print seriedacercare+" --> X" 
    aList = []

    for item in g_data:
        links = item.contents[1].find_all("a", text=seriedacercare)
	
        for link in links:
                xxx = item.contents[3].find_all("a", href=True)
                for link in xxx:
                        trovo = link["href"]
                        aList.append(trovo);
	        
                str0 = aList [0]
                str1 = str0.replace('javascript:downloadSub("', '')
                str2 = str1.replace(');','')
                str = str2.replace('"','')
                data = str.split(',')

		filename = data[0].split('/')
                nomefile = path+dirname+'/'+filename[1]
		print nomefile
                
                sito = "http://www.subspedia.tv/scaricaSub.php?path="
                url = sito+data[0]+'&serie='+data[1]+'&stagione='+data[2]+'&numero='+data[3]

                myfile = urllib.URLopener()
                myfile.retrieve(url, nomefile)
		
		os.rename((path+dirname),(path+"0subbed "+seriedacercare))

		t = time.localtime()
                timestamp = time.strftime('%d-%b-%Y %H:%M', t)
                log = timestamp+' --> '+seriedacercare+'\n'
                out_file=open("/var/log/mySub.log", "a")
                out_file.write(log)
                out_file.close()  

		messaggio = "Trovato nuovo sottotitolo per "+seriedacercare
		subprocess.Popen(["/mnt/usb/telegram.sh", messaggio])