Permalink
Browse files

now 'python manage.py runserver' can be used to quickly iterate on a …

…presentation without having to re-run a separate build step every time the script narration is changed.
  • Loading branch information...
1 parent 2ed5cd4 commit b987fe6b3167be99e86d41b1ee7d36b98b1baa2a @toolness committed Oct 9, 2011
Showing with 142 additions and 68 deletions.
  1. +4 −4 README.md
  2. +133 −0 manage.py
  3. +0 −60 quickpreso.py
  4. +5 −4 static-files/play-script.js
View
@@ -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
View
133 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)
View
@@ -1,60 +0,0 @@
-import os
-import wave
-import subprocess
-import json
-
-ROOT = os.path.abspath(os.path.dirname(__file__))
-path = lambda *x: os.path.join(ROOT, *x)
-
-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 main():
- print "generating script.ogg and script.json from script.html."
- script = open(path('static-files', 'script.html'), 'r')
- speak = []
- for line in script:
- line = line.strip()
- if line and not line.startswith('<'):
- speak.append(line)
- speak.append(0.5)
- durations = say(speak, path('static-files', 'script.ogg'))
- json.dump(durations, open(path('static-files', 'script.json'), 'w'))
- print "done."
-
-if __name__ == '__main__':
- main()
@@ -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();
@@ -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);

0 comments on commit b987fe6

Please sign in to comment.