Skip to content

Commit

Permalink
now 'python manage.py runserver' can be used to quickly iterate on a …
Browse files Browse the repository at this point in the history
…presentation without having to re-run a separate build step every time the script narration is changed.
  • Loading branch information
toolness committed Oct 9, 2011
1 parent 2ed5cd4 commit b987fe6
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 68 deletions.
8 changes: 4 additions & 4 deletions README.md
Expand Up @@ -16,13 +16,13 @@ The generated presentations use [Popcorn.js][] for syncing audio to video conten

## Quick Start ##

1. Edit [script.html][] to your liking. Each plain-text line is text to be narrated, while all HTML elements are slides that the narration voices over.
1. Run `python manage.py runserver`.

2. Run `python quickpreso.py`. This will use OS X's `say` command-line tool to automatically generate audio for your narration and `ffmpeg` to convert the final audio to Ogg Vorbis. It also generates information about where slide changes occur in the narration.
2. Edit [script.html][] to your liking. Each plain-text line is text to be narrated, while all HTML elements are slides that the narration voices over.

3. View [index.html][] in Firefox or Chrome.
3. Open/reload http://127.0.0.1:8020 in Firefox or Chrome.

4. Repeat steps 1-3 as necessary until you're happy with your presentation. When you're finished, just upload everything in the `static-files` directory to a web server.
4. Repeat steps 2-3 as necessary until you're happy with your presentation. When you're finished, just upload everything in the `static-files` directory to a web server.

[script.html]: https://github.com/toolness/quickpreso/blob/master/static-files/script.html
[index.html]: https://github.com/toolness/quickpreso/blob/master/static-files/index.html
133 changes: 133 additions & 0 deletions manage.py
@@ -0,0 +1,133 @@
"""
usage: go.py <command>
commands:
runserver - run web server on 127.0.0.1 port %(port)s
build - build all content
"""

from wsgiref.simple_server import make_server
from wsgiref.util import FileWrapper
import os
import sys
import mimetypes
import wave
import subprocess
import hashlib
import json

ROOT = os.path.abspath(os.path.dirname(__file__))
path = lambda *x: os.path.join(ROOT, *x)

port = 8020
htmlpath = path('static-files', 'script.html')
oggpath = path('static-files', 'script.ogg')
jsonpath = path('static-files', 'script.json')

def say(words, filename, voice='Alex'):
null = open('/dev/null', 'w')
finalwav = path('final.wav')
out = wave.open(finalwav, 'w')
nchannels = 1
sampwidth = 2
framerate = 22050
comptype = 'NONE'
compname = 'not compressed'
out.setparams((nchannels, sampwidth, framerate, 0, comptype, compname))
durations = []
tmpwave = path('tmp.wave')
tmpwav = path('tmp.wav')
for word in words:
if isinstance(word, basestring):
subprocess.check_call(['say', '-v', voice, '-o', tmpwave, word])
subprocess.check_call(['ffmpeg', '-y', '-i', tmpwave, tmpwav],
stdout=null, stderr=subprocess.STDOUT)
inp = wave.open(tmpwav, 'r')
durations.append(float(inp.getnframes()) / framerate)
out.writeframes(inp.readframes(inp.getnframes()))
inp.close()
for tmpfilename in [tmpwave, tmpwav]:
os.unlink(tmpfilename)
elif isinstance(word, (int, float)):
out.writeframes('\0' * (sampwidth * int(word * framerate)))
durations.append(float(word))
else:
raise ValueError('unexpected: %s' % word)
out.close()
subprocess.check_call(['ffmpeg', '-y', '-i', finalwav,
'-acodec', 'libvorbis', '-aq', '1', filename],
stdout=null, stderr=subprocess.STDOUT)
os.unlink(finalwav)
null.close()
return durations

def build():
print "generating script.ogg and script.json from script.html."
script = open(htmlpath, 'r')

speak = []
for line in script:
line = line.strip()
if line and not line.startswith('<'):
speak.append(line)
speak.append(0.5)
speakhash = hashlib.sha1(json.dumps(speak)).hexdigest()
if os.path.exists(jsonpath) and os.path.exists(oggpath):
oldmetadata = json.load(open(jsonpath, 'r'))
if oldmetadata['speakhash'] == speakhash:
print "done (no changes needed)."
return
metadata = dict(speakhash=speakhash, durations=say(speak, oggpath))
json.dump(metadata, open(jsonpath, 'w'))
print "done."

def make_app():
def app(environ, start_response):
origpath = environ['PATH_INFO']

if origpath.endswith('/'):
origpath = '%sindex.html' % origpath
fileparts = origpath[1:].split('/')
fullpath = path('static-files', *fileparts)
fullpath = os.path.normpath(fullpath)
(mimetype, encoding) = mimetypes.guess_type(fullpath)
if fullpath in [jsonpath, oggpath]:
build()
if (fullpath.startswith(ROOT) and
not '.git' in fullpath and
os.path.isfile(fullpath) and
mimetype):
filesize = os.stat(fullpath).st_size
start_response('200 OK', [('Content-Type', mimetype),
('Content-Length', str(filesize)),
('Accept-Ranges', 'bytes')])
return FileWrapper(open(fullpath, 'rb'))

start_response('404 Not Found', [('Content-Type', 'text/plain')])
return ['Not Found: ', origpath]

return app

def serve(ip=''):
ipstr = ip
if not ipstr:
ipstr = 'all IP interfaces'
server = make_server(ip, port, make_app())
print "serving on %s port %d" % (ipstr, port)
server.serve_forever()

if __name__ == "__main__":
if len(sys.argv) < 2:
print __doc__ % globals()
sys.exit(1)

cmd = sys.argv[1]

if cmd == 'runserver':
serve('127.0.0.1')
elif cmd == 'build':
build()
else:
print "unknown command: %s" % cmd
sys.exit(1)
60 changes: 0 additions & 60 deletions quickpreso.py

This file was deleted.

9 changes: 5 additions & 4 deletions static-files/play-script.js
@@ -1,6 +1,6 @@
$(window).ready(function() {
var scriptLoaded = jQuery.Deferred();
var durationsLoaded = jQuery.getJSON("script.json");
var metadataLoaded = jQuery.getJSON("script.json");
$("#script").load("script.html", function() { scriptLoaded.resolve(); });
$("#subtitle-toggle").click(function() {
$("#subtitle").fadeToggle();
Expand All @@ -9,16 +9,17 @@ $(window).ready(function() {
else
$(this).text("Show subtitles")
});
jQuery.when(durationsLoaded, scriptLoaded).done(function(durations) {
jQuery.when(metadataLoaded, scriptLoaded).done(function(metadata) {
var i = 0;
var currTime = 0;
var currVisual = null;
var pop = Popcorn("#voiceover");
var durations = metadata[0].durations;
$("#script").contents().each(function() {
if (this.nodeType == this.TEXT_NODE) {
var subtitle = jQuery.trim(this.nodeValue);
var speakingTime = durations[0][i];
var pauseTime = durations[0][i+1];
var speakingTime = durations[i];
var pauseTime = durations[i+1];
if (currVisual) {
currVisual.end = currTime + speakingTime + pauseTime;
pop.footnote(currVisual);
Expand Down

0 comments on commit b987fe6

Please sign in to comment.