dilluns, 8 d’agost de 2011

Un retardador de subtítols SubRip amb Python

EL cap de setmana passat em vaig dedicar a buscar subtítols en anglès per a pel·lícules en anglès. Va molt bé per a l'aprenentatge i la pràctica de l'idioma el poder veure la pel·lícula en versió original i, a l'hora, poder llegir el que estan dient en el mateix idioma. I després, tornar a veure la pel·lícula sense subtítols. Són tècniques d'aprenentatge.

El cas és que vaig trobar alguns subtítols però als posar-los al VLC amb la pel·lícula corresponent resultava que no estaven sincronitzats amb la imatge. El VLC permet adelantar o retardar el temps d'aparició dels subtítols, però és poc pràctic, ja que cal sincronitzar cada cop que obres la pel·lícula. O, almenys, no vaig veure com guardar la sincronització; i amb el reproductor de DVD del menjador és pitjor perquè no tinc la  possibilitat de sincronitzar.

Per tant, vaig decidir que la solució era fer un retardador per als subtítols i així generar fitxers de subtítols per a les pel·lícules que tinc i que estiguin correctament sincronitzats amb la imatge.


En el post d'avui, doncs,programo una petita aplicació amb python (versió 2.6.5, un pel antiga, però és la que tinc instal·lada a l'Ubuntu) que servirà per retardar o avançar el moment d'aparició de subtítols en un fitxer de subtítols del tipus SubRip (extensió .srt).

La WikiPedia diu del format SubRip qu és "probablement, el més senzill dels formats de subtítols". Els fitxers SubRip amb extensió .srt, són de text pla. El format de temps emprat és "hores:minuts:segons,milisegons". El separador decimal és una coma, en comptes d'un punt perquè l'especificació original és francesa. El salt de línia és, tot sovint,CR+LF. Els subtítols es numeren en seqüència, començant per 1. Els subtítols se separen per una línia en blanc.

En resum:

Número del subtítol
temps d'inici --> temps de fi
text del subtítol (una o més línies) 
Línia en blanc.

un exemple de fixer SubRip (prova.srt)

1
00:00:20,000 --> 00:00:24,400
Altocumulus clouds occur between six thousand

2
00:00:24,600 --> 00:00:27,800
and twenty thousand feet above ground level.

 Doncs bé, amb el següent script Python podré retardar o avançar l'aparició dels subtítols.

#!/usr/bin/python
# coding: latin-1

# L'objectiu del programa és retardar o adelantar el temps d'aparició del subtítol
# el programa pren tres arguments: fitxer original, fitxer de sortida i
# temps expressat en mili segons a avançar o retardar (un negatiu, vol dir avançar)
# El fitxer de sortida serà un fitxer .srt igual a l'original, però amb tots els
# temps retardats en la quantitat de mili segons indicada

import optparse 
from datetime import datetime
from datetime import timedelta

def main():
    # analitza els arguments rebuts
    usage = "\t%prog filein.srt fileout.srt delay"
    usage = usage + "\n\texample 1: %prog filein.srt fileout.srt 10000 --> delays 10 seconds"
    usage = usage + "\n\texample 2: %prog filein.srt fileout.srt -- -10000 --> advances 10 seconds"
    parser = optparse.OptionParser(usage)
    (options, args) = parser.parse_args()

 
    # if args != 3 (fitxer d'entrada, fitxer de sortida, delay) aleshores error
    if len(args) != 3:
        parser.error("incorrect number of arguments")

    # calcula el delta
        delta =  timedelta(milliseconds=int(args[2]))

    # obre el fitxer per llegir-lo
    f1 = open(args[0])
    f2 = open(args[1], "w")
    for line in f1:
            tokens = line.split(" --> ")
            if len(tokens) == 2:
            
        # strip() --> trim spaces
           
        TempsInici = datetime.strptime(tokens[0].strip(), "%H:%M:%S,%f") + delta
           
        TempsFi = datetime.strptime(tokens[1].strip(), "%H:%M:%S,%f") + delta

           
        # multilínia amb "\"
           
        line = \
           
        str(TempsInici.hour).zfill(2) + ":" + \
           
        str(TempsInici.minute).zfill(2) + ":" + \
           
        str(TempsInici.second).zfill(2) + "," + \
           
        str(int(TempsInici.microsecond / 1000)).zfill(3) + " --> " + \
           
        str(TempsFi.hour).zfill(2) + ":" + \
           
        str(TempsFi.minute).zfill(2) + ":" + \
           
        str(TempsFi.second).zfill(2) + "," + \
           
        str(int(TempsFi.microsecond/ 1000)).zfill(3) + "\r\n"
   
        
            f2.write(line)
    

    f1.close()
    f2.close()

#main
if __name__ == "__main__":
    main()


El programa és trivial, però en faré alguns comentaris.

Us d'optparse per a fer el tractament dels arguments d'entrada.  A partir de la versió 2.7 aquest mòdul es considera obsolet i cal fer servir argparse. Ara bé, la forma com funcionen tots dos mòduls és similar. A destacar com es fa el missatge d'us, i com es tornen en un diccionari les opcions (que en aquest cas, no n'hi han) i en un array els arguments posicionals (fitxer-d'entrada, fitxer de sortida, retard).

A destacar com passar arguments negatius (en aquest cas, això vol dir avançar l'aparició dels subtítols) fent servir  "--". El exemple que acompanya l'explicació de l'ús és aclaridor:

delayer.py entrada.srt sortida.srt -- -10000

Avança l'aparició dels subtítols 10 segons

En cas de no disposar dels arguments suficient, finalitza l'execució de l'script amb parser.error

Immediatament que ha tractat els arguments d'entrada, procedeix a calcular el retard amb:

delta =  timedelta(milliseconds=int(args[2]))

A continuació obre el fitxer original per a lectura i el fitxer de sortida.

La iteració línia a línia pel fitxer original es senzilla amb la potent sintaxi del for:

for line in f1:

Per a cada línia, verifica si es pot dividir amb -->. Les línies que tinguin --> seran les de temps i les que cal tractar.

El càlcul dels nous temps es fa amb el parell de línies

TempsInici = datetime.strptime(tokens[0].strip(), "%H:%M:%S,%f") + delta
TempsFi = datetime.strptime(tokens[1].strip(), "%H:%M:%S,%f") + delta

A destacar l'us de strptime  que converteix una cadena en un objecte datetime fent servir una cadena de format. El format "%H:%M:%S,%f" correspon a  hora:minut:segon,milisegons. El mètode strip(), per la seva part, elimina espais en blanc per davant i pel darrera de la cadena, com el trim de java.

Finalment, els camps útils dels datetime TempsInici i TempsFi es reordenen i s'ajunten en una cadena de text en la següent línia

line = \
str(TempsInici.hour).zfill(2) + ":" + \
str(TempsInici.minute).zfill(2) + ":" + \
str(TempsInici.second).zfill(2) + "," + \
str(int(TempsInici.microsecond / 1000)).zfill(3) + " --> " + \
str(TempsFi.hour).zfill(2) + ":" + \
str(TempsFi.minute).zfill(2) + ":" + \
str(TempsFi.second).zfill(2) + "," + \
str(int(TempsFi.microsecond/ 1000)).zfill(3) + "\r\n"



Aquesta instrucció llarga es divideix en vàries línies fent servir \.
Els camps numèrics es converteixen a cadena amb str().
El mètode de cadena zfill(longitud) zero fill posa zeros per l'esquerra fins a que la cadena té la longitud indicada.
Els datetime no tenen un camp millisecond, però sí que el tenen microsecond. aleshores divideixo aquest camp per 1000 i el converteixo a enter amb int(). Finalment afegeixo un CR+LF

Per acabar, fora de l'if, s'escriu al fitxer de sortida la línia. Si era una línia amb --> haurà estat processada, si no, es manté com a l'original.

Es tanquen els fitxers i s'invoca el main. Aquest if __name__ == "__main__":  és un "truc" de python que permet que aquest script pugui ser utilitzat com a llibreria des d'un altre script: en aquest cas, la variable __name__ prendria un valor diferent de __main__.

Cap comentari:

Publica un comentari a l'entrada