miércoles, abril 27, 2005

Ajustar subtítulos (o cómo pierde el tiempo un informático)

Una buena forma de practicar idiomas (por lo menos en lo que a la comprensión se refiere, hablarlos y escribirlos ya es otra cosa) es ver películas con subtítulos. Hace poco me enteré de que se podían poner los subtítulos a las películas en divx simplemente copiando un fichero ".sub" con el mismo nombre de la película cuando la reproduces.

Me puse a ver una película en inglés, con los subtítulos recién bajados (también en inglés), y cuál fue mi chasco cuando descubrí que el texto aparecía desfasado unos 18 segundos con respecto a la voz. Cualquier persona normal hubiese probado a bajarse otro fichero, o simplemente hubiera prescindido de los subtítulos. Pero yo no soy una persona normal. Así que le eché un vistazo al fichero de los subtítulos y aquí tenéis un ejemplo de lo que vi:

0:0:27.99,0:0:29.71
My children...

0:0:29.83,0:0:31.82
from the very beginning...

0:0:31.94,0:0:36.23
it was the children[br]who gave me my power.
Consiste en líneas con los tiempos de inicio y fin seguidas por los textos que aparecen en esos intervalos.

Me puse manos a la obra e hice un programilla que leía el fichero y sumaba (o restaba) una cantidad de tiempo constante a todos los indicadores temporales. Tras un rato haciendo pruebas y arreglando fallos (es lo que pasa cuando uno se pone a programar en plan albañilería), ya tenía el programa funcionando. Copié el nuevo fichero y me puse a ver la película. Los primeros textos estaban en una sincronía perfecta, pero noté que según iba avanzando la película se iban volviendo a quedar desfasados. Vaya jodienda, tanto trabajo para nada. Pero no me iba a dar por vencido tan fácilmente.

Volvía a reproducir la película, anotando los tiempos en los que empezaban cada frase y comparándolos con los que había en el fichero. Efectivamente, la diferencia no era constante, sino que cada vez se hacía mayor. Tomé unas cuantas muestras, repartidas por todo el metraje de la película y las copié en la hoja de cálculo. Pinté una gráfica en la que en un eje estaban los tiempos observados y en otra los definidos en el fichero de subtítulos. Aquello parecía una recta, por lo menos era algo esperanzador, no todo estaba perdido.

Cogí el libro de Álgebra de primero de la carrera, le quité el polvo que tenía encima y busqué cómo se hacía una aproximación por mínimos cuadrados. Ah, aquellas fórmulas me resultaban tan familiares y entrañables... al fin encontré una sección en la que explicaban cómo aproximar una recta, justo lo que yo necesitaba. Antes me hubiese puesto a mirar los teoremas para asegurarme de que entendía por qué funcionaba, pero ahora me bastaba con aplicarlo. 5 años dan para perder mucho interés por las cosas.

Ahora que ya tenía la fórmula y los datos, tenía que encontrar la solución al problema. Abrí el octave y me puse a echar las cuentas con los vectores y las matrices. Resultado final: los tiempos reales estaban relacionados con los predefinidos por la siguiente función: f(t) = -15 + t*0.96

Cambié mi programilla para que además de sumar un tiempo, multiplicase por una constante y... alucinante, los textos coincidían con la voz. Increible que una chapuza así haya funcionado.

Para los que gusten de estas cosillas, les pongo el código del super-programón, hecho en Java.


import java.io.*;
import java.util.StringTokenizer;

public class SubAjust {

private static String SEPARATORS = "";
private static String NOT_SEPARATORS = "0123456789" + Time.DELIMS;

BufferedReader _br;
Time _dt;
double _factor;

public static void main(String[] args) {
for (char c = 0; c < 256; c++) {
if (Character.isDefined(c) && NOT_SEPARATORS.indexOf(c) == -1) {
SEPARATORS += c;
}
}

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Time t = new Time(args[0]);
double f = Double.parseDouble(args[1]);

try {
new SubAjust(br, t, f).parse();
} catch (IOException e) {
e.printStackTrace();
}
}

public SubAjust(BufferedReader br, Time t, double f) {
_br = br;
_dt = t;
_factor = f;
}

public void parse() throws IOException {
String line;

line = _br.readLine();
while(line != null) {
try {
System.out.println(parseLine(line));
}
catch (Exception e) {
System.out.println(line);
}
line = _br.readLine();
}
}

private String parseLine(String line) {
String tmp;
String time1text;
String time2text;
String padding;
Time time1;
Time time2;

StringTokenizer st1 = new StringTokenizer(line, SEPARATORS);
StringTokenizer st2 = new StringTokenizer(line, NOT_SEPARATORS);

padding = st2.nextToken();
time1text = st1.nextToken();
time2text = st1.nextToken();

time1 = new Time(time1text);
time2 = new Time(time2text);

time1 = time1.mult(_factor).sum(_dt);
time2 = time2.mult(_factor).sum(_dt);

tmp = time1.toString() + padding + time2.toString();
return tmp;
}
}

class Time {

public static final int [] BOUNDS = {60, 60, 100};
public static final String DELIMS = "::.";

long centseconds = 0;

Time() {
}

Time(String s) throws IllegalArgumentException {
String tmp;
long mult;
int from;
int to;
try {
from = 0;
to = 0;
for (int i = 0; i < BOUNDS.length; i++) {
mult = 1;
to = s.indexOf(DELIMS.charAt(i), from);
tmp = s.substring(from, to);
for (int j = i; j < BOUNDS.length; j++) {
mult *= BOUNDS[j];
}
centseconds += Long.parseLong(tmp) * mult;
from = to + 1;
}
tmp = s.substring(from);
centseconds += Long.parseLong(tmp);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}

public String toString() {
String tmp = "";
long cs = centseconds;
for (int i = BOUNDS.length - 1; i >= 0; i--) {
tmp = DELIMS.charAt(i) + "" + (cs % BOUNDS[i]) + tmp;
cs /= BOUNDS[i];
}
tmp = cs + tmp;
return tmp;
}

public Time sum(Time o) {
Time tmp = new Time();
tmp.centseconds = this.centseconds + o.centseconds;
return tmp;
}

public Time mult(double d) {
Time tmp = new Time();
tmp.centseconds = (long) (centseconds * d);
return tmp;
}
}

¿Y a cuento de qué viene todo esto? ah, sí... esto era un ejemplo en el que había pensado para hablar sobre la alfabetización informática. La mayoría de la gente que se viese en la necesidad de hacer algo parecido, ¿qué habría hecho? seguramente cambiar a mano, una a una, todas las líneas, mientras iba dándole a la pausa a la película y miraba en qué segundo empezaban cada frase. A ojo de buen cubero, tardaría en hacer eso el doble de la duración de la película y además ya la habría visto para cuando quisiera utilizar los nuevos subtítulos.

Pues cosas así pasan a diario cuando la gente trabaja con ordenadores (puede que no tan exageradas). En un próximo post daremos cuenta de ello.


Actualización


Hay otro formato de subtítulos con el que me he encontrado, que tiene las marcas de tiempo por fotogramas. Un ejemplo:

{11186}{11271}Sixty-eight...sixty-nine...
{11272}{11325}seventy...seventy-one...
{11326}{11362}Come on, will you?

De nuevo, estaba desincronizado, por lo que hice el nuevo ajuste por mínimos cuadrados (de frames a centésimas de segundos) tomando algunas muestras y un nuevo programa, esta vez en Python, que trataba con ese formato.

#!/usr/bin/python
#cambiar estos valores (pendiente y corte en origen)
m = 3.3368
r = -14.1243

import sys

def csecsToTime(csecs):
t = csecs
result = "." + str(t%100)
t /= 100
result = ":" + str(t % 60) + result
t /= 60
result = ":" + str(t % 60) + result
t /= 60
result = str(t) + result
return result

def transform(orig):
return csecsToTime(int(int(orig) * m + r))

f = file(sys.argv[1])

for line in f:
tokens = line.replace("{","").split("}")
print transform(tokens[0]) + "," + transform(tokens[1])
print tokens[2].replace("\r","")

El programa es mucho más corto que el anterior, más que por la mayor sencillez de Python, por la simplicidad del fichero a procesar (no había subtítulos que ocupasen múltiples líneas, no había que leer marcas en formato H:M:S.CS) y que los parámetros están incluidos en el fuente, que para eso es un script.

1 comentario: