Newer
Older
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# 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)
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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)
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
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()
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}
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
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("_")]))