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:
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
1
mediagoblin/media_types/audio/audioprocessing.py
Symbolic link
1
mediagoblin/media_types/audio/audioprocessing.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../extlib/freesound/audioprocessing.py
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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\')'
|
||||
|
||||
@@ -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...')
|
||||
|
||||
@@ -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__
|
||||
|
||||
|
||||
@@ -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="vorbis"" />
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user