Cellmagic that makes "beep" sound when cell execution is done

This is a working prototype of a cell magic that plays a “beep” sound after cell execution.

%%finish_with_beep
for i in range(0,10000000):
    x = i+1
print("Hello World")
from IPython import get_ipython
from IPython.core.magic import Magics, cell_magic, magics_class
from IPython.display import Audio

@magics_class
class MyMagic(Magics):
    @cell_magic
    def finish_with_beep(self, line, cell):
        self.shell.run_cell(cell)
        print("beep")
        sound_file = 'beep.wav'
        display(Audio(sound_file, autoplay=True))


ipy = get_ipython()
ipy.register_magics(MyMagic)

I think that %%finish_with_beep can be useful for people who want to be notified when a long-running cell execution is finished.

3 Likes

What happens when you then open a notebook with many such cells? :wink:

You might find jupyter-helpers/notifications.py at 9feb0af6563b56d02688c18e1a9a415d15d9b1a2 · krassowski/jupyter-helpers · GitHub and

useful.

3 Likes

By the way different, versions of notebook add the cells with different delay (or in batches) so you can get different music on re-opening of a notebook depending on the environment :smiley: (if not using one of the workarounds that delete the audio after playing).

2 Likes

get different music on re-opening of a notebook depending on the environment :smiley:

This can be advertised as a jupy-synth feature :smiley:

Thanks for pointing me to your code, I am now using the InvisibleAudio class into the cell magic definition, and the Audio player is now indeed hidden.
But re-opening the notebook will still play all sounds.
I really don’t mind that small bug, so for my personal use I won’t implement a fix for that.
But if someone is interested in tweaking this finish_with_beep magic, feel free to go ahead :slight_smile:

from IPython import get_ipython
from IPython.core.magic import Magics, cell_magic, magics_class
from IPython.display import display, Audio

OUTPUT_AREA_CLASS = 'jp-OutputArea-child'
OUT_CLASS = 'jp-transient-html'

def hide_closest(css_class):
    return (
        f"var widget_our = this.closest('.{OUT_CLASS}');"
        f"var that = widget_our ? widget_our : this;"
        f"that.closest('.{css_class}').style.display = 'none';"
    )

class InvisibleAudio(Audio): # by @krassowski 
    def _repr_html_(self):
        audio = super()._repr_html_()
        audio = audio.replace(
            '<audio',
            (
                '<audio onended="'
                + hide_closest(css_class=OUTPUT_AREA_CLASS)
                + '" onloadstart="' + hide_closest(css_class=OUTPUT_AREA_CLASS) + '"'
            )
        )
        return f'<div style="display:none">{audio}</div>'


@magics_class
class MyMagic(Magics):
    @cell_magic
    def finish_with_beep(self, line, cell):
        self.shell.run_cell(cell)
        #print("beep")
        sound_file = 'beep.wav'
        display(InvisibleAudio(filename='beep.wav', autoplay=True))

ipy = get_ipython()
ipy.register_magics(MyMagic)

That’s what transient_html trick is for if I recall correctly.

from IPython import get_ipython
from IPython.core.magic import Magics, cell_magic, magics_class
from IPython.display import display, Audio
from threading import Thread
from time import sleep

OUTPUT_AREA_CLASS = 'jp-OutputArea-child'
OUT_CLASS = 'jp-transient-html'

def delayed_close(out, element, delay):
    sleep(delay)
    element.close()
    out.clear_output()
    out.close()

def hide_closest(css_class):
    return (
        f"var widget_our = this.closest('.{OUT_CLASS}');"
        f"var that = widget_our ? widget_our : this;"
        f"that.closest('.{css_class}').style.display = 'none';"
    )

class InvisibleAudio(Audio):
    def _repr_html_(self):
        audio = super()._repr_html_()
        audio = audio.replace(
            '<audio',
            (
                '<audio onended="'
                + hide_closest(css_class=OUTPUT_AREA_CLASS)
                + '" onloadstart="' + hide_closest(css_class=OUTPUT_AREA_CLASS) + '"'
            )
        )
        return f'<div style="display:none">{audio}</div>'

def transient_html(code: str, lifetime: float = 3):
    try:
        from ipywidgets import HTML
        from ipywidgets.widgets import Output
        element = HTML(code)
        out = Output()
        out.add_class(OUT_CLASS)

        thread = Thread(target=delayed_close, args=(out, element, lifetime))

        with out:
            display(element)

        display(out)

        thread.start()

    except ImportError:
        from IPython.display import HTML
        element = HTML(code)
        display(element)


@magics_class
class MyMagic(Magics):
    @cell_magic
    def finish_with_beep(self, line, cell):
        self.shell.run_cell(cell)
        sound_file = 'beep.wav'
        audio = InvisibleAudio(filename='beep.wav', autoplay=True)
        display(audio)
        transient_html(audio._repr_html_())


ipy = get_ipython()
ipy.register_magics(MyMagic)

I’ve tried that, but sound is still coming when re-opening the notebook.
Maybe display(audio) is the problem?

Yes, it’s either display or transient_html.

with only transient_html there is no sound when running the cell, only this output:

Sorry, have not seen this. Do you have ipywidgets installed?

nope, I did not install them before, but now I have that.
The cells with cellmagic show now the message Loading widget... for 3 seconds, and then the message disappears. But no sound still.

Lab or Notebook? Did you restart (fully) after installing ipywidgets?

Lab. And yes, full restart.