Newer
Older
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# Start of synth.py - drive various speech synthesizers
def quickGuess(letters,lettersPerSec): return math.ceil(letters*1.0/lettersPerSec)
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Synth(object):
# Subclasses need to re-implement these:
def supports_language(self,lang): return 0
def not_so_good_at(self,lang): return 0
def works_on_this_platform(self): return 0
# def guess_length(self,lang,text) - MUST define this.
# OPTIONAL def play(self,lang,text) play in realtime, returns a non-(False,0,None) value on error
# OPTIONAL def makefile(self,lang,text) return unique filename (doesn't have to be ready yet) + finish_makefile(self) (ensure all files ready (may get more synth requests later))
# (at least one of the above must be defined; if don't define the second then can't use this synth offline)
# Remember to include it in all_synth_classes below
################## 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: os.remove(v)
except: pass # someone may have removed it already, e.g. cache-synth.py's renaming
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)]
t = self.makefile(lang,text)
self.fileCache[(lang,textKey)] = t
return t
def finish_makefile(self): pass
def transliterate(self,lang,text,forPartials=1): return None # could return a transliteration for the GUI if you want (forPartials is 1 if the translit. is for synth from partials, 0 if is for GUI - may not translit. quite so aggressively in the latter case - but NB if partials are used then the GUI will show them instead of calling with 0)
def can_transliterate(self,lang): return 0
try:
import warnings
warnings.filterwarnings("ignore","tempnam is a potential security risk to your program")
except ImportError: pass
def unzip_and_delete(f,specificFiles="",ignore_fail=0):
if ignore_fail:
# we had better at least check that the unzip command exists
if not got_program("unzip"):
show_warning("Please unzip "+f+" (Gradint cannot unzip it for you as there's no 'unzip' program on this system)")
return 1
show_info("Attempting to extract %s, please wait\n" % (f,))
if os.system("unzip -uo "+f+" "+specificFiles) and not ignore_fail:
show_warning("Warning: Failed to unzip "+f)
return 0
else:
# unzip seemed to work
os.remove(f)
show_info(f+" unpacked successfully\n")
return 1
class OSXSynth_Say(Synth):
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/say")
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\"" % (text.replace('"',''),))
def makefile(self,lang,text):
fname = os.tempnam()+extsep+"aiff"
system("say -o %s \"%s\"" % (fname,text.replace('"','')))
return aiff2wav(fname)
def aiff2wav(fname):
if not system("sox \"%s\" \"%s\"" % (fname,fname[:-4]+"wav")):
os.remove(fname)
fname=fname[:-4]+"wav"
# else just return aiff and hope for the best (TODO won't work with cache-synth)
return fname
class OSXSynth_OSAScript(Synth):
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")
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
# def play(self,lang,text): os.popen("osascript","w").write('say "%s"\n' % (text,)) # better not have the realtime one because osascript can introduce a 2-3second delay (and newer machines will not be using this class anyway; they'll be using OSXSynth_Say)
def makefile(self,lang,text):
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 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):
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")
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
def play(self,lang,text):
if not text=="Error in graddint program.": # (just in case it's unclear)
if text.endswith(';'): doLabel(text[:-1])
else: doLabel(text)
audio.say(text)
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
if winsound or mingw32: toNull=" > nul"
else: toNull=" >/dev/null" # stdout only, not stderr, because we want to see any errors that happen
def ensure_unicode(text):
if type(text)==type(u""): return text
else:
try: return unicode(text,"utf-8")
except UnicodeDecodeError: raise Exception("problem decoding "+repr(text))
class PttsSynth(Synth):
def __init__(self):
Synth.__init__(self)
self.program=None
self.offlineOnly = False
if ptts_program: self.program=ptts_program
elif not winsound and not mingw32 and not cygwin: return
# You can rename ptts.exe to ptts-offline.exe as a hack if you think your SAPI works only when generating words offline but not for actually playing them. This is no longer documented in vocab.txt because usually if the system is that broken then it won't work offline either.
if not self.program:
for i in ["ptts.exe","ptts-offline.exe"]:
if fileExists(i):
if cygwin: self.program='"'+os.getcwd()+cwd_addSep+i+'"' # (don't just do this anyway because some windows systems don't like it)
else: self.program = i
self.offlineOnly = 'offline' in i
break
if not self.program:
# (in case someone's running on Windows from source)
show_warning("Warning: ptts.exe not found (required for SAPI 5 speech) - get it from the Windows gradint distribution (or elsewhere)")
if cygwin: self.lily_file = win2cygwin(lily_file)
else: self.lily_file = lily_file
if fileExists(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
else: self.lily_file = None
def supports_language(self,lang): return lang in 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:
d=os.getcwd()
ret=self.sapi_unicode(sapiVoices[lang][0],ensure_unicode(text),speed=sapiSpeeds.get(lang,None))
p=os.popen(self.program+self.speedParam(sapiSpeeds.get(lang,None))+toNull,"w")
p.write(text+"\n")
return p.close()
elif lang=='zh':
d=os.getcwd()
ret=self.sapi_unicode("VW Lily",self.preparePinyinPhrase(text),speed=sapiSpeeds.get(lang,None))
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])
if not toFile: extra=""
else:
extra=' -w '+changeToDirOf(toFile,1)+' -c 1'
if sampleRate: extra += (' -s '+str(sampleRate))
ret=system(self.program+' -u '+changeToDirOf(unifile,1)+' -voice "'+voice+'"'+extra+self.speedParam(speed)+toNull) # (both changeToDirOf will give same directory because both from tempnam)
def speedParam(self,speed):
if speed: return " -r "+str(speed)
else: return ""
def makefile(self,lang,text):
fname = os.tempnam()+dotwav
oldcwd=os.getcwd()
if lang in sapiVoices: self.sapi_unicode(sapiVoices[lang][0],ensure_unicode(text),fname,sapiVoices[lang][1],speed=sapiSpeeds.get(lang,None))
elif lang=="en": os.popen(self.program+' -c 1 -w '+changeToDirOf(fname,1)+self.speedParam(sapiSpeeds.get(lang,None))+toNull,"w").write(text+"\n") # (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)
self.sapi_unicode("VW Lily",self.preparePinyinPhrase(text),fname,16000,speed=sapiSpeeds.get(lang,None))
d = sapi_sox_bug_workaround(read(fname)); open(fname,"wb").write(d)
if cygwin: os.system("chmod -x '"+fname+"'")
return fname
def preparePinyinPhrase(self,pinyin):
def stripPunc(p,even_full_stop=1): # because synth won't like punctuation in the dictionary entries
toStrip=',?;":\'!' # (see note below re hyphens)
if even_full_stop: toStrip+='.'
for punc in toStrip: p=p.replace(punc,"")
return p
if __name__=="__main__": pinyin=stripPunc(pinyin,0) # (if not running as a library, strip ALL punctuation ANYWAY, because the voice can pause a bit long) (but don't strip full stops)
# Split it into words to avoid it getting too long
# (max 30 chars in a word, max 30 syllables & 240 bytes in its definition, no duplicate entries. Splitting into words should be OK, but perhaps had better make sure the words on the left don't get too long in case the user omits spaces.)
pinyin=ensure_unicode(pinyin) # allow existing hanzi as well (TODO also allow/convert unicode tone marks)
Loading
Loading full blame...