#! /usr/bin/env python """ This program will allow annotating without taking your fingers off of the keyboard. Copyright (C) 2006 Scott Shawcroft This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. This program will allow annotating without taking your fingers off of the keyboard. It will automatically store start and stop times along with the text. """ ##Program Info## """ Keyboard shortcuts: Enter: play and submit text or pause (only works when bottom right entry is focussed) ctrl-o: open file ctrl-s: save file All other keys will simply enter text. Text shortcuts: (you must use one of these initially!!! ')' - Chapters used for navigation. ':' - Subtitles. Note: end times are at the beginning of the same type of entry (ie. chapter or subtitle). Also, you may edited the description text in the main text window. Just be aware that anything not underlined is not considered part of the clip's text. """ import pygtk pygtk.require('2.0') import gtk import pango import pygst pygst.require ("0.10") import gst import mimetypes import sys import locale #Import custom element import gobject from textsrc import * from scriptview import * from time_converter import * import clip gobject.type_register(TextSource) class Keystroke: #This class handles all aspects of the program. Nothing fancy here. FRAME_RATE = 29.97 MODE_REVIEW = 0 MODE_TRANSCRIBE = 1 INSTRUCTIONS = """Welcome to Keystroke. Everything in Keystroke is run through a keystroke or hotkey, if you will. This method of control is ideal for the transcription of media because it allows for easy control of the media while never taking your fingers off of the keys. To get you started here is a list of all the keystrokes. Additionally, you will need to know two formats for differing between dialog and chapter 'tracks'. These formats will allow for the simultaneous chapter and dialog inputting. For dialog it allows a speaker to be defined. WARNING: This text will disappear upon import or open. Keystroke - Action: ctrl-o - Launches a file select to open a media file. ctrl-i - Launches a file select to open previous data. ctrl-s - Launches a file select to save the data. NOTE: You can select an output type via the file type. enter - Pauses/plays media and submits new text on pause->play. ctrl-r - Seeks back to the current clip start time. ctrl-t - Switches the current track playing over the video. ctrl-b - Seeks to the previous clip in the current track. ctrl-n - Seeks to the next clip in the current track. Input formats: NOTE: Replace everything inbetween <> inclusive. 1) ') ' Ex: ') Creative Commons' 2) ': ' Ex: 'Scott:Whee!' 3) Neither, inherited from last defined. Bugs and Oddities: To continue transcribing after reviewing pause after the last clip to set the new start point. Things will inherit from last entry. Credits: Scott Shawcroft - Author - scotts4@u.washington.edu Jason Kivlighn - Roommate License: This program allows annotating without taking your fingers off of the keyboard. Copyright (C) 2006 Scott Shawcroft This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor Boston, MA 02110-1301, USA. This program will allow annotating without taking your fingers off of the keyboard. It will automatically store start and stop times along with the text. """ TITLE = "Keystroke - Martini Alpha" #Below are all of our callbacks. def delete_event(self,widget,event,data=None): # Closes the window smoothly. pass def destroy(self, widget, data=None): # Closes the window. gtk.main_quit() def open_file(self, accel_group, window, keyval, modifier): if self.show_keystrokes: print "Ctrl - O" dialog = gtk.FileChooserDialog("Open",self.window,gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) #File filters. filter = gtk.FileFilter() filter.set_name("Yay for ogg!") filter.add_mime_type("application/ogg") dialog.add_filter(filter) filter = gtk.FileFilter() filter.set_name("MP3 Please") filter.add_mime_type("audio/mpeg") dialog.add_filter(filter) response = dialog.run() if response == gtk.RESPONSE_OK: self.pipeline.set_state(gst.STATE_NULL) self.current_file = dialog.get_filename() self.playbin.props.uri = "file://" + self.current_file self.pipeline.set_state(gst.STATE_PAUSED); self.pipeline_duration = 1000 self.push_text() self.scriptview.clear() self.mode = self.MODE_TRANSCRIBE dialog.destroy() def import_file(self, accel_group, window, keyval, modifier): if self.show_keystrokes: print "Ctrl - I" dialog = gtk.FileChooserDialog("Import",self.window,gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) #File filters. filter = gtk.FileFilter() filter.set_name("CMML") filter.add_pattern("*.cmml") dialog.add_filter(filter) filter = gtk.FileFilter() filter.set_name("SMIL") filter.add_pattern("*.smil") dialog.add_filter(filter) response = dialog.run() filename = None if response == gtk.RESPONSE_OK: filename = dialog.get_filename() format = dialog.get_filter() dialog.destroy() if filename != None: self.progress.props.text = "Importing " + filename self.start_time.set_text("xx:xx:xx.xxx") self.end_time.set_text("xx:xx:xx.xxx") self.scriptview.clear() if format.get_name() == "CMML": self.import_cmml(filename) elif format.get_name() == "SMIL": self.import_smil(filename) def save_file(self, accel_group, window, keyval, modifier): if self.show_keystrokes: print "Ctrl - S" dialog = gtk.FileChooserDialog("Save",self.window,gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) dialog.set_current_name(self.current_file) #File filters. filter = gtk.FileFilter() filter.set_name("SRT") filter.add_pattern("*.srt") dialog.add_filter(filter) filter = gtk.FileFilter() filter.set_name("CMML") filter.add_pattern("*.cmml") dialog.add_filter(filter) filter = gtk.FileFilter() filter.set_name("TXT") filter.add_pattern("*.txt") dialog.add_filter(filter) response = dialog.run() if response == gtk.RESPONSE_OK: filename = dialog.get_filename() format = dialog.get_filter() if format.get_name() == "SRT": data = self.scriptview.export_buffer() self.export_srt(filename,data) elif format.get_name() == "CMML": data = self.scriptview.export_buffer() self.export_cmml(filename,data) elif format.get_name() == "TXT": data = self.scriptview.get_all_text() self.export_txt(filename,data) dialog.destroy() def next_track_cb(self, accel_group, window, keyval, modifier): if self.show_keystrokes: print "Ctrl - T" new_track = self.scriptview.next_track() data = self.scriptview.export_buffer() self.textsource.set_cliplist(data[new_track]) # Navigation functions. def restart_clip(self, accel_group, window, keyval, modifier): if self.show_keystrokes: print "Ctrl - R" self.seek(to_ms(self.start_time.get_text())) def previous_clip(self, accel_group, window, keyval, modifier): if self.show_keystrokes: print "Ctrl - B" previous = self.scriptview.previous(self.pipeline.query_position(gst.FORMAT_TIME)[0]/gst.MSECOND) if previous: self.seek(previous.get_start()) self.scriptview.highlight(previous) self.mode = self.MODE_REVIEW self.start_time.set_text(format_time(previous.get_start())) self.end_time.set_text(format_time(previous.get_end())) if previous.get_text()!=None: self.clip_text.set_text(previous.get_text()) else: self.clip_text.set_text("") self.progress.props.fraction = float(previous.get_start())/self.pipeline_duration else: print "No previous clips in the current track." def next_clip(self, accel_group, window, keyval, modifier): if self.show_keystrokes: print "Ctrl - N" next = self.scriptview.next(self.pipeline.query_position(gst.FORMAT_TIME)[0]/gst.MSECOND) if next: self.seek(next.get_start()) self.scriptview.highlight(next) self.progress.props.fraction = float(next.get_start())/self.pipeline_duration self.start_time.set_text(format_time(next.get_start())) self.end_time.set_text(format_time(next.get_end())) self.clip_text.set_text(next.get_text()) elif self.end_time.get_text()!="xx:xx:xx.xxx": self.seek(to_ms(self.end_time.get_text())) self.mode = self.MODE_TRANSCRIBE def just_play(self, accel_group, window, keyval, modifier): if self.show_keystrokes: print "Ctrl - P" if (self.pipeline.get_state()[1] != gst.STATE_PLAYING): self.pipeline.set_state(gst.STATE_PLAYING); self.progress.props.text = self.current_file + ": Playing." def seek(self,start,end=None): if start==end: if self.debug: print "Error: Trying to seek to two identical times at " + start + "!" return elif end==None: result = self.pipeline.seek(1.0, gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE, gst.SEEK_TYPE_SET, gst.MSECOND*start, gst.SEEK_TYPE_SET, self.pipeline_duration*gst.MSECOND) else: result = self.pipeline.seek(1.0, gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE, gst.SEEK_TYPE_SET, gst.MSECOND*start, gst.SEEK_TYPE_SET, gst.MSECOND*end) #Push relevant text to overlay. return result def push_text_cb(self, accel_group, window, keyval, modifier): self.push_text() def push_text(self): data = self.scriptview.export_buffer() self.textsource.set_cliplist(data[data.keys()[0]]) def enter_activated(self, accel_group, window, keyval, modifier): '''Callback which handles actions upon enter activation.''' # Output key. if self.show_keystrokes: print "Enter" # Current time. current_time = self.pipeline.query_position(gst.FORMAT_TIME)[0]/gst.MSECOND # If going from playing to paused. if (self.pipeline.get_state()[1] == gst.STATE_PLAYING): self.pipeline.set_state(gst.STATE_PAUSED); # Update time. if self.mode==self.MODE_TRANSCRIBE: self.end_time.set_text(format_time(current_time)) # Update all fields. elif self.mode==self.MODE_REVIEW: current = self.scriptview.current_clip(current_time) if current!=-2 and current!=None: self.scriptview.highlight(current) self.start_time.set_text(format_time(current.get_start())) self.end_time.set_text(format_time(current.get_end())) self.clip_text.set_text(current.get_text()) else: self.mode=self.MODE_TRANSCRIBE self.start_time.set_text(format_time(current_time)) self.clip_text.set_text("") # Update progress bar. self.progress.props.text = self.current_file + ": Paused." if self.pipeline_duration==1000: self.pipeline_duration = self.pipeline.query_duration(gst.FORMAT_TIME)[0]/gst.MSECOND self.progress.props.fraction = float(current_time)/self.pipeline_duration # If going from paused to play elif self.pipeline.get_state()[1] == gst.STATE_PAUSED: # If transcribing. if self.mode==self.MODE_TRANSCRIBE: text = self.clip_text.get_text() if text!='': # Check if entry is chapter. if text[:1] == ")": track = 'default' text = text[1:] else: track = None # Check for possible mistype of ; in place of : if track==None and text.find(":")==-1 and text.find(";")!=-1: dialog = gtk.Dialog("Did you mean...", self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_NO, gtk.RESPONSE_NO, gtk.STOCK_YES, gtk.RESPONSE_YES)) label = gtk.Label("It looks like you may have intended to have a new speaker but mistakenly typed a semi-colon (;) instead of a colon (:). Would you like to change the semi-colon to a colon in, '"+text+"'?") label.set_line_wrap(True) dialog.vbox.pack_start(label, True, False, 0) dialog.set_default_response(gtk.RESPONSE_YES) label.show() if dialog.run() == gtk.RESPONSE_YES: text = text.replace(";", ":", 1) dialog.destroy() # Check if entry is dialog. if text.find(":")!=-1: speaker = text[:text.find(":")] text = text[text.find(":")+1:] loc = locale.getlocale(locale.LC_ALL)[0] if loc==None: loc="en_US" track = "subtitle-" + loc else: speaker = "" self.scriptview.append(clip.clip(to_ms(self.start_time.get_text()),to_ms(self.end_time.get_text()),text.strip(),track,speaker)) self.clip_text.set_text("") self.start_time.set_text(format_time(self.pipeline.query_position(gst.FORMAT_TIME)[0]/gst.MSECOND)) self.end_time.set_text("xx:xx:xx.xxx") self.scriptview.scroll_to_bottom() elif self.mode==self.MODE_REVIEW: pass #FIXME this is where to put clip update code. self.pipeline.set_state(gst.STATE_PLAYING); self.progress.props.text = self.current_file + ": Playing." return def export_txt(self,filename,data): #Check for valid filename. if filename[filename.rfind("."):].lower()!=".txt": #not correct extension/no extension if filename.rfind(".")>len(filename)-6: #incorrect extension removed filename = filename.rsplit(".",1)[0] filename = filename + ".txt" f = open(filename,"w") f.write(data) f.close() def export_srt(self,filename,data): #Check for valid filename. tracks = "subtitle-en" if filename[filename.rfind("."):].lower()!=".srt": #not correct extension/no extension if filename.rfind(".")>len(filename)-6: #incorrect extension removed filename = filename.rsplit(".",1)[0] tracks = "all" for track in data.keys(): if track == tracks or tracks == "all": x = 1 file_buffer = '' for clip in data[track]: file_buffer += str(x) + "\n" + format_time(clip.get_start()).replace(".",",") + " --> " + format_time(clip.get_end()).replace(".",",") + "\n" + clip.get_text() + "\n\n" x += 1 if tracks!="all": f = open(filename,"w") f.write(file_buffer) f.close() else: f = open(filename + "_" + track + ".srt","w") f.write(file_buffer) f.close() def export_cmml(self,filename,data): # generate header. cmml_file = '\n\n\n\n' if (self.current_file.find('/') != -1): stream_id = self.current_file.rsplit("/",1)[1].rsplit(".",1)[0] else: stream_id = self.current_file.rsplit(".",1)[0] stream_type = mimetypes.guess_type(self.current_file)[0] cmml_file += '\n\t\n\t\n\n\n' % (stream_id,stream_type,self.current_file) # Add each clip. x = 1 for track in data.keys(): for clip in data[track]: if track.startswith("subtitle"): cmml_file += '\n' % (x,clip.get_speaker().capitalize(),format_time(clip.get_start()),format_time(clip.get_end()),clip.get_track()) else: cmml_file += '\n' % (x,format_time(clip.get_start()),format_time(clip.get_end())) cmml_file += '\t%s\n' % (clip.get_text()) cmml_file += '\n\n' x += 1 cmml_file += '' if filename[filename.rfind("."):].lower()!=".cmml": #not correct extension/no extension if filename.rfind(".")>len(filename)-6: #incorrect extension removed filename = filename.rsplit(".",1)[0] filename = filename + ".cmml" f = open(filename,"w") f.write(cmml_file) f.close() def import_cmml(self, filename): import xml.dom.minidom f = open(filename,"r") doc = xml.dom.minidom.parse(f) file_imports = doc.getElementsByTagName('import') self.pipeline.set_state(gst.STATE_NULL) for node in file_imports: self.current_file = node.getAttribute('src') self.playbin.props.uri = "file://" + self.current_file self.pipeline_duration = 3600*65*100000000 #Only used in the initial text push. clips = doc.getElementsByTagName('clip') self.progress.props.fraction = 0 total_clips = len(clips) x=0 for node in clips: # load data start_time = node.getAttribute('start') end_time = node.getAttribute('end') track = node.getAttribute('track') title = node.getAttribute('title') #don't force child nodes :-) if node.getElementsByTagName('desc')[0].firstChild != None: desc = node.getElementsByTagName('desc')[0].firstChild.nodeValue self.scriptview.append(clip.clip(to_ms(start_time),to_ms(end_time),desc,track,title)) x += 1 self.progress.props.fraction = x/total_clips self.push_text() self.pipeline.set_state(gst.STATE_PAUSED); self.pipeline.get_state() # Let pipeline catch up to script. self.pipeline_duration = self.pipeline.query_duration(gst.FORMAT_TIME)[0]/gst.MSECOND if (end_time!="" and to_ms(end_time) < self.pipeline_duration-3*60*1000) or to_ms(start_time) < self.pipeline_duration-4*60*1000: #If last clip is towards end, skip and play from the beginning. If not go to the last clip. if end_time!="": self.seek(to_ms(end_time)) self.start_time.set_text(end_time) else: self.seek(to_ms(start_time)) self.start_time.set_text(start_time) self.mode = self.MODE_TRANSCRIBE else: self.mode = self.MODE_REVIEW f.close() return def import_smil(self,filename): import xml.dom.minidom f = open(filename,"r") doc = xml.dom.minidom.parse(f) clips = doc.getElementsByTagName('video') self.progress.props.fraction = 0 total_clips = len(clips) last_time=None x=0 for node in clips: # load data start_time = format_time(int(node.getAttribute('clipBegin'))/self.FRAME_RATE*1000) end_time = format_time(int(node.getAttribute('clipEnd'))/self.FRAME_RATE*1000) self.scriptview.append(clip.clip(to_ms(start_time),to_ms(end_time),"/placeholder/","default")) x += 1 self.progress.props.fraction = x/total_clips self.current_track = "default" self.mode = self.MODE_REVIEW f.close() return def bus_call(self, bus, message): if message.type == gst.MESSAGE_ERROR: print message.parse_error()[1] return True #Below are functions generally called once. def __init__(self): # Setup the program. The gui and gstreamer pipeline. self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.connect("delete_event", self.delete_event) self.window.connect("destroy", self.destroy) self.window.set_title(self.TITLE) self.window.set_default_size(500,300) self.vbox = gtk.VBox() #Window accelerators/hotkeys. self.accels = gtk.AccelGroup() self.accels.connect_group(gtk.gdk.keyval_from_name("o"),gtk.gdk.CONTROL_MASK,gtk.ACCEL_VISIBLE,self.open_file) # ctrl - o self.accels.connect_group(gtk.gdk.keyval_from_name("s"),gtk.gdk.CONTROL_MASK,gtk.ACCEL_VISIBLE,self.save_file) # ctrl - s self.accels.connect_group(gtk.gdk.keyval_from_name("t"),gtk.gdk.CONTROL_MASK,gtk.ACCEL_VISIBLE,self.next_track_cb) # ctrl - t self.accels.connect_group(gtk.gdk.keyval_from_name("b"),gtk.gdk.CONTROL_MASK,gtk.ACCEL_VISIBLE,self.previous_clip) # ctrl - b self.accels.connect_group(gtk.gdk.keyval_from_name("n"),gtk.gdk.CONTROL_MASK,gtk.ACCEL_VISIBLE,self.next_clip) # ctrl - n self.accels.connect_group(gtk.gdk.keyval_from_name("r"),gtk.gdk.CONTROL_MASK,gtk.ACCEL_VISIBLE,self.restart_clip) # ctrl - r self.accels.connect_group(gtk.gdk.keyval_from_name("p"),gtk.gdk.CONTROL_MASK,gtk.ACCEL_VISIBLE,self.just_play) # ctrl - p self.accels.connect_group(gtk.gdk.keyval_from_name("i"),gtk.gdk.CONTROL_MASK,gtk.ACCEL_VISIBLE,self.import_file) # ctrl - i self.accels.connect_group(gtk.gdk.keyval_from_name("w"),gtk.gdk.CONTROL_MASK,gtk.ACCEL_VISIBLE,self.push_text_cb) # ctrl - w self.accels.connect_group(gtk.gdk.keyval_from_name("Return"),0,gtk.ACCEL_VISIBLE,self.enter_activated) # enter self.window.add_accel_group(self.accels) self.sw = gtk.ScrolledWindow() self.sw.set_policy(gtk.POLICY_NEVER,gtk.POLICY_AUTOMATIC) self.sw.set_shadow_type(gtk.SHADOW_IN) self.scriptview = ScriptView(self.INSTRUCTIONS) self.sw.add(self.scriptview) self.hbox = gtk.HBox() self.start_time = gtk.Entry() self.start_time.set_width_chars(12) self.start_time.set_text("xx:xx:xx.xxx") self.start_time.set_max_length(12) self.start_time.set_size_request(92,-1) self.start_time.props.can_focus = False self.end_time = gtk.Entry() self.end_time.set_width_chars(12) self.end_time.set_text("xx:xx:xx.xxx") self.end_time.set_max_length(12) self.end_time.set_size_request(92,-1) self.end_time.props.can_focus = False self.progress = gtk.ProgressBar() self.progress.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT) self.progress.set_size_request(280, 20) self.progress.props.text = "Nothing loaded." self.progress.set_ellipsize(pango.ELLIPSIZE_END) self.clip_text = gtk.Entry() self.hbox.pack_start(self.start_time, False, False) self.hbox.pack_start(self.end_time, False, False) self.hbox.pack_start(self.clip_text, True, True) self.hbox.set_spacing(6) self.vbox.pack_start(self.sw,True,True) self.vbox.pack_start(self.hbox,False,False) self.vbox.pack_start(self.progress,False,False) self.vbox.set_spacing(6) self.window.add(self.vbox) self.window.set_border_width(6) #gstreamer setup self.use_overlays = True self.blue_easter_egg = False self.pipeline = gst.Pipeline() self.playbin = gst.element_factory_make ("playbin") self.playbin.props.audio_sink = gst.element_factory_make ("alsasink") if self.use_overlays: self.video_process = gst.Bin() self.timeoverlay = gst.element_factory_make ("timeoverlay") self.textoverlay = gst.element_factory_make ("textoverlay") self.xvimagesink = gst.element_factory_make ("xvimagesink") if self.blue_easter_egg: self.effect = gst.element_factory_make ("agingtv") self.colorspace = gst.element_factory_make("ffmpegcolorspace"); self.colorspace2 = gst.element_factory_make("ffmpegcolorspace"); self.textsource = TextSource() self.video_process.add (self.timeoverlay,self.textoverlay,self.xvimagesink,self.textsource) self.timeoverlay.link(self.textoverlay) self.textsource.get_pad("src").link(self.textoverlay.get_pad ("text_sink")) if self.blue_easter_egg: self.video_process.add(self.effect,self.colorspace,self.colorspace2) self.textoverlay.link(self.colorspace) self.colorspace.link(self.effect) self.effect.link(self.colorspace2) self.colorspace2.link (self.xvimagesink) else: self.textoverlay.link(self.xvimagesink) self.video_process.add_pad (gst.GhostPad ("sink",self.timeoverlay.get_pad ("video_sink"))) self.playbin.props.video_sink = self.video_process else: self.playbin.props.video_sink = gst.element_factory_make ("cacasink") self.playbin.props.vis_plugin = gst.element_factory_make ("goom") self.pipeline.add (self.playbin) bus = self.pipeline.get_bus() bus.add_watch (self.bus_call) self.pipeline_duration = 1000 #Something non-zero to avoid non-zero errors. self.mode = self.MODE_REVIEW self.show_keystrokes = False self.debug = False self.window.show_all() self.clip_text.grab_focus() def main(self): if len(sys.argv) > 1: self.pipeline.set_state(gst.STATE_NULL) self.current_file = sys.argv[1] self.playbin.props.uri = "file://" + self.current_file self.pipeline.set_state(gst.STATE_PAUSED); self.pipeline_duration = 10 gtk.main() if __name__ == "__main__": keystroke = Keystroke() keystroke.main()