dimecres, 10 d’agost del 2011

Retardador de subtítols SubRip (again bis). Groovy version.

El darrer post va ser la versió BeanShell del retardador [1] [2]. No he pogut resistir-me a fer  la versió amb Groovy.


import java.text.DateFormat
import java.text.SimpleDateFormat

// els arguments estan a la variable args

// verifica que té 3 arguments
if (args.length != 3 ) {
    sUsage = "Usage:\tdelayer filein.srt fileout.srt delay" +
             "\n\texample 1: delayer filein.srt fileout.srt 10000 --> delays 10 seconds" +
             "\n\texample 2: delayer filein.srt fileout.srt -10000 --> advances 10 seconds"

    print "incorrect number of arguments"
    print(sUsage)
    System.exit(1)
}

// l'anterior també es podria fer amb CliBuilder

// calcula el delta
lDelta = Long.parseLong(args[2])

// prepara el SimpleDateFormat
sdf = new SimpleDateFormat("HH:mm:ss,SSS")

// obre els fitxers
fFileIn = new File(args[0])
fFileOut = new File(args[1])

// buffer
sFileOut = ""

fFileIn.eachLine { sLine -> 
                sTokens = sLine.split("-->")
                if (sTokens.length == 2) {
                    // si té dos elements, és la línia de temps
                              
                    // els "pela"
                    sTempsInici = sTokens[0].trim()
                    sTempsFi = sTokens[1].trim()

                    // els converteix a Date
                    dTempsInici = sdf.parse(sTempsInici)
                    dTempsFi = sdf.parse(sTempsFi)
                              
                    // Els suma el retard
                    dTempsIniciDelayed = new Date(dTempsInici.getTime() + lDelta)
                    dTempsFiDelayed = new Date(dTempsFi.getTime() + lDelta)

                    // converteix els Date a String amb el mateix SimpleDateFormat
                    sTempsIniciDelayed = sdf.format(dTempsIniciDelayed)
                    sTempsFiDelayed = sdf.format(dTempsFiDelayed)

                    // Munta la línia
                    sLine = "${sTempsIniciDelayed} --> ${sTempsFiDelayed}"
    }
               
    sFileOut += "${sLine}\r\n"
}
// escriu el buffer
fFileOut.write(sFileOut)

// no hi ha close . L'objecte File no en té. ni li cal.

// acaba aquí


La versió en Groovy té un aspecte sensiblement diferent de la versió BeanShell. Ara bé, cal dir que per a escriure el codi Groovy he parti del codi BeanShell, que, excepte per la presa d'arguments l'intèrpret Groovy l'ha agafat sense cap més queixa. Diguem que amb el BeanShell ja tenia el 99% del Groovy fet. Per la seva banda, el BeanShell era, pràcticament, Java.

Però, és clar, el codi resultant no tenia l'aspecte d'un script Groovy.  Així que vaig començar a modificar-lo per a seguir un estil de codificació més Groovy.

Primer de tot, el tractament dels arguments és més senzill que amb BeanShell, doncs args ésdisponible directament. Com amb la vesió Python, hauria pogut utilitzar un mòdul per a tractar els arguments de la línia de comandes, el CliBuilder, però ho deixo per a una altre ocasió.

Observar que no he fet servir ";" .

Importació de llibreries Java i ús de SimpleDateFormat per a fer el parseig de ñes cadenes i el formateig dels dates, com amb BeanShell.

Simplificació de la lectura i escriptura dels fitxers amb l'objecte Groovy File. Aquest objecte NO és l'objecte Java File. File de Groovy simplifica el tractament dels fitxers. Fixem-nos com n'hi ha prou amb obrir el fitxer per nom i després, fent us d'una closure,  llegeix línia per línia el fitxer. M'he estalviat els BufferedReader.

Per a cada línia fa el mateix tractament  que amb l'script BeanShell però, en comptes d'escriure línia a línia, el que fa és construir una cadena sFile, un buffer, amb tota la informació del fitxer. En comptes de fer servir l'operador '+' per a concatenar, opta per una solució més Groovyana:

sLine = "${sTempsIniciDelayed} --> ${sTempsFiDelayed}"

Un cop processades totes les línies, escriu el buffer de cop, amb el mètode write de l'objecte File.

És evident la simplificació comparant amb BeanShell, o que l'equivalent amb Java. M'atreviria a dir que, fins i tot, és més simplificat que amb Python.

Però tampoc aniré més enllà. Es tracta d'un script molt senzill i no seria correcte treure'n conclusions sobre la potència dels tres llenguatges. Segur que en totes tres versions, Python, BeanShell, Groovy es podrien buscar simplificacions addicionals. I en tot cas, a l'hora de triar una solució o altre, hi han factors que poden ser molt més determinants que la potència del llenguatge: disponibilitat d'entorns de desenvolupament, coneixements i experiència dels programadors...

Prenguem, doncs, aquest script d'avui només com un petit tast d'un altre aroma del cafè: Groovy, un altre llenguatge per a la JVM.

Retardador de subtítols SubRip (again). BeanShell version.

El mateix retardador de subtítols del post anterior, ara fet amb BeanShell.

Suposant que guardo l'script al fitxer delayer.bsh i que tinc el fitxer prova.srt del post anterior, aleshores puc retardar l'aparició dels subtítols amb la següent línia de comandes:

./delayer.bsh prova.srt output.srt 10000

Vet aquí el codi:

#!/usr/bin/bsh
 

import java.text.DateFormat;
import java.text.SimpleDateFormat;

// obté els arguments
args = this.interpreter.get("bsh.args");

// verifica que té 3 arguments
if (args.length != 3 ) {
    sUsage = "Usage:\tdelayer filein.srt fileout.srt delay" +
             "\n\texample 1: delayer filein.srt fileout.srt 10000 --> delays 10 seconds" +
             "\n\texample 2: delayer filein.srt fileout.srt -10000 --> advances 10 seconds";

    print("incorrect number of arguments");
    print(sUsage);
    System.exit(1);
}

// calcula el delta
lDelta = Long.parseLong(args[2]);

// prepara el SimpleDateFormat
sdf = new SimpleDateFormat("HH:mm:ss,SSS");

// obre els fitxers args[0] per a lectura i args[1] per a escriptura
fFileIn = new FileReader(args[0]);
fFileOut = new FileWriter(args[1]);
brFileIn = new BufferedReader(fFileIn);
brFileOut = new BufferedWriter(fFileOut);

while((sLine = brFileIn.readLine()) != null) {
    sTokens= sLine.split("-->");   // mètode split() milor que StringTokenizer des de JDK1.4.
    if (sTokens.length == 2) {
    // si té dos elements, és la línia de temps

    // els "pela"
    sTempsInici = sTokens[0].trim();
    sTempsFi = sTokens[1].trim();

    // els converteix a Date
    dTempsInici = sdf.parse(sTempsInici);
    dTempsFi = sdf.parse(sTempsFi);

    // Els suma el retard
    dTempsIniciDelayed = new Date(dTempsInici.getTime() + lDelta);
    dTempsFiDelayed = new Date(dTempsFi.getTime() + lDelta);

    // converteix els Date a String amb el mateix SimpleDateFormat
    sTempsIniciDelayed = sdf.format(dTempsIniciDelayed);
    sTempsFiDelayed = sdf.format(dTempsFiDelayed);

    // Munta la línia
    sLine = sTempsIniciDelayed + " --> " + sTempsFiDelayed;
    }

    // escriu la línia
    brFileOut.write(sLine);
    // i salta de línia
    brFileOut.newLine();
}

// tanca fitxers i buffered readers / writers
brFileOut.close();
brFileIn.close();
fFileIn.close();
fFileOut.close();

// i acaba aquí


Molt poques coses a dir. Si de cas destacar que:

No he declarat els tipus de cap variable.

No ha calgut importar les classes de java.io ni java.util.En canvi sí les de java.text

Us de

// obté els arguments
args = this.interpreter.get("bsh.args");


per obtenir els arguments de la línia de comandes.

Evidentment, la versió amb Python i optparse era més potent funcionalment pel que fa al tractament dels arguments d'entrada. Ara bé, a la simplicitat en aquest cas juga a favor i fa que l'argument de retard negatiu es tracti de forma més natural que amb la solució Python.

Us del format HH:mm:ss,MMM (hores de  0 a 23:minuts:segons,milisegons) al SimpleDateFormat, tant per a parsejar strings com per a formatar dates.

I finalment, l'us d'Split, en comptes del clàssic StringTokenizer per a obtenir els temps d'inici i final de presentació dels subtítols.

Què és més eficient? Python o BeanShell? Per a aquest cas, ho deixo en empat. Per mi ha estat més fàcil BeanShell que Python, però crec que això respon al fet que estic molt més familiaritzat amb Java.

El proper pas podria ser escriure el mateix script en Groovy. Sospito que només caldrà tocar el pas d'arguments i poca cosa més.

dilluns, 8 d’agost del 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__.