GiLgAmEzH: Incognitosis y la PEP8

Hace un tiempo trabajando con fades Facu me propuso "estirar" el "Maximum Line Length" para probar si resultaba un poco más cómodo. A mi me pareció perfecto ya que entiendo que deben quedar muy pocas terminales de 80 columnas. Eso sí antes de dar un paso adelante confirmamos con la PEP8 que se puede llevar hasta 100 caracteres, sin romper esta santa ¿ley?.

Some teams strongly prefer a longer line length. For code maintained exclusively or primarily by a team that can reach agreement on this issue, it is okay to increase the nominal line length from 80 to 100 characters (effectively increasing the maximum length to 99 characters), provided that comments and docstrings are still wrapped at 72 characters.

Así que ahí fuimos. Como yo tengo una linea que me marca ese límite esto fue lo que cambié en mi .vimrc

function! ConditionalLoad()
highlight ColorColumn guibg=PaleTurquoise1
if getcwd() =~ $HOME . "/code/fades/*"
    set colorcolumn=99
else
    set colorcolumn=80
endif
endfunction
autocmd BufRead *.py call ConditionalLoad()
autocmd BufRead *.rst call ConditionalLoad()

El código de esa función es de Facundo.

El punto del cual tomé especial nota es que para los comentarios y docstrings la PEP8 recomienda mantenerse dentro de los 72 caracteres

that comments and docstrings are still wrapped at 72 characters.

Hoy hice click en mi RSS sobre una nota de Incognitosis (a quién normalmente leo por RSS) y me llamó la atención el hermoso diseño del blog. Así que dejé un comentario. Apenas termine de escribir el comentario me acordé de los 72 chars y fui a comprobarlo

/images/javipass_pep8.png

Efectivamente la causa de que el blog de Javi Pastor sea tan cómodo para la lectura es su respeto por la PEP8!!

Ya que estamos les recomiendo que no dejen de prestarle un ojo a Incognitosis, es una de los pocos blogs que llevo años leyendo.

Diego Sarmentero: MagneBot Released!

Finally!! MagneBot is here!!

My first steps in Indie Game development :D

It's a really simple game (simple as in simplicity), but difficult to play to test your reaction skills!


You can get it from:

"Join MagneBot on this crazy surfing experience through the space station!

What is at the end of the tunnel in this space station?
No one knows, but you can be sure that MagneBot is going to try to find out!!

Help MagneBot collect as much power as you can, but be careful, nothing is as easy as it looks.
Also... Why is MagneBot in his underwear? Dude, get some pants!"








    Martín Gaitán: 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 [ ]:
    
    

    Martín Gaitán: 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 [ ]:
    
    

    Facundo Batista: Metaclasses in Python, the easy way (a real life example)


    They say that metaclasses make your head explode. They also say that if you're not absolutely sure what are metaclasses, then you don't need them.

    And there you go, happily coding through life, jumping and singing in the meadow, until suddenly you get into a dark forest and find the most feared enemy: you realize that some magic needs to be done.


    The necessity

    Why you may need metaclasses? Let's see this specific case, my particular (real life) experience.

    It happened that at work I have a script that verifies the remote scopes service for the Ubuntu Phone, checking that all is nice and crispy.

    The test itself is simple, I won't put it here because it's not the point, but it's isolated in a method named _check, that receives the scope name and returns True if all is fine.

    So, the first script version did (removed comments and docstrings, for brevity):

        class SuperTestCase(unittest.TestCase):

            def test_all_scopes(self):
                for scope in self._all_scopes:
                    resp = self._check(scope)
                    self.assertTrue(resp)

    The problem with this approach is that all the checks are inside the same test. If one check fails, the rest is not executed (because the test is interrupted there, and fails).

    Here I found something very interesting, the (new in Python 3) subTest call:

        class SuperTestCase(unittest.TestCase):

            def test_all_scopes(self):
                for scope in self._all_scopes:
                    with self.subTest(scope=scope):
                        resp = self._check(scope)
                        self.assertTrue(resp)

    Now, each "sub test" internally is executed independently of the other. So, they all are executed (all checks are done) no matter if one or more fail.

    Awesome, right? Well, no.

    Why not? Because even if internally everything is handled as independent subtest, from the outside point of view it still is one single test.

    This has several consequences. One of those is that the all-inside test takes too long, and you can't know what was going on (note that each of these checks hit the network!), as the test runner just show progress per test (not subtest).

    The other inconvenient is that there is not a way to call the script to run only one of those subtests... I can tell it to execute only the all-inside test, but that would mean to execute all the subtests... which, again, takes a lot of time.

    So, what I really needed? Something that allows me to express the assertion in one test, but that in reality it were several methods. So, I needed something that, from a single method, reproduce them so the class actually had several ones. This is, write code for a class that Python would find different. This is, metaclasses.


    Metaclasses, but easy

    Luckily, since a couple of years ago (or more), Python provides a simpler way to achieve the same that could be done with metaclasses. This is: class decorators.

    Class decorators, very similar to method decorators, receive the class that is defined below itself, and its response is considered by Python the real definition of the class. If you don't have the concept, you may read a little here about decorators, and a more deep article about decorators and metaclasses here, but it's not mandatory.

    So, I wrote the following class decorator (explained below):

        def test_multiplier(klass):
            """Multiply those multipliable tests."""
            for meth_name in (x for x in dir(klass) if x.startswith("test_")):
                meth = getattr(klass, meth_name)
                argspec = inspect.getfullargspec(meth)

                # only get those methods that are to be multiplied
                if len(argspec.args) == 2 and len(argspec.defaults) == 1:
                    param_name = argspec.args[1]
                    mult_values = argspec.defaults[0]

                    # "move" the usefult method to something not automatically executable
                    delattr(klass, meth_name)
                    new_meth_name = "_multiplied_" + meth_name
                    assert not hasattr(klass, new_meth_name)
                    setattr(klass, new_meth_name, meth)
                    new_meth = getattr(klass, new_meth_name)

                    # for each of the given values, create a new method which will call the given method
                    # with only a value at the time
                    for multv in mult_values:
                        def f(self, multv=multv):
                            return new_meth(self, **{param_name: multv})

                        meth_mult_name = meth_name + "_" + multv.replace(" ", "_")[:30]
                        assert not hasattr(klass, meth_mult_name)
                        setattr(klass, meth_mult_name, f)

            return klass

    The basics are: it receives a class, it returns a slightly modified class ;). For each of the methods that starts with "test_", I checked those that had two args (not only 'self'), and that the second argument were named.

    So, it would actually get the method defined in the following structure and leave the rest alone:

        @test_multiplier
        class SuperTestCase(unittest.TestCase):

            def test_all_scopes(self, scope=_all_scopes):
                resp = self.checker.hit_search(scope, '')
                self.assertTrue(resp)

    For that kind of method, the decorator will move it to something not named "test_*" (so we can call it but it won't be called by automatic test infrastructure), and then create, for each value in the "_scopes" there, a method (with a particular name which doesn't really matter, but needs to be different and is nice to be informative to the user) that calls the original method, passing "scope" with the particular value.

    So, for example, let's say that _all_scopes is ['foo', 'bar']. Then, the decorator will rename test_all_scopes to _multiplied_test_all_scopes, and then create two new methods like this::

        def test_all_scopes_foo(self, multv='foo'):
            return self._multiplied_test_all_scopes(scope=multv)

        def test_all_scopes_foo(self, multv='bar'):
            return self._multiplied_test_all_scopes(scope=multv)

    The final effect is that the test infrastructure (internally and externally) finds those two methods (not the original one), and calls them. Each one individually, informing progress individually, the user being able to execute them individually, etc.

    So, at the end, all gain, no loss, and a fun little piece of Python code :)

    Manuel Kaufmann (Humitos): PyDay en Asunción, Paraguay

    Hace un poco más de una semana que estamos en Paraguay con @EllaQuímica y hemos conocido gente hermosa a través de las calles, la comunidad de CouchSurfing y también Python. Nuestro primer contacto por estos lares fue Javier, con quien hablé en la Python Conference Argentina 2014 y quedamos de juntarnos si es que mi viaje me llevaría por Asunción.

    Una vez cerca de Asunción, me puse en contacto con él y también hice un poco de ruido en diferentes listas de correo de la zona (MiSol y Python Argentina) donde me pusieron en contacto con Mauro también. Así, se fue sumando gente y hoy somos un grupo de 13 personas con muchas ganas de formar (y formando) una comunidad "Python Paraguay".

    organizadores.thumbnail.jpeg

    Parte del comitée de organizadores

    Como primer evento de esta comunidad emergente, estamos organizando, junto a SENATICs, un #PyDay en Asunción, Paraguay el Sábado 28 de Marzo desde las 9:30hs en L'Office Santos, ubicado en Gral. Santos 1170.

    Difusión del evento y de la comunidad "Python Paraguay":

    GiLgAmEzH: Día internacional de la Mujer Trabajadora

    El 8 de marzo se festeja justamente lo que dice el titulo de este post. no es el día de las Legrand, las thatcher, las Alfano, etc. Es un buen momento también para recordar que no es únicamente una cuestión de género.

    Feliz día!

    Si Dios fuera una mujer

    ¿Y si Dios fuera mujer?
    pregunta Juan sin inmutarse,
    vaya, vaya si Dios fuera mujer
    es posible que agnósticos y ateos
    no dijéramos no con la cabeza
    y dijéramos sí con las entrañas.
    
    Tal vez nos acercáramos a su divina desnudez
    para besar sus pies no de bronce,
    su pubis no de piedra,
    sus pechos no de mármol,
    sus labios no de yeso.
    
    Si Dios fuera mujer la abrazaríamos
    para arrancarla de su lontananza
    y no habría que jurar
    hasta que la muerte nos separe
    ya que sería inmortal por antonomasia
    y en vez de transmitirnos SIDA o pánico
    nos contagiaría su inmortalidad.
    
    Si Dios fuera mujer no se instalaría
    lejana en el reino de los cielos,
    sino que nos aguardaría en el zaguán del infierno,
    con sus brazos no cerrados,
    su rosa no de plástico
    y su amor no de ángeles.
    
    Ay Dios mío, Dios mío
    si hasta siempre y desde siempre
    fueras una mujer
    qué lindo escándalo sería,
    qué venturosa, espléndida, imposible,
    prodigiosa blasfemia.
    

    Facundo Batista: Novedades pythónicas: fades, CDPedia, Django y curso


    Algunas, varias y sueltas.

    A nivel de proyectos, le estuvimos metiendo bastante con Nico a fades. La verdad es que la versión 2 que sacamos la semana pasada está piolísima... si usás virtualenvs, no dejes de pegarle una mirada.

    Otro proyecto con el que estuve es CDPedia... la parte de internacionalización está bastante potable, y eso también me llevó a renovar la página principal que te muestra cuando la abrís, así que puse a tirar una nueva versión de la de español, y luego seguirá una de portugués (¡cada imagen tarda como una semana!).

    Hace un rato subí a la página de tutoriales de Python Argentina el Tutorial de Django en español (¡gracias Matías Bordese por el material!). Este tuto antes estaba en un dominio que ahora venció, y nos pareció interesante que esté todo en el mismo lugar, facilita que la gente lo encuentre.

    Finalmente, empecé a organizar mi Segundo Curso Abierto de Python. Esta vez lo quiero hacer por la zona de Palermo, o alrededores (la vez pasada fue en microcentro). Todavía no tengo reservado un lugar, y menos fechas establecidas, pero el formato va a ser similar al anterior. Con respecto al sitio, si alguien conoce un buen lugar para alquilar "aulas", me avisa, :)

    Manuel Kaufmann (Humitos): Verduras y precios paraguayos

    Más allá de todo lo nuevo que tiene un país diferente a donde uno ha nacido, sus costumbre, sus ideas y su cultura en general. También está el tema de los precios y su moneda.

    Es un poco complicado al principio acostumbrarse a una moneda en donde todo se maneja en miles y por otro lado todo el mundo es millonario (el salario mínimo es de 1.800.000 gs) ;)

    Además, para nosotros, que llevamos moneda Argentina todo nos sale mucho más caro. Así que, hacer esa conversión de Guaraníes, Dólares y luego a Pesos es bastante frustrante. Más vale pagar con los ojos cerrados y disfrutar de todos los días. Pero claro, ¿cuánto tiempo puede durar esa dinámica? Un poco hay que pensar, por lo menos, no comer nada de lo que es extremadamente caro: ¡Lechuga!.

    ¿Quién hubiera pensado que la lechuga común iba a ser tan cara en algún momento?

    Para poder hacer un análisis de qué cosas nos conviene comer / comprar, empecé a tomar algunas fotografías de los precios de las frutas y las verduras en el Supermecado Stock de Paraguay:

    IMG_20150304_194103.thumbnail.jpg

    Pimiento, repollo, coliflor, cebolla colorada, zapallo, remolacha, etc

    IMG_20150304_194116.thumbnail.jpg

    Puerro, achicoria, apio, espinaca, remedio, cebollita de hoja, lechuga, etc

    IMG_20150304_194124.thumbnail.jpg

    Manzana, durazno, mburucuya, melon, uva, etc

    IMG_20150304_194148.thumbnail.jpg

    Mandioca, papa, cebolla, etc

    IMG_20150304_194337.thumbnail.jpg

    Repollo, zanahoria, locote, tomate

    Nota

    Acá hierven la mandioca y la usan como acompañamiento de las carnes, como si fuera un pan. Y comen muchísima mandioca, está por todos lados.

    IMG_20150304_194226.thumbnail.jpg

    Naranja, limón, manzana coleg, banana

    IMG_20150304_194238.thumbnail.jpg

    Mandarina, pomelo, batata, banana de oro, piña brasil, batata

    Saquen sus propias conclusiones y, si llegan a algún algoritmo para reducir los gastos, me avisan que lo implementamos de toque.