Audio thumbnailing & spectrograms, media plugins use sniffing

* Added extlib/freesound/audioprocessing.py
* config_spec
  * Added create_spectrogram setting
  * Added media:medium and media:thumb max_{width,height} settings
* Added sniffing logic to
  - audio.processing:sniff_handler
  - video.processing:sniff_handler
* Changed audio.processing:sniff_handler logic
* Added audio thumbnailing functionality to audio.processing
  (works only with create_spectrogram enabled)
* Refractored contexts in audio.processing
* Added audio.transcoders:AudioThumbnailer
  Used for creating spectrograms and spectrogram thumbnails -
  Wadsworth's Constant, we meet again :)
* audio.transcoders:AudioTranscoder
  - Added mux_string kwarg
  - Delete self.pipeline on self.halt()
* Changed str.format formatting in image.processing:sniff_handler
  Had {1} without an {0}, changed to {0}
* Refractored VideoTranscoder to use transcode() for transcoding instead
  of __init__()
* Added discover() method to video.transcoders:VideoTranscoder
* Added spectrogram display to media_displays/audio.html
* Updated test_submission to reflect changes in media plugin delegation
This commit is contained in:
Joar Wandborg
2012-02-28 21:59:38 +01:00
parent 9f46a79dde
commit 10085b7739
11 changed files with 843 additions and 27 deletions

View File

@@ -65,6 +65,14 @@ base_url = string(default="/mgoblin_media/")
storage_class = string(default="mediagoblin.storage.filestorage:BasicFileStorage")
base_dir = string(default="%(here)s/user_dev/media/queue")
[media:medium]
max_width = integer(default=640)
max_height = integer(default=640)
[media:thumb]
max_width = integer(default=180)
max_height = integer(default=180)
[media_type:mediagoblin.media_types.video]
# Should we keep the original file?
keep_original = boolean(default=False)
@@ -72,6 +80,7 @@ keep_original = boolean(default=False)
[media_type:mediagoblin.media_types.audio]
# vorbisenc qualiy
quality = float(default=0.3)
create_spectrogram = boolean(default=False)
[beaker.cache]

View File

@@ -24,7 +24,16 @@ from mediagoblin.media_types.ascii import asciitoimage
_log = logging.getLogger(__name__)
SUPPORTED_EXTENSIONS = ['txt', 'asc', 'nfo']
def sniff_handler(media_file, **kw):
if not kw.get('media') == None:
name, ext = os.path.splitext(kw['media'].filename)
clean_ext = ext[1:].lower()
if clean_ext in SUPPORTED_EXTENSIONS:
return True
return False
def process_ascii(entry):

View File

@@ -0,0 +1 @@
../../../extlib/freesound/audioprocessing.py

View File

@@ -21,9 +21,10 @@ import os
from mediagoblin import mg_globals as mgg
from mediagoblin.processing import create_pub_filepath
from mediagoblin.media_types.audio.transcoders import AudioTranscoder
from mediagoblin.media_types.audio.transcoders import AudioTranscoder, \
AudioThumbnailer
_log = logging.getLogger()
_log = logging.getLogger(__name__)
def sniff_handler(media_file, **kw):
transcoder = AudioTranscoder()
@@ -33,7 +34,9 @@ def sniff_handler(media_file, **kw):
if data.is_audio == True and data.is_video == False:
return True
except:
return False
pass
return False
def process_audio(entry):
audio_config = mgg.global_config['media_type:mediagoblin.media_types.audio']
@@ -51,10 +54,9 @@ def process_audio(entry):
original=os.path.splitext(
queued_filepath[-1])[0]))
ogg_tmp = tempfile.NamedTemporaryFile()
transcoder = AudioTranscoder()
with ogg_tmp:
transcoder = AudioTranscoder()
with tempfile.NamedTemporaryFile() as ogg_tmp:
transcoder.transcode(
queued_filename,
@@ -72,11 +74,54 @@ def process_audio(entry):
entry.media_data['audio'] = {
u'length': int(data.audiolength)}
thumbnail_tmp = tempfile.NamedTemporaryFile()
if audio_config['create_spectrogram']:
spectrogram_filepath = create_pub_filepath(
entry,
'{original}-spectrogram.jpg'.format(
original=os.path.splitext(
queued_filepath[-1])[0]))
with thumbnail_tmp:
with tempfile.NamedTemporaryFile(suffix='.wav') as wav_tmp:
_log.info('Creating WAV source for spectrogram')
transcoder.transcode(
queued_filename,
wav_tmp.name,
mux_string='wavenc')
thumbnailer = AudioThumbnailer()
with tempfile.NamedTemporaryFile(suffix='.jpg') as spectrogram_tmp:
thumbnailer.spectrogram(
wav_tmp.name,
spectrogram_tmp.name,
width=mgg.global_config['media:medium']['max_width'])
_log.debug('Saving spectrogram...')
mgg.public_store.get_file(spectrogram_filepath, 'wb').write(
spectrogram_tmp.read())
entry.media_files['spectrogram'] = spectrogram_filepath
with tempfile.NamedTemporaryFile(suffix='.jpg') as thumb_tmp:
thumbnailer.thumbnail_spectrogram(
spectrogram_tmp.name,
thumb_tmp.name,
(mgg.global_config['media:thumb']['max_width'],
mgg.global_config['media:thumb']['max_height']))
thumb_filepath = create_pub_filepath(
entry,
'{original}-thumbnail.jpg'.format(
original=os.path.splitext(
queued_filepath[-1])[0]))
mgg.public_store.get_file(thumb_filepath, 'wb').write(
thumb_tmp.read())
entry.media_files['thumb'] = thumb_filepath
else:
entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg']
mgg.queue_store.delete_file(queued_filepath)
entry.save()

View File

@@ -16,8 +16,10 @@
import pdb
import logging
from PIL import Image
from mediagoblin.processing import BadMediaFail
from mediagoblin.media_types.audio import audioprocessing
_log = logging.getLogger(__name__)
@@ -56,6 +58,73 @@ try:
except ImportError:
raise Exception('gst/pygst >= 0.10 could not be imported')
import numpy
class AudioThumbnailer(object):
def __init__(self):
_log.info('Initializing {0}'.format(self.__class__.__name__))
def spectrogram(self, src, dst, **kw):
width = kw['width']
height = int(kw.get('height', float(width) * 0.3))
fft_size = kw.get('fft_size', 2048)
callback = kw.get('progress_callback')
processor = audioprocessing.AudioProcessor(
src,
fft_size,
numpy.hanning)
samples_per_pixel = processor.audio_file.nframes / float(width)
spectrogram = audioprocessing.SpectrogramImage(width, height, fft_size)
for x in range(width):
if callback and x % (width / 10) == 0:
callback((x * 100) / width)
seek_point = int(x * samples_per_pixel)
(spectral_centroid, db_spectrum) = processor.spectral_centroid(
seek_point)
spectrogram.draw_spectrum(x, db_spectrum)
if callback:
callback(100)
spectrogram.save(dst)
def thumbnail_spectrogram(self, src, dst, thumb_size):
'''
Takes a spectrogram and creates a thumbnail from it
'''
if not (type(thumb_size) == tuple and len(thumb_size) == 2):
raise Exception('size argument should be a tuple(width, height)')
im = Image.open(src)
im_w, im_h = [float(i) for i in im.size]
th_w, th_h = [float(i) for i in thumb_size]
wadsworth_position = im_w * 0.3
start_x = max((
wadsworth_position - (th_w / 2.0),
0.0))
stop_x = start_x + (im_h * (th_w / th_h))
th = im.crop((
int(start_x), 0,
int(stop_x), int(im_h)))
if th.size[0] > th_w or th.size[1] > th_h:
th.thumbnail(thumb_size, Image.ANTIALIAS)
th.save(dst)
class AudioTranscoder(object):
def __init__(self):
_log.info('Initializing {0}'.format(self.__class__.__name__))
@@ -103,17 +172,21 @@ class AudioTranscoder(object):
quality = kw.get('quality', 0.3)
mux_string = kw.get(
'mux_string',
'vorbisenc quality={0} ! webmmux'.format(quality))
# Set up pipeline
self.pipeline = gst.parse_launch(
'filesrc location="{src}" ! '
'decodebin2 ! queue ! audiorate tolerance={tolerance} ! '
'audioconvert ! audio/x-raw-float,channels=2 ! '
'vorbisenc quality={quality} ! webmmux ! '
'{mux_string} ! '
'progressreport silent=true ! '
'filesink location="{dst}"'.format(
src=src,
tolerance=80000000,
quality=quality,
mux_string=mux_string,
dst=dst))
self.bus = self.pipeline.get_bus()
@@ -141,6 +214,9 @@ class AudioTranscoder(object):
self.halt()
def halt(self):
if getattr(self, 'pipeline', False):
self.pipeline.set_state(gst.STATE_NULL)
del self.pipeline
_log.info('Quitting MainLoop gracefully...')
gobject.idle_add(self._loop.quit)
@@ -149,8 +225,12 @@ if __name__ == '__main__':
logging.basicConfig()
_log.setLevel(logging.INFO)
transcoder = AudioTranscoder()
data = transcoder.discover(sys.argv[1])
res = transcoder.transcode(*sys.argv[1:3])
#transcoder = AudioTranscoder()
#data = transcoder.discover(sys.argv[1])
#res = transcoder.transcode(*sys.argv[1:3])
thumbnailer = AudioThumbnailer()
thumbnailer.spectrogram(*sys.argv[1:], width=640)
pdb.set_trace()

View File

@@ -42,7 +42,7 @@ def sniff_handler(media_file, **kw):
_log.info('Found file extension in supported filetypes')
return True
else:
_log.debug('Media present, extension not found in {1}'.format(
_log.debug('Media present, extension not found in {0}'.format(
SUPPORTED_FILETYPES))
else:
_log.warning('Need additional information (keyword argument \'media\')'

View File

@@ -29,6 +29,18 @@ _log = logging.getLogger(__name__)
_log.setLevel(logging.DEBUG)
def sniff_handler(media_file, **kw):
transcoder = transcoders.VideoTranscoder()
try:
data = transcoder.discover(media_file.name)
_log.debug('Discovered: {0}'.format(data.__dict__))
if data.is_video == True:
return True
except:
_log.error('Exception caught when trying to discover {0}'.format(
kw.get('media')))
return False
def process_video(entry):
@@ -61,7 +73,8 @@ def process_video(entry):
with tmp_dst:
# Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
transcoder = transcoders.VideoTranscoder(queued_filename, tmp_dst.name)
transcoder = transcoders.VideoTranscoder()
transcoder.transcode(queued_filename, tmp_dst.name)
# Push transcoded video to public storage
_log.debug('Saving medium...')

View File

@@ -25,8 +25,6 @@ import pdb
import urllib
_log = logging.getLogger(__name__)
logging.basicConfig()
_log.setLevel(logging.DEBUG)
CPU_COUNT = 2
try:
@@ -340,10 +338,15 @@ class VideoTranscoder:
that it was refined afterwards and therefore is done more
correctly.
'''
def __init__(self, src, dst, **kwargs):
def __init__(self):
_log.info('Initializing VideoTranscoder...')
self.loop = gobject.MainLoop()
def transcode(self, src, dst, **kwargs):
'''
Transcode a video file into a 'medium'-sized version.
'''
self.source_path = src
self.destination_path = dst
@@ -357,6 +360,30 @@ class VideoTranscoder:
self._setup()
self._run()
def discover(self, src):
'''
Discover properties about a media file
'''
_log.info('Discovering {0}'.format(src))
self.source_path = src
self._setup_discover(discovered_callback=self.__on_discovered)
self.discoverer.discover()
self.loop.run()
return self._discovered_data
def __on_discovered(self, data, is_media):
if not is_media:
self.__stop()
raise Exception('Could not discover {0}'.format(self.source_path))
self._discovered_data = data
self.__stop_mainloop()
def _setup(self):
self._setup_discover()
self._setup_pipeline()
@@ -369,12 +396,14 @@ class VideoTranscoder:
_log.debug('Initializing MainLoop()')
self.loop.run()
def _setup_discover(self):
def _setup_discover(self, **kw):
_log.debug('Setting up discoverer')
self.discoverer = discoverer.Discoverer(self.source_path)
# Connect self.__discovered to the 'discovered' event
self.discoverer.connect('discovered', self.__discovered)
self.discoverer.connect(
'discovered',
kw.get('discovered_callback', self.__discovered))
def __discovered(self, data, is_media):
'''
@@ -614,14 +643,15 @@ class VideoTranscoder:
if __name__ == '__main__':
os.nice(19)
logging.basicConfig()
from optparse import OptionParser
parser = OptionParser(
usage='%prog [-v] -a [ video | thumbnail ] SRC DEST')
usage='%prog [-v] -a [ video | thumbnail | discover ] SRC [ DEST ]')
parser.add_option('-a', '--action',
dest='action',
help='One of "video" or "thumbnail"')
help='One of "video", "discover" or "thumbnail"')
parser.add_option('-v',
dest='verbose',
@@ -645,13 +675,18 @@ if __name__ == '__main__':
_log.debug(args)
if not len(args) == 2:
if not len(args) == 2 and not options.action == 'discover':
parser.print_help()
sys.exit()
transcoder = VideoTranscoder()
if options.action == 'thumbnail':
VideoThumbnailer(*args)
elif options.action == 'video':
def cb(data):
print('I\'m a callback!')
transcoder = VideoTranscoder(*args, progress_callback=cb)
transcoder.transcode(*args, progress_callback=cb)
elif options.action == 'discover':
print transcoder.discover(*args).__dict__

View File

@@ -20,7 +20,14 @@
{% block mediagoblin_media %}
<div class="audio-media">
<audio controls="controls"
{% if 'spectrogram' in media.media_files %}
<div class="audio-spectrogram">
<img src="{{ request.app.public_store.file_url(
media.media_files.spectrogram) }}"
alt="Spectrogram" />
</div>
{% endif %}
<audio class="audio-player" controls="controls"
preload="metadata">
<source src="{{ request.app.public_store.file_url(
media.media_files.ogg) }}" type="video/webm; encoding=&quot;vorbis&quot;" />

View File

@@ -231,7 +231,8 @@ class TestSubmission:
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html']
form = context['submit_form']
assert re.match(r'^Could not extract any file extension from ".*?"$', str(form.file.errors[0]))
assert 'Sorry, I don\'t support that file type :(' == \
str(form.file.errors[0])
assert len(form.file.errors) == 1
# NOTE: The following 2 tests will ultimately fail, but they