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
Showing
with 451 additions and 209 deletions
#!/usr/bin/env python
# (should work in either Python 2 or Python 3)
# Character-learning support program
# (C) 2006-2013, 2020 Silas S. Brown. Version 0.3.
# 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.
# Where to find history:
# on GitHub at https://github.com/ssb22/gradint
# and on GitLab at https://gitlab.com/ssb22/gradint
# and on BitBucket https://bitbucket.org/ssb22/gradint
# and at https://gitlab.developers.cam.ac.uk/ssb22/gradint
# and in China: https://gitee.com/ssb22/gradint
listenAddr='127.0.0.1'
firstPortNo=9876
tableFile = "characters.txt" # for first-time setup
knownFile = "known-chars.txt" # ditto
dumpFile = "charlearn-data" # for saving progress
reviseFile = "revise.txt" # for requesting more revision next time (will be deleted after integration into progress)
import sys,os.path
if sys.argv[-1].startswith("--"): gradint = None # (don't need to speak if we're processing options, see at end)
elif os.path.isfile("gradint.py"): import gradint
else: gradint = None # won't speak characters
import random,os,time,socket
try: from subprocess import getoutput
except: from commands import getoutput
try: from cPickle import Pickler,Unpickler
except: from pickle import Pickler,Unpickler
try: from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
except: from http.server import BaseHTTPRequestHandler, HTTPServer
try: import thread
except: import _thread as thread
def byPriority(a): return a.priority
priorityIfGotWrong = -10
priorityOfOtherCharWrong = -4
priorityOfGroupWrong = 0
maxShowInGroup = 5 ; priorityBreakGroup = 10
initSessionLen = sessionLen = 2 ; maxSessionLen = 10 ; sampleConst = 1.5
def updateSessionLen():
global sessionLen
sessionLen = min(max(sessionLen,int(thechars.countKnown()[1]+0.95)),maxSessionLen)
# did have /sampleConst after countKnown()[1] but doesn't seem necessary
already_spoken = {}
gradint_busy = 0
def speak_bkg():
gradint.just_synthesize()
global gradint_busy
gradint_busy = 0
class SingleChar:
def __init__(self,hanzi,pinyin):
self.hanzi = hanzi ; self.pinyin = pinyin
self.priority = 0 ; self.similarityGroup = None
self.supposedToKnow = 0
def formatPinyin(self): return self.pinyin.replace("\n","<BR>") # (could make it into actual tone marks also)
def htmlString(self,parent,step=1,left=0):
self.supposedToKnow = 1
r=u'<html><head><title>hanzi</title><meta http-equiv="Content-Type" content="text/html; charset=%s"></head><body><h1>%s</h1>' % (parent.charset,self.hanzi)
if step==1: r+=self.yesno('Do you know what this is? (%d remaining)' % left,2,0)
else:
r += self.formatPinyin() + "<HR>"
if step<=0:
if self.similarityGroup:
l = []
for c in parent.chars:
if c.similarityGroup == self.similarityGroup and not id(c)==id(self): l.append(c)
l.sort(key=byPriority)
r+="Not to be confused with:"
for c in l[:maxShowInGroup-1]: r+='<h1>%s</h1>%s' % (c.hanzi,c.formatPinyin())
r += '<hr>'
if parent.thisSession:
r+='<A HREF="/%s">Next character</A>' % str(random.random())
if step==-1:
# got it right - might as well take that link automatically
r=parent.processRequest("/").decode(parent.charset).replace('</body></html>','')
else:
updateSessionLen()
r+='<A HREF="/quit">Quit</A> | <A HREF="/%s">Another %d</A>' % (str(random.random()),sessionLen)
if step==0:
self.priority=priorityIfGotWrong
self.speak(parent.charset)
else:
# knew it
self.priority += 1
if self.priority > 0:
if self.priority < 25000: self.priority *= 2 # give new characters a chance
else: self.priority = 50000 # level off
else: self.priority /= 2 # TRY this for a while - will make chars got-wrong recover more quickly (again to give new chars a chance)
parent.save()
elif step==2:
r+=self.yesno('Did you get it right?',-1,3)
self.speak(parent.charset)
elif step==3:
r+='What did you think it was?<P>'
toOut = [] # (pinyin,hanzi,id,is-in-same-group)
for c in parent.chars:
if c.similarityGroup and c.similarityGroup==self.similarityGroup: sameGrp=True
else: sameGrp=False # need to do it this way because Python sometimes returns 'None' from that expression
if c.supposedToKnow and not id(c)==id(self): toOut.append((c.pinyin,c.hanzi,id(c),sameGrp)) # NOT formatPinyin, because may want to i-search it
toOut.sort()
if len(toOut) > 20: r+="(Hint: On some browsers you can use find-as-you-type)<P>"
for outSameGroup in [True,False]:
oldL=len(r)
for p,hanzi,val,sameGrp in toOut:
if sameGrp==outSameGroup: r+='%s <A HREF="/%d_%d">%s</A><BR>' % (hanzi,id(self),val,p)
if len(r)>oldL and outSameGroup: r += '<HR>' # between chars in same group and others
r+='<A HREF="/%d=0">None of the above</A>' % id(self)
if not parent.thisSession:
global already_spoken ; already_spoken = {} # reset it so "Another N" does speak them
return r + '</body></html>'
def speak(self,charset):
if self.hanzi in already_spoken: return
already_spoken[self.hanzi] = 1 # don't set a self. attribute - it'll get pickled for next session
if gradint:
gradint.justSynthesize = self.hanzi.decode(charset).encode('utf-8')
global gradint_busy
while gradint_busy: time.sleep(0.5)
gradint_busy = 1
thread.start_new_thread(speak_bkg,())
def yesno(self,question,ifyes,ifno): return question+'<P><A ID="y" HREF="/%d=%d">Yes</A><SCRIPT>document.getElementById("y").focus()</SCRIPT> | <A HREF="/%d=%d">No</A>' % (id(self),ifyes,id(self),ifno) # (don't use the js anywhere except yes/no, because 'next character' etc may have too much on the screen and we don't want the focus() to scroll)
the_speaker_process = None
def terminate_server():
# portable signal.alarm(1)
time.sleep(1); os.abort()
def B(s):
if type(u"")==type(""): return s.encode('utf-8')
else: return s
def S(s):
if type(u"")==type("") and not type(s)==type(""): return s.decode('utf-8')
else: return s
class CharDbase:
def __init__(self):
self.counter = 0 ; self.nextPriority = 0
self.similarityGroups = 0
self.chars = [] ; self.thisSession = []
self.readTable() ; self.readKnown() ; self.readRevise()
def debug_printKnown(self):
print ("-*- coding: %s -*-" % (self.charset,))
for c in self.chars:
if c.supposedToKnow: print ("%s %s" % (c.priority,c.hanzi))
def readTable(self):
addingTo = 0
if self.chars: addingTo = 1
lines=open(tableFile,'rb').readlines()
if lines[0].startswith(B("charset:")):
self.charset = S(lines[0].split()[-1])
lines = lines[1:]
else: self.charset = "iso-8859-1"
for line in lines: self.addCharFromFreqTable(line.decode(self.charset),addingTo)
def readKnown(self):
try:
o=open(knownFile)
except IOError: return
for line in o.readlines(): self.makeCharKnown(line.split()[0])
def readRevise(self):
try:
o=open(reviseFile)
except IOError: return
for line in o.readlines(): self.makeCharRevise(line.split()[0])
def makeCharKnown(self,hanzi):
if not hanzi: return # blank lines etc
for c in self.chars:
if c.hanzi==hanzi:
if not c.supposedToKnow:
c.supposedToKnow = 1
c.priority = priorityOfGroupWrong # just to check
return
print ("WARNING: character '%s' in %s was not in %s - ignoring" % (repr(hanzi),knownFile,tableFile))
def makeCharRevise(self,hanzi):
if not hanzi: return # blank lines etc
for c in self.chars:
if c.hanzi==hanzi:
c.supposedToKnow = 1
c.priority = priorityIfGotWrong
return
print ("WARNING: character '%s' in %s was not in %s - ignoring" % (repr(hanzi),reviseFile,tableFile))
def addCharFromFreqTable(self,line,checkAlreadyThere):
hanzi,pinyin = line.split(None,1)
c=SingleChar(hanzi,pinyin.replace("\\n","\n"))
c.priority = self.nextPriority ; self.nextPriority += 1
if checkAlreadyThere:
for c2 in self.chars:
if c2.hanzi == hanzi: return
self.chars.append(c)
def charIdToChar(self,charId):
char = None
for c in self.chars:
if id(c)==charId:
char = c ; break
assert char ; return char
def processRequest(self,path):
if '=' in path:
charId,step = map(lambda x:int(x),path[1:].split('='))
char = self.charIdToChar(charId)
elif '_' in path: # grouping
char,char2 = map(lambda x:self.charIdToChar(int(x)),path[1:].split('_'))
if not char.similarityGroup and not char2.similarityGroup: # new group:
self.similarityGroups += 1
char.similarityGroup = char2.similarityGroup = self.similarityGroups
elif not char.similarityGroup: char.similarityGroup = char2.similarityGroup
elif not char2.similarityGroup: char2.similarityGroup = char.similarityGroup
elif not char.similarityGroup == char2.similarityGroup: # merge 2 different groups:
for c in self.chars:
if c.similarityGroup == char2.similarityGroup: c.similarityGroup = char.similarityGroup
step = 0 # normal got-wrong for this character
char.priority = priorityIfGotWrong # here also, for the loop below
char2.priority = min(char2.priority,priorityOfOtherCharWrong)
for c in self.chars:
if c.similarityGroup == char.similarityGroup:
if c.priority >= priorityBreakGroup: c.similarityGroup=None
elif c.priority > priorityOfGroupWrong: c.priority = priorityOfGroupWrong
elif path=="/status":
self.chars.sort(key=byPriority)
cp=self.chars[:] ; r='<html><head><title>Current Status</title><meta http-equiv="Content-Type" content="text/html; charset=%s"></head><body><h2>Current Status</h2>(score/priority number is shown to the left of each item)<br>' % (self.charset,)
while cp:
if not cp[0].supposedToKnow:
del cp[0] ; continue
if cp[0].priority >= priorityBreakGroup: thisGrp=[0]
else: thisGrp=list(filter(lambda x:x==0 or (cp[x].similarityGroup and cp[x].similarityGroup==cp[0].similarityGroup and cp[x].priority < priorityBreakGroup),range(len(cp))))
if len(thisGrp)>1 and not r.endswith("<hr>"): r+="<hr>"
if len(thisGrp)>1: r+="<em>"+str(len(thisGrp))+" similar items:</em><br>"
for g in thisGrp: r += str(cp[g].priority)+": "+cp[g].hanzi+" "+cp[g].pinyin+"<br>"
if len(thisGrp)>1: r+="<hr>"
thisGrp.reverse()
for toDel in thisGrp: del cp[toDel]
return (r+"</body></html>").encode(self.charset)
else:
if path=="/checkallknown": self.thisSession = list(filter(lambda x:x.supposedToKnow,self.chars)) # TODO: Document this URL
char,step = self.chooseChar(),1
return char.htmlString(self,step,len(self.thisSession)).encode(self.charset)
def chooseChar(self):
if not self.thisSession:
self.chars.sort(key=byPriority)
if sessionLen==initSessionLen:
self.thisSession = self.chars[:sessionLen] # introduce in order the first time (especially if the second one is just a straight line ("yi1"), as one beginner thought the program had gone wrong when he saw this)
self.thisSession.reverse() # because taken out by pop()
else: self.thisSession = random.sample(self.chars[:int(sessionLen*sampleConst)],sessionLen) # TODO need a better way than that. NB high priority should be VERY likely, but others should have a chance. try as-is for now
return self.thisSession.pop()
def save(self): Pickler(open(dumpFile,"wb"),-1).dump(self)
def countKnown(self):
charsSeen = sessnLen = charsSecure = newChars = 0
secure=[] ; insecure=[]
self.chars.sort(key=byPriority)
for c in self.chars:
if c.supposedToKnow:
charsSeen += 1
if c.priority>0: secure.append(c.hanzi)
else: insecure.append(c.hanzi)
else: newChars += 1
if newChars == 2: sessnLen = charsSeen
return charsSeen,sessnLen,secure,insecure
try:
dumped = open(dumpFile,"rb")
except IOError: dumped = None
if dumped:
thechars = Unpickler(dumped).load()
dumped.close()
thechars.thisSession = []
if os.stat(tableFile).st_mtime > os.stat(dumpFile).st_mtime: thechars.readTable()
try:
if os.stat(knownFile).st_mtime > os.stat(dumpFile).st_mtime: thechars.readKnown()
except OSError: pass
try:
if os.stat(reviseFile).st_mtime > os.stat(dumpFile).st_mtime: thechars.readRevise()
except OSError: pass
updateSessionLen()
else:
thechars=CharDbase()
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path.startswith("/fav"):
self.send_response(404) ; self.end_headers() ; return
self.send_response(200)
self.send_header("Content-type","text/html; charset="+thechars.charset)
self.end_headers()
if self.path.startswith("/quit"):
r=thechars.processRequest("/status").decode(thechars.charset)
r=r[:r.index("<body>")+6]+"Server terminating."+r[r.index("<body>")+6:]
self.wfile.write(r.encode(thechars.charset))
thread.start_new_thread(terminate_server,()) # can terminate the server after this request
else: self.wfile.write(thechars.processRequest(self.path))
self.wfile.close() # needed or will wait for bkg speaking processes etc
def do_session():
portNo = firstPortNo ; server = None
while portNo < firstPortNo+100:
try:
server = HTTPServer((listenAddr,portNo),RequestHandler)
break
except socket.error: portNo += 1
assert server, "Couldn't find a port to run the server on"
if ("win" not in sys.platform) and getoutput("which x-www-browser 2>/dev/null"): # (try to find x-www-browser, but not on windows/cygwin/darwin)
os.system("x-www-browser http://localhost:%d/%s &" % (portNo,str(random.random()))) # shouldn't need a sleep as should take a while to start anyway
else:
try:
import webbrowser
webbrowser.open_new("http://localhost:%d/%s" % (portNo,str(random.random())))
except ImportError: pass # fall through to command-line message
# Do this as well, in case that command failed:
print ("") ; print ("") ; print ("")
print ("Server running. If a web browser does not appear automatically,")
print ("please start one yourself and go to")
print ("http://localhost:%d/%d" % (portNo,random.randint(1,99999)))
print ("") ; print ("") ; print ("")
server.serve_forever()
if sys.argv[-1]=='--count':
x,y,sec,insec=thechars.countKnown()
print ("%d (of which %d seem secure)" % (x,len(sec)))
elif sys.argv[-1]=='--show-secure':
x,y,sec,insec=thechars.countKnown()
print (" ".join(sec))
elif sys.argv[-1]=='--show-wfx':
# the result of this might need charset conversion
# (and the conversion of charlearn scores to Wenlin histories is only approximate)
print ("""<?xml version='1.0'?>
<!-- Wenlin Flashcard XML file -->
<stack owner='Anonymous' reward='points'>""")
thechars.chars.sort(key=byPriority)
for c in thechars.chars:
print ("<card type='d'><question>"+c.hanzi+"</question>")
trials = "" ; score = 0
if c.supposedToKnow:
if c.priority < 0:
trials += "n"
p = priorityIfGotWrong
while p < c.priority:
trials += "y" ; score += 1
p /= 2
p = 1
while p < c.priority:
trials += "y" ; score += 1
p *= 2
print ("<history score='%d' trials='%d' recent='%s'></history></card>" % (score,len(trials),trials))
print ("</stack>")
else: do_session()
charset: euc-jp
あ a
い i
う u
え e
お o
か ka
き ki
く ku
け ke
こ ko
さ sa
し shi
す su
せ se
そ so
た ta
ち chi
つ tsu
て te
と to
な na
に ni
ぬ nu
ね ne
の no
は ha
ひ hi
ふ fu
へ he
ほ ho
ま ma
み mi
む mu
め me
も mo
や ya
ゆ yu
よ yo
ら ra
り ri
る ru
れ re
ろ ro
わ wa
を wo
ん n
ア a
イ i
ウ u
エ e
オ o
カ ka
キ ki
ク ku
ケ ke
コ ko
サ sa
シ shi
ス su
セ se
ソ so
タ ta
チ chi
ツ tsu
テ te
ト to
ナ na
ニ ni
ヌ nu
ネ ne
ノ no
ハ ha
ヒ hi
フ fu
ヘ he
ホ ho
マ ma
ミ mi
ム mu
メ me
モ mo
ヤ ya
ユ yu
ヨ yo
ラ ra
リ ri
ル ru
レ re
ロ ro
ワ wa
ヲ wo
ン n
Installing Gradint on Linux systems
-----------------------------------
Gradint does not need to be installed, it can
just run from the current directory.
If you do want to make a system-wide installation
(for example if you want to make a package for a
Linux distribution), I suggest doing the following
as root:
mkdir /usr/share/gradint
cp gradint.py /usr/share/gradint/
cd samples/utils
for F in *.py *.sh; do
export DestFile=/usr/bin/gradint-$(echo $F|sed -e 's/\..*//')
cp $F $DestFile
chmod +x $DestFile
done
cd ../.. ; rm -rf samples/utils
tar -zcf /usr/share/gradint/new-user.tgz \
advanced.txt settings.txt vocab.txt samples
cat > /usr/bin/gradint <<EOF
#!/bin/bash
if ! test -e "$HOME/gradint"; then
echo "You will need some prompts and samples in your home directory."
echo "Is it OK to unpack an example into $HOME/gradint ?"
echo "Ctrl-C to quit or Enter to continue"
read
echo -n "Unpacking... "
mkdir "$HOME/gradint"
cd "$HOME/gradint"
tar -zxf /usr/share/gradint/new-user.tgz
echo "done."
echo "Please check the contents of $HOME/gradint"
echo "especially the README files."
echo "Then you can run gradint again."
exit
fi
cd "$HOME/gradint"
python /usr/share/gradint/gradint.py $@
EOF
chmod +x /usr/bin/gradint
For a distribution you might also have to write
man pages and tidy up the help text etc.
Depends: python + a sound player (e.g. alsa-utils)
Recommends: python-tk python-tksnack sox libsox-fmt-all madplay
There's an article explaining how to set up the Android emulator at
http://www.linuxjournal.com/article/10940
(on a real phone, need to go to Application settings and enable Unknown sources)
The Gradint files go into the device's sl4a/scripts directory.
On the emulator, you can use e.g.:
platform-tools/adb push ~/gradint/gradint.py /sdcard/sl4a/scripts
platform-tools/adb push ~/gradint/samples /sdcard/sl4a/scripts/samples
(the linuxjournal article said tools/ instead of platform-tools/
- may depend on which version you have)
Does not work with very large files, so for partials do not use audiodata.dat
(use the individual syllable files instead)
------- Making Gradint into a self-contained .apk file -------
This might not work (the device might fail to accept the resulting .apk file)
so this is only for reference. Requires an Android SDK installation.
cd /tmp
wget http://android-scripting.googlecode.com/hg/android/script_for_android_template.zip
wget http://people.pwf.cam.ac.uk/ssb22/gradint/gradint-android.zip
mkdir android
cd android
unzip ../gradint-android.zip
cd ..
mkdir apk
cd apk
unzip ../script_for_android_template.zip
export ANDROID_SDK=~/android-sdk-linux_x86/ # or wherever
sh configure_package.sh org.ucam.ssb22.gradint
leafpad build.xml # change project name to "Gradint"
mv ../android/gradint.py res/raw/script.py
mv ../android/advanced.txt ../android/settings.txt ../android/samples ../android/vocab.txt res/raw/
ant debug
# now copy bin/Gradint-debug.apk to the device
(Might need to go to Application settings and enable
Unknown sources, as you probably need for SL4A and Python)
File deleted
File deleted
xian4zai4 wo3men5 yao4 deng3, ran2hou4 fu4xi2. zai4 di4 yi1 ke4 wo3men5 hai2 mei2you3 xue2xi2 hen3 duo1 ci2yu3 suo3yi3 ting2dun4 bi3jiao4 chang2. dan4shi4 zai4 wei4lai2 de5 ke4 wo3men5 mei2you3 zhe4yang4 chang2 de5 ting2dun4.
#!/bin/bash
export SamplesDir="samples/" # Must include trailing /
export ProgressFile="progress.txt"
if ! test -e $SamplesDir; then echo "Error: $SamplesDir does not exist (are you in the right directory?)"; exit 1; fi
if ! test -e $ProgressFile; then echo "Error: $ProgressFile does not exist (are you in the right directory?)";exit 1;fi
if test "a$1" == a; then
echo "Usage: $0 oldname newname"
echo "oldname and newname are relative to $SamplesDir, and can be prefixes of several files/directories"
echo "Moves files from one samples directory to another, keeping $ProgressFile adjusted. Make sure gradint is not running (including waiting for start) when in use."
exit 1
fi
export Src=$1
export Dest=$2
find $SamplesDir -follow -type f | grep ^$SamplesDir$Src | \
while true; do read || break;
export SrcFile=$REPLY
export DestFile=$(echo $SrcFile|sed -e "s|^$SamplesDir$Src|$SamplesDir$Dest|")
mkdir -p $DestFile ; rmdir $DestFile # ensure parent dirs exist before moving file across
mv -b $SrcFile $DestFile
export SrcFile=$(echo $SrcFile|sed -e "s|$SamplesDir||")
export DestFile=$(echo $DestFile|sed -e "s|$SamplesDir||")
gzip -fdc $ProgressFile | sed -e "s|$SrcFile|$DestFile|g" > /tmp/newprog ; mv /tmp/newprog $ProgressFile # (ideally should re-write to batch these changes, but leave like this for now in case need to recover from unfinished operation)
done
rmdir $SamplesDir$Src 2>/dev/null >/dev/null # IF it's a directory
#!/usr/bin/env python
# Program to strip any silence from the beginning/end of a
# sound file (must be real 0-bytes not background noise)
# (This is useful as a "splitter" post-processor when
# getting samples from CD-ROMs e.g. "Colloquial Chinese" -
# don't use audacity here because some versions of audacity
# distort 8-bit audio files)
# Needs 'sox' + splitter
from splitter import *
for wavFile in sys.argv[1:]:
# Figure out sox parameters
header = sndhdr.what(wavFile)
if not header: raise IOError("Problem opening %s" % (wavFile,))
(wtype,rate,channels,wframes,bits) = header
if bits==8: soxBits="-b -u" # unsigned
elif bits==16: soxBits="-w -s" # signed
elif bits==32: soxBits="-l -s" # signed
else: raise Exception("Unsupported bits per sample")
soxParams = "-t raw %s -r %d -c %d" % (soxBits,rate,channels)
rawFile = wavFile + ".raw"
# Now ready to convert to raw, and read it in
convertToRaw(soxParams,wavFile,rawFile)
o=open(rawFile,"rb")
allData=o.read()
o.close()
# Now figure out how many samples we can take out
bytesPerSample = channels*int(bits/8)
if bytesPerSample==1: silenceVal=chr(128)
else: silenceVal=chr(0)
startIdx = 0
while startIdx < len(allData):
if not allData[startIdx]==silenceVal: break
startIdx = startIdx + 1
startIdx = int(startIdx/bytesPerSample) * bytesPerSample
endIdx = len(allData)
while endIdx:
if not allData[endIdx-1]==silenceVal: break
endIdx = endIdx - 1
endIdx = endIdx - len(allData) # put it into -ve notatn
endIdx = int(endIdx/bytesPerSample) * bytesPerSample
endIdx = endIdx + len(allData) # avoid 0
sys.stderr.write("Debugger: Clipping %s to %d:%d\n" % (wavFile,startIdx,endIdx))
allData = allData[startIdx:endIdx]
# Write back the file, and convert it back to wav
o=open(rawFile,"wb")
o.write(allData)
o.close()
convertToWav(soxParams,rawFile,wavFile)
# Clean up
os.unlink(rawFile)
all:
echo "Please run 'make' in the directory above, not here."
exit 1
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 '-' in lang: lang=lang[:lang.index("-")]
if not lang in d: d[lang]=[]
d[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 d.items()[:]:
if k in macVoices:
try:
for m in macVoices[k].split():
for vv in v:
if m.lower() == vv.lower():
d2[k] = [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 macVoices.items():
try:
for kk,vv in d.items():
for m in v.split():
for vvv in vv:
if m.lower() == vvv.lower():
d2[k] = [vvv] ; found=1 ; raise BreakOut()
except BreakOut: pass
if d.keys()==['en'] and not found: return {"en":""} # just use the default
for k,v in d2.items()[:]: d2[k]='-v "'+v[0]+'" '
return d2
File deleted
File deleted
File deleted
開頭
今日個堂上完啦
而家我哋要等一陣,然後翻溫。喺第一課我哋仲未學習好多嘅詞語,所以停頓會比較長,但係喺未來嘅課程,我哋唔會有咁長嘅停頓