FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • ssb22/gradint
  • st822/gradint
2 results
Show changes
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# (either Python 2 or Python 3)
program_name = "gradint.cgi v1.06 (c) 2011 Silas S. Brown. GPL v3+"
program_name = "gradint.cgi v1.38 (c) 2011,2015,2017-25 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
......@@ -13,15 +14,37 @@ program_name = "gradint.cgi v1.06 (c) 2011 Silas S. Brown. GPL v3+"
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
gradint_dir = "/home/ssb22/gradint" # include samples/prompts
path_add = "/home/ssb22/gradint/bin" # include sox, lame, espeak, maybe oggenc
lib_path_add = "/home/ssb22/gradint/lib"
espeak_data_path = "/home/ssb22/gradint"
gradint_dir = "$HOME/gradint" # include samples/prompts
path_add = "$HOME/gradint/bin" # include sox, lame, espeak, maybe oggenc
lib_path_add = "$HOME/gradint/lib"
espeak_data_path = "$HOME/gradint"
import os, os.path, sys, commands, cgi, cgitb, urllib ; cgitb.enable()
import Cookie, random
import os, os.path, sys, cgi, urllib, time, re
import tempfile, getpass
myTmp = tempfile.gettempdir()+os.sep+getpass.getuser()+"-gradint-cgi"
try: from commands import getoutput # Python 2
except: from subprocess import getoutput # Python 3
try: from urllib import quote,quote_plus,unquote # Python 2
except: from urllib.parse import quote,quote_plus,unquote # Python 3
try: from importlib import reload # Python 3
except: pass
home = os.environ.get("HOME","")
if not home:
try:
import pwd
home = os.path.expanduser("~{0}".format(pwd.getpwuid(os.getuid())[0]))
except: home=0
if not home: home = ".." # assume we're in public_html
gradint_dir = gradint_dir.replace("$HOME",home)
path_add = path_add.replace("$HOME",home)
lib_path_add = lib_path_add.replace("$HOME",home)
espeak_data_path = espeak_data_path.replace("$HOME",home)
try: import Cookie # Python 2
except: import http.cookies as Cookie # Python 3
import random
if "QUERY_STRING" in os.environ and "&" in os.environ["QUERY_STRING"] and ";" in os.environ["QUERY_STRING"]: os.environ["QUERY_STRING"]=os.environ["QUERY_STRING"].replace(";","%3B") # for dictionary sites to add words that contain semicolon
query = cgi.parse()
try: query = cgi.FieldStorage(encoding="utf-8") # Python 3
except: query = cgi.FieldStorage() # Python 2
os.chdir(gradint_dir) ; sys.path.insert(0,os.getcwd())
os.environ["PATH"] = path_add+":"+os.environ["PATH"]
if "LD_LIBRARY_PATH" in os.environ: os.environ["LD_LIBRARY_PATH"] = lib_path_add+":"+os.environ["LD_LIBRARY_PATH"]
......@@ -29,60 +52,76 @@ else: os.environ["LD_LIBRARY_PATH"] = lib_path_add
os.environ["ESPEAK_DATA_PATH"] = espeak_data_path
cginame = os.sep+sys.argv[0] ; cginame=cginame[cginame.rindex(os.sep)+1:]
sys.stderr=open("/dev/null","w") ; sys.argv = [] ; import gradint
sys.stderr=open("/dev/null","w") ; sys.argv = []
gradint = None
def reinit_gradint(): # if calling again, also redo setup_userID after
global gradint,langFullName
if gradint:
if sys.version_info[0]>2: gradint.map,gradint.filter,gradint.chr=gradint._map,gradint._filter,gradint.unichr # undo Python 3 workaround in preparation for it to be done again, because reload doesn't do this (at least not on all Python versions)
gradint = reload(gradint)
else: import gradint
gradint.waitOnMessage = lambda *args:False
langFullName = {}
for l in gradint.ESpeakSynth().describe_supported_languages().split():
abbr,name = gradint.S(l).split("=")
langFullName[abbr]=name.replace("_","-")
# Try to work out probable default language:
lang = os.environ.get("HTTP_ACCEPT_LANGUAGE","")
if lang:
for c in [',',';','-']:
if c in lang: lang=lang[:lang.index(c)]
if not lang in langFullName: lang=""
global noGTranslate
if lang:
gradint.firstLanguage = lang
if lang=="en": noGTranslate = True
else:
gradint.secondLanguage="en" # (most probable default)
noGTranslate = lang in gradint.GUI_translations # (unless perhaps any are incomplete)
elif " zh-" in os.environ.get("HTTP_USER_AGENT",""): # Chinese iPhone w/out Accept-Language
gradint.firstLanguage,gradint.secondLanguage = "zh","en"
noGTranslate = True # (don't know if it even pops up on that browser, but anyway)
lDic = {}
for l in gradint.ESpeakSynth().describe_supported_languages().split():
abbr,name = l.split("=")
lDic[abbr]=name
# Try to work out probable default language:
lang = os.environ.get("HTTP_ACCEPT_LANGUAGE","")
if lang:
for c in [',',';','-']:
if c in lang: lang=lang[:lang.index(c)]
if not lang in lDic: lang=""
if lang:
gradint.firstLanguage = lang
if not lang=="en": gradint.secondLanguage="en"
elif " zh-" in os.environ.get("HTTP_USER_AGENT",""): gradint.firstLanguage,gradint.secondLanguage = "zh","en" # Chinese iPhone
reinit_gradint()
def main():
if "id" in query: # e.g. from redirectHomeKeepCookie
queryID = query.getfirst("id")
if not re.match("[A-Za-z0-9_.-]",queryID): return htmlOut("Bad query.  Bad, bad query.") # to avoid cluttering the disk if we're being given random queries by an attacker. IDs we generate are numeric only, but allow alphanumeric in case server admin wants to generate them. Don't allow =, parens, etc (likely random SQL query)
os.environ["HTTP_COOKIE"]="id="+queryID
print ('Set-Cookie: id=' + queryID+'; expires=Wed, 1 Dec 2036 23:59:59 GMT') # TODO: S2G
if has_userID(): setup_userID() # always, even for justSynth, as it may include a voice selection (TODO consequently being called twice in many circumstances, could make this more efficient)
if "id" in query:
os.environ["HTTP_COOKIE"]="id="+query["id"][0]
print 'Set-Cookie: id=' + query["id"][0]+'; expires=Wed, 1 Dec 2036 23:59:59 GMT'
filetype=""
if "filetype" in query: filetype=query["filetype"][0]
if "filetype" in query: filetype=query.getfirst("filetype")
if not filetype in ["mp3","ogg","wav"]: filetype="mp3"
for k in query.keys():
if k.startswith("del-"):
k=urllib.unquote(urllib.unquote(k)) # might be needed
k=unquote(unquote(k)) # might be needed
if '=' in k:
l2,l1 = k[4:].split('=')
setup_userID()
gradint.delOrReplace(gradint.ensure_unicode(l2),gradint.ensure_unicode(l1),"","","delete")
return listVocab(True)
if "js" in query: # just synthesize (js=text jsl=language)
if "jsl" in query: justSynth(query["js"][0], query["jsl"][0],filetype=filetype)
else: justSynth(query["js"][0],filetype=filetype)
if "jsl" in query: justSynth(query.getfirst("js"), query.getfirst("jsl"),filetype=filetype)
else: justSynth(query.getfirst("js"),filetype=filetype)
elif "spk" in query: # speak (l1,l2 the langs, l1w,l2w the words)
gradint.justSynthesize="0"
if "l2w" in query and query["l2w"][0]:
if "l2w" in query and query.getfirst("l2w"):
gradint.startBrowser=lambda *args:0
if query["l2"][0]=="zh" and gradint.sanityCheck(query["l2w"][0],"zh"): gradint.justSynthesize += "#en Pinyin needs tones. Please go back and add tone numbers." # speaking it because alert box might not work and we might be being called from HTML5 Audio stuff (TODO maybe duplicate sanityCheck in js, if so don't call HTML5 audio, then we can have an on-screen message here)
else: gradint.justSynthesize += "#"+query["l2"][0].replace("#","").replace('"','')+" "+query["l2w"][0].replace("#","").replace('"','')
if "l1w" in query and query["l1w"][0]: gradint.justSynthesize += "#"+query["l1"][0].replace("#","").replace('"','')+" "+query["l1w"][0].replace("#","").replace('"','')
if gradint.justSynthesize=="0": return htmlOut('You must type a word in the box before pressing the Speak button.'+backLink) # TODO maybe add a Javascript test to the form also, IF can figure out if window.alert works
if query.getfirst("l2")=="zh" and gradint.generalCheck(query.getfirst("l2w"),"zh"): gradint.justSynthesize += "#en Pinyin needs tones. Please go back and add tone numbers." # speaking it because alert box might not work and we might be being called from HTML5 Audio stuff (TODO maybe duplicate generalCheck in js, if so don't call HTML5 audio, then we can have an on-screen message here)
else: gradint.justSynthesize += "#"+query.getfirst("l2").replace("#","").replace('"','')+" "+query.getfirst("l2w").replace("#","").replace('"','')
if "l1w" in query and query.getfirst("l1w"): gradint.justSynthesize += "#"+query.getfirst("l1").replace("#","").replace('"','')+" "+query.getfirst("l1w").replace("#","").replace('"','')
if gradint.justSynthesize=="0": return htmlOut(withLocalise('You must type a word in the box before pressing the Speak button.')+backLink) # TODO maybe add a Javascript test to the form also, IF can figure out if window.alert works
serveAudio(stream = len(gradint.justSynthesize)>100, filetype=filetype)
elif "add" in query: # add to vocab (l1,l2 the langs, l1w,l2w the words)
if "l2w" in query and query["l2w"][0] and "l1w" in query and query["l1w"][0]:
if "l2w" in query and query.getfirst("l2w") and "l1w" in query and query.getfirst("l1w"):
gradint.startBrowser=lambda *args:0
if query["l2"][0]=="zh": scmsg=gradint.sanityCheck(query["l2w"][0],"zh")
else: scmsg=None
if scmsg: htmlOut(scmsg+''+backLink)
else: addWord(query["l1w"][0],query["l2w"][0],query["l1"][0],query["l2"][0])
else: htmlOut('You must type words in both boxes before pressing the Add button.'+backLink) # TODO maybe add a Javascript test to the form also, IF can figure out a way to tell whether window.alert() works or not
if query.getfirst("l2")=="zh": gcmsg=gradint.generalCheck(query.getfirst("l2w"),"zh")
else: gcmsg=None
if gcmsg: htmlOut(gradint.B(gcmsg)+gradint.B(backLink))
else: addWord(query.getfirst("l1w"),query.getfirst("l2w"),query.getfirst("l1"),query.getfirst("l2"))
else: htmlOut(withLocalise('You must type words in both boxes before pressing the Add button.')+backLink) # TODO maybe add a Javascript test to the form also, IF can figure out a way to tell whether window.alert() works or not
elif "bulkadd" in query: # bulk adding, from authoring options
dirID = setup_userID()
def isOK(x):
......@@ -94,13 +133,13 @@ def main():
def mycmp(x,y): return cmp(int(x[1:]),int(y[1:]))
keyList = sorted(filter(lambda x:isOK(x),query.keys()),mycmp)
for k in keyList:
l2w,l1w = query[k][0].split('=',1)
addWord(l1w,l2w,query["l1"][0],query["l2"][0],False)
l2w,l1w = query.getfirst(k).split('=',1)
addWord(l1w,l2w,query.getfirst("l1"),query.getfirst("l2"),False)
redirectHomeKeepCookie(dirID,"&dictionary=1") # '1' is special value for JS-only back link; don't try to link to referer as it might be a generated page
elif "clang" in query: # change languages (l1,l2)
dirID = setup_userID()
if (gradint.firstLanguage,gradint.secondLanguage) == (query["l1"][0],query["l2"][0]) and not query["clang"][0]=="ignore-unchanged": return htmlOut('You must change the settings before pressing the Change Languages button.'+backLink) # (external scripts can set clang=ignore-unchanged)
gradint.updateSettingsFile(gradint.settingsFile,{"firstLanguage": query["l1"][0],"secondLanguage":query["l2"][0]})
if (gradint.firstLanguage,gradint.secondLanguage) == (query.getfirst("l1"),query.getfirst("l2")) and not query.getfirst("clang")=="ignore-unchanged": return htmlOut(withLocalise('You must change the settings before pressing the Change Languages button.')+backLink) # (external scripts can set clang=ignore-unchanged)
gradint.updateSettingsFile(gradint.settingsFile,{"firstLanguage": query.getfirst("l1"),"secondLanguage":query.getfirst("l2")})
redirectHomeKeepCookie(dirID)
elif "swaplang" in query: # swap languages
dirID = setup_userID()
......@@ -108,7 +147,7 @@ def main():
redirectHomeKeepCookie(dirID)
elif "editsave" in query: # save 'vocab'
dirID = setup_userID()
if "vocab" in query: vocab=query["vocab"][0]
if "vocab" in query: vocab=query.getfirst("vocab")
else: vocab="" # user blanked it
open(gradint.vocabFile,"w").write(vocab)
redirectHomeKeepCookie(dirID)
......@@ -116,13 +155,25 @@ def main():
dirID = setup_userID()
try: v=open(gradint.vocabFile).read()
except: v="" # (shouldn't get here unless they hack URLs)
htmlOut('<form action="'+cginame+'" method="post"><textarea name="vocab" style="width:100%;height:80%" rows="15" cols="50">'+v+'</textarea><br><input type=submit name=editsave value="Save changes"> | <input type=submit name=dummy value="Cancel"></form>',"Text edit your vocab list")
elif "lesson" in query: # make lesson
htmlOut('<form action="'+cginame+'" method="post"><textarea name="vocab" style="width:100%;height:80%" rows="15" cols="50">'+v+'</textarea><br><input type=submit name=editsave value="Save changes"> | <input type=submit name=placeholder value="Cancel"></form>',"Text edit your vocab list")
elif "lesson" in query: # make lesson ("Start lesson" button)
setup_userID()
gradint.maxNewWords = int(query["new"][0]) # (shouldn't need sensible-range check here if got a dropdown; if they really want to hack the URL then ok...)
gradint.maxLenOfLesson = int(float(query["mins"][0])*60)
gradint.maxNewWords = int(query.getfirst("new")) # (shouldn't need sensible-range check here if got a dropdown; if they really want to hack the URL then ok...)
gradint.maxLenOfLesson = int(float(query.getfirst("mins"))*60)
# TODO save those settings for next time also?
serveAudio(stream = True, inURL = False, filetype=filetype)
elif "bigger" in query or "smaller" in query:
u = setup_userID() ; global zoom
if "bigger" in query: zoom = int(zoom*1.1)
else: zoom = int(zoom/1.1 + 0.5)
open(u+"-zoom.txt","w").write("%d\n" % zoom)
listVocab(True)
elif any("variant"+str(c) in query for c in range(max(len(gradint.GUI_translations[v]) for v in gradint.GUI_translations.keys() if v.startswith("@variants-")))):
for c in range(max(len(gradint.GUI_translations[v]) for v in gradint.GUI_translations.keys() if v.startswith("@variants-"))): #TODO duplicate code
if "variant"+str(c) in query: break
u = setup_userID()
gradint.updateSettingsFile(u+"-settings.txt",{"scriptVariants":{gradint.GUI_languages.get(gradint.firstLanguage,gradint.firstLanguage):c}})
setup_userID() ; listVocab(True)
elif "voNormal" in query: # voice option = normal
setup_userID()
gradint.voiceOption=""
......@@ -131,21 +182,32 @@ def main():
elif "vopt" in query: # set voice option
setup_userID()
for v in gradint.guiVoiceOptions:
if v.lower()=="-"+query["vopt"][0].lower():
if v.lower()=="-"+query.getfirst("vopt").lower():
gradint.voiceOption = v
gradint.updateSettingsFile(gradint.settingsFile,{"voiceOption":v})
break
listVocab(True)
elif "lFinish" in query:
dirID = setup_userID()
try: os.rename(gradint.progressFile+'-new',gradint.progressFile)
except: pass # probably a duplicate GET
try: os.remove(gradint.progressFile+'-ts') # the timestamp file
except: pass
redirectHomeKeepCookie(dirID)
elif not isAuthoringOption(query): listVocab(has_userID()) # default screen
def U(x):
try: return x.decode('utf-8')
except: return x
def isAuthoringOption(query):
# TODO document the ?author=1 option
if "author" in query:
htmlOut('<form action="'+cginame+'" method="post"><h2>Gradint word list authoring mode</h2>This can help you put word lists on your website. The words will be linked to this Gradint server so your visitors can choose which ones to hear and/or add to their personal lists.<p>Type any text in the box below; use blank lines to separate paragraphs. To embed a word list in your text, type:<br><em>phrase 1</em>=<em>meaning 1</em><br><em>phrase 2</em>=<em>meaning 2</em><br><em>phrase 3</em>=<em>meaning 3</em><br>etc, and <b>make sure there is a blank line before and after the list</b>. Then press <input type=submit name="generate" value="Generate HTML">.<p>Language for phrases: '+langSelect('l2',gradint.secondLanguage)+' and for meanings: '+langSelect('l1',gradint.firstLanguage)+'<p><textarea name="text" style="width:100%;height:80%" rows="15" cols="50"></textarea><br><input type=submit name="generate" value="Generate HTML"></form>',"Word list authoring",links=0)
# TODO maybe langSelect for mand+cant together ? (but many wordlists wld be topolect-specific)
elif "generate" in query:
l1,l2,txt = query["l1"][0],query["l2"][0],query["text"][0]
paras = "\n".join([l.strip() for l in txt.replace("\r\n","\n").replace("\r","\n").decode('utf-8').split("\n")]).split("\n\n")
l1,l2,txt = query.getfirst("l1"),query.getfirst("l2"),query.getfirst("text")
paras = "\n".join([l.strip() for l in U(txt).replace("\r\n","\n").replace("\r","\n").split("\n")]).split("\n\n")
need_h5a = False
for i in xrange(len(paras)):
lines = filter(lambda x:x,paras[i].split("\n")) # filter needed for trailing newline on document
......@@ -163,14 +225,16 @@ def allLinesHaveEquals(lines):
for l in lines:
if not '=' in l: return False
return True
gradintUrl = os.environ.get("SCRIPT_URI","") # will be http:// or https:// as appropriate
if not gradintUrl and all(x in os.environ for x in ["REQUEST_SCHEME","SERVER_NAME","SCRIPT_NAME"]): gradintUrl = os.environ["REQUEST_SCHEME"]+"://"+os.environ["SERVER_NAME"]+os.environ["SCRIPT_NAME"]
if not gradintUrl: gradintUrl = "gradint.cgi" # guessing
def authorWordList(lines,l1,l2):
gradintUrl = os.environ["SCRIPT_URI"]
r=[] ; count = 0
# could have target="gradint" in the following, but it may be in a background tab (target="_blank" not recommended as could accumulate many)
r.append('<form action="%s" method="post" accept-charset="utf-8"><table style="margin-left:auto;margin-right:auto;border:thin solid blue"><tr><td colspan=3 style="text-align:center"><em>Click on each word for audio</em></td></tr>' % gradintUrl)
for l in lines:
l2w,l1w = l.split('=',1)
r.append('<tr><td><input type="checkbox" name="W%d" value="%s=%s" checked></td><td>%s</td><td>%s</td></tr>' % (count,l2w,l1w,justsynthLink(l2w.encode('utf-8'),l2).replace('HREF="'+cginame+'?','HREF="'+gradintUrl+'?').decode('utf-8'),justsynthLink(l1w.encode('utf-8'),l1).replace('HREF="'+cginame+'?','HREF="'+gradintUrl+'?').decode('utf-8')))
r.append('<tr class="notranslate"><td><input type="checkbox" name="W%d" value="%s=%s" checked></td><td>%s</td><td>%s</td></tr>' % (count,l2w,l1w,U(justsynthLink(l2w.encode('utf-8'),l2)).replace('HREF="'+cginame+'?','HREF="'+gradintUrl+'?'),U(justsynthLink(l1w.encode('utf-8'),l1)).replace('HREF="'+cginame+'?','HREF="'+gradintUrl+'?')))
count += 1
# could have target="gradint" in the following href, but see comment above
r.append('<tr><td colspan=3><input type="submit" name="bulkadd" value="Add selected words"> to your <a href="%s">personal list</a></td></tr></table><input type="hidden" name="l1" value="%s"><input type="hidden" name="l2" value="%s"></form>' % (gradintUrl,l1,l2))
......@@ -193,52 +257,107 @@ def justSynth(text,lang="",filetype=""):
def justsynthLink(text,lang=""): # assumes written function h5a
if lang in gradint.synth_partials_voices and gradint.guiVoiceOptions: cacheInfo="&curVopt="+gradint.voiceOption
else: cacheInfo=""
return '<A HREF="'+cginame+'?js='+urllib.quote_plus(text)+'&jsl='+urllib.quote_plus(lang)+cacheInfo+'" onClick="javascript:return h5a(this);">'+text+'</A>'
return '<A HREF="'+cginame+'?js='+gradint.S(quote_plus(text))+'&jsl='+quote_plus(lang)+cacheInfo+'" onClick="return h5a(this);">'+gradint.S(text)+'</A>'
# TODO if h5a's canPlayType etc works, cld o/p a lesson as a JS web page that does its own 'take out of event stream' and 'progress write-back'. wld need to code that HERE by inspecting the finished Lesson object, don't call play().
zoom = 100 # in case browser device lacks a zoom UI, we'll provide one
noGTranslate = False
def htmlOut(body_u8,title_extra="",links=1):
print "Content-type: text/html; charset=utf-8" ; print
if noGTranslate: print ("Google: notranslate")
print ("Content-type: text/html; charset=utf-8\n")
if title_extra: title_extra=": "+title_extra
print '<html><head><title>Gradint Web edition'+title_extra+'</title>'
print '<meta name="viewport" content="width=device-width">'
print '</head><body>'+body_u8
print '<HR>'
print ('<html lang="en"><head><title>Gradint Web edition'+title_extra+'</title>')
print ('<meta name="mobileoptimized" content="0"><meta name="viewport" content="width=device-width">')
print ('<script>if(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches)document.write("<style>body,input,textarea { background-color: black; color: #c0c000; } select,input[type=submit],input[type=button] { background-color: #300020; color: #c0c000; } input[type=text] { border: grey groove; } select[disabled],input[disabled] { background-color: #101010; color: #b0b000; } a:link { color: #00b000; } a:visited { color: #00c0c0; } a:hover { color: red; }</style>");</script>')
if not zoom==100: print('<style>body{font-size:%d%%;}body *{font-size:100%%;}</style>' % zoom)
print ('</head><body>')
if type(body_u8)==type(u""): body_u8=body_u8.encode('utf-8')
if hasattr(sys.stdout,'buffer'): # Python 3
sys.stdout.flush()
sys.stdout.buffer.write(body_u8)
sys.stdout.flush()
else: print(body_u8)
print ('<HR>')
if links:
print 'This is Gradint Web edition. If you need recorded words or additional functions, please <A HREF="http://people.pwf.cam.ac.uk/ssb22/gradint/">download the full version of Gradint</A>.'
print ('This is Gradint Web edition. If you need recorded words or additional functions, please <A HREF="//ssb22.user.srcf.net/gradint/">download the full version of Gradint</A>.')
# TODO @ low-priority: Android 3 <input type="file" accept="audio/*;capture=microphone"></input>
if "iPhone" in os.environ.get("HTTP_USER_AGENT","") and gradint.secondLanguage=="zh": print '<p>You can also try the Open University <A HREF="http://itunes.apple.com/gb/app/chinese-characters-first-steps/id441549197?mt=8#">Chinese Characters First Steps</A> iPhone application.'
print '<p>'+program_name[:program_name.index("(")]+"using "+gradint.program_name[:gradint.program_name.index("(")]
print "</body></html>"
backLink = ' <A HREF="'+cginame+'" onClick="javascript:history.go(-1);return false">Back</A>' # TODO may want to add a random= to the non-js HREF
print ('<p>'+program_name[:program_name.index("(")]+"using "+gradint.program_name[:gradint.program_name.index("(")])
print ("</body></html>")
backLink = ' <A HREF="'+cginame+'" onClick="history.go(-1);return false">Back</A>' # TODO may want to add a random= to the non-js HREF
def serveAudio(stream=0, filetype="mp3", inURL=1):
# caller imports gradint (and sets justSynthesize or whatever) first
if filetype=="mp3": print "Content-type: audio/mpeg"
else: print "Content-type: audio/"+filetype # ok for ogg, wav?
if os.environ.get("HTTP_IF_MODIFIED_SINCE",""):
print ("Status: 304 Not Modified\n\n") ; return
httpRange = re.match("bytes=([0-9]*)-([0-9]*)$",os.environ.get('HTTP_RANGE','')) # we MUST support Range: for some iOS players (Apple did not follow the HTTP standard of having a sensible fallback if servers respond with 200, and Apache will not do Range for us if we're CGI). Single Range should be sufficient.
if httpRange: httpRange = httpRange.groups()
if httpRange==('',''): httpRange = None # must spec one
if httpRange:
if not httpRange[0]: httpRange=[-int(httpRange[1]),None]
elif not httpRange[1]: httpRange=[int(httpRange[0]),None]
else: httpRange=[int(httpRange[0]),int(httpRange[1])+1]
print ("Status: 206 Partial Content")
stream = 0
if filetype=="mp3": print ("Content-type: audio/mpeg")
else: print ("Content-type: audio/"+filetype) # ok for ogg, wav?
if inURL:
print "Last-Modified: Sun, 06 Jul 2008 13:20:05 GMT"
print "Expires: Wed, 1 Dec 2036 23:59:59 GMT"
print ("Last-Modified: Sun, 06 Jul 2008 13:20:05 GMT")
print ("Expires: Wed, 1 Dec 2036 23:59:59 GMT") # TODO: S2G
print ("Content-disposition: attachment; filename=gradint."+filetype) # helps with some browsers that can't really do streaming
gradint.out_type = filetype
gradint.waitBeforeStart = 0
def mainOrSynth():
oldProgress = None ; rollback = False
if not gradint.justSynthesize and 'h5a' in query:
try: oldProgress = open(gradint.progressFile,'rb').read()
except: pass
rollback = True
if "lesson" in query: random.seed(query.getfirst("lesson")) # so clients that re-GET same lesson from partway through can work
try: gradint.main()
except SystemExit:
if not gradint.justSynthesize:
o1,o2 = gradint.write_to_stdout,gradint.outputFile
reinit_gradint() ; setup_userID()
gradint.write_to_stdout,gradint.outputFile = o1,o2
gradint.setSoundCollector(gradint.SoundCollector())
gradint.justSynthesize = "en Problem generating the lesson. Check we have prompts for those languages." ; gradint.main()
if oldProgress: open(gradint.progressFile,'wb').write(oldProgress)
rollback = oldProgress = None
if rollback: # roll back pending lFinish
os.rename(gradint.progressFile,gradint.progressFile+'-new')
if oldProgress: open(gradint.progressFile,'wb').write(oldProgress)
# end of def mainOrSynth
if stream:
print "Content-disposition: attachment; filename=gradint.mp3" # helps with some browsers that can't really do streaming
print ; sys.stdout.flush()
print ("")
sys.stdout.flush()
gradint.write_to_stdout = 1
gradint.outputFile="-."+filetype ; gradint.setSoundCollector(gradint.SoundCollector())
mainOrSynth()
else:
tempdir = commands.getoutput("mktemp -d")
gradint.write_to_stdout = 0
gradint.outputFile=tempdir+"/serveThis."+filetype ; gradint.setSoundCollector(gradint.SoundCollector())
gradint.waitBeforeStart = 0
mainOrSynth()
print "Content-Length:",os.stat(tempdir+"/serveThis."+filetype).st_size
print ; sys.stdout.flush()
os.system("cat "+tempdir+"/serveThis."+filetype)
tempdir = tempfile.mkdtemp()
fn,fn2 = tempdir+"/I."+filetype, tempdir+"/O."+filetype
if httpRange and "lesson" in query: # try to cache it
try: os.mkdir(myTmp)
except: pass # exist ok
for f in os.listdir(myTmp):
if os.stat(myTmp+os.sep+f).st_mtime < time.time()-4000:
os.remove(myTmp+os.sep+f)
fn = gradint.outputPrefix+str(int(query.getfirst("lesson")))+"."+filetype # (don't be tricked into clobbering paths with non-int lesson IDs)
if not os.path.exists(fn):
gradint.outputFile=fn
gradint.setSoundCollector(gradint.SoundCollector())
mainOrSynth()
if httpRange:
total = os.stat(fn).st_size
open(fn2,"wb").write(open(fn,"rb").read()[httpRange[0]:httpRange[1]])
if httpRange[0]<0: httpRange[0] += total
if not httpRange[1]: httpRange[1] = total
print("Content-Range: bytes %d-%d/%d" % (httpRange[0],httpRange[1]-1,total))
else: fn2 = fn
print ("Content-Length: "+repr(os.stat(fn2).st_size)+"\n")
sys.stdout.flush()
os.system("cat "+fn2) # components already validated so no quoting required
os.system("rm -r "+tempdir)
def addWord(l1w,l2w,l1,l2,out=True):
......@@ -247,56 +366,88 @@ def addWord(l1w,l2w,l1,l2,out=True):
if not ((gradint.firstLanguage,gradint.secondLanguage) == (l2,l1) and "HTTP_REFERER" in os.environ and not cginame in os.environ["HTTP_REFERER"]): gradint.updateSettingsFile(gradint.settingsFile,{"firstLanguage": l1,"secondLanguage":l2})
gradint.firstLanguage,gradint.secondLanguage = l1,l2
if (l1w+"_"+l1,l2w+"_"+l2) in map(lambda x:x[1:],gradint.parseSynthVocab(gradint.vocabFile,forGUI=1)):
if out: htmlOut('This word is already in your list.'+backLink)
if out: htmlOut(withLocalise('This word is already in your list.')+backLink)
return
gradint.appendVocabFileInRightLanguages().write(l2w+"="+l1w+"\n")
gradint.appendVocabFileInRightLanguages().write(gradint.B(l2w)+gradint.B("=")+gradint.B(l1w)+gradint.B("\n"))
if not out: return
if "HTTP_REFERER" in os.environ and not cginame in os.environ["HTTP_REFERER"]: extra="&dictionary="+urllib.quote(os.environ["HTTP_REFERER"])
if "HTTP_REFERER" in os.environ and not cginame in os.environ["HTTP_REFERER"]: extra="&dictionary="+quote(os.environ["HTTP_REFERER"])
else: extra=""
redirectHomeKeepCookie(dirID,extra)
def redirectHomeKeepCookie(dirID,extra=""):
print "Location: "+cginame+"?random="+str(random.random())+"&id="+dirID[dirID.rindex("/")+1:]+extra ; print
dirID = gradint.S(dirID) # just in case
print ("Location: "+cginame+"?random="+str(random.random())[2:]+"&id="+dirID[dirID.rindex("/")+1:]+extra+"\n")
def langSelect(name,curLang):
curLang = gradint.espeak_language_aliases.get(curLang,curLang)
return '<select name="'+name+'">'+''.join(['<option value="'+abbr+'"'+gradint.cond(abbr==curLang," selected","")+'>'+localise(abbr)+' ('+abbr+')'+'</option>' for abbr in sorted(lDic.keys())])+'</select>'
return '<select name="'+name+'">'+''.join(['<option value="'+abbr+'"'+gradint.cond(abbr==curLang," selected","")+'>'+localise(abbr)+' ('+abbr+')'+'</option>' for abbr in sorted(langFullName.keys())])+'</select>'
def numSelect(name,nums,curNum): return '<select name="'+name+'">'+''.join(['<option value="'+str(num)+'"'+gradint.cond(num==curNum," selected","")+'>'+str(num)+'</option>' for num in nums])+'</select>'
def localise(x):
def localise(x,span=0):
r=gradint.localise(x)
if r==x: return lDic.get(gradint.espeak_language_aliases.get(x,x),x)
else: return r.encode('utf-8')
if r==x: return langFullName.get(gradint.espeak_language_aliases.get(x,x),x)
if span==1: r="<span lang=\""+gradint.firstLanguage+"\">"+r+"</span>"
elif span==2: r+='" lang="'+gradint.firstLanguage
if type(r)==type("")==type(u""): return r # Python 3
else: return r.encode('utf-8') # Python 2
for k,v in {"Swap":{"zh":u"交换","zh2":u"交換"},
"Text edit":{"zh":u"文本编辑"},
"Delete":{"zh":u"删除","zh2":u"刪除"},
"Really delete this word?":{"zh":u"真的删除这个词?","zh2":u"真的刪除這個詞?"},
"Your word list":{"zh":u"你的词汇表","zh2":u"你的詞彙表"},
"click for audio":{"zh":u"击某词就听声音","zh2":u"擊某詞就聽聲音"},
"Repeats":{"zh":u"重复计数","zh2":u"重複計數"},
"To edit this list on another computer, type":{"zh":u"要是想在其他的电脑或手机编辑这个词汇表,请在别的设备打","zh2":u"要是想在其他的電腦或手機編輯這個詞彙表,請在別的設備打"},
"Please wait while the lesson starts to play":{"zh":u"稍等本课正开始播放","zh2":u"稍等本課正開始播放"},
"Bigger":{"zh":u"大"},"Smaller":{"zh":u"小"},
'You must type a word in the box before pressing the Speak button.':{"zh":u"按‘发音’前,应该框里打字。","zh2":u"按‘發音’前,應該框裡打字。"},
'You must type words in both boxes before pressing the Add button.':{"zh":u"按‘添加’前,应该在两框里打字。","zh2":u"按‘添加’前,應該在兩框裡打字。"},
'You must change the settings before pressing the Change Languages button.':{"zh":u"按‘选择其他语言’前,应该转换语言设定。","zh2":u"按‘選擇其他語言’前,應該轉換語言設定。"},
'This word is already in your list.':{"zh":u"本词已经在您的词汇表。","zh2":u"本詞已經在您的詞彙表。"},
"Your word list is empty.":{"zh":u"词汇表没有词汇,加一些吧","zh2":u"詞彙表沒有詞彙,加一些吧"}
}.items():
if not k in gradint.GUI_translations: gradint.GUI_translations[k]=v
def withLocalise(x): return x+" "+localise(x,1)
def h5a():
body = """<script language="Javascript"><!--
function h5a(link) { if (document.createElement) {
body = """<script><!--
function h5a(link,endFunc) { if (document.createElement) {
var ae = document.createElement('audio');
if (ae.canPlayType && function(s){return s!="" && s!="no"}(ae.canPlayType('audio/mpeg'))) {
if (link.href) ae.setAttribute('src', link.href);
else ae.setAttribute('src', link);
ae.play();
return false; }"""
if gradint.got_program("oggenc"): body += """else if (ae.canPlayType && function(s){return s!="" && s!="no"}(ae.canPlayType('audio/ogg'))) {
if (link.href) ae.setAttribute('src', link.href+"&filetype=ogg");
else ae.setAttribute('src', link+"&filetype=ogg");
ae.play();
return false; }"""
function cp(t,lAdd) { if(ae.canPlayType && function(s){return s!="" && s!="no"}(ae.canPlayType(t))) {
if (link.href) ae.setAttribute('src', link.href+lAdd);
else ae.setAttribute('src', link+lAdd);
if (typeof endFunc !== 'undefined') { ae.addEventListener("ended", endFunc, false); ae.addEventListener("timeupdate",function(e){t=ae.currentTime;m=Math.floor(t/60);t=Math.floor(t%60);document.forms[0].lesson.value=m+(t<10?":0":":")+t},false) }
ae.play(); return true; // Safari can say "Unhandled Promise Rejection: AbortError: The operation was aborted." in console log, but plays anyway when loaded
} return false; }
if (cp('audio/mpeg','')) return false;"""
if gradint.got_program("oggenc"): body += """else if (cp('audio/ogg',"&filetype=ogg")) return false;"""
body += """} return true; }
//--></script>"""
return body
def hasVoiceOptions(l):
if not l in gradint.synth_partials_voices: return False
if not gradint.guiVoiceOptions: return False
try: voices = os.listdir(gradint.partialsDirectory+os.sep+l)
except: voices = []
for v in voices:
if "-" in v and v[:v.index("-")] in voices: return True
def listVocab(hasList): # main screen
firstLanguage,secondLanguage = gradint.firstLanguage, gradint.secondLanguage
# TODO button onClick: careful of zh w/out tones, wld need to JS this
body = h5a() + '<center><form action="'+cginame+'">'
gotVoiceOptions = (gradint.secondLanguage in gradint.synth_partials_voices or gradint.firstLanguage in gradint.synth_partials_voices) and gradint.guiVoiceOptions
# TODO what if it's in synth_partials_voices but NOT the one that has guiVoiceOptions ? (e.g. Cantonese when both Mandarin voices are installed) (currently displaying 'non-functional' voice option buttons when that happens)
body += '<input type=submit name=bigger value="%s"> | <input type=submit name=smaller value="%s">' % (localise("Bigger"),localise("Smaller"))
GUIlang = gradint.GUI_languages.get(firstLanguage,firstLanguage)
if "@variants-"+GUIlang in gradint.GUI_translations:
body += " -- " # separating from big/small
count = 0
for variant in gradint.GUI_translations["@variants-"+GUIlang]:
if count: body += " | "
body += '<input type=submit name="variant'+str(count)+'" value="'+gradint.cond(type("")==type(u""),variant,variant.encode('utf-8'))+'"'+gradint.cond(gradint.scriptVariants.get(GUIlang,0)==count,' disabled="disabled"',"")+'>'
count += 1
body += "<br>"
gotVoiceOptions = (hasVoiceOptions(gradint.secondLanguage) or hasVoiceOptions(gradint.firstLanguage))
if gotVoiceOptions:
body += 'Voice option: <input type=submit name=voNormal value="Normal"'+gradint.cond(gradint.voiceOption=="",' disabled="disabled"',"")+'>'
for v in gradint.guiVoiceOptions: body += ' | <input type=submit name=vopt value="'+v[1].upper()+v[2:]+'"'+gradint.cond(gradint.voiceOption==v,' disabled="disabled"',"")+'>'
......@@ -305,53 +456,59 @@ def listVocab(hasList): # main screen
# must have autocomplete=off if capturing keycode 13
if gotVoiceOptions: cacheInfo="&curVopt="+gradint.voiceOption
else: cacheInfo=""
body += (localise("Word in %s") % localise(secondLanguage))+': <input type=text name=l2w autocomplete=off onkeydown="if(event.keyCode==13) {document.forms[0].spk.click();return false} else return true"> <input type=submit name=spk value="'+localise("Speak")+'" onClick="javascript: if (!document.forms[0].l1w.value && !document.forms[0].l2w.value) return true; else return h5a(\''+cginame+'?spk=1&l1w=\'+document.forms[0].l1w.value+\'&l2w=\'+document.forms[0].l2w.value+\'&l1=\'+document.forms[0].l1.value+\'&l2=\'+document.forms[0].l2.value+\''+cacheInfo+'\');"><br>'+(localise("Meaning in %s") % localise(firstLanguage))+': <input type=text name=l1w autocomplete=off onkeydown="if(event.keyCode==13) {document.forms[0].add.click();return false} else return true"> <input type=submit name=add value="'+(localise("Add to %s") % localise("vocab.txt").replace(".txt",""))+'"><script language="Javascript"><!--\nvar emptyString="";document.write(\' <input type=submit name=dummy value="'+localise("Clear input boxes")+'" onClick="javascript:document.forms[0].l1w.value=document.forms[0].l2w.value=emptyString;document.forms[0].l2w.focus();return false">\')\n//--></script><p>'+localise("Your first language")+': '+langSelect('l1',firstLanguage)+' '+localise("second")+': '+langSelect('l2',secondLanguage)+' <nobr><input type=submit name=clang value="'+localise("Change languages")+'"><input type=submit name=swaplang value="'+localise("Swap")+'"></nobr>'
body += (localise("Word in %s",1) % localise(secondLanguage))+': <input type=text name=l2w autocomplete=off onkeydown="if(event.keyCode==13) {document.forms[0].spk.click();return false} else return true" onfocus="document.forms[0].onsubmit=\'document.forms[0].onsubmit=&quot;return true&quot;;document.forms[0].spk.click();return false\'" onblur="document.forms[0].onsubmit=\'return true\'"> <input type=submit name=spk value="'+localise("Speak",2)+'" onClick="if (!document.forms[0].l1w.value && !document.forms[0].l2w.value) return true; else return h5a(\''+cginame+'?spk=1&l1w=\'+document.forms[0].l1w.value+\'&l2w=\'+document.forms[0].l2w.value+\'&l1=\'+document.forms[0].l1.value+\'&l2=\'+document.forms[0].l2.value+\''+cacheInfo+'\');"><br>'+(localise("Meaning in %s",1) % localise(firstLanguage))+': <input type=text name=l1w autocomplete=off onkeydown="if(event.keyCode==13) {document.forms[0].add.click();return false} else return true" onfocus="document.forms[0].onsubmit=\'document.forms[0].onsubmit=&quot;return true&quot;;document.forms[0].add.click();return false\'" onblur="document.forms[0].onsubmit=\'return true\'"> <input type=submit name=add value="'+(localise("Add to %s",2) % localise("vocab.txt").replace(".txt",""))+'"><script><!--\nvar emptyString="";document.write(\' <input type=submit name=placeholder value="'+localise("Clear input boxes",2)+'" onClick="document.forms[0].l1w.value=document.forms[0].l2w.value=emptyString;document.forms[0].l2w.focus();return false">\')\n//--></script><p><nobr>'+localise("Your first language",1)+': '+langSelect('l1',firstLanguage)+'</nobr> <nobr>'+localise("second",1)+': '+langSelect('l2',secondLanguage)+'</nobr> <nobr><input type=submit name=clang value="'+localise("Change languages",2)+'"><input type=submit name=swaplang value="'+localise("Swap",2)+'"></nobr>' # onfocus..onblur updating onsubmit is needed for iOS "Go" button
def htmlize(l,lang):
if type(l)==type([]) or type(l)==type(()): return htmlize(l[-1],lang)
if "!synth:" in l: return htmlize(l[l.index("!synth:")+7:l.rfind("_")],lang)
l = gradint.B(l)
if gradint.B("!synth:") in l: return htmlize(l[l.index(gradint.B("!synth:"))+7:l.rfind(gradint.B("_"))],lang)
return justsynthLink(l,lang)
def deleteLink(l1,l2):
r = []
for l in [l2,l1]:
if type(l)==type([]) or type(l)==type(()) or not "!synth:" in l: return "" # Web-GUI delete in poetry etc not yet supported
r.append(urllib.quote(l[l.index("!synth:")+7:l.rfind("_")]))
r.append(localise("Delete"))
return '<TD><input type=submit name="del-%s%%3d%s" value="%s" onClick="javascript: return confirm(\'Really delete this word?\');"></TD>' % tuple(r)
if type(l)==type([]) or type(l)==type(()) or not gradint.B("!synth:") in gradint.B(l): return "" # Web-GUI delete in poetry etc not yet supported
l = gradint.B(l)
r.append(gradint.S(quote(l[l.index(gradint.B("!synth:"))+7:l.rfind(gradint.B("_"))])))
r.append(localise("Delete",2))
return ('<td><input type=submit name="del-%s%%3d%s" value="%s" onClick="return confirm(\''+localise("Really delete this word?")+'\');"></td>') % tuple(r)
if hasList:
gradint.availablePrompts = gradint.AvailablePrompts() # needed before ProgressDatabase()
# gradint.cache_maintenance_mode=1 # don't transliterate on scan -> NO, including this scans promptsDirectory!
gradint.ESpeakSynth.update_translit_cache=lambda *args:0 # do it this way instead
data = gradint.ProgressDatabase().data ; data.reverse()
if data: hasList = "<p><TABLE style=\"border: thin solid green\"><caption><nobr>Your word list</NOBR> <NOBR>(click for audio)</NOBR> <input type=submit name=edit value=\""+localise("Text edit")+"\"></caption><TR><TH>Repeats</TH><TH>"+localise(gradint.secondLanguage)+"</TH><TH>"+localise(gradint.firstLanguage)+"</TH></TR>"+"".join(["<TR><TD>%d</TD><TD>%s</TD><TD>%s</TD>%s" % (num,htmlize(dest,gradint.secondLanguage),htmlize(src,gradint.firstLanguage),deleteLink(src,dest)) for num,src,dest in data])+"</TABLE>"
if data: hasList = "<p><table style=\"border: thin solid green\"><caption><nobr>"+localise("Your word list",1)+"</nobr> <nobr>("+localise("click for audio",1)+")</nobr> <input type=submit name=edit value=\""+localise("Text edit",2)+"\"></caption><tr><th>"+localise("Repeats",1)+"</th><th>"+localise(gradint.secondLanguage,1)+"</th><th>"+localise(gradint.firstLanguage,1)+"</th></tr>"+"".join(["<tr class=\"notranslate\"><td>%d</td><td lang=\"%s\">%s</td><td lang=\"%s\">%s</td>%s" % (num,gradint.secondLanguage,htmlize(dest,gradint.secondLanguage),gradint.firstLanguage,htmlize(src,gradint.firstLanguage),deleteLink(src,dest)) for num,src,dest in data])+"</table>"
else: hasList=""
else: hasList=""
if hasList: body += '<P><table style="border:thin solid blue"><tr><td>'+numSelect('new',range(2,10),gradint.maxNewWords)+' '+localise("new words in")+' '+numSelect('mins',[15,20,25,30],int(gradint.maxLenOfLesson/60))+' '+localise('mins')+' <input type=submit name=lesson value="'+localise("Start lesson")+'"></td></tr></table>'
if hasList: body += '<p><table style="border:thin solid blue"><tr><td>'+numSelect('new',range(2,10),gradint.maxNewWords)+' '+localise("new words in")+' '+numSelect('mins',[15,20,25,30],int(gradint.maxLenOfLesson/60))+' '+localise('mins')+""" <input type=submit name=lesson value="""+'"'+localise("Start lesson",2)+"""" onClick="document.forms[0].lesson.disabled=1; document.forms[0].lesson.value=&quot;"""+localise("Please wait while the lesson starts to play")+"""&quot;;document.d0=new Date();return h5a('"""+cginame+'?lesson='+str(random.random())[2:]+"""&h5a=1&new='+document.forms[0].new.value+'&mins='+document.forms[0].mins.value,function(){if(new Date()-document.d0>60000)location.href='"""+cginame+'?lFinish='+str(random.random())[2:]+"""';else{document.forms[0].lesson.value='PLAY ERROR'}})"></td></tr></table>""" # when lesson ended, refresh with lFinish which saves progress (interrupts before then cancel it), but don't save progress if somehow got the ended event in 1st minute as that could be a browser issue
if "dictionary" in query:
if query["dictionary"][0]=="1": body += '<script language="Javascript"><!--\ndocument.write(\'<p><a href="javascript:history.go(-1)">Back to referring site</a>\')\n//--></script>' # apparently it is -1, not -2; the redirect doesn't count as one (TODO are there any JS browsers that do count it as 2?)
else: body += '<p><a href="'+query["dictionary"][0]+'">Back to dictionary</a>' # TODO check for cross-site scripting
if not hasList: hasList="<P>Your word list is empty."
if query.getfirst("dictionary")=="1": body += '<script><!--\ndocument.write(\'<p><a href="javascript:history.go(-1)">'+localise("Back to referring site",1)+'</a>\')\n//--></script>' # apparently it is -1, not -2; the redirect doesn't count as one (TODO are there any JS browsers that do count it as 2?)
else: body += '<p><a href="'+query.getfirst("dictionary")+'">'+localise("Back to dictionary",1)+'</a>' # TODO check for cross-site scripting
if hasList:
if "://" in gradintUrl: hasList += "<p>"+localise("To edit this list on another computer, type",1)+" <kbd>"+gradintUrl.replace(".","<wbr>.").replace("/","<wbr>/")+"?id="+re.sub("([0-9]{4})(?!$)",r"\1<wbr><span><!-- (this is not a phone number) --></span>",getCookieId())+"</kbd>" # span needed for iOS at least
else: hasList="<p>"+localise("Your word list is empty.",1)
body += hasList
htmlOut(body+'</form></center><script language="Javascript"><!--\ndocument.forms[0].l2w.focus()\n//--></script>')
htmlOut(body+'</form></center><script><!--\ndocument.forms[0].l2w.focus()\n//--></script>')
def has_userID():
def has_userID(): # TODO: can just call getCookieId with not too much extra overhead
cookie_string = os.environ.get('HTTP_COOKIE',"")
if cookie_string:
cookie = Cookie.SimpleCookie()
cookie.load(cookie_string)
return 'id' in cookie
def getCookieId():
cookie_string = os.environ.get('HTTP_COOKIE',"")
if not cookie_string: return
cookie = Cookie.SimpleCookie()
cookie.load(cookie_string)
if 'id' in cookie: return cookie['id'].value.replace('"','').replace("'","").replace("\\","")
def setup_userID():
# MUST call before outputting headers (may set cookie)
# Use the return value of this with -settings.txt, -vocab.txt etc
if cginame=="gradint.cgi": dirName = "cgi-gradint-users" # as previous versions
else: dirName = cginame+"-users" # TODO document this feature (you can symlink something-else.cgi to gradint.cgi and it will have a separate user directory) (however it still reports gradint.cgi on the footer)
if not os.path.exists(dirName): os.system("mkdir "+dirName)
userID = None
cookie_string = os.environ.get('HTTP_COOKIE',"")
if cookie_string:
cookie = Cookie.SimpleCookie()
cookie.load(cookie_string)
if 'id' in cookie: userID = cookie['id'].value.replace('"','').replace("'","").replace("\\","")
userID = getCookieId()
need_write = (userID and not os.path.exists(dirName+'/'+userID+'-settings.txt')) # maybe it got cleaned up
if not userID:
while True:
......@@ -359,14 +516,28 @@ def setup_userID():
if not os.path.exists(dirName+'/'+userID+'-settings.txt'): break
open(dirName+'/'+userID+'-settings.txt','w') # TODO this could still be a race condition (but should be OK under normal circumstances)
need_write = 1
print 'Set-Cookie: id=' + userID+'; expires=Wed, 1 Dec 2036 23:59:59 GMT'
userID = dirName+'/'+userID
print ('Set-Cookie: id=' + userID+'; expires=Wed, 1 Dec 2036 23:59:59 GMT') # TODO: S2G
userID0, userID = userID, dirName+os.sep+userID # already validated
gradint.progressFileBackup=gradint.pickledProgressFile=None
gradint.vocabFile = userID+"-vocab.txt"
gradint.progressFile = userID+"-progress.txt"
gradint.settingsFile = userID+"-settings.txt"
gradint.outputPrefix = myTmp+os.sep+userID0+"-"
if need_write: gradint.updateSettingsFile(gradint.settingsFile,{'firstLanguage':gradint.firstLanguage,'secondLanguage':gradint.secondLanguage})
else: gradint.readSettings(gradint.settingsFile)
gradint.auto_advancedPrompt=1 # prompt in L2 if we don't have L1 prompts on the server, what else can we do...
if os.path.exists(userID+"-zoom.txt"):
global zoom ; zoom = int(open(userID+"-zoom.txt").read().strip())
return userID
main()
try: main()
except Exception as e:
print ("Content-type: text/plain; charset=utf-8\n")
sys.stdout.flush()
import traceback
try: traceback.print_exc(file=sys.stdout)
except: pass
sys.stdout.flush()
if hasattr(sys.stdout,"buffer"): buf = sys.stdout.buffer
else: buf = sys.stdout
buf.write(repr(e).encode("utf-8"))
#!/usr/bin/env python
# (compatible with both Python 2 and Python 3)
# Script to generate an HTML table of the contents of a lesson
# for summarizing it to a teacher or native speaker.
# Reads from progressFile and progressFileBackup.
# Version 1.04 (c) 2011 Silas S. Brown. License: GPL
# Version 1.07 (c) 2011, 2020-21, 2025 Silas S. Brown. License: GPL
# Example use:
# python lesson-table.py | ssh some-server 'mutt -e "set record = \"\";" -e "set charset=\"utf-8\"; set send_charset=\"utf-8\"; set content_type=\"text/html\";" to-address -s "Gradint report"' || echo Send failed
samples_url = None # or "http://example.org/path/to/samples/"
# export samples_url=http://example.org/path/to/samples/ # or omit
# python lesson-table.py [gradint-params] | ssh some-server 'mutt -e "set record = \"\";" -e "set charset=\"utf-8\"; set send_charset=\"utf-8\"; set content_type=\"text/html\";" to-address -s "Gradint report"' || echo Send failed
import gradint, os
samples_url = os.getenv("samples_url","")
from gradint import B,S
newpf = gradint.progressFile
gradint.progressFile = gradint.progressFileBackup
gradint.pickledProgressFile=None
......@@ -31,16 +34,19 @@ newProg = gradint.ProgressDatabase(alsoScan=0)
gradint.mergeProgress(newProg.data,mergeIn)
del mergeIn
changes = [] ; count=0
newProg.data.sort()
gradint.sort(newProg.data,gradint.cmpfunc)
for tries,l1,l2 in newProg.data:
if not tries: continue
key = gradint.norm_filelist(l1,l2)
oldTries = opd.get(key,0)
if not oldTries==tries: changes.append((oldTries,count,tries-oldTries,l1,l2))
if not oldTries==tries: changes.append((oldTries,count,tries-oldTries,S(l1),S(l2)))
count += 1
del newProg,opd
changes.sort()
print '<html><head><meta http_equiv="Content-type" value="text/html; charset=utf-8"><title>Gradint lesson report</title></head><body><h2>Gradint lesson report</h2><table border><tr><th>Repeats before</th><th>Repeats today</th><th>Question</th><th>Answer</th></tr>' # (have Question/Answer order rather than Word/Meaning, because if it's L2-only poetry then the question is the previous line, which is not exactly "meaning")
print ('<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>Gradint lesson report</title><meta name="mobileoptimized" content="0"><meta name="viewport" content="width=device-width"><script>if(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches)document.write("<style>body { background-color: black; color: #c0c000; } a:link { color: #00b000; } a:visited { color: #00c0c0; } a:hover { color: red; }</style>");if(navigator.languages && navigator.languages.indexOf("en")>=0) document.write(\'<meta name="google" content="notranslate">\')</script></head><body><h2>Gradint lesson report</h2>')
if gradint.unix and gradint.got_program("zgrep"):
print (os.popen("zgrep '^# collection=' \"%s\"" % gradint.progressFile).read()[2:].rstrip())
print ('<table border><tr><th>Repeats before</th><th>Repeats today</th><th>Question</th><th>Answer</th></tr>') # (have Question/Answer order rather than Word/Meaning, because if it's L2-only poetry then the question is the previous line, which is not exactly "meaning")
had_h5a = False
def h5aCode(filename):
......@@ -49,7 +55,7 @@ def h5aCode(filename):
global had_h5a
if not had_h5a:
had_h5a = True
print """<script language="Javascript"><!--
print ("""<script language="Javascript"><!--
function h5a(link,type) { if (document.createElement) {
var ae = document.createElement('audio');
if (ae.canPlayType && function(s){return s!="" && s!="no"}(ae.canPlayType(type))) {
......@@ -57,7 +63,7 @@ function h5a(link,type) { if (document.createElement) {
else ae.setAttribute('src', link);
ae.play();
return false; } } return true; }
//--></script>"""
//--></script>""")
return r
def real_h5aCode(filename):
if filename.endswith(gradint.dotmp3): return ' onClick="javascript:return h5a(this,\'audio/mpeg\')"'
......@@ -65,12 +71,14 @@ def real_h5aCode(filename):
else: return ""
def wrappable(f):
z = u'\u200b'.encode('utf-8') # zero-width space
z = u'\u200b' # zero-width space
if not type(u"")==type(""): z=z.encode('utf-8') # Py2
return f.replace(os.sep,os.sep+z).replace('_',z+'_')
def checkVariant(l,ensureTxt=0):
l=S(l)
if os.sep in l: fname=l[l.rindex(os.sep)+1:]
else: fname=l
variants = gradint.variantFiles.get(gradint.samplesDirectory+os.sep+l,[fname])
variants = map(S,gradint.variantFiles.get(B(gradint.samplesDirectory+os.sep+l),[fname]))
if fname in variants: return l # ok
# else no default variant, need to pick one for the link
for v in variants:
......@@ -81,12 +89,13 @@ def checkVariant(l,ensureTxt=0):
return l[:l.rindex(os.sep)+1]+v
def link(l):
if type(l)==type([]): return link(l[-1])
if l.lower().endswith(gradint.dottxt): l="!synth:"+gradint.u8strip(gradint.read(gradint.samplesDirectory+os.sep+checkVariant(l,1))).strip(gradint.wsp)+"_"+gradint.languageof(l)
l = S(l)
if l.lower().endswith(gradint.dottxt): l="!synth:"+S(gradint.u8strip(gradint.read(gradint.samplesDirectory+os.sep+checkVariant(l,1)))).strip(gradint.wsp)+"_"+gradint.languageof(l)
if "!synth:" in l:
if gradint.languageof(l) not in [gradint.firstLanguage,gradint.secondLanguage]: l=gradint.textof(l)+" ("+gradint.languageof(l)+")"
else: l=gradint.textof(l)
if gradint.languageof(l) not in [gradint.firstLanguage,gradint.secondLanguage]: l=S(gradint.textof(l))+" ("+gradint.languageof(l)+")"
else: l=S(gradint.textof(l))
return l.replace('&','&amp;').replace('<','&lt;')
if samples_url: return '<A HREF="'+samples_url+checkVariant(l)+'"'+h5aCode(checkVariant(l))+'>'+wrappable(l)+'</A>'
return wrappable(l).replace('&','&amp;').replace('<','&lt;')
for b4,pos,today,l1,l2 in changes: print '<tr><td>%d</td><td>%d</td><td>%s</td><td>%s</td></tr>' % (b4,today,link(l1),link(l2))
print '</table></body></html>'
for b4,pos,today,l1,l2 in changes: print ('<tr><td>%d</td><td>%d</td><td class="notranslate">%s</td><td class="notranslate">%s</td></tr>' % (b4,today,link(l1),link(l2)))
print ('</table></body></html>')
#!/usr/bin/env python
# (either Python 2 or Python 3)
# safety-check-progressfile.py:
# The purpose of this script is to check
# progress.txt for safety. Because it's
......
#!/bin/bash
# Gradint online samples browser v1.0 (c) 2011 Silas S. Brown. License: GPL
# Gradint online samples browser v1.1 (c) 2011,2013 Silas S. Brown. License: GPL
# Works as an "indexing" CGI.
# To set up in Apache, make .htaccess with:
......@@ -28,7 +28,11 @@ if ! test "a$REQUEST_URI" == a; then
cd "/home/ssb22/public_html/$(echo "$REQUEST_URI"|sed -e 's/?.*//')"
fi # else assume the server put us in the right directory, like mathopd does
export Filename="$(pwd|sed -e 's,.*/,,').zip"
if echo "$SERVER_SOFTWARE"|grep Apache >/dev/null; then
echo "Status: 200 OK" # overriding the 403
fi # (mathopd doesn't need this, and not tested with all mathopd versions)
Filename="$(pwd|sed -e 's,.*/,,').zip"
if test "$QUERY_STRING" == zip || test "a$(echo "$REQUEST_URI"|sed -e 's/.*?//')" == azip; then
echo Content-type: application/zip
......@@ -66,8 +70,8 @@ function h5a(link) {
//--></script>
EOF
for N in *; do
export Size=$(du -h --apparent-size -s "$N"|cut -f1)
if echo "$N"|grep '\.txt$'>/dev/null && echo $Size|grep '^[0-9]*$' >/dev/null;then export Size="$(cat "$N")";else export Size="($Size)"; fi
Size=$(du -h --apparent-size -s "$N"|cut -f1)
if echo "$N"|grep '\.txt$'>/dev/null && echo $Size|grep '^[0-9]*$' >/dev/null;then Size="$(cat "$N")";else Size="($Size)"; fi
echo "<LI><A HREF=\"$N\" onClick=\"javascript:return h5a(this)\">$N</A> $Size</LI>"
done
echo "</UL></BODY></HTML>"
......
......@@ -13,34 +13,35 @@
# be used. E.g.: export ESPEAK_CGI_URL="/~userID/espeak.cgi"
# (TODO: this script ignores the possibility of synthesizing phrases from partials)
# Version 1.1, (c) Silas S. Brown, License: GPL
# Version 1.2, (c) Silas S. Brown, License: GPL
from gradint import *
if not synthCache: synthCache_contents = []
langs=[secondLanguage,firstLanguage]
o=open(vocabFile,"rU")
justHadP=1
print '<html><HEAD><META HTTP-EQUIV=Content-type CONTENT="text/html; charset=utf-8"><meta name="viewport" content="width=device-width"></HEAD><body>' # (assume utf8 in case there's any hanzi, but TODO what if using another charset for another language?)
for l in o.readlines():
print ('<html><HEAD><META HTTP-EQUIV=Content-type CONTENT="text/html; charset=utf-8"><meta name="viewport" content="width=device-width"></HEAD><body>') # (assume utf8 in case there's any hanzi, but TODO what if using another charset for another language?)
for l in o:
l2=l.lower()
if l2.startswith("set language ") or l2.startswith("set languages "): langs=l.split()[2:]
if not l.strip():
# blank line
if not justHadP: print "<P>"
if not justHadP: print ("<P>")
justHadP=1 ; continue
if not justHadP: print "<BR>"
if not justHadP: print ("<BR>")
if l2.startswith("set language ") or l2.startswith("set languages ") or l2.startswith("limit on") or l2.startswith("limit off") or l2.startswith("begin poetry") or l2.startswith("end poetry"):
print "<EM>%s</EM>" % (l,)
print ("<EM>%s</EM>" % (l,))
elif l2.startswith("#"):
# comment (and may be part of multi-line comment)
if not l[1:].strip().startswith("<!--"): print "<small>#</small> "
print l[1:]
if not l[1:].strip().startswith("<!--"): print ("<small>#</small> ")
print (l[1:])
else:
# vocab line
langsAndWords=zip(langs,map(lambda x:x.strip(),l.split("=")))
out = []
for lang,word in langsAndWords:
fname=synthCache_transtbl.get(word.lower()+"_"+lang+dotwav,word.lower()+"_"+lang+dotwav)
lang,word = S(lang),S(word)
fname=S(synthCache_transtbl.get(word.lower()+"_"+lang+dotwav,word.lower()+"_"+lang+dotwav))
found = 0
for fn2 in [fname,fname.replace(dotwav,dotmp3)]:
if fn2 in synthCache_contents:
......@@ -48,9 +49,10 @@ for l in o.readlines():
found = 1 ; break
if not found:
if os.getenv("ESPEAK_CGI_URL"):
import urllib
out.append("<A HREF=\""+os.getenv("ESPEAK_CGI_URL")+"?"+urllib.urlencode({"t":word,"l":lang})+"\">"+word+"</A>")
try: from urllib import urlencode # Python 2
except: from urllib.parse import urlencode # Python 3
out.append("<A HREF=\""+os.getenv("ESPEAK_CGI_URL")+"?"+urlencode({"t":word,"l":lang})+"\">"+word+"</A>")
else: out.append(word)
print " = ".join(out)
print (" = ".join(out))
justHadP=0
print "</body></html>"
print ("</body></html>")
File moved
../gradint.py:
make -C .. gradint.py
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -23,17 +23,14 @@
def initialGlue(): return Glue(0,maxLenOfLesson)
try: import bisect
try: from bisect import insort
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))
def book(self,start,finish): insort(self.bookedList,(start,finish))
earliestAllowedEvent = 0 # for "don't start before" hacks, so can keep all initial glue starting at 0
......@@ -123,10 +120,7 @@ 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
def sgn(a): return [1,-1][a<0]
class StretchedTooFar(Exception): pass
class GluedEvent(object):
......@@ -244,9 +238,9 @@ class Lesson(object):
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):
def addSequence(self,gluedEventList,canTrack=True):
bookIn(gluedEventList,self.schedule)
if not gluedListTracker==None: gluedListTracker.append(gluedEventList)
if not gluedListTracker==None and canTrack: gluedListTracker.append(gluedEventList)
glueStart = 0 ; lastI = None
for i in gluedEventList:
i.event.setOnLeaves("sequenceID",self.eventListCounter) # for max_lateness stuff
......@@ -268,7 +262,7 @@ class Lesson(object):
latenessCap = {} ; nextStart = 0
for t,event in self.events:
if nextStart:
for k in latenessCap.keys(): latenessCap[k] += (nextStart-(t+event.length)) # the gap
for k in list(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))
......@@ -315,17 +309,17 @@ def decide_subst_synth(cache_fname):
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 k,v in list(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 = B(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 cache_fname[:1]==B("_"): 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:
if (re and re.search(str,cache_fname)) or cache_fname.find(str)>=0:
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("_")]))
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(B("_"))]))
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -17,7 +17,7 @@ def init_scanSamples():
dirsWithIntros = []
filesWithExplanations = {}
singleLinePoems = {} # keys are any poem files which are single line only, so as to avoid saying 'beginning' in prompts
variantFiles = {} # careful with clearing this if prompts is using it also (hence called only below and in loop.py before prompt scan)
variantFiles = {} # maps dir+fname to (no dir+) fname list, main use is in fileToEvent. Careful with clearing this if prompts is using it also (hence init_scanSamples is called only below and in loop.py before prompt scan)
init_scanSamples() ; emptyCheck_hack = 0
def scanSamples(directory=None):
if not directory: directory=samplesDirectory
......@@ -59,7 +59,7 @@ def import_recordings(destDir=None):
if checkFirst:
for lang in [firstLanguage,secondLanguage]:
for ext in [dotwav,dotmp3]:
if f[:f.rfind(extsep)]+"_"+lang+ext in curFiles: raise CannotOverwriteExisting()
if checkIn(f[:f.rfind(extsep)]+"_"+lang+ext,curFiles): raise CannotOverwriteExisting()
continue
if not destDir:
if not getYN("Import the recordings that are in "+importDir+"?"): break
......@@ -74,7 +74,7 @@ def import_recordings(destDir=None):
try:
import shutil
shutil.copy2(importDir+os.sep+f,destDir+os.sep+f)
except: open(destDir+os.sep+f,"wb").write(read(importDir+os.sep+f))
except: write(destDir+os.sep+f,read(importDir+os.sep+f))
os.remove(importDir+os.sep+f)
numFound += 1
if numFound: open(destDir+os.sep+"settings"+dottxt,"w").write("firstLanguage=\""+firstLanguage+"\"\nsecondLanguage=\""+secondLanguage+"\"\n")
......@@ -83,7 +83,7 @@ def import_recordings(destDir=None):
def exec_in_a_func(x): # helper function for below (can't be nested in python 2.3)
# Also be careful of http://bugs.python.org/issue4315 (shadowing globals in an exec) - better do this in a dictionary
d={"firstLanguage":firstLanguage,"secondLanguage":secondLanguage}
exec x in d
exec (x,d)
return d["secondLanguage"],d["firstLanguage"]
def check_has_variants(directory,ls):
if directory==promptsDirectory: return True
......@@ -101,9 +101,9 @@ def getLsDic(directory):
if not (directory.find(exclude_from_scan)==-1): return {}
try: ls = os.listdir(directory)
except: return {} # (can run without a 'samples' directory at all if just doing synth)
if "settings"+dottxt in ls:
if checkIn("settings"+dottxt,ls):
# Sort out the o/p from import_recordings (and legacy record-with-HDogg.bat if anyone's still using that)
oddLanguage,evenLanguage = exec_in_a_func(u8strip(read(directory+os.sep+"settings"+dottxt).replace("\r\n","\n")).strip(wsp))
oddLanguage,evenLanguage = exec_in_a_func(wspstrip(u8strip(read(directory+os.sep+"settings"+dottxt).replace("\r\n","\n"))))
if oddLanguage==evenLanguage: oddLanguage,evenLanguage="_"+oddLanguage,"-meaning_"+evenLanguage # if user sets languages the same, assume they want -meaning prompts
else: oddLanguage,evenLanguage="_"+oddLanguage,"_"+evenLanguage
for f in ls:
......@@ -115,15 +115,15 @@ def getLsDic(directory):
os.rename(directory+os.sep+f,directory+os.sep+f[:i]+(("%0"+str(len(str(len(ls))))+"d") % (int((int(num)-1)/2)*2+1))+cond(int(num)%2,oddLanguage,evenLanguage)+f[f.rfind(extsep):])
os.remove(directory+os.sep+"settings"+dottxt)
ls = os.listdir(directory)
ls.sort() ; lsDic = {}
ls.sort()
lsDic = {} # key is file w/out extension but INCLUDING any variant number. Value is full filename if it's an extension we know about, "" if it's a file we can't process, or None if it's a directory (in which case key includes any 'extension' if the directory has one)
has_variants = check_has_variants(directory,ls)
for file in ls:
filelower = file.lower()
# in lsDic if it's in the list (any extension); =filename if it's an extension we know about; =None if it's a directory (in which case the key is the full filename), ottherwise =""
if has_variants and file.find("_",file.find("_")+1)>-1: languageOverride=file[file.find("_")+1:file.find("_",file.find("_")+1)]
if filelower.endswith(dottxt) and checkIn((file+extsep)[:file.rfind(extsep)],lsDic): continue # don't let a .txt override a recording if both exist with same variant number
if has_variants and file.find("_",file.find("_")+1)>=0: languageOverride=file[file.find("_")+1:file.find("_",file.find("_")+1)] # for can_be_synthesized below
else: languageOverride=None
if filelower.endswith(dottxt) and (file+extsep)[:file.rfind(extsep)] in lsDic: continue # don't let a .txt override a recording if both exist
if (filelower.endswith(dottxt) and file.find("_")>-1 and can_be_synthesized(file,directory,languageOverride)) or filelower.endswith(dotwav) or filelower.endswith(dotmp3): val = file
if (filelower.endswith(dottxt) and file.find("_")>=0 and can_be_synthesized(file,directory,languageOverride)) or filelower.endswith(dotwav) or filelower.endswith(dotmp3): val = file
else:
val = ""
if filelower.endswith(extsep+"zip"): show_warning("Warning: Ignoring "+file+" (please unpack it first)") # so you can send someone a zip file for their recorded words folder and they'll know what's up if they don't unpack it
......@@ -131,37 +131,49 @@ def getLsDic(directory):
lsDic[file]=None # a directory: store full name even if it has extsep in it. Note however that we don't check isDirectory() if it's .wav etc as that would take too long. (however some dirnames can contain dots)
# (+ NB need to store the directories specifically due to cases like course/ and course.pdf which may otherwise result in 2 traversals of "course" if we check isDirectory on 'extension is either none or unknown')
continue
elif (file+extsep)[:file.rfind(extsep)] in lsDic: continue # don't let a .txt~ or other unknown extension override a .txt
elif checkIn((file+extsep)[:file.rfind(extsep)],lsDic): continue # don't let a .txt~ or other unknown extension override a .txt
lsDic[(file+extsep)[:file.rfind(extsep)]] = val # (this means if there's both mp3 and wav, wav will overwrite as comes later)
if has_variants:
ls=list2set(ls) ; newVs = []
for k,v in lsDic.items():
ls=list2set(ls)
newVs = {} # variantFiles keys we added or changed
for k,v in list(lsDic.items()):
# check for _lang_variant.ext and take out the _variant,
# but keep them in variantFiles dict for fileToEvent to put back
if not v or (not directory==promptsDirectory and v.find("_explain_")>-1): continue # don't get confused by that
if not v or (not directory==promptsDirectory and v.find("_explain_")>=0): continue # skip directories, and don't get confused by explanation files
last_ = v.rfind("_")
if last_==-1: continue
penult_ = v.rfind("_",0,last_)
if penult_==-1: continue
del lsDic[k]
newK,newV = k[:k.rfind("_")], v[:v.rfind("_")]+v[v.rfind(extsep):]
if not newK in lsDic: lsDic[newK] = newV
else: # variants of different file types? better store them all under one (fileToEvent will sort out). (Testing if the txt can be synth'd has already been done above)
if v.endswith(dottxt) and not lsDic[newK].endswith(dottxt): # if any variants are .txt then we'd better ensure the key is, so transliterate etc finds it. So move the key over to the .txt one.
old_dirV = directory+os.sep+lsDic[newK]
# Now k = filename without extension but including a variant number, and v = full filename
del lsDic[k] # we don't want variant numbers in lsDic, we want them in variantFiles instead
newK,newV = k[:k.rfind("_")], v[:v.rfind("_")]+v[v.rfind(extsep):] # = k and v without the variant number (we'll add the real v to variantFiles[dir+newV] below, so it will be findable without variant number)
new_dirV = B(directory)+B(os.sep)+B(newV)
if not checkIn(newK,lsDic): # filename without variant number doesn't exist (for any extension)
lsDic[newK] = newV # so start it
assert not checkIn(new_dirV,variantFiles)
variantFiles[new_dirV] = [v]
elif v.endswith(dottxt) and not lsDic[newK].endswith(dottxt): # filename without variant number DOES exist (or we made the key when we saw a previous variant), and this new variant is .txt but the one without variant number is not. If any variants are .txt then we'd better ensure the key maps to a .txt file (so transliterate etc finds it) and recordings are counted as variants of this .txt file, rather than .txt as variants of recordings.
old_dirV = B(directory+os.sep+lsDic[newK]) # the variantFiles key for the recording(s) we've already put in lsDic (but it'll be in variantFiles only if it HAD a variant number when we saw it, which won't be the case if the first variant had no number)
if checkIn(old_dirV,variantFiles):
d = variantFiles[old_dirV]
del variantFiles[old_dirV]
lsDic[newK] = newV
variantFiles[directory+os.sep+newV] = d
lsDic[newK] = newV # just add to the previous key
else: newV = lsDic[newK]
dir_newV = directory+os.sep+newV
if not dir_newV in variantFiles:
variantFiles[dir_newV] = []
if newV in ls: variantFiles[dir_newV].append(newV) # the no-variants name is also a valid option
variantFiles[dir_newV].append(v)
newVs.append(dir_newV)
for v in newVs: random.shuffle(variantFiles[v])
variantFiles[new_dirV] = d
else: variantFiles[new_dirV] = [B(lsDic[newK])] # the recording had no variant number, but now we know it does have variants, so put in the recording as first variant of the .txt key
variantFiles[new_dirV].append(v)
if checkIn(old_dirV,newVs):
del newVs[old_dirV]
newVs[new_dirV] = 1
lsDic[newK] = newV
else: # filename without variant number does exist (or we made the key), and we need to add new variant
newV = lsDic[newK]
new_dirV = B(directory)+B(os.sep)+B(newV)
if not checkIn(new_dirV,variantFiles): # without variant number exists but isn't in variantFiles, so we need to add it as a variant before we add this new variant. We know the key from lsDic.
variantFiles[new_dirV] = [B(newV)]
variantFiles[new_dirV].append(v)
newVs[new_dirV]=1
for v in list(newVs.keys()):
assert checkIn(v,variantFiles), repr(sorted(list(variantFiles.keys())))+' '+repr(v)
random.shuffle(variantFiles[v])
return lsDic
def scanSamples_inner(directory,retVal,doLimit):
......@@ -169,14 +181,14 @@ def scanSamples_inner(directory,retVal,doLimit):
secLangSuffix = "_"+secondLanguage
lsDic = getLsDic(directory)
intro = intro_filename+"_"+firstLanguage
if intro in lsDic: dirsWithIntros.append((directory[len(samplesDirectory)+len(os.sep):],lsDic[intro]))
if not doLimit: doLimit = limit_filename in lsDic
doPoetry = poetry_filename in lsDic
if checkIn(intro,lsDic): dirsWithIntros.append((directory[len(samplesDirectory)+len(os.sep):],lsDic[intro]))
if not doLimit: doLimit = checkIn(limit_filename,lsDic)
doPoetry = checkIn(poetry_filename,lsDic)
if doPoetry:
# check which language the poetry is to be in (could be L1-to-L2, L2-to-L3, L2-only, or L3-only)
def poetry_language():
def poetry_language(firstLangSuffix,secLangSuffix,lsDic):
ret = ""
for file,withExt in lsDic.items():
for file,withExt in list(lsDic.items()):
if withExt:
if file.endswith(secLangSuffix): ret=secLangSuffix # but stay in the loop
elif (not file.endswith(firstLangSuffix)):
......@@ -184,11 +196,11 @@ def scanSamples_inner(directory,retVal,doLimit):
for l in otherLanguages:
if not l in llist and file.endswith("_"+l): return "_"+l
return ret
doPoetry = poetry_language()
doPoetry = poetry_language(firstLangSuffix,secLangSuffix,lsDic)
prefix = directory[len(samplesDirectory)+cond(samplesDirectory,len(os.sep),0):] # the directory relative to samplesDirectory
if prefix: prefix += os.sep
lastFile = None # for doPoetry
items = lsDic.items() ; items.sort()
items = list(lsDic.items()) ; items.sort()
for file,withExt in items:
swapWithPrompt = 0
if not withExt:
......@@ -204,7 +216,7 @@ def scanSamples_inner(directory,retVal,doLimit):
wordSuffix=None
for l in otherLanguages:
if not l in [firstLanguage,secondLanguage] and file.endswith("_"+l):
if l in otherFirstLanguages: swapWithPrompt=1
if checkIn(l,otherFirstLanguages): swapWithPrompt=1
wordSuffix="_"+l ; break
if not wordSuffix: continue # can't do anything with this file
if swapWithPrompt or firstLanguage==secondLanguage: promptFile=None
......@@ -227,7 +239,7 @@ def scanSamples_inner(directory,retVal,doLimit):
# poetry without first-language prompts
if lastFile:
promptToAdd = prefix+lastFile[-1]
if promptToAdd in singleLinePoems: del singleLinePoems[promptToAdd]
if checkIn(promptToAdd,singleLinePoems): del singleLinePoems[promptToAdd]
else:
promptToAdd = prefix+withExt # 1st line is its own prompt
singleLinePoems[promptToAdd]=1
......@@ -236,7 +248,7 @@ def scanSamples_inner(directory,retVal,doLimit):
retVal.append((0,promptToAdd,prefix+withExt))
if emptyCheck_hack: return
if explanationFile: filesWithExplanations[prefix+withExt]=explanationFile
if doLimit: limitedFiles[prefix+withExt]=prefix
if doLimit: limitedFiles[B(prefix+withExt)]=prefix
lastFile = [promptFile,withExt]
cache_maintenance_mode=0 # hack so cache-synth.py etc can cache promptless words for use in justSynthesize, and words in prompts themselves
......@@ -249,62 +261,62 @@ def parseSynthVocab(fname,forGUI=0):
if not fileExists(fname): return []
if not emptyCheck_hack: doLabel("Reading "+fname)
allLangs = list2set([firstLanguage,secondLanguage]+otherLanguages)
for l in u8strip(read(fname)).replace("\r","\n").split("\n"):
for l in u8strip(read(fname)).replace(B("\r"),B("\n")).split(B("\n")):
# TODO can we make this any faster on WinCE with large vocab lists? (tried SOME optimising already)
if not "=" in l: # might be a special instruction
if not B("=") in l: # might be a special instruction
if not l: continue
canProcess = 0 ; l2=l.strip(wsp)
if not l2 or l2[0]=='#': continue
canProcess = 0 ; l2=bwspstrip(l)
if not l2 or l2[0:1]==B('#'): continue
l2=l2.lower()
if l2.startswith("set language ") or l2.startswith("set languages "):
langs=l.split()[2:] ; someLangsUnknown = 0
if l2.startswith(B("set language ")) or l2.startswith(B("set languages ")):
langs=map(S,l.split()[2:]) ; someLangsUnknown = 0
maxsplit = len(langs)-1
for l in langs:
if not l in allLangs: someLangsUnknown = 1
elif l2.startswith("limit on"):
if not checkIn(l,allLangs): someLangsUnknown = 1
elif l2.startswith(B("limit on")):
doLimit = 1 ; limitNo += 1
elif l2.startswith("limit off"): doLimit = 0
elif l2.startswith("begin poetry"): doPoetry,lastPromptAndWord,disablePoem = True,None,False
elif l2.startswith("end poetry"): doPoetry = lastPromptAndWord = None
elif l2.startswith("poetry vocab line:"): doPoetry,lastPromptAndWord = 0,cond(lastPromptAndWord,lastPromptAndWord,0) # not None, in case we're at the very start of a poem (see "just processed"... at end)
elif l2.startswith(B("limit off")): doLimit = 0
elif l2.startswith(B("begin poetry")): doPoetry,lastPromptAndWord,disablePoem = True,None,False
elif l2.startswith(B("end poetry")): doPoetry = lastPromptAndWord = None
elif l2.startswith(B("poetry vocab line")): doPoetry,lastPromptAndWord = 0,cond(lastPromptAndWord,lastPromptAndWord,0) # not None, in case we're at the very start of a poem (see "just processed"... at end)
else: canProcess=1
if not canProcess: continue
elif "#" in l and l.strip(wsp)[0]=='#': continue # guard condition '"#" in l' improves speed
elif B('#') in l and bwspstrip(l)[0:1]==B('#'): continue # guard condition "'#' in l" improves speed
if forGUI: strCount=""
else:
strCount = "%05d!synth:" % (count,)
count += 1
langsAndWords = zip(langs,l.split("=",maxsplit)) # don't try strip on a map() - it's faster to do it as-needed below
langsAndWords = list(zip(langs,l.split(B("="),maxsplit))) # don't try strip on a map() - it's faster to do it as-needed below
# (maxsplit means you can use '=' signs in the last language, e.g. if using SSML with eSpeak)
if someLangsUnknown: langsAndWords = filter(lambda x:x[0] in allLangs, langsAndWords)
if someLangsUnknown: langsAndWords = filter(lambda x,a=allLangs:checkIn(x[0],a), langsAndWords)
# Work out what we'll use for the prompt. It could be firstLanguage, or it could be one of the other languages if we see it twice (e.g. if 2nd language is listed twice then the second one will be the prompt for 2nd-language-to-2nd-language learning), or it could be the only language if we're simply listing words for cache maintenance
if firstLanguage==secondLanguage: langsAlreadySeen = {}
else: langsAlreadySeen = {firstLanguage:True}
def findPrompt():
def findPrompt(langsAndWords,langsAlreadySeen,doPoetry,strCount):
i=0
while i<len(langsAndWords):
lang,word = langsAndWords[i] ; i += 1
isReminder = cache_maintenance_mode and len(langsAndWords)==1 and not doPoetry
if (lang in langsAlreadySeen or isReminder) and (lang in getsynth_cache or can_be_synthesized("!synth:"+word+"_"+lang)): # (check cache because most of the time it'll be there and we don't need to go through all the text processing in can_be_synthesized)
if (lang in langsAlreadySeen or isReminder) and (lang in getsynth_cache or can_be_synthesized(B("!synth:")+B(word)+B("_")+B(lang))): # (check cache because most of the time it'll be there and we don't need to go through all the text processing in can_be_synthesized)
if not word: continue
elif word[0] in wsp or word[-1] in wsp: word=word.strip(wsp) # avoid call if unnecessary
return strCount+word+"_"+lang, cond(isReminder,0,i)
elif word[0:1] in bwsp or word[-1:] in bwsp: word=bwspstrip(word) # avoid call if unnecessary
return B(strCount)+word+B("_"+lang), cond(isReminder,0,i)
langsAlreadySeen[lang]=True
return None,0
prompt,onePastPromptIndex = findPrompt()
prompt,onePastPromptIndex = findPrompt(langsAndWords,langsAlreadySeen,doPoetry,strCount)
if not prompt and len(langsAndWords)>1: # 1st language prompt not found; try 2nd language to 3rd language etc
langsAlreadySeen = list2dict(otherFirstLanguages) ; prompt,onePastPromptIndex = findPrompt()
langsAlreadySeen = list2dict(otherFirstLanguages) ; prompt,onePastPromptIndex = findPrompt(langsAndWords,langsAlreadySeen,doPoetry,strCount)
if not prompt:
langsAlreadySeen = {secondLanguage:True} ; prompt,onePastPromptIndex = findPrompt()
langsAlreadySeen = {secondLanguage:True} ; prompt,onePastPromptIndex = findPrompt(langsAndWords,langsAlreadySeen,doPoetry,strCount)
prompt_L1only = prompt # before we possibly change it into a list etc. (Actually not necessarily L1 see above, but usually is)
if doPoetry:
if prompt and lastPromptAndWord:
if lastPromptAndWord[0]: prompt=[lastPromptAndWord[0],prompt,lastPromptAndWord[1]] # L1 for line 1, L1 for line2, L2 for line 1
else: prompt=[lastPromptAndWord[1],prompt] # line 1 doesn't have L1 but line 2 does, so have L2 for line 1 + L1 for line 2
if lastPromptAndWord[0]: prompt=[S(lastPromptAndWord[0]),S(prompt),S(lastPromptAndWord[1])] # L1 for line 1, L1 for line2, L2 for line 1
else: prompt=[S(lastPromptAndWord[1]),S(prompt)] # line 1 doesn't have L1 but line 2 does, so have L2 for line 1 + L1 for line 2
elif not prompt:
if lastPromptAndWord:
prompt=lastPromptAndWord[-1]
if lastPromptAndWord[-1] in singleLinePoems: del singleLinePoems[lastPromptAndWord[-1]]
if checkIn(lastPromptAndWord[-1],singleLinePoems): del singleLinePoems[lastPromptAndWord[-1]]
else:
prompt = 1 # file itself (see below)
if prompt:
......@@ -312,53 +324,55 @@ def parseSynthVocab(fname,forGUI=0):
while i<len(langsAndWords):
lang,word = langsAndWords[i] ; i+=1
if i==onePastPromptIndex or (lang==firstLanguage and not firstLanguage==secondLanguage) or not word: continue # if 1st language occurs more than once (target as well as prompt) then don't get confused - this vocab file is probably being used with reverse settings
elif word[0] in wsp or word[-1] in wsp: word=word.strip(wsp) # avoid call if unnecessary
if lang in getsynth_cache or can_be_synthesized("!synth:"+word+"_"+lang):
elif word[0:1] in bwsp or word[-1:] in bwsp: word=bwspstrip(word) # avoid call if unnecessary
if checkIn(lang,getsynth_cache) or can_be_synthesized(B("!synth:")+word+B("_"+lang)):
if not (doPoetry and disablePoem):
f=strCount+word+"_"+lang
f=B(strCount)+word+B("_"+lang)
if prompt==1 or prompt==f: # a file with itself as the prompt (either explicitly or by omitting any other prompt)
prompt=f
singleLinePoems[f]=1
ret.append((0,prompt,f))
ret.append((0,S(prompt),S(f)))
if emptyCheck_hack: return ret
if doLimit: limitedFiles[f]="synth:"+str(limitNo)
if doLimit: limitedFiles[f]=B("synth:"+str(limitNo))
if doPoetry: lastPromptAndWord = [prompt_L1only,f]
elif doPoetry: disablePoem=1 # if one of the lines can't be synth'd, disable the rest of the poem (otherwise get wrongly connected lines, disconnected lines, or re-introduction of isolated lines that were previously part of a poem but can't be synth'd on this platform)
if not lastPromptAndWord==None: doPoetry = 1 # just processed a "poetry vocab line:" (lastPromptAndWord is either the real last prompt and word, or 0 if we were at the start)
if not lastPromptAndWord==None: doPoetry = 1 # just processed a "poetry vocab line" (lastPromptAndWord is either the real last prompt and word, or 0 if we were at the start)
return ret
def sanitise_otherLanguages():
for l in otherFirstLanguages:
if not l in otherLanguages: otherLanguages.append(l)
if not checkIn(l,otherLanguages): otherLanguages.append(l)
for l in otherLanguages:
if not l in possible_otherLanguages: possible_otherLanguages.append(l)
if not checkIn(l,possible_otherLanguages): possible_otherLanguages.append(l)
sanitise_otherLanguages()
# Prompt file syntax: word_language.wav
# or: word_language_2.wav .. (alternatives chosen at random)
# ('word' can also be a language name)
class PromptException(Exception):
class MessageException(Exception):
def __init__(self,message): self.message = message
def __repr__(self): return self.message
class PromptException(MessageException): pass
auto_advancedPrompt=0 # used by gradint.cgi
class AvailablePrompts(object):
reservedPrefixes = list2set(map(lambda x:x.lower(),["whatmean","meaningis","repeatAfterMe","sayAgain","longPause","begin","end",firstLanguage,secondLanguage] + possible_otherLanguages))
def __init__(self):
self.lsDic = getLsDic(promptsDirectory)
self.prefixes = {}
for k,v in self.lsDic.items():
for k,v in list(self.lsDic.items()):
if v: self.prefixes[k[:k.rfind("_")]]=1 # delete language
else: del self.lsDic[k] # !poetry etc doesn't make sense in prompts
self.prefixes = self.prefixes.keys()
self.prefixes = list(self.prefixes.keys())
self.user_is_advanced = None
def getRandomPromptList(self,promptsData,language):
random.shuffle(self.prefixes)
for p in self.prefixes:
if p.lower() in self.reservedPrefixes: continue
if checkIn(p.lower(),self.reservedPrefixes): continue
try:
theList = self.getPromptList(p,promptsData,language)
return theList
except PromptException: pass
raise PromptException("Can't find a non-reserved prompt suitable for language '%s'" % (language))
raise PromptException("Can't find a non-reserved prompt suitable for language '%s'. Try creating tryToSay_%s%s etc in %s" % (language,language,dotwav,promptsDirectory))
def getPromptList(self,prefix,promptsData,language):
# used for introducing foreign-language prompts to
# beginners. language is the suffix of the language we're *learning*.
......@@ -368,11 +382,11 @@ class AvailablePrompts(object):
if p > advancedPromptThreshold2:
self.user_is_advanced = 1 ; break # got a reasonably advanced user
beginnerPrompt = prefix+"_"+firstLanguage
if not beginnerPrompt in self.lsDic:
if not checkIn(beginnerPrompt,self.lsDic):
if self.user_is_advanced and not language==secondLanguage and prefix+"_"+secondLanguage in self.lsDic: beginnerPrompt=prefix+"_"+secondLanguage # No first language prompt, but in advanced mode may be able to find a second-language prompt for a 3rd language
else: beginnerPrompt = None
advancedPrompt = prefix+"_"+language
if not advancedPrompt in self.lsDic:
if not checkIn(advancedPrompt,self.lsDic):
# Must use beginnerPrompt
if beginnerPrompt: r=[self.lsDic[beginnerPrompt]]
else:
......@@ -380,7 +394,7 @@ class AvailablePrompts(object):
else: raise PromptException("Can't find "+prefix+"_"+language+", "+prefix+"_"+firstLanguage+" or "+prefix+"_"+secondLanguage)
elif not beginnerPrompt:
# Must use advancedPrompt
if (not self.user_is_advanced) and cond(language==secondLanguage,advancedPromptThreshold,advancedPromptThreshold2): raise PromptException("Prompt '%s' is too advanced; need '%s_%s' (unless you set %s=0 in advanced%stxt)" % (advancedPrompt,prefix,firstLanguage,cond(language==secondLanguage,"advancedPromptThreshold","advancedPromptThreshold2"),extsep))
if (not self.user_is_advanced) and not auto_advancedPrompt and cond(language==secondLanguage,advancedPromptThreshold,advancedPromptThreshold2): raise PromptException("Prompt '%s' is too advanced; need '%s_%s' (unless you set %s=0 in advanced%stxt)" % (advancedPrompt,prefix,firstLanguage,cond(language==secondLanguage,"advancedPromptThreshold","advancedPromptThreshold2"),extsep))
r=[self.lsDic[advancedPrompt]]
elif promptsData.get(advancedPrompt,0) >= cond(language==secondLanguage,advancedPromptThreshold,advancedPromptThreshold2): r=[self.lsDic[advancedPrompt]]
elif promptsData.get(advancedPrompt,0) >= cond(language==secondLanguage,transitionPromptThreshold,transitionPromptThreshold2): r=[self.lsDic[advancedPrompt], self.lsDic[beginnerPrompt]]
......@@ -393,7 +407,7 @@ class AvailablePrompts(object):
# Increment advancedPrompt, taking care not to go
# past the threshold if it's not available yet
adv = promptsData.get(advancedPrompt,0)
if advancedPrompt in self.lsDic or adv <= cond(language==secondLanguage,transitionPromptThreshold,transitionPromptThreshold2):
if checkIn(advancedPrompt,self.lsDic) or adv <= cond(language==secondLanguage,transitionPromptThreshold,transitionPromptThreshold2):
adv += 1
promptsData[advancedPrompt] = adv
# and finally,
......@@ -406,14 +420,14 @@ def introductions(zhFile,progressData):
for d,fname in dirsWithIntros[:]:
found = 0
for p in progressData:
if p[-1].startswith(d) and p[0]:
if B(p[-1]).startswith(B(d)) and p[0]:
# this dir has already been introduced
found=1 ; dirsWithIntros.remove((d,fname)) ; break
if found: continue
if zhFile.startswith(d): toIntroduce.append((d,fname))
if B(zhFile).startswith(B(d)): toIntroduce.append((d,fname))
toIntroduce.sort() # should put shorter ones 1st
return map(lambda (x,fname): fileToEvent(cond(x,x+os.sep,"")+fname), toIntroduce)
return map(lambda x: fileToEvent(cond(x[0],x[0]+os.sep,"")+x[1]), toIntroduce)
def explanations(zhFile):
if zhFile in filesWithExplanations: return fileToEvent(zhFile.replace(dotmp3,dotwav).replace(dottxt,dotwav).replace(dotwav,"_explain_"+firstLanguage+filesWithExplanations[zhFile][-len(dotwav):]))
if checkIn(zhFile,filesWithExplanations): return fileToEvent(zhFile.replace(dotmp3,dotwav).replace(dottxt,dotwav).replace(dotwav,"_explain_"+firstLanguage+filesWithExplanations[zhFile][-len(dotwav):]))
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -23,17 +23,19 @@ appTitle += time.strftime(" %A") # in case leave 2+ instances on the desktop
def waitOnMessage(msg):
global warnings_printed
if type(msg)==type(u""): msg2=msg.encode("utf-8")
else: msg2=msg
else:
try: msg2,msg=msg,msg.decode("utf-8")
except AttributeError: msg2=msg # Python 2.1 has no .decode
if appuifw:
t=appuifw.Text() ; t.add(u"".join(warnings_printed)+msg) ; appuifw.app.body = t # in case won't fit in the query() (and don't use note() because it doesn't wait)
appuifw.query(u""+msg,'query')
appuifw.query(msg,'query')
elif android:
# android.notify("Gradint","".join(warnings_printed)+msg) # doesn't work?
android.dialogCreateAlert("Gradint","".join(warnings_printed)+msg)
android.dialogSetPositiveButtonText("OK")
android.dialogShow() ; android.dialogGetResponse()
elif app:
if not (winsound or winCEsound or mingw32 or cygwin): show_info(msg2+"\n\nWaiting for you to press OK on the message box... ",True) # in case terminal is in front
if not (winsound or winCEsound or mingw32 or cygwin): show_info(msg2+B("\n\nWaiting for you to press OK on the message box... "),True) # in case terminal is in front
app.todo.alert = "".join(warnings_printed)+msg
while True:
try:
......@@ -42,8 +44,8 @@ def waitOnMessage(msg):
time.sleep(0.5)
if not (winsound or winCEsound or mingw32 or cygwin): show_info("OK\n",True)
else:
if clearScreen(): msg2 = "This is "+program_name.replace("(c)","\n(c)")+"\n\n"+msg2 # clear screen is less confusing for beginners, but NB it may not happen if warnings etc
show_info(msg2+"\n\n"+cond(winCEsound,"Press OK to continue\n","Press Enter to continue\n"))
if clearScreen(): msg2 = B("This is "+program_name.replace("(c)","\n(c)")+"\n\n")+msg2 # clear screen is less confusing for beginners, but NB it may not happen if warnings etc
show_info(msg2+B("\n\n"+cond(winCEsound,"Press OK to continue\n","Press Enter to continue\n")))
sys.stderr.flush() # hack because some systems don't do it (e.g. some mingw32 builds), and we don't want the user to fail to see why the program is waiting (especially when there's an error)
try:
raw_input(cond(winCEsound,"See message under this window.","")) # (WinCE uses boxes for raw_input so may need to repeat the message - but can't because the prompt is size-limited, so need to say look under the window)
......@@ -54,13 +56,14 @@ def waitOnMessage(msg):
def getYN(msg,defaultIfEof="n"):
if appuifw:
appuifw.app.body = None
return appuifw.query(u""+msg,'query')
return appuifw.query(ensure_unicode(msg),'query')
elif android:
android.dialogCreateAlert("Gradint",msg)
android.dialogSetPositiveButtonText("Yes") # TODO do we have to localise this ourselves or can we have a platform default?
android.dialogSetNegativeButtonText("No")
android.dialogShow()
return android.dialogGetResponse().result['which'] == 'positive'
try: return android.dialogGetResponse().result['which'] == 'positive'
except KeyError: return 0 # or raise SystemExit, no 'which'
elif app:
app.todo.question = localise(msg)
while app and not hasattr(app,"answer_given"): time.sleep(0.5)
......@@ -74,7 +77,7 @@ def getYN(msg,defaultIfEof="n"):
while not ans=='y' and not ans=='n':
try: ans = raw_input("%s\nPress y for yes, or n for no. Then press Enter. --> " % (msg,))
except EOFError:
ans=defaultIfEof ; print ans
ans=defaultIfEof ; print (ans)
clearScreen() # less confusing for beginners
if ans=='y': return 1
return 0
......@@ -89,7 +92,7 @@ def primitive_synthloop():
old_js = justSynthesize
if appuifw:
if not justSynthesize: justSynthesize=""
justSynthesize=appuifw.query(u"Say:","text",u""+justSynthesize)
justSynthesize=appuifw.query(u"Say:","text",ensure_unicode(justSynthesize))
if justSynthesize: justSynthesize=justSynthesize.encode("utf-8")
else: break
else:
......@@ -102,18 +105,18 @@ def primitive_synthloop():
if (winCEsound or riscos_sound or android) and not justSynthesize: break # because no way to send EOF (and we won't be taking i/p from a file)
if interactive and not readline:
interactive="('a' for again) Say: "
if justSynthesize=="a": justSynthesize=old_js
if B(justSynthesize)==B("a"): justSynthesize=old_js
oldLang = lang
if justSynthesize: lang = just_synthesize(interactive,lang)
if justSynthesize: lang = S(just_synthesize(interactive,lang))
# and see if it transliterates:
if justSynthesize and lang and not "#" in justSynthesize:
if justSynthesize.startswith(lang+" "):
if justSynthesize and lang and not B('#') in B(justSynthesize):
if B(justSynthesize).startswith(B(lang)+B(" ")):
t = transliterates_differently(justSynthesize[len(lang+" "):],lang)
if t: t=lang+" "+t
else: t = transliterates_differently(justSynthesize,lang)
if t:
if appuifw: justSynthesize = t
else: show_info("Spoken as "+t+"\n")
else: show_info(B("Spoken as ")+t+B("\n"))
if warnings_printed: # at end not beginning, because don't want to overwrite the info message if appuifw
if appuifw:
t=appuifw.Text()
......@@ -124,6 +127,9 @@ def primitive_synthloop():
warnings_printed = []
if not lang: lang=oldLang
if android:
if not isDirectory("/mnt/sdcard/svox") and not isDirectory("/system/tts/lang_pico"): waitOnMessage("English voice might not be installed. Check under Home > Menu > Settings > Voice output > text to speech > Pico > English")
def startBrowser(url): # true if success
if winCEsound: return None # user might be paying per byte! + difficult to switch back if no Alt-Tab program
try:
......@@ -131,7 +137,7 @@ def startBrowser(url): # true if success
g=webbrowser.get()
except: g=0
if g and (winCEsound or macsound or (hasattr(g,"background") and g.background) or (hasattr(webbrowser,"BackgroundBrowser") and g.__class__==webbrowser.BackgroundBrowser) or (hasattr(webbrowser,"Konqueror") and g.__class__==webbrowser.Konqueror)):
return g.open_new(url)
return g.open_new(S(url))
# else don't risk it - it might be text-mode and unsuitable for multitask-with-gradint
if winsound: return not os.system('start "%ProgramFiles%\\Internet Explorer\\iexplore.exe" '+url) # use os.system not system here (don't know why but system() doesn't always work for IE)
# (NB DON'T replace % with %%, it doesn't work. just hope nobody set an environment variable to any hex code we're using in mp3web)
......@@ -144,7 +150,7 @@ def clearScreen():
warnings_printed = []
return
if winsound or mingw32: os.system("cls")
else: os.system("clear 1>&2") # (1>&2 in case using stdout for something else)
else: os.system("clear >&2") # (>&2 in case using stdout for something else)
return True
cancelledFiles = []
......@@ -164,17 +170,17 @@ def handleInterrupt(): # called only if there was an interrupt while the runner
# cancelledEvent = runner.queue[0][-1][0] worked in python 2.3, but sched implementation seems to have changed in python 2.5 so we're using copy_of_runner_events instead
if hasattr(cancelledEvent,"wordToCancel") and cancelledEvent.wordToCancel: cancelledFiles.append(cancelledEvent.wordToCancel)
if not app and needCountItems and cancelledFiles: show_info("(%d cancelled items)...\n" % len(cancelledFiles))
global repeatMode ; repeatMode = 0 # so Ctrl-C on justSynth-with-R works
global repeatMode ; repeatMode = "interrupted"
tkNumWordsToShow = 10 # the default number of list-box items
def addStatus(widget,status,mouseOnly=0):
# Be VERY CAREFUL with status line changes. Don't do it on things that are focused by default (except with mouseOnly=1). Don't do it when the default status line might be the widest thing (i.e. when list box is not displayed) or window size could jump about too much. And in any event don't use lines longer than about 53 characters (the approx default width of the listbox when using monospace fonts).
# (NB addStatus now takes effect only when the list box is displayed anyway, so OK for buttons that might also be displayed without it)
widget.bind('<Enter>',lambda *args:app.set_statusline(status))
widget.bind('<Enter>',lambda e=None,status=status:app.set_statusline(status))
widget.bind('<Leave>',app.restore_statusline)
if not mouseOnly:
widget.bind('<FocusIn>',lambda *args:app.set_statusline(status))
widget.bind('<FocusIn>',lambda e=None,app=app,status=status:app.set_statusline(status))
widget.bind('<FocusOut>',app.restore_statusline)
def makeButton(parent,text,command):
button = Tkinter.Button(parent)
......@@ -199,12 +205,17 @@ def addLabel(row,label):
def CXVMenu(e): # callback for right-click
e.widget.focus()
m=Tkinter.Menu(None, tearoff=0, takefocus=0)
ctrl=cond(macsound,"<Command-","<Control-")
m.add_command(label="Cut",command=(lambda e=e: e.widget.event_generate(ctrl+'x>')))
m.add_command(label="Copy",command=(lambda e=e: e.widget.event_generate(ctrl+'-c>')))
m.add_command(label="Paste",command=(lambda e=e: e.widget.event_generate(ctrl+'-v>')))
m.add_command(label="Delete",command=(lambda e=e: e.widget.event_generate('<Delete>')))
m.add_command(label="Select All",command=(lambda e=e: selectAll(e)))
if macsound:
cut,copy,paste = "<<Cut>>","<<Copy>>","<<Paste>>"
else:
ctrl="<Control-"
cut,copy,paste = ctrl+'x>',ctrl+'c>',ctrl+'v>'
def evgen(e,cmd): e.widget.event_generate(cmd)
funclist = [("Paste",paste),("Delete",'<Delete>')]
if not macsound:
funclist = [("Cut",cut),("Copy",copy)]+funclist # doesn't work reliably on Mac Tk
for l,cmd in funclist: m.add_command(label=l,command=(lambda e=e,c=cmd,evgen=evgen: e.widget.after(10,evgen,e,c)))
m.add_command(label="Select All",command=(lambda e=e: e.widget.after(10,selectAll,e)))
m.tk_popup(e.x_root-3, e.y_root+3,entry="0")
def selectAll(e):
e.widget.event_generate('<Home>')
......@@ -218,13 +229,15 @@ def addTextBox(row,wide=0):
text = Tkinter.StringVar(row)
entry = Tkinter.Entry(row,textvariable=text)
entry.bind('<ButtonRelease-3>',CXVMenu)
if macsound: entry.bind('<Control-ButtonRelease-1>',CXVMenu)
if macsound:
entry.bind('<Control-ButtonRelease-1>',CXVMenu)
entry.bind('<ButtonRelease-2>',CXVMenu)
if winCEsound:
if WMstandard: # non-numeric inputs no good on WMstandard Tkinter
def doRawInput(text,entry):
app.input_to_set = text
app.menu_response="input"
entry.bind('<Return>',lambda e:doRawInput(text,entry))
entry.bind('<Return>',lambda e,doRawInput=doRawInput,text=text,entry=entry:doRawInput(text,entry))
if wide: # put help in 1st wide textbox
global had_doRawInput
try: had_doRawInput
......@@ -233,10 +246,10 @@ def addTextBox(row,wide=0):
text.set("(Push OK to type A-Z)") # (if changing this message, change it below too)
class E: pass
e=E() ; e.widget = entry
entry.after(10,lambda *args:selectAll(e))
entry.after(10,lambda _=None,e=e:selectAll(e))
else: # PocketPC: try to detect long clicks. This is awkward. time.time is probably 1sec resolution so will get false +ves if go by that only.
def timeStamp(entry): entry.buttonPressTime=time.time()
entry.bind('<ButtonPress-1>',lambda e:timeStamp(entry))
entry.bind('<ButtonPress-1>',lambda e,timeStamp=timeStamp,entry=entry:timeStamp(entry))
global lastDblclkAdvisory,lastDblclk
lastDblclkAdvisory=lastDblclk=0
def pasteInstructions(t):
......@@ -248,8 +261,8 @@ def addTextBox(row,wide=0):
def doPaste(text,entry):
text.set(entry.selection_get(selection="CLIPBOARD"))
global lastDblclk ; lastDblclk=time.time()
entry.bind('<ButtonRelease-1>',lambda e:pasteInstructions(time.time()-getattr(entry,"buttonPressTime",time.time())))
entry.bind('<Double-Button-1>',lambda e:doPaste(text,entry))
entry.bind('<ButtonRelease-1>',lambda e,entry=entry,pasteInstructions=pasteInstructions:pasteInstructions(time.time()-getattr(entry,"buttonPressTime",time.time())))
entry.bind('<Double-Button-1>',lambda e,doPaste=doPaste,text=text,entry=entry:doPaste(text,entry))
# Tkinter bug workaround (some versions): event_generate from within a key event handler can be unreliable, so the Ctrl-A handler delays selectAll by 10ms:
entry.bind(cond(macsound,'<Command-a>','<Control-a>'),(lambda e:e.widget.after(10,lambda e=e:selectAll(e))))
bindUpDown(entry,False)
......@@ -286,14 +299,14 @@ def make_output_row(parent):
# if there aren't any options then return None
# we also put script-variant selection here, if any
row = None
def getRow(row):
def getRow(row,parent):
if not row:
row = Tkinter.Frame(parent)
row.pack(fill=Tkinter.X,expand=1)
return row
GUIlang = GUI_languages.get(firstLanguage,firstLanguage)
if "@variants-"+GUIlang in GUI_translations: # the firstLanguage has script variants
row=getRow(row)
if checkIn("@variants-"+GUIlang,GUI_translations): # the firstLanguage has script variants
row=getRow(row,parent)
if not hasattr(app,"scriptVariant"): app.scriptVariant = Tkinter.StringVar(app)
count = 0
for variant in GUI_translations["@variants-"+GUIlang]:
......@@ -301,7 +314,7 @@ def make_output_row(parent):
count += 1
app.scriptVariant.set(str(scriptVariants.get(GUIlang,0)))
if synth_partials_voices and guiVoiceOptions:
row=getRow(row)
row=getRow(row,parent)
if not hasattr(app,"voiceOption"): app.voiceOption = Tkinter.StringVar(app)
Tkinter.Radiobutton(row, text=u" Normal ", variable=app.voiceOption, value="", indicatoron=forceRadio).pack({"side":"left"})
for o in guiVoiceOptions: Tkinter.Radiobutton(row, text=u" "+o[1].upper()+o[2:]+u" ", variable=app.voiceOption, value=o, indicatoron=forceRadio).pack({"side":"left"})
......@@ -310,9 +323,9 @@ def make_output_row(parent):
if not hasattr(app,"outputTo"):
app.outputTo = Tkinter.StringVar(app) # NB app not parent (as parent is no longer app)
app.outputTo.set("0") # not "" or get tri-state boxes on OS X 10.6
row=getRow(row)
row=getRow(row,parent)
rightrow = addRightRow(row) # to show beginners this row probably isn't the most important thing despite being in a convenient place, we'll right-align
def addFiletypeButton(fileType):
def addFiletypeButton(fileType,rightrow):
ftu = fileType.upper()
t = Tkinter.Radiobutton(rightrow, text=cond(forceRadio,""," ")+ftu+" ", variable=app.outputTo, value=fileType, indicatoron=forceRadio)
bindUpDown(t,True)
......@@ -326,13 +339,13 @@ def make_output_row(parent):
addStatus(t,"Select this to send all sounds to\nthe speaker, not to files on disk")
bindUpDown(t,True)
t.pack({"side":"left"})
if got_program("lame"): addFiletypeButton("mp3")
if got_windows_encoder: addFiletypeButton("wma")
if got_program("neroAacEnc") or got_program("faac") or got_program("afconvert"): addFiletypeButton("aac")
if got_program("oggenc"): addFiletypeButton("ogg")
if got_program("toolame"): addFiletypeButton("mp2")
if got_program("speexenc"): addFiletypeButton("spx")
addFiletypeButton("wav")
if got_program("lame"): addFiletypeButton("mp3",rightrow)
if got_windows_encoder: addFiletypeButton("wma",rightrow)
if got_program("neroAacEnc") or got_program("faac") or got_program("afconvert"): addFiletypeButton("aac",rightrow)
if got_program("oggenc") or got_program("oggenc2"): addFiletypeButton("ogg",rightrow)
if got_program("toolame"): addFiletypeButton("mp2",rightrow)
if got_program("speexenc"): addFiletypeButton("spx",rightrow)
addFiletypeButton("wav",rightrow)
# "Get MP3 encoder" and "Get WMA encoder" changed to "MP3..." and "WMA..." to save width (+ no localisation necessary)
if unix and not got_program("lame") and got_program("make") and got_program("gcc") and (got_program("curl") or got_program("wget")): addButton(rightrow,"MP3...",app.getEncoder,status="Press this to compile an MP3 encoder\nso Gradint can output to MP3 files") # (checking gcc as well as make because some distros strangely have make but no compiler; TODO what if has a non-gcc compiler)
# (no longer available) elif (winsound or mingw32) and not got_windows_encoder and not got_program("lame"): addButton(rightrow,"WMA...",app.getEncoder,status="Press this to download a WMA encoder\nso Gradint can output to WMA files")
......@@ -341,19 +354,19 @@ def make_output_row(parent):
def updateSettingsFile(fname,newVals):
# leaves comments etc intact, but TODO does not cope with changing variables that have been split over multiple lines
replacement_lines = []
try: oldLines=u8strip(read(fname)).replace("\r\n","\n").split("\n")
try: oldLines=u8strip(read(fname)).replace(B("\r\n"),B("\n")).split(B("\n"))
except IOError: oldLines=[]
for l in oldLines:
found=0
for k in newVals.keys():
if l.startswith(k):
replacement_lines.append(k+"="+repr(newVals[k]))
for k in list(newVals.keys()):
if l.startswith(B(k)):
replacement_lines.append(B(k+"="+repr(newVals[k])))
del newVals[k]
found=1
if not found: replacement_lines.append(l)
for k,v in newVals.items(): replacement_lines.append(k+"="+repr(v))
if replacement_lines and replacement_lines[-1]: replacement_lines.append("") # ensure blank line at end so there's a \n but we don't add 1 more with each save
open(fname,"w").write("\n".join(replacement_lines))
for k,v in list(newVals.items()): replacement_lines.append(B(k+"="+repr(v)))
if replacement_lines and replacement_lines[-1]: replacement_lines.append(B("")) # ensure blank line at end so there's a \n but we don't add 1 more with each save
writeB(open(fname,"w"),B("\n").join(replacement_lines))
def asUnicode(x): # for handling the return value of Tkinter entry.get()
try: return u""+x # original behaviour
......@@ -362,16 +375,27 @@ def asUnicode(x): # for handling the return value of Tkinter entry.get()
except: return x.decode("iso-8859-1") # TODO can we get what it actually IS? (on German WinXP, sys.getdefaultencoding==ascii and locale==C but Tkinter still returns Latin1)
def setupScrollbar(parent,rowNo):
onLeft = winCEsound or olpc
s = Tkinter.Scrollbar(parent,takefocus=0)
s.grid(row=rowNo,column=cond(winCEsound or olpc,0,1),sticky="ns"+cond(winCEsound or olpc,"w","e"))
s.grid(row=rowNo,column=cond(onLeft,0,1),sticky="ns"+cond(onLeft,"w","e"))
try: parent.rowconfigure(rowNo,weight=1)
except: pass
c=Tkinter.Canvas(parent,bd=0,width=200,height=100,yscrollcommand=s.set)
c.grid(row=rowNo,column=cond(winCEsound or olpc,1,0),sticky="nsw")
c.grid(row=rowNo,column=cond(onLeft,1,0),sticky="nsw")
s.config(command=c.yview)
scrolledFrame=Tkinter.Frame(c) ; c.create_window(0,0,window=scrolledFrame,anchor="nw")
# Mousewheel binding. TODO the following bind_all assumes only one scrolledFrame on screen at once (redirect all mousewheel events to the frame; necessary as otherwise they'll go to buttons etc)
scrolledFrame.bind_all('<Button-4>',lambda *args:c.yview("scroll","-1","units"))
scrolledFrame.bind_all('<Button-5>',lambda *args:c.yview("scroll","1","units"))
# DON'T bind <MouseWheel> on Windows - our version of Tk will segfault when it occurs. See http://mail.python.org/pipermail/python-bugs-list/2005-May/028768.html but we can't patch our library.zip's Tkinter anymore (TODO can we use newer Tk DLLs and ensure setup.bat updates them?)
app.ScrollUpHandler = lambda e=None,c=c:c.yview("scroll","-1","units")
app.ScrollDownHandler = lambda e=None,c=c:c.yview("scroll","1","units")
if macsound:
def ScrollHandler(event):
if event.delta>0: app.ScrollUpHandler()
else: app.ScrollDownHandler()
scrolledFrame.bind_all('<MouseWheel>',ScrollHandler)
# DON'T bind <MouseWheel> on Windows - our version of Tk will segfault when it occurs. See http://mail.python.org/pipermail/python-bugs-list/2005-May/028768.html but we can't patch our library.zip's Tkinter anymore (TODO can we use newer Tk DLLs and ensure setup.bat updates them?)
else: # for X11:
scrolledFrame.bind_all('<Button-4>',app.ScrollUpHandler)
scrolledFrame.bind_all('<Button-5>',app.ScrollDownHandler)
return scrolledFrame, c
# GUI presets buttons:
......@@ -379,8 +403,8 @@ shortDescriptionName = "short-description"+dottxt
longDescriptionName = "long-description"+dottxt
class ExtraButton(object):
def __init__(self,directory):
self.shortDescription = u8strip(read(directory+os.sep+shortDescriptionName)).strip(wsp)
if fileExists(directory+os.sep+longDescriptionName): self.longDescription = u8strip(read(directory+os.sep+longDescriptionName)).strip(wsp)
self.shortDescription = wspstrip(u8strip(read(directory+os.sep+shortDescriptionName)))
if fileExists(directory+os.sep+longDescriptionName): self.longDescription = wspstrip(u8strip(read(directory+os.sep+longDescriptionName)))
else: self.longDescription = self.shortDescription
self.directory = directory
def add(self):
......@@ -397,7 +421,7 @@ class ExtraButton(object):
try: ls = os.listdir(samplesDirectory)
except: os.mkdir(samplesDirectory)
name1=newName
while newName in ls: newName+="1"
while checkIn(newName,ls): newName+="1"
name2=newName
newName = samplesDirectory+os.sep+newName
os.rename(self.directory,newName)
......@@ -406,13 +430,13 @@ class ExtraButton(object):
which_collection = localise(" has been added to your collection.")
o=open(vocabFile,"a")
o.write("# --- BEGIN "+self.shortDescription+" ---\n")
o.write(u8strip(read(newName+os.sep+"add-to-vocab"+dottxt)).strip(wsp)+"\n")
o.write(wspstrip(u8strip(read(newName+os.sep+"add-to-vocab"+dottxt)))+"\n")
o.write("# ----- END "+self.shortDescription+" ---\n")
if hasattr(app,"vocabList"): del app.vocabList # so re-reads
os.remove(newName+os.sep+"add-to-vocab"+dottxt)
if fileExists(newName+os.sep+"add-to-languages"+dottxt):
changed = 0
for lang in u8strip(read(newName+os.sep+"add-to-languages"+dottxt)).strip(wsp).split():
for lang in wspstrip(u8strip(read(newName+os.sep+"add-to-languages"+dottxt))).split():
if not lang in [firstLanguage,secondLanguage]+otherLanguages:
otherLanguages.append(lang) ; changed = 1
if changed: sanitise_otherLanguages(), updateSettingsFile("advanced"+dottxt,{"otherLanguages":otherLanguages,"possible_otherLanguages":possible_otherLanguages})
......@@ -450,8 +474,8 @@ def focusButton(button):
try: button.config(state=state)
except: pass # maybe not a button
for t in range(250,1000,250): # (NB avoid epilepsy's 5-30Hz!)
app.after(t,lambda *args:flashButton(button,"active"))
app.after(t+150,lambda *args:flashButton(button,"normal"))
app.after(t,lambda e=None,flashButton=flashButton,button=button:flashButton(button,"active"))
app.after(t+150,lambda e=None,flashButton=flashButton,button=button:flashButton(button,"normal"))
# (Don't like flashing, but can't make it permanently active as it won't change when the focus does)
if WMstandard: GUI_omit_statusline = 1 # unlikely to be room (and can disrupt nav)
......@@ -462,22 +486,24 @@ def startTk():
Tkinter.Frame.__init__(self, master)
class EmptyClass: pass
self.todo = EmptyClass() ; self.toRestore = []
self.ScrollUpHandler = self.ScrollDownHandler = lambda e=None:True
global app ; app = self
make_extra_buttons_waiting_list()
if olpc: self.master.option_add('*font',cond(extra_buttons_waiting_list,'Helvetica 9','Helvetica 14'))
elif macsound and Tkinter.TkVersion>=8.6: self.master.option_add('*font','System 13') # ok with magnification. Note >13 causes square buttons. (Including this line causes "Big print" to work)
elif macsound:
if Tkinter.TkVersion>=8.6: self.master.option_add('*font','System 13') # ok with magnification. Note >13 causes square buttons. (Including this line causes "Big print" to work)
if "AppTranslocation" in os.getcwd(): self.todo.alert="Your Mac is using \"app translocation\" to stop Gradint from writing to its folder. This will cause many problems. Quit Gradint, drag it to a different folder and run it again."
elif WMstandard: self.master.option_add('*font','Helvetica 7') # TODO on ALL WMstandard devices?
if winsound or cygwin or macsound: self.master.resizable(1,0) # resizable in X direction but not Y (latter doesn't make sense, see below). (Don't do this on X11 because on some distros it results in loss of automatic expansion as we pack more widgets.)
elif unix:
import commands
if commands.getoutput("xlsatoms|grep COMPIZ").find("COMPIZ")>-1:
if getoutput("xlsatoms|grep COMPIZ_WINDOW").find("COMPIZ")>=0: # (not _COMPIZ_WM_WINDOW_BLUR, that's sometimes present outside Compiz)
# Compiz sometimes has trouble auto-resizing our window (e.g. on Ubuntu 11.10)
self.master.geometry("%dx%d" % (self.winfo_screenwidth(),self.winfo_screenheight()))
if not GUI_always_big_print: self.todo.alert = "Gradint had to maximize itself because your window manager is Compiz which sometimes has trouble handling Tkinter window sizes"
self.extra_button_callables = []
self.pack(fill=Tkinter.BOTH,expand=1)
self.leftPanel = Tkinter.Frame(self)
self.leftPanel.pack(side="left",fill=Tkinter.X,expand=1) # "fill" needed so listbox can fill later
self.leftPanel.pack(side="left",fill=Tkinter.BOTH,expand=1)
self.rightPanel = None # for now
self.cancelling = 0 # guard against multiple presses of Cancel
self.Label = Tkinter.Label(self.leftPanel,text="Please wait a moment")
......@@ -489,33 +515,37 @@ def startTk():
nominalSize = intor0(f[-1])
if nominalSize: f=" ".join(f[:-1])+" %d"
else: # Tk 8.5+ ?
f=str(self.tk.eval('set font [font actual '+' '.join(f)+']')).split()
upNext = 0
for i in range(len(f)):
for f2 in ['set font [font actual '+' '.join(f)+']', # Tk 8.5
'set font [font actual default]']: # Tk 8.6
f=str(self.tk.eval(f2)).split()
upNext = 0
for i in range(len(f)):
if f[i]=="-size": upNext=1
elif upNext:
nominalSize=intor0(f[i])
if nominalSize<0: nominalSize,f[i] = -nominalSize,"-%d"
else: f[i]="%d"
break
if nominalSize==long(32768)*long(65536): nominalSize = 0 # e.g. Tk 8.6 on Ubuntu 16.04 when using the first eval string above
elif f2=='set font [font actual default]': nominalSize *= 0.77 # kludge for Tk 8.6 on Ubuntu 16.04 to make large-print calculation below work
if nominalSize: break
f=" ".join(f)
if not "%d" in f: raise Exception("wrong format") # caught below
if (not checkIn("%d",f)) or not nominalSize: raise Exception("wrong format") # caught below
pixelSize = self.Label.winfo_reqheight()-2*int(str(self.Label["borderwidth"]))-2*int(str(self.Label["pady"]))
# NB DO NOT try to tell Tk a desired pixel size - you may get a *larger* pixel size. Need to work out the desired nominal size.
approx_lines_per_screen_when_large = 25 # TODO really? (24 at 800x600 192dpi 15in but misses the status line, but OK for advanced users. setting 25 gives nominal 7 which is rather smaller.)
largeNominalSize = int(nominalSize*self.Label.winfo_screenheight()/approx_lines_per_screen_when_large/pixelSize)
if largeNominalSize >= nominalSize+3:
self.bigPrintFont = f % largeNominalSize
self.bigPrintMult = largeNominalSize*1.0/nominalSize
if GUI_always_big_print:
self.master.option_add('*font',self.bigPrintFont)
self.Label["font"]=self.bigPrintFont
del self.bigPrintFont ; self.isBigPrint=1
self.bigPrint0()
else: self.after(100,self.check_window_position) # (needs to happen when window is already drawn if you want it to preserve the X co-ordinate)
except: pass # wrong font format or something - can't do it
if winCEsound and ask_teacherMode: self.Label["font"]="Helvetica 16" # might make it slightly easier
self.remake_cancel_button(localise("Cancel lesson"))
self.Cancel.focus() # (default focus if we don't add anything else, e.g. reader)
self.copyright_string = u"This is "+(u""+program_name).replace("(c)",u"\n\u00a9").replace("-",u"\u2013")
self.copyright_string = u"This is "+ensure_unicode(program_name).replace("(c)",u"\n\u00a9").replace("-",u"\u2013")
self.Version = Tkinter.Label(self.leftPanel,text=self.copyright_string)
addStatus(self.Version,self.copyright_string)
if olpc: self.Version["font"]='Helvetica 9'
......@@ -631,7 +661,9 @@ def startTk():
updateUserRow(1)
if hasattr(self,"bigPrintFont"):
self.BigPrintButton = addButton(self.leftPanel,localise("Big print"),self.bigPrint)
self.BigPrintButton["font"]=self.bigPrintFont
try: self.BigPrintButton["font"]=self.bigPrintFont
except:
self.BigPrintButton.pack_forget() ; del self.BigPrintButton, self.bigPrintFont
self.remake_cancel_button(localise("Quit"))
if not GUI_omit_statusline: self.Version.pack(fill=Tkinter.X,expand=1)
if olpc or self.todo.set_main_menu=="test" or GUI_for_editing_only: self.showtest() # olpc: otherwise will just get a couple of options at the top and a lot of blank space (no way to centre it)
......@@ -640,7 +672,7 @@ def startTk():
self.restore_copyright()
if hasattr(self.todo,"alert"):
# we have to do it on THIS thread (especially on Windows / Cygwin; Mac OS and Linux might get away with doing it from another thread)
tkMessageBox.showinfo(self.master.title(),self.todo.alert)
tkMessageBox.showinfo(self.master.title(),S(self.todo.alert))
del self.todo.alert
if hasattr(self.todo,"question"):
self.answer_given = tkMessageBox.askyesno(self.master.title(),self.todo.question)
......@@ -705,7 +737,7 @@ def startTk():
addLabel(self.LessonRow,localise("mins"))
self.MakeLessonButton=addButton(self.LessonRow,localise("Start lesson"),self.makelesson,{"side":"left"},status="Press to create customized lessons\nusing the words in your collection")
self.lastOutTo=-1 # so it updates the Start Lesson button if needed
self.MakeLessonButton.bind('<FocusIn>',(lambda *args:app.after(10,lambda *args:app.MinsEntry.selection_clear())))
self.MakeLessonButton.bind('<FocusIn>',(lambda e=None,app=app:app.after(10,lambda e=None,app=app:app.MinsEntry.selection_clear())))
def sync_listbox_etc(self):
if not hasattr(self,"vocabList"):
if hasattr(self,"needVocablist"): return # already waiting for main thread to make one
......@@ -736,18 +768,18 @@ def startTk():
if synthCache:
cacheManagementOptions = [] # (text, oldKey, newKey, oldFile, newFile)
for t,l in [(text1.encode('utf-8'),secondLanguage),(text2.encode('utf-8'),firstLanguage)]:
k,f = synthcache_lookup("!synth:"+t+"_"+l,justQueryCache=1)
k,f = synthcache_lookup(B("!synth:")+t+B("_")+B(l),justQueryCache=1)
if f:
if (partials_langname(l) in synth_partials_voices or get_synth_if_possible(l,0)): # (no point having these buttons if there's no chance we can synth it by any method OTHER than the cache)
if k in synthCache_transtbl and k[0]=="_": cacheManagementOptions.append(("Keep in "+l+" cache",k,k[1:],0,0))
elif k[0]=="_": cacheManagementOptions.append(("Keep in "+l+" cache",0,0,f,f[1:]))
if k in synthCache_transtbl: cacheManagementOptions.append(("Reject from "+l+" cache",k,"__rejected_"+k,0,0))
if (checkIn(partials_langname(l),synth_partials_voices) or get_synth_if_possible(l,0)): # (no point having these buttons if there's no chance we can synth it by any method OTHER than the cache)
if checkIn(k,synthCache_transtbl) and B(k[:1])==B("_"): cacheManagementOptions.append(("Keep in "+l+" cache",k,k[1:],0,0))
elif B(k[:1])==B("_"): cacheManagementOptions.append(("Keep in "+l+" cache",0,0,f,f[1:]))
if checkIn(k,synthCache_transtbl): cacheManagementOptions.append(("Reject from "+l+" cache",k,"__rejected_"+k,0,0))
else: cacheManagementOptions.append(("Reject from "+l+" cache",0,0,f,"__rejected_"+f))
else:
k,f = synthcache_lookup("!synth:__rejected_"+t+"_"+l,justQueryCache=1)
if not f: k,f = synthcache_lookup("!synth:__rejected__"+t+"_"+l,justQueryCache=1)
k,f = synthcache_lookup(B("!synth:__rejected_")+t+B("_"+l),justQueryCache=1)
if not f: k,f = synthcache_lookup(B("!synth:__rejected__")+t+B("_"+l),justQueryCache=1)
if f:
if k in synthCache_transtbl: cacheManagementOptions.append(("Undo "+l+" cache reject",k,k[11:],0,0))
if checkIn(k,synthCache_transtbl): cacheManagementOptions.append(("Undo "+l+" cache reject",k,k[11:],0,0))
else: cacheManagementOptions.append(("Undo "+l+" cache reject",0,0,f,f[11:]))
elif l==secondLanguage and mp3web and not ';' in t: cacheManagementOptions.append(("Get from "+mp3webName,0,0,0,0))
if not hasattr(self,"cacheManagementOptions"):
......@@ -762,16 +794,16 @@ def startTk():
if not (text1 or text2): self.ListBox.selection_clear(0,'end') # probably just added a new word while another was selected (added a variation) - clear selection to reduce confusion
else: return # don't try to be clever with searches when editing an existing item (the re-ordering can be confusing)
text1,text2 = text1.lower().replace(" ",""),text2.lower().replace(" ","") # ignore case and whitespace when searching
l=map(lambda (x,y):x+"="+y, filter(lambda (x,y):text1 in x.lower().replace(" ","") and text2 in y.lower().replace(" ",""),self.vocabList)[-tkNumWordsToShow:])
l=map(lambda x:x[0]+"="+x[1], filter(lambda x,text1=text1,text2=text2:x[0].lower().replace(" ","").find(text1)>-1 and x[1].lower().replace(" ","").find(text2)>-1,self.vocabList)[-tkNumWordsToShow:])
l.reverse() ; synchronizeListbox(self.ListBox,l) # show in reverse order, in case the bottom of the list box is off-screen
def doSynthcacheManagement(self,oldKey,newKey,oldFname,newFname):
# should be a quick operation - might as well do it in the GUI thread
if (oldKey,oldFname) == (0,0): # special for mp3web
self.menu_response="mp3web" ; return
if oldKey in synthCache_transtbl:
if checkIn(oldKey,synthCache_transtbl):
if newKey: synthCache_transtbl[newKey]=synthCache_transtbl[oldKey]
else: del synthCache_transtbl[oldKey]
open(synthCache+os.sep+transTbl,'w').write("".join([v+" "+k+"\n" for k,v in synthCache_transtbl.items()]))
open(synthCache+os.sep+transTbl,'wb').write(B("").join([v+B(" ")+k+B("\n") for k,v in list(synthCache_transtbl.items())]))
if oldFname:
del synthCache_contents[oldFname]
if newFname:
......@@ -783,7 +815,12 @@ def startTk():
for control,current,restoreTo in self.toRestore:
if asUnicode(control.get())==current: control.set(restoreTo)
self.toRestore = []
def stripText(self,*args): self.Text1.set(fix_commas(hanzi_and_punc(asUnicode(self.Text1.get()))))
def stripText(self,*args):
t = self.Text1.get()
u = asUnicode(t)
v = fix_commas(hanzi_and_punc(u))
if t==u: v=asUnicode(v)
self.Text1.set(v)
def thin_down_for_lesson(self):
if hasattr(self,"OutputRow"): self.OutputRow.pack_forget()
if hasattr(self,"CopyFromButton"):
......@@ -810,12 +847,17 @@ def startTk():
self.Label.pack() ; self.CancelRow.pack()
self.Label["text"] = "Working..." # (to be replaced by time indication on real-time, not on output-to-file)
self.Cancel["text"] = localise("Quit")
def bigPrint(self,*args):
self.thin_down_for_lesson()
def bigPrint0(self):
self.master.option_add('*font',self.bigPrintFont)
self.Version["font"]=self.Label["font"]=self.bigPrintFont
self.sbarWidth = int(16*self.bigPrintMult)
self.master.option_add('*Scrollbar*width',self.sbarWidth) # (works on some systems; usually ineffective on Mac)
self.Label["font"]=self.bigPrintFont
del self.bigPrintFont # (TODO do we want an option to undo it? or would that take too much of the big print real-estate.)
self.isBigPrint=1
def bigPrint(self,*args):
self.thin_down_for_lesson()
self.Version["font"]=self.bigPrintFont
self.bigPrint0()
if self.rightPanel: # oops, need to re-construct it
global extra_buttons_waiting_list
extra_buttons_waiting_list = []
......@@ -891,8 +933,8 @@ def startTk():
self.L2Entry.bind('<Return>',self.changeLanguages)
for e in [self.L1Entry,self.L2Entry]: e.bind('<Button-1>',(lambda e:e.widget.after(10,lambda e=e:selectAll(e))))
self.ChangeLanguageButton = addButton(self.row3,"",self.changeLanguages,status="Use this button to set your\nfirst and second languages") # will set text in updateLanguageLabels
self.ChangeLanguageButton.bind('<FocusIn>',(lambda *args:app.after(10,lambda *args:app.L2Entry.selection_clear())))
self.AddButton.bind('<FocusIn>',(lambda *args:app.after(10,lambda *args:app.L1Entry.selection_clear()))) # for backwards tabbing
self.ChangeLanguageButton.bind('<FocusIn>',(lambda e=None,app=app:app.after(10,lambda e=None,app=app:app.L2Entry.selection_clear())))
self.AddButton.bind('<FocusIn>',(lambda e=None,app=app:app.after(10,lambda e=None,app=app:app.L1Entry.selection_clear()))) # for backwards tabbing
if GUI_omit_settings and (vocabFile==user0[1] or fileExists(vocabFile)): self.row3.pack_forget()
if textEditorCommand:
self.RecordedWordsButton = addButton(self.row4,"",self.showRecordedWords,{"side":"left"},status="This button lets you manage recorded\n(as opposed to computer-voiced) words")
......@@ -939,8 +981,8 @@ def startTk():
m=Tkinter.Menu(None, tearoff=0, takefocus=0)
for i in range(len(lastUserNames)):
if lastUserNames[i] and not i==intor0(self.userNo.get()):
if fileExists(addUserToFname(user0[1],i)): m.add_command(label=u"Copy vocab list from "+lastUserNames[i],command=(lambda e=None,i=i:self.copyVocabFrom(i)))
m.add_command(label=u"Copy recordings to/from "+lastUserNames[i],command=(lambda e=None,i=i:self.setToOpen((addUserToFname(user0[0],i),addUserToFname(user0[0],intor0(self.userNo.get()))))))
if fileExists(addUserToFname(user0[1],i)): m.add_command(label=u"Copy vocab list from "+lastUserNames[i],command=(lambda e=None,i=i,self=self:self.copyVocabFrom(i)))
m.add_command(label=u"Copy recordings to/from "+lastUserNames[i],command=(lambda e=None,i=i,self=self:self.setToOpen((addUserToFname(user0[0],i),addUserToFname(user0[0],intor0(self.userNo.get()))))))
m.tk_popup(self.CopyFromButton.winfo_rootx(),self.CopyFromButton.winfo_rooty(),entry="0")
def setToOpen(self,toOpen): self.menu_response,self.toOpen = "samplesCopy",toOpen
def copyVocabFrom(self,userNo):
......@@ -950,11 +992,12 @@ def startTk():
select_userNumber(intor0(self.userNo.get()),updateGUI=0)
vCurrent = list2set(vocabLinesWithLangs())
o=appendVocabFileInRightLanguages()
if not o: return # IOError
langs = (secondLanguage,firstLanguage)
for newLangs,line in vCopyFrom:
if (newLangs,line) in vCurrent: continue # already got it
if not newLangs==langs: o.write("SET LANGUAGES "+" ".join(list(newLangs))+"\n")
o.write(line+"\n")
if checkIn((newLangs,line),vCurrent): continue # already got it
if not newLangs==langs: o.write(B("SET LANGUAGES ")+B(" ").join(list(newLangs))+B("\n"))
o.write(B(line)+B("\n"))
langs = newLangs
o.close()
if hasattr(self,"vocabList"): del self.vocabList # re-read
......@@ -969,15 +1012,15 @@ def startTk():
global firstLanguage,secondLanguage
firstLanguage1=asUnicode(self.L1Text.get()).encode('utf-8')
secondLanguage1=asUnicode(self.L2Text.get()).encode('utf-8')
if (firstLanguage,secondLanguage) == (firstLanguage1,secondLanguage1): # they didn't change anything
if (B(firstLanguage),B(secondLanguage)) == (firstLanguage1,secondLanguage1): # they didn't change anything
langs = ESpeakSynth().describe_supported_languages()
msg = (localise("To change languages, edit the boxes that say '%s' and '%s', then press the '%s' button.") % (firstLanguage,secondLanguage,localise("Change languages")))+"\n\n"+localise("Recorded words may be in ANY languages, and you may choose your own abbreviations for them. However if you want to use the computer voice for anything then please use standard abbreviations.")
if langs:
if tkMessageBox.askyesno(self.master.title(),msg+" "+localise("Would you like to see a list of the standard abbreviations for languages that can be computer voiced?")): self.todo.alert = localise("Languages with computer voices (some better than others):")+"\n"+langs
else: self.todo.alert = msg+" "+localise("(Sorry, a list of these is not available on this system - check eSpeak installation.)")
return
need_redisplay = "@variants-"+GUI_languages.get(firstLanguage,firstLanguage) in GUI_translations or "@variants-"+GUI_languages.get(firstLanguage1,firstLanguage1) in GUI_translations # if EITHER old or new lang has variants, MUST reconstruct that row. (TODO also do it anyway to get the "Speaker" etc updated? but may cause unnecessary flicker if that's no big problem)
firstLanguage,secondLanguage = firstLanguage1,secondLanguage1
need_redisplay = checkIn("@variants-"+GUI_languages.get(firstLanguage,firstLanguage),GUI_translations) or checkIn("@variants-"+GUI_languages.get(S(firstLanguage1),S(firstLanguage1)),GUI_translations) # if EITHER old or new lang has variants, MUST reconstruct that row. (TODO also do it anyway to get the "Speaker" etc updated? but may cause unnecessary flicker if that's no big problem)
firstLanguage,secondLanguage = S(firstLanguage1),S(secondLanguage1)
updateSettingsFile(settingsFile,{"firstLanguage":firstLanguage,"secondLanguage":secondLanguage})
if need_redisplay:
self.thin_down_for_lesson()
......@@ -1037,7 +1080,10 @@ def startTk():
self.menu_response="add"
def zap_newlines(self): # in case someone pastes in text that contains newlines, better not keep them when adding to vocab
text1,text2 = asUnicode(self.Text1.get()),asUnicode(self.Text2.get())
t1,t2 = text1.replace("\n"," ").replace("\r","").strip(wsp), text2.replace("\n"," ").replace("\r","").strip(wsp)
# (also remove the simple visual markup that Wenlin sometimes adds)
t1,t2=text1,text2
for zap in ["\n","\r","<b>","</b>","<i>","</i>","<u>","</u>"]: t1,t2=t1.replace(zap,""),t2.replace(zap,"")
t1,t2 = wspstrip(t1),wspstrip(t2)
if not t1==text1: self.Text1.set(t1)
if not t2==text2: self.Text2.set(t2)
def getEncoder(self,*args):
......@@ -1106,24 +1152,26 @@ def guiVocabList(parsedVocab):
if b.endswith(fl2): b=b[:fl2Len]
elif b.endswith(fl3): b=readText(b)
else: continue
ret.append((unicode(c,"utf-8"),unicode(b,"utf-8")))
ret.append((ensure_unicode(c),ensure_unicode(b)))
return ret
def readText(l): # see utils/transliterate.py (running guiVocabList on txt files from scanSamples)
l = samplesDirectory+os.sep+l
if l in variantFiles: # oops. just read the 1st .txt variant
if os.sep in l: lp=(l+os.sep)[:l.rfind(os.sep)]+os.sep
else: lp = ""
varList = filter(lambda x:x.endswith(dottxt),variantFiles[l])
l = B(samplesDirectory)+B(os.sep)+B(l)
if checkIn(l,variantFiles): # oops. just read the 1st .txt variant
if B(os.sep) in l: lp=(l+B(os.sep))[:l.rfind(B(os.sep))]+B(os.sep)
else: lp = B("")
varList = filter(lambda x:x.endswith(B(dottxt)),variantFiles[l])
varList.sort() # so at least it consistently returns the same one. TODO utils/ cache-synth.py list-synth.py synth-batchconvert-helper.py all use readText() now, can we get them to cache the other variants too?
l = lp + varList[0]
return u8strip(read(l)).strip(wsp)
return bwspstrip(u8strip(read(l)))
def singular(number,s):
s=localise(s)
if firstLanguage=="en" and number==1 and s[-1]=="s": return s[:-1]
return s
def localise(s):
d = GUI_translations.get(s,{}) ; s2 = 0
if s=="zh-yue" or s=="zhy": k="cant"
else: k=s
d = GUI_translations.get(k,{}) ; s2 = 0
GUIlang = GUI_languages.get(firstLanguage,firstLanguage)
if scriptVariants.get(GUIlang,0): s2 = d.get(GUIlang+str(scriptVariants[GUIlang]+1),0)
if not s2: s2 = d.get(GUIlang,s)
......@@ -1172,10 +1220,7 @@ if useTK:
textEditorCommand=explorerCommand=None
if winsound or mingw32 or cygwin:
textEditorName="Notepad" ; textEditorWaits=1
# Try Notepad++ first, otherwise plain notepad
textEditorCommand = programFiles+os.sep+"Notepad++"+os.sep+"notepad++.exe"
if fileExists(textEditorCommand): textEditorCommand='"'+textEditorCommand+'" -multiInst -notabbar -nosession'
else: textEditorCommand="notepad"
textEditorCommand="notepad"
explorerCommand="explorer"
elif macsound:
textEditorName="TextEdit"
......@@ -1183,22 +1228,27 @@ if useTK:
if got_program("bbedit"):
textEditorName="bbedit"
textEditorCommand="bbedit -w" ; textEditorWaits=1
elif got_program("edit"): # TextWrangler
textEditorName="edit"
textEditorCommand="edit -w" ; textEditorWaits=1
if sys.version.startswith("2.3.5") and "DISPLAY" in os.environ: explorerCommand = None # 'open' doesn't seem to work when running from within Python in X11 on 10.4
else: explorerCommand="open"
elif unix:
if "KDE_FULL_SESSION" is os.environ and got_program("kfmclient"):
if "KDE_FULL_SESSION" in os.environ and got_program("kfmclient"):
# looks like we're in a KDE session and can use the kfmclient command
textEditorCommand=explorerCommand="kfmclient exec"
elif not olpc and got_program("gnome-open"):
textEditorCommand=explorerCommand="gnome-open"
elif got_program("nautilus"): explorerCommand="nautilus"
elif got_program("pcmanfm"): explorerCommand="pcmanfm" # LXDE, LXQt
elif got_program("pcmanfm-qt"): explorerCommand="pcmanfm-qt" # might not work as well as pcmanfm on 24.04
elif got_program("rox"):
# rox is available - try using that to open directories
# (better not use it for editor as it might not be configured)
# (TODO if both rox and gnome are available, can we tell which one the user prefers?)
explorerCommand="rox"
# anyway, see if we can find a nice editor
for editor in ["leafpad","gedit","nedit","kedit","xedit"]:
for editor in ["leafpad","featherpad","gedit","nedit","kedit","xedit"]:
if got_program(editor):
textEditorName=textEditorCommand=editor
textEditorWaits = 1
......@@ -1208,7 +1258,12 @@ if useTK:
break
# End of finding editor - now start GUI
try:
import thread,Tkinter,tkMessageBox
try: import thread
except ImportError: import _thread as thread
try: import Tkinter,tkMessageBox
except:
import tkinter as Tkinter
from tkinter import messagebox as tkMessageBox
forceRadio=(macsound and 8.49<Tkinter.TkVersion<8.59) # indicatoron doesn't do very well in OS X 10.6 (Tk 8.5) unless we patched it
if olpc:
def interrupt_main(): os.kill(os.getpid(),2) # sigint
......@@ -1228,8 +1283,9 @@ if useTK:
def openDirectory(dir,inGuiThread=0):
if winCEsound:
if not dir[0]=="\\": dir=os.getcwd()+cwd_addSep+dir # must be absolute
ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=u"\\Windows\\fexplore",Parameters=u""+dir)))
ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=u"\\Windows\\fexplore",Parameters=ensure_unicode(dir))))
elif explorerCommand:
if ' ' in dir: dir='"'+dir+'"'
cmd = explorerCommand+" "+dir
if winsound or mingw32: cmd="start "+cmd # (not needed on XP but is on Vista)
elif unix: cmd += "&"
......@@ -1241,20 +1297,21 @@ def openDirectory(dir,inGuiThread=0):
if inGuiThread: tkMessageBox.showinfo(app.master.title(),msg)
else: waitOnMessage(msg)
def sanityCheck(text,language,pauseOnError=0): # text is utf-8; returns error message if any
def generalCheck(text,language,pauseOnError=0): # text is utf-8; returns error message if any
if not text: return # always OK empty strings
if pauseOnError:
ret = sanityCheck(text,language)
ret = generalCheck(text,language)
if ret: waitOnMessage(ret)
return ret
if language=="zh":
allDigits = True
for t in text:
allDigits = True ; text=B(text)
for i in xrange(len(text)):
t = text[i:i+1]
if ord(t)>127: return # got hanzi or tone marks
if t in "12345": return # got tone numbers
if t not in "0123456789. ": allDigits = False
if t in B("12345"): return # got tone numbers
if t not in B("0123456789. "): allDigits = False
if allDigits: return
return "Pinyin needs tones. Please go back and add tone numbers to "+text+"."+cond(startBrowser("http://www.pristine.com.tw/lexicon.php?query="+fix_pinyin(text,[]).replace("1","1 ").replace("2","2 ").replace("3","3 ").replace("4","4 ").replace("5"," ").replace(" "," ").strip(wsp).replace(" ","+"))," Gradint has pointed your web browser at an online dictionary that might help.","")
return B("Pinyin needs tones. Please go back and add tone numbers to ")+text+B(".")+cond(startBrowser(B("http://www.mdbg.net/chinese/dictionary?wdqb=")+bwspstrip(fix_pinyin(text,[])).replace(B("5"),B("")).replace(B(" "),B("+"))),B(" Gradint has pointed your web browser at an online dictionary that might help."),B(""))
def check_for_slacking():
if fileExists(progressFile): checkAge(progressFile,localise("It has been %d days since your last Gradint lesson. Please try to have one every day."))
......@@ -1269,12 +1326,12 @@ def checkAge(fname,message):
if days>=5 and (days%5)==0: waitOnMessage(message % days)
def s60_addVocab():
label1,label2 = u""+localise("Word in %s") % localise(secondLanguage),u""+localise("Meaning in %s") % localise(firstLanguage)
label1,label2 = ensure_unicode(localise("Word in %s") % localise(secondLanguage)),ensure_unicode(localise("Meaning in %s") % localise(firstLanguage))
while True:
result = appuifw.multi_query(label1,label2) # unfortunately multi_query can't take default items (and sometimes no T9!), but Form is too awkward (can't see T9 mode + requires 2-button save via Options) and non-multi query would be even more modal
if not result: return # cancelled
l2,l1 = result # guaranteed to both be populated
while sanityCheck(l2.encode('utf-8'),secondLanguage,1):
while generalCheck(l2.encode('utf-8'),secondLanguage,1):
l2=appuifw.query(label1,"text",u"")
if not l2: return # cancelled
# TODO detect duplicates like Tk GUI does?
......@@ -1282,7 +1339,7 @@ def s60_addVocab():
appendVocabFileInRightLanguages().write((l2+"="+l1+"\n").encode("utf-8"))
def s60_changeLang():
global firstLanguage,secondLanguage
result = appuifw.multi_query(u""+localise("Your first language")+" (e.g. "+firstLanguage+")",u""+localise("second")+" (e.g. "+secondLanguage+")")
result = appuifw.multi_query(ensure_unicode(localise("Your first language")+" (e.g. "+firstLanguage+")"),ensure_unicode(localise("second")+" (e.g. "+secondLanguage+")"))
if not result: return # cancelled
l1,l2 = result
firstLanguage,secondLanguage = l1.encode('utf-8').lower(),l2.encode('utf-8').lower()
......@@ -1296,7 +1353,7 @@ def s60_runLesson():
def s60_viewVocab():
global justSynthesize
doLabel("Reading your vocab list, please wait...")
vList = map(lambda (l2,l1):l2+u"="+l1, guiVocabList(parseSynthVocab(vocabFile,1)))
vList = map(lambda x:x[0]+u"="+x[1], guiVocabList(parseSynthVocab(vocabFile,1)))
if not vList: return waitOnMessage("Your computer-voiced vocab list is empty.")
while True:
appuifw.app.body = None
......@@ -1306,8 +1363,8 @@ def s60_viewVocab():
action = appuifw.popup_menu([u"Speak (just "+secondLanguage+")",u"Speak ("+secondLanguage+" and "+firstLanguage+")",u"Change "+secondLanguage,u"Change "+firstLanguage,u"Delete item",u"Cancel"], vList[sel])
if action==0 or action==1:
doLabel("Speaking...")
justSynthesize = secondLanguage+" "+l2.encode('utf-8')
if action==1: justSynthesize += ("#"+firstLanguage+" "+l1.encode('utf-8'))
justSynthesize = B(secondLanguage)+B(" ")+l2.encode('utf-8')
if action==1: justSynthesize += (B('#')+B(firstLanguage)+B(" ")+l1.encode('utf-8'))
just_synthesize()
justSynthesize = ""
elif action==5: pass
......@@ -1316,11 +1373,11 @@ def s60_viewVocab():
oldL1,oldL2 = l1,l2
if action==2:
first=1
while first or (l2 and sanityCheck(l2.encode('utf-8'),secondLanguage,1)):
first=0 ; l2=appuifw.query(u""+secondLanguage,"text",l2)
while first or (l2 and generalCheck(l2.encode('utf-8'),secondLanguage,1)):
first=0 ; l2=appuifw.query(ensure_unicode(secondLanguage),"text",l2)
if not l2: continue
elif action==3:
l1 = appuifw.query(u""+firstLanguage,"text",l1)
l1 = appuifw.query(ensure_unicode(firstLanguage),"text",l1)
if not l1: continue
doLabel("Processing")
delOrReplace(oldL2,oldL1,l2,l1,cond(action==4,"delete","replace"))
......@@ -1328,36 +1385,81 @@ def s60_viewVocab():
del vList[sel]
if not vList: return # empty
else: vList[sel] = l2+"="+l1
def android_addVocab():
while True:
l2 = None
while not l2 or generalCheck(l2.encode('utf-8'),secondLanguage,1):
l2 = android.dialogGetInput("Add word","Word in %s" % localise(secondLanguage)).result
if not l2: return # cancelled
l1 = android.dialogGetInput("Add word","Meaning in %s" % localise(firstLanguage)).result
if not l1: return # cancelled
# TODO detect duplicates like Tk GUI does?
android.makeToast(u"Added "+l2+"="+l1)
appendVocabFileInRightLanguages().write((l2+"="+l1+"\n").encode("utf-8"))
def android_changeLang():
global firstLanguage,secondLanguage
l1 = android.dialogGetInput("Gradint","Enter your first language",firstLanguage).result
if not l1: return # cancelled
l2 = android.dialogGetInput("Gradint","Enter your second language",secondLanguage).result
if not l2: return # cancelled
firstLanguage,secondLanguage = l1.encode('utf-8').lower(),l2.encode('utf-8').lower()
updateSettingsFile(settingsFile,{"firstLanguage":firstLanguage,"secondLanguage":secondLanguage})
def delOrReplace(L2toDel,L1toDel,newL2,newL1,action="delete"):
langs = [secondLanguage,firstLanguage]
v=u8strip(read(vocabFile)).replace("\r\n","\n").replace("\r","\n")
o=open(vocabFile,"w") ; found = 0
if last_u8strip_found_BOM: o.write('\xef\xbb\xbf') # re-write it
v=v.split("\n")
v=u8strip(read(vocabFile)).replace(B("\r\n"),B("\n")).replace(B("\r"),B("\n"))
if paranoid_file_management:
fname = os.tempnam()
o = open(fname,"w")
else: o=open(vocabFile,"w")
found = 0
if last_u8strip_found_BOM: writeB(o,LB('\xef\xbb\xbf')) # re-write it
v=v.split(B("\n"))
if v and not v[-1]: v=v[:-1] # don't add an extra blank line at end
for l in v:
l2=l.lower()
if l2.startswith("set language ") or l2.startswith("set languages "):
langs=l.split()[2:] ; o.write(l+"\n") ; continue
thisLine=map(lambda x:x.strip(wsp),l.split("=",len(langs)-1))
if l2.startswith(B("set language ")) or l2.startswith(B("set languages ")):
langs=map(S,l.split()[2:]) ; writeB(o,l+B("\n")) ; continue
thisLine=map(bwspstrip,l.split(B("="),len(langs)-1))
if (langs==[secondLanguage,firstLanguage] and thisLine==[L2toDel.encode('utf-8'),L1toDel.encode('utf-8')]) or (langs==[firstLanguage,secondLanguage] and thisLine==[L1toDel.encode('utf-8'),L2toDel.encode('utf-8')]):
# delete this line. and maybe replace it
found = 1
if action=="replace":
if langs==[secondLanguage,firstLanguage]: o.write(newL2.encode("utf-8")+"="+newL1.encode("utf-8")+"\n")
else: o.write(newL1.encode("utf-8")+"="+newL2.encode("utf-8")+"\n")
else: o.write(l+"\n")
if langs==[secondLanguage,firstLanguage]: writeB(o,newL2.encode("utf-8")+B("=")+newL1.encode("utf-8")+B("\n"))
else: writeB(o,newL1.encode("utf-8")+B("=")+newL2.encode("utf-8")+B("\n"))
else: writeB(o,l+B("\n"))
o.close()
if paranoid_file_management:
write(vocabFile,read(fname))
os.remove(fname)
return found
def maybeCanSynth(lang): return lang in synth_partials_voices or get_synth_if_possible(lang,0) or synthCache
def maybeCanSynth(lang): return checkIn(lang,synth_partials_voices) or get_synth_if_possible(lang,0) or synthCache
def android_main_menu():
while True:
menu=[]
if maybeCanSynth(secondLanguage):
menu.append((unicode(localise("Just speak a word")),primitive_synthloop))
doVocab = maybeCanSynth(firstLanguage)
if doVocab: menu.append((unicode(localise("Add word to my vocab")),android_addVocab))
menu.append((unicode(localise("Make lesson from vocab")),lesson_loop))
# if doVocab: menu.append((u"View/change vocab",android_viewVocab)) # (TODO but lower priority because SL4A has an editor)
else: menu.append((unicode(localise("Make lesson")),lesson_loop))
menu += [(unicode(localise("Record word(s) with mic")),android_recordWord),(unicode(localise("Change languages")),android_changeLang)]
menu.append((unicode(localise("Quit")),None))
android.dialogCreateAlert("Gradint","Choose an action")
android.dialogSetItems(map (lambda x:x[0], menu))
android.dialogShow()
try: function = menu[android.dialogGetResponse().result['item']][1]
except KeyError: break # probably an error condition: don't try to redisplay, just quit
if function: function() # and redisplay after
else: break # quit
def s60_main_menu():
while True:
appuifw.app.body = None # NOT text saying version no etc - has distracting blinking cursor
menu=[]
if maybeCanSynth(secondLanguage):
menu.append((u"Just speak a word",primitive_synthloop))
menu.append((u"Just speak a word",primitive_synthloop)) # no localise() as S60 is not guaranteed to be able to display the characters
doVocab = maybeCanSynth(firstLanguage)
if doVocab: menu.append((u"Add word to my vocab",s60_addVocab))
menu.append((u"Make lesson from vocab",s60_runLesson))
......@@ -1373,20 +1475,18 @@ def s60_main_menu():
def downloadLAME():
# Sourceforge keep making this harder!
return not system("""if which curl >/dev/null 2>/dev/null; then export Curl="curl -L"; else export Curl="wget -O -"; fi
if ! test -e lame*.tar.gz; then
export Link="$($Curl "http://sourceforge.net/project/showfiles.php?group_id=290&package_id=309"|grep tar.gz|head -1)"
echo "Got HTML: $Link" 1>&2
export Link="$(echo "$Link"|sed -e 's,href="/,href="http://sourceforge.net/,' -e 's/.*http:/http:/' -e 's/.tar.gz.*/.tar.gz/')"
echo "Following link to $Link" 1>&2
if ! $Curl "$Link" > lame.tar.gz; then
# Removed code to check for latest version, as we
# can't use v3.100 due to Lame bug 488.
return not system("""if which curl >/dev/null 2>/dev/null; then Curl="curl -L"; else Curl="wget -O -"; fi
if ! [ -e lame*.tar.gz ]; then
if ! $Curl "https://sourceforge.net/projects/lame/files/lame/3.99/lame-3.99.5.tar.gz/download" > lame.tar.gz; then
rm -f lame.tar.gz; exit 1
fi
if grep downloads.sourceforge lame.tar.gz 2>/dev/null; then
export Link="$(cat lame.tar.gz|grep downloads.sourceforge|head -1)"
echo "Got HTML 2: $Link" 1>&2
export Link="$(echo "$Link"|sed -e 's/.*http/http/' -e 's,.*/projects,http://sourceforge.net/projects,' -e 's/".*//')"
echo "Following link 2 to $Link" 1>&2
Link="$(cat lame.tar.gz|grep downloads.sourceforge|head -1)"
echo "Got HTML: $Link" >&2
Link="$(echo "$Link"|sed -e 's/.*http/http/' -e 's,.*/projects,http://sourceforge.net/projects,' -e 's/".*//')"
echo "Following link to $Link" >&2
if ! $Curl "$Link" > lame.tar.gz; then
rm -f lame.tar.gz; exit 1
fi
......@@ -1400,9 +1500,8 @@ def gui_event_loop():
disable_once_per_day = cond(getYN(localise("Do you want Gradint to start by itself and remind you to practise?")),0,1)
updateSettingsFile("advanced"+dottxt,{"disable_once_per_day":disable_once_per_day})
if disable_once_per_day: # signal the background process to stop next time
for f in ["background1"+dottxt,"background2"+dottxt]:
try: os.remove(f)
except: pass
try: os.remove("background"+dottxt)
except: pass
if orig_onceperday&2: check_for_slacking()
while app:
while not hasattr(app,"menu_response"):
......@@ -1415,13 +1514,16 @@ def gui_event_loop():
if emulated_interruptMain: check_for_interrupts()
time.sleep(0.3)
menu_response = app.menu_response
del app.menu_response
if menu_response=="input": # WMstandard
app.todo.input_response=raw_input()
elif menu_response=="go":
gui_outputTo_start()
if not soundCollector: app.todo.add_briefinterrupt_button = 1
try: lesson_loop()
except PromptException,prEx: waitOnMessage("Problem finding prompts:\n"+prEx.message) # and don't quit, user may be able to fix
except PromptException:
prEx = sys.exc_info()[1]
waitOnMessage("Problem finding prompts:\n"+prEx.message) # and don't quit, user may be able to fix
except KeyboardInterrupt: pass # probably pressed Cancel Lesson while it was still being made (i.e. before handleInterrupt)
if app and not soundCollector: app.todo.remove_briefinterrupt_button = 1 # (not app if it's closed by the close box)
gui_outputTo_end()
......@@ -1436,7 +1538,7 @@ def gui_event_loop():
waitOnMessage("WARNING: Word may not save non-Western characters properly. Try an editor like MADE instead (need to set its font).") # TODO Flinkware MADE version 2.0.0 has been known to insert spurious carriage returns at occasional points in large text files
if not app.fileToEdit[0]=="\\": app.fileToEdit=os.getcwd()+cwd_addSep+app.fileToEdit # must be absolute
if not fileExists(app.fileToEdit): open(app.fileToEdit,"w") # at least make sure it exists
ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=u""+app.fileToEdit)))
ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=ensure_unicode(app.fileToEdit))))
waitOnMessage("When you've finished editing "+app.fileToEdit+", close it and start gradint again.")
return
elif textEditorCommand:
......@@ -1457,7 +1559,7 @@ def gui_event_loop():
if time.time() < t+3: waitOnMessage(textEditorName+" returned control to Gradint in less than 3 seconds. Perhaps you already had an instance running and it loaded the file remotely. Press OK when you have finished editing the file.")
newContents = read(fileToEdit)
if not newContents==oldContents:
if paranoid_file_management: open(app.fileToEdit,"w").write(newContents)
if paranoid_file_management: write(app.fileToEdit,newContents)
if app.fileToEdit==vocabFile:
app.wordsExist=1 ; del app.vocabList # re-read
else: waitOnMessage("The changes you made to "+app.fileToEdit+" will take effect when you quit Gradint and start it again.")
......@@ -1484,18 +1586,21 @@ def gui_event_loop():
text1 = asUnicode(app.Text1.get()).encode('utf-8') ; text2 = asUnicode(app.Text2.get()).encode('utf-8')
if not text1 and not text2: app.todo.alert=u"Before pressing the "+localise("Speak")+u" button, you need to type the text you want to hear into the box."
else:
msg=sanityCheck(text1,secondLanguage)
if msg: app.todo.alert=u""+msg
if text1.startswith(B('#')): msg="" # see below
else: msg=generalCheck(text1,secondLanguage)
if msg: app.todo.alert=ensure_unicode(msg)
else:
app.set_watch_cursor = 1 ; app.toRestore = []
global justSynthesize ; justSynthesize = ""
def doControl(text,lang,control):
global justSynthesize
global justSynthesize ; text=B(text)
restoreTo = asUnicode(control.get())
if text:
if can_be_synthesized("!synth:"+text+"_"+lang): justSynthesize += ("#"+lang+" "+text)
else: app.todo.alert="Cannot find a synthesizer that can say '"+text+"' in language '"+lang+"' on this system"
t=transliterates_differently(text,lang)
if text.startswith(B('#')): justSynthesize = B(justSynthesize)+text # hack for direct control of just_synthesize from the GUI (TODO document it in advanced.txt? NB we also bypass the GUI transliteration in the block below)
elif text:
if can_be_synthesized(B("!synth:")+text+B("_")+B(lang)):
justSynthesize=B(justSynthesize)+(B("#")+B(lang)+B(" ")+B(text))
else: app.todo.alert=B("Cannot find a synthesizer that can say '")+text+B("' in language '")+B(lang)+B("' on this system")
t=S(transliterates_differently(text,lang))
if t: # (don't go straight into len() stuff, it could be None)
if unix and len(t)>300 and hasattr(app,"isBigPrint"): app.todo.alert="Transliteration suppressed to work around Ubuntu bug 731424" # https://bugs.launchpad.net/ubuntu/+bug/731424
else:
......@@ -1511,7 +1616,6 @@ def gui_event_loop():
elif ask_teacherMode: # Do the L2, then ask if actually WANT the L1 as well (might be useful on WinCE etc, search-and-demonstrate-L2)
doSynth()
if app and not getYN("Also speak the %s?" % firstLanguage):
if app: del app.menu_response
continue
doControl(text2,firstLanguage,app.Text2)
doSynth()
......@@ -1536,7 +1640,7 @@ def gui_event_loop():
if not app: break
found=0
for f in scanDirs()[0].keys():
if not f in oldLs and (f.lower().endswith(dotmp3) or f.lower().endswith(dotwav)) and getYN("Use "+f[f.rfind(os.sep)+1:]+"?"): # TODO don't ask this question too many times if there are many and they're all 'no'
if not checkIn(f,oldLs) and (f.lower().endswith(dotmp3) or f.lower().endswith(dotwav)) and getYN("Use "+f[f.rfind(os.sep)+1:]+"?"): # TODO don't ask this question too many times if there are many and they're all 'no'
system("mp3gain -r -s r -k -d 10 \""+f+"\"") # (if mp3gain command is available; ignore errors if not (TODO document in advanced.txt)) (note: doing here not after the move, in case synthCache is over ftpfs mount or something)
uf=scFile=text1.encode("utf-8")+"_"+secondLanguage+f[-4:].lower()
try:
......@@ -1569,30 +1673,29 @@ def gui_event_loop():
if not getYN("Download failed. Try again?"): break
if worked:
app.setLabel("Compiling...")
if system("""tar -zxvf lame*.tar.gz && cd lame-* && if ./configure && make; then ln -s $(pwd)/frontend/lame ../lame || true; else cd .. ; rm -rf lame*; exit 1; fi"""): app.todo.alert = "Compile failed"
if system("""tar -zxvf lame*.tar.gz && cd lame-* && if ./configure && make; then ln -s $(pwd)/frontend/lame ../lame || true; else cd .. ; rm -rf lame*; exit 1; fi"""):
app.todo.alert = "Compile failed"
if macsound:
app.todo.alert += ". Check the system has Xcode with command-line license accepted (try running gcc from the Terminal)"
# might be asked to run: sudo xcodebuild -license
app.todo.set_main_menu = 1
elif (menu_response=="add" or menu_response=="replace") and not (app.Text1.get() and app.Text2.get()): app.todo.alert="You need to type text in both boxes before adding the word/meaning pair to "+vocabFile
elif menu_response=="add" and hasattr(app,"vocabList") and (asUnicode(app.Text1.get()),asUnicode(app.Text2.get())) in app.vocabList:
elif menu_response=="add" and hasattr(app,"vocabList") and checkIn((asUnicode(app.Text1.get()),asUnicode(app.Text2.get())),app.vocabList):
# Trying to add a word that's already there - do we interpret this as a progress adjustment?
app.set_watch_cursor = 1
t1,t2 = asUnicode(app.Text1.get()),asUnicode(app.Text2.get())
lang2,lang1=t1.lower(),t2.lower() # because it's .lower()'d in progress.txt
d = ProgressDatabase(0)
l1find = "!synth:"+lang1.encode('utf-8')+"_"+firstLanguage
l1find = S(B("!synth:")+lang1.encode('utf-8')+B("_"+firstLanguage))
found = 0
msg=(u""+localise("%s=%s is already in %s.")) % (t1,t2,vocabFile)
msg=(ensure_unicode(localise("%s=%s is already in %s.")) % (t1,t2,vocabFile))
for listToCheck in [d.data,d.unavail]:
if found: break
for item in listToCheck:
if (item[1]==l1find or (type(item[1])==type([]) and l1find in item[1])) and item[2]=="!synth:"+lang2.encode('utf-8')+"_"+secondLanguage:
if (item[1]==l1find or (type(item[1])==type([]) and checkIn(l1find,item[1]))) and item[2]==S(B("!synth:")+lang2.encode('utf-8')+B("_"+secondLanguage)):
if not item[0]: break # not done yet - as not-found
newItem0 = reviseCount(item[0])
app.unset_watch_cursor = 1
# suggested reduction:
thresholds=[1,2,knownThreshold,reallyKnownThreshold,meaningTestThreshold,randomDropThreshold,randomDropThreshold2] ; thresholds.sort() ; thresholds.reverse()
newItem0 = 0
for i in range(len(thresholds)-1):
if item[0]>thresholds[i]:
newItem0=thresholds[i+1] ; break
if getYN(msg+" "+localise("Repeat count is %d. Reduce this to %d for extra revision?" % (item[0],newItem0))):
app.set_watch_cursor = 1
listToCheck.remove(item)
......@@ -1605,14 +1708,15 @@ def gui_event_loop():
app.todo.alert=msg+" "+localise("Repeat count is 0, so we cannot reduce it for extra revision.")
elif menu_response=="add":
text1 = asUnicode(app.Text1.get()).encode('utf-8') ; text2 = asUnicode(app.Text2.get()).encode('utf-8')
msg=sanityCheck(text1,secondLanguage)
if msg: app.todo.alert=u""+msg
msg=generalCheck(text1,secondLanguage)
if msg: app.todo.alert=ensure_unicode(msg)
else:
o=appendVocabFileInRightLanguages()
o.write(text1+"="+text2+"\n") # was " = " but it slows down parseSynthVocab
if not o: continue # IOError
writeB(o,text1+B("=")+text2+B("\n")) # was " = " but it slows down parseSynthVocab
o.close()
if paranoid_file_management:
if filelen(vocabFile)<filelen(vocabFile+"~"): app.todo.alert="Vocab file corruption! You'd better restore the ~ backup."
if filelen(vocabFile)<filelen(vocabFile+"~") or chr(0) in readB(open(vocabFile,"rb"),1024): app.todo.alert="Vocab file corruption! You'd better restore the ~ backup."
if hasattr(app,"vocabList"): app.vocabList.append((ensure_unicode(text1),ensure_unicode(text2)))
app.todo.clear_text_boxes=app.wordsExist=1
elif menu_response=="delete" or menu_response=="replace":
......@@ -1625,19 +1729,19 @@ def gui_event_loop():
if found and menu_response=="replace": # maybe hack progress.txt as well (taken out of the above loop for better failsafe)
d = ProgressDatabase(0)
lang2,lang1=lang2.lower(),lang1.lower() # because it's .lower()'d in progress.txt
l1find = "!synth:"+lang1.encode('utf-8')+"_"+firstLanguage
l1find = S(B("!synth:")+lang1.encode('utf-8')+B("_"+firstLanguage))
for item in d.data:
if (item[1]==l1find or (type(item[1])==type([]) and l1find in item[1])) and item[2]=="!synth:"+lang2.encode('utf-8')+"_"+secondLanguage and item[0]:
if (item[1]==l1find or (type(item[1])==type([]) and checkIn(l1find,item[1]))) and item[2]==S(B("!synth:")+lang2.encode('utf-8')+B("_"+secondLanguage)) and item[0]:
app.unset_watch_cursor = 1
if not getYN(localise("You have repeated %s=%s %d times. Do you want to pretend you already repeated %s=%s %d times?") % (lang2,lang1,item[0],t2,t1,item[0])):
if not getYN(localise("You have repeated %s=%s %d times. Do you want to pretend you already repeated %s=%s %d times?") % (S(lang2),S(lang1),item[0],S(t2),S(t1),item[0])):
app.set_watch_cursor = 1 ; break
d.data.remove(item)
l1replace = "!synth:"+t2.encode('utf-8')+"_"+firstLanguage
l1replace = S(B("!synth:")+t2.encode('utf-8')+B("_"+firstLanguage))
if type(item[1])==type([]):
l = item[1]
l[l.index(l1find)] = l1replace
else: l=l1replace
item = (item[0],l,"!synth:"+t1.encode('utf-8')+"_"+secondLanguage)
item = (item[0],l,S(B("!synth:")+t1.encode('utf-8')+B("_"+secondLanguage)))
d.data.append(item)
app.set_watch_cursor = 1
for i2 in d.unavail:
......@@ -1650,35 +1754,39 @@ def gui_event_loop():
app.todo.clear_text_boxes=1
app.unset_watch_cursor = 1
if not found: app.todo.alert = "OOPS: Item to delete/replace was not found in "+vocabFile
if app: del app.menu_response
def vocabLinesWithLangs(): # used for merging different users' vocab files
langs = [secondLanguage,firstLanguage] ; ret = []
try: v=u8strip(read(vocabFile)).replace("\r","\n")
except IOError: v=""
for l in v.split("\n"):
try: v=u8strip(read(vocabFile)).replace(B("\r"),B("\n"))
except IOError: v=B("")
for l in v.split(B("\n")):
l2=l.lower()
if l2.startswith("set language ") or l2.startswith("set languages "): langs=l.split()[2:]
if l2.startswith(B("set language ")) or l2.startswith(B("set languages ")): langs=map(S,l.split()[2:])
elif l: ret.append((tuple(langs),l)) # TODO what about blank lines? (currently they'd be considered duplicates)
return ret
def appendVocabFileInRightLanguages():
# check if we need a SET LANGUAGE
langs = [secondLanguage,firstLanguage]
try: v=u8strip(read(vocabFile)).replace("\r","\n")
except IOError: v=""
for l in v.split("\n"):
try: v=u8strip(read(vocabFile)).replace(B("\r"),B("\n"))
except IOError: v=B("")
for l in v.split(B("\n")):
l2=l.lower()
if l2.startswith("set language ") or l2.startswith("set languages "): langs=l.split()[2:]
o=open(vocabFile,"a")
if not v.endswith("\n"): o.write("\n")
if not langs==[secondLanguage,firstLanguage]: o.write("SET LANGUAGES "+secondLanguage+" "+firstLanguage+"\n")
if l2.startswith(B("set language ")) or l2.startswith(B("set languages ")):
langs=l.split()[2:]
for i in range(len(langs)): langs[i]=S(langs[i])
try: o=open(vocabFile,"ab") # (ensure binary on Python 3)
except IOError:
show_warning("Cannot write to "+vocabFile+" (current directory is "+os.getcwd()+")")
return
if not v.endswith(B("\n")): o.write(B("\n"))
if not langs==[secondLanguage,firstLanguage]: o.write(B("SET LANGUAGES "+secondLanguage+" "+firstLanguage+"\n"))
return o
def transliterates_differently(text,lang):
global last_partials_transliteration ; last_partials_transliteration=None
global partials_are_sporadic ; o=partials_are_sporadic ; partials_are_sporadic = None # don't want to touch the counters here
if synthcache_lookup("!synth:"+text+"_"+lang):
if synthcache_lookup(B("!synth:")+B(text)+B("_")+B(lang)):
partials_are_sporadic = o
if last_partials_transliteration and not last_partials_transliteration==text: return last_partials_transliteration
else: return # (don't try to translit. if was in synth cache - will have no idea which synth did it)
......@@ -1702,13 +1810,13 @@ def gui_outputTo_start():
except: pass
gui_output_counter = 1 # now local because we also got prefix
if justSynthesize:
if '#' in justSynthesize[1:]: prefix="" # multiple languages
if B('#') in B(justSynthesize)[1:]: prefix=B("") # multiple languages
else: # prefix the language that's being synth'd
prefix=justSynthesize.split()[0]
if prefix.startswith('#'): prefix=prefix[1:]
else: prefix = "lesson"
prefix=B(justSynthesize).split()[0]
if prefix.startswith(B('#')): prefix=prefix[1:]
else: prefix = B("lesson")
while not outputFile or fileExists(outputFile):
outputFile=gui_output_directory+os.sep+prefix+str(gui_output_counter)+extsep+app.outputTo.get()
outputFile=gui_output_directory+os.sep+S(prefix)+str(gui_output_counter)+extsep+app.outputTo.get()
gui_output_counter += 1
global write_to_stdout ; write_to_stdout = 0
global out_type ; out_type = app.outputTo.get()
......@@ -1741,9 +1849,10 @@ def gui_outputTo_end(openDir=True):
# NB we're passing this to cmd, NOT bash:
cmd = "cscript \""+pFiles+"\\Windows Media Components\\Encoder\\WMCmd.vbs\" -input \""+o+"\" -output \""+f+"\" -profile a20_1 -a_content 1"
elif t=="aac": cmd="afconvert \""+o+"\" -d aac \""+f+"\"" # could also use "afconvert file.wav -d samr file.amr", but amr is bigger than aac and not as good; don't know if anyone has a device that plays amr but not aac.
# afconvert default is 64kbit AAC. if want 96+ for music, use -b 96000 after the -d aac (and if want iTunes to be able to accept it, specify extension mp4 instead of aac to afconvert; do not rename aac to mp4, but tell afconvert it's mp4)
else: assert 0
if cygwin:
assert not "'" in cmd, "apostrophees in pathnames could cause trouble on cygwin"
assert not "'" in cmd, "apostrophes in pathnames could cause trouble on cygwin"
cmd="echo '"+cmd+" && exit' | cmd" # seems the only way to get it to work on cygwin
system(cmd)
os.remove(outputFile)
......@@ -1761,7 +1870,7 @@ def gui_outputTo_end(openDir=True):
def main():
global useTK,justSynthesize,waitBeforeStart,traceback,appTitle,app,warnings_toprint
if useTK:
if justSynthesize and not justSynthesize[-1]=='*': appTitle=cond('#' in justSynthesize,"Gradint","Reader") # not "language lesson"
if justSynthesize and not B(justSynthesize)[-1:]==B('*'): appTitle=cond(B('#') in B(justSynthesize),"Gradint","Reader") # not "language lesson"
startTk()
else:
app = None # not False anymore
......@@ -1777,20 +1886,27 @@ def rest_of_main():
except NameError: ceLowMemory=0
if ceLowMemory and getYN("Low memory! Python may crash. Turn off progress saving for safety?"): saveProgress=0
if justSynthesize=="-": primitive_synthloop()
elif justSynthesize and justSynthesize[-1]=='*':
if B(justSynthesize)==B("-"): primitive_synthloop()
elif justSynthesize and B(justSynthesize)[-1:]==B('*'):
justSynthesize=justSynthesize[:-1]
waitBeforeStart = 0
just_synthesize() ; lesson_loop()
elif justSynthesize: just_synthesize()
elif app and waitBeforeStart: gui_event_loop()
elif appuifw: s60_main_menu()
elif android: android_main_menu()
else: lesson_loop()
except SystemExit: pass
except SystemExit:
e = sys.exc_info()[1]
exitStatus = e.code
except KeyboardInterrupt: pass
except PromptException,prEx:
except PromptException:
prEx = sys.exc_info()[1]
waitOnMessage("\nProblem finding prompts:\n"+prEx.message+"\n")
exitStatus = 1
except MessageException:
mEx = sys.exc_info()[1]
waitOnMessage(mEx.message+"\n") ; exitStatus = 1
except:
w="\nSomething has gone wrong with my program.\nThis is not your fault.\nPlease let me know what it says.\nThanks. Silas\n"+exc_info()
try: import traceback
......@@ -1814,7 +1930,7 @@ def rest_of_main():
except: pass
waitOnMessage(w.strip())
if not useTK:
if tracebackFile: sys.stderr.write(read("last-gradint-error"+extsep+"txt"))
if tracebackFile: writeB(sys.stderr,read("last-gradint-error"+extsep+"txt"))
elif traceback: traceback.print_exc() # will be wrong if there was an error in speaking
exitStatus = 1
if appuifw: raw_input() # so traceback stays visible
......@@ -1827,7 +1943,10 @@ def rest_of_main():
elif not app==None: pass # (gets here if WAS 'app' but was closed - DON'T output anything to stderr in this case)
elif appuifw: appuifw.app.set_exit()
elif riscos_sound: show_info("You may now close this Task Window.\n")
elif not android: show_info("\n") # in case got any \r'd string there - don't want to confuse the next prompt
elif not android:
try:
doLabelLastLen ; show_info("\n") # if got any \r'd string there - don't want to confuse the next prompt
except NameError: pass # no doLabelLastLen - no \r
RM_running = 0
if exitStatus: sys.exit(exitStatus)
......
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -25,29 +25,34 @@ class ProgressDatabase(object):
self.unavail = mergeProgress(self.data,scanSamples()+parseSynthVocab(vocabFile))
if not cache_maintenance_mode:
doLabel("Checking transliterations")
global tList # for Python 2.1
tList = {}
def addVs(ff,dirBase):
if dirBase: dirBase += os.sep
if dirBase+ff in variantFiles:
if os.sep in ff: ffpath=ff[:ff.rfind(os.sep)+1]
else: ffpath=""
variantList=map(lambda x:ffpath+x,variantFiles[dirBase+ff])
def addVs(ff,dirBase): # add variants of ff to tList which we might need to transliterate
dirBase,ff = B(dirBase),B(ff)
if dirBase: dirBase += B(os.sep)
if checkIn(dirBase+ff,variantFiles):
if B(os.sep) in ff: ffpath=ff[:ff.rfind(B(os.sep))+1]
else: ffpath=B("")
variantList=map(lambda x,f=ffpath:f+B(x),variantFiles[dirBase+ff])
else: variantList = [ff]
l=languageof(ff)
for f in variantList:
if f.lower().endswith(dottxt): text=u8strip(read(dirBase+f)).strip(wsp)
elif f.find("!synth")==-1: continue # don't need to translit. filenames of wav's etc
f = B(f)
if f.lower().endswith(B(dottxt)):
text=bwspstrip(u8strip(read(dirBase+f)))
elif f.find(B("!synth"))==-1: continue # don't need to translit. filenames of wav's etc
else: text = textof(f)
if not l in tList: tList[l]={}
if not checkIn(l,tList): tList[l]={}
tList[l][text]=1
for ff in availablePrompts.lsDic.values(): addVs(ff,promptsDirectory)
for _,l1,l2 in self.data:
if not type(l1)==type([]): l1=[l1]
for ff in l1+[l2]: addVs(ff,samplesDirectory)
doLabel("Transliterating")
for lang,dic in tList.items():
for lang,dic in list(tList.items()):
s = get_synth_if_possible(lang,0)
if s and hasattr(s,"update_translit_cache"): s.update_translit_cache(lang,dic.keys())
if s and hasattr(s,"update_translit_cache"): s.update_translit_cache(lang,list(dic.keys()))
del tList
self.didScan = alsoScan
def _load_from_binary(self):
if pickledProgressFile and fileExists(pickledProgressFile):
......@@ -55,11 +60,12 @@ class ProgressDatabase(object):
global firstLanguage, secondLanguage, otherLanguages
if compress_progress_file or (unix and got_program("gzip")):
if paranoid_file_management: open(pickledProgressFile) # ensure ready
f = os.popen('gzip -fdc "'+pickledProgressFile+'"',"rb")
f = os.popen('gzip -fdc "'+pickledProgressFile+'"',popenRB)
else: f=open(pickledProgressFile,"rb")
try: thingsToSet, tup = pickle.Unpickler(f).load()
except: return False # probably moved to a different Python version or something
exec(thingsToSet)
self._py3_fix()
return True
# otherwise drop out and return None
def _load_from_text(self,fromString=0):
......@@ -67,11 +73,11 @@ class ProgressDatabase(object):
elif fileExists(progressFile):
if compress_progress_file or (unix and got_program("gzip")):
if paranoid_file_management: open(progressFile) # ensure ready
expr = os.popen('gzip -fdc "'+progressFile+'"',"rb").read()
expr = readB(os.popen('gzip -fdc "'+progressFile+'"',popenRB))
else: expr = read(progressFile)
else: expr = None
if expr:
expr = u8strip(expr) # just in case progress.txt has been edited in Notepad
expr = u8strip(expr).replace(B("\r\n"),B("\n")) # just in case progress.txt has been edited in Notepad
# First, try evaluating it as self.data (legacy progress.txt from older versions). If that doesn't work, execute it (newer versions).
global firstLanguage, secondLanguage, otherLanguages
try: self.data = eval(expr)
......@@ -81,49 +87,80 @@ class ProgressDatabase(object):
except: codeop = 0
if codeop: # try a lower-memory version (in case text file has been edited by hand and we're on NSLU2 or something) - don't compile all of it at once
lineCache = []
for l in expr.replace("\r\n","\n").split("\n"):
for l in expr.replace(B("\r\n"),B("\n")).split(B("\n")):
lineCache.append(l)
if lineCache[-1].endswith(","): continue # no point trying to compile if it's obviously incomplete
code = codeop.compile_command("\n".join(lineCache))
if lineCache[-1].endswith(B(",")): continue # no point trying to compile if it's obviously incomplete
code = codeop.compile_command("# coding=utf-8\n"+S(B("\n").join(lineCache)))
if code:
lineCache = []
exec code
else: exec(expr)
exec(code)
else: exec(B("# coding=utf-8\n")+expr)
del expr
# Remove legacy extentions in promptsData (needed only when loading from text, as this was before pickledProgressFile was added)
for k in self.promptsData.keys():
for k in list(self.promptsData.keys()):
if k.endswith(dotwav) or k.endswith(dotmp3):
self.promptsData[k[:-len(dotwav)]]=self.promptsData[k]
del self.promptsData[k]
self._py3_fix()
def _saved_by_py3(self):
# NB the Windows version of Gradint is still Python 2.3 so generator expressions (new in 2.4) would be a syntax error even though this code is never reached in that version, so:
for l in [self.data,self.unavail]:
for i in l:
for j in i[1:]:
if type(j)==str: j=[j]
for k in j:
for c in k:
if ord(c) > 255: return True # must have been written by the Python 3 version
def _py3_fix(self):
if not type("")==type(u""): return
# we're Python 3, and we might have just loaded data from Python 2. Might have to encode as Latin-1 then decode as UTF-8. But don't do this if file was in fact saved by Python 3.
if self._saved_by_py3(): return
for l in [self.data,self.unavail]:
for i in range(len(l)):
for j in [1,2]:
if type(l[i][j])==str: l[i]=l[i][:j]+(S2(LB(l[i][j])),)+l[i][j+1:]
elif type(l[i][j])==list: l[i]=l[i][:j]+(map(lambda x:S2(LB(x)),l[i][j]),)+l[i][j+1:]
def _py3_fix_on_save(self):
if type("")==type(u"") and not self._saved_by_py3(): self.unavail.append((1,u"\u2014","[Py3]")) # ensure there's at least one, to prevent a py3_fix redo
def save(self,partial=0):
if need_say_where_put_progress: show_info("Saving "+cond(partial,"partial ","")+"progress to "+progressFile+"... ")
else: show_info("Saving "+cond(partial,"partial ","")+"progress... ")
self._py3_fix_on_save()
global progressFileBackup
# Remove 0-repeated items (helps editing by hand)
data = [] # don't use self.data - may want to make another lesson after saving
for a,b,c in self.data:
if a: data.append(denumber_filelists(a,b,c))
data.sort(cmpfunc) # to normalise when using diff etc
sort(data,cmpfunc) # to normalise when using diff etc
if progressFileBackup:
try:
import shutil
shutil.copy2(progressFile,progressFileBackup) # preserve timestamp etc if shutil is available
except:
try:
open(progressFileBackup,'wb').write(read(progressFile))
try: write(progressFileBackup,read(progressFile))
except IOError: pass # maybe progressFile not made yet
progressFileBackup = None
while True:
try:
if compress_progress_file: f=os.popen('gzip -9 > "'+progressFile+'"','w')
if compress_progress_file:
if paranoid_file_management: fn=os.tempnam() # on some ftpfs setups gzip can fail causing silent corruption
else: fn=progressFile
f=os.popen('gzip -9 > "'+fn+'"','w')
else: f = open(progressFile,'w')
global progressFileHeader
if type(u"")==type(""): # Python 3: ensure UTF-8
import codecs
realF,f = f,codecs.getwriter("utf-8")(f.buffer)
progressFileHeader=progressFileHeader.replace("mode: python ","mode: python; coding: utf-8")
else: realF = f
f.write(progressFileHeader)
f.write("firstLanguage=\"%s\"\nsecondLanguage=\"%s\"\n# otherLanguages=%s\n" % (firstLanguage,secondLanguage,otherLanguages)) # Note: they're declared "global" above (and otherLanguages commented out here for now, since may add to it in advanced.txt) (Note also save_binary below.)
if self.didScan: f.write("# collection=%d done=%d left=%d lessonsLeft=%d\n" % (len(self.data),len(data),len(self.data)-len(data),(len(self.data)-len(data)+maxNewWords-1)/maxNewWords))
if self.didScan and maxNewWords: f.write("# collection=%d done=%d left=%d lessonsLeft=%d\n" % (len(self.data),len(data),len(self.data)-len(data),(len(self.data)-len(data)+maxNewWords-1)/maxNewWords))
prettyPrintLongList(f,"self.data",data)
f.write("self.promptsData=") ; pprint.PrettyPrinter(indent=2,width=60,stream=f).pprint(self.promptsData)
prettyPrintLongList(f,"self.unavail",self.unavail)
f.close()
realF.close()
if compress_progress_file and paranoid_file_management: write(progressFile,read(fn)),os.remove(fn)
self.save_binary(data)
except IOError: # This can happen for example on some PocketPC devices if you reconnect the power during progress save (which is likely if you return the device to the charger when lesson finished)
if app or appuifw or android:
......@@ -135,10 +172,17 @@ class ProgressDatabase(object):
if not app and not appuifw and not android: show_info("done\n")
def save_binary(self,data): # save a pickled version if possible (no error if not)
if not (pickledProgressFile and pickle): return
self._py3_fix_on_save()
try:
if compress_progress_file: f=os.popen('gzip -9 > "'+pickledProgressFile+'"','wb')
if compress_progress_file:
if paranoid_file_management: fn=os.tempnam()
else: fn=pickledProgressFile # TODO near-duplicate code with above
f=os.popen('gzip -9 > "'+fn+'"',popenWB)
if hasattr(f,'buffer'): _,f = f,f.buffer
else: f = open(pickledProgressFile,'wb')
pickle.Pickler(f,-1).dump(("self.data,self.promptsData,self.unavail,firstLanguage,secondLanguage = tup", (data,self.promptsData,self.unavail,firstLanguage,secondLanguage)))
f.close()
if compress_progress_file and paranoid_file_management: write(pickledProgressFile,read(fn)),os.remove(fn)
except IOError: pass # OK if not got permissions to do it (NB need to catch the write as well because popen won't throw, and don't have to worry about a corrupted partial binary because loader would ignore it)
def savePartial(self,filesNotPlayed):
curPD,curDat = self.promptsData, self.data[:] # in case want to save a more complete one later
......@@ -146,7 +190,7 @@ class ProgressDatabase(object):
if hasattr(self,"previous_filesNotPlayed"):
i=0
while i<len(filesNotPlayed):
if filesNotPlayed[i] in self.previous_filesNotPlayed: i+=1
if checkIn(filesNotPlayed[i],self.previous_filesNotPlayed): i+=1
else: del filesNotPlayed[i] # cumulative effects if managed to play it last time but not this time (and both lessons incomplete)
self.previous_filesNotPlayed = filesNotPlayed = list2set(filesNotPlayed)
if not filesNotPlayed:
......@@ -160,7 +204,7 @@ class ProgressDatabase(object):
l.append(self.data[i][2])
found=0
for ii in l:
if ii in filesNotPlayed:
if checkIn(ii,filesNotPlayed):
self.data[i] = self.oldData[i]
found=1 ; break
if not found and not self.data[i] == self.oldData[i]: changed = 1
......@@ -170,7 +214,7 @@ class ProgressDatabase(object):
def makeLesson(self):
global maxLenOfLesson
self.l = Lesson()
self.data.sort(cmpfunc) ; jitter(self.data)
sort(self.data,cmpfunc) ; jitter(self.data)
self.oldData = self.data[:] # for handling interrupts & partial progress saves
self.exclude = {} ; self.do_as_poem = {}
# First priority: Recently-learned old words
......@@ -192,28 +236,34 @@ class ProgressDatabase(object):
for p in poems:
for l in p: self.do_as_poem[self.responseIndex[l]] = p
self.addToLesson(reallyKnownThreshold,-1,1,1,-1)
if not self.l.events:
# desperado if someone really wants extra revision of few words
global randomDropLevel, randomDropLevel2
rdl,rdl2,randomDropLevel,randomDropLevel2 = randomDropLevel,randomDropLevel2,0,0
self.addToLesson(reallyKnownThreshold,-1,1,1,-1)
randomDropLevel, randomDropLevel2 = rdl,rdl2
l = self.l ; del self.l, self.responseIndex, self.do_as_poem
if not l.events: raise Exception("Didn't manage to put anything in the lesson")
if commentsToAdd: l.addSequence(commentSequence())
if commentsToAdd: l.addSequence(commentSequence(),False)
if orderlessCommentsToAdd:
for c in orderlessCommentsToAdd:
try:
l.addSequence([GluedEvent(Glue(1,maxLenOfLesson),fileToEvent(c,""))])
l.addSequence([GluedEvent(Glue(1,maxLenOfLesson),fileToEvent(c,""))],False)
except StretchedTooFar:
sys.stderr.write("Was trying to add %s\n" % (c,))
show_info(("Was trying to add %s\n" % (c,)),True)
raise
# Add note on "long pause", for beginners
longpause = "longpause_"+firstLanguage
if not advancedPromptThreshold and not longpause in availablePrompts.lsDic: longpause = "longpause_"+secondLanguage
if not advancedPromptThreshold and not checkIn(longpause,availablePrompts.lsDic): longpause = "longpause_"+secondLanguage
o=maxLenOfLesson ; maxLenOfLesson = max(l.events)[0]
if longpause in availablePrompts.lsDic and self.promptsData.get(longpause,0)==0:
if checkIn(longpause,availablePrompts.lsDic) and self.promptsData.get(longpause,0)==0:
try:
def PauseEvent(): return fileToEvent(availablePrompts.lsDic[longpause],promptsDirectory)
firstPauseMsg = PauseEvent()
def PauseEvent(longpause): return fileToEvent(availablePrompts.lsDic[longpause],promptsDirectory)
firstPauseMsg = PauseEvent(longpause)
# the 1st potentially-awkward pause is likely to be a beepThreshold-length one
l.addSequence([GluedEvent(Glue(1,maxLenOfLesson),CompositeEvent([firstPauseMsg,Event(max(5,beepThreshold-firstPauseMsg.length))]))])
l.addSequence([GluedEvent(Glue(1,maxLenOfLesson),CompositeEvent([firstPauseMsg,Event(max(5,beepThreshold-firstPauseMsg.length))]))],False)
while True:
l.addSequence([GluedEvent(Glue(1,maxLenOfLesson),CompositeEvent([PauseEvent(),Event(50)]))])
l.addSequence([GluedEvent(Glue(1,maxLenOfLesson),CompositeEvent([PauseEvent(longpause),Event(50)]))],False)
self.promptsData[longpause] = 1
except StretchedTooFar: pass
maxLenOfLesson = o
......@@ -246,7 +296,7 @@ class ProgressDatabase(object):
numFailures = 0 ; startTime = time.time() # for not taking too long
for i in xrange(len(self.data)):
if maxNumToAdd>-1 and numberAdded >= maxNumToAdd: break # too many
if i in self.exclude: continue # already had it
if checkIn(i,self.exclude): continue # already had it
(timesDone,promptFile,zhFile)=self.data[i]
if timesDone < minTimesDone or (maxTimesDone>=0 and timesDone > maxTimesDone): continue # out of range this time
if timesDone >= knownThreshold: thisNumToTry = min(random.choice([2,3,4]),numToTry)
......@@ -255,7 +305,7 @@ class ProgressDatabase(object):
# dropping it at random
self.exclude[i] = 1 # pretend we've done it
continue
if i in self.do_as_poem:
if checkIn(i,self.do_as_poem):
# this is part of a "known poem" and let's try to do it in sequence
self.try_add_poem(self.do_as_poem[i]) ; continue
oldPromptsData = self.promptsData.copy()
......@@ -264,8 +314,8 @@ class ProgressDatabase(object):
global earliestAllowedEvent ; earliestAllowedEvent = 0
if not timesDone and type(promptFile)==type([]):
# for poems: if any previously-added new word makes part of the prompt, try to ensure this one is introduced AFTER that one
for f,t in newWordTimes.items():
if f in promptFile: earliestAllowedEvent = max(earliestAllowedEvent,t)
for f,t in list(newWordTimes.items()):
if checkIn(f,promptFile): earliestAllowedEvent = max(earliestAllowedEvent,t)
if not timesDone: newWordTimes[zhFile] = maxLenOfLesson # by default (couldn't fit it in). (add even if not type(promptFile)==type([]), because it might be a first line)
try: self.l.addSequence(seq)
except StretchedTooFar: # If this happens, couldn't fit the word in anywhere. If this is "filling in gaps" then it's likely that we won't be able to fit in any more words this lesson, so stop trying.
......@@ -314,7 +364,7 @@ class ProgressDatabase(object):
covered = 0
for timesDone,promptFile,zhFile in self.data:
if timesDone: covered += 1
x = (covered > 1000) # arbitrary
x = (covered > veryExperiencedThreshold)
self.cached_very_experienced = x
return x
def message(self):
......@@ -323,8 +373,8 @@ class ProgressDatabase(object):
for timesDone,promptFile,zhFile in self.data:
if timesDone:
covered += 1
if zhFile.find(exclude_from_coverage)==-1: actualCovered += 1
if zhFile.find(exclude_from_coverage)==-1: actualTotal += 1
if B(zhFile).find(B(exclude_from_coverage))==-1: actualCovered += 1
if B(zhFile).find(B(exclude_from_coverage))==-1: actualTotal += 1
l=cond(app,localise,lambda x:x)
toRet = (l("You have %d words in your collection") % total)
if not total==actualTotal: toRet += (" (actually %d)" % actualTotal)
......@@ -339,15 +389,23 @@ def prettyPrintLongList(f,thing,data):
if winCEsound: p=0 # don't use WinCE's PrettyPrinter here - it inconsistently escapes utf8 sequences (result can't reliably be edited in MADE etc)
else: p=pprint.PrettyPrinter(indent=2,width=60,stream=f)
for start in range(0,len(data),step):
dat = data[start:start+step]
if type("")==type(u""): # Python 3: probably best to output strings rather than bytes
for i in range(len(dat)):
for j in [1,2]:
if type(dat[i][j])==bytes:
dat[i]=dat[i][:j]+(S2(dat[i][j]),)+dat[i][j+1:]
elif type(dat[i][j])==list:
dat[i]=dat[i][:j]+(map(S2,dat[i][j]),)+dat[i][j+1:]
if start: f.write(thing+"+=")
else: f.write(thing+"=")
if p:
t = time.time()
p.pprint(data[start:start+step])
p.pprint(dat)
if not start and (time.time()-t)*(len(data)/step) > 5: p=0 # machine is too slow - use faster version on next iteration
else: # faster version - not quite as pretty
f.write("[")
for d in data[start:start+step]: f.write(" "+repr(d)+",\n")
for d in dat: f.write(" "+repr(d)+",\n")
f.write("]\n")
def calcDropLevel(timesDone):
......@@ -372,15 +430,23 @@ def cmpfunc(x,y):
if r: return r # skipping the rest if x[0]!=y[0]
if x[0]: return cmpfunc_test(x,y) # our special order is needed only for new words (to ensure correct order of introduction)
def my_toString(x):
if type(x)==type([]): return "".join(x)
else: return x
x2 = (my_toString(x[1]).replace(os.sep,chr(0)), my_toString(x[2]).replace(os.sep,chr(0)))
y2 = (my_toString(y[1]).replace(os.sep,chr(0)), my_toString(y[2]).replace(os.sep,chr(0)))
if type(x)==type([]): return B("").join(map(B,x))
else: return B(x)
x2 = (my_toString(x[1]).replace(B(os.sep),chr(0)), my_toString(x[2]).replace(B(os.sep),chr(0)))
y2 = (my_toString(y[1]).replace(B(os.sep),chr(0)), my_toString(y[2]).replace(B(os.sep),chr(0)))
return cmpfunc_test(x2,y2)
def cmpfunc_test(x,y):
if x < y: return -1
elif x > y: return 1
else: return 0
try:
if x < y: return -1
elif x > y: return 1
else: return 0
except: # probably Python 3 can't compare list vs string
if x[0] < y[0]: return -1
elif x[0] > y[0]: return 1
x,y = repr(x),repr(y)
if x < y: return -1
elif x > y: return 1
else: return 0
def denumber_filelists(r,x,y):
if type(x)==type([]): x=map(lambda z:denumber_synth(z),x)
......@@ -389,18 +455,18 @@ def denumber_filelists(r,x,y):
else: y=denumber_synth(y)
return (r,x,y)
def denumber_synth(z,also_norm_extsep=0):
zf = z.find("!synth:")
if zf>-1:
z=B(z) ; zf = z.find(B("!synth:"))
if zf>=0:
z=lower(z[zf:]) # so ignores the priority-number it had (because the vocab.txt file might have been re-organised hence changing all the numbers). Also a .lower() so case changes don't change progress. (Old versions of gradint said .lower() when parsing vocab.txt, but this can cause problems with things like Mc[A-Z].. in English espeak)
if z.endswith(dotwav) or z.endswith(dotmp3): return z[:z.rindex(extsep)] # remove legacy extensions from synth vocab
elif also_norm_extsep: return z.replace("\\","/").replace(".","/") # so compares equally across platforms with os.sep and extsep differences
if z.endswith(B(dotwav)) or z.endswith(B(dotmp3)): return z[:z.rindex(B(extsep))] # remove legacy extensions from synth vocab
elif also_norm_extsep: return z.replace(B("\\"),B("/")).replace(B("."),B("/")) # so compares equally across platforms with os.sep and extsep differences
return z
def norm_filelist(x,y):
def noext(x): return (x+extsep)[:x.rfind(extsep)] # so user can change e.g. wav to mp3 without disrupting progress.txt
if type(x)==type([]): x=tuple(map(lambda z:denumber_synth(noext(z),1),x))
def noext(x): return (B(x)+B(' '))[:B(x).rfind(B(extsep))] # so user can change e.g. wav to mp3 without disrupting progress.txt (the ' ' is simply removed if rfind returns -1)
if type(x)==type([]): x=tuple(map(lambda z,noext=noext:denumber_synth(noext(z),1),x))
else: x=denumber_synth(noext(x),1)
if type(y)==type([]): y=tuple(map(lambda z:denumber_synth(noext(z),1),y))
if type(y)==type([]): y=tuple(map(lambda z,noext=noext:denumber_synth(noext(z),1),y))
else: y=denumber_synth(noext(y),1)
return (x,y)
def mergeProgress(progList,scan):
......@@ -426,7 +492,7 @@ def mergeProgress(progList,scan):
renames = {}
for (_,j,k) in scan:
key = norm_filelist(j,k)
if key in proglistDict:
if checkIn(key,proglistDict):
# an existing item - but in the case of synth'd vocab, we need to take the capitals/lower-case status from the scan rather than from the progress file (see comment above in denumber_synth) so:
progList[proglistDict[key]]=(progList[proglistDict[key]][0],j,k)
elif type(key[0])==type("") and (key[0]+key[1]).find("!synth")==-1 and ("_" in key[0] and "_" in key[1]):
......@@ -439,16 +505,16 @@ def mergeProgress(progList,scan):
while ki>lastDirsep and not "0"<=normK[ki]<="9": ki -= 1
if ki<=lastDirsep: break
key2 = (key[0][:ki+1]+key[0][key[0].rindex("_"):],key[1][:ki+1]+key[1][key[1].rindex("_"):])
if key2 in proglistDict:
if not key2 in renames: renames[key2] = []
if checkIn(key2,proglistDict):
if not checkIn(key2,renames): renames[key2] = []
renames[key2].append((j,k))
found=1 ; break
while ki>lastDirsep and "0"<=normK[ki]<="9": ki -= 1
if not found: progList.append((0,j,k)) # new item
else: progList.append((0,j,k)) # ditto
scanlistDict[key]=1
for k,v in renames.items():
if k in scanlistDict or len(v)>1: # can't make sense of this one - just add the new stuff
for k,v in list(renames.items()):
if checkIn(k,scanlistDict) or len(v)>1: # can't make sense of this one - just add the new stuff
for jj,kk in v: progList.append((0,jj,kk))
else: progList[proglistDict[k]]=(progList[proglistDict[k]][0],v[0][0],v[0][1])
# finally, separate off any with non-0 progress that are
......@@ -458,7 +524,7 @@ def mergeProgress(progList,scan):
n = 0 ; unavailList = []
while n<len(progList):
i,j,k = progList[n]
if not norm_filelist(j,k) in scanlistDict:
if not checkIn(norm_filelist(j,k), scanlistDict):
unavailList.append((i,j,k))
del progList[n]
else: n += 1
......@@ -514,11 +580,11 @@ def jitter(list):
# need to work on them more quickly, and can limit manually
limitCounts = {} ; i = 0 ; imax = len(list)
while i < imax:
if list[i][0]==0 and (list[i][-1] in limitedFiles): # or not languageof(list[i][2])==secondLanguage):
if list[i][0]==0 and checkIn(list[i][-1],limitedFiles): # or not languageof(list[i][2])==secondLanguage):
# if not languageof(list[i][2])==secondLanguage: countNo="other-langs"
# else:
countNo = limitedFiles[list[i][-1]]
if not countNo in limitCounts: limitCounts [countNo] = 0
if not checkIn(countNo,limitCounts): limitCounts [countNo] = 0
limitCounts [countNo] += 1
# (below is a hack: if already moved something, set limit_words to 1. May want to do it better than that e.g. go back and ensure the first thing only left 1 as well, or share out limit_words among any concurrently available new items that are just about to be introduced)
if limitCounts [countNo] > cond(imax==len(list),limit_words,1) or (countNo=="other-langs" and limitCounts [countNo] > 1):
......@@ -543,14 +609,14 @@ def find_known_poems(progressData):
nextLineDic[line]=response # TODO check what would happen if 2 different poems in vocab.txt share an identical line (or if responseIndex is ambiguous in any way)
hasPreviousLine[response]=True
poems = []
for poemFirstLine in filter(lambda x:not x in hasPreviousLine,nextLineDic.keys()):
for poemFirstLine in filter(lambda x,hasPreviousLine=hasPreviousLine:not x in hasPreviousLine,nextLineDic.keys()):
poemLines = [] ; line = poemFirstLine
poem_is_viable = True
while True:
poemLines.append(line)
if not line in responseIndex or progressData[responseIndex[line]][0] < reallyKnownThreshold:
if not checkIn(line,responseIndex) or progressData[responseIndex[line]][0] < reallyKnownThreshold:
poem_is_viable = False ; break # whole poem not in database yet, or is but not well-rehearsed
if not line in nextLineDic: break
if not checkIn(line,nextLineDic): break
line = nextLineDic[line]
if poem_is_viable: poems.append(poemLines)
return poems, responseIndex
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -18,7 +18,12 @@ def doOneLesson(dbase):
lesson = dbase.makeLesson()
else:
soFar = "Re-loading saved lesson, so not scanning collection."
if compress_progress_file: lesson=pickle.Unpickler(os.popen('gzip -fdc "'+saveLesson+'"','rb')).load()
if compress_progress_file:
pp = os.popen('gzip -fdc "'+saveLesson+'"',popenRB)
if hasattr(pp,'buffer'): ppb = pp.buffer
else: ppb = pp
lesson=pickle.Unpickler(ppb).load()
del ppb,pp
else: lesson=pickle.Unpickler(open(saveLesson,'rb')).load()
if app and not dbase: app.setNotFirstTime()
while 1:
......@@ -37,12 +42,19 @@ def doOneLesson(dbase):
app.setLabel("Starting lesson")
app.cancelling = 0
lesson.play()
if not gluedListTracker==None:
global lastLessonMade ; lastLessonMade = lesson # used by utils/trace.py
if dbase and saveProgress and not dbase.saved_completely: # justSaveLesson is a no-op if not first time through lesson (because scripts that use it probably mean "save if not already save"; certainly don't mean "play if is saved")
if cancelledFiles: dbase.savePartial(cancelledFiles)
else: dbase.save()
if dbase.saved_completely and app: app.setNotFirstTime() # dbase.saved_completely could have been done by EITHER of the above (e.g. overlapping partial saves)
if saveLesson:
if compress_progress_file: pickle.Pickler(os.popen('gzip -9 > "'+saveLesson+'"','wb'),-1).dump(lesson)
if compress_progress_file:
pp = os.popen('gzip -9 > "'+saveLesson+'"',popenWB)
if hasattr(pp,'buffer'): ppb=pp.buffer
else: ppb = pp
pickle.Pickler(ppb,-1).dump(lesson) # TODO: paranoid_file_management ? (c.f. saveProgress)
del ppb,pp
else: pickle.Pickler(open(saveLesson,"wb"),-1).dump(lesson)
saveLesson = None # so saves only the first when doing multiple lessons
if justSaveLesson: break
......@@ -52,15 +64,15 @@ def doOneLesson(dbase):
def disable_lid(restore): # for portable netbooks (like eee), can close lid & keep listening
if unix:
if app and not outputFile:
import commands ; global oldLid,warnedAC
global oldLid,warnedAC
try: warnedAC
except: warnedAC=0
if (not restore) and commands.getoutput("cat /proc/acpi/ac_adapter/AC*/state 2>/dev/null").find("off-line")>-1 and not warnedAC:
if (not restore) and getoutput("cat /proc/acpi/ac_adapter/AC*/state 2>/dev/null").find("off-line")>=0 and not warnedAC:
waitOnMessage("Some quirky Linux battery managers turn speakers off mid-lesson, so AC power is recommended.") ; warnedAC=1 # (TODO what if pull out AC during the lesson without looking at the screen? Spoken message??)
ls = "et org.gnome.settings-daemon.plugins.power lid-close-" ; src=["ac","battery"]
if restore and oldLid[0]: return [commands.getoutput("gsettings s"+ls+p+"-action "+q+" 2>/dev/null") for p,q in zip(src,oldLid)]
oldLid = [commands.getoutput("gsettings g"+ls+p+"-action 2>/dev/null").replace("'","") for p in src]
if oldLid[0]: [commands.getoutput("gsettings s"+ls+p+"-action blank 2>/dev/null") for p in src]
if restore and oldLid[0]: return [getoutput("gsettings s"+ls+p+"-action "+q+" 2>/dev/null") for p,q in zip(src,oldLid)]
oldLid = [getoutput("gsettings g"+ls+p+"-action 2>/dev/null").replace("'","") for p in src]
if oldLid[0]: [getoutput("gsettings s"+ls+p+"-action blank 2>/dev/null") for p in src]
if loadLesson==-1: loadLesson=(fileExists(saveLesson) and time.localtime(os.stat(saveLesson).st_mtime)[:3]==time.localtime()[:3])
......@@ -78,7 +90,7 @@ def lesson_loop():
dbase = ProgressDatabase()
if not dbase.data:
msg = "There are no words to put in the lesson."
if app or appuifw:
if app or appuifw or android:
drop_to_synthloop = False
msg = localise(msg)+"\n"+localise("Please add some words first.")
else:
......
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -17,7 +17,7 @@ def filesToEvents(files,dirBase=None):
# (e.g. when learning poetry, may have a composite
# prompt)
if not type(files)==type([]): files = [files]
return CompositeEvent(map(lambda x:fileToEvent(x,dirBase),files))
return CompositeEvent(map(lambda x,d=dirBase:fileToEvent(x,d),files))
class Partials_Synth(Synth):
# text is really a list of lists of filenames
......@@ -40,11 +40,12 @@ class Partials_Synth(Synth):
# the first syllable in 1st list can optionally be the header file to use
fname = os.tempnam()+dotwav
o=open(fname,"wb")
if not (text and text[0] and text[0][0].endswith(dotwav)): o.write(read(partialsDirectory+os.sep+"header"+dotwav))
if not (text and text[0] and B(text[0][0]).endswith(B(dotwav))): o.write(read(partialsDirectory+os.sep+"header"+dotwav))
for phrase in text:
datFileInUse = 0 ; assert type(phrase)==type([])
for f in phrase:
if f in audioDataPartials:
f = S(f)
if checkIn(f,audioDataPartials):
datFile,offset,size = audioDataPartials[f]
if not datFileInUse: datFileInUse = open(partialsDirectory+os.sep+datFile,"rb")
datFileInUse.seek(offset) ; o.write(datFileInUse.read(size))
......@@ -58,16 +59,20 @@ class Partials_Synth(Synth):
def fileToEvent(fname,dirBase=None):
if dirBase==None: dirBase=samplesDirectory
if dirBase: dirBase += os.sep
orig_fname = fname
if os.sep in fname and fname.find("!synth:")==-1: dirBase,fname = dirBase+fname[:fname.rindex(os.sep)+1], fname[fname.rindex(os.sep)+1:]
if "_" in fname: lang = languageof(fname)
dirBase,fname = B(dirBase),B(fname)
if dirBase: dirBase += B(os.sep)
orig_DB,orig_fname = dirBase,fname
if B(os.sep) in fname and fname.find(B("!synth:"))==-1: dirBase,fname = dirBase+fname[:fname.rindex(B(os.sep))+1], fname[fname.rindex(B(os.sep))+1:]
if B("_") in fname: lang = languageof(fname)
else: lang="-unknown-" # so can take a simple wav file, e.g. for endAnnouncement
if dirBase+fname in variantFiles:
if checkIn(dirBase+fname,variantFiles):
variantFiles[dirBase+fname]=variantFiles[dirBase+fname][1:]+[variantFiles[dirBase+fname][0]] # cycle through the random order of variants
fname=variantFiles[dirBase+fname][0]
if fname.lower().endswith(dottxt) and "_" in fname: fname = "!synth:"+u8strip(read(dirBase+fname)).strip(wsp)+'_'+lang
if fname.find("!synth:")>-1:
fname=B(variantFiles[dirBase+fname][0])
if fname.lower().endswith(B(dottxt)) and B("_") in fname:
ftxt = bwspstrip(u8strip(read(dirBase+fname)))
if not ftxt: raise MessageException(B("File ")+fname+B(" in ")+dirBase+B(" has no text in it; please fix this")) # nicer message than catching it at a lower level
fname = B("!synth:")+B(ftxt)+B('_')+B(lang)
if fname.find(B("!synth:"))>=0:
s = synthcache_lookup(fname)
if type(s)==type([]): # trying to synth from partials
if filter(lambda x:not type(x)==type([]), s): # but not completely (switching between partials and synth in a long text), this is more tricky:
......@@ -88,9 +93,11 @@ def fileToEvent(fname,dirBase=None):
e.append(Event(betweenPhrasePause))
e=CompositeEvent(e[:-1]) # omit trailing pause
if not lessonIsTight(): e.length=math.ceil(e.length) # (TODO slight duplication of logic from SampleEvent c'tor)
elif s: e=SampleEvent(synthCache+os.sep+s) # single file in synth cache
else: e=synth_event(languageof(fname),textof(fname))
e.is_prompt=(dirBase==promptsDirectory+os.sep)
elif s: e=SampleEvent(synthCache+os.sep+S(s)) # single file in synth cache
else:
e=synth_event(languageof(fname),textof(fname))
e.file = orig_DB+orig_fname # for trace.py check_for_pictures
e.is_prompt=(dirBase==B(promptsDirectory+os.sep))
else: e=SampleEvent(dirBase+fname)
e.setOnLeaves('wordToCancel',orig_fname)
return e
......@@ -104,22 +111,24 @@ if mp3web: # synth-cache must exist
if synthCache:
# this listdir() call can take ages on rpcemu if it's large
if riscos_sound: show_info("Reading synthCache... ")
try: synthCache_contents = os.listdir(synthCache)
try: synthCache_contents = map(B,os.listdir(synthCache))
except: synthCache_contents = synthCache = []
for i in synthCache_contents:
if i.upper()==transTbl: # in case it's a different case
transTbl=i ; break
if i.upper()==B(transTbl): # in case it's a different case
transTbl=S(i) ; break
synthCache_contents = list2dict(synthCache_contents) # NOT 2set, as the GUI can delete things from it
if riscos_sound: show_info("done\n")
synthCache_transtbl = {}
if synthCache and transTbl in synthCache_contents:
if synthCache and checkIn(B(transTbl),synthCache_contents):
ensure_nodups = {} # careful of duplicate filenames being appended to trans.tbl, make sure they override previous entries
for l in open(synthCache+os.sep+transTbl).readlines():
v,k = l.strip(wsp).split(None,1)
if v in ensure_nodups: del synthCache_transtbl[ensure_nodups[v]]
for l in open(synthCache+os.sep+transTbl,'rb').readlines():
v,k = bwspstrip(l).split(None,1)
if checkIn(v,ensure_nodups): del synthCache_transtbl[ensure_nodups[v]]
ensure_nodups[v]=k ; synthCache_transtbl[k]=v
del ensure_nodups
def textof(fname): return fname[fname.find('!synth:')+7:fname.rfind('_')]
def textof(fname):
fname = B(fname)
return fname[fname.find(B('!synth:'))+7:fname.rfind(B('_'))]
last_partials_transliteration = None
synth_partials_cache = {} ; scl_disable_recursion = 0
def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=None):
......@@ -127,8 +136,9 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
if dirBase==None: dirBase=samplesDirectory
if dirBase: dirBase += os.sep
if not lang: lang = languageof(fname)
if fname.lower().endswith(dottxt):
try: fname = fname[:fname.rfind("_")]+"!synth:"+u8strip(read(dirBase+fname)).strip(wsp)+"_"+lang
fname = B(fname)
if fname.lower().endswith(B(dottxt)):
try: fname = fname[:fname.rfind(B("_"))]+B("!synth:")+bwspstrip(u8strip(read(S(B(dirBase)+B(fname)))))+B("_")+B(lang)
except IOError: return 0,0 # probably trying to synthcache_lookup a file with variants without first choosing a variant (e.g. in anticipation() to check for sporadic cache entries in old words) - just ignore this
text = textof(fname)
useSporadic = -1 # undecided (no point accumulating counters for potentially-unbounded input)
......@@ -137,10 +147,10 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
if synthCache:
for init in "_","":
for ext in "wav","mp3":
k=init+text.lower()+"_"+lang+extsep+ext
s=synthCache_transtbl.get(k,k)
if s in synthCache_contents: ret=s
elif s.lower().endswith(dotwav) and s[:-len(dotwav)]+dotmp3 in synthCache_contents: ret=s[:-len(dotwav)]+dotmp3
k=B(init)+text.lower()+B("_"+lang+extsep+ext)
s=B(synthCache_transtbl.get(k,k))
if checkIn(s,synthCache_contents): ret=s
elif s.lower().endswith(B(dotwav)) and checkIn(s[:-len(dotwav)]+B(dotmp3),synthCache_contents): ret=s[:-len(dotwav)]+B(dotmp3)
else: ret=0
if ret:
if justQueryCache==1: ret=(k,ret)
......@@ -150,8 +160,8 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
elif tryHarder: tryHarder=ret
else: return ret
if justQueryCache==1: return 0,0
if lang not in synth_partials_voices: l,translit=None,None # don't bother trying to transliterate here if there aren't even any partials for that language
elif (lang,text) not in synth_partials_cache:
if not checkIn(lang,synth_partials_voices): l,translit=None,None # don't bother trying to transliterate here if there aren't even any partials for that language
elif not checkIn((lang,text),synth_partials_cache):
# See if we can transliterate the text first.
synth,translit = get_synth_if_possible(lang,0,to_transliterate=True),None
if espeak_language_aliases.get(lang,lang) in ["zhy","zh-yue"]:
......@@ -164,17 +174,17 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
if lang=="zh": t2=sort_out_pinyin_3rd_tones(pinyin_uColon_to_V(t2)) # need to do this BEFORE stripPuncEtc, for correct sandhi blocking
phraseList = stripPuncEtc(t2.lower())
l = [synth_from_partials(phrase,lang) for phrase in phraseList] # TODO do we really want to be able to pick new voices at every phrase? if not, would have to pass the pause points into synth_from_partials itself
if None in l: # at least one of the partials-phrases failed
if checkIn(None,l): # at least one of the partials-phrases failed
global scl_disable_recursion
if len(t2)<100 or not filter(lambda x:x,l) or scl_disable_recursion: l=None # don't mix partials and synth for different parts of a short phrase, it's too confusing (TODO make the 100 configurable?)
elif type(get_synth_if_possible(lang,0))==EkhoSynth: l=None # some faulty versions of Ekho are more likely to segfault if called on fragments (e.g. if the fragment ends with some English), so don't do this with Ekho (unless can confirm it's at least ekho_4.5-2ubuntu10.04 .. not all versions of ekho can report their version no.)
else: # longer text and SOME can be synth'd from partials: go through it more carefully
t2=fix_compatibility(ensure_unicode(text2.replace(chr(0),"")).replace(u"\u3002",".").replace(u"\u3001",",")).encode('utf-8')
for t in ".!?:;,": t2=t2.replace(t,t+chr(0))
for t in ".!?:;,": t2=t2.replace(B(t),B(t)+chr(0))
l=[]
scl_disable_recursion = 1
for phrase in filter(lambda x:x,t2.split(chr(0))):
ll=synthcache_lookup("!synth:"+phrase+"_"+lang,dirBase,0,0,lang)
ll=synthcache_lookup(B("!synth:")+phrase+B("_"+lang),dirBase,0,0,lang)
if type(ll)==type([]): l += ll
else: l.append(synth_event(lang,phrase,0))
scl_disable_recursion = 0
......@@ -189,7 +199,7 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
if l: return l
if tryHarder and not tryHarder==True: return tryHarder
if printErrors and synthCache and not (app and winsound):
r = repr(text.lower()+"_"+lang)
r = repr(text.lower()+B("_"+lang))
if len(r)>100: r=r[:100]+"..."
global NICcount
try: NICcount += 1
......@@ -206,9 +216,10 @@ def can_be_synthesized(fname,dirBase=None,lang=None):
else: return get_synth_if_possible(lang) # and this time print the warning
def stripPuncEtc(text):
# For sending text to synth_from_partials. Removes spaces and punctuation from text, and returns a list of the text split into phrases.
for t in " -_'\"()[]": text=text.replace(t,"")
for t in ".!?:;": text=text.replace(t,",")
return filter(lambda x:x,text.split(","))
text = B(text)
for t in " -_'\"()[]": text=text.replace(B(t),B(""))
for t in ".!?:;": text=text.replace(B(t),B(","))
return filter(lambda x:x,text.split(B(",")))
for zipToCheck in ["yali-voice","yali-lower","cameron-voice"]:
if riscos_sound:
......@@ -216,7 +227,10 @@ for zipToCheck in ["yali-voice","yali-lower","cameron-voice"]:
elif not winsound: # ok if mingw32, appuifw etc (unzip_and_delete will warn)
for d in [os.getcwd()+cwd_addSep,".."+os.sep,samplesDirectory+os.sep]:
f=d+zipToCheck+".exe"
if fileExists(f): unzip_and_delete(f,ignore_fail=1) # ignore the error exit status from unzip, which will be because of extra bytes at the beginning
if fileExists(f):
unzip_and_delete(f,ignore_fail=1) # ignore the error exit status from unzip, which will be because of extra bytes at the beginning
try: os.unlink("setup.bat")
except: pass
# Filename / Unicode translation - need some safety across filesystems. synthCache(+utils) could be done this way also rather than having TRANS.TBL (however I'm not sure it would save that much code)
non_normal_filenames = {} ; using_unicode_filenames=0
......@@ -228,7 +242,7 @@ def filename2unicode(f):
def u8_or_raw(s):
try: return unicode(s,"utf-8")
except UnicodeDecodeError: return unicode(s,"latin1") # (actually should try the local codepage on Windows for correct display, but at least this stops a crash)
if f.find("_u")>-1 or f.find("_U")>-1:
if f.find("_u")>=0 or f.find("_U")>=0:
try: return unicode(f.replace("_u","\\u").replace("_U","\\u"),"unicode_escape")
except UnicodeDecodeError: # oops, need to be more careful
ret = []
......@@ -249,7 +263,7 @@ def filename2unicode(f):
return u
def unicode2filename(u):
if using_unicode_filenames: return u
if u in non_normal_filenames: return non_normal_filenames[u]
if checkIn(u,non_normal_filenames): return non_normal_filenames[u]
f=u.encode("unicode_escape").replace("\\u","_u")
for unsafe_char in "?+*<=": f=f.replace(unsafe_char,"_u%04x" % ord(unsafe_char))
return f
......@@ -264,7 +278,7 @@ if partialsDirectory and isDirectory(partialsDirectory):
try:
ela = espeak_language_aliases
format,values = pickle.Unpickler(open(partials_cache_file,"rb")).load()
if format==partialsCacheFormat: exec format+"=values"
if format==partialsCacheFormat: exec (format+"=values")
if not (ela==espeak_language_aliases and dirsToStat[0][0]==partialsDirectory): espeak_language_aliases,dirsToStat=ela,[]
del ela,format,values
except MemoryError: raise # has been known on winCEsound when we're a library module (so previous memory check didn't happen)
......@@ -278,7 +292,7 @@ if partialsDirectory and isDirectory(partialsDirectory):
langs = os.listdir(partialsDirectory)
dirsToStat.append((partialsDirectory,os.stat(partialsDirectory)))
audioDataPartials = {} ; synth_partials_voices = {}
partials_raw_mode = "header"+dotwav in langs
partials_raw_mode = checkIn("header"+dotwav,langs)
for l in langs:
try: voices = os.listdir(partialsDirectory+os.sep+l)
except: voices = []
......@@ -287,7 +301,7 @@ if partialsDirectory and isDirectory(partialsDirectory):
for v in voices:
if "-" in v and v[:v.index("-")] in voices:
suffix=v[v.index("-"):]
if not suffix in guiVoiceOptions: guiVoiceOptions.append(suffix)
if not checkIn(suffix,guiVoiceOptions): guiVoiceOptions.append(suffix)
start,mid,end = [],[],[] ; flags=0
try: files = os.listdir(partialsDirectory+os.sep+l+os.sep+v)
except: files = []
......@@ -307,10 +321,11 @@ if partialsDirectory and isDirectory(partialsDirectory):
while True:
fftell = ff.tell()
char = ff.read(1)
if not "0"<=char<="9": break
size,fname = (char+ff.readline(256)).strip(wsp).split(None,1)
if not B("0")<=char<=B("9"): break
size,fname = bwspstrip(char+ff.readline(256)).split(None,1)
try: size=int(size)
except: break # binary just happened to start with "0"-"9"
fname = S(fname)
addFile(fname)
amend.append(l+os.sep+v+os.sep+fname)
audioDataPartials[l+os.sep+v+os.sep+fname] = (f,offset,size)
......@@ -320,41 +335,41 @@ if partialsDirectory and isDirectory(partialsDirectory):
if partials_raw_mode:
if not f.endswith(extsep+"raw"): return
elif not f.endswith(dotwav) or f.endswith(dotmp3): return
if f.find("-s")>-1 or f.find("-i")>-1: start.append(f) # 'start' or 'initial'
elif not "-" in f or f.find('-m')>-1: mid.append(f)
elif f.find('-e')>-1 or f.find('-f')>-1: end.append(f) # 'end' or 'finish'
if f.find("-s")>=0 or f.find("-i")>=0: start.append(f) # 'start' or 'initial'
elif not "-" in f or f.find('-m')>=0: mid.append(f)
elif f.find('-e')>=0 or f.find('-f')>=0: end.append(f) # 'end' or 'finish'
for f in files: addFile(f)
def byReverseLength(a,b): return len(b)-len(a)
start.sort(byReverseLength) ; mid.sort(byReverseLength) ; end.sort(byReverseLength) # important if there are some files covering multiple syllables (and do it to start,mid,end not to files initially, so as to catch files within audiodata.dat also)
sort(start,byReverseLength) ; sort(mid,byReverseLength) ; sort(end,byReverseLength) # important if there are some files covering multiple syllables (and do it to start,mid,end not to files initially, so as to catch files within audiodata.dat also)
def toDict(l): # make the list of filenames into a dict of short-key -> [(long-key, filename) list]. short-key is the shortest possible key.
if not l: return {}
l2 = [] ; kLen = len(l[0])
for i in l:
if "-" in i: key=i[:i.index("-")]
else: key=i[:i.rindex(extsep)]
if key.find("_u")>-1 or key.find("_U")>-1: # a unicode partial with a portable filename?
if key.find("_u")>=0 or key.find("_U")>=0: # a unicode partial with a portable filename?
key = filename2unicode(key).encode('utf-8')
l2.append((key,i))
kLen=min(kLen,len(key))
l = {}
for k,i in l2:
if not k[:kLen] in l: l[k[:kLen]]=[]
if not checkIn(k[:kLen],l): l[k[:kLen]]=[]
l[k[:kLen]].append((k,i))
return l
thisLangVoices.append((v,toDict(start),toDict(mid),toDict(end),flags))
synth_partials_voices[l] = thisLangVoices
if l in espeak_language_aliases: partials_language_aliases[espeak_language_aliases[l]]=l
if checkIn(l,espeak_language_aliases): partials_language_aliases[espeak_language_aliases[l]]=l
if riscos_sound or winCEsound: show_info("done\n")
if pickle:
try: pickle.Pickler(open(partials_cache_file,"wb"),-1).dump((partialsCacheFormat,eval(partialsCacheFormat)))
except IOError: pass # ignore write errors as it's only a cache
except OSError: pass
if partials_raw_mode:
(wtype,wrate,wchannels,wframes,wbits) = sndhdr.what(partialsDirectory+os.sep+"header"+dotwav)
partials_raw_0bytes = int(betweenPhrasePause*wrate)*wchannels*(wbits/8)
(wtype,wrate,wchannels,wframes,wbits) = swhat(partialsDirectory+os.sep+"header"+dotwav)
partials_raw_0bytes = int(betweenPhrasePause*wrate)*wchannels*int(wbits/8)
else: synth_partials_voices,partials_raw_mode = {},None
if "cant" in synth_partials_voices: synth_partials_voices["zhy"]=synth_partials_voices["zh-yue"]=synth_partials_voices["cant"]
if checkIn("cant",synth_partials_voices): synth_partials_voices["zhy"]=synth_partials_voices["zh-yue"]=synth_partials_voices["cant"]
def partials_langname(lang):
lang = espeak_language_aliases.get(lang,lang)
......@@ -363,29 +378,30 @@ def partials_langname(lang):
def synth_from_partials(text,lang,voice=None,isStart=1):
lang = partials_langname(lang)
text=text.strip(wsp) # so whitespace between words is ignored on the recursive call
text=bwspstrip(B(text)) # so whitespace between words is ignored on the recursive call
if lang=="zh": # hack for Mandarin - higher tone 5 after a tone 3 (and ma5 after 4 or 5 also)
lastNum = None
for i in range(len(text)):
if text[i] in "123456":
if text[i]=="5" and (lastNum=="3" or (lastNum>"3" and i>2 and text[i-2:i+1]=="ma5")): # (TODO ne5 also? but only if followed by some form of question mark, and that might have been dropped)
if text[i:i+1] in B("123456"):
if text[i:i+1]==B("5") and (lastNum==B("3") or (lastNum and lastNum>B("3") and i>2 and text[i-2:i+1]==B("ma5"))): # (TODO ne5 also? but only if followed by some form of question mark, and that might have been dropped)
# see if we got a "tone 6" (higher tone 5)
# don't worry too much if we haven't
r=synth_from_partials(text[:i]+"6"+text[i+1:],lang,voice,isStart)
r=synth_from_partials(text[:i]+B("6")+text[i+1:],lang,voice,isStart)
if r: return r
else: break
elif lastNum: break # don't look beyond 1st 2
lastNum = text[i]
lastNum = text[i:i+1]
if not voice: # try all voices for the language, see if we can find one that can say all the necessary parts
if not lang in synth_partials_voices: return None
if not checkIn(lang,synth_partials_voices): return None
needCalibrated=False
if lang=="zh": # hack for Mandarin - avoid consecutive 1st tones on non-calibrated voices
# (DON'T do 3rd tone sandhi here - that's the caller's responsibility and we don't want it done twice now there's sandhi-blocking rules)
lastNum=None
for c in text:
if c=="1" and lastNum=="1":
for i in xrange(len(text)):
c = text[i:i+1]
if c==B("1") and lastNum==B("1"):
needCalibrated=True ; break # TODO: unless this syllable is exactly the same as the last syllable (a repeated syllable is always ok to use even if uncalibrated)
if c in "123456": lastNum=c
if c in B("123456"): lastNum=c
# end of hack for Mandarin
vTry = synth_partials_voices[lang]
if voiceOption:
......@@ -399,12 +415,13 @@ def synth_from_partials(text,lang,voice=None,isStart=1):
r = synth_from_partials(text,lang,v)
if r:
if partials_raw_mode and v[-1]&2: r.insert(0,"header"+dotwav)
return map(lambda x:lang+os.sep+v[0]+os.sep+x,r)
return map(lambda x,v=v,lang=lang:lang+os.sep+v[0]+os.sep+x,r)
return None
dir, start, mid, end, flags = voice
def lookup_dic(text,dic):
text = S(text)
if dic:
for k,v in dic.get(text[:len(dic.keys()[0])],[]):
for k,v in dic.get(text[:len(list(dic.keys())[0])],[]):
if text.startswith(k): return k,v
return None,None
if not text: return [] # (shouldn't happen)
......@@ -435,12 +452,12 @@ def optimise_partial_playing(ce):
for e in ce.eventList[1:]:
if not soundFileType(e.file)==fileType: return ce # must be all the same type for this optimisation
s = None
if fileType=="mp3" and madplay_program and not macsound and not cygwin: # (don't do this on cygwin because cygwin will require changeToDirOf and that could get awkward)
if fileType=="mp3" and madplay_path and mp3Player==madplay_path and not macsound and not cygwin: # (don't do this on cygwin because cygwin will require changeToDirOf and that could get awkward)
# mp3 probably has encoding gaps etc, but we can try our best
if playProgram=="aplay": s=ShellEvent(madplay_program+' -q -A $Vol$'+''.join(map(lambda x:' "'+x.file+'"', ce.eventList))+' -o wav:-|aplay -q',True) # (set retryOnFail=True)
else: s=ShellEvent(madplay_program+' -q -A $Vol$'+''.join(map(lambda x:' "'+x.file+'"', ce.eventList)),True)
if wavPlayer=="aplay": s=ShellEvent(mp3Player+' -q -A $Vol$'+''.join(map(lambda x:' "'+x.file+'"', ce.eventList))+' -o wav:-|aplay -q',True) # (set retryOnFail=True)
else: s=ShellEvent(mp3Player+' -q -A $Vol$'+''.join(map(lambda x:' "'+x.file+'"', ce.eventList)),True)
s.VolReplace="soundVolume_dB"
elif (not fileType=="mp3") and playProgram in ["aplay","sox"]:
elif (not fileType=="mp3") and (wavPlayer in ["aplay","sox"] or wavPlayer.strip().endswith("<")):
# if they're all the same format, we can use sox concatenation (raw, with an unspecified-length wav header at start)
# (don't try to do that if different formats - the low-end hardware may not take the rate conversion)
ok=gotSox
......@@ -450,21 +467,25 @@ def optimise_partial_playing(ce):
if not simplified_header(e.file)==format:
ok=False ; break
if ok:
s=ShellEvent('set -o pipefail;('+'&&'.join(['cat "%s" | sox -t %s - -t wav - $Vol$ 2>/dev/null' % (ce.eventList[0].file,fileType)]+['cat "%s" | sox -t %s - -t raw - $Vol$'%(e.file,fileType) for e in ce.eventList[1:]])+')'+sox_ignoreLen+'|'+cond(playProgram=="aplay",'aplay -q','sox -t wav - '+sox_type+' '+oss_sound_device),True)
if wavPlayer=="aplay": wpMod="aplay -q"
elif wavPlayer.strip().endswith("<"): wpMod=wavPlayer.strip()[:-1] # nc etc
else: wpMod='sox -t wav - '+sox_type+' '+oss_sound_device
s=ShellEvent('set -o pipefail;('+'&&'.join(['cat "%s" | sox -t %s - -t wav - $Vol$ 2>/dev/null' % (S(ce.eventList[0].file),fileType)]+['cat "%s" | sox -t %s - -t raw - $Vol$'%(S(e.file),fileType) for e in ce.eventList[1:]])+')'+sox_ignoreLen+'|'+wpMod,True)
s.VolReplace="sox_effect"
elif playProgram=="aplay" and not sox_effect: s=ShellEvent('aplay -q '+''.join(map(lambda x:' "'+x.file+'"', ce.eventList)),True) # (which is not quite as good but is the next best thing) (and hope they don't then try to re-play a saved lesson with a volume adjustment)
elif wavPlayer=="aplay" and not sox_effect: s=ShellEvent('aplay -q '+''.join(map(lambda x:' "'+x.file+'"', ce.eventList)),True) # (which is not quite as good but is the next best thing) (and hope they don't then try to re-play a saved lesson with a volume adjustment)
if s:
s.length = ce.length
s.equivalent_event_list = ce.eventList
return s
else: return ce # can't figure out an optimisation in these circumstances
def simplified_header(fname):
h=sndhdr.what(fname)
# called by optimise_partial_playing(_list)
h=swhat(fname)
# ignore num frames i.e. h[3], just compare formats
if h: return h[:3]+h[4:]
def optimise_partial_playing_list(ceList):
# similar to above, but returns a ShellEvent for a list of ce's that are to be separated by short pauses, or None if can't do this optimisation. This is because sox on NSLU2's etc has too much latency for the short pauses.
if (soundCollector and not saveLesson) or not playProgram=="aplay" or not gotSox: return
if (soundCollector and not saveLesson) or not wavPlayer=="aplay" or not gotSox: return
format = None ; l = [] ; theLen = 0
for ce in ceList:
for e in ce.eventList:
......
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -27,7 +27,7 @@ def play(event):
timeout_time = time.time() + max(10,event.length/3) # don't loop *forever* if unable to start playing (especially if we're being used in a reminder system etc, it may be best to exit eventually)
if lessonStartTime and not soundCollector:
if hasattr(event,"max_lateness"): timeout_time = min(timeout_time, lessonStartTime + (copy_of_runner_events[0][2]+event.max_lateness))
if hasattr(event,"sequenceID") and event.sequenceID in sequenceIDs_to_cancel: timeout_time = 0
if hasattr(event,"sequenceID") and checkIn(event.sequenceID,sequenceIDs_to_cancel): timeout_time = 0
play_error = "firstTime"
while play_error and time.time()<=timeout_time: # use <= rather than < in case we have only 1sec precision
if not play_error=="firstTime":
......@@ -41,18 +41,19 @@ def play(event):
if hasattr(event,"sequenceID"): sequenceIDs_to_cancel[event.sequenceID]=True # TODO what if its last event has "endseq" attribute, do we want to decrement wordsLeft early?
if hasattr(event,"endseq"): wordsLeft[event.endseq] -= 1
del copy_of_runner_events[0]
if soundCollector: doLabel("%d%% completed" % (soundCollector.tell()*100/lessonLen))
else:
line2 = "" # report what you'd lose if you cancel now (in case you're deciding whether to answer the phone etc)
new,old=wordsLeft[True],wordsLeft[False]
if new: line2="%d new " % new
if old:
if line2: line2 += ("+ %d old " % old)
else: line2="%d old words " % old
elif new: line2 += "words "
if line2: line2=cond(app or appuifw or android,"\n",", ")+line2+"remain"
if not lessonStartTime: lessonStartTime = time.time() # the actual time of the FIRST event (don't set it before as there may be delays). (we're setting this at the END of the 1st event - the extra margin should be ok, and can help with start-of-lesson problems with slow disks.)
if finishTime and time.time() >= emergency_lessonHold_to: doLabel("%s (finish %s)%s" % (time.strftime("%H:%M",time.localtime(time.time())),time.strftime("%H:%M",time.localtime(finishTime)),line2)) # was %I:%M but don't like leading '0' in PM times. 2nd condition added because might press 'brief interrupt' while playing.
if soundCollector: return doLabel("%d%% completed" % (soundCollector.tell()*100/lessonLen))
line2 = "" # report what you'd lose if you cancel now (in case you're deciding whether to answer the phone etc), + say how many already cancelled (for diagnosing results of interruptions caused by phone events etc on those platforms)
new,old=wordsLeft[True],wordsLeft[False]
if new: line2="%d new " % new
if old:
if line2: line2 += ("+ %d old " % old)
else: line2="%d old words " % old
elif new: line2 += "words "
if line2:
line2=cond(app or appuifw or android,"\n",", ")+line2+"remain"
if cancelledFiles: line2 += "\n("+str(len(cancelledFiles))+" cancelled)"
if not lessonStartTime: lessonStartTime = time.time() # the actual time of the FIRST event (don't set it before as there may be delays). (we're setting this at the END of the 1st event - the extra margin should be ok, and can help with start-of-lesson problems with slow disks.)
if finishTime and time.time() >= emergency_lessonHold_to: doLabel("%s (finish %s)%s" % (time.strftime("%H:%M",time.localtime(time.time())),time.strftime("%H:%M",time.localtime(finishTime)),line2)) # was %I:%M but don't like leading '0' in PM times. 2nd condition added because might press 'brief interrupt' while playing.
def doLabel(labelText):
labelText = ensure_unicode(labelText)
if app: app.setLabel(labelText)
......@@ -96,77 +97,105 @@ if ask_teacherMode:
def maybe_unicode(label):
if app or appuifw or android:
try: return unicode(label,'utf-8')
except: return label # ??
except: return label # e.g. Python 3
else: return repr(label)
madplay_program = 0
if (winsound or mingw32) and fileExists("madplay.exe"): madplay_program = "madplay.exe"
madplay_path = None
if (winsound or mingw32) and fileExists("madplay.exe"): madplay_path = "madplay.exe"
elif riscos_sound:
amplay = "$.!Boot.Resources.!System.350.Modules.Audio.MP3.AMPlayer"
if fileExists(amplay): os.system(amplay) # seems it doesn't always load at boot; no-op if run again
else: amplay = 0
elif unix and hasattr(os,"popen"):
madplay_program = os.popen("PATH=$PATH:. which madplay 2>/dev/null").read().strip(wsp)
if not fileExists(cond(cygwin,madplay_program+".exe",madplay_program)): madplay_program=0 # in case of a Unix 'which' returning error on stdout
if madplay_program and not winsound and not mingw32: madplay_program='"'+madplay_program+'"' # in case there's spaces etc in the path
madplay_path = os.popen("PATH=$PATH:. which madplay 2>/dev/null").read()
try: madplay_path = wspstrip(madplay_path)
except: madplay_path = madplay_path.strip()
if not fileExists(cond(cygwin,madplay_path+".exe",madplay_path)): madplay_path="" # in case of a Unix 'which' returning error on stdout
if madplay_path and not winsound and not mingw32: madplay_path='"'+madplay_path+'"' # in case there's spaces etc in the path
if madplay_path and not mp3Player: mp3Player=madplay_path
def intor0(v):
try: return int(v)
except ValueError: return 0
def digitPrefix(v): # in case we don't have regexp library
l = []
for d in list(v):
if '0' <= d <= '9': l.append(d)
else: break
return intor0(''.join(l))
playProgram = mpg123 = "" ; sox_effect=""
sox_8bit, sox_16bit, sox_ignoreLen = "-b", "-w", ""
sox_effect=""
sox_8bit, sox_16bit, sox_ignoreLen, sox_signed = "-b", "-w", "", "-s"
# Older sox versions (e.g. the one bundled with Windows Gradint) recognise -b and -w only; sox v14+ recognises both that and -1/-2; newer versions recognise only -1/-2. We check for newer versions if unix. (TODO riscos / other?)
soundVolume_dB = math.log(soundVolume)*(-6/math.log(0.5))
if unix:
if macsound: got_afplay = got_program("afplay") # 10.5+, use in preference to the bundled qtplay which requires PowerPC or Rosetta
sox_formats=os.popen("sox --help 2>&1").read() # NOT .lower() yet
if sox_formats.lower().startswith("sox: sox v"):
soxMaj = intor0(sox_formats[10:sox_formats.index('.')])
if soxMaj>=14:
sox_8bit, sox_16bit = "-1", "-2" # see comment above
if soxMaj==14 and sox_formats[13]<'3': pass
else: sox_ignoreLen = "|sox --ignore-length -t wav - -t wav - 2>/dev/null"
if sox_formats.lower().find("wav")>-1: gotSox=1
else:
gotSox=0
if got_program("sox"): show_warning("SOX found, but it can't handle WAV files. Ubuntu users please install libsox-fmt-all.")
def sox_check():
global sox_8bit, sox_16bit, sox_ignoreLen, sox_signed, sox_formats
sox_formats=os.popen("sox --help 2>&1").read() # NOT .lower() yet
sf2 = ' '.join(sox_formats.lower().split())
if sf2.startswith("sox: sox v"):
if sf2[10]==' ': soxMaj=15 # guess (broken HomeBrew install)
else: soxMaj = intor0(sf2[10:sf2.index('.')])
else: soxMaj=0
if soxMaj>=14:
if soxMaj==14 and sf2[13]<'3': pass
else: sox_ignoreLen = "|sox --ignore-length -t wav - -t wav - 2>/dev/null"
if soxMaj==14 and sf2[13]<'4': sox_8bit, sox_16bit = "-1", "-2" # see comment above
else: sox_8bit, sox_16bit, sox_signed = "-b 8", "-b 16", "-e signed-integer" # TODO: check if 14.3 accepts these also (at least 14.4 complains -2 etc is deprecated)
return sf2.find("wav")>=0, sf2.find("mp3")>=0
gotSox,soxMp3 = sox_check()
if macsound:
if not gotSox and not os.system("mv sox-14.4.2 sox && rm sox.README"): gotSox,soxMp3 = sox_check() # see if that one works instead (NB must use os.system here: our system() has not yet been defined)
if not gotSox and got_program("sox"):
if macsound or cygwin: xtra=""
else: xtra=". Ubuntu users please install libsox-fmt-all."
show_warning("SoX found but can't handle WAV, so you won't be able to write lessons to files for later"+xtra)
else: gotSox = got_program("sox")
wavPlayer_override = not (not wavPlayer)
if winsound or mingw32:
# in winsound can use PlaySound() but better not use it for LONGER sounds - find a playProgram anyway for those (see self.length condition in play() method below)
# in winsound can use PlaySound() but better not use it for LONGER sounds - find a wavPlayer anyway for those (see self.length condition in play() method below)
# (TODO sndrec32.exe loads the whole of the file into memory before playing. but mplayer/mplay32 sometimes halts on a yes/no dialogue about settings, and Media Player can't take files on command line so needs correct file association and executable permissions. And many of the freeware command-line players have the same limitations as winsound.)
# TODO now that we (usually) have tkSnack bundled with the Windows version, can we try that also (with file=) before sndrec32?
if fileExists(os.environ.get("windir","C:\\Windows")+"\\system32\\sndrec32.exe"): playProgram = "start /min sndrec32 /play /close" # TODO could also use ShellExecute or some other utility to make it completely hidden
if not wavPlayer and fileExists(os.environ.get("windir","C:\\Windows")+"\\system32\\sndrec32.exe"): wavPlayer = "start /min sndrec32 /play /close" # TODO could also use ShellExecute or some other utility to make it completely hidden
elif unix and not macsound:
sox_type = "-t ossdsp -s "+sox_16bit # (we will check that sox can do ossdsp below) (always specify 16-bit because if we're adjusting the volume of 8-bit wav's then we could lose too many bits in the adjustment unless we first convert to 16-bit)
sox_type = "-t ossdsp "+sox_signed+" "+sox_16bit # (we will check that sox can do ossdsp below) (always specify 16-bit because if we're adjusting the volume of 8-bit wav's then we could lose too many bits in the adjustment unless we first convert to 16-bit)
if not soundVolume==1: sox_effect=" vol "+str(soundVolume)
if sox_effect and not gotSox:
show_warning("Warning: trying to adjust soundVolume when 'sox' is not on the system might not work")
# (need a warning here, because if using 'aplay' then sox o/p is 2>/dev/null (see below) so a missing sox won't be obvious)
if sox_formats.find("alsa")>=0 and isDirectory("/dev/snd"):
sox_type=sox_type.replace("ossdsp","alsa")
oss_sound_device = " " # older versions could take "hw:0,0" but just leave at -t alsa now?
if not oss_sound_device:
dsps_to_check = []
if sox_formats.find("ossdsp")>-1: dsps_to_check += ["/dev/sound/dsp","/dev/dsp"]
if sox_formats.find("sunau")>-1: dsps_to_check += ["/dev/audio"]
if sox_formats.find("ossdsp")>=0:
dsps_to_check += ["/dev/sound/dsp","/dev/dsp"]
if sox_formats.find("sunau")>=0: dsps_to_check += ["/dev/audio"]
for dsp in dsps_to_check:
if fileExists_stat(dsp):
oss_sound_device = dsp
if dsp=="/dev/audio": sox_type="-t sunau -s "+sox_16bit
if dsp=="/dev/audio": sox_type="-t sunau "+sox_signed+" "+sox_16bit
break
if sox_formats.find("-q")>-1: sox_type="-q "+sox_type
# Try to find playProgram (and maybe mpg123, for use if no madplay or mp3-playing playProgram)
if oss_sound_device and not cygwin and gotSox: playProgram = "sox"
elif cygwin and got_program("sndrec32"): # XP's Sound Recorder (vista's is called soundreorder.exe but won't do this) (+ don't have to worry about the >2G memory bug as not applicable to playing)
playProgram = "sndrec32 /play /close" # prefer this to esdplay due to cygwin esdplay delaying every other call and being asynchronous
if got_program("cmd"): playProgram = "cmd /c start /min "+playProgram # TODO could also use ShellExecute or some other utility to make it completely hidden
elif cygwin and oss_sound_device and got_program("play"): playProgram = "play" # this is part of sox, but it'll be the sox installed in cygwin rather than any sox.exe in gradint directory from Windows version
else:
if sox_formats.find("-q")>=0: sox_type="-q "+sox_type
if not wavPlayer:
if oss_sound_device and not cygwin and gotSox: wavPlayer = "sox"
elif cygwin and got_program("sndrec32"): # XP's Sound Recorder (vista's is called soundreorder.exe but won't do this) (+ don't have to worry about the >2G memory bug as not applicable to playing)
wavPlayer = "sndrec32 /play /close" # prefer this to esdplay due to cygwin esdplay delaying every other call and being asynchronous
if got_program("cmd"): wavPlayer = "cmd /c start /min "+wavPlayer # TODO could also use ShellExecute or some other utility to make it completely hidden
elif cygwin and oss_sound_device and got_program("play"): wavPlayer = "play" # this is part of sox, but it'll be the sox installed in cygwin rather than any sox.exe in gradint directory from Windows version
else:
otherPrograms = ["aplay","esdplay","auplay","wavp","playmus","mplayer","playwave","alsaplayer"] # alsaplayer is a last resort because the text-mode version may or may not be installed; hopefully they'll have alsa-utils installed which includes 'aplay'. (playwave has been known to clip some files)
for otherProgram in otherPrograms:
if got_program(otherProgram):
playProgram = otherProgram
wavPlayer = otherProgram
break
if not cygwin and not madplay_program:
if not cygwin and not mp3Player:
for mpg in ["mpg123","mpg321","mad123","mplayer"]:
if got_program(mpg):
mpg123 = mpg ; break
if not playProgram and not outputFile: show_warning("Warning: no known "+cond(mpg123,"non-MP3 ","")+"sound-playing command found on this system\n (checked for sox with /dev/dsp etc, also checked for play "+" ".join(otherPrograms)+")\n - expect problems with realtime lessons"+cond(mpg123," unless everything is MP3",""))
may_need_mp3_warning = ((playProgram or winsound or riscos_sound or mingw32) and not (mpg123 or gotSox or madplay_program))
mp3Player = mpg ; break
if not wavPlayer and not outputFile: show_warning("Warning: no known "+cond(mp3Player,"non-MP3 ","")+"sound-playing command found on this system\n (checked for sox with /dev/dsp etc, also checked for play "+" ".join(otherPrograms)+")\n - expect problems with realtime lessons"+cond(mp3Player," unless everything is MP3",""))
may_need_mp3_warning = ((wavPlayer or winsound or riscos_sound or mingw32) and not (mp3Player or gotSox))
def maybe_warn_mp3():
global may_need_mp3_warning
if may_need_mp3_warning:
......@@ -179,7 +208,7 @@ sox_same_endian = sox_little_endian = ""
if gotSox and unix:
# should only have to run this test if macsound (don't bother on NSLU2's etc):
# (wav is little-endian, so if it doesn't pass the string through then it interpreted the i/p as big-endian)
if macsound and os.popen('echo "This is a test" | sox -t raw -r 8000 '+sox_16bit+' -s -c 1 - -t wav - 2>/dev/null').read().find("This is a test")==-1:
if macsound and readB(os.popen('echo "This is a test" | sox -t raw -r 8000 '+sox_16bit+' '+sox_signed+' -c 1 - -t wav - 2>/dev/null')).find(B("This is a test"))==-1:
sox_little_endian = " -x"
if not big_endian: sox_same_endian = " -x"
elif big_endian: sox_little_endian = " -x"
......@@ -188,35 +217,47 @@ def changeToDirOf(file,winsound_also=0):
# used before running a non-cygwin program in the cygwin environment (due to directory differences etc)
# and (with winsound_also) before running a program on Windows without needing to quote the filename (e.g. because some versions of eSpeak won't write to a quoted wav file when called from popen). Note windows os.chdir DOES change the drive also. Use this only if filename will not contain special characters (e.g. should be able to use it for temp files).
# NB if winsound_also is set, will return file "quoted" on other systems (so can set winsound_also and not worry about whether or not it should be quoted)
file = S(file)
if winCEsound and not ' ' in file: return file # don't need to quote
elif winsound_also and not (winsound or mingw32 or cygwin): return '"'+file+'"'
elif (cygwin or ((winsound or mingw32) and winsound_also)) and os.sep in file:
os.chdir(file[:file.rfind(os.sep)])
return file[file.rfind(os.sep)+1:]
else: return file
elif riscos_sound:
if file.find("..")==-1:
c = os.getcwd()
return c[c.index('$'):]+os.sep+file
return file
def system(cmd):
# Don't call os.system for commands like sound playing, because if you do then any Control-C interrupt will go to that rather than to gradint as we want, and it will pop up a large blank console window in Windows GUI-only version
if riscos_sound or not hasattr(os,"popen"): return os.system(cmd) # no popen
if unix and ';' in cmd: cmd='/bin/bash -c "'+cmd.replace('\\','\\\\').replace('"','\\"').replace('$','\\$')+'"' # not /bin/sh if it's complex
if riscos_sound and type("")==type(u""): # work around memory issues with os.system() in RISC OS Python 3.8 (not needed if the command is a module rather than a program)
import subprocess
return subprocess.Popen(S(cmd).replace('"','').split()).wait() # must be S() not B() here (TODO: what if space in a filename? TODO: catch swi.error and say please install TaskRunner module?)
if not hasattr(os,"popen"): return os.system(cmd)
if unix and (';' in cmd or '<' in cmd): cmd='/bin/bash -c "'+cmd.replace('\\','\\\\').replace('"','\\"').replace('$','\\$')+'"' # not /bin/sh if it's complex
try: r=os.popen(cmd)
except: return os.system(cmd) # too many file descriptors open or something
r.read() ; return r.close()
signal=0
if unix:
# Unix: make sure "kill" on gradint's pid includes the players:
# Unix: make sure "kill" on gradint's pid includes the players:
try:
os.setpgrp()
import signal
def siggrp(sigNo,*args):
os.killpg(os.getpgrp(),sigNo)
os.abort() # in case still here
signal.signal(sigNo,signal.SIG_IGN)
os.killpg(os.getpgrp(),sigNo) # players etc
raise KeyboardInterrupt # clean up, rm tempfiles etc
signal.signal(signal.SIGTERM,siggrp)
else: signal=0
except: pass
# Event(len) gives a pause of that length
# SampleEvent extends this to actually play something:
def soundFileType(file):
if extsep in file: return file[file.rindex(extsep)+1:].lower()
file,sep = B(file),B(extsep)
if sep in file: return S(file[file.rindex(sep)+1:].lower())
else: return "wav"
def lessonIsTight(): return maxLenOfLesson <= 10*60 * min(1.8,max(1,maxNewWords/5.0)) # ?
......@@ -231,8 +272,8 @@ class SampleEvent(Event):
if not lessonIsTight() and not useExactLen: approxLen = math.ceil(self.exactLen) # (if <=10min in lesson, don't round up to next second because we want a tighter fit)
Event.__init__(self,approxLen)
def __repr__(self):
if use_unicode_filenames: return self.file.encode('utf-8')
else: return self.file
if use_unicode_filenames: return self.file.encode('utf-8') # winCEsound, will be Python 2
else: return S(self.file)
def __del__(self):
if hasattr(self,"isTemp"):
import time,os # in case gc'd
......@@ -240,7 +281,9 @@ class SampleEvent(Event):
try: return os.unlink(self.file)
except: time.sleep(0.2) # may have taken extra time for the player to load
if not fileExists_stat(self.file): break # unlink suceeded and still threw exception ??
def makesSenseToLog(self): return not self.file.startswith(promptsDirectory) # (NB "not prompts" doesn't necessarily mean it'll be a sample - may be a customised additional comment)
def makesSenseToLog(self):
if hasattr(self,"is_prompt"): return not self.is_prompt # e.g. prompt from synth-cache
return not B(self.file).startswith(B(promptsDirectory)) # (NB "not prompts" doesn't necessarily mean it'll be a sample - may be a customised additional comment)
def play(self): # returns a non-{False,0,None} value on error
if paranoid_file_management:
if not hasattr(self,"isTemp"): open(self.file) # ensure ready for reading
......@@ -248,7 +291,7 @@ class SampleEvent(Event):
if soundCollector: soundCollector.addFile(self.file,self.exactLen)
elif appuifw:
fname = self.file
if not fname[1]==":": fname=os.getcwd()+cwd_addSep+fname # must be full drive:\path
if not B(fname[1:2])==B(":"): fname=B(os.getcwd()+cwd_addSep)+B(fname) # must be full drive:\path
sound = audio.Sound.open(ensure_unicode(fname))
sound.play()
try: time.sleep(self.length) # TODO or exactLen?
......@@ -257,56 +300,61 @@ class SampleEvent(Event):
return
elif android:
fname = self.file
if not fname[0]=='/': fname=os.getcwd()+'/'+fname
android.mediaPlay("file://"+fname)
if not B(fname[0])==B('/'): fname=B(os.getcwd()+'/')+fname
android.mediaPlay("file://"+S(fname))
return
elif fileType=="mp3" and madplay_program and not macsound and not playProgram=="aplay":
elif fileType=="mp3" and madplay_path and mp3Player==madplay_path and not macsound and not wavPlayer=="aplay":
oldcwd = os.getcwd()
play_error = system(madplay_program+' -q -A '+str(soundVolume_dB)+' "'+changeToDirOf(self.file)+'"') # using changeToDirOf because on Cygwin it might be a non-cygwin madplay.exe that someone's put in the PATH. And keeping the full path to madplay.exe because the PATH may contain relative directories.
play_error = system(mp3Player+' -q -A '+str(soundVolume_dB)+' "'+changeToDirOf(self.file)+'"') # using changeToDirOf because on Cygwin it might be a non-cygwin madplay.exe that someone's put in the PATH. And keeping the full path to madplay.exe because the PATH may contain relative directories.
os.chdir(oldcwd)
return play_error
elif winCEsound and fileType=="mp3":
# we can handle MP3 on WinCE by opening in Media Player. Too bad it ignores requests to run minimized.
fname = self.file
if not fname[0]=="\\": fname=os.getcwd()+cwd_addSep+fname # must be full path
r=not ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=u""+fname)))
if not B(fname[0])==B("\\"): fname=os.getcwd()+cwd_addSep+fname # must be full path
r=not ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=ensure_unicode(fname))))
time.sleep(self.length) # exactLen may not be enough
elif (winsound and not (self.length>10 and playProgram)) or winCEsound: # (don't use winsound for long files if another player is available - it has been known to stop prematurely)
elif (winsound and not (self.length>10 and wavPlayer)) or winCEsound: # (don't use winsound for long files if another player is available - it has been known to stop prematurely)
if fileType=="mp3": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file
try:
if winsound: winsound.PlaySound(file,winsound.SND_FILENAME)
else: # winCEsound
fname = self.file
if not fname[0]=="\\": fname=os.getcwd()+cwd_addSep+fname # must be full path
ctypes.cdll.coredll.sndPlaySoundW(u""+fname,1) # 0=sync 1=async
if not B(fname[0])==B("\\"): fname=os.getcwd()+cwd_addSep+fname # must be full path
ctypes.cdll.coredll.sndPlaySoundW(ensure_unicode(fname),1) # 0=sync 1=async
time.sleep(self.exactLen) # if async. Async seems to be better at avoiding crashes on some handhelds.
except RuntimeError: return 1
elif macsound:
if got_afplay: return system("afplay \"%s\"" % (self.file,))
else:
try: unicode(self.file,"ascii")
except UnicodeDecodeError: # qtplay can't always handle non-ASCII
t=os.tempnam()+self.file[self.file.rindex(extsep):]
open(t,"w").write(open(self.file).read())
ret=system("qtplay \"%s\"" % (t,))
os.remove(t)
return ret
return system("qtplay \"%s\"" % (self.file,))
if got_afplay: player="afplay"
else: player="qtplay"
try: unicode(self.file,"ascii")
except UnicodeDecodeError: # Mac command line can't always handle non-ASCII
t=os.tempnam()+self.file[self.file.rindex(extsep):]
write(t,open(self.file).read())
ret=system(player+" \"%s\"" % (t,))
os.remove(t)
return ret
return system(player+" \"%s\"" % (S(self.file),))
elif riscos_sound:
if fileType=="mp3": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file) # (TODO find a RISC OS program that can play the MP3s directly?)
if fileType=="mp3":
if amplay: return os.system("AMPlay \"%s\"" % (S(self.file),)) # module call, so no need for subprocess
file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file
system("PlayIt_Play \"%s\"" % (file,))
elif playProgram.find('sndrec32')>-1:
global playit_loaded
try: playit_loaded
except: playit_loaded=not os.system("Run PlayIt:Ensure -e 1.68") # relies on PlayIt$Path having been set by !PlayIt.!Boot when seen by filer
os.system("PlayIt_Play \"%s\"" % (S(file),)) # module call, so no need for subprocess; TODO: not ARMv7 compatible apparently (crashes on Pi400, sox does also, AMPlay can't play wav), saying "use mp3" in index.html for now
elif wavPlayer.find('sndrec32')>=0:
if fileType=="mp3": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file
oldDir = os.getcwd()
t=time.time()
os.system(playProgram+' "'+changeToDirOf(file)+'"') # don't need to call our version of system() here
if playProgram.find("start")>-1: time.sleep(max(0,self.length-(time.time()-t))) # better do this - don't want events overtaking each other if there are delays. exactLen not always enough. (but do subtract the time already taken, in case command extensions have been disabled and "start" is synchronous.)
os.system(wavPlayer+' "'+changeToDirOf(file)+'"') # don't need to call our version of system() here
if wavPlayer.find("start")>=0: time.sleep(max(0,self.length-(time.time()-t))) # better do this - don't want events overtaking each other if there are delays. exactLen not always enough. (but do subtract the time already taken, in case command extensions have been disabled and "start" is synchronous.)
os.chdir(oldDir)
elif fileType=="mp3" and mpg123 and not sox_effect and not (playProgram=="aplay" and madplay_program): return system(mpg123+' "'+self.file+'"')
elif playProgram=="sox":
elif fileType=="mp3" and mp3Player and not sox_effect and not (wavPlayer=="aplay" and mp3Player==madplay_path): return system(mp3Player+' "'+S(self.file)+'"')
elif wavPlayer=="sox" and (soxMp3 or not fileType=="mp3"):
# To make it more difficult:
# sox v12.x (c. 2001) - bug when filenames contain 2 spaces together, and needs input from re-direction in this case
# sox 14.0 on Cygwin - bug when input is from redirection, unless using cat | ..
......@@ -315,7 +363,7 @@ class SampleEvent(Event):
# riscos can't do re-direction (so hope not using a buggy sox) (but again don't have to worry about this if playing because will use PlayIt)
# + on some setups (e.g. Linux 2.6 ALSA with OSS emulation), it can fail without returning an error code if the DSP is busy, which it might be if (for example) the previous event is done by festival and is taking slightly longer than estimated
t = time.time()
play_error = system('cat "%s" | sox -t %s - %s %s%s >/dev/null' % (self.file,fileType,sox_type,oss_sound_device,sox_effect))
play_error = system('cat "%s" | sox -t %s - %s %s%s >/dev/null' % (S(self.file),fileType,sox_type,oss_sound_device,sox_effect))
if play_error: return play_error
else:
# no error, but did it take long enough?
......@@ -324,18 +372,20 @@ class SampleEvent(Event):
if timeDiff==0 and self.exactLen < 1.5: return 0 # (we'll let that one off for systems that have limited clock precision)
if not app: show_info("play didn't take long enough - maybe ") # .. problem playing sound
return 1
elif playProgram=="aplay" and ((not fileType=="mp3") or madplay_program or gotSox):
if madplay_program and fileType=="mp3": return system(madplay_program+' -q -A '+str(soundVolume_dB)+' "'+self.file+'" -o wav:-|aplay -q') # changeToDirOf() not needed because this won't be cygwin (hopefully)
elif gotSox and (sox_effect or fileType=="mp3"): return system('cat "'+self.file+'" | sox -t '+fileType+' - -t wav '+sox_16bit+' - '+sox_effect+' 2>/dev/null|aplay -q') # (make sure o/p is 16-bit even if i/p is 8-bit, because if sox_effect says "vol 0.1" or something then applying that to 8-bit would lose too many bits)
elif wavPlayer=="aplay" and ((not fileType=="mp3") or madplay_path or gotSox):
if madplay_path and fileType=="mp3": return system(madplay_path+' -q -A '+str(soundVolume_dB)+' "'+S(self.file)+'" -o wav:-|aplay -q') # changeToDirOf() not needed because this won't be cygwin (hopefully)
elif gotSox and (sox_effect or fileType=="mp3"): return system('cat "'+S(self.file)+'" | sox -t '+fileType+' - -t wav '+sox_16bit+' - '+sox_effect+' 2>/dev/null|aplay -q') # (make sure o/p is 16-bit even if i/p is 8-bit, because if sox_effect says "vol 0.1" or something then applying that to 8-bit would lose too many bits)
# (2>/dev/null to suppress sox "can't seek to fix wav header" problems, but don't pick 'au' as the type because sox wav->au conversion can take too long on NSLU2 (probably involves rate conversion))
else: return system('aplay -q "'+self.file+'"')
else: return system('aplay -q "'+S(self.file)+'"')
# May also be able to support alsa directly with sox (aplay not needed), if " alsa" is in sox -h's output and there is /dev/snd/pcmCxDxp (e.g. /dev/snd/pcmC0D0p), but sometimes it doesn't work, so best stick with aplay
# TODO: auplay can take -volume (int 0-100) and stdin; check esdplay capabilities also
elif fileType=="mp3" and mpg123: return system(mpg123+' "'+self.file+'"')
elif playProgram:
if fileType=="mp3" and not playProgram=="mplayer": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file
return system(playProgram+' "'+file+'"')
elif fileType=="mp3" and mp3Player and not sox_effect: return system(mp3Player+' "'+S(self.file)+'"')
elif wavPlayer:
if fileType=="mp3" and not wavPlayer=="mplayer": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=S(self.file)
if sox_effect and wavPlayer.strip().endswith("<"): return system('sox "%s" -t wav - %s | %s' % (file,sox_effect,wavPlayer.strip()[:-1]))
return system(wavPlayer+' "'+file+'"')
elif fileType=="mp3" and mp3Player: return system(mp3Player+' "'+S(self.file)+'"') # ignore sox_effect
else: show_warning("Don't know how to play \""+self.file+'" on this system')
br_tab=[(0 , 0 , 0 , 0 , 0),
......@@ -359,22 +409,24 @@ def rough_guess_mp3_length(fname):
maybe_warn_mp3() # in case there's no mp3 player
# (NB this is only a rough guess because it doesn't support VBR
# and doesn't even check all sync bits. It should be fairly quick though.)
o = open(fname) ; i = -1
while i==-1:
o = open(fname,"rb") ; i = -1
while True:
head=o.read(512)
if len(head)==0: raise IndexError # read the whole file and not found a \xFF byte??
i=head.find('\xFF')
if i+2 < len(head): head += o.read(3)
o.close()
b=ord(head[i+1])
i=head.find(LB('\xFF'))
if i==-1: continue
if i+2 < len(head): head += o.read(3)
o.seek(o.tell()-len(head)+i+2) ; b=ord(head[i+1:i+2])
if b >= 0xE0: break # valid frame header starts w. 11 1-bits (not just 8: some files with embedded images could throw that off)
s = o.tell() ; o.close()
layer = 4-((b&6)>>1)
if b&24 == 24: # bits are 11 - MPEG version is 1
column = layer-1 # MPEG 1 layer 1, 2 or 3
elif layer==1: column = 3 # MPEG 2+ layer 1
else: column = 4 # MPEG 2+ layer 2+
bitrate = br_tab[ord(head[i+2])>>4][column]
bitrate = br_tab[ord(head[i+2:i+3])>>4][column]
if bitrate==0: bitrate=48 # reasonable guess for speech
return filelen(fname)*8.0/(bitrate*1000)
return (filelen(fname)-s)*8.0/(bitrate*1000)
except IndexError: raise Exception("Invalid MP3 header in file "+repr(fname))
def filelen(fname):
......@@ -383,15 +435,30 @@ def filelen(fname):
return fileLen
def lengthOfSound(file):
if file.lower().endswith(dotmp3): return rough_guess_mp3_length(file)
if B(file).lower().endswith(B(dotmp3)): return rough_guess_mp3_length(file)
else: return pcmlen(file)
if type("")==type(u""): # Python 3
import wave
def swhat(file):
if file.lower().endswith(os.extsep+"wav"):
o = wave.open(file,'rb')
return "wav",o.getframerate(),o.getnchannels(),o.getnframes(),8*o.getsampwidth()
else: # fallback non-WAV
import sndhdr # before Python 3.13
return sndhdr.what(file)
else: # Python 2
import sndhdr
swhat = sndhdr.what
def pcmlen(file):
header = sndhdr.what(file)
if not header: raise IOError("Problem opening file '%s'" % (file,))
header = swhat(file)
(wtype,wrate,wchannels,wframes,wbits) = header
divisor = wrate*wchannels*wbits/8 # do NOT optimise with (wbits>>3), because wbits could be 4
if not divisor: raise IOError("Cannot parse sample format of '%s'" % (file,))
if android:
if wrate==6144: # might be a .3gp from android_recordFile
d = open(file).read()
if 'mdat' in d: return (len(d)-d.index('mdat'))/1500.0 # this assumes the bitrate is roughly the same as in my tests, TODO figure it out properly
divisor = wrate*wchannels*int(wbits/8) # do NOT optimise with (wbits>>3), because wbits could be 4
if not divisor: raise IOError("Cannot parse sample format of '%s': %s" % (file,repr(header)))
return (filelen(file) - 44.0) / divisor # 44 is a typical header length, and .0 to convert to floating-point
##########################################################
......@@ -400,30 +467,32 @@ class SoundCollector(object):
def __init__(self):
self.rate = 44100 # so ok for oggenc etc
if out_type=="raw" and write_to_stdout: self.o=sys.stdout
elif out_type=="ogg": self.o=os.popen("oggenc -o \"%s\" -r -C 1 -q 0 -" % (cond(write_to_stdout,"-",outputFile),),"wb") # oggenc assumes little-endian, which is what we're going to give it
elif out_type=="ogg": self.o=os.popen(oggenc()+" -o \"%s\" -r -C 1 -q 0 -" % (cond(write_to_stdout,"-",outputFile),),popenWB) # oggenc assumes little-endian, which is what we're going to give it
elif out_type=="aac":
if got_program("neroAacEnc"): self.o=os.popen("sox %s - -t wav - | neroAacEnc -br 32000 -if - -of \"%s\"" % (self.soxParams(),cond(write_to_stdout,"-",outputFile)),"wb") # (TODO optionally use -2pass, on a physical input file like the afconvert code)
else: self.o=os.popen("faac -b 32 -P%s -C 1 -o \"%s\" -" % (cond(big_endian,""," -X"),cond(write_to_stdout,"-",outputFile)),"wb") # (TODO check that faac on big-endian needs the -X removed when we're giving it little-endian. It SHOULD if the compile is endian-dependent.)
elif out_type=="mp3": self.o=os.popen("lame -r%s%s -m m --vbr-new -V 9 - \"%s\"" % (lame_endian_parameters(),lame_quiet(),cond(write_to_stdout,"-",outputFile)),"wb") # (TODO check that old versions of lame won't complain about the --vbr-new switch. And some very old hardware players may insist on MPEG-1 rather than MPEG-2, which would need different parameters)
if got_program("neroAacEnc"): self.o=os.popen("sox %s - -t wav - | neroAacEnc -br 32000 -if - -of \"%s\"" % (self.soxParams(),cond(write_to_stdout,"-",outputFile)),popenWB) # (TODO optionally use -2pass, on a physical input file like the afconvert code)
else: self.o=os.popen("faac -b 32 -P%s -C 1 -o \"%s\" -" % (cond(big_endian,""," -X"),cond(write_to_stdout,"-",outputFile)),popenWB) # (TODO check that faac on big-endian needs the -X removed when we're giving it little-endian. It SHOULD if the compile is endian-dependent.)
elif out_type=="mp3": self.o=os.popen("lame -r%s%s -m m --vbr-new -V 9 - \"%s\"" % (lame_endian_parameters(),lame_quiet(),cond(write_to_stdout,"-",outputFile)),popenWB) # (TODO check that old versions of lame won't complain about the --vbr-new switch. And some very old hardware players may insist on MPEG-1 rather than MPEG-2, which would need different parameters)
# Older versions of gradint used BladeEnc, with these settings: "BladeEnc -br 48 -mono -rawmono STDIN \"%s\"", but lame gives much smaller files (e.g. 3.1M instead of 11M) - it handles the silences more efficiently for a start).
# Size regression would probably also happen if we used shineenc or comprec: fixed-point, so likely faster than floating-point lame on low-end ARM etc, but won't reduce bitrate for silences.
# Anyway, on a Raspberry 1 B+, oggenc runs ~5x faster than lame (probably due to the type of floating-point in use), and speexenc 10-15x faster than lame, and both produce smaller files. So if MP3-compatibility is not required (and you're not concerned about further quality loss of any samples already stored in mp3 format) then ogg or spx would be a better choice.
# Typical file sizes for a 30-minute lesson: OGG 2.7M, neroAacEnc 3.0M at 32000 (you might be able to put up with 1.8M at 18000 or 2.2M at 24000), MP3 3.1M, MP2 3.4M, faac 3.7M, WAV 152M
# TODO try AAC+? aacplusenc wavfile(or -) aacfile kbits, 10,12,14,18,20,24,32,40 (or 48 for stereo), but will need a player to test it
# (mp2 could possibly be made a bit smaller by decreasing the -5, but don't make it as low as -10)
elif out_type=="spx":
self.rate = 32000 # could also use 16000 and -w, or even 8000, but those are not so good for language learning
self.o=os.popen("speexenc -u --vbr --dtx - "+cond(write_to_stdout,"-",outputFile),"wb") # and write 16-bit little-endian mono
self.o=os.popen("speexenc -u --vbr --dtx - "+cond(write_to_stdout,"-",outputFile),popenWB) # and write 16-bit little-endian mono
elif out_type=="mp2":
self.rate = 22050
self.o=os.popen("toolame %s -s %f -v -5 -p 4 -m m - \"%s\"" % (cond(big_endian,"-x",""),self.rate/1000.0,cond(write_to_stdout,"-",outputFile)),"wb") # TODO check that toolame compiled on big-endian architectures really needs -x to accept little-endian input
self.o=os.popen("toolame %s -s %f -v -5 -p 4 -m m - \"%s\"" % (cond(big_endian,"-x",""),self.rate/1000.0,cond(write_to_stdout,"-",outputFile)),popenWB) # TODO check that toolame compiled on big-endian architectures really needs -x to accept little-endian input
elif not out_type=="raw":
if out_type=="wav": self.rate=22050 # try not to take TOO much disk space
self.o=os.popen("sox %s - -t %s \"%s\"" % (self.soxParams(),out_type,cond(write_to_stdout,"-",outputFile)),"wb")
self.o=os.popen("sox %s - -t %s \"%s\"" % (self.soxParams(),out_type,cond(write_to_stdout,"-",outputFile)),popenWB)
else: self.o = open(outputFile,"wb")
self.theLen = 0
self.silences = []
def soxParams(self):
# Have 16-bit mono, signed, little-endian
return ("-t raw "+sox_16bit+" -s -r %d -c 1" % (self.rate,))+sox_little_endian
return ("-t raw "+sox_16bit+" "+sox_signed+" -r %d -c 1" % (self.rate,))+sox_little_endian
def tell(self):
# How many seconds have we had? (2 because 16-bit)
return 1.0*self.theLen/self.rate/2
......@@ -434,16 +503,16 @@ class SoundCollector(object):
sampleNo = int(0.5+seconds*self.rate)
if not sampleNo: sampleNo=1 # so don't lock on rounding errors
byteNo = sampleNo*2 # since 16-bit
outfile_writeBytes(self.o,"\0"*byteNo)
outfile_writeBytes(self.o,chr(0)*byteNo)
self.theLen += byteNo
def addFile(self,file,length):
def addFile(self,file,length): # length ignored in this version
fileType=soundFileType(file)
if fileType=="mp3": file,fileType = theMp3FileCache.decode_mp3_to_tmpfile(file),"wav" # in case the system needs madplay rather than sox
if fileType=="mp3": file,fileType = theMp3FileCache.decode_mp3_to_tmpfile(file),"wav" # in case the system needs madplay etc rather than sox
if riscos_sound:
os.system("sox -t %s \"%s\" %s tmp0" % (fileType,file,self.soxParams()))
system("sox -t %s \"%s\" %s tmp0" % (fileType,file,self.soxParams()))
handle=open("tmp0","rb")
elif winsound or mingw32: handle = os.popen(("sox -t %s - %s - < \"%s\"" % (fileType,self.soxParams(),file)),"rb")
else: handle = os.popen(("cat \"%s\" | sox -t %s - %s -" % (file,fileType,self.soxParams())),"rb")
elif winsound or mingw32: handle = os.popen(("sox -t %s - %s - < \"%s\"" % (fileType,self.soxParams(),file)),popenRB)
else: handle = os.popen(("cat \"%s\" | sox -t %s - %s -" % (S(file),fileType,self.soxParams())),popenRB)
self.theLen += outfile_writeFile(self.o,handle,file)
if riscos_sound:
handle.close() ; os.unlink("tmp0")
......@@ -453,9 +522,9 @@ class SoundCollector(object):
t1 = self.tell()
self.addSilence(betweenBeeps/2.0)
if riscos_sound:
os.system(beepCmd() % (self.soxParams(),"tmp0"))
system(beepCmd(self.soxParams(),"tmp0"))
data=read("tmp0") ; os.unlink("tmp0")
else: data=os.popen((beepCmd() % (self.soxParams(),"-")),"rb").read()
else: data=readB(os.popen(beepCmd(self.soxParams(),"-"),popenRB))
outfile_writeBytes(self.o,data)
self.theLen += len(data)
self.addSilence(betweenBeeps/2.0)
......@@ -474,7 +543,7 @@ class SoundCollector(object):
if not app: show_info("Lengths of silences: %s (total %s)\n" % (self.silences,ttl))
if not outputFile=="-": outfile_close(self.o)
def outfile_writeBytes(o,bytes):
try: o.write(bytes)
try: writeB(o,bytes)
except IOError: outfile_write_error()
def outfile_close(o):
try: o.close()
......@@ -482,24 +551,30 @@ def outfile_close(o):
def outfile_writeFile(o,handle,filename):
data,theLen = 1,0
while data:
data = handle.read(102400)
data = readB(handle,102400)
outfile_writeBytes(o,data)
theLen += len(data)
if not filename.startswith(partialsDirectory+os.sep): assert theLen, "No data when reading "+filename+": check for sox crash" # (but allow empty partials e.g. r5. TODO if it's from EkhoSynth it could be a buggy version of Ekho)
if not B(filename).startswith(B(partialsDirectory+os.sep)): assert theLen, "No data when reading "+S(filename)+": check for sox crash" # (but allow empty partials e.g. r5. TODO if it's from EkhoSynth it could be a buggy version of Ekho)
return theLen
def outfile_write_error(): raise IOError("Error writing to outputFile: either you are missing an encoder for "+out_type+", or the disk is full or something.")
def oggenc(): # 2016: some Windows builds are now called oggenc2
global cached_oggenc
try: return cached_oggenc
except: pass
if got_program("oggenc"): cached_oggenc = "oggenc"
else: cached_oggenc = "oggenc2"
return cached_oggenc
def lame_endian_parameters():
# The input to lame will always be little-endian regardless of which architecture we're on and what kind of sox build we're using.
# lame 3.97 has -x (swap endian) parameter, needed with little-endian i/p on little-endian architecture
# lame 3.98+ has changed the default of -x and introduced explicit --big-endian and --little-endian.
# (Note: None of this would be needed if we give lame a WAV input, as email-lesson.sh does. But lame 3.97 on Windows faults on wav inputs.)
lameVer = os.popen("lame --version").read()
if lameVer.find("version ")>-1:
if lameVer.find("version ")>=0:
lameVer = lameVer[lameVer.index("version "):].split()[1]
if lameVer and '.' in lameVer and (lameVer[0]>'3' or intor0(lameVer[2:4])>97):
if lameVer and '.' in lameVer and (intor0(lameVer[:lameVer.index('.')])>3 or digitPrefix(lameVer[lameVer.index('.')+1:])>97):
# Got 3.98+ - explicitly tell it the endianness (but check for alpha releases first - some of them don't deal with either this or the 3.97 behaviour very well)
if lameVer.find("alpha")>-1 and lameVer[0]=="3" and intor0(lameVer[2:4])==98: show_warning("Warning: You have a 3.98 alpha release of LAME.\nIf the MP3 file is white noise, try a different LAME version.")
if lameVer.find("alpha")>=0 and lameVer[0]=="3" and intor0(lameVer[2:4])==98: show_warning("Warning: You have a 3.98 alpha release of LAME.\nIf the MP3 file is white noise, try a different LAME version.")
return " --little-endian"
# otherwise fall-through to older lame behaviour:
if big_endian: return "" # TODO are we sure we don't need -x on lame 3.97 PPC as well?
......@@ -514,11 +589,15 @@ beepType = 0
beepCmds = ["sox -t nul - %s %s synth trapetz 880 trim 0 0:0.05",
"sox -t nul - %s %s synth sine 440 trim 0 0:0.05"]*3+["sox -t nul - %s %s synth trapetz 440 trim 0 0:0.05",
"sox -t nul - %s %s synth sine 440 trim 0 0:0.05"]*2+["sox -t nul - %s %s synth 220 trim 0 0:0.05"]
def beepCmd():
def beepCmd(soxParams,fname):
global beepType
r = beepCmds[beepType]
beepType += 1
if beepType==len(beepCmds): beepType=0
if unix:
# not all versions of sox support -t nul; /dev/zero is safer on Unix
r=r.replace("-t nul -","%s /dev/zero" % (soxParams,))
r = r % (soxParams,fname)
return r
# -----------------------------------------------------
......@@ -527,14 +606,14 @@ def beepCmd():
class ShSoundCollector(object):
def __init__(self):
self.file2command = {}
self.commands = ["C() { echo -n $1% completed $'\r' 1>&2;}"]
self.commands = ["C() { echo -n $1% completed $'\r' >&2;}"]
self.seconds = self.lastProgress = 0
if write_to_stdout: self.o=sys.stdout
else: self.o = open(outputFile,"wb")
start = """#!/bin/bash
if echo "$0"|grep / >/dev/null; then export S="$0"; else export S=$(which "$0"); fi
export P="-t raw %s -s -r 44100 -c 1"
tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignore endian issues because the wav header it generates below will specify the same as its natural endian-ness)
if echo "$0"|grep / >/dev/null; then S="$0"; else S=$(which "$0"); fi
P="-t raw %s %s -r 44100 -c 1"
tail -1 "$S" | bash\nexit\n""" % (sox_16bit,sox_signed) # S=script P=params for sox (ignore endian issues because the wav header it generates below will specify the same as its natural endian-ness)
outfile_writeBytes(self.o,start)
self.bytesWritten = len(start) # need to keep a count because it might be stdout
self.commands.append("sox $P - -t wav - </dev/null 2>/dev/null") # get the wav header with unspecified length
......@@ -552,7 +631,7 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor
while gap > betweenBeeps+0.05:
t1 = self.tell()
self.addSilence(betweenBeeps/2.0)
self.commands.append(beepCmd() % ("$P","-"))
self.commands.append(beepCmd("$P","-"))
self.seconds += 0.05
self.addSilence(betweenBeeps/2.0)
gap -= (self.tell()-t1)
......@@ -560,9 +639,9 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor
def addFile(self,file,length):
fileType=soundFileType(file)
self.seconds += length
if not file in self.file2command:
if not checkIn(file,self.file2command):
if fileType=="mp3": fileData,fileType = decode_mp3(file),"wav" # because remote sox may not be able to do it
elif compress_SH and unix: handle=os.popen("cat \""+file+"\" | sox -t "+fileType+" - -t "+fileType+" "+sox_8bit+" - 2>/dev/null","rb") # 8-bit if possible (but don't change sample rate, as we might not have floating point)
elif compress_SH and unix: handle=os.popen("cat \""+S(file)+"\" | sox -t "+fileType+" - -t "+fileType+" "+sox_8bit+" - 2>/dev/null",popenRB) # 8-bit if possible (but don't change sample rate, as we might not have floating point)
else: handle = open(file,"rb")
offset, length = self.bytesWritten, outfile_writeFile(self.o,handle,file)
self.bytesWritten += length
......@@ -584,7 +663,7 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor
def finished(self):
if outputFile_appendSilence: self.addSilence(outputFile_appendSilence,False)
outfile_writeBytes(self.o,"\n") # so "tail" has a start of a line
self.commands.append("C 100;echo 1>&2;exit")
self.commands.append("C 100;echo >&2;exit")
for c in self.commands: outfile_writeBytes(self.o,c+"\n")
outfile_writeBytes(self.o,"tail -%d \"$S\" | bash\n" % (len(self.commands)+1))
if not write_to_stdout:
......@@ -599,30 +678,44 @@ def dd_command(offset,length):
warned_about_sox_decode = 0
def warn_sox_decode():
global warned_about_sox_decode
if not warned_about_sox_decode and not sox_ignoreLen:
show_warning("Had to use sox to decode MP3 (as no madplay etc); some versions of sox truncate the end of MP3s") # but 14.3+ (sox_ignoreLen set) should be OK
if not warned_about_sox_decode:
r = []
if macsound: r.append("the sox bundled with Mac Gradint was not compiled with MP3 support (please install madplay or a better sox)") # (or upgrade to a version of Mac OS that has afconvert)
if not sox_ignoreLen: r.append("some versions of sox truncate the end of MP3s (please upgrade sox or install madplay/mpg123)") # sox 14.3+ (sox_ignoreLen set) should be OK
if r: r.insert(0,"Had to use sox to decode MP3")
if r: show_warning('; '.join(r))
warned_about_sox_decode = 1
def decode_mp3(file):
def decode_mp3(file): # Returns WAV data including header. TODO: this assumes it's always small enough to read the whole thing into RAM (should be true if it's 1 word though, and decode_mp3 isn't usually used unless we're making a lesson file rather than running something in justSynthesize)
file = S(file)
if riscos_sound:
warn_sox_decode()
os.system("sox -t mp3 \""+file+"\" -t wav"+cond(compress_SH," "+sox_8bit,"")+" tmp0")
warn_sox_decode() # TODO: can use madplay or AMPlay to decode if correctly installed
system("sox -t mp3 \""+file+"\" -t wav"+cond(compress_SH," "+sox_8bit,"")+" tmp0")
data=read("tmp0") ; os.unlink("tmp0")
return data
elif madplay_program or got_program("mpg123"):
elif madplay_path:
oldDir = os.getcwd()
d=readB(os.popen(madplay_path+cond(compress_SH," -R 16000 -b 8","")+" -q \""+changeToDirOf(file)+"\" -o wav:-",popenRB))
if winsound: d=d.replace(B("data\xFF"),B("data\x7F"),1) # sox bug workaround
os.chdir(oldDir) ; return d
elif got_program("mpg123"): # do NOT try to read its stdout (not only does it write 0 length, which we can fix, but some versions can also write wrong bitrate, which is harder for us to fix)
oldDir = os.getcwd()
if madplay_program: d=os.popen(madplay_program+cond(compress_SH," -R 16000 -b 8","")+" -q \""+changeToDirOf(file)+"\" -o wav:-","rb").read()
else: d=os.popen("mpg123 -q -w - \""+changeToDirOf(file)+"\"","rb").read()
os.chdir(oldDir)
# fix length (especially if it's mpg123)
wavLen = len(d)-8 ; datLen = wavLen-36 # assumes no other chunks
if datLen<0: raise IOError("decode_mp3 got bad wav") # better than ValueError for the chr() in the following line
return d[:4] + chr(wavLen&0xFF)+chr((wavLen>>8)&0xFF)+chr((wavLen>>16)&0xFF)+chr(wavLen>>24) + d[8:40] + chr(datLen&0xFF)+chr((datLen>>8)&0xFF)+chr((datLen>>16)&0xFF)+chr(datLen>>24) + d[44:]
tfil = os.tempnam()+dotwav
system("mpg123 -q -w \""+tfil+"\" \""+changeToDirOf(file)+"\"")
if compress_SH and gotSox: dat = readB(os.popen("sox \""+tfil+"\" -t wav "+sox_8bit+" - ",popenRB))
else: dat = open(tfil,"rb").read()
os.unlink(tfil) ; os.chdir(oldDir) ; return dat
elif macsound and got_program("afconvert"):
tfil = os.tempnam()+dotwav
system("afconvert -f WAVE -d I16@44100 \""+file+"\" \""+tfil+"\"")
if compress_SH and gotSox: dat = readB(os.popen("sox \""+tfil+"\" -t wav "+sox_8bit+" - ",popenRB))
else: dat = open(tfil,"rb").read()
os.unlink(tfil) ; return dat
elif unix:
if gotSox:
if soxMp3:
warn_sox_decode()
return os.popen("cat \""+file+"\" | sox -t mp3 - -t wav"+cond(compress_SH," "+sox_8bit,"")+" - ","rb").read()
return readB(os.popen("cat \""+S(file)+"\" | sox -t mp3 - -t wav"+cond(compress_SH," "+sox_8bit,"")+" - ",popenRB))
else:
show_warning("Don't know how to decode "+file+" on this system")
show_warning("Don't know how to decode "+file+" on this system. Try installing madplay or mpg123.")
return ""
else: raise Exception("decode_mp3 called on a setup that's not Unix and doesn't have MADplay. Need to implement non-cat sox redirect.")
......@@ -630,14 +723,17 @@ def decode_mp3(file):
class Mp3FileCache(object):
def __init__(self): self.fileCache = {}
def __del__(self):
import os # as it might already have been gc'd
for v in self.fileCache.values():
try: import os # as it might already have been gc'd
except: pass
try:
for v in self.fileCache.values():
try: os.remove(v)
except: pass # somebody may have removed it already
except: pass
def decode_mp3_to_tmpfile(self,file):
if not file in self.fileCache:
if not checkIn(file,self.fileCache):
self.fileCache[file] = os.tempnam()+dotwav
open(self.fileCache[file],"wb").write(decode_mp3(file))
write(self.fileCache[file],decode_mp3(file))
return self.fileCache[file]
theMp3FileCache = Mp3FileCache()
......@@ -664,7 +760,7 @@ if outputFile:
if out_type=="sh": soundCollector,sample_table_hack = ShSoundCollector(), 1
else: soundCollector = SoundCollector()
waitBeforeStart = 0
if unix and out_type in ["ogg","mp3"] and os.uname()[4].startswith("arm"): show_warning("Note: On armel, compile lame or oggenc with -fno-finite-math-only, or use lame -b 64 (or higher). See http://martinwguy.co.uk/martin/debian/no-finite-math-only")
if unix and out_type in ["ogg","mp3"] and os.uname()[4].startswith("arm"): show_warning("Note: On armel, compile lame or oggenc with -fno-finite-math-only, or use lame -b 64 (or higher)")
if not (soundCollector and out_type=="sh"): compress_SH = False # regardless of its initial setting (because it's used outside ShSoundCollector)
def collector_time(): return soundCollector.tell()
def collector_sleep(s): soundCollector.addSilence(s)
......
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -51,13 +51,13 @@ class PlayerInput(InputSource): # play to speakers while recording to various de
global paranoid_file_management
if use_unicode_filenames: fileToPlay=ensure_unicode(fileToPlay)
else:
assert not type(fileToPlay)==type(u"")
assert type(fileToPlay)==type("")
if not paranoid_file_management and filter(lambda x:ord(x)>=128,list(fileToPlay)): paranoid_file_management = True # hack to try to work around a Tkinter fault on Linux with utf-8 filenames
if paranoid_file_management: # try to ensure it's ready for reading
if filelen(fileToPlay)<1048576:
# only small - copy to temp 1st
self.fileToDel = os.tempnam()+fileToPlay[fileToPlay.rfind(extsep):]
open(self.fileToDel,"wb").write(read(fileToPlay))
write(self.fileToDel,read(fileToPlay))
fileToPlay=self.fileToDel
else: open(fileToPlay)
if fileToPlay.lower().endswith(dotwav) and filelen(fileToPlay)<1048576: self.sound=tkSnack.Sound(load=fileToPlay) # in-memory if <1M (saves problems with Windows keeping them open even after object deleted), TODO is this still needed now that .destroy() is called properly? (but might be a good idea to keep it in anyway)
......@@ -86,11 +86,12 @@ class PlayerInput(InputSource): # play to speakers while recording to various de
elapsedTime = self.elapsedTime()
if elapsedTime>=self.length-self.startSample*1.0/self.sampleRate: self.close()
else:
import thread
def stopMe(self,thread_id):
try: import thread
except ImportError: import _thread as thread
def stopMe(self,thread_id,elapsedTime):
time.sleep(max(0.5,self.length-self.startSample*1.0/self.sampleRate-elapsedTime))
self.autostop(thread_id)
thread.start_new_thread(stopMe,(self,thread_id))
thread.start_new_thread(stopMe,(self,thread_id,elapsedTime))
def elapsedTime(self):
try: t=tkSnack.audio.elapsedTime()
except: t=0.0
......@@ -132,35 +133,16 @@ class PlayerInput(InputSource): # play to speakers while recording to various de
theRecorderControls.current_recordFrom_button = theRecorderControls.old_recordFrom_button
app.todo.undoRecordFrom=True # we might not be the GUI thread
if not tkSnack:
if macsound: # might still be able to use Audio Recorder
if fileExists("AudioRecorder.zip"): unzip_and_delete("AudioRecorder.zip")
if fileExists("Audio Recorder.app/plist"): # Audio Recorder with our special preferences list
runAudioRecorderYet = 0
def MacStartRecording():
global runAudioRecorderYet
if not runAudioRecorderYet: os.system("mv ~/Library/Preferences/com.benshan.AudioRecorder31.plist ~/Library/Preferences/com.benshan.AudioRecorder31.plist-OLD 2>/dev/null ; cp Audio\\ Recorder.app/plist ~/Library/Preferences/com.benshan.AudioRecorder31.plist; open Audio\\ Recorder.app")
os.system("osascript -e 'Tell application \"Audio Recorder\" to Record'")
runAudioRecorderYet = 1
def MacStopRecording(): os.system("osascript -e 'Tell application \"Audio Recorder\" to Stop'")
MacRecordingFile = "/tmp/audiorec-output-for-gradint.wav" # specified in the plist
def quitAudioRecorder():
if runAudioRecorderYet: os.system("osascript -e 'Tell application \"Audio Recorder\" to quit' ; rm ~/Library/Preferences/com.benshan.AudioRecorder31.plist ; mv ~/Library/Preferences/com.benshan.AudioRecorder31.plist-OLD ~/Library/Preferences/com.benshan.AudioRecorder31.plist 2>/dev/null")
import atexit ; atexit.register(quitAudioRecorder)
del MicInput
class MicInput(InputSource): # Mac Audio Recorder version
def startRec(self,outFile,lastStopRecVal=None):
self.fileToWrite = outFile
MacStartRecording()
def stopRec(self):
MacStopRecording()
os.rename(MacRecordingFile,self.fileToWrite)
tkSnack = "MicOnly"
elif unix and useTK and isDirectory("/dev/snd") and got_program("arecord"): # no tkSnack, but can record via ALSA (but no point doing the tests if not useTK)
if useTK and not tkSnack:
if unix and isDirectory("/dev/snd") and got_program("arecord"): # can record via ALSA
recParams = (os.P_NOWAIT,got_program("arecord"),"arecord","-f","S16_LE","-r","22050")
elif gotSox: recParams = (os.P_NOWAIT,got_program("sox"),"sox","-d","-r","22050","-c","1")+tuple(sox_16bit.split()+sox_signed.split())
else: recParams = None
if recParams:
del MicInput
class MicInput(InputSource):
def startRec(self,outFile,lastStopRecVal=0.5):
self.pid = os.spawnl(os.P_NOWAIT,"/bin/bash","/bin/bash","-c","arecord -f S16_LE -r 22050 "+shell_escape(outFile))
self.pid = os.spawnl(*(recParams+(outFile,)))
time.sleep(lastStopRecVal) # allow process to start
def stopRec(self):
os.kill(self.pid,2) # INT
......@@ -208,7 +190,7 @@ def wavToMp3(directory):
def makeMp3Zips(baseDir,outDir,zipNo=0,direc=None):
zipSplitThreshold = 5*1048576 # to be safe (as will split when it goes OVER that)
if baseDir==outDir: return zipNo # omit
if baseDir==outDir or baseDir.endswith(extsep+"zip"): return zipNo # omit the output dir, plus any existing ZIP files
elif not direc:
for f in os.listdir(baseDir): zipNo = makeMp3Zips(baseDir,outDir,zipNo,f)
elif isDirectory(baseDir+os.sep+direc): zipNo = makeMp3Zips(baseDir+os.sep+direc,outDir,zipNo)
......@@ -230,7 +212,7 @@ def getAmplify(directory):
for f in os.listdir(directory):
factor = None
if f.endswith(dotwav) and not system("sox \""+directory+os.sep+f+"\" "+out2nul+" stat 2> \""+statfile+"\""):
for l in read(statfile).replace("\r","\n").split("\n"):
for l in S(read(statfile)).replace("\r","\n").split("\n"):
if l.startswith("Volume adjustment:"): factor=l.split()[2]
if not factor: continue
tmplist.append([float(factor),f,factor])
......@@ -260,7 +242,7 @@ def doAmplify(directory,fileList,factor):
class ButtonScrollingMixin(object):
# expects self.ourCanvas
def bindFocusIn(self,b):
b.bind('<FocusIn>',lambda *args:self.scrollIntoView(b))
b.bind('<FocusIn>',lambda e=None,self=self,b=b:self.scrollIntoView(b))
if not hasattr(app,"gotFocusInHandler"):
# (see scrollIntoView method's use of justGotFocusIn)
app.gotFocusInHandler=1
......@@ -285,7 +267,7 @@ class ButtonScrollingMixin(object):
if by+bh<=cy+ch: return # make this the last one - don't loop consuming CPU on bottom of list
elif by < cy: self.ourCanvas.yview("scroll","-1","units")
else: return # done
app.after(10,lambda *args:self.continueScrollIntoView(button))
app.after(10,lambda e=None,self=self,button=button:self.continueScrollIntoView(button))
class RecorderControls(ButtonScrollingMixin):
def __init__(self):
......@@ -323,7 +305,7 @@ class RecorderControls(ButtonScrollingMixin):
self.frame.pack_forget()
theISM.setInputSource(None)
def addButton(self,row,col,text,command,colspan=None):
if (row,col) in self.coords2buttons: self.coords2buttons[(row,col)].grid_forget()
if checkIn((row,col),self.coords2buttons): self.coords2buttons[(row,col)].grid_forget()
b = makeButton(self.grid,text=text,command=command)
self.bindFocusIn(b)
self.coords2buttons[(row,col)] = b
......@@ -336,17 +318,56 @@ class RecorderControls(ButtonScrollingMixin):
if col: self.coords2buttons[(row,col)].grid(row=row,column=col,columnspan=colspan)
else: self.coords2buttons[(row,col)].grid(row=row,column=0,columnspan=colspan,sticky="w")
def addLabel(self,row,col,utext):
if (row,col) in self.coords2buttons: self.coords2buttons[(row,col)].grid_forget()
self.coords2buttons[(row,col)] = self.makeLabel_lenLimit(utext)
self.coords2buttons[(row,col)].grid(row=row,column=col,sticky="w")
if col==0: self.coords2buttons[(row,col)].bind('<Button-1>',lambda *args:self.startRename(row,col,utext))
if checkIn((row,col),self.coords2buttons): self.coords2buttons[(row,col)].grid_forget()
rc = self.coords2buttons[(row,col)] = self.makeLabel_lenLimit(utext)
rc.grid(row=row,column=col,sticky="w")
if col==0:
rc.bind('<Button-1>',lambda e=None,self=self,row=row,col=col,utext=utext:self.startRename(row,col,utext))
if not winCEsound:
def contextMenu(e,row=row,col=col,self=self,utext=utext):
# TODO: document this context menu?
m=Tkinter.Menu(None, tearoff=0, takefocus=0)
m.add_command(label="Rename",command=lambda e=None,self=self,row=row,col=col,utext=utext:self.startRename(row,col,utext))
if self.currentDir.startswith(samplesDirectory): m.add_command(label="Add extra revision",command=lambda e=None,self=self,utext=utext:self.addRevision(utext))
m.add_command(label="Delete",command=lambda e=None,self=self,utext=utext:self.delete(utext))
m.tk_popup(e.x_root-3, e.y_root+3,entry="0")
rc.bind('<ButtonRelease-3>',contextMenu)
if macsound:
rc.bind('<Control-ButtonRelease-1>',contextMenu)
rc.bind('<ButtonRelease-2>',contextMenu)
if not winCEsound:
def delete(self,filename):
toDel = [] ; fs=filename.encode('utf-8')
for f in os.listdir(self.currentDir):
if f.startswith(S(fs)): toDel.append(f)
if not toDel: return tkMessageBox.showinfo(filename,"No files found") # shouldn't happen
if tkMessageBox.askyesno(filename,"Really delete "+" ".join(toDel)+"?"):
for d in toDel: os.remove(self.currentDir+os.sep+d)
self.undraw() ; self.draw() # TODO incremental update? (need to check really does affect just that row; careful with variants, synth, etc)
def addRevision(self,filename):
# c.f. gui_event_loop menu_response=="add" when already in vocabList
app.set_watch_cursor = 1
d = ProgressDatabase(0)
found = 0
curDir = self.currentDir[len(samplesDirectory)+len(os.sep):]
if curDir: curDir += os.sep
for item in d.data: # TODO: don't run this loop in the GUI thread!
if not item[2].startswith(curDir+filename.encode('utf-8')+"_"): continue
if not item[0]: break # not done yet
newItem0 = reviseCount(item[0])
if tkMessageBox.askyesno(filename,localise("Repeat count is %d. Reduce this to %d for extra revision?" % (item[0],newItem0))):
d.data.remove(item)
d.data.append((newItem0,item[1],item[2]))
d.save()
found = 1 ; break
if not found: tkMessageBox.showinfo(filename,localise("Repeat count is 0, so we cannot reduce it for extra revision."))
def makeLabel_lenLimit(self,utext): return Tkinter.Label(self.grid,text=utext,wraplength=int(self.ourCanvas.winfo_screenwidth()/(1+len(self.languagesToDraw))))
def addSynthLabel(self,filename,row,col):
try: ftext = ensure_unicode(u8strip(read(filename).strip(wsp)))
try: ftext = ensure_unicode(u8strip(bwspstrip(read(filename))))
except IOError: return False
l = self.makeLabel_lenLimit(ftext)
l.grid(row=row,column=col,columnspan=2,sticky="w")
l.bind('<Button-1>',lambda *args:self.startSynthEdit(l,row,col,filename))
l.bind('<Button-1>',lambda e=None,self=self,l=l,row=row,col=col,filename=filename:self.startSynthEdit(l,row,col,filename))
return True # do NOT put it in self.coords2buttons (not to do with space bar stuff etc)
def startSynthEdit(self,l,row,col,filename):
if hasattr(self,"renameToCancel"):
......@@ -354,29 +375,29 @@ class RecorderControls(ButtonScrollingMixin):
self.cancelRename(rr,cc)
if l: l.grid_forget()
editText,editEntry = addTextBox(self.grid,"nopack")
try: editText.set(ensure_unicode(u8strip(read(filename).strip(wsp))))
try: editText.set(ensure_unicode(u8strip(bwspstrip(read(filename)))))
except IOError: pass
editEntry.grid(row=row,column=col,sticky='we',columnspan=2)
editEntry.bind('<Return>',lambda *args:self.doEdit(editText,editEntry,row,col,filename))
editEntry.bind('<Escape>',lambda *args:self.cancelEdit(editEntry,row,col,filename))
editEntry.bind('<Return>',lambda e=None,self=self,editText=editText,editEntry=editEntry,row=row,col=col,filename=filename:self.doEdit(editText,editEntry,row,col,filename))
editEntry.bind('<Escape>',lambda e=None,self=self,editEntry=editEntry,row=row,col=col,filename=filename:self.cancelEdit(editEntry,row,col,filename))
focusButton(editEntry)
if hasattr(self.coords2buttons.get((row-1,col+1),""),"is_synth_label"):
self.addLabel(row-1,col+1,localise("(synth'd)"))
self.coords2buttons[(row-1,col+1)].is_synth_label = True
def doEdit(self,editText,editEntry,row,col,filename):
text = asUnicode(editText.get()).encode("utf-8").strip(wsp)
if text: open(filename,"w").write(text+"\n")
text = bwspstrip(asUnicode(editText.get()).encode("utf-8"))
if text: writeB(open(filename,"w"),text+B("\n"))
else:
try: os.remove(filename)
except: pass
self.cancelEdit(editEntry,row,col,filename)
if row+1 < self.addMoreRow and (row+1,col+1) in self.coords2buttons: focusButton(self.coords2buttons[(row+1,col+1)]) # focus the next "synth" button if it exists (don't press it as well like file renaming because it might be a variant etc, TODO can we skip variants?)
if row+1 < self.addMoreRow and checkIn((row+1,col+1),self.coords2buttons): focusButton(self.coords2buttons[(row+1,col+1)]) # focus the next "synth" button if it exists (don't press it as well like file renaming because it might be a variant etc, TODO can we skip variants?)
def cancelEdit(self,editEntry,row,col,filename):
editEntry.grid_forget()
labelAdded = self.addSynthLabel(filename,row,col)
if hasattr(self.coords2buttons.get((row-1,col+1),""),"is_synth_label"):
if labelAdded: self.addLabel(row-1,col+1,localise("(synth'd)"))
else: self.addButton(row-1,col+1,text=localise("Synthesize"),command=(lambda *args:self.startSynthEdit(None,row,col,filename)))
else: self.addButton(row-1,col+1,text=localise("Synthesize"),command=(lambda e=None,self=self,row=row,col=col,filename=filename:self.startSynthEdit(None,row,col,filename)))
self.coords2buttons[(row-1,col+1)].is_synth_label = True
def amplify(self,*args):
self.AmplifyButton["text"] = localise("Please wait") # TODO not in the GUI thread !! (but lock our other buttons while it's doing it)
......@@ -398,7 +419,7 @@ class RecorderControls(ButtonScrollingMixin):
numZips = makeMp3Zips(self.currentDir,self.currentDir+os.sep+"zips")
if numZips:
openDirectory(self.currentDir+os.sep+"zips",1)
if numZips>1: app.todo.alert=localise("Please send the %d zip files as %d separate messages, in case one very large message doesn't get through.") % (zipNo,zipNo)
if numZips>1: app.todo.alert=localise("Please send the %d zip files as %d separate messages, in case one very large message doesn't get through.") % (numZips,numZips)
else: app.todo.alert=localise("You may now send the zip file by email.")
else: app.todo.alert=localise("No recordings found")
self.undraw() ; self.draw()
......@@ -406,11 +427,11 @@ class RecorderControls(ButtonScrollingMixin):
if hasattr(self,"renameToCancel"):
rr,cc = self.renameToCancel
self.cancelRename(rr,cc)
if self.has_variants and filename.find(" (")>-1:
if self.has_variants and filename.find(" (")>=0:
app.todo.alert=self.renamevar_msg
return
self.renameToCancel = (row,col)
if (row,col) in self.coords2buttons: self.coords2buttons[(row,col)].grid_forget()
if checkIn((row,col),self.coords2buttons): self.coords2buttons[(row,col)].grid_forget()
renameText,renameEntry = addTextBox(self.grid,"nopack")
renameEntry['width']=min(8,len(filename)+2)
renameEntry.theText = renameText
......@@ -427,20 +448,20 @@ class RecorderControls(ButtonScrollingMixin):
selectAllFunc = selectAll
class E: pass
e=E() ; e.widget = renameEntry
self.ourCanvas.after(50,lambda *args:(e.widget.focus(),self.scrollIntoView(e.widget),selectAllFunc(e)))
renameEntry.bind('<Return>',lambda *args:self.doRename(row,col))
renameEntry.bind('<Escape>',lambda *args:self.cancelRename(row,col))
self.ourCanvas.after(50,lambda _=None,e=e,self=self,selectAllFunc=selectAllFunc:(e.widget.focus(),self.scrollIntoView(e.widget),selectAllFunc(e)))
renameEntry.bind('<Return>',lambda e=None,self=self,row=row,col=col:self.doRename(row,col))
renameEntry.bind('<Escape>',lambda e=None,self=self,row=row,col=col:self.cancelRename(row,col))
def doRename(self,row,col):
if hasattr(self,"renameToCancel"): del self.renameToCancel
try: origName = self.coords2buttons[(row,col)].origName
except AttributeError: return # event must have fired twice for some reason?
newNames = filter(lambda x:x,asUnicode(self.coords2buttons[(row,col)].theText.get()).split("\n")) # multiline paste, ignore blank lines
newNames = filter(lambda x,self=self,row=row,col=col:x,asUnicode(self.coords2buttons[(row,col)].theText.get()).split("\n")) # multiline paste, ignore blank lines
for newName in newNames:
if not origName: # extra lines - need to get their origNames
if row==self.addMoreRow: self.addMore()
elif not (row,col) in self.coords2buttons: row += 1 # skip extra row if there are notes
elif not checkIn((row,col),self.coords2buttons): row += 1 # skip extra row if there are notes
origName=self.coords2buttons[(row,col)]["text"]
if self.has_variants and origName.find(" (")>-1:
if self.has_variants and origName.find(" (")>=0:
app.todo.alert=self.renamevar_msg
break
if len(newNames)>1 and not '0'<=newName[0]<='9': # multiline paste and not numbered - we'd better keep the original number
......@@ -452,7 +473,7 @@ class RecorderControls(ButtonScrollingMixin):
except:
tkMessageBox.showinfo(app.master.title(),localise("Could not rename %s to %s") % (origName,newName))
return
self.addButton(row,col,text=newName,command=(lambda e=None,f=self.currentDir+os.sep+newName:self.changeDir(f)))
self.addButton(row,col,text=newName,command=(lambda e=None,self=self,f=self.currentDir+os.sep+newName:self.changeDir(f)))
else: # not a directory - rename individual files
self.doStop() # just in case
for lang in list2set([firstLanguage,secondLanguage]+otherLanguages+self.languagesToDraw): # not just self.languagesToDraw, as a student of more languages than these might not have them all showing and still expect renames to work
......@@ -463,53 +484,53 @@ class RecorderControls(ButtonScrollingMixin):
except:
tkMessageBox.showinfo(app.master.title(),localise("Could not rename %s to %s") % (origName+"_"+lang+ext,newName+"_"+lang+ext)) # TODO undo any that did succeed first! + check for destination-already-exists (OS may not catch it)
return
if not lang in self.languagesToDraw: continue
if not checkIn(lang,self.languagesToDraw): continue
self.updateFile(unicode2filename(newName+"_"+lang+ext),row,self.languagesToDraw.index(lang),cond(ext==dottxt,0,2)) # TODO the 2 should be 1 if and only if we didn't just record it
updated=True
if not updated and lang in self.languagesToDraw: self.updateFile(unicode2filename(newName+"_"+lang+dotwav),row,self.languagesToDraw.index(lang),0)
if not updated and checkIn(lang,self.languagesToDraw): self.updateFile(unicode2filename(newName+"_"+lang+dotwav),row,self.languagesToDraw.index(lang),0)
self.addLabel(row,col,newName)
# TODO what about updating progress.txt with wildcard changes (cld be going too far - we have the move script in utilities)
origName = None # get any others from the form
row += 1
if len(newNames)==1 and row<self.addMoreRow: # put cursor on the next one
if not (row,col) in self.coords2buttons: row += 1 # skip extra row if there are notes
if not checkIn((row,col),self.coords2buttons): row += 1 # skip extra row if there are notes
if row<self.addMoreRow:
origName=self.coords2buttons[(row,col)]["text"]
if not isDirectory(unicode2filename(self.currentDir+os.sep+origName)): self.startRename(row,0,origName)
def cancelRename(self,row,col):
if hasattr(self,"renameToCancel"): del self.renameToCancel
origName = self.coords2buttons[(row,col)].origName
if isDirectory(unicode2filename(self.currentDir+os.sep+origName)): self.addButton(row,col,text=origName,command=(lambda e=None,f=ensure_unicode(self.currentDir+os.sep+origName).encode('utf-8'):self.changeDir(f)))
if isDirectory(unicode2filename(self.currentDir+os.sep+origName)): self.addButton(row,col,text=origName,command=(lambda e=None,self=self,f=ensure_unicode(self.currentDir+os.sep+origName).encode('utf-8'):self.changeDir(f)))
else: self.addLabel(row,col,origName)
def updateFile(self,filename,row,languageNo,state,txtExists="unknown"): # state: 0 not exist, 1 already existed, 2 we just created it
if not os.sep in filename: filename = self.currentDir+os.sep+filename
recFilename = filename
if recFilename.lower().endswith(dotmp3): recFilename=recFilename[:-len(dotmp3)]+dotwav # always record in WAV; can compress to MP3 after
if state: # exists
if not tkSnack or tkSnack=="MicOnly": self.addButton(row,2+3*languageNo,text=localise("Play"),command=(lambda e=None,f=filename:(self.doStop(),SampleEvent(f).play()))) # but if got full tkSnack, might as well use setInputSource instead to be consistent with the non-_ version:
else: self.addButton(row,2+3*languageNo,text=localise("Play"),command=(lambda e=None,f=filename:(self.doStop(),theISM.setInputSource(PlayerInput(f,not self.syncFlag)),self.setSync(False))))
if state: # sound file exists
if not tkSnack or tkSnack=="MicOnly" or wavPlayer_override: self.addButton(row,2+3*languageNo,text=localise("Play"),command=(lambda e=None,f=filename,self=self:(self.doStop(),SampleEvent(f).play()))) # but if got full tkSnack, might as well use setInputSource instead to be consistent with the non-_ version:
else: self.addButton(row,2+3*languageNo,text=localise("Play"),command=(lambda e=None,f=filename,self=self:(self.doStop(),theISM.setInputSource(PlayerInput(f,not self.syncFlag)),self.setSync(False))))
if tkSnack and (state==2 or self.always_enable_rerecord):
self.addButton(row,3+3*languageNo,text=localise("Re-record"),command=(lambda e=None,f=recFilename,r=row,l=languageNo:self.doRecord(f,r,l,needToUpdatePlayButton=(not filename==recFilename))))
self.addButton(row,3+3*languageNo,text=localise("Re-record"),command=(lambda e=None,f0=filename,f=recFilename,r=row,l=languageNo,self=self:self.doRecord(f,r,l,needToUpdatePlayButton=(not f0==f))))
else:
self.addLabel(row,3+3*languageNo,"")
self.need_reRecord_enabler = not (not tkSnack)
else: # does not exist
else: # sound file does not exist
synthFilename = filename[:filename.rfind(extsep)]+dottxt
if txtExists=="unknown": txtExists=fileExists(synthFilename)
if txtExists: self.addLabel(row,2+3*languageNo,localise("(synth'd)"))
elif self.always_enable_synth and get_synth_if_possible(self.languagesToDraw[languageNo],0): self.addButton(row,2+3*languageNo,text=localise("Synthesize"),command=(lambda *args:self.startSynthEdit(None,row+1,1+3*languageNo,synthFilename)))
elif self.always_enable_synth and get_synth_if_possible(self.languagesToDraw[languageNo],0): self.addButton(row,2+3*languageNo,text=localise("Synthesize"),command=(lambda e=None,self=self,row=row,languageNo=languageNo,synthFilename=synthFilename:self.startSynthEdit(None,row+1,1+3*languageNo,synthFilename)))
else: self.addLabel(row,2+3*languageNo,localise("(empty)"))
self.coords2buttons[(row,2+3*languageNo)].is_synth_label = True
if winCEsound and not tkSnack: self.addLabel(row,3+3*languageNo,"")
else: self.addButton(row,3+3*languageNo,text=localise("Record"),command=(lambda e=None,f=recFilename,r=row,l=languageNo:self.doRecord(f,r,l)))
else: self.addButton(row,3+3*languageNo,text=localise("Record"),command=(lambda e=None,f=recFilename,r=row,l=languageNo,self=self:self.doRecord(f,r,l)))
def add_addMore_button(self):
if winCEsound and not tkSnack: pass # no 'add more words' button on WinCE; use PocketPC record button instead
else: self.addButton(self.addMoreRow,0,text=localise("Add more words"),command=(lambda *args:self.addMore()),colspan=cond(self.need_reRecord_enabler,2,4))
if self.need_reRecord_enabler: self.addButton(self.addMoreRow,2,text=localise("Re-record"),command=(lambda *args:self.global_rerecord()),colspan=2)
self.addButton(self.addMoreRow,4,text=localise("New folder"),command=(lambda *args:self.newFolder()),colspan=3)
else: self.addButton(self.addMoreRow,0,text=localise("Add more words"),command=(lambda e=None,self=self:self.addMore()),colspan=cond(self.need_reRecord_enabler,2,4))
if self.need_reRecord_enabler: self.addButton(self.addMoreRow,2,text=localise("Re-record"),command=(lambda e=None,self=self:self.global_rerecord()),colspan=2)
self.addButton(self.addMoreRow,4,text=localise("New folder"),command=(lambda e=None,self=self:self.newFolder()),colspan=3)
def del_addMore_button(self):
if (self.addMoreRow,0) in self.coords2buttons: self.coords2buttons[(self.addMoreRow,0)].grid_forget() # old 'add more' button
if (self.addMoreRow,2) in self.coords2buttons: self.coords2buttons[(self.addMoreRow,2)].grid_forget() # old 're-record' button
if checkIn((self.addMoreRow,0),self.coords2buttons): self.coords2buttons[(self.addMoreRow,0)].grid_forget() # old 'add more' button
if checkIn((self.addMoreRow,2),self.coords2buttons): self.coords2buttons[(self.addMoreRow,2)].grid_forget() # old 're-record' button
self.coords2buttons[(self.addMoreRow,4)].grid_forget() # old 'new folder' button
def addMore(self,*args):
self.del_addMore_button()
......@@ -522,7 +543,7 @@ class RecorderControls(ButtonScrollingMixin):
self.gridLabel(lang,self.addMoreRow)
self.addMoreRow += 2 ; self.maxPrefix += 1
self.add_addMore_button()
def gridLabel(self,lang,row): Tkinter.Label(self.grid,text=" "+localise(cond(lang.find("-meaning_")>-1,"meaning",lang))+": ").grid(row=row,column=1+3*self.languagesToDraw.index(lang))
def gridLabel(self,lang,row): Tkinter.Label(self.grid,text=" "+localise(cond(lang.find("-meaning_")>=0,"meaning",lang))+": ").grid(row=row,column=1+3*self.languagesToDraw.index(lang))
def doRecord(self,filename,row,languageNo,needToUpdatePlayButton=False):
if not tkSnack: return tkMessageBox.showinfo(app.master.title(),localise("Sorry, cannot record on this computer because the tkSnack library (python-tksnack) is not installed."))
theISM.startRecording(filename)
......@@ -530,7 +551,7 @@ class RecorderControls(ButtonScrollingMixin):
self.coords2buttons[(row,3+3*languageNo)]["text"]=localise("Stop")
self.updateForStopOrChange()
self.currentRecording = (filename,row,languageNo)
self.coords2buttons[(row,3+3*languageNo)]["command"]=(lambda *args:self.doStop())
self.coords2buttons[(row,3+3*languageNo)]["command"]=(lambda e=None,self=self:self.doStop())
if app.scanrow.get()=="2": # "stop"
focusButton(self.coords2buttons[(row,3+3*languageNo)])
else:
......@@ -538,14 +559,14 @@ class RecorderControls(ButtonScrollingMixin):
if app.scanrow.get()=="1": # move along 1st
while languageNo+1<len(self.languagesToDraw):
languageNo += 1
if (row,3+3*languageNo) in self.coords2buttons:
if checkIn((row,3+3*languageNo),self.coords2buttons):
focusButton(self.coords2buttons[(row,3+3*languageNo)])
return
languageNo = 0 # start of the row
# fall-through - vertical movement
for r in [row+1,row+2]:
if r==self.addMoreRow: self.addMore()
if (r,3+3*languageNo) in self.coords2buttons:
if checkIn((r,3+3*languageNo),self.coords2buttons):
return focusButton(self.coords2buttons[(r,3+3*languageNo)])
def doStop(self,*args):
theISM.stopRecording()
......@@ -563,9 +584,9 @@ class RecorderControls(ButtonScrollingMixin):
if hasattr(self,"oldCanvasBbox") and bbox==self.oldCanvasBbox: pass
else:
self.oldCanvasBbox = bbox
c.config(scrollregion=bbox,width=bbox[2],height=min(c["height"],c.winfo_screenheight()/2,bbox[3]))
c.config(scrollregion=bbox,width=bbox[2],height=min(int(c["height"]),int(c.winfo_screenheight()/2),int(bbox[3])))
if hasattr(self,"currentRecording") and not theISM.currentOutfile: self.doStop() # ensure GUI updates the recording button after player auto-stop (for want of a better place to put it)
app.after(cond(winCEsound,3000,600),lambda *args:self.reconfigure_scrollbar())
app.after(cond(winCEsound,3000,600),lambda e=None,self=self:self.reconfigure_scrollbar())
def setSync(self,syncFlag): self.syncFlag = syncFlag
def newFolder(self,*args):
count=0
......@@ -585,12 +606,12 @@ class RecorderControls(ButtonScrollingMixin):
self.doStop()
theISM.setInputSource(PlayerInput(filename,not self.syncFlag))
self.current_recordFrom_button = (row, self.coords2buttons[(row,0)])
self.addButton(row,0,text=localise("Stop"),command=(lambda *args:(self.doStop(),theISM.setInputSource(MicInput()))),colspan=1)
self.addButton(row,0,text=localise("Stop"),command=(lambda e=None,self=self,theISM=theISM:(self.doStop(),theISM.setInputSource(MicInput()))),colspan=1)
col = 1
for inc in [-30, -5, 5, 30]:
if inc<0: text="<"+str(-inc)
else: text=str(inc)+">"
self.addButton(row,col,text=text,command=(lambda e=None,i=inc:self.handleSkip(filename,i)))
self.addButton(row,col,text=text,command=(lambda e=None,i=inc,self=self,filename=filename:self.handleSkip(filename,i)))
col += 1
def handleSkip(self,filename,i):
self.protect_currentRecordFrom()
......@@ -604,7 +625,7 @@ class RecorderControls(ButtonScrollingMixin):
if hasattr(self,"current_recordFrom_button") and self.current_recordFrom_button:
row, button = self.current_recordFrom_button
for col in range(1+3*len(self.languagesToDraw)):
if (row,col) in self.coords2buttons:
if checkIn((row,col),self.coords2buttons):
self.coords2buttons[(row,col)].grid_forget()
del self.coords2buttons[(row,col)]
button.grid(row=row,column=0,columnspan=1+3*len(self.languagesToDraw),sticky="w")
......@@ -643,6 +664,10 @@ class RecorderControls(ButtonScrollingMixin):
else: self.languagesToDraw = [secondLanguage,firstLanguage] # each lang cn take 3 columns, starting at column 1 (DO need to regenerate this every draw - languages may have changed!)
if self.currentDir==samplesDirectory: app.master.title(localise("Recordings manager"))
else: app.master.title(localise("Recordings manager: ")+filename2unicode((os.sep+self.currentDir)[(os.sep+self.currentDir).rindex(os.sep)+1:]))
if hasattr(app,"isBigPrint") and winsound:
# Vista sometimes has window placement problems here
try: app.master.geometry("+0+0")
except: pass
if not self.snack_initialized:
if tkSnack and not tkSnack=="MicOnly":
tkSnack.initializeSnack(app)
......@@ -654,7 +679,8 @@ class RecorderControls(ButtonScrollingMixin):
app.scanrow.set("0")
self.reconfigure_scrollbar()
if tkSnack: theISM.setInputSource(MicInput())
self.frame=Tkinter.Frame(app.leftPanel) ; self.frame.pack()
self.frame=Tkinter.Frame(app.leftPanel)
self.frame.pack(fill=Tkinter.Y,expand=1)
self.need_reRecord_enabler = 0 # no previously-existing words yet (when we get existing words we 'lock' them and have to unlock by pressing a global 'rerecord' button 1st, just in case)
......@@ -662,6 +688,15 @@ class RecorderControls(ButtonScrollingMixin):
else:
r = Tkinter.Frame(self.frame)
r.grid(row=1,sticky="e",columnspan=2)
if hasattr(app,"isBigPrint") and macsound:
# Try to make up for the fact that we can't always increase the width of the scrollbar (and the keyboard often loses focus). Add extra up/down buttons. (TODO: does any other platform need this?)
r2 = Tkinter.Frame(r)
r2.pack({"side":"right"})
addButton(r2,unichr(8593),lambda e=None,app=app:app.ScrollUpHandler(),"left")
addButton(r2,unichr(8595),lambda e=None,app=app:app.ScrollDownHandler(),"left")
Tkinter.Label(r,text=" ").pack({"side":"right"}) # TODO: more flexible spacer
r = Tkinter.Frame(r)
r.pack({"side":"right"})
Tkinter.Label(r,text=localise("Action of spacebar during recording")).pack()
r=Tkinter.Frame(r) ; r.pack()
for button in [
......@@ -678,7 +713,7 @@ class RecorderControls(ButtonScrollingMixin):
maxPrefix = 0 ; self.has_recordFrom_buttons = False
if not self.currentDir==samplesDirectory and os.sep in self.currentDir:
self.addButton(curRow,0,text=localise("(Up)"),command=(lambda e=None,f=self.currentDir[:self.currentDir.rindex(os.sep)]:self.changeDir(f)))
self.addButton(curRow,0,text=localise("(Up)"),command=(lambda e=None,f=self.currentDir[:self.currentDir.rindex(os.sep)],self=self:self.changeDir(f)))
curRow += 1
l = os.listdir(self.currentDir)
def cmpfunc(a,b): # sort alphabetically but ensure L2 comes before L1 for tab order
......@@ -690,19 +725,19 @@ class RecorderControls(ButtonScrollingMixin):
if a>b: return 1
elif b>a: return -1
else: return 0
l.sort(cmpfunc)
sort(l,cmpfunc)
self.has_variants = check_has_variants(self.currentDir,l)
allLangs = list2set([firstLanguage,secondLanguage]+possible_otherLanguages)
hadDirectories = False
for fname in l:
flwr = fname.lower() ; isMeaning=0
if firstLanguage==secondLanguage and firstLanguage+"-meaning_"+secondLanguage in fname: isMeaning,languageOverride = True, firstLanguage+"-meaning_"+secondLanguage # hack for re-loading a dir of word+meaning in same language. TODO hope not combining -meaning_ with variants
elif self.has_variants and fname.find("_",fname.find("_")+1)>-1 and not fname.find("_explain_")>-1: languageOverride=fname[fname.find("_")+1:fname.find("_",fname.find("_")+1)]
elif self.has_variants and fname.find("_",fname.find("_")+1)>=0 and not fname.find("_explain_")>=0: languageOverride=fname[fname.find("_")+1:fname.find("_",fname.find("_")+1)]
else: languageOverride=None
if isDirectory(self.currentDir+os.sep+fname):
if not flwr in ["zips","utils","advanced utilities"]: # NOT "prompts", that can be browsed
newDir = self.currentDir+os.sep+fname
self.addButton(curRow,0,text=filename2unicode(fname),command=(lambda e=None,f=newDir:self.changeDir(f)))
self.addButton(curRow,0,text=filename2unicode(fname),command=(lambda e=None,f=newDir,self=self:self.changeDir(f)))
# TODO if _disabled have an Enable button ?
# if not have a Disable ??
# (NB though the above button will have a column span)
......@@ -710,12 +745,14 @@ class RecorderControls(ButtonScrollingMixin):
focusButton(self.coords2buttons[(curRow,0)])
dirToHighlight = None # done
curRow += 1
if fileExists(self.currentDir+os.sep+fname+os.sep+longDescriptionName): description=u8strip(read(self.currentDir+os.sep+fname+os.sep+longDescriptionName)).strip(wsp)
elif fileExists(self.currentDir+os.sep+fname+os.sep+shortDescriptionName): description=u8strip(read(self.currentDir+os.sep+fname+os.sep+shortDescriptionName)).strip(wsp)
if fileExists(self.currentDir+os.sep+fname+os.sep+longDescriptionName): description=wspstrip(u8strip(read(self.currentDir+os.sep+fname+os.sep+longDescriptionName)))
elif fileExists(self.currentDir+os.sep+fname+os.sep+shortDescriptionName): description=wspstrip(u8strip(read(self.currentDir+os.sep+fname+os.sep+shortDescriptionName)))
else: description=None
if description:
l = Tkinter.Label(self.grid,text=" "+description,wraplength=self.ourCanvas.winfo_screenwidth())
l.grid(row=curRow,column=0,columnspan=1+3*len(self.languagesToDraw),sticky="w")
try: sbarWidth = app.sbarWidth
except: sbarWidth = 16 # default
ll = Tkinter.Label(self.grid,text=" "+description,wraplength=self.ourCanvas.winfo_screenwidth()-sbarWidth-50) # allow for borders on Windows (TODO: is 50px always right?)
ll.grid(row=curRow,column=0,columnspan=1+3*len(self.languagesToDraw),sticky="w")
curRow += 1
if not flwr=="prompts": hadDirectories = True
elif "_" in fname and (languageOverride in allLangs or languageof(fname) in allLangs): # something_lang where lang is a recognised language (don't just take "any _" because some podcasts etc will have _ in them)
......@@ -740,24 +777,24 @@ class RecorderControls(ButtonScrollingMixin):
if iprefix>maxPrefix: maxPrefix=iprefix # max existing numerical prefix
if (flwr.endswith(dotwav) or flwr.endswith(dotmp3) or flwr.endswith(dottxt)): # even if not languageOverride in self.languagesToDraw e.g. for prompts - helps setting up gradint in a language it doesn't have prompts for (creates blank rows for the prefixes that other languages use). TODO do we want to add 'and languageOverride in self.languagesToDraw' if NOT in prompts?
if not prefix in prefix2row:
if not checkIn(prefix,prefix2row):
self.addLabel(curRow,0,utext=filename2unicode(prefix))
foundTxt = {}
for lang in self.languagesToDraw:
if realPrefix+"_"+lang+afterLang+dottxt in l: foundTxt[lang]=(self.currentDir+os.sep+realPrefix+"_"+lang+afterLang+dottxt,2+3*self.languagesToDraw.index(lang))
if checkIn(realPrefix+"_"+lang+afterLang+dottxt,l): foundTxt[lang]=(self.currentDir+os.sep+realPrefix+"_"+lang+afterLang+dottxt,2+3*self.languagesToDraw.index(lang))
prefix2row[prefix] = curRow
for lang in self.languagesToDraw: # preserve tab order
if lang==languageOverride and not flwr.endswith(dottxt):
self.updateFile(fname,curRow,self.languagesToDraw.index(lang),state=1)
languageOverride=None # so not done again
else: self.updateFile(prefix+"_"+lang+dotwav,curRow,self.languagesToDraw.index(lang),state=0,txtExists=(lang in foundTxt))
else: self.updateFile(prefix+"_"+lang+dotwav,curRow,self.languagesToDraw.index(lang),state=0,txtExists=checkIn(lang,foundTxt))
self.gridLabel(lang,curRow)
for filename,col in foundTxt.values(): self.addSynthLabel(filename,curRow+1,col)
curRow += 2
if languageOverride in self.languagesToDraw and not flwr.endswith(dottxt):
if checkIn(languageOverride,self.languagesToDraw) and not flwr.endswith(dottxt):
self.updateFile(fname,prefix2row[prefix],self.languagesToDraw.index(languageOverride),state=1)
elif (flwr.endswith(dotwav) or flwr.endswith(dotmp3)) and tkSnack and not tkSnack=="MicOnly": # no _ in it but we can still play it for splitting
self.addButton(curRow,0,text=(localise("Record from %s") % (filename2unicode(fname),)),command=(lambda e=None,r=curRow,f=self.currentDir+os.sep+fname:self.doRecordFrom(f,r)))
self.addButton(curRow,0,text=(localise("Record from %s") % (filename2unicode(fname),)),command=(lambda e=None,r=curRow,f=self.currentDir+os.sep+fname,self=self:self.doRecordFrom(f,r)))
self.has_recordFrom_buttons = True
curRow += 1
self.addMoreRow = curRow ; self.maxPrefix = maxPrefix+1
......@@ -776,14 +813,24 @@ class RecorderControls(ButtonScrollingMixin):
addButton(r2,localise("Record from file"),self.do_recordFromFile,"left")
if got_program("lame"): self.CompressButton = addButton(r2,localise("Compress all"),self.all2mp3_or_zip,"left") # was "Compress all recordings" but it takes too much width
# TODO else can we see if it's possible to get the encoder on the fly, like in the main screen? (would need some restructuring)
elif got_program("zip") and (explorerCommand or winCEsound): self.CompressButton = addButton(r2,localise("Zip for email"),lambda *args:self.all2mp3_or_zip,"left")
elif got_program("zip") and (explorerCommand or winCEsound): self.CompressButton = addButton(r2,localise("Zip for email"),lambda e=None,self=self:self.all2mp3_or_zip(),"left")
addButton(r2,localise(cond(recorderMode,"Quit","Back to main menu")),self.finished,"left")
if winCEsound and not tkSnack: msg="Click on filenames at left to rename; click synthesized text to edit it"
else: msg="Choose a word and start recording. Then press space to advance (see control at top). You can also browse and manage previous recordings. Click on filenames at left to rename (multi-line pastes are allowed); click synthesized text to edit it."
Tkinter.Label(self.frame,text=msg,wraplength=cond(hasattr(app,"isBigPrint") or olpc or winCEsound,self.ourCanvas.winfo_screenwidth(),min(int(self.ourCanvas.winfo_screenwidth()*.7),512))).grid(columnspan=2) # (512-pixel max. so the column isn't too wide to read on wide screens, TODO increase if the font is large)
if olpc or winCEsound: labelwidth = self.ourCanvas.winfo_screenwidth()
elif hasattr(app,"isBigPrint"): labelwidth = self.ourCanvas.winfo_screenwidth()-50 # allow for borders on Windows (TODO: is 50px always right?)
else: labelwidth=min(int(self.ourCanvas.winfo_screenwidth()*.7),512) # (512-pixel max. so the column isn't too wide to read on wide screens, TODO increase if the font is large)
Tkinter.Label(self.frame,text=msg,wraplength=labelwidth).grid(columnspan=2)
# (Don't worry about making the text files editable - editable filenames should be enough + easier to browse the result outside Gradint; can include both languages in the filename if you like - hope the users figure this out as we don't want to make the instructions too complex)
def reviseCount(num):
# suggested reduction for revision
thresholds=[1,2,knownThreshold,reallyKnownThreshold,meaningTestThreshold,randomDropThreshold,randomDropThreshold2] ; thresholds.sort() ; thresholds.reverse()
for i in range(len(thresholds)-1):
if num>thresholds[i]: return thresholds[i+1]
return 0
def doRecWords(): # called from GUI thread
if hasattr(app,"LessonRow"): app.thin_down_for_lesson() # else recorderMode
app.Label.pack_forget() ; app.CancelRow.pack_forget()
......@@ -793,18 +840,39 @@ def doRecWords(): # called from GUI thread
theRecorderControls.draw()
app.wordsExist = 1 # well not necessarily, but see comments re "Create word list"
# Functions for recording on S60 phones:
# Functions for recording on Android and S60 phones:
def android_recordFile(language):
fname = os.getcwd()+os.sep+"newfile_"+language+dotwav
while True:
android.recorderStartMicrophone(fname) # TODO: python-for-android's MediaRecorderFacade.java startAudioRecording uses default output format and encoder, which likely means that so-called .wav file is really a .3gp file. Have worked around in pcmlen for now, but don't know if the assumptions made there are universal, plus we don't want to name these files .wav if they're not really .wav
android.dialogCreateAlert("Recording",language)
android.dialogSetPositiveButtonText("Stop")
android.dialogShow() ; android.dialogGetResponse()
android.recorderStop()
android.mediaPlay("file://"+fname)
if not getYN("Are you happy with this?"):
os.remove(fname) ; continue
return fname
def android_recordWord():
if not getYN("Ready to record "+secondLanguage+" word?"): return
def ipFunc(prompt,value=u""): return android.dialogGetInput("Gradint",prompt,value).result
droidOrS60RecWord(android_recordFile,ipFunc)
def s60_recordWord():
def ipFunc(prompt,value=u""): return appuifw.query(prompt,"text",value)
droidOrS60RecWord(s60_recordFile,ipFunc)
def droidOrS60RecWord(recFunc,inputFunc):
if secondLanguage==firstLanguage: l1Suffix, l1Display = firstLanguage+"-meaning_"+firstLanguage, "meaning"
else: l1Suffix, l1Display = firstLanguage, firstLanguage
while True:
l2 = s60_recordFile(secondLanguage)
l2 = recFunc(secondLanguage)
if not l2: return
l1 = None
while not l1:
if (not maybeCanSynth(firstLanguage)) or getYN("Record "+l1Display+" too? (else computer voice)"): l1 = s60_recordFile(l1Suffix) # (TODO what if maybeCanSynth(secondLanguage) but not first, and we want to combine 2nd-lang synth with 1st-lang recorded? low priority as if recording will prob want to rec L2)
if (not maybeCanSynth(firstLanguage)) or getYN("Record "+l1Display+" too? (else computer voice)"): l1 = recFunc(l1Suffix) # (TODO what if maybeCanSynth(secondLanguage) but not first, and we want to combine 2nd-lang synth with 1st-lang recorded? low priority as if recording will prob want to rec L2)
else:
l1txt = appuifw.query(u""+firstLanguage+" text:","text")
l1txt = inputFunc(u""+firstLanguage+" text:")
if l1txt:
l1 = "newfile_"+firstLanguage+dottxt
open(l1,"w").write(l1txt.encode("utf-8"))
......@@ -812,21 +880,22 @@ def s60_recordWord():
os.remove(l2) ; break
if not l1: continue
ls = list2set(os.listdir(samplesDirectory))
def inLs(prefix):
for ext in [dotwav,dotmp3,dottxt]:
for l in [firstLanguage,secondLanguage]:
if prefix+"_"+l+ext in ls: return 1
c = 1
while inLs("%02d" % c): c += 1
origPrefix = prefix = u""+("%02d" % c)
def inLs(prefix,ls):
for l in ls:
if l.startswith(prefix) and len(l) > len(prefix) and l[len(prefix)] not in "0123456789": return True
global recCount
try: recCount += 1
except: recCount = 1
while inLs("%02d" % recCount,ls): recCount += 1
origPrefix = prefix = ensure_unicode("%02d" % recCount)
while True:
prefix = appuifw.query(u"Filename:","text",prefix)
prefix = inputFunc(u"Filename:",prefix)
if not prefix: # pressed cancel ??
if getYN("Discard this recording?"):
os.remove(l1) ; os.remove(l2) ; return
recCount-=1;os.remove(l1);os.remove(l2);return
else:
prefix = origPrefix ; continue
if not inLs(prefix) or getYN("File exists. overwrite?"): break
if not inLs(prefix,ls) or getYN("File exists. overwrite?"): break
if samplesDirectory: prefix=samplesDirectory+os.sep+prefix
os.rename(l1,prefix+l1[l1.index("_"):])
os.rename(l2,prefix+l2[l2.index("_"):])
......@@ -835,15 +904,15 @@ def s60_recordFile(language):
fname = "newfile_"+language+dotwav
while True:
S=audio.Sound.open(os.getcwd()+os.sep+fname)
def forgetS():
def forgetS(fname,S):
S.close()
try: os.remove(fname)
except: pass
if not getYN("Press OK to record "+language+" word"): return forgetS()
if not getYN("Press OK to record "+language+" word"): return forgetS(fname,S)
S.record()
ret = getYN("Press OK to stop") ; S.stop()
if not ret:
forgetS() ; continue
forgetS(fname,S) ; continue
S.play()
ret = getYN("Are you happy with this?")
S.stop() ; S.close()
......
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -42,7 +42,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData):
# work out number of repetitions needed. not sure if this should be configurable somewhere.
first_repeat_is_unessential = 0
if not numTimesBefore: # New word. If there are L2 variants, introduce them all if possible.
numVariants = min(3,len(variantFiles.get(samplesDirectory+os.sep+zhFile,[0]))) # TODO really max to 3? or 4? or .. ?
numVariants = min(3,len(variantFiles.get(B(samplesDirectory)+B(os.sep)+B(zhFile),[0]))) # TODO really max to 3? or 4? or .. ?
if numVariants>1 and lessonIsTight(): numVariants = 1 # hack
numRepeats = numVariants + cond(numVariants>=cond(availablePrompts.user_is_advanced,2,3),0,1)
elif numTimesBefore == 1: numRepeats = 3
......@@ -53,7 +53,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData):
else: numRepeats = 1
if numRepeats==1:
k,f = synthcache_lookup(zhFile,justQueryCache=1)
if f and k[0]=="_" and not textof(zhFile) in subst_synth_counters:
if f and B(k[:1])==B("_") and not checkIn(textof(zhFile),subst_synth_counters):
# Hack: an experimental cache entry but only 1 repetition - what do we do?
c=random.choice([1,2,3])
if c==1: pass # do nothing
......@@ -65,7 +65,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData):
theList = []
if instrIsPrefix: theList = instructions
theList.append(promptEvent)
if promptFile==zhFile and not promptFile in singleLinePoems:
if promptFile==zhFile and not checkIn(promptFile,singleLinePoems):
# A multi-line poem with no first-language prompts, so we're using each fragment as a prompt for the next, but the first fragment is its own prompt, which means that 'prompt' is ambiguous. Say "beginning" to disambiguate it.
theList = theList + map(lambda x:fileToEvent(x,promptsDirectory), availablePrompts.getPromptList("begin",promptsData,languageof(zhFile)))
if not instrIsPrefix: theList += instructions
......@@ -111,10 +111,11 @@ def reverseAnticipation(promptFile,zhFile,promptsData):
return CompositeEvent(theList)
def languageof(file):
assert "_" in file, "no _ in %s" % (file,)
s=file[file.rindex("_")+1:]
if extsep in s: return s[:s.rindex(extsep)]
else: return s
file = B(file)
assert B("_") in file, "no _ in %s" % (repr(file),)
s=file[file.rindex(B("_"))+1:]
if B(extsep) in s: return S(s[:s.rindex(B(extsep))])
else: return S(s)
def commentSequence():
sequence = []
......@@ -128,7 +129,7 @@ def anticipationSequence(promptFile,zhFile,start,to,promptsData,introList):
# (try number from 'start' to 'to', EXCLUDING 'to')
sequence = []
# First one has initialGlue() whatever the value of 'start' is
if meaningTestThreshold and to==start+1 and start>meaningTestThreshold and random.choice([1,2])==1 and not type(promptFile)==type([]) and promptFile.find("_"+firstLanguage+extsep)>=0:
if meaningTestThreshold and to==start+1 and start>meaningTestThreshold and random.choice([1,2])==1 and not type(promptFile)==type([]) and B(promptFile).find(B("_"+firstLanguage+extsep))>=0:
# *** not sure about that condition - should the random be more biased?
# (the type() and following condition is a hack that ensures this is not used for poetry etc where there are composite prompts or the prompt is the previous line. TODO would be better to keep track of which samples are poetic, because the above breaks down on the first line of a poem that has a translation into the first language because that looks like a normal prompt/response - but ok for now)
firstItem = reverseAnticipation(promptFile,zhFile,promptsData)
......
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -26,15 +26,18 @@ class Synth(object):
################## don't have to re-implement below
def __init__(self): self.fileCache = {}
def __del__(self):
import os # as it might already have been gc'd
for v in self.fileCache.values():
try: import os # as it might already have been gc'd
except: pass # might still be able to use it
try:
for v in self.fileCache.values():
try: os.remove(v)
except: pass # someone may have removed it already, e.g. cache-synth.py's renaming
except: pass
self.fileCache = {} # essential for the globalEspeakSynth hack (or get crash when try to make multiple lessons to file)
def makefile_cached(self,lang,text):
if type(text)==type([]): textKey=repr(text)
else: textKey=text
if (lang,textKey) in self.fileCache: return self.fileCache[(lang,textKey)]
if checkIn((lang,textKey),self.fileCache): return self.fileCache[(lang,textKey)]
t = self.makefile(lang,text)
self.fileCache[(lang,textKey)] = t
return t
......@@ -67,45 +70,85 @@ class OSXSynth_Say(Synth):
def __init__(self): Synth.__init__(self)
def works_on_this_platform(self):
if not (macsound and fileExists("/usr/bin/say")): return False
self.voices = self.scanVoices() ; return True
def supports_language(self,lang): return lang in self.voices
global osxSayVoicesScan
try: osxSayVoicesScan # singleton
except: osxSayVoicesScan = self.scanVoices()
self.voices = osxSayVoicesScan ; return True
def supports_language(self,lang): return checkIn(lang,self.voices)
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
def play(self,lang,text): return system("say %s\"%s\"" % (self.voices[lang],text.replace('"','')))
def play(self,lang,text): return system("say %s\"%s\"" % (S(self.voices[lang]),S(self.transliterate(lang,text)).replace('"','')))
# TODO 10.7+ may also support -r rate (WPM), make that configurable in advanced.txt ?
def makefile(self,lang,text):
fname = os.tempnam()+extsep+"aiff"
system("say -o %s %s\"%s\"" % (fname,self.voices[lang],text.replace('"','')))
system("say %s-o %s \"%s\"" % (S(self.voices[lang]),fname,S(self.transliterate(lang,text)).replace('"','')))
return aiff2wav(fname)
def transliterate(self,lang,text,forPartials=0):
if not self.voices[lang] in ['-v "Ting-Ting" ','-v "Tingting" ']: return text
# The hanzi-to-pinyin conversion in the Ting-Ting voice is not always as good as eSpeak's, but it can be controlled with pinyin.
ut = ensure_unicode(text)
if u"\u513f" in ut or u"\u5152" in ut: return text # might be erhua - better pass to the synth as-is
es = ESpeakSynth()
if not es.works_on_this_platform() or not es.supports_language('zh'): return text
return es.transliterate('zh',text,forPartials)
def can_transliterate(self,lang):
if not self.voices.get(lang,0) in ['-v "Ting-Ting" ','-v "Tingting" ']: return 0
es = ESpeakSynth()
return es.works_on_this_platform() and es.supports_language('zh')
def scanVoices(self):
d = {}
try: from AppKit import NSSpeechSynthesizer
except: return {"en":""} # no -v parameter at all
for vocId in NSSpeechSynthesizer.availableVoices():
vocAttrib = NSSpeechSynthesizer.attributesForVoice_(vocId)
lang = vocAttrib['VoiceLanguage']
if lang.startswith("en-"): lang="en" # TODO do any others need hyphen dropping? careful / check 10.7's Cantonese etc
if not lang in d: d[lang]=[]
d[lang].append(vocAttrib['VoiceName'].encode('utf-8'))
found=0
for k,v in d.items()[:]:
try:
from AppKit import NSSpeechSynthesizer
voiceAttrs=[NSSpeechSynthesizer.attributesForVoice_(vocId) for vocId in NSSpeechSynthesizer.availableVoices()]
except: # maybe we're running under Homebrew Python instead of /usr/bin/python; in at least some recent OS X versions we should be able to get a voice list with 'say -v ?' instead (I'm not sure how far back that goes, so leaving in the above NSSpeechSynthesizer method as well)
voiceAttrs = []
for l in os.popen('say -v "?" </dev/null 2>/dev/null').readlines():
if not '#' in l: continue
name,lang=l[:l.index('#')].rsplit(None,1)
voiceAttrs.append({'VoiceName':name,'VoiceLanguage':lang.replace('_','-')})
if not voiceAttrs: return {"en":""} # maybe we're on ancient OS X: don't use a -v parameter at all
for vocAttrib in voiceAttrs:
if not checkIn('VoiceName',vocAttrib): continue
if not checkIn('VoiceLanguage',vocAttrib):
lang={"Damayanti":"id","Maged":"ar","Stine":"nb"}.get(vocAttrib['VoiceName'],None) # TODO: can sometimes use VoiceLocaleIdentifier instead, dropping the _ part (but can't even do that with Damayanti on 10.7)
if not lang: continue # TODO: output VoiceName in a warning?
else: lang = vocAttrib['VoiceLanguage']
if '-' in lang: lang=lang[:lang.index("-")]
d.setdefault(lang,[]).append(vocAttrib['VoiceName'].encode('utf-8'))
found=0 ; d2=d.copy()
class BreakOut(Exception): pass
# First, check for voice matches in same language beginning
for k,v in list(d.items()):
if k in macVoices:
try:
for m in macVoices[k].split():
if m in v:
d[k] = [m] ; found=1 ; break
if len(d[k])>1: d[k]=[d[k][0]]
if d.keys()==['en'] and not found: return {"en":""} # just use the default
for k,v in d.items()[:]: d[k]='-v "'+v[0]+'" '
return d
for vv in v:
if B(m.lower()) == B(vv.lower()):
d2[k] = [S(vv)] ; found=1 ; del macVoices[k] ; raise BreakOut()
except BreakOut: pass
if len(d2[k])>1: d2[k]=[d2[k][0]]
# Then check across languages (e.g. cant -> zh-...)
for k,v in list(macVoices.items()):
try:
for kk,vv in list(d.items()):
for m in v.split():
for vvv in vv:
if B(m.lower()) == B(vvv.lower()):
d2[k] = [S(vvv)] ; found=1 ; raise BreakOut()
except BreakOut: pass
if list(d.keys())==['en'] and not found: return {"en":""} # just use the default
for k,v in list(d2.items()): d2[k]='-v "'+S(v[0])+'" '
return d2
def aiff2wav(fname):
if not system("sox \"%s\" \"%s\"" % (fname,fname[:-4]+"wav")):
# good, we converted it to wav
os.remove(fname)
fname=fname[:-4]+"wav"
# else just return aiff and hope for the best (TODO won't work with cache-synth)
# else just return aiff and hope for the best (TODO won't work with cache-synth; TODO can get here when 'say' gave empty output, e.g. just a dot, and the returned aiff might raise IOError when constructing a SampleEvent)
return fname
class OSXSynth_OSAScript(Synth):
# for old Macs that don't have a "say" command
def __init__(self): Synth.__init__(self)
def supports_language(self,lang): return lang=="en"
def works_on_this_platform(self): return macsound and fileExists("/usr/bin/osascript")
......@@ -115,16 +158,15 @@ class OSXSynth_OSAScript(Synth):
fname = os.tempnam()+extsep+"aiff"
os.popen("osascript","w").write('say "%s" saving to "%s"\n' % (text,fname))
return aiff2wav(fname)
# TODO: if the user has installed an OS X voice that supports another language, can use say -v voicename ( or 'using \"voicename\"' for the osascript version ) (but I have no access to a suitably-configured Mac for testing this)
class OldRiscosSynth(Synth):
def __init__(self): Synth.__init__(self)
def supports_language(self,lang): return lang=="en"
def works_on_this_platform(self): return riscos_sound and not os.system("sayw .")
def works_on_this_platform(self): return riscos_sound and got_program("*sayw")
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
def play(self,lang,text): return system("sayw %s" % (text,))
class S60Synth(Synth):
class S60Synth(Synth): # TODO: figure out if S60 Python can call external programs; might be able to use eSpeak http://pvagner.webranet.sk/espeak/espeak.sisx
def __init__(self): Synth.__init__(self)
def supports_language(self,lang): return lang=="en" # (audio.say always uses English even when other languages are installed on the device)
def works_on_this_platform(self): return appuifw and hasattr(audio,"say")
......@@ -137,7 +179,7 @@ class S60Synth(Synth):
class AndroidSynth(Synth):
def __init__(self): Synth.__init__(self)
def supports_language(self,lang): return lang=="en" # TODO others?
def supports_language(self,lang): return lang==systemVoice # TODO others? (but ttsSpeak can't set language)
def works_on_this_platform(self): return android
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
def play(self,lang,text): android.ttsSpeak(text)
......@@ -165,7 +207,7 @@ class PttsSynth(Synth):
# must keep the full path even on non-cygwin because we're adding ,1 to changeToDirOf (hope we don't hit a Windows version that doesn't like this). But we can keep relative paths if tempdir_is_curdir. (TODO if this breaks when not tempdir_is_curdir, could try copying ptts.exe to temp, but would need to delete it afterwards)
if cygwin or not tempdir_is_curdir: self.program='"'+os.getcwd()+cwd_addSep+i+'"'
else: self.program = i
self.offlineOnly = 'offline' in i
self.offlineOnly = checkIn('offline',i)
break
if not self.program:
# (in case someone's running on Windows from source)
......@@ -174,15 +216,15 @@ class PttsSynth(Synth):
else: self.lily_file = lily_file
if fileExists(self.lily_file):
self.old_lily_data=read(self.lily_file)
if "zh" in sapiVoices and sapiVoices["zh"][0].lower()=="vw lily": del sapiVoices["zh"] # because we don't want to bypass our own interface to lily if a user set that without realising it's not needed
if checkIn("zh",sapiVoices) and sapiVoices["zh"][0].lower()=="vw lily": del sapiVoices["zh"] # because we don't want to bypass our own interface to lily if a user set that without realising it's not needed
else: self.lily_file = None
def supports_language(self,lang): return lang in sapiVoices or lang=="en" or (self.lily_file and lang=="zh")
def supports_language(self,lang): return checkIn(lang,sapiVoices) or lang=="en" or (self.lily_file and lang=="zh")
# Voice list: os.popen("echo | "+self.program+" -vl").read().split("\n"). If any .lower() contain "espeak-" then after the "-" is an espeak language code see ESpeakSynth (it may not want unicode). Other voices may also have support for specific languages - may sometimes be able to use <lang langid="locale-hex-code"/> (e.g. 809 UK, 804 Chinese (PRC) 404 Taiwan, 411 Japan) but no way for gradint to tell if successful
def works_on_this_platform(self): return self.program
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate, especially if they're going to set the speed in the control panel!
def play(self,lang,text):
if self.offlineOnly: return SampleEvent(self.makefile_cached(lang,text)).play()
if lang in sapiVoices:
if checkIn(lang,sapiVoices):
d=os.getcwd()
ret=self.sapi_unicode(sapiVoices[lang][0],ensure_unicode(text),speed=sapiSpeeds.get(lang,None))
os.chdir(d) ; return ret
......@@ -198,7 +240,7 @@ class PttsSynth(Synth):
def sapi_unicode(self,voice,unicode_string,toFile=None,sampleRate=None,speed=None):
# Speaks unicode_string in 'voice'. toFile (if present) must be something that was returned by tempnam. May change the current directory.
if voice=="Ekho Cantonese": unicode_string = preprocess_chinese_numbers(fix_compatibility(unicode_string),isCant=2) # hack to duplicate the functionality of EkhoSynth
unifile=os.tempnam() ; open(unifile,"wb").write(codecs.utf_16_encode(unicode_string)[0])
unifile=os.tempnam() ; write(unifile,codecs.utf_16_encode(unicode_string)[0])
if not toFile: extra=""
else:
extra=' -w '+changeToDirOf(toFile,1)+' -c 1'
......@@ -211,7 +253,7 @@ class PttsSynth(Synth):
def makefile(self,lang,text):
fname = os.tempnam()+dotwav
oldcwd=os.getcwd()
if lang in sapiVoices: r=self.sapi_unicode(sapiVoices[lang][0],ensure_unicode(text),fname,sapiVoices[lang][1],speed=sapiSpeeds.get(lang,None))
if checkIn(lang,sapiVoices): r=self.sapi_unicode(sapiVoices[lang][0],ensure_unicode(text),fname,sapiVoices[lang][1],speed=sapiSpeeds.get(lang,None))
elif lang=="en":
p=os.popen(self.program+' -c 1 -w '+changeToDirOf(fname,1)+self.speedParam(sapiSpeeds.get(lang,None))+toNull,"w") # (can specify mono but can't specify sample rate if it wasn't mentioned in sapiVoices - might make en synth-cache bigger than necessary but otherwise no great problem)
p.write(text+"\n")
......@@ -222,7 +264,7 @@ class PttsSynth(Synth):
else: r=0 # shouldn't get here
os.chdir(oldcwd)
assert not r,"ptts.exe failed"
d = sapi_sox_bug_workaround(read(fname)); open(fname,"wb").write(d)
d = sapi_sox_bug_workaround(read(fname)); write(fname,d)
if cygwin: os.system("chmod -x '"+fname+"'")
return fname
def preparePinyinPhrase(self,pinyin):
......@@ -256,9 +298,9 @@ class PttsSynth(Synth):
kVal="_%d" % count ; count += 1 # better not make kVal too long otherwise the voice can insert awkward pauses
dicWrite.append('"%s","%s","p"\r\n' % (kVal,p2))
rVal.append(p.replace(p2,kVal)) # (leave in full stops etc; assumes p2 is a substring of p, which is why hyphens are taken out before stripPunc)
open(self.lily_file,"wb").write(''.join(dicWrite))
write(self.lily_file,''.join(dicWrite))
return ''.join(rVal).replace('@','') # (WITHOUT spaces, otherwise pauses far too much)
def restore_lily_dict(self): open(self.lily_file,"wb").write(self.old_lily_data) # done ASAP rather than on finalise, because need to make sure it happens (don't leave the system in an inconsistent state for long)
def restore_lily_dict(self): write(self.lily_file,self.old_lily_data) # done ASAP rather than on finalise, because need to make sure it happens (don't leave the system in an inconsistent state for long)
def sapi_sox_bug_workaround(wavdata):
# SAPI writes an 'EVNT' block after the sound data, and some versions of sox don't recognise this. NB this hack is not very portable (relies on SAPI5 guaranteeing to write exactly one EVNT chunk and the bytes 'EVNT' never occur inside it, otherwise would need proper parsing)
f=wavdata.rfind("EVNT")
......@@ -268,13 +310,14 @@ py_final_letters="aeginouvrAEGINOUVR:" # (don't just pick up on tone numbers, bu
def sort_out_pinyin_3rd_tones(pinyin):
# Tone sandhi blocking rules: Need to stop 3rd-tones sortout at end of any 2-syllable word + "gei3 ni3" + "wo3 xiang3".
# Also need to stop at phrase breaks and any English word (or hanzi, although may get awkward cases with 3rd-tone hanzi mixed with pinyin, but that's no big worry as lily isn't too reliable anyway and with partials it'll be transliterated)
segments = [] ; thisSeg = "" ; syls = 0
def endsWithSpecialWordpair(segLower): return segLower.endswith("gei3 ni3") or segLower.endswith("gei3 wo3") or segLower.endswith("ni3 xiang3") or segLower.endswith("wo3 xiang3")
for c in pinyin:
if ord(c)>128 or c in ".,?;" or (c==" " and syls==2) or endsWithSpecialWordpair(thisSeg.lower()):
segments.append(thisSeg) ; thisSeg="" ; syls = 0
elif c==" ": syls = 0
elif c in "12345": syls += 1
segments = [] ; thisSeg = B("") ; syls = 0 ; pinyin=B(pinyin)
def endsWithSpecialWordpair(segLower): return segLower.endswith(B("gei3 ni3")) or segLower.endswith(B("gei3 wo3")) or segLower.endswith(B("ni3 xiang3")) or segLower.endswith(B("wo3 xiang3"))
for i in xrange(len(pinyin)):
c = pinyin[i:i+1]
if ord(c)>128 or c in B(".,?;") or (c==B(" ") and syls==2) or endsWithSpecialWordpair(thisSeg.lower()):
segments.append(thisSeg) ; thisSeg=B("") ; syls = 0
elif c==B(" "): syls = 0
elif c in B("12345"): syls += 1
thisSeg += c
segments.append(thisSeg)
# Now go for each segment
......@@ -282,19 +325,19 @@ def sort_out_pinyin_3rd_tones(pinyin):
for seg in segments:
i=0
while i<len(seg):
while i<len(seg) and seg[i] not in '12345': i+=1
if i<len(seg) and seg[i]=='3' and i and seg[i-1] in py_final_letters:
while i<len(seg) and seg[i:i+1] not in B('12345'): i+=1
if i<len(seg) and seg[i:i+1]==B('3') and i and seg[i-1:i] in B(py_final_letters):
toneToChange = i ; numThirdsAfter = 0
j = i
while True:
j += 1
while j<len(seg) and seg[j] not in '12345': j+=1
if j<len(seg) and seg[j]=='3' and seg[j-1] in py_final_letters: numThirdsAfter+=1
while j<len(seg) and seg[j:j+1] not in B('12345'): j+=1
if j<len(seg) and seg[j:j+1]==B('3') and seg[j-1:j] in B(py_final_letters): numThirdsAfter+=1
else: break
if numThirdsAfter % 2: seg=seg[:toneToChange]+'2'+seg[toneToChange+1:]
if numThirdsAfter % 2: seg=seg[:toneToChange]+B('2')+seg[toneToChange+1:]
i += 1
ret.append(seg)
return "".join(ret)
return B("").join(ret)
class FliteSynth(Synth):
def __init__(self): Synth.__init__(self)
......@@ -310,10 +353,10 @@ if winsound or mingw32 or cygwin: del FliteSynth.play # because on some (even hi
if macsound:
# See if we need to unpack eSpeak and/or set the variables
import commands
f=commands.getoutput("echo espeak*-OSX.zip")
f=getoutput("echo espeak*-OSX.zip")
if fileExists(f): unzip_and_delete(f)
f=commands.getoutput("echo espeak*/speak")
if not os.system("espeak*/speak64 -h >/dev/null 2>/dev/null"): os.system("mv espeak*/speak64 espeak*/speak") # because some newer macOS releases want 64bit-only apps, and we won't need the dual PPC/i386 one on those
f=getoutput("echo espeak*/speak")
if fileExists(f) and fileExists(f[:-5]+"espeak-data/phontab"):
os.environ["ESPEAK_DATA_PATH"]=os.getcwd()+cwd_addSep+f[:-6]
os.environ["PATH"]=os.getcwd()+cwd_addSep+f[:-6]+":"+os.environ["PATH"]
......@@ -338,22 +381,26 @@ espeak_language_aliases = { "cant":"zhy" }
class SimpleZhTransliterator(object): # if not got eSpeak on system
def can_transliterate(self,lang): return lang=="zh"
def transliterate(self,lang,text,forPartials=1,from_espeak=0):
if lang=="zh" and not text.find("</")>-1: # (not </ - don't do this if got SSML)
text = preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text))).encode("utf-8")
found=0
for t in text:
if ord(t)>=128:
found=1 ; break
if not found and text.lower()==fix_pinyin(text,[]): return text # don't need espeak
elif from_espeak: return [text] # This transliterate() and ESpeakSynth's transliterate() work together - don't call espeak if there aren't any special characters (this saves launching a lot of espeak processes unnecessarily when synthing from partials), but DO proceed if fix_pinyin changes something, as in this case we need to check for embedded en words so fix_pinyin doesn't add spurious 5's, + embedded letters etc.
elif not found: return fix_pinyin(text,[]) # No ESpeak on system and fix_pinyin needed to do something - best we can do is hope there aren't any embedded English words (because if there are, they'll have spurious 5's added)
def transliterate(self,lang,text,forPartials=1,for_espeak=0):
text = B(text)
if not lang=="zh": return text
if text.find(B("</")) > -1: return text # (SSML)
text = preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text))).replace(u'\u0144g','ng2').replace(u'\u0148g','ng3').replace(u'\u01f9g','ng4').encode("utf-8") # (ng2/3/4 substitution here because not all versions of eSpeak can do it)
found=0
for i in xrange(len(text)):
if ord(text[i:i+1])>=128:
found=1 ; break
if not found and text.lower()==fix_pinyin(text,[]): return text # don't need espeak if no non-ASCII (but DO need espeak if fix_pinyin changes something, as in this case we need to check for embedded en words so fix_pinyin doesn't add spurious 5's, + embedded letters etc)
elif for_espeak:
for s,r in [('\xc4\x80', '\xc4\x81'), ('\xc3\x81', '\xc3\xa1'), ('\xc7\x8d', '\xc7\x8e'), ('\xc3\x80', '\xc3\xa0'), ('\xc4\x92', '\xc4\x93'), ('\xc3\x89', '\xc3\xa9'), ('\xc4\x9a', '\xc4\x9b'), ('\xc3\x88', '\xc3\xa8'), ('\xc5\x8c', '\xc5\x8d'), ('\xc3\x93', '\xc3\xb3'), ('\xc7\x91', '\xc7\x92'), ('\xc3\x92', '\xc3\xb2')]: text = text.replace(LB(s),LB(r)) # work around espeak bug where capital pinyin letters with tone marks can result in bad transiterations
return [text] # as list so ESpeakSynth's transliterate_multiple will further process it
elif not found: return fix_pinyin(text,[]) # No ESpeak on system and fix_pinyin needed to do something - best we can do is hope there aren't any embedded English words (because if there are, they'll have spurious 5's added)
simpleZhTransliterator = SimpleZhTransliterator()
def shell_escape(text):
text = text.replace('\\','\\\\').replace('"','\\"')
if unix: text=text.replace("$","\\$").replace("`","\\`").replace("!","! ")
return '"'+text+'"'
text = B(text).replace(B('\\'),B('\\\\')).replace(B('"'),B('\\"'))
if unix: text=text.replace(B("$"),B("\\$")).replace(B("`"),B("\\`")).replace(B("!"),B("! "))
return B('"')+text+B('"')
espeakTranslitCacheFile = "espeak-translit-cache"+extsep+"bin" # TODO to advanced.txt?
class ESpeakSynth(Synth):
......@@ -361,11 +408,14 @@ class ESpeakSynth(Synth):
Synth.__init__(self)
self.languages = {} ; self.program=""
tryList = []
if riscos_sound and 'eSpeak$dir' in os.environ: tryList=[os.environ['eSpeak$dir']+'.espeak-dat',os.environ['eSpeak$dir']+'.espeak-data']
if riscos_sound:
if checkIn('eSpeak$Dir',os.environ):
os.system("RMEnsure speak 3.49 Run <eSpeak$Dir>.speakmod")
tryList=[os.environ['eSpeak$Dir']+'.espeak-dat',os.environ['eSpeak$Dir']+'.espeak-data']
elif winsound or mingw32: tryList=[programFiles+"\\eSpeak\\espeak-data"]
elif winCEsound: tryList=["\\espeak-data"] # Can't try \\Storage Card because our eSpeak compile can't cope with spaces (and quoting it does not work)
else:
tryList=[os.environ.get("HOME","")+"espeak-data","/usr/share/espeak-data","/usr/local/share/espeak-data"]
tryList=[os.environ.get("HOME","")+"espeak-data","/usr/share/espeak-data","/usr/local/share/espeak-data","/usr/lib/x86_64-linux-gnu/espeak-data"]
if cygwin: tryList.append(programFiles+"/eSpeak/espeak-data")
if os.environ.get("ESPEAK_DATA_PATH",""): tryList.insert(0,os.environ["ESPEAK_DATA_PATH"]+os.sep+"espeak-data")
langList = []
......@@ -375,6 +425,13 @@ class ESpeakSynth(Synth):
langList = os.listdir(self.place+os.sep+"voices")
except: self.place = None
if langList: break
if unix: # espeak might know where its data is
if not self.place:
versionLine = (filter(lambda x:x.strip(),os.popen("(speak --help||espeak --help) 2>/dev/null").read().split("\n"))+[""])[0]
if versionLine.find("Data at:")>=0:
self.place = versionLine[versionLine.index("Data at:")+8:].strip()
try: langList = os.listdir(self.place+os.sep+"voices")
except: self.place = None
for l in langList[:]:
if l in ["default","!v","mb"]: langList.remove(l)
elif isDirectory(self.place+os.sep+"voices"+os.sep+l):
......@@ -383,40 +440,42 @@ class ESpeakSynth(Synth):
else: self._add_lang(l,l)
self.theProcess = None
self.translitCache = {}
if pickle and fileExists(espeakTranslitCacheFile):
if self.place:
if pickle and fileExists(espeakTranslitCacheFile):
try: placeStat,tc = pickle.Unpickler(open(espeakTranslitCacheFile,"rb")).load()
except: placeStat,tc = (),{}
if placeStat==tuple(os.stat(self.place)): self.translitCache = tc # otherwise regenerate it because eSpeak installation has changed (TODO if you overwrite an existing _dict file in-place, it might not update the stat() of espeak-data and the cache might not be re-generated when it should; espeak's --compile seems ok though)
if self.place: self.place=self.place[:self.place.rindex(os.sep)] # drop the \espeak-data, so can be used in --path=
self.place=self.place[:self.place.rindex(os.sep)] # drop the \espeak-data, so can be used in --path=
def _add_lang(self,lang,fname):
lang,fname = S(lang),S(fname)
if "~" in lang: return # emacs backup files
self.languages[lang]=fname
for l in open(self.place+os.sep+"voices"+os.sep+fname).read(256).replace("\r","\n").split("\n"):
if l.startswith("language "):
l=l[9:].strip(wsp)
if not l==lang:
if l in espeak_language_aliases.values(): # aliasing to an alias - update it
for k,v in espeak_language_aliases.items():
if v==l: espeak_language_aliases[k]=lang
espeak_language_aliases[l] = lang
for l in open(self.place+os.sep+"voices"+os.sep+fname,"rb").read(256).replace(B("\r"),B("\n")).split(B("\n")):
if l.startswith(B("language ")):
l=bwspstrip(l[9:])
if not l==B(lang):
Sl = S(l)
if Sl in espeak_language_aliases.values(): # aliasing to an alias - update it
for k,v in list(espeak_language_aliases.items()):
if v==Sl: espeak_language_aliases[k]=lang
espeak_language_aliases[Sl] = lang
def describe_supported_languages(self):
ret=[]
items=self.languages.items() ; items.sort()
items=list(self.languages.items()) ; items.sort()
for k,v in items:
if "-" in k and not k=="zh-yue": continue # skip variants in the report (but do recognise them)
o=open(self.place+os.sep+"espeak-data"+os.sep+"voices"+os.sep+v)
line=""
o=open(self.place+os.sep+"espeak-data"+os.sep+"voices"+os.sep+v,'rb')
for t in range(10):
line=o.readline()
if line.find("name")>-1:
lname = line.split()[1].replace("_test","").replace("-test","").replace("-experimental","").replace("-expertimental","") # (delete the -test etc for more screen real-estate, as this is used only for explaining what the language abbreviations mean)
if line.find(B("name"))>=0:
lname = S(line.split()[1]).replace("_test","").replace("-test","").replace("-experimental","").replace("-expertimental","") # (delete the -test etc for more screen real-estate, as this is used only for explaining what the language abbreviations mean)
if not lname: continue
lname=lname[0].upper()+lname[1:]
ret.append(k+"="+lname)
break
return " ".join(ret)
def supports_language(self,lang): return espeak_language_aliases.get(lang,lang) in self.languages
def not_so_good_at(self,lang): return lang not in prefer_espeak
def supports_language(self,lang): return checkIn(espeak_language_aliases.get(lang,lang),self.languages)
def not_so_good_at(self,lang): return not checkIn(lang,prefer_espeak)
def works_on_this_platform(self):
if len(self.languages.items())==0: return 0
if winCEsound:
......@@ -428,7 +487,7 @@ class ESpeakSynth(Synth):
elif cygwin: toTry=[programFiles+"/eSpeak/command_line/espeak.exe"]
else: toTry = []
if toTry: # windows or cygwin
if "ESPEAK_DATA_PATH" in os.environ:
if checkIn("ESPEAK_DATA_PATH",os.environ):
toTry.insert(0,os.environ["ESPEAK_DATA_PATH"]+os.sep+"espeak.exe")
toTry.insert(0,os.environ["ESPEAK_DATA_PATH"]+os.sep+"command_line"+os.sep+"espeak.exe")
for t in toTry:
......@@ -439,9 +498,8 @@ class ESpeakSynth(Synth):
return False
else: # not windows or cygwin
self.program="speak"
if riscos_sound: return True # we've already confirmed <eSpeak$dir> works in the constructor
import commands
loc=commands.getoutput("locale -a|grep -i 'utf-*8$'|head -1").strip(wsp)
if riscos_sound: return True # we've already confirmed <eSpeak$Dir> works in the constructor (if it doesn't, we'll just have an empty language list)
loc=wspstrip(getoutput("locale -a 2>/dev/null|grep -i 'utf-*8$'|head -1"))
if loc: loc="LC_CTYPE="+loc+" " # in case espeak can't find a utf-8 locale by itself
self.program=loc+"speak"
if got_program("speak"): return True
......@@ -449,11 +507,13 @@ class ESpeakSynth(Synth):
self.program = loc+"espeak"
return got_program("espeak")
def guess_length(self,lang,text):
if text.find("</")>-1: # might be SSML - don't count inside <...>
text=B(text)
if text.find(B("</"))>=0: # might be SSML - don't count inside <...>
l=inSsml=0
for c in text:
if c=="<": inSsml=1
elif c==">": inSsml=0
for i in xrange(len(text)):
c = text[i:i+1]
if c==B("<"): inSsml=1
elif c==B(">"): inSsml=0
elif not inSsml: l += 1
else: l=len(text)
latency = 0
......@@ -464,7 +524,7 @@ class ESpeakSynth(Synth):
t = time.time()
self.play("en","")
self.latency = time.time() - t # 2secs on eeePC Ubuntu 11.10, mostly AFTER the utterance
if self.latency > 0.5: sys.stderr.write("espeak_pipe_through latency is "+str(int(self.latency*10)/10.0)+"\n")
if self.latency > 0.5: show_info("espeak_pipe_through latency is "+str(int(self.latency*10)/10.0)+"\n",True)
latency = self.latency
return quickGuess(l,12)+latency
def can_transliterate(self,lang): return espeak_language_aliases.get(lang,lang) in ["zh","zhy","zh-yue"] and not riscos_sound # TODO it's OK on RISC OS if the eSpeak version is recent enough to do --phonout=filename; TODO aliases for zhy (but not usually a problem as can_transliterate is called only for preference)
......@@ -473,12 +533,12 @@ class ESpeakSynth(Synth):
time.sleep(0.3) # 0.2 not always long enough for transliterations (get empty output file if try to read too soon, then loop waiting for it to have contents)
return self.winCE_wait(expectedOutputFile,infileToDel)
def winCE_start(self,parameters):
s = ShellExecuteInfo(60,File=u""+self.program,Parameters=u"--path="+self.place+" "+parameters,fMask=0x40)
s = ShellExecuteInfo(60,File=ensure_unicode(self.program),Parameters=u"--path="+self.place+" "+parameters,fMask=0x40)
ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(s))
self.hProcess = s.hProcess # TODO check it's not NULL (failed to run)
def winCE_wait(self,expectedOutputFile,infileToDel=None,needDat=1):
# won't always work: if app and not app.Label["text"].strip(): app.setLabel("Waiting for eSpeak") # in case it doesn't produce output
ctypes.cdll.coredll.WaitForSingleObject(self.hProcess,4294967295) # i.e. 0xFFFFFFFF but that throws up a warning on Python 2.3
ctypes.cdll.coredll.WaitForSingleObject(self.hProcess,long(65535)*long(65537)) # i.e. 0xFFFFFFFF but that throws up a warning on Python 2.3; Python 2.1 won't accept 4294967295 without L but Python 3 says syntax error if L, so need to multiply
ctypes.cdll.coredll.CloseHandle(self.hProcess)
# In some rare circumstances, that command won't wait (e.g. process hadn't started despite the fact we delayed), so check the output files also.
# (Leave WaitForSingleObject in as it could save some CPU cycles / potential OS crashes on some WinCE versions)
......@@ -487,11 +547,11 @@ class ESpeakSynth(Synth):
if firstIter: firstIter -= 1
else: time.sleep(0.2),check_for_interrupts() # (latter needed in case it gets stuck)
try:
if needDat: dat=read(u""+expectedOutputFile)
else: dat=open(u""+expectedOutputFile).read(8)
if needDat: dat=read(ensure_unicode(expectedOutputFile))
else: dat=open(ensure_unicode(expectedOutputFile)).read(8)
except: continue # error on trying to read output
if not dat: continue # output read as empty
if expectedOutputFile.endswith(dotwav) and (len(dat)<8 or dat[6:8]=="\xff\x7f"): continue # length field not yet written
if expectedOutputFile.endswith(dotwav) and (len(dat)<8 or dat[6:8]==LB("\xff\x7f")): continue # length field not yet written
# (TODO how could we make sure a long transliteration has finished, if the OS lets us open the file before done and if WaitForSingleObject doesn't work?)
if not firstIter: time.sleep(0.2) # just in case
if infileToDel:
......@@ -500,7 +560,7 @@ class ESpeakSynth(Synth):
return dat
def update_translit_cache(self,lang,textList): # forPartials=1 assumed
if not lang=="zh": return # TODO if expand 'transliterate' to do other languages, make sure to update this also, and the cache format
if self.translitCache: textList=filter(lambda x:x not in self.translitCache, textList)
if self.translitCache: textList=filter(lambda x,self=self:not checkIn(x,self.translitCache), textList)
step = 1000 # should be about right?
for i in range(0,len(textList),step):
tl = textList[i:i+step]
......@@ -512,56 +572,86 @@ class ESpeakSynth(Synth):
try: pickle.Pickler(open(espeakTranslitCacheFile,"wb"),-1).dump((tuple(os.stat(self.place+os.sep+"espeak-data")),self.translitCache))
except IOError: pass # 'permission denied' is ok
def transliterate(self,lang,text,forPartials=1):
if lang=="zh" and text in self.translitCache: return self.translitCache[text] # (TODO add "and forPartials"? but don't need to bother with this extra condition on slow systems)
if lang=="zh" and checkIn(text,self.translitCache): return self.translitCache[text] # (TODO add "and forPartials"? but don't need to bother with this extra condition on slow systems)
return self.transliterate_multiple(lang,[text],forPartials)[0] # and don't cache it - could be experimental, and we don't want cache to grow indefinitely
if unix:
def check_dicts(self,lang,txt):
if not hasattr(self,"dictsChecked"): self.dictsChecked = {}
if checkIn(lang,self.dictsChecked) or not lang in ["zh","zhy","ru"]: return
if type(txt)==list: txt=B("").join(txt)
if re.match(B("^[ -~]*$"),txt): return # don't need to warn about dictionaries if we were given all-ASCII input (TODO: and tone marks?)
if filelen(self.place+os.sep+"espeak-data"+os.sep+lang+"_dict")<100000: show_warning("Warning: the eSpeak on this system has only a short dictionary for language '"+lang+"' - please install the Additional Data at espeak.sourceforge.net/data")
# TODO: Ubuntu 18.04's espeak-data package does NOT include the additional data; IS included by espeak-ng-data; on unix prefer the espeak-ng command and ask it for its own data path?
self.dictsChecked[lang]=1
else:
def check_dicts(self,lang,txt): pass
def transliterate_multiple(self,lang,textList,forPartials=1,keepIndexList=0):
# Call eSpeak once for multiple transliterations, for greater efficiency (especially on systems where launching a process is expensive e.g. WinCE).
# Note: Don't make textList TOO long, because the resulting data must fit on the (RAM)disk and in memory.
retList = [] ; write_to_espeak = [] ; indexList = []
split_token = "^^^" # must be something not defined in the _rules files
textList = map(lambda x:B(x), textList)
self.check_dicts(lang,textList)
for text in textList: # DON'T escape_jyutping (treat as en words)
if lang=="zh":
if keepIndexList: # making the cache - can we go a bit faster?
try: t = unicode(text,"ascii") # if no utf, know is OK (but ONLY if keepIndexList, as the result is imprecise)
except UnicodeDecodeError: t = simpleZhTransliterator.transliterate(lang,text,from_espeak=1)
else: t = simpleZhTransliterator.transliterate(lang,text,from_espeak=1)
except UnicodeDecodeError: t = simpleZhTransliterator.transliterate(lang,text,for_espeak=1)
else: t = simpleZhTransliterator.transliterate(lang,text,for_espeak=1)
else: t=[fix_compatibility(ensure_unicode(text)).encode("utf-8")]
if t and not riscos_sound: # same TODO as above re RISC OS
if type(t)==type([]):
indexList.append(len(retList))
retList.append(None) # result not filled in yet
if lang=="zh": tt=pinyin_uColon_to_V(t[0].replace("-","/")) # NB fix_compatibility has already been done (as has preprocess_chinese_numbers), by simpleZhTransliterator above
if lang=="zh": tt=pinyin_uColon_to_V(t[0].replace(B("-"),B("/"))) # NB fix_compatibility has already been done (as has preprocess_chinese_numbers), by simpleZhTransliterator above
else: tt=t[0]
write_to_espeak.append(fix_commas(tt).replace(split_token," "))
write_to_espeak.append(fix_commas(tt).replace(B(split_token),B(" ")))
# (replacing - with / because espeak zh voice treats / as a silent word separator but - is ignored; - is used as a word separator in MeiLing etc. so if you want to write the hanzi for wei2ren2 but you want it to be wei4+ren2, you can hack in this way. TODO document?)
else: retList.append(t)
else: retList.append(None)
if keepIndexList: self.lastIndexList = indexList
if not indexList: return retList
overruns = [] # elements that need to be merged with their following elements (duplicates allowed because indices change after each merge), used when we're transliterating very long texts (not usually as part of a lesson) because some versions of espeak truncate very long lines
i = 0
while i < len(write_to_espeak):
if len(write_to_espeak[i]) > 500:
x = write_to_espeak[i].decode('utf-8')
write_to_espeak[i] = x[:150].encode('utf-8')
write_to_espeak.insert(i+1,x[150:].encode('utf-8'))
overruns.append(i-len(overruns))
i += 1
fname = os.tempnam()
open(fname,"w").write((".\n"+split_token+" ").join(write_to_espeak))
writeB(open(fname,"w"),(B(".\n")+B(split_token)+B(" ")).join(write_to_espeak))
oldcwd=os.getcwd()
if winCEsound:
translit_out = os.tempnam()
data=self.winCE_run(' -v%s -q -X -f %s --phonout=%s' % (espeak_language_aliases.get(lang,lang),fname,translit_out),translit_out)
os.remove(translit_out)
else: data=os.popen(self.program+' -v%s -q -X -f %s%s' % (espeak_language_aliases.get(lang,lang),changeToDirOf(fname,1),cond(unix," 2>&1","")),"rb").read() # popen2 might not work, so had better do it this way:
elif checkIn(lang,espeak_preprocessors): data=readB(os.popen('%s < %s | %s -v%s -q -X %s' % (espeak_preprocessors[lang],changeToDirOf(fname,1),self.program,espeak_language_aliases.get(lang,lang),cond(unix," 2>&1","")),popenRB))
else: data=readB(os.popen('%s -v%s -q -X -f %s%s' % (self.program,espeak_language_aliases.get(lang,lang),changeToDirOf(fname,1),cond(unix," 2>&1","")),popenRB)) # popen2 might not work, so had better do it this way
os.chdir(oldcwd) ; os.remove(fname)
data = data.replace("\r\n","\n").split("\nTranslate '"+split_token+"'\n")
if len(data)==2*len(indexList)-1:
data = data.replace(B("\r\n"),B("\n")).split(B("\nTranslate '")+B(split_token)+B("'\n"))
if len(data)==2*(len(indexList)+len(overruns))-1:
# split points are doubled - better take every ODD item. (NB the text in between is NOT necessarily blank - espeak can flush its sentence cache there)
d2 = []
for i in xrange(0,len(data),2): d2.append(data[i])
data = d2
for o in overruns:
data[o] += data[o+1]
del data[o+1]
if not len(data)==len(indexList):
if not (winsound or macsound): show_warning("Warning: eSpeak's transliterate returned wrong number of items (%d instead of %d). Falling back to separate runs for each item (slower)." % (len(data),len(indexList)))
return None
for index,dat in zip(indexList,data):
en_words={} # any en words that espeak found embedded in the text
r=[] ; lastWasBlank=False
delete_last_r_if_blank = 0
delete_last_r_if_blank = 0 ; appendNext = 0
thisgroup_max_priority,thisgroup_enWord_priority = 0.5,0
for l in dat.strip(wsp).split("\n"):
for l in bwspstrip(dat).split(B("\n")):
if appendNext: # (see below)
r.append(l[l.index(B("["))+1:l.index(B("]"))])
appendNext = 0 ; continue
# print "Debugger:",l.strip()
# get en_words for fix_pinyin (and for making sure we embed them in cant)
lWords = l.split()
if lWords: int0 = intor0(lWords[0])
......@@ -569,40 +659,46 @@ class ESpeakSynth(Synth):
if int0:
if int0 > thisgroup_max_priority:
thisgroup_max_priority = int0
if lWords[-1]=="[_^_]": thisgroup_enWord_priority = int0 # so far it looks like this is going to be an English word
if lWords[-1]==B("[_^_]"): thisgroup_enWord_priority = int0 # so far it looks like this is going to be an English word
else: # a split between the groups
if thisgroup_enWord_priority == thisgroup_max_priority: # the choice with the highest priority was the one containing the [_^_] to put the word into English
en_words[r[-1]]=1
thisgroup_max_priority,thisgroup_enWord_priority = 0.5,0
# end of getting en_words
if lang=="zh" and not lastWasBlank and r and (l.startswith("Replace") or l.startswith("Translate") or l.startswith("Found")): r[-1]+="," # (because not-blank is probably the line of phonemes)
elif not lang=="zh" and l.startswith("_|") and r: r[-1] += "," # works for zh-yue
if lang=="zh" and r and ((not lastWasBlank and (l.startswith(B("Replace")) or l.startswith(B("Translate")) or l.startswith(B("Found")))) or l.find(B("';'"))>1 or l.find(B("','"))>1): r[-1]+=B(",") # (because not-blank is probably the line of phonemes)
elif not lang=="zh" and l.startswith(B("_|")) and r: r[-1] += B(",") # works for zh-yue
if delete_last_r_if_blank and not l: r=r[:-1] # "Translate" followed by blank line is probably corner-brackets or something; don't want that confusing the transliteration (especially if it's for partials)
delete_last_r_if_blank = 0
foundLetter=0
if l.startswith("Translate "):
toAppend=l[l.index("'")+1:-1].replace("\xc3\xbc","v")
if not (toAppend in en_words and r and toAppend==r[-1]):
if l.startswith(B("Translate ")):
toAppend=l[l.index(B("'"))+1:-1].replace(LB("\xc3\xbc"),B("v"))
if toAppend==LB("\xc2\xa0"): continue # stray no-break space (don't let this interfere with being able to do partials)
if not (checkIn(toAppend,en_words) and r and toAppend==r[-1]):
# TODO what about partial English words? e.g. try "kao3 testing" - translate 'testing' results in a translate of 'test' also (which assumes it's already in en mode), resulting in a spurious word "test" added to the text box; not sure how to pick this up without parsing the original text and comparing with the Replace rules that occurred
r.append(toAppend)
delete_last_r_if_blank = 1
delete_last_r_if_blank = 1 # this depends on original Jonathan Duddington eSpeak and is not compatible with eSpeak NG which does not always give us blank lines. TODO: if want eSpeak NG compatibility (which might be a good idea as mainstream GNU/Linux distributions now ship eSpeak NG and just call it "espeak"), we might want to pick up on THIS l ending with B("[_^_]") and next Translate being an identical syllable. That might not be the only problem.
else: en_words[toAppend]=1
else: # not Translate
if lang=="zh" and l.startswith("Found: ") and ((l[7]==l[9]=="'" and "a"<=l[8]<="z") or (l[8]==" " and "a"<=l[7]<="z")): # an alphabetical letter - we can say this as a Chinese letter and it should be compatible with more partials-based synths. But DON'T do this if going to give it to a unit-selection synth - 'me1' and 'ne1' don't have hanzi and some synths will have difficulty saying them.
if l[8]==' ': letter=l[7]
else: letter=l[8]
if forPartials: r.append("a1 bo1 ci1 de1 e1 fou1 ge1 he1 yi1 ji1 ke1 le1 me1 ne1 wo1 po1 qi1 ri4 si1 te4 yu1 wei4 wu1 xi1 ye1 zi1".split()[ord(letter)-ord('a')])
else: r.append(letter)
if lang=="zh" and l.startswith(B("Found: ")) and ((l[7:8]==l[9:10]==B("'") and B("a")<=l[8:9]<=B("z")) or (l[8:9]==B(" ") and B("a")<=l[7:8]<=B("z"))): # an alphabetical letter - we can say this as a Chinese letter and it should be compatible with more partials-based synths. But DON'T do this if going to give it to a unit-selection synth - 'me1' and 'ne1' don't have hanzi and some synths will have difficulty saying them.
if l[8:9]==B(' '): letter=l[7:8]
else: letter=l[8:9]
if forPartials: r.append(B("a1 bo1 ci1 de1 e1 fou1 ge1 he1 yi1 ji1 ke1 le1 me1 ne1 wo1 po1 qi1 ri4 si1 te4 yu1 wei4 wu1 xi1 ye1 zi1").split()[ord(letter)-ord('a')])
else: # a letter in something we're transliterating for a pinyin-driven unit-selection synth
r.append(letter)
en_words[r[-1]]=1
foundLetter = 1
elif not lang=="zh" and l.startswith("Found: ") and (ord(l[7])>127 or (l[7]=="'" and ord(l[8])>127)): # (espeak 1.40 puts in l[7], 1.44 surrounds in quotes)
r.append(l[l.index("[")+1:l.index("]")])
lastWasBlank=(l.startswith("Replace") or not l or foundLetter) # (take 'Replace' lines as blank, so 'Translate' doesn't add a second comma. ditto letters thing.)
while r and r[-1] and r[-1][-1]==',': r[-1]=r[-1][:-1] # strip any trailing commas
if lang=="zh": retList[index]=fix_pinyin(" ".join(r),en_words)
else: retList[index]=" ".join(r)
elif not lang=="zh" and l.startswith(B("Found: ")) and (ord(l[7:8])>127 or (l[7:8]==B("'") and ord(l[8:9])>127)): # (espeak 1.40 puts in l[7], 1.44 surrounds in quotes)
if not B("[") in l: appendNext=1 # probably a spurious newline in the Found quote (espeak 1.48.03)
else: r.append(l[l.index(B("["))+1:l.index(B("]"))])
lastWasBlank=(l.startswith(B("Replace")) or not l or foundLetter) # (take 'Replace' lines as blank, so 'Translate' doesn't add a second comma. ditto letters thing.)
while r and r[-1] and r[-1][-1:]==B(','): r[-1]=r[-1][:-1] # strip any trailing commas
if lang=="zh": retList[index]=fix_pinyin(B(" ").join(r),en_words)
else: retList[index]=B(" ").join(r)
return retList
def escape_jyutping(self,text): return re.sub(r"([abcdefghjklmnopstuwz][a-z]*[1-7])",r"[[\1]]",text) # TODO what if user already escaped it?
def escape_jyutping(self,text): return re.sub(B(r"([abcdefghjklmnopstuwz][a-z]*[1-7])"),B(r"[[\1]]"),B(text)) # TODO what if user already escaped it?
def play(self,lang,text):
lang = S(lang)
self.check_dicts(lang,text)
if espeak_language_aliases.get(lang,lang) in ["zhy","zh-yue"]: text=self.escape_jyutping(preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text)),isCant=1).encode("utf-8"))
elif lang=="zh": text=fix_commas(preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text))).encode('utf-8'))
if winCEsound: # need to play via makefile, and careful not to leave too many tempfiles or take too long
......@@ -629,10 +725,11 @@ class ESpeakSynth(Synth):
return ret
elif unix or winsound or mingw32 or cygwin:
# Windows command line is not always 100% UTF-8 safe, so we'd better use a pipe. Unix command line OK but some espeak versions have a length limit. (No pipes on riscos.)
p=os.popen(self.program+cond(text.find("</")>-1," -m","")+' -v%s -a%d %s' % (espeak_language_aliases.get(lang,lang),100*soundVolume,espeak_pipe_through),"wb")
p.write(text.replace(". ",".\n")+"\n") ; return p.close() # (see comment below re adding newlines)
else: return system(self.program+cond(text.find("</")>-1," -m","")+' -v%s -a%d %s %s' % (espeak_language_aliases.get(lang,lang),100*soundVolume,shell_escape(text),espeak_pipe_through)) # (-m so accepts SSML tags)
p=os.popen(self.program+cond(B(text).find(B("</"))>=0," -m","")+' -v%s -a%d %s' % (espeak_language_aliases.get(lang,lang),100*soundVolume,espeak_pipe_through),"w")
writeB(p,B(text).replace(B(". "),B(".\n"))+B("\n")) ; return p.close() # (see comment below re adding newlines)
else: return system(B(self.program+cond(B(text).find(B("</"))>=0," -m","")+' -v%s -a%d ' % (espeak_language_aliases.get(lang,lang),100*soundVolume))+shell_escape(text)+B(' '+espeak_pipe_through)) # (-m so accepts SSML tags)
def makefile(self,lang,text,is_winCEhint=0):
self.check_dicts(lang,text)
if espeak_language_aliases.get(lang,lang) in ["zhy","zh-yue"]: text=self.escape_jyutping(preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text)),isCant=1).encode("utf-8"))
elif lang=="zh": text=fix_commas(preprocess_chinese_numbers(fix_compatibility(ensure_unicode(text))).encode('utf-8'))
if hasattr(self,"winCEhint"): # waiting for a previous async one that was started with is_winCEhint=1
......@@ -642,7 +739,7 @@ class ESpeakSynth(Synth):
return fname
fname = os.tempnam()+dotwav
oldcwd=os.getcwd()
sysCommand = cond(winCEsound,"",self.program)+cond(text.find("</")>-1," -m","")+' -v%s -w %s%s' % (espeak_language_aliases.get(lang,lang),cond(unix,"/dev/stdout|cat>",""),changeToDirOf(fname,1))
sysCommand = cond(winCEsound,"",self.program)+cond(B(text).find(B("</"))>=0," -m","")+' -v%s -w %s%s' % (espeak_language_aliases.get(lang,lang),cond(unix,"/dev/stdout|cat>",""),changeToDirOf(fname,1))
# (Unix use stdout and cat because some espeak versions truncate the output file mid-discourse)
# (eSpeak wavs are 22.05k 16-bit mono; not much point down-sampling to 16k to save 30% storage at expense of CPU)
if winsound or mingw32: os.popen(sysCommand,"w").write(text+"\n") # must pipe the text in
......@@ -656,9 +753,9 @@ class ESpeakSynth(Synth):
else:
# we can make it asynchronously (still need to pipe)
# (add end-of-sentence newlines due to short line buffer in some versions of espeak)
sysCommand='echo '+shell_escape(text.replace(". ",".\n"))+'|'+sysCommand
sysCommand=B('echo ')+shell_escape(B(text).replace(B(". "),B(".\n")))+B('|')+B(sysCommand)
if not self.theProcess: self.theProcess = os.popen("/bin/bash","w")
self.theProcess.write('cd "'+os.getcwd()+'"\n'+sysCommand+"\n")
writeB(self.theProcess,B('cd "'+os.getcwd()+'"\n')+sysCommand+B("\n"))
self.theProcess.flush()
os.chdir(oldcwd)
return fname
......@@ -669,12 +766,12 @@ class ESpeakSynth(Synth):
def fix_commas(text):
# some versions of espeak won't honour ordinary commas in among Chinese text if the ordinary commas don't have spaces after
# also put 2 spaces after full stop, and make sure capitalised
i=0
i=0 ; text=B(text)
while i<len(text)-1:
if text[i] in '.,?;!':
tRest = text[i+1:].strip(wsp)
if tRest and (ord(tRest[0])>=128 or 'a'<=tRest[0].lower()<='z'):
text=text[:i+1]+cond(text[i] in ".?!"," "+tRest[0].upper()," "+tRest[0])+tRest[1:]
if text[i:i+1] in B('.,?;!'):
tRest = bwspstrip(text[i+1:])
if tRest and (ord(tRest[:1])>=128 or B('a')<=tRest[:1].lower()<=B('z')):
text=text[:i+1]+cond(text[i:i+1] in B(".?!"),B(" ")+tRest[:1].upper(),B(" ")+tRest[:1])+tRest[1:]
i+=1
return text
......@@ -683,33 +780,33 @@ def fix_pinyin(pinyin,en_words):
if en_words:
ret=[]
def stripPunc(w):
i=0 ; j=len(w) ; w=w.lower()
while i<len(w) and not 'a'<=w[i]<='z': i+=1
while j>1 and not ('a'<=w[j-1]<='z' or '1'<w[j-1]<='5'): j-=1
w=B(w) ; i=0 ; j=len(w) ; w=w.lower()
while i<len(w) and not B('a')<=w[i:i+1]<=B('z'): i+=1
while j>1 and not (B('a')<=w[j-1:j]<=B('z') or B('1')<w[j-1:j]<=B('5')): j-=1
return w[i:j]
for w in pinyin.split():
if stripPunc(w) in en_words: ret.append(w)
if checkIn(stripPunc(w),en_words): ret.append(w)
else: ret.append(fix_pinyin(w,[]))
return ' '.join(ret)
return B(' ').join(ret)
i=0
pinyin=pinyin_uColon_to_V(pinyin)+"@@@" # (includes .lower; @@@ for termination)
pinyin=pinyin_uColon_to_V(pinyin)+B("@@@") # (includes .lower; @@@ for termination)
while i<len(pinyin):
if pinyin[i] in "12345":
if pinyin[i:i+1] in B("12345"):
moveBy=0
if pinyin[i+1] in "iuv": moveBy=1 # these are never initial letters
elif pinyin[i+1]=="o" and not pinyin[i+2] in "u12345": moveBy=1 # "o" and "ou" are valid syllables, but a number before "o" is likely to be premature especially if the "o" is not itself followed by a number (or "u")
elif pinyin[i+1:i+3]=="ng" and not pinyin[i+3] in "aeiouv": moveBy=2 # before an -ng, but NOT before a -n g-(vowel)
elif pinyin[i+1] in "nr" and not pinyin[i+2] in "aeiouv" and not (pinyin[i+1]=="r" and i and not pinyin[i-1]=="e") and not pinyin[i+1:i+3]=="r5": moveBy=1 # before -n or -r (as final not as initial) (but NB -r is only on -er, otherwise it's an r5. and if it already says r5, leave it.)
if moveBy: pinyin=pinyin[:i]+pinyin[i+1:i+moveBy+1]+pinyin[i]+pinyin[i+moveBy+1:]
if pinyin[i+1:i+2] in B("iuv"): moveBy=1 # these are never initial letters
elif pinyin[i+1:i+2]==B("o") and not pinyin[i+2:i+3] in B("u12345"): moveBy=1 # "o" and "ou" are valid syllables, but a number before "o" is likely to be premature especially if the "o" is not itself followed by a number (or "u")
elif pinyin[i+1:i+3]==B("ng") and not pinyin[i+3:i+4] in B("aeiouv"): moveBy=2 # before an -ng, but NOT before a -n g-(vowel)
elif pinyin[i+1:i+2] in B("nr") and not pinyin[i+2:i+3] in B("aeiouv") and not (pinyin[i+1:i+2]==B("r") and i and not pinyin[i-1:i]==B("e")) and not pinyin[i+1:i+3]==B("r5"): moveBy=1 # before -n or -r (as final not as initial) (but NB -r is only on -er, otherwise it's an r5. and if it already says r5, leave it.)
if moveBy: pinyin=pinyin[:i]+pinyin[i+1:i+moveBy+1]+pinyin[i:i+1]+pinyin[i+moveBy+1:]
i+=1
i=0
while i<len(pinyin): # check for missing 5's
if (pinyin[i] in "aeiouvr" and pinyin[i+1] not in "aeiouv12345") or (ord('a')<=ord(pinyin[i])<=ord('z') and not (ord("a")<=ord(pinyin[i+1])<=ord("z") or pinyin[i+1] in "12345")): # ("alnum and next is not alnum" is not strictly necessary, but we do need to add 5's after en-like words due to 'fix_pinyin(t)==t' being used as a do-we-need-proper-translit. condition in SimpleZhTransliterator, otherwise get problems with things like "c diao4" going to eSpeak when it could go to partials-with-letter-substitutions)
if pinyin[i+1:i+3]=="ng" and not pinyin[i+3] in "aeiouv":
if pinyin[i+3] not in "12345": pinyin=pinyin[:i+3]+"5"+pinyin[i+3:]
elif (pinyin[i+1]=="n" or pinyin[i:i+2]=="er") and not pinyin[i+2] in "aeiouv" and not pinyin[i]=="r":
if pinyin[i+2] not in "12345": pinyin=pinyin[:i+2]+"5"+pinyin[i+2:]
else: pinyin=pinyin[:i+1]+"5"+pinyin[i+1:]
if (pinyin[i:i+1] in B("aeiouvr") and pinyin[i+1:i+2] not in B("aeiouv12345")) or (ord('a')<=ord(pinyin[i:i+1])<=ord('z') and not (ord("a")<=ord(pinyin[i+1:i+2])<=ord("z") or pinyin[i+1:i+2] in B("12345"))): # ("alnum and next is not alnum" is not strictly necessary, but we do need to add 5's after en-like words due to 'fix_pinyin(t)==t' being used as a do-we-need-proper-translit. condition in SimpleZhTransliterator, otherwise get problems with things like "c diao4" going to eSpeak when it could go to partials-with-letter-substitutions)
if pinyin[i+1:i+3]==B("ng") and not pinyin[i+3:i+4] in B("aeiouv"):
if pinyin[i+3:i+4] not in B("12345"): pinyin=pinyin[:i+3]+B("5")+pinyin[i+3:]
elif (pinyin[i+1:i+2]==B("n") or pinyin[i:i+2]==B("er")) and not pinyin[i+2:i+3] in B("aeiouv") and not pinyin[i:i+1]==B("r"):
if pinyin[i+2:i+3] not in B("12345"): pinyin=pinyin[:i+2]+B("5")+pinyin[i+2:]
else: pinyin=pinyin[:i+1]+B("5")+pinyin[i+1:]
i+=1
return pinyin[:-3] # remove the @@'s
......@@ -722,8 +819,8 @@ def remove_tone_numbers(utext): # for hanzi_and_punc to take out numbers that ca
def preprocess_chinese_numbers(utext,isCant=0): # isCant=1 for Cantonese, 2 for hanzi (and if 1 or 2, also assumes input may be jyutping not just pinyin)
# Hack for reading years digit by digit:
for year in ["nian2",u"\u5e74"]: # TODO also " nian2" to catch that? what of multiple spaces?
while utext.find(year)>=4 and 1200 < intor0(utext[utext.find(year)-4:utext.find(year)]) < 2300: # TODO is that range right?
yrStart = utext.find(year)-4
while utext.find(year)>=4 and 1200 < intor0(utext[utext.index(year)-4:utext.index(year)]) < 2300: # TODO is that range right?
yrStart = utext.index(year)-4
utext = utext[:yrStart] + " ".join(list(utext[yrStart:yrStart+4]))+" "+utext[yrStart+4:]
# End of hack for reading years
i=0
......@@ -787,9 +884,9 @@ def espeak_stdout_works():
except ValueError: return False
def espeak_volume_ok():
# if has "zh", should be recent enough
return "zh" in ESpeakSynth().languages
if unix and not macsound and not (oss_sound_device=="/dev/sound/dsp" or oss_sound_device=="/dev/dsp"):
if playProgram=="aplay" and espeak_stdout_works(): espeak_pipe_through="--stdout|aplay -q" # e.g. NSLU2
return checkIn("zh",ESpeakSynth().languages)
if wavPlayer_override or (unix and not macsound and not (oss_sound_device=="/dev/sound/dsp" or oss_sound_device=="/dev/dsp")):
if wavPlayer=="aplay" and espeak_stdout_works(): espeak_pipe_through="--stdout|aplay -q" # e.g. NSLU2
else: del ESpeakSynth.play # because we have no way of sending it to the alternative device, so do it via a file
if hasattr(FliteSynth,"play"): del FliteSynth.play
if hasattr(ESpeakSynth,"play") and (soundVolume<0.04 or (soundVolume<0.1 and not espeak_volume_ok()) or soundVolume>2): del ESpeakSynth.play # old versions of espeak are not very good at less than 10% volume, so generate offline and use sox
......@@ -835,19 +932,78 @@ class FestivalSynth(Synth):
if oss_sound_device:
def play(self,lang,text):
if not self.theProcess: self.startProcess()
self.theProcess.write("(Parameter.set 'Audio_Command \"play --device=%s \$FILE vol %.1f\")\n(tts_text \"%s\" nil)\n" % (oss_sound_device,5*soundVolume,text)) # (tts_text text nil) can be better than (SayText text) because it splits into multiple utterances if necessary
self.theProcess.write("(Parameter.set 'Audio_Command \"play --device=%s \\$FILE vol %.1f\")\n(tts_text \"%s\" nil)\n" % (oss_sound_device,5*soundVolume,text)) # (tts_text text nil) can be better than (SayText text) because it splits into multiple utterances if necessary
self.theProcess.flush()
# else send it via a file, because we haven't got code to give it to play to the other devices directly
def makefile(self,lang,text):
if not self.theProcess: self.startProcess()
fname = os.tempnam()+dotwav
self.theProcess.write("(Parameter.set 'Audio_Command \"sox \$FILE %s vol 5\")\n(SayText \"%s\")\n" % (fname,text))
self.theProcess.write("(Parameter.set 'Audio_Command \"sox \\$FILE %s vol 5\")\n(SayText \"%s\")\n" % (fname,text))
self.theProcess.flush()
return fname
def finish_makefile(self):
if self.theProcess: self.theProcess.close()
self.theProcess = None
class CoquiSynth(Synth):
def __init__(self):
Synth.__init__(self)
self.synths = {}
def works_on_this_platform(self):
if not unix: return 0 # I'm unable to test elsewhere
self.base = os.environ.get("HOME","")+"/.local/share/tts"
return isDirectory(self.base) # Voices require large downloads the first time they are used, so we'll use only already-downloaded voices
def supports_language(self,lang):
for a in os.listdir(self.base): # don't use any() with a generator func because we need to be Python 2.3 compatible
if a.startswith("tts_models--"+lang+"-"): return True # TODO: might not want to use all downloaded models, or might not want to use for all input types (e.g. zh does not support pinyin)
def guess_length(self,lang,text): return quickGuess(len(text),cond(lang in ["zh"],6,12)) # need better estimate
def makefile(self,lang,text):
text = ensure_unicode(text)
if lang=="zh": text += u"\u3002" # otherwise that model can glitch and repeat the last word of the phrase
if not lang in self.synths:
import torch;from TTS.api import TTS # shouldn't fault if models are downloaded to ~/.local/share/tts (unless uninstalled and not cleaned up...)
# We can assume Python 3 by this point, but must still use syntax compatible with Python 2
for a in sorted(os.listdir(self.base)):
if a.startswith("tts_models--"+lang+"-"):
self.synths[lang]=TTS(a.replace("--","/")).to(cond(torch.cuda.is_available(),"cuda","cpu"))
break
fname = os.tempnam()+dotwav
self.synths[lang].tts_to_file(text,file_path=fname)
return fname
class PiperSynth(Synth):
def __init__(self):
Synth.__init__(self)
self.lCache = {}
def works_on_this_platform(self):
if not unix: return 0 # I can't test on other platforms
for self.program in ["piper/piper","./piper"]:
if fileExists(self.program): return True
def supports_language(self,lang):
if lang in self.lCache: return self.lCache[lang]
for d in [".","piper"]:
foundSubdir = False
for f in os.listdir(d):
if f=="piper": foundSubdir=True
if (f.startswith(lang+"_") or f.startswith(lang+"-")) and f.endswith('.onnx'):
self.lCache[lang] = d+"/"+f
return self.lCache[lang]
if not foundSubdir: break
def guess_length(self,lang,text): return quickGuess(len(text),cond(lang in ["zh"],6,12)) # need better estimate
def transliterate(self,lang,text,forPartials=0):
# Piper TTS models are controlled by eSpeak phonemes, so we should be able to get eSpeak to do this
es = ESpeakSynth()
if not es.works_on_this_platform() or not es.supports_language(lang): return text
return es.transliterate(lang,text,forPartials)
def can_transliterate(self,lang):
es = ESpeakSynth()
return es.works_on_this_platform() and es.supports_language(lang)
def makefile(self,lang,text):
fname = os.tempnam()+dotwav
f=os.popen(self.program+' --model "'+self.supports_language(lang)+'" --output_file "'+fname+'"',popenWB)
f.write(text+"\n") ; f.close()
return fname
class GeneralSynth(Synth):
def __init__(self): Synth.__init__(self)
def supports_language(self,lang):
......@@ -870,7 +1026,7 @@ class GeneralFileSynth(Synth):
return 0
def works_on_this_platform(self): return extra_speech_tofile
def guess_length(self,lang,text):
if not lang in self.letters: self.letters[lang]=self.duration[lang]=0
if not checkIn(lang,self.letters): self.letters[lang]=self.duration[lang]=0
if self.letters[lang]<25:
self.letters[lang] += len(text)
self.duration[lang] += SampleEvent(self.makefile_cached(lang,text)).exactLen
......@@ -887,6 +1043,7 @@ class GeneralFileSynth(Synth):
return fname
all_synth_classes = [GeneralSynth,GeneralFileSynth] # at the beginning so user can override
all_synth_classes += [CoquiSynth,PiperSynth] # override espeak if present (especially PiperSynth)
for s in synth_priorities.split(): # synth_priorities no longer in advanced.txt (see system.py above) but we can still support it
if s.lower()=="ekho": all_synth_classes.append(EkhoSynth)
elif s.lower()=="espeak": all_synth_classes.append(ESpeakSynth)
......@@ -894,7 +1051,7 @@ for s in synth_priorities.split(): # synth_priorities no longer in advanced.txt
all_synth_classes.append(OSXSynth_Say)
all_synth_classes.append(OSXSynth_OSAScript) # (prefer _Say if >=10.3 because it's faster)
elif s.lower()=="sapi": all_synth_classes.append(PttsSynth)
all_synth_classes = all_synth_classes + [FestivalSynth,FliteSynth,OldRiscosSynth,S60Synth,AndroidSynth]
all_synth_classes += [FestivalSynth,FliteSynth,OldRiscosSynth,S60Synth,AndroidSynth]
prefer_espeak = prefer_espeak.split()
viable_synths = []
......@@ -905,9 +1062,10 @@ def setSoundCollector(sc):
global soundCollector, viable_synths, getsynth_cache
soundCollector,viable_synths,getsynth_cache = sc,[],{}
def get_synth_if_possible(language,warn=1,to_transliterate=False):
if language in getsynth_cache and not to_transliterate: return getsynth_cache[language] # most common case (vocab.txt parse)
language = S(language)
if checkIn(language,getsynth_cache) and not to_transliterate: return getsynth_cache[language] # most common case (vocab.txt parse)
if language==None:
if not None in getsynth_cache: getsynth_cache[None]=Partials_Synth()
if not checkIn(None,getsynth_cache): getsynth_cache[None]=Partials_Synth()
return getsynth_cache[None]
global viable_synths, warned_about_nosynth
if not viable_synths:
......@@ -917,18 +1075,24 @@ def get_synth_if_possible(language,warn=1,to_transliterate=False):
if to_transliterate: # for partials: return a synth that can transliterate the language, if possible
for synth in viable_synths:
if synth.supports_language(language) and synth.can_transliterate(language): return synth
if language=="zh": return simpleZhTransliterator # in case haven't got eSpeak
for synth in viable_synths:
if language=="zh": return simpleZhTransliterator # fallback if no eSpeak
for synth in viable_synths: # find a good one ?
if synth.supports_language(language) and not synth.not_so_good_at(language):
getsynth_cache[language]=synth ; return synth
for synth in viable_synths:
for synth in viable_synths: # OK, not-so-good one ?
if synth.supports_language(language):
getsynth_cache[language]=synth ; return synth
if warn and language not in synth_partials_voices and not language==firstLanguage and language in ["zh","cant","zhy","zh-yue"] and not language in warned_about_nosynth:
# Some people fail to install Yali etc because web advertising taught them to ignore sidebars :-(
warned_about_nosynth[language] = 1
show_warning("You don't have "+cond(language=="zh","Yali Cheng's Mandarin","Cameron Wong's Cantonese")+" voice installed, only a more basic robot voice. Please see the sidebar on the Gradint website for downloads.")
if app: waitOnMessage("In the meantime, you'll have to bear with this....")
getsynth_cache[language]=synth # only if warn (otherwise wait until we're called again, then warn)
return synth
if (not warn) or language not in [firstLanguage,secondLanguage]+possible_otherLanguages: return None # without printing a warning
if not language in warned_about_nosynth:
if not checkIn(language,warned_about_nosynth):
warned_about_nosynth[language] = 1
canSay = []
if language in synth_partials_voices: canSay.append("recorded syllables (partials)")
if checkIn(language,synth_partials_voices): canSay.append("recorded syllables (partials)")
if synthCache: canSay.append("recorded phrases (synthCache)")
if canSay: canSay="\n - can use only "+" and ".join(canSay)
else: canSay="\n (did you read ALL the comments in vocab.txt?)"
......@@ -943,28 +1107,28 @@ def pinyin_uColon_to_V(pinyin):
pinyin = pinyin.lower()
pristineU = unichr(0xfc).encode('utf-8')
pinyin = pinyin.replace("j"+pristineU,"ju").replace("q"+pristineU,"qu").replace("x"+pristineU,"xu").replace(pristineU,"v").replace(unichr(0xea).encode('utf-8'),"e") # for pristine's pinyin
pinyin = B(pinyin).replace(B("j")+pristineU,B("ju")).replace(B("q")+pristineU,B("qu")).replace(B("x")+pristineU,B("xu")).replace(pristineU,B("v")).replace(unichr(0xea).encode('utf-8'),B("e")) # for pristine's pinyin
return pinyin.replace("u:","v").replace("leu","lv").replace("neu","nv")
return pinyin.replace(B("u:"),B("v")).replace(B("leu"),B("lv")).replace(B("neu"),B("nv"))
class SynthEvent(Event):
def __init__(self,text,synthesizer,language,is_prompt=0):
assert text,"Trying to speak zero-length text"
self.text = text ; self.synthesizer = synthesizer
self.text = B(text) ; self.synthesizer = synthesizer
self.modifiedText = self.text
if language=="en":
self.modifiedText = self.modifiedText.replace("\xE2\x80\xA7","").replace("\xE2\x80\xB2","") # remove syllable boundaries and primes (usually just confuse speech synths)
if not self.text[-1] in ";.!?-" and not (';' in self.text and ';' in self.text[self.text.index(';')+1:]): self.modifiedText += ';' # prosody hack (some synths sound a bit too much like 'disjointed strict commands' without this)
self.modifiedText = self.modifiedText.replace(LB("\xE2\x80\xA7"),B("")).replace(LB("\xE2\x80\xB2"),B("")) # remove syllable boundaries and primes (usually just confuse speech synths)
if not self.text[-1:] in B(";.!?-") and not (B(';') in self.text and B(';') in self.text[self.text.index(B(';'))+1:]): self.modifiedText += B(';') # prosody hack (some synths sound a bit too much like 'disjointed strict commands' without this)
elif language=="zh":
# normalise pinyin
# (note - this code is NOT used for partials synth, only for passing to espeak etc. see elsewhere for partials synth)
self.modifiedText = pinyin_uColon_to_V(self.modifiedText) # includes .lower()
# and put space between every syllable of w, if it's one word only (the Lily voice seems to stand a better chance of getting it right that way, and occasionally other voices do too, e.g. "chang2yuan3" in at least some versions of eSpeak, not to mention Loquendo Lisheng
self.modifiedText = self.modifiedText.replace("-"," ") # for Lily, Lisheng etc. NB replace hyphen with space not with "", otherwise can get problems with phrases like "wang4en1-fu4yi4"
if (not " " in self.modifiedText) and ("1" in self.modifiedText or "2" in self.modifiedText or "3" in self.modifiedText or "4" in self.modifiedText or "5" in self.modifiedText):
for t in ["1","2","3","4","5"]: self.modifiedText = self.modifiedText.replace(B(t+"-"),B(t+" ")) # for Lily, Lisheng etc. NB replace hyphen with space not with "", otherwise can get problems with phrases like "wang4en1-fu4yi4". DON'T do it except after tone marks, because for hanzi we might want to use hyphens for word-boundary disambiguation.
if (not B(" ") in self.modifiedText) and (B("1") in self.modifiedText or B("2") in self.modifiedText or B("3") in self.modifiedText or B("4") in self.modifiedText or B("5") in self.modifiedText):
self.modifiedText=fix_pinyin(self.modifiedText,[]) # better call that before doing the following (in case any digits in the wrong place)
for f in py_final_letters:
for t in "12345": self.modifiedText=self.modifiedText.replace(f+t,f+t+" ")
for t in "12345": self.modifiedText=self.modifiedText.replace(B(f+t),B(f+t+" "))
if synthesizer.__class__ in [GeneralSynth, GeneralFileSynth]:
# some e.g. eSpeak use capitals to start a new sentence, so need to undo some of the .lower() that pinyin_uColon_to_V did.
# (ESpeakSynth already calls fix_commas in play() and makefile() so don't need to do it here.)
......@@ -999,7 +1163,7 @@ class SynthEvent(Event):
self.sound = self.synthesizer.makefile_cached(self.language,self.modifiedText)
self.synthesizer.finish_makefile()
if sample_table_hack:
if not self.sound in sample_table_hack_lengthDic: sample_table_hack_lengthDic[self.sound]=SampleEvent(self.sound).exactLen
if not checkIn(self.sound,sample_table_hack_lengthDic): sample_table_hack_lengthDic[self.sound]=SampleEvent(self.sound).exactLen
soundCollector.addFile(self.sound,sample_table_hack_lengthDic[self.sound])
open(self.sound,"wb") # i.e. truncate at 0 bytes to save space (but keep around so no name clashes)
elif self.sound:
......@@ -1059,7 +1223,7 @@ def abspath_from_start(p): # for just_synthesize to check for paths relative to
os.chdir(d)
return r
def just_synthesize(callSanityCheck=0,lastLang_override=None):
def just_synthesize(callGeneralCheck=0,lastLang_override=None):
# Handle the justSynthesize setting (see advanced.txt)
global startAnnouncement,endAnnouncement,logFile,synth_partials_cache
synth_partials_cache = {} # to stop 'memory leak' when running from the GUI
......@@ -1069,7 +1233,7 @@ def just_synthesize(callSanityCheck=0,lastLang_override=None):
called_synth = 0
# we re-generate the lesson on each repeat, so sporadic-synthcache stuff works
global repeatMode ; repeatMode = 1
while repeatMode:
while repeatMode and not repeatMode=="interrupted":
repeatMode = 0
less = Lesson()
lastStartTime = lastEndTime = lastWasDelay = 0
......@@ -1079,20 +1243,23 @@ def just_synthesize(callSanityCheck=0,lastLang_override=None):
ret=can_be_synthesized(fname)
if ret: return fileToEvent(fname)
else: show_warning("Can't say "+repr(fname)) # previous warnings should have said why (e.g. partials-only language)
for line in justSynthesize.split("#"):
line = line.strip(wsp) ; l = line.split(None,1)
if extsep in line and fileExists(line): event = fileToEvent(line,"")
elif extsep in line and fileExists(abspath_from_start(line)): event = fileToEvent(abspath_from_start(line),"")
elif line=='R':
for line in B(justSynthesize).split(B('#')):
line = bwspstrip(line)
l = line.split(None,1)
if B(extsep) in line and fileExists(line): event = fileToEvent(line,"")
elif B(extsep) in line and fileExists(abspath_from_start(line)): event = fileToEvent(abspath_from_start(line),"")
elif line==B('R'):
repeatMode=1 ; continue
elif len(l)==1:
try: delayVal = float(l[0])
except ValueError: delayVal = None
if delayVal==None:
# no float value; assume it's a single word to synth in secondLanguage or whatever was the last language used
show_warning("Assuming that %s is a word to synthesize in language '%s'" % (repr(l[0]),lastLanguage))
if callSanityCheck and sanityCheck(l[0],lastLanguage,1): return
event = checkCanSynth("!synth:"+l[0]+"_"+lastLanguage)
r = repr(l[0])
if r[:1]=="b": r=r[1:]
show_warning("Assuming that %s is a word to synthesize in language '%s'" % (r,lastLanguage))
if callGeneralCheck and generalCheck(l[0],lastLanguage,1): return
event = checkCanSynth("!synth:"+S(l[0])+"_"+S(lastLanguage))
if not event: continue # couldn't synth
called_synth = 1
else:
......@@ -1104,17 +1271,17 @@ def just_synthesize(callSanityCheck=0,lastLang_override=None):
lang, text = l
if lang=="sh:": event = ShellEvent(text)
else:
fname = "!synth:"+text+"_"+lang
fname = B("!synth:")+B(text)+B("_")+B(lang)
if not can_be_synthesized(fname):
if lang in [firstLanguage,secondLanguage]+otherLanguages:
show_warning("Can't say %s in %s" % (repr(text),repr(lang)))
lastLanguage=lang ; continue
# otherwise, user might have omitted lang by mistake
show_warning("Assuming %s was meant to be synthesized in language '%s'" % (cond("#" in justSynthesize or len(repr(line))<10,"that '"+repr(line)+"'","this line"),lastLanguage))
if callSanityCheck and sanityCheck(line,lastLanguage,1): return
event = checkCanSynth("!synth:"+line+"_"+lastLanguage)
show_warning("Assuming %s was meant to be synthesized in language '%s'" % (cond(B('#') in B(justSynthesize) or len(repr(line))<10,"that '"+repr(line)+"'","this line"),lastLanguage))
if callGeneralCheck and generalCheck(line,lastLanguage,1): return
event = checkCanSynth("!synth:"+S(line)+"_"+S(lastLanguage))
else:
if callSanityCheck and sanityCheck(text,lang,1): return
if callGeneralCheck and generalCheck(text,lang,1): return
event = checkCanSynth(fname)
lastLanguage = lang
if not event: continue
......@@ -1128,5 +1295,6 @@ def just_synthesize(callSanityCheck=0,lastLang_override=None):
global dbase ; dbase = None # for handleInterrupt
less.play()
startAnnouncement,endAnnouncement,logFile = oldStart,oldEnd,oldLogfile
if repeatMode=="interrupted": sys.exit(1) # better tell the calling script
if not called_synth: return None
return lastLanguage
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -63,21 +63,26 @@ if winsound:
try: ctypes.cdll.commdlg
except: WMstandard = True
if macsound and __name__=="__main__": os.system("clear 1>&2") # so warnings etc start with a clear terminal (1>&2 just in case using stdout for something else)
if macsound and __name__=="__main__": os.system("clear >&2") # so warnings etc start with a clear terminal (>&2 just in case using stdout for something else)
if riscos_sound: sys.stderr.write("Loading Gradint...\n") # in case it takes a while
try: import android
except: android = 0
try: import androidhelper as android
except:
try: import android
except: android = 0
if android:
android = android.Android()
android.makeToast("Loading Gradint")
try: android = android.Android()
except:
print ("\n"*50+" *** Your SL4A server has crashed ***\n Please restart SL4A\n (or restart your phone)\n and try running Gradint again.\n\n\n")
raise SystemExit
wsp = '\t\n\x0b\x0c\r ' # whitespace characters - ALWAYS use .strip(wsp) not .strip(), because someone added \xa0 (iso8859-1 no-break space) to string.whitespace on WinCE Python, and that can break processing of un-decoded UTF8 strings, e.g. a Chinese phrase ending "\xe5\x86\xa0"! (and assign to string.whitespace does not work around this.)
wsp = '\t\n\x0b\x0c\r ' ; bwsp=B(wsp) # whitespace characters - ALWAYS use .strip(wsp) not .strip(), because someone added \xa0 (iso8859-1 no-break space) to string.whitespace on WinCE Python, and that can break processing of un-decoded UTF8 strings, e.g. a Chinese phrase ending "\xe5\x86\xa0"! (and assign to string.whitespace does not work around this.)
# As .split() can't take alternative characters (and re-writing in Python is probably slow), just be careful with using it on un-decoded utf-8 stuff. (split(None,1) is ok if 1st word won't end in an affected character)
warnings_printed = [] ; app = False # False is a hack for "maybe later"
warnings_toprint = []
def show_warning(w):
if w+"\n" in warnings_printed: return
if not app and not app==False and not appuifw and not android:
if winCEsound and len(w)>100: w=w[:100]+"..." # otherwise can hang winCEsound's console (e.g. a long "assuming that" message from justSynthesize)
sys.stderr.write(w+"\n")
......@@ -90,21 +95,16 @@ def show_info(i,always_stderr=False):
if not riscos_sound and not always_stderr and hasattr(sys.stderr,"isatty") and not sys.stderr.isatty(): return # be quiet if o/p is being captured by cron etc (but isatty() might always return false on RISC OS
if winCEsound and len(i)>101: i=i[:100]+"..."+i[-1] # otherwise can hang winCEsound's console
if type(i)==type(u""): i=i.encode('utf-8')
try: sys.stderr.write(i)
try: writeB(sys.stderr,i)
except IOError: pass
# For pre-2.3 versions of Python (e.g. 2.2 on Symbian S60 and Mac OS 10.3):
try: True
except: exec("True = 1 ; False = 0")
# TODO make sure to avoid writing "string1 in string2" without thinking - if string1 is multiple characters it won't work on pre-2.3
# TODO check all lambda functions for Python2.2 compatibility
# (TODO: GUI_translations, if not set in advanced.txt, won't work properly on pre-2.3 - it'll take them as Latin-1)
# (TODO: and if it *IS* set in advanced.txt, will 2.2's exec() correctly exec a unicode string?)
# Check if we're on big-endian architecture (relevant to sox etc)
try: import struct
except: struct=0
if struct and struct.pack("h",1)[0]=='\x00': big_endian = 1
if struct and B(struct.pack("h",1)[0])==B('\x00'): big_endian = 1
else: big_endian = 0
# RISC OS has a different extension separator because "." is used as a directory separator (from the original 1982 BBC Micro DFS with 1-character directories)
......@@ -122,6 +122,14 @@ def list2dict(l):
return d
try: list2set = set
except NameError: list2set = list2dict
def checkIn(k,obj):
try: return k in obj # dict or set
except:
try: return obj.has_key(k) # Python 2.1 (may raise TypeError, AttributeError etc if try to use the "in" operator as above, but has_key rm'd from Python3)
except: return obj.find(k) > -1 # Python 2.1 strings
try: object
except:
class object: pass # Python 2.1
# settings.txt and advanced.txt
# (done here before the variables start to be used in
......@@ -170,16 +178,23 @@ if use_unicode_filenames:
def u8strip(d):
global last_u8strip_found_BOM ; last_u8strip_found_BOM = 0
if d.startswith('\xef\xbb\xbf'):
d = B(d)
if d.startswith(LB('\xef\xbb\xbf')):
last_u8strip_found_BOM = 1
return d[3:] # ignore Notepad's UTF-8 BOM's
else: return d
def bwspstrip(s):
try: return s.strip(bwsp)
except: return s.strip() # Python 2.1
def wspstrip(s):
try: return s.strip(wsp)
except: return s.strip() # Python 2.1
GUI_translations_old = GUI_translations
configFiles = map(lambda x:x+dottxt,["advanced","settings"]) # MUST have settings last so can have per-user override of scriptVariants
if not hasattr(sys,"argv"): sys.argv=" " # some Symbian versions
starting_directory = os.getcwd()
if not fileExists(configFiles[0]):
if macsound and "_" in os.environ:
if macsound and checkIn("_",os.environ):
s=os.environ["_"] ; s=s[:s.rfind(os.sep)]
os.chdir(s)
if not fileExists(configFiles[0]):
......@@ -188,9 +203,14 @@ if not fileExists(configFiles[0]):
os.chdir(s)
if not fileExists(configFiles[0]) and sys.argv and (os.sep in sys.argv[0] or (os.sep=='\\' and '/' in sys.argv[0])):
# try the sys.argv[0] directory, in case THAT works
if os.sep=="\\" and '/' in sys.argv[0] and fileExists(sys.argv[0].replace('/','\\')): sys.argv[0]=sys.argv[0].replace('/','\\') # hack for some Windows Python builds accepting / in command line but reporting os.sep as \
if os.sep=="\\" and '/' in sys.argv[0] and fileExists(sys.argv[0].replace('/','\\')): sys.argv[0]=sys.argv[0].replace('/','\\') # hack for some Windows Python builds accepting slash in command line but reporting os.sep as backslash
os.chdir(starting_directory)
os.chdir(sys.argv[0][:sys.argv[0].rfind(os.sep)])
if not fileExists(configFiles[0]): # argv[0] might be a symlink
os.chdir(starting_directory)
try: rp = os.path.realpath(sys.argv[0])
except: rp = 0 # e.g. no os.path, or no os.path.realpath
if rp: os.chdir(rp[:rp.rfind(os.sep)])
if not fileExists(configFiles[0]):
# Finally, try the module pathname, in case some other Python program has imported us without changing directory. Apparently we need to get this from an exception.
try: raise 0
......@@ -204,7 +224,7 @@ if not fileExists(configFiles[0]):
# directory should be OK by now
if sys.platform.find("ymbian")>-1: sys.path.insert(0,os.getcwd()+os.sep+"lib")
import time,sched,sndhdr,random,math,pprint,codecs
import time,sched,random,math,pprint,codecs
def exc_info(inGradint=True):
import sys # in case it's been gc'd
......@@ -216,29 +236,32 @@ def exc_info(inGradint=True):
while tbObj and hasattr(tbObj,"tb_next") and tbObj.tb_next: tbObj=tbObj.tb_next
if tbObj and hasattr(tbObj,"tb_lineno"): w += (" at line "+str(tbObj.tb_lineno))
if inGradint:
if tbObj and hasattr(tbObj,"tb_frame") and hasattr(tbObj.tb_frame,"f_code") and hasattr(tbObj.tb_frame.f_code,"co_filename") and not tbObj.tb_frame.f_code.co_filename.find("gradint"+extsep+"py")>-1: w += (" in "+tbObj.tb_frame.f_code.co_filename+"\n")
else: w += (" in "+program_name[:program_name.index("(c)")]+"\n")
if tbObj and hasattr(tbObj,"tb_frame") and hasattr(tbObj.tb_frame,"f_code") and hasattr(tbObj.tb_frame.f_code,"co_filename") and not tbObj.tb_frame.f_code.co_filename.find("gradint"+extsep+"py")>=0: w += (" in "+tbObj.tb_frame.f_code.co_filename)
else: w += (" in "+program_name[:program_name.index("(c)")])
w += " on Python "+sys.version.split()[0]+"\n"
del tbObj
return w
def read(fname): return open(fname,"rb").read()
def write(fname,data): open(fname,"wb").write(data)
def readSettings(f):
try: fdat = u8strip(read(f)).replace("\r","\n")
try: fdat = u8strip(read(f)).replace(B("\r"),B("\n"))
except: return show_warning("Warning: Could not load "+f)
try: fdat = unicode(fdat,"utf-8")
except: return show_warning("Problem decoding utf-8 in "+f)
try: exec(fdat) in globals()
try: exec(fdat,globals())
except: show_warning("Error in "+f+" ("+exc_info(False)+")")
synth_priorities = "eSpeak MacOS SAPI Ekho" # old advanced.txt had this instead of prefer_espeak; we can still support it
dir1 = list2set(dir()+["dir1","f","last_u8strip_found_BOM"])
dir1 = list2set(dir()+["dir1","f","last_u8strip_found_BOM","__warningregistry__"])
for f in configFiles: readSettings(f)
for d in dir():
if not d in dir1 and eval(d) and not type(eval(d))==type(lambda *args:0): # (ignore unrecognised options that evaluate false - these might be an OLD unused option with a newer gradint rather than vice versa; also ignore functions as these could be used in command-line parameters)
if not checkIn(d,dir1) and eval(d) and not type(eval(d))==type(lambda *args:0): # (ignore unrecognised options that evaluate false - these might be an OLD unused option with a newer gradint rather than vice versa; also ignore functions as these could be used in command-line parameters)
show_warning("Warning: Unrecognised option in config files: "+d)
del dir1
GUI_translations_old.update(GUI_translations) ; GUI_translations = GUI_translations_old # in case more have been added since advanced.txt last update
def cond(a,b,c):
# Python 2.4 can inline "b if a else c" but Python 2.3 can't
if a: return b
else: return c
......@@ -261,25 +284,44 @@ if paranoid_file_management:
# For ftpfs etc. Retry on errno 13 (permission denied), and turn append into a copy. Otherwise occasionally get vocab.txt truncated.
_old_open = open
def tryIO(func):
for tries in range(10)+["last"]:
for tries in list(range(10))+["last"]:
try: return func()
except IOError,err:
if tries=="last" or not err.errno in [5,13]: raise
except IOError:
err = sys.exc_info()[1]
if tries=="last" or not err.errno in [5,13,None]: raise
time.sleep(0.5)
def read(file): return tryIO(lambda x=file:_old_open(x,"rb").read())
def open(file,mode="r"):
def _write(fn,data):
tryIO(lambda x=fn,y=data:_old_open(x,"wb").write(y))
time.sleep(0.5)
if not filelen(fn)==len(data):
# might be a version of curlftpfs that can't shorten files - try delete and restart (although this can erase permissions info)
os.remove(fn)
tryIO(lambda x=fn,y=data:_old_open(x,"wb").write(y))
if not filelen(fn)==len(data): raise IOError("wrong length")
if not read(fn)==data: raise IOError("verification failure on "+repr(fn))
def write(fn,data): return tryIO(lambda x=fn,y=data:_write(x,y))
def open(file,mode="r",forAppend=0):
if "a" in mode:
try: dat = open(file,"rb").read()
except IOError,err:
except IOError:
err = sys.exc_info()[1]
if err.errno==2: dat = "" # no such file or directory
else: raise
if len(dat) < filelen(file): raise IOError("short read")
try: os.rename(file,file+"~") # just in case!
except: pass
o=open(file,"wb")
o=open(file,"wb",1)
o.write(dat)
return o
return tryIO(lambda x=file,m=mode:_old_open(x,m))
r=tryIO(lambda x=file,m=mode:_old_open(x,m))
if "w" in mode and not forAppend and filelen(file): # it's not truncating (see _write above)
r.close()
os.unlink(file)
r=tryIO(lambda x=file,m=mode:_old_open(x,m))
return r
if seedless: random.seed(0)
# Different extension separators again
if not extsep==".":
......@@ -301,11 +343,11 @@ for p in [progressFile,progressFileBackup,pickledProgressFile]:
if extsep in p[1]: p=(p[0],p[1][:p[1].rfind(extsep)]) # here rather than earlier to cover cases where extsep is in a directory name but not in the filename
if oldDir==None: oldDir=p
elif not oldDir==p:
sys.stderr.write("ERROR: progressFile, progressFileBackup and pickledProgressFile, if not None, must have same directory and major part of filename. Gradint will not run otherwise. This sanity-check was added in case some script sets progressFile to something special but forgets to set the others.\n")
sys.stderr.write("ERROR: progressFile, progressFileBackup and pickledProgressFile, if not None, must have same directory and major part of filename. Gradint will not run otherwise. This coherence check was added in case some script sets progressFile to something special but forgets to set the others.\n")
sys.exit(1)
# Check for RISC OS pre-1970 clock problem (actually quite likely if testing on the rpcemu emulator without setting the clock)
if riscos_sound and hex(int(time.time())).find("0xFFFFFFFF")>-1 and not outputFile:
if riscos_sound and hex(int(time.time())).find("0xFFFFFFFF")>=0 and not outputFile:
sys.stderr.write("ERROR: time.time() is not usable - gradint cannot run interactively.\n")
sys.stderr.write("This error can be caused by the RISC OS clock being at 1900 (the Unix time functions start at 1970).\nClose this task window, set the clock and try again.\n")
sys.exit()
......@@ -328,7 +370,7 @@ Tk_might_display_wrong_hanzi = wrong_hanzi_message = "" ; forceRadio=0
if macsound:
try: os.remove("_tkinter.so") # it might be an old patched version for the wrong OS version
except: pass
def tkpatch(): # patch Mac OS Tk to the included v8.6 (as v8.4 on OS10.5 has hanzi problem and v8.5 on 10.6 has fontsize problems etc)
def tkpatch(): # (called only on specific older versions of Mac OS X) patch Mac OS Tk to the included v8.6 (as v8.4 on OS10.5 has hanzi problem and v8.5 on 10.6 has fontsize problems etc)
f="/System/Library/Frameworks/Python.framework/Versions/"+sys.version[:3]+"/lib/python"+sys.version[:3]+"/lib-dynload/_tkinter.so"
if fileExists(f): # we might be able to patch this one up
if not isDirectory("Frameworks") and fileExists("Frameworks.tbz"): os.system("tar -jxvf Frameworks.tbz && rm Frameworks.tbz && chmod -R +w Frameworks")
......@@ -343,6 +385,7 @@ if macsound:
elif sys.version[:5] == "2.5.1": # 10.5
if not tkpatch(): Tk_might_display_wrong_hanzi="10.5"
elif sys.version[:5] == "2.6.1": tkpatch() # 10.6 (still has Tk8.5, hanzi ok but other problems)
elif sys.version[:5] == "2.7.5": tkpatch() # 10.9 (problems with "big print" button if don't do this)
if Tk_might_display_wrong_hanzi: wrong_hanzi_message = "NB: In Mac OS "+Tk_might_display_wrong_hanzi+", Chinese\ncan display wrongly here." # so they don't panic when it does
# Handle keeping progress file and temp directories etc if we're running from a live CD
......@@ -358,8 +401,8 @@ def progressFileOK():
except: return 0
if winsound: # will try these dirs in reverse order:
tryList = ["C:\\TEMP\\gradint-progress.txt", "C:\\gradint-progress.txt", "C:gradint-progress.txt"]
if "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ: tryList.append(os.environ["HOMEDRIVE"]+os.environ["HOMEPATH"]+os.sep+"gradint-progress.txt")
elif "HOME" in os.environ: tryList=[os.environ["HOME"]+os.sep+"gradint-progress.txt"]
if checkIn("HOMEDRIVE",os.environ) and checkIn("HOMEPATH",os.environ): tryList.append(os.environ["HOMEDRIVE"]+os.environ["HOMEPATH"]+os.sep+"gradint-progress.txt")
elif checkIn("HOME",os.environ): tryList=[os.environ["HOME"]+os.sep+"gradint-progress.txt"]
elif riscos_sound: tryList=["$.gradint-progress/txt"]
else: tryList = []
foundPF = okPF = 0 ; defaultProgFile = progressFile
......@@ -403,29 +446,33 @@ if winsound or winCEsound or mingw32 or riscos_sound or not hasattr(os,"tempnam"
global tempnam_no ; tempnam_no += 1
return tmpPrefix+str(tempnam_no)
os.tempnam = os.tmpnam = tempnam
elif (macsound or sys.platform.lower().find("bsd")>0) and os.environ.get("TMPDIR",""): # BSD tempnam uses P_tmpdir instead, override
tempnam0 = os.tempnam
os.tempnam=lambda *args:tempnam0(os.environ["TMPDIR"])
if disable_once_per_day==1:
if once_per_day==3: sys.exit()
else: once_per_day=0
if once_per_day&2 and not hasattr(sys,"_gradint_innerImport"): # run every day
currentDay = None
# markerFile logic to avoid 2 background copies etc (can't rely on taskkill beyond WinXP)
# (however this doesn't protect against uninstall + immediate reinstall)
markerFile,toDel="background1"+dottxt,"background2"+dottxt
if fileExists(markerFile): markerFile,toDel=toDel,markerFile
try: os.remove(toDel)
except OSError: pass
open(markerFile,"w").write("(delete this file to make the background process quit on next check)\n")
while fileExists(markerFile):
# markerFile logic to avoid 2+ background copies (can't rely on taskkill beyond WinXP)
myID = str(time.time())
try: myID += str(os.getpid())
except: pass
markerFile="background"+dottxt
open(markerFile,"w").write(myID)
def reador0(f):
try: return read(f)
except: return 0
while reador0(markerFile)==myID:
if not currentDay == time.localtime()[:3]: # first run of day
currentDay = time.localtime()[:3]
if __name__=="__main__": # can do it by importing gradint
sys._gradint_innerImport = 1
try:
try: reload(gradint)
except NameError: import gradint
gradint.orig_onceperday = once_per_day
gradint.main()
try: reload(gradint)
except NameError: import gradint
gradint.orig_onceperday = once_per_day
try: gradint.main()
except SystemExit: pass
elif winsound and fileExists("gradint-wrapper.exe"): # in this setup we can do it by recursively calling gradint-wrapper.exe
s=" ".join(sys.argv[1:])
......@@ -438,6 +485,8 @@ if once_per_day&2 and not hasattr(sys,"_gradint_innerImport"): # run every day
show_warning("Not doing once_per_day&2 logic because not running as main program")
# (DO need to be able to re-init the module - they might change advanced.txt etc)
break
if len(sys.argv)>1: sys.argv.append(";")
sys.argv.append("disable_once_per_day=0") # don't let a disable_once_per_day=2 in argv result in repeated questioning
time.sleep(3600) # delay 1 hour at a time (in case hibernated)
if once_per_day&1 and fileExists(progressFile) and time.localtime(os.stat(progressFile).st_mtime)[:3]==time.localtime()[:3]: sys.exit() # already run today
try: orig_onceperday
......@@ -446,9 +495,9 @@ except: orig_onceperday=0
if winsound:
# check for users putting support files/folders in the desktop shortcuts folder and thinking it's the gradint folder
# We can't do much about detecting users on non-English Windows who have heeded the warning about moving the "Desktop" folder to the real desktop but then mistook this for the gradint folder when adding flite (but hopefully they'll be using ptts/espeak anyway, and yali has an installer)
if "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ: dr=os.environ["HOMEDRIVE"]+os.environ["HOMEPATH"]
if checkIn("HOMEDRIVE",os.environ) and checkIn("HOMEPATH",os.environ): dr=os.environ["HOMEDRIVE"]+os.environ["HOMEPATH"]
else: dr="C:\\Program Files" # as setup.bat (location for gradint on Win95 etc)
if "USERPROFILE" in os.environ: dr=os.environ["USERPROFILE"]
if checkIn("USERPROFILE",os.environ): dr=os.environ["USERPROFILE"]
if not dr[-1]=="\\": dr += "\\"
try: dirList = os.listdir(dr+"Desktop\\gradint\\") # trailing \ important, otherwise it can include gradint.zip etc on Desktop
except: dirList = []
......@@ -464,11 +513,42 @@ elif macsound:
os.system('open ../Gradint.app')
sys.exit(0)
elif fileExists_stat("../Gradint 2.app/deleteme"):
import thread ; thread.start_new_thread(lambda *x:(time.sleep(2),os.system('rm -rf "../Gradint 2.app"')),())
try: import thread
except ImportError: import _thread as thread
thread.start_new_thread(lambda *x:(time.sleep(2),os.system('rm -rf "../Gradint 2.app"')),())
def got_program(prog):
# Test to see if the program 'prog' is on the system, as portable as possible. NB some Unix 'which' output an error to stdout instead of stderr, so check the result exists.
return (winsound and fileExists(prog+".exe")) or (unix and fileExists_stat(os.popen("which "+prog+" 2>/dev/null").read().strip(wsp)))
if winsound:
if fileExists(prog+".exe"): return prog+".exe"
elif riscos_sound:
if prog[:1]=="*": # module
os.system("help "+prog[1:]+" { > _tstCmd_ }")
got = open("_tstCmd_").read().find(prog[1:].upper())>-1
os.unlink("_tstCmd_") ; return got
return checkIn("Alias$"+prog,os.environ) # works in Python 3.8 but not 2.7 (Alias$ vars hidden)
elif unix:
try:
try: from shutil import which as find_executable # PEP 632
except: from distutils.spawn import find_executable
if (":"+os.environ.get("PATH","")).find(":.")>-1:
prog = find_executable(prog)
else: # at least some distutils assume that "." is in the PATH even when it isn't, so do it ourselves without checking "."
oldCwd = os.getcwd()
pList = os.environ.get("PATH","").split(':')
if pList:
done=0
for p in pList:
try: os.chdir(p)
except: continue
done=1 ; break
if done:
prog = find_executable(prog)
os.chdir(oldCwd)
except ImportError:
# fall back to running 'which' in a shell (probably slower if got_program is called repeatedly)
prog = wspstrip(os.popen("which "+prog+" 2>/dev/null").read())
if not fileExists_stat(prog): prog=None # some Unix 'which' output an error to stdout instead of stderr, so check the result exists
return prog
def win2cygwin(path): # convert Windows path to Cygwin path
if path[1]==":": return "/cygdrive/"+path[0].lower()+path[2:].replace("\\","/")
......@@ -500,17 +580,11 @@ def check_for_interrupts(): # used on platforms where thread.interrupt_main won'
raise KeyboardInterrupt
# If forking, need to do so BEFORE importing any Tk module (we can't even verify Tk exists 1st)
if outputFile or justSynthesize or appuifw or not (winsound or winCEsound or mingw32 or macsound or riscos_sound or cygwin or "DISPLAY" in os.environ): useTK = 0
if useTK and runInBackground and not (winsound or mingw32) and hasattr(os,"fork") and not "gradint_no_fork" in os.environ:
import fcntl, termios
if outputFile or justSynthesize or appuifw or not (winsound or winCEsound or mingw32 or macsound or riscos_sound or cygwin or checkIn("DISPLAY",os.environ)): useTK = 0
if useTK and runInBackground and not (winsound or mingw32) and hasattr(os,"fork") and not checkIn("gradint_no_fork",os.environ):
if os.fork(): sys.exit()
os.setpgid(0,0)
os.setsid()
if os.fork(): sys.exit()
try: tty = os.open("/dev/tty", os.O_RDWR)
except: tty = None
if not tty==None:
fcntl.ioctl(tty, termios.TIOCNOTTY, 0)
os.close(tty)
devnull = os.open("/dev/null", os.O_RDWR)
for fd in range(3): os.dup2(devnull,fd)
else: runInBackground = 0
......@@ -534,8 +608,8 @@ try:
import locale
locale.setlocale(locale.LC_ALL, 'C')
except: pass
if not '\xc4'.lower()=='\xc4': # buggy setlocale (e.g. S60) can create portability issues with progress files
lTrans="".join([chr(c) for c in range(ord('A'))]+[chr(c) for c in range(ord('a'),ord('z')+1)]+[chr(c) for c in range(ord('Z')+1,256)])
if not LB('\xc4').lower()==LB('\xc4'): # buggy setlocale (e.g. S60) can create portability issues with progress files
lTrans=B("").join([chr(c) for c in range(ord('A'))]+[chr(c) for c in range(ord('a'),ord('z')+1)]+[chr(c) for c in range(ord('Z')+1,256)])
def lower(s): return s.translate(lTrans) # (may crash if Unicode)
else:
def lower(s): return s.lower()
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# (Python 2 or Python 3, but more fully tested on 2)
program_name = "gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+."
program_name = "gradint v3.095 (c) 2002-25 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
......@@ -21,8 +22,48 @@ appTitle = "Language lesson"
import sys,os
if not sys.version_info[0]==2: # oh panic, someone's probably trying to run us on Py3k
sys.stderr.write("Sorry, Gradint cannot run on Python "+repr(sys.version_info[0])+"\nPlease install a 2.x version of Python (must be 2.2+).\n")
sys.exit(1)
if sys.version_info[0]>2:
_map,_filter = map,filter
def map(*args): return list(_map(*args))
def filter(*args): return list(_filter(*args))
from functools import cmp_to_key
def sort(l,c): l.sort(key=cmp_to_key(c))
raw_input,unichr,xrange,long = input,chr,range,int
def chr(x): return unichr(x).encode('latin1')
from subprocess import getoutput
popenRB,popenWB = "r","w"
def unicode(b,enc):
if type(b)==str: return b
return b.decode(enc)
else: # Python 2
def sort(l,c): l.sort(c)
popenRB,popenWB = "rb","wb"
bytes = str
try: from commands import getoutput
except ImportError: pass
# For pre-2.3 versions of Python (e.g. 2.2 on Symbian S60 and Mac OS 10.3):
try: True
except: exec("True = 1 ; False = 0")
def readB(f,m=None):
if hasattr(f,"buffer"): f0,f=f,f.buffer # Python 3 non-"b" file
if m: return f.read(m)
else: return f.read() # no "None" in Python 2
def writeB(f,b):
if hasattr(f,"buffer"): f0,f=f,f.buffer # Python 3 non-"b" file
f.write(b)
def B(x):
if type(x)==bytes: return x
try: return x.encode('utf-8')
except: return x # maybe not a string
def LB(x):
if type(x)==bytes: return x
try: return x.encode('latin1')
except: return x
def S(x):
if type(x)==bytes and not bytes==str: return x.decode('utf-8')
return x
def S2(s):
try: return S(s)
except: return s # coding errors OK in unavail, leave as byte-string
# --------------------------------------------------------
# This file is part of the source code of
# gradint v0.998 (c) 2002-2012 Silas S. Brown. GPL v3+.
# This file is part of the source code of Gradint
# (c) Silas S. Brown.
# 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
......@@ -43,21 +43,21 @@ def setup_samplesDir_ifNec(d=0): # if the user doesn't have a samples directory,
if not d: d=samplesDirectory
if not isDirectory(d):
os.mkdir(d)
if fileExists(user0[0]+os.sep+"README"+dottxt): open(d+os.sep+"README"+dottxt,'wb').write(read(user0[0]+os.sep+"README"+dottxt))
if fileExists(user0[0]+os.sep+"README"+dottxt): write(d+os.sep+"README"+dottxt,read(user0[0]+os.sep+"README"+dottxt))
def get_userNames(): # list of unicode user names or []
ret=[]
u=userNameFile ; c=0
while fileExists(u):
ret.append(unicode(u8strip(read(u)).strip(wsp),'utf-8'))
ret.append(unicode(bwspstrip(u8strip(read(u))),'utf-8'))
c += 1 ; u=addUserToFname(userNameFile,c)
global lastUserNames ; lastUserNames = ret
return ret
def set_userName(N,unicodeName): open(addUserToFname(userNameFile,N),"w").write(unicodeName.encode("utf-8")+"\n") # implicitly adds if N=num+1
def set_userName(N,unicodeName): writeB(open(addUserToFname(userNameFile,N),"w"),unicodeName.encode("utf-8")+B("\n")) # implicitly adds if N=num+1
def wrapped_set_userName(N,unicodeName):
if unicodeName.strip(wsp): set_userName(N,unicodeName)
if wspstrip(unicodeName): set_userName(N,unicodeName)
else: app.todo.alert="You need to type the person's name in the box before you press "+localise("Add new name") # don't waitOnMessage because we're in the GUI thread
GUI_usersRow = lastUserNames = None
......@@ -91,7 +91,7 @@ def updateUserRow(fromMainMenu=0):
userBSM = ButtonScrollingMixin() ; userBSM.ourCanvas = c
else: userBSM = None
for i in range(len(names)):
if names[i].strip(wsp):
if wspstrip(names[i]):
r=Tkinter.Radiobutton(row, text=names[i], variable=app.userNo, value=str(i), takefocus=0)
r.grid(row=i+1,column=0,sticky="w")
r["command"]=cmd=lambda e=None,i=i: select_userNumber(i)
......@@ -107,41 +107,42 @@ def updateUserRow(fromMainMenu=0):
r=Tkinter.Frame(row) ; r.grid(row=i+1,column=0,columnspan=4)
text,entry = addTextBox(r)
if not fromMainMenu: entry.focus() # because user has just pressed the "add other students" button, or has just added a name and may want to add another
l=lambda *args:(wrapped_set_userName(i,asUnicode(text.get())),updateUserRow())
l=lambda e=None,wrapped_set_userName=wrapped_set_userName,i=i,text=text:(wrapped_set_userName(i,asUnicode(text.get())),updateUserRow())
addButton(r,localise("Add new name"),l)
entry.bind('<Return>',l)
if not i: Tkinter.Label(row,text="The first name should be that of the\nEXISTING user (i.e. YOUR name).").grid(row=i+2,column=0,columnspan=4)
if userBSM: userBSM.bindFocusIn(r) # for shift-tab from the bottom
if hasattr(row,"widgetsToDel"): row.widgetsToDel.append(r)
if not names[i]: break
if userBSM: c.after(cond(winCEsound,1500,300),lambda *args:c.config(scrollregion=c.bbox(Tkinter.ALL),width=c.bbox(Tkinter.ALL)[2],height=min(c["height"],c.winfo_screenheight()/2,c.bbox(Tkinter.ALL)[3]))) # hacky (would be better if it could auto shrink on resize)
else: row.widgetsToDel.append(addButton(row,localise("Family mode (multiple user)"),lambda *args:(set_userName(0,""),updateUserRow())))
if userBSM: c.after(cond(winCEsound,1500,300),lambda e=None,c=c:c.config(scrollregion=c.bbox(Tkinter.ALL),width=c.bbox(Tkinter.ALL)[2],height=min(c["height"],c.winfo_screenheight()/2,c.bbox(Tkinter.ALL)[3]))) # hacky (would be better if it could auto shrink on resize)
else: row.widgetsToDel.append(addButton(row,localise("Family mode (multiple user)"),lambda e=None:(set_userName(0,""),updateUserRow())))
def renameUser(i,radioButton,parent,cancel=0):
if hasattr(radioButton,"in_renaming"):
if hasattr(radioButton,"in_renaming"): # finish the rename
del radioButton.in_renaming
n=asUnicode(radioButton.renameText.get())
if cancel: pass
elif not n.strip(wsp) and len(lastUserNames)>1: tkMessageBox.showinfo(app.master.title(),"You can't have blank user names unless there is only one user. Keeping the original name instead.")
elif not wspstrip(n) and (len(lastUserNames)>1 and not (len(lastUserNames)==2 and not lastUserNames[-1])): tkMessageBox.showinfo(app.master.title(),"You can't have blank user names unless there is only one user. Keeping the original name instead.")
else:
set_userName(i,n)
lastUserNames[i] = n
radioButton["text"]=n
radioButton.renameEntry.grid_forget()
radioButton.grid(row=i+1,column=0,sticky="w")
else:
else: # start the rename
radioButton.in_renaming = 1
radioButton.grid_forget()
radioButton.renameText,radioButton.renameEntry = addTextBox(parent,"nopack")
radioButton.renameEntry.grid(row=i+1,column=0)
radioButton.renameText.set(lastUserNames[i])
radioButton.renameEntry.focus()
radioButton.after(10,lambda *args:radioButton.renameEntry.event_generate('<End>'))
radioButton.renameEntry.bind('<Return>',lambda *args:renameUser(i,radioButton,parent))
radioButton.renameEntry.bind('<Escape>',lambda *args:renameUser(i,radioButton,parent,cancel=1))
radioButton.after(10,lambda e=None,radioButton=radioButton:radioButton.renameEntry.event_generate('<End>'))
radioButton.renameEntry.bind('<Return>',lambda e=None,radioButton=radioButton,i=i,parent=parent:renameUser(i,radioButton,parent))
radioButton.renameEntry.bind('<Escape>',lambda e=None,i=i,radioButton=radioButton,parent=parent:renameUser(i,radioButton,parent,cancel=1))
def deleteUser(i):
for n in ["Are you sure","Are you REALLY sure","This is your last chance: Are you REALLY SURE"]:
if not tkMessageBox.askyesno(app.master.title(),u""+n+" you want to delete "+lastUserNames[i]+" permanently, including any vocabulary list and recordings?"): return
if not tkMessageBox.askyesno(app.master.title(),ensure_unicode(n)+" you want to delete "+lastUserNames[i]+" permanently, including any vocabulary list and recordings?"): return
numUsers=len(lastUserNames)
for fileOrDir in user0+(userNameFile,):
d=addUserToFname(fileOrDir,i)
......