Curso de Python para ciencias e ingeniería, nueva edición

/images/Newsletter4-Banner_20120705_12-44-50-800.jpg

Está abierta la inscripción para el nuevo dictado de mi curso de introducción a Python para ciencias e ingenierías, en la Facultad de Ciencias Exáctas, Físicas y Naturales de la Universidad Nacional de Córdoba (aprobado como curso de extensión - Resolución 1272/2015, bajo el nombre "Python como herramienta para la ingeniería") desde el miércoles 14 de ocubre de 18 a 22hs, durante 5 semanas.

Este curso una versión mejorada del que dicté en la Facultad de Matemática, Física y Astronomía en mayo pasado, avalado como curso de extensión (Resolución HCD 107/2015)

El precio está absolutamente subsidiado por las ganas de que más ingenieros e investigadores programen Python. No te demores en anotarte , los cupos son muy limitados!

Se entregarán certificados a quienes completen asistencia.

Leer más…

Curso de Python para ñoños

¡Los cupos irán a sorteo! Anotate. Si no entrás para en esta edición, quedás anotado para la próxima que será muy pronto.

La demanda nos desbordó. En menos de 2 dias tenemos inscriptos para llenar dos veces el laboratorio que tenemos disponible! Algunos ya están averiguando la disponibilidad del Estadio Kempes para hacer el próximo.

Como recién hoy (martes 7 de abril) se realizó la difusión oficial desde FaMAF, la decisión de los organizadores es permitir la inscripción de todos los interesados y hacer un sorteo de las 25 plazas aranceladas y otro para las 15 plazas gratuitas reservadas para estudiantes de grado de FaMAF.

La justificación de un sorteo en vez de tomar el orden de inscripción la dió el Dr. Nicolás Wolovick, que junto al Dr. Pedro Pury fueron los gestores para que el curso se oficializara, con un argumento democráticamente ñoño:

Estar conectado 24/7 por 3G, recibir el tweet, e inscribirse, no es justo, es una condición de posibilidad que no todos tienen. La distribución uniforme es la que mayor entropía tiene :)

Si estás interesado, es importante que te inscribas a través del formulario. Así tendremos una lista bien grande de "argumentos" para reeditar el curso lo más pronto posible.

Otra posibilidad es que averigües hagas lobby en tu empresa o laboratorio sobre la posibilidad de realizar el curso in house y, si tienen interés, lo charlamos.


/images/Newsletter4-Banner_20120705_12-44-50-800.jpg

A partir del 29 de abril voy a dar el curso Introducción a Python para ciencias e ingenierías en la Facultad de Matemática, Astronomía y Física (FaMAF) de la Universidad Nacional de Córdoba.

Este curso es una versión revisada y extendida del que dí en la ScipyCon Argentina 2014 y durará 8 clases de 2hs cada una. Será los miércoles de 18hs a 20hs en el laboratorio de computación de la facultad. No hace falta contar con equipo propio y el único pre-requisito es tener nociones básicas de programación en cualquier lenguaje.

El costo del curso es $400. Hay becas para estudiantes de grado de FaMAF.

Acá está el formulario de pre-inscripción (LOS CUPOS IRAN A SORTEO).

El curso está reconocido como Curso de Extensión de FaMAF (Res. HCD 107/2015) y se entregarán certificados oficiales a quienes completen asistencia y participación.

Atención Los cupos son muy limitados!

Leer más…

Sergio Massa y #LaGente

Anoche me reí mucho con el hashtag #LaGente, que se viralizó mientras Alejandro Fantino entrevistaba, una vez más, al inefable candidato presidencial Sergio Massa.

Me acordé entonces de un post de Zulko, cuyo blog es un compilado de gemas ñoñamente divertidas. Allí muestra cómo recortar automáticamente los pedacitos de un video que mencionen una palabra o frase, basándose en las marcas de tiempo del archivo de subtítulos, utilizando su maravillosa biblioteca Moviepy y un poco de Python. Más o menos lo que hace videogrep, pero más prolijo.

La herramienta youtube-dl (que también es genial y hecha en Python), permite no sólo bajar videos de youtube y los subtitulos existentes, sino que también puede bajar el "subtitulo automático". En general son bastante malos pero es suficientemente efectivo para encontrar pequeñas frases.

Todo sea por "la gente": manos a la obra

Lo primero que necesitamos es una lista de videos donde Sergio Massa hable. Hice una búsqueda, decidí ignorar algunos (parodias, por ejemplo) y generé una lista. Hay varias maneras de obtener este listado de las primeras paginas de resultados, yo utilicé el rústico y efectivo webscrapping:

In [1]:
from pyquery import PyQuery
links = []
skip = ('M0yuFHbhYLY','TLmMh9Qvmic', 'rY4Hwvn6GlA')

for page in range(1, 5):
    pq = PyQuery('https://www.youtube.com/results?search_query=entrevista+sergio+massa&page=%s' % page)
    pq.make_links_absolute()
    links.extend([pq(a).attr('href') for a in pq('a.yt-uix-tile-link') if pq(a).attr('href').split('v=')[1] not in skip])
links
Out[1]:
['https://www.youtube.com/watch?v=8pP8G3fSAcY',
 'https://www.youtube.com/watch?v=g6QSwxUo1aw',
 'https://www.youtube.com/watch?v=_9FN6CI8fD4',
 'https://www.youtube.com/watch?v=5wqwNDpkZOo',
 'https://www.youtube.com/watch?v=V865E4mBiHU',
 'https://www.youtube.com/watch?v=TPrGNJnMS9U',
 'https://www.youtube.com/watch?v=SVTl11hG9Gs',
 'https://www.youtube.com/watch?v=Df_dwb5XHQM',
 'https://www.youtube.com/watch?v=sptBkyfq1VU',
 'https://www.youtube.com/watch?v=tzjz1xrNu3k',
 'https://www.youtube.com/watch?v=k-CGbuOo8do',
 'https://www.youtube.com/watch?v=_L-B_wHsEec',
 'https://www.youtube.com/watch?v=iFOABIQdo9Q',
 'https://www.youtube.com/watch?v=WOlRIKGrBWY',
 'https://www.youtube.com/watch?v=a-mCgN6W9ek',
 'https://www.youtube.com/watch?v=x5vhchv3zAY',
 'https://www.youtube.com/watch?v=bi5eK7i59w0',
 'https://www.youtube.com/watch?v=VNHV3D_6o4E',
 'https://www.youtube.com/watch?v=MWVZ6JDU9V8',
 'https://www.youtube.com/watch?v=v-JmdgVZqVc',
 'https://www.youtube.com/watch?v=FBFHpdxsyYU',
 'https://www.youtube.com/watch?v=WXmTc83l1sQ',
 'https://www.youtube.com/watch?v=GfNgds5vS60',
 'https://www.youtube.com/watch?v=UHRa34A6rDg',
 'https://www.youtube.com/watch?v=xVU-EjnuksU',
 'https://www.youtube.com/watch?v=-IXymTZZM6o',
 'https://www.youtube.com/watch?v=tzvwDTPyTHQ',
 'https://www.youtube.com/watch?v=a19z6EVWpQ4',
 'https://www.youtube.com/watch?v=rAOvF8X_nzM',
 'https://www.youtube.com/watch?v=wtvl4esdMGU',
 'https://www.youtube.com/watch?v=1YPHDDH1Az0',
 'https://www.youtube.com/watch?v=w7TnghsrJUo',
 'https://www.youtube.com/watch?v=qBT-6HpSrwc',
 'https://www.youtube.com/watch?v=JM-xblTxLGc',
 'https://www.youtube.com/watch?v=kMymsVsmETY',
 'https://www.youtube.com/watch?v=K1-dfiVfbOI',
 'https://www.youtube.com/watch?v=VnoiHVlR-So',
 'https://www.youtube.com/watch?v=hMTzJyLiXE4',
 'https://www.youtube.com/watch?v=VGQPNQ1Bhkg',
 'https://www.youtube.com/watch?v=0oR4z7SsY14',
 'https://www.youtube.com/watch?v=Cl4r8h_Hlak',
 'https://www.youtube.com/watch?v=gJFmek-YgYo',
 'https://www.youtube.com/watch?v=9VQ7Ov5W_tM',
 'https://www.youtube.com/watch?v=rKwKImVrYu4',
 'https://www.youtube.com/watch?v=LJwj9SHC9EU',
 'https://www.youtube.com/watch?v=-08OEpFThiw',
 'https://www.youtube.com/watch?v=BPJBl5y2P2g',
 'https://www.youtube.com/watch?v=MvkXlg9ZbL4',
 'https://www.youtube.com/watch?v=7KgIa4fX_Ng',
 'https://www.youtube.com/watch?v=upNLrHtzeBI',
 'https://www.youtube.com/watch?v=Y-norf1BKAs',
 'https://www.youtube.com/watch?v=QMvAl_fxQSA',
 'https://www.youtube.com/watch?v=3os_uXUOvcM',
 'https://www.youtube.com/watch?v=ZE_aChIEELo',
 'https://www.youtube.com/watch?v=iKI-8ceuR-A',
 'https://www.youtube.com/watch?v=CASdYLquQII',
 'https://www.youtube.com/watch?v=5cvyi1CcpYs',
 'https://www.youtube.com/watch?v=NVEw-YIAy5A',
 'https://www.youtube.com/watch?v=yMXn04-GQTY',
 'https://www.youtube.com/watch?v=RCCzZGcGg5k',
 'https://www.youtube.com/watch?v=FqMFKGsXLOE',
 'https://www.youtube.com/watch?v=MVOvQb8KBm0',
 'https://www.youtube.com/watch?v=ENvWfMwnJ_0',
 'https://www.youtube.com/watch?v=bs7xGm293Vs',
 'https://www.youtube.com/watch?v=7OvrK-U-axI',
 'https://www.youtube.com/watch?v=VHeWqPqs4vo',
 'https://www.youtube.com/watch?v=nVOEi9FESn8',
 'https://www.youtube.com/watch?v=eikTWAvFwTE',
 'https://www.youtube.com/watch?v=BU2amn3QdWk',
 'https://www.youtube.com/watch?v=GiB1pOuEvqg',
 'https://www.youtube.com/watch?v=GAPN17lTJ9c',
 'https://www.youtube.com/watch?v=4Ja1uZbMM8E',
 'https://www.youtube.com/watch?v=F1dAfCR4rc0',
 'https://www.youtube.com/watch?v=334O9xh-CQY',
 'https://www.youtube.com/watch?v=KgNmw3sJ0g8',
 'https://www.youtube.com/watch?v=-SQSue4-PLk',
 'https://www.youtube.com/watch?v=HPE4PHlYySo']

Luego, el paso lento: bajar los videos. Al parecer, Youtube no genera un subtitulo automático para videos demasiado largo, así que limité hasta 30 minutos.

In [2]:
for link in links:
    !youtube-dl --write-auto-sub --sub-lang es --max-filesize 30.00m {link}

Con el material crudo disponible (aunque puede ser que no se hayan encontrado subtitulos para todos los videos), podemos copiar descaradamente partes del código de Zulko (levemente adaptado)

In [3]:
import re 
import os
import glob
import random
from moviepy.editor import VideoFileClip, concatenate, TextClip, CompositeVideoClip


def convert_time(timestring):
    """ Converts a string into seconds """
    nums = [float(t) for t in re.findall(r'\d+', timestring)]
    return 3600 * nums[0] + 60*nums[1] + nums[2] + nums[3]/1000


def get_time_texts(file):
    with open(file) as f:
        lines = f.readlines()

    times_texts = []
    current_times , current_text = None, ""
    for line in lines:
        times = re.findall("[0-9]*:[0-9]*:[0-9]*,[0-9]*", line)
        if times != []:
            current_times = [convert_time(t) for t in times]
        elif line == '\n':
            times_texts.append((current_times, current_text))
            current_times, current_text = None, ""
        elif current_times is not None:
            current_text = current_text + line.replace("\n"," ")
    return times_texts

def find_word(word, times_texts, padding=.4):
    """ Finds all 'exact' (t_start, t_end) for a word """
    matches = [re.search(word, text)
               for (t,text) in times_texts]
    return [(t1 + m.start()*(t2-t1)/len(text) - padding,
             t1 + m.end()*(t2-t1)/len(text) + padding)
             for m,((t1,t2),text) in zip(matches, times_texts)
             if (m is not None)]


def get_subclips(video_path, cuts):   
    video = VideoFileClip(video_path)
    return [video.subclip(start, end) for (start,end) in cuts]


def get_all_subclips_for(word, pattern='*.mp4', sub_ext='.es.srt', shuffle=True):
    subclips = []
    for mp4 in glob.glob(pattern):
        sub = os.path.splitext(mp4)[0] + sub_ext
        try:
            times = find_word(word, get_time_texts(sub))
        except IOError:
            # ignore video if it hasn't subtitle
            continue
        cuts = get_subclips(mp4, times)
        subclips.extend(cuts)
    if shuffle:
        random.shuffle(subclips)
    return subclips

La función get_all_subclip recibe la frase a buscar y devuelve un listado de segmentos donde, muy probablemente, se pronuncia.

In [4]:
gente = get_all_subclips_for('la gente')
len(gente)
Out[4]:
77

El problema es que aunque es muy probable que sea Sergio Massa el que diga "la gente" en sus entrevistas, a veces es el entrevistador, a veces youtube entendió mal al desgrabar y a veces el código recortador la pifia. Por este motivo hay que descartar los segmentos que no sirven.

Se me ocurrió hacerlo visualmente: los pegué todos, superponiendo el índice al que corresponde cada segmento, para luego anotar los que no sirven y filtrarlos en otra pasada.

In [5]:
def make_preview(subclips):
    subclips_ = []
    for (i, clip) in enumerate(subclips):
        txt_clip = TextClip(str(i),fontsize=70, color='white')
        txt_clip = txt_clip.set_pos('center').set_duration(clip.duration)
        clip = CompositeVideoClip([clip, txt_clip])
        subclips_.append(clip)

    final = concatenate(subclips_, method='compose')
    final.write_videofile('preview.webm', codec='libvpx', fps=24)    
In [6]:
make_preview(gente)
[MoviePy] >>>> Building video preview.webm
[MoviePy] Writing audio in previewTEMP_MPY_wvf_snd.ogg
[MoviePy] Done.
[MoviePy] Writing video preview.webm
[MoviePy] Done.
[MoviePy] >>>> Video ready: preview.webm 

El resultado me permitió hacer el tamizado

In [7]:
ignore = [2, 3, 8, 12, 17, 19, 25, 28, 32, 36, 38, 40, 41, 44, 49, 55, 56, 61, 62, 66, 73, 74]
subclips_cleaned = [i for j, i in enumerate(gente) if j not in ignore]

Aunque no tengo idea de edición de videos, y porque de verdad creo que es un demamogo impresentable que no debería presidir ni una junta vecinal, quería darle un toque final, con una pequeña frase

In [8]:
import numpy as np
from moviepy.video.tools.segmenting import findObjects

def arrive(screenpos,i,nletters):
    v = np.array([-1,0])
    d = lambda t : max(0, 3-3*t)
    return lambda t: screenpos-400*v*d(t-0.2*i)

screensize = (640,360)
txtClip = TextClip('Yn tragr ab rf obyhqn'.decode('rot13'), color='white', font="Amiri-Bold", kerning=5, fontsize=50)
cvc = CompositeVideoClip( [txtClip.set_pos('center')],
                        size=screensize)

letters = findObjects(cvc) # a list of ImageClips

def moveLetters(letters, funcpos):
    return [ letter.set_pos(funcpos(letter.screenpos,i,len(letters)))
              for i,letter in enumerate(letters)]

ending = CompositeVideoClip(moveLetters(letters, arrive), size=screensize).subclip(0, 10)
In [9]:
# le damos una mezcladita más
random.shuffle(subclips_cleaned)
subclips_cleaned.append(ending)
make_final(subclips_cleaned, 'massa_lagente_final.webm')
[MoviePy] >>>> Building video massa_lagente_final.webm
[MoviePy] Writing audio in massa_lagente_finalTEMP_MPY_wvf_snd.ogg
[MoviePy] Done.
[MoviePy] Writing video massa_lagente_final.webm
[MoviePy] Done.
[MoviePy] >>>> Video ready: massa_lagente_final.webm 

Y este es el resultado:

In [ ]: