Forked from
Silas S. Brown / gradint
407 commits behind the upstream repository.
-
Silas S. Brown authored
git-svn-id: http://svn.code.sf.net/p/e-guidedog/code/ssb22/gradint@997 29193198-4895-4776-b068-10539e920549
Silas S. Brown authoredgit-svn-id: http://svn.code.sf.net/p/e-guidedog/code/ssb22/gradint@997 29193198-4895-4776-b068-10539e920549
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
booktime.py 17.92 KiB
# This file is part of the source code of
# gradint v0.9966 (c) 2002-2011 Silas S. Brown. GPL v3+.
# 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 3 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.
# Start of booktime.py - handle booking events into the lesson
# Bin-packing algorithm is a bit primitive but it
# should do
# Event - a single event, such as a Chinese phrase or an
# English question - has a length (including any reasonable
# pause after it). ALSO 'glue' between events (e.g. a
# graduated interval) that is flexible and that can be
# interspersed with other events. (A sequence of
# repetitions should START with glue.)
def initialGlue(): return Glue(0,maxLenOfLesson)
try: import bisect
except:
class bisect: pass
bisect=bisect()
def insort(l,item):
l.append(item) ; l.sort()
bisect.insort = insort
class Schedule(object):
# A sorted list of (start,finish) times that are booked
def __init__(self): self.bookedList = []
def book(self,start,finish): bisect.insort(self.bookedList,(start,finish))
earliestAllowedEvent = 0 # for "don't start before" hacks, so can keep all initial glue starting at 0
class GlueOrEvent(object):
# 'invisible' is non-0 if this is "glue" and other
# events can take place at the same time.
def __init__(self,length=0,plusMinus=0,invisible=0):
self.length = length
self.plusMinus = plusMinus
self.invisible = invisible
# assert invisible or length > 0, "Length is %s" % (length,) # no longer valid now synth.py can generate a 0-length final event
def makesSenseToLog(self): return 0
def bookIn(self,schedule,start):
if not self.invisible:
schedule.book(start,start+self.length)
def addToEvents(self,events,startTime):
assert not self.invisible
events.append((startTime,self))
def overlaps(self,start,schedule,direction):
# Returns how much it has to move, given 'start'
# direction is +1 or -1 - which way to move
# (return value is always unsigned)
if self.invisible: return 0 # never has to move
if not schedule.bookedList: return 0 # assume earliestAllowedEvent does not apply when schedule is empty
oldStart = start
if direction==1: # moving forwards
start = max(start,earliestAllowedEvent)
count = 0 ; blLen = len(schedule.bookedList)
while count<blLen and schedule.bookedList[count][1] <= start: # finishes before or as we start - irrelevant
count += 1
while count<blLen and schedule.bookedList[count][0]<start+self.length: # starts before we finish
start = schedule.bookedList[count][1]
count += 1
return start-oldStart
else: # moving backwards
if start<earliestAllowedEvent: return oldStart+1 # hack: force being placed before the beginning of the lesson, which should result in rejecting it
count = len(schedule.bookedList)-1
finish=start+self.length
while count>=0 and schedule.bookedList[count][0] >= finish: # starts after or as we finish - irrelevant
count -= 1
while count>=0 and schedule.bookedList[count][1]>start: # finishes after we start
start = schedule.bookedList[count][0]-self.length
count -= 1
return oldStart-start
def will_be_played(self): pass
def play(self): pass
def setOnLeaves(self,name,value):
# Set some value that should propagate to the "leaves" (only) of the event tree
# i.e. to the events that matter to addToEvents.
# (and only if they don't already have such an attribute, so we can put the exceptions first.)
if not hasattr(self,name): exec('self.'+name+'='+repr(value))
def setOnLastLeaf(self,name,value): self.setOnLeaves(name,value)
class Event (GlueOrEvent):
def __init__(self,length):
GlueOrEvent.__init__(self,length)
def play(self):
# Playing a "silence event". Normally don't need to do anything.
# However, if playing in real time and the scheduler is behind, then
# this delay is probably important - so have at least most of it.
# AND if not real-time then we DON'T want to beep during this silence
# (long-phrase/pause/long-answer is different from pause between sequences)
if soundCollector: soundCollector.addSilence(self.length*0.7,False)
else: mysleep(min(3,self.length*0.7))
class CompositeEvent (Event):
# An event made up of several others in sequence with
# nothing intervening
def __init__(self,eventList):
length = 0
for i in eventList: length += i.length
Event.__init__(self,length)
self.eventList = eventList
def addToEvents(self,events,startTime):
for i in self.eventList:
i.addToEvents(events,startTime)
startTime = startTime + i.length
def play(self): # not normally called, except perhaps by partials, so don't worry about gap-events etc in eventList
for e in self.eventList: e.play()
def setOnLeaves(self,name,value):
for e in self.eventList: e.setOnLeaves(name,value)
def setOnLastLeaf(self,name,value): self.eventList[-1].setOnLastLeaf(name,value)
def makesSenseToLog(self):
if hasattr(self,"is_prompt"): return not self.is_prompt
for e in self.eventList:
if e.makesSenseToLog(): return True
def __repr__(self): return "{"+(" ".join([str(e) for e in self.eventList]))+"}"
class Glue (GlueOrEvent):
def __init__(self,length,plusMinus):
GlueOrEvent.__init__(self,length,plusMinus,1)
def sgn(a):
# Not all versions of Python have this built-in
if a: return a/abs(a)
else: return 1
class StretchedTooFar(Exception): pass
class GluedEvent(object):
# Some glue before an event
def __init__(self,glue,event):
self.glue = glue
self.event = event
self.glue.adjustment = 0
self.glue.preAdjustment = None
def randomPreAdjustment(self):
if self.glue.length < randomAdjustmentThreshold: self.glue.preAdjustment = 0
elif is_first_lesson: # hack - bias against making the silences TOO long
self.glue.preAdjustment = random.gauss(0,self.glue.plusMinus)
if self.glue.preAdjustment<0: self.glue.preAdjustment = - self.glue.preAdjustment
if self.glue.preAdjustment > self.glue.plusMinus: self.glue.preAdjustment = 0
self.glue.preAdjustment -= self.glue.plusMinus
else:
self.glue.preAdjustment = random.gauss(0,self.glue.plusMinus)
if abs(self.glue.preAdjustment) > self.glue.plusMinus:
self.glue.preAdjustment = self.glue.plusMinus # err on the +ve side
def adjustGlue(self,glueStart,schedule,direction):
# How far does it need to move?
# Use preAdjustment, not adjustment, in case
# re-adjusting due to a change of direction
needMove = self.event.overlaps(glueStart+self.glue.length+self.glue.preAdjustment,schedule,direction)
# needMove*direction now gives delta from the
# pre-adjusted glue end; need to add preAdjustment
# to get delta from the unadjusted
needMove=needMove*direction+self.glue.preAdjustment
direction=sgn(needMove) ; needMove=abs(needMove)
if needMove > self.glue.plusMinus \
or (direction<0 and needMove > self.glue.length)\
or glueStart+self.glue.length+needMove*direction+self.event.length > maxLenOfLesson:
raise StretchedTooFar()
self.glue.adjustment = needMove * direction
def getAdjustedEnd(self,glueStart):
return glueStart+self.glue.length+self.glue.adjustment+self.event.length
def bookIn(self,schedule,glueStart):
self.event.bookIn(schedule,glueStart+self.glue.length+self.glue.adjustment)
def getEventStart(self,glueStart):
return glueStart+self.glue.length+self.glue.adjustment
def setOnLeaves(self,name,value): self.event.setOnLeaves(name,value)
def setOnLastLeaf(self,name,value): self.event.setOnLastLeaf(name,value)
def setGlue(gluedEventList, schedule, glueStart = 0):
# Uses tail recursion / backtracking with exceptions
# (Note: can throw an exception to outermost level if
# there is no solution)
if not gluedEventList: return
try:
if gluedEventList[0].glue.preAdjustment==None: gluedEventList[0].randomPreAdjustment() # (Do NOT re-do on backtrack: should find a fit regardless of the pre-adjustment)
gluedEventList[0].adjustGlue(glueStart,schedule,1)
setGlue(gluedEventList[1:],schedule,gluedEventList[0].getAdjustedEnd(glueStart))
except StretchedTooFar:
if glueStart==0: raise StretchedTooFar # don't even try pushing it the other way (NB randomPreAdjustment will be 0 for the initial glue)
gluedEventList[0].adjustGlue(glueStart,schedule,-1)
setGlue(gluedEventList[1:],schedule,gluedEventList[0].getAdjustedEnd(glueStart))
def setGlue_wrapper(gluedEventList, schedule):
# Normally setGlue will do "first fit" and will throw
# an exception if the first fit can't possibly work with
# the REST of the sequence. Really want a good algorithm
# that sorts it out properly, but meanwhile we can try
# just a bit harder to fit it in if first-fit fails.
# gluedEventList[0].glue.length should be 0 but we can increase it here (only).
if len(gluedEventList)==1: return setGlue(gluedEventList,schedule) # no point doing the stuff below on length-1 lists
worked = 0
while (not worked) and gluedEventList[0].glue.length < maxLenOfLesson:
try:
setGlue(gluedEventList, schedule)
worked = 1
except StretchedTooFar:
# try harder
gluedEventList[0].glue.length += (10+gluedEventList[0].glue.adjustment) # (NB preAdjustment will be 0, so don't have to worry about that)
# (TODO the 10 should be a constant)
if not worked: raise StretchedTooFar()
def bookIn(gluedEventList,schedule):
setGlue_wrapper(gluedEventList,schedule)
glueStart = 0
for i in gluedEventList:
i.bookIn(schedule,glueStart)
glueStart = i.getAdjustedEnd(glueStart)
gluedListTracker=None # set to [] if want to track them (used in utils/diagram.py)
class Lesson(object):
def __init__(self):
self.schedule = Schedule()
self.events = [] # list of (time,event)
self.newWords = self.oldWords = 0
self.eventListCounter = 0
if startAnnouncement:
w = fileToEvent(startAnnouncement,"")
w.bookIn(self.schedule,0)
w.addToEvents(self.events,0)
if endAnnouncement:
w = fileToEvent(endAnnouncement,"")
wStart = maxLenOfLesson-w.length
w.bookIn(self.schedule,wStart)
w.addToEvents(self.events,wStart)
def message(self):
t,event = max(self.events)
finish = int(0.5+t+event.length)
teacher_extra = ""
if teacherMode:
self.events.sort()
for t,event in self.events:
if event.makesSenseToLog():
teacher_extra="\nFirst word will be "+maybe_unicode(str(event))
break
l=cond(app,localise,lambda x:x)
if self.oldWords or teacherMode:
return l("Today's lesson teaches %d new words\nand revises %d old words\n\nPlaying time: %d %s %d %s") % (self.newWords,self.oldWords,finish/60,singular(finish/60,"minutes"),finish%60,singular(finish%60,"seconds"))+teacher_extra
else:
# less confusing message for a beginner
return l("Today we will learn %d words\nThis will require %d %s %d %s\nFollow the spoken instructions carefully") % (self.newWords,finish/60,singular(finish/60,"minutes"),finish%60,singular(finish%60,"seconds"))
def addSequence(self,gluedEventList):
bookIn(gluedEventList,self.schedule)
if not gluedListTracker==None: gluedListTracker.append(gluedEventList)
glueStart = 0 ; lastI = None
for i in gluedEventList:
i.event.setOnLeaves("sequenceID",self.eventListCounter) # for max_lateness stuff
i.event.setOnLeaves("importance",len(gluedEventList)) # ditto
startTime = i.getEventStart(glueStart)
i.event.addToEvents(self.events,startTime)
glueStart = i.getAdjustedEnd(glueStart)
if lastI: lastI.event.setOnLeaves("max_lateness",max(1,10/len(gluedEventList))+min(lastI.glue.plusMinus-lastI.glue.adjustment,i.glue.plusMinus+i.glue.adjustment))
# (Note that this setting of max_lateness is not particularly clever - it can detect if a sequence's existing scheduling has been pushed beyond its limits, but it can't dynamically re-schedule the sequence as a whole when that happens. Hopefully people's emergency interruptions won't be too long.)
lastI = i
if lastI:
lastI.event.setOnLeaves("max_lateness",max(1,10/len(gluedEventList))+lastI.glue.plusMinus-lastI.glue.adjustment)
if hasattr(gluedEventList[0],"timesDone"): lastI.event.setOnLastLeaf("endseq",not gluedEventList[0].timesDone)
self.eventListCounter += 1
def cap_max_lateness(self):
# if an event of importance I has a max lateness of M, then all previous events with importance <I have to cap their max lateness to M+(intervening gaps) so as not to make it late.
# (sorts events as a side-effect)
self.events.sort() ; self.events.reverse()
latenessCap = {} ; nextStart = 0
for t,event in self.events:
if nextStart:
for k in latenessCap.keys(): latenessCap[k] += (nextStart-(t+event.length)) # the gap
nextStart = t
if not hasattr(event,"importance"): continue # (wasn't added via addSequence, probably not a normal lesson)
event.max_lateness=min(event.max_lateness,latenessCap.get(event.importance,maxLenOfLesson))
for i in range(event.importance): latenessCap[i]=min(latenessCap.get(i,maxLenOfLesson),event.max_lateness)
del event.importance # (might as well save some filespace in saveLesson while we're here)
self.events.reverse() # so sorted again
def play(self):
if (synthCache_test_mode or synthCache_test_mode==[]) and not hasattr(self,"doneSubst"):
subst_some_synth_for_synthcache(self.events)
self.doneSubst=1
global runner, finishTime, lessonLen, wordsLeft
wordsLeft={False:self.oldWords,True:self.newWords}
initLogFile()
for (t,event) in self.events: event.will_be_played()
if soundCollector:
for synth in viable_synths: synth.finish_makefile() # TODO: might want to do that if realtime also, but on most reasonably-specified machines it should be OK to start playing before everything has been synth'd (see also play method in synth.py which makes allowance for slow start)
finishTime = None # unspecified
if self.events:
lessonLen = self.events[-1][0]+self.events[-1][1].length
if lessonLen>60: # otherwise probably a small justSynthesize job - don't clutter the terminal with progress
finishTime = int(0.5+(time.time() + lessonLen))
if (riscos_sound or winCEsound) and not app and not soundCollector: show_info("Started at %s, will finish at %s\n" % (time.strftime("%H:%M",time.localtime(time.time())),time.strftime("%H:%M",time.localtime(finishTime))))
global sequenceIDs_to_cancel ; sequenceIDs_to_cancel = {}
global copy_of_runner_events ; copy_of_runner_events = []
global lessonStartTime ; lessonStartTime = 0 # will be set to time.time() on 1st event
# make the runner as late as possible
if soundCollector: runner = sched.scheduler(collector_time,collector_sleep)
else: runner = sched.scheduler(time.time,mysleep)
for (t,event) in self.events: copy_of_runner_events.append((event,runner.enter(t,1,play,(event,)),t))
# TODO what if Brief Interrupt appears during that events loop and someone presses it (will act as a Cancel and go back to main)
try: runner.run()
except KeyboardInterrupt: handleInterrupt()
runner = None
if soundCollector: soundCollector.finished()
if logFileHandle: logFileHandle.close()
subst_synth_counters = {} # global so it carries over when using justSynthesize in repeat mode
def decide_subst_synth(cache_fname):
subst_synth_counters[cache_fname] = subst_synth_counters.get(cache_fname,0)+1
return subst_synth_counters[cache_fname] in [2,4] or (subst_synth_counters[cache_fname]>5 and random.choice([1,2])==1)
def subst_some_synth_for_synthcache(events):
# turn SOME synthcache events back into synth events (for testing new synths etc)
reverse_transTbl = {}
for k,v in synthCache_transtbl.items(): reverse_transTbl[v]=k
for i in range(len(events)):
if hasattr(events[i][1],"file") and events[i][1].file.startswith(synthCache+os.sep):
cache_fname = events[i][1].file[len(synthCache+os.sep):]
cache_fname = reverse_transTbl.get(cache_fname,cache_fname)
if cache_fname[0]=="_": continue # a sporadically-used synthCache entry anyway
if type(synthCache_test_mode)==type([]):
found=0
for str in synthCache_test_mode:
if (re and re.search(str,cache_fname)) or cache_fname.find(str)>-1:
found=1 ; break
if found: continue
lang = languageof(cache_fname)
if get_synth_if_possible(lang) and decide_subst_synth(cache_fname): events[i] = (events[i][0],synth_event(lang,cache_fname[:cache_fname.rindex("_")]))