diff --git a/.gitignore b/.gitignore
index b25c15b..9b5e2e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
*~
+__pycache__
diff --git a/Makefile b/Makefile
index 537592e..9d79987 100644
--- a/Makefile
+++ b/Makefile
@@ -232,14 +232,14 @@ publish: $(All_Versions) gradint.py
grep ^program_name < src/top.py|head -1|sed -e 's/.*radint v/v/' -e 's/ .*/./' > ~/homepage/public/gradint/latest-version.txt
make clean
~/homepage/update
- ssh st0rage "cd eGuidedog/ssb22/gradint; screen -d -m /bin/bash -c 'sleep 60;. build-sync.sh'"
gradint-build.7z:
mkdir /tmp/gradint-build00
cp -r * /tmp/gradint-build00
rm -r /tmp/gradint-build00/LICENSE /tmp/gradint-build00/README.md /tmp/gradint-build00/charlearn
mv /tmp/gradint-build00 gradint
- cd gradint ; make clean ; rm -rf extras ; cd ..
+ make -C gradint clean
+ rm -rf gradint/extras
7za a gradint-build.7z gradint/
rm -rf gradint
@@ -266,6 +266,7 @@ CD: $(Mac_Files) gradint.zip
echo;echo;echo "Made CD directory. Can add gradint/samples, gradint/vocab.txt, gradint/espeak for Windows, gradint/espeak-.. for OSX, sox Win/Mac binaries, oggenc or whatever for Windows, etc."
cleanup:
- rm -f `find . -type f -name '*~' -o -name '*.pyc' -o -name DEADJOE`
+ find . -type f '(' -name '*~' -o -name '*.pyc' -o -name DEADJOE ')' -exec rm -vf '{}' ';'
+ rm -rvf __pycache__ # must be separate from find, as some find implementations exec before trying to descend and then error
clean: cleanup
- rm -f gradint.py $(All_Versions) src/defaults.py gradint-installer.command gradint.dmg
+ rm -rf gradint.py $(All_Versions) src/defaults.py gradint-installer.command gradint.dmg
diff --git a/advanced.txt b/advanced.txt
index 0dc41ae..ca37321 100644
--- a/advanced.txt
+++ b/advanced.txt
@@ -47,7 +47,8 @@ otherLanguages = ["cant","ko","jp"]
# able to tell the difference between cant_en.wav and an
# ordinary English prompt and might use it wrongly.
-possible_otherLanguages = ["cant","ko","jp","en","zh"]
+possible_otherLanguages = ["cant","ko","jp","en","zh",
+ "zhy","zh-yue"]
# You can also fill in otherFirstLanguages below
# (using the same ["item","item"] format) to
@@ -95,7 +96,7 @@ prefer_espeak = "en"
# "zh" for Zhongwen (Mandarin).
# - You can improve eSpeak's English by installing
# Festival's dictionary and using lexconvert to convert
-# it, see http://ssb22.user.srcf.net/gradint/lexconvert.html
+# it, see http://ssb22.user.srcf.net/lexconvert/
# (this has already been done in the bundled version).
# - eSpeak is not very natural-sounding, but it is very
# clear and accurate in English and some other languages
@@ -201,7 +202,7 @@ systemVoice = "en"
# - Festival Lite on Windows (if all else fails) :
# put flite.exe in the gradint folder
#
-# - Linux: install Festival, or flite if you want a US accent
+# - GNU/Linux: install Festival, or flite for US accent
#
# - S60: the phone's built-in speech can be used
#
@@ -211,6 +212,22 @@ systemVoice = "en"
# older "Speech!" utility. These can be used only for
# playing in real-time, not for generating files.
+# Coqui voices are experimentally supported on GNU/Linux.
+# Setup: pip install coqui-tts[server,zh,ja,ko]
+# Then download the voices you want, e.g.:
+# from TTS.api import TTS;langs = {}
+# for m in TTS().list_models(): langs.setdefault(m.split('/')[1].split('-')[0],[]).append(m)
+#
+# TTS(langs["zh"][0])
+# TTS('tts_models/en/jenny/jenny')
+# (If any model crashes during download, be sure to delete the
+# result from ~/.local/share/tts before running Gradint. For
+# example vocoder_models--ja--kokoro--hifigan_v1 may crash.
+# I did say support for these voices is experimental.)
+# Gradint detects voices that have been downloaded
+# (but prefer_espeak overrides this). The Chinese
+# voice does NOT support pinyin.
+
# You can also set extra_speech to a list of
# (language prefix, command), for example:
# extra_speech=[ ("la","say-latvian"),("de","say-german") ]
@@ -350,7 +367,7 @@ lily_file = "C:\\Program Files\\NeoSpeech\\Lily16\\data-common\\userdict\\userdi
# somewhere under C:\Program Files\VW\VT\Lily\M16-SAPI5\lib\
# but I don't know exactly)
-# If you want to use SAPI under WINE in Linux
+# If you want to use SAPI under WINE in GNU/Linux
# then you can set ptts_program:
ptts_program = None
# (hint: run winecfg and set Windows version to Millenium (ME)
@@ -759,7 +776,7 @@ gui_output_directory = "output"
# in which case the first directory that EXISTS will be used
# (or the last one on the list if all else fail).
# Useful if the directory to your MP3 player only appears when
-# it's plugged in for example. With Linux automounters you can
+# it's plugged in for example. With GNU/Linux automounters
# set "/media/*" as one of the directories, and it will expand to
# whatever removable device is mounted IF there is only one.
diff --git a/hanzi-prompts/begin_zh-yue.txt b/hanzi-prompts/begin_zh-yue.txt
new file mode 100644
index 0000000..62cad49
--- /dev/null
+++ b/hanzi-prompts/begin_zh-yue.txt
@@ -0,0 +1 @@
+開頭
diff --git a/hanzi-prompts/end_zh-yue.txt b/hanzi-prompts/end_zh-yue.txt
new file mode 100644
index 0000000..679afff
--- /dev/null
+++ b/hanzi-prompts/end_zh-yue.txt
@@ -0,0 +1 @@
+今日個堂上完啦
diff --git a/hanzi-prompts/longpause_zh-yue.txt b/hanzi-prompts/longpause_zh-yue.txt
new file mode 100644
index 0000000..18d9f6c
--- /dev/null
+++ b/hanzi-prompts/longpause_zh-yue.txt
@@ -0,0 +1 @@
+而家我哋要等一陣,然後翻溫。喺第一課我哋仲未學習好多嘅詞語,所以停頓會比較長,但係喺未來嘅課程,我哋唔會有咁長嘅停頓
diff --git a/hanzi-prompts/meaningis_zh-yue.txt b/hanzi-prompts/meaningis_zh-yue.txt
new file mode 100644
index 0000000..a4c75cb
--- /dev/null
+++ b/hanzi-prompts/meaningis_zh-yue.txt
@@ -0,0 +1 @@
+意思係
diff --git a/hanzi-prompts/nowPleaseSay_zh-yue.txt b/hanzi-prompts/nowPleaseSay_zh-yue.txt
new file mode 100644
index 0000000..92923db
--- /dev/null
+++ b/hanzi-prompts/nowPleaseSay_zh-yue.txt
@@ -0,0 +1 @@
+而家請講
diff --git a/hanzi-prompts/pleaseSay_zh-yue.txt b/hanzi-prompts/pleaseSay_zh-yue.txt
new file mode 100644
index 0000000..cce3b70
--- /dev/null
+++ b/hanzi-prompts/pleaseSay_zh-yue.txt
@@ -0,0 +1 @@
+請講
diff --git a/hanzi-prompts/repeatAfterMe_zh-yue.txt b/hanzi-prompts/repeatAfterMe_zh-yue.txt
new file mode 100644
index 0000000..09aaa03
--- /dev/null
+++ b/hanzi-prompts/repeatAfterMe_zh-yue.txt
@@ -0,0 +1 @@
+請跟住講
diff --git a/hanzi-prompts/sayAgain_zh-yue.txt b/hanzi-prompts/sayAgain_zh-yue.txt
new file mode 100644
index 0000000..13ca92f
--- /dev/null
+++ b/hanzi-prompts/sayAgain_zh-yue.txt
@@ -0,0 +1 @@
+再講一次
diff --git a/hanzi-prompts/tryToSay_zh-yue.txt b/hanzi-prompts/tryToSay_zh-yue.txt
new file mode 100644
index 0000000..d43c674
--- /dev/null
+++ b/hanzi-prompts/tryToSay_zh-yue.txt
@@ -0,0 +1 @@
+試吓講
diff --git a/hanzi-prompts/whatSay_zh-yue.txt b/hanzi-prompts/whatSay_zh-yue.txt
new file mode 100644
index 0000000..aed1a57
--- /dev/null
+++ b/hanzi-prompts/whatSay_zh-yue.txt
@@ -0,0 +1 @@
+點講
diff --git a/hanzi-prompts/whatmean_zh-yue.txt b/hanzi-prompts/whatmean_zh-yue.txt
new file mode 100644
index 0000000..0aaf415
--- /dev/null
+++ b/hanzi-prompts/whatmean_zh-yue.txt
@@ -0,0 +1 @@
+乜嘢意思?
diff --git a/hanzi-prompts/whatmean_zh-yue_2.txt b/hanzi-prompts/whatmean_zh-yue_2.txt
new file mode 100644
index 0000000..87e6d63
--- /dev/null
+++ b/hanzi-prompts/whatmean_zh-yue_2.txt
@@ -0,0 +1 @@
+係乜嘢意思?
diff --git a/hanzi-prompts/whatmean_zh-yue_3.txt b/hanzi-prompts/whatmean_zh-yue_3.txt
new file mode 100644
index 0000000..da79d2e
--- /dev/null
+++ b/hanzi-prompts/whatmean_zh-yue_3.txt
@@ -0,0 +1 @@
+乜嘢意思呢?
diff --git a/mac/start-gradint.app/Contents/MacOS/start-gradint b/mac/start-gradint.app/Contents/MacOS/start-gradint
index 1b697b7..c0f531b 100755
--- a/mac/start-gradint.app/Contents/MacOS/start-gradint
+++ b/mac/start-gradint.app/Contents/MacOS/start-gradint
@@ -1,5 +1,6 @@
#!/bin/bash
-export PATH="$PATH:/usr/local/bin" # in case lame etc is there
+export PATH="/usr/local/bin:$PATH" # for python3 override + in case lame etc is there
+cd "${BASH_SOURCE%/*}/../.." # needed on macOS 14, possibly 13
if sw_vers 2>/dev/null|grep ^ProductVersion.*1[2-9]; then # macOS 12+
if test $(python3 -c 'import tkinter,sys;print(sys.version_info[:3]>=(3,10,1))' 2>/dev/null) = "True"; then exec python3 gradint.py; fi
osascript -e "tell application (path to frontmost application as text) to display dialog \"macOS 12 bundled a broken version of the GUI libraries: please install Python 3 from python.org before running Gradint\" buttons {\"OK\"} with icon stop"
diff --git a/samples/utils/autosplit.py b/samples/utils/autosplit.py
old mode 100644
new mode 100755
diff --git a/samples/utils/cache-synth.py b/samples/utils/cache-synth.py
old mode 100644
new mode 100755
diff --git a/samples/utils/cleanup-cache.py b/samples/utils/cleanup-cache.py
old mode 100644
new mode 100755
diff --git a/samples/utils/diagram.py b/samples/utils/diagram.py
old mode 100644
new mode 100755
diff --git a/samples/utils/list-synth.py b/samples/utils/list-synth.py
old mode 100644
new mode 100755
diff --git a/samples/utils/list2cache.py b/samples/utils/list2cache.py
old mode 100644
new mode 100755
diff --git a/samples/utils/manual-splitter.py b/samples/utils/manual-splitter.py
old mode 100644
new mode 100755
diff --git a/samples/utils/player.py b/samples/utils/player.py
old mode 100644
new mode 100755
index e09ed01..409d814
--- a/samples/utils/player.py
+++ b/samples/utils/player.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# (should work in both Python 2 and Python 3)
-# Simple sound-playing server v1.56
+# Simple sound-playing server v1.58
# Silas S. Brown - public domain - no warranty
# connect to port 8124 (assumes behind firewall)
@@ -13,8 +13,9 @@
import socket, select, os, sys, os.path, time, re
for a in sys.argv[1:]:
- if a.startswith("--rpi-bluetooth-setup"): # tested on Raspberry Pi 400 with Raspbian 11; also tested on Raspberry Pi Zero W with Raspbian 10 Lite (with the device already paired: needed to say "scan on", "discovery on", remove + pair in bluetoothctl). Send Eth=(bluetooth Ethernet addr) to start. Note that the setup command reboots the system.
- os.system('if [ -e /etc/xdg/lxsession/LXDE-pi/autostart ]; then mkdir -p /home/pi/.config/lxsession/LXDE-pi && cp /etc/xdg/lxsession/LXDE-pi/autostart /home/pi/.config/lxsession/LXDE-pi/ && echo sudo ethtool --set-eee eth0 eee off >> /home/pi/.config/lxsession/LXDE-pi/autostart && echo python '+os.path.join(os.getcwd(),sys.argv[0])+' >> /home/pi/.config/lxsession/LXDE-pi/autostart; else (echo "[Unit]";echo "Descrption=Gradint player utility";echo "[Service]";echo "Type=oneshot";echo "ExecStart='+os.path.join(os.getcwd(),sys.argv[0])+'";echo "[Install]";echo "WantedBy=multi-user.target") > player.service && sudo mv player.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable player && chmod +x '+sys.argv[0]+' && awk '+"'"+'// {print} /^import / {print "os.system('+"'"+'"'+"'"+'"'+"'"+'pulseaudio --start'+"'"+'"'+"'"+'"'+"'"+')"}'+"'"+' < '+sys.argv[0]+' > .playerTMP && mv .playerTMP '+sys.argv[0]+'; fi && sudo "apt-get -y install sox mpg123 pulseaudio pulseaudio-module-bluetooth && usermod -G bluetooth -a pi && (echo load-module module-switch-on-connect;echo load-module module-bluetooth-policy;echo load-module module-bluetooth-discover) >> /etc/pulse/default.pa && (echo [General];echo FastConnectable = true) >> /etc/bluetooth/main.conf && reboot"') # (eee off: improves reliability of gigabit ethernet on RPi400)
+ if a.startswith("--rpi-bluetooth-setup"): # tested on Raspberry Pi 400 with OS versions 11 and 12; also tested on Raspberry Pi Zero W with Raspbian 10 Lite (with the device already paired: needed to say "scan on", "discovery on", remove + pair in bluetoothctl). Send Eth=(bluetooth Ethernet addr) to start. Note that the setup command reboots the system.
+ # NOTE: If running on Pi with OS 12 and you've also done "raspi-config" to set things back to PulseAudio (as needed for example for language-synchronised Bluetooth playing in http://ssb22.user.srcf.net/s60/video.html notes), you might need to replace 'ExecStart=' with 'ExecStart=bash -c "while ! ssh localhost true; do sleep 1; done; ssh localhost ' below (and add a " at end of line), and do an ssh-keygen and add to authorized_keys, so player is run in a separate session from systemd (even though the user is the same; it's not clear why this is needed)
+ os.system('(echo "[Unit]";echo "Description=Gradint player utility";echo "[Service]";echo "Type=oneshot";echo "ExecStart='+os.path.join(os.getcwd(),sys.argv[0])+'";echo "WorkingDirectory='+os.path.getcwd()+'";echo User="$(whoami)";echo "[Install]";echo "WantedBy=multi-user.target") > player.service && sudo mv player.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable player && chmod +x '+sys.argv[0]+' && sudo bash -c "apt-get -y install sox mpg123 pulseaudio pulseaudio-module-bluetooth && usermod -G bluetooth -a $USER && (echo load-module module-switch-on-connect;echo load-module module-bluetooth-policy;echo load-module module-bluetooth-discover) >> /etc/pulse/default.pa && (echo [General];echo FastConnectable = true) >> /etc/bluetooth/main.conf && reboot"') # (eee off: improves reliability of gigabit ethernet on RPi400)
elif a=="--aplay": use_aplay = True # aplay and madplay, for older embedded devices, NOT tested together with --rpi-bluetooth-* above
elif a.startswith("--delegate="): delegate_to_check=a.split('=')[1] # will ping that IP and delegate all sound to it when it's up. E.g. if it has better amplification but it's not always switched on.
elif a.startswith("--chime="): chime_mp3=a.split('=')[1] # if clock bell desired, e.g. echo '$i-14vfff$c48o0l1b- @'|mwr2ly > chime.ly && lilypond chime.ly && timidity -Ow chime.midi && audacity chime.wav (amplify + trim) + mp3-encode (keep default 44100 sample rate so ~38 frames per sec). Not designed to work with --delegate. Pi1's 3.5mm o/p doesn't sound very good with this bell.
@@ -69,9 +70,9 @@
continue
elif d=='QUIT':
s.close() ; break
- elif d=="Eth=": # Eth=ethernet address, to connect via Bluetooth, tested on Raspberry Pi 400 with Raspbian 11
+ elif d=="Eth=": # Eth=ethernet address to connect via Bluetooth (see --rpi-bluetooth-setup above)
eth = S(c.recv(17))
- assert re.match("^[A-Fa-f0-9:]*$",eth)
+ assert re.match("^[A-Fa-f0-9:]+$",eth)
os.system("M=/dev/null;E="+eth+";if ! pacmd list-sinks | grep "+eth.replace(":","_")+" >$M; then while true; do bluetoothctl --timeout 1 disconnect | grep Missing >$M||sleep 5;T=5;while ! bluetoothctl --timeout $T connect $E | tee $M | egrep \"Connection successful|Device $E Connected: yes\"; do sleep 5; T=10;M=/dev/stderr;bluetoothctl --timeout 1 devices;echo Retrying $E; done ; Got=0; for Try in 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m n o p q r s t u v w x y z; do if pacmd list-sinks | grep "+eth.replace(":","_")+" >/dev/null; then Got=1; break; fi; sleep 1; done; if [ $Got = 1 ] ; then break; fi; done; fi; pacmd set-default-sink bluez_sink."+eth.replace(":","_")+".a2dp_sink") # ; play /usr/share/scratch/Media/Sounds/Animal/Dog1.wav # (not really necessary if using 'close the socket' to signal we're ready)
c.close() ; continue
elif d=="Eth0":
diff --git a/samples/utils/recover-unavail.py b/samples/utils/recover-unavail.py
old mode 100644
new mode 100755
diff --git a/samples/utils/synth-batchconvert-helper.py b/samples/utils/synth-batchconvert-helper.py
old mode 100644
new mode 100755
diff --git a/samples/utils/trace.py b/samples/utils/trace.py
old mode 100644
new mode 100755
diff --git a/samples/utils/transliterate.py b/samples/utils/transliterate.py
old mode 100644
new mode 100755
diff --git a/server/cantonese.py b/server/cantonese.py
old mode 100644
new mode 100755
index a5698c1..ecc4576
--- a/server/cantonese.py
+++ b/server/cantonese.py
@@ -5,7 +5,7 @@
# cantonese.py - Python functions for processing Cantonese transliterations
# (uses eSpeak and Gradint for help with some of them)
-# v1.42 (c) 2013-15,2017-23 Silas S. Brown. License: GPL
+# v1.48 (c) 2013-15,2017-24 Silas S. Brown. License: GPL
cache = {} # to avoid repeated eSpeak runs,
# zi -> jyutping or (pinyin,) -> translit
@@ -64,7 +64,7 @@ def hanzi_only(unitext): return u"".join(filter(lambda x:0x4e00<=ord(x)<0xa700 o
def py2nums(pinyin):
if not type(pinyin)==type(u""):
pinyin = pinyin.decode('utf-8')
- assert pinyin.strip(), "blank pinyin" # saves figuring out a findall TypeError
+ if not pinyin.strip(): return ""
global pinyin_dryrun
if pinyin_dryrun:
pinyin_dryrun = list(pinyin_dryrun)
@@ -91,7 +91,7 @@ def adjust_jyutping_for_pinyin(hanzi,jyutping,pinyin):
i = 0 ; tones = re.finditer('[1-7]',jyutping) ; j2 = []
for h,p in zip(list(hanzi),pinyin):
try: j = getNext(tones).end()
- except StopIteration: return jyutping # one of the zin has no Cantonese reading, which we'll pick up later on "failed to fix"
+ except StopIteration: return jyutping # one of the hanzi has no Cantonese reading in our data: we'll warn "failed to fix" below
j2.append(jyutping[i:j]) ; i = j
if h in py2j and p.lower() in py2j[h]: j2[-1]=j2[-1][:re.search("[A-Za-z]*[1-7]$",j2[-1]).start()]+py2j[h][p.lower()]
return "".join(j2)+jyutping[i:]
@@ -100,8 +100,9 @@ def adjust_jyutping_for_pinyin(hanzi,jyutping,pinyin):
u"\u4E3A\u70BA":{"wei2":"wai4","wei4":"wai6"},
u"\u4E50\u6A02":{"le4":"lok6","yue4":"ngok6"},
u"\u4EB2\u89AA":{"qin1":"can1","qing4":"can3"},
+u"\u4EC0":{"shen2":"sam6","shi2":"sap6"}, # unless zaap6
u"\u4F20\u50B3":{"chuan2":"cyun4","zhuan4":"zyun6"},
-u"\u4FBF":{"bian4":"pin4","pian2":"bin6"},
+u"\u4FBF":{"bian4":"bin6","pian2":"pin4"},
u"\u5047":{"jia3":"gaa2","jia4":"gaa3"},
u"\u5174\u8208":{"xing1":"hing1","xing4":"hing3"},
# u"\u5207":{"qie4":"cai3","qie1":"cit3"}, # WRONG (rm'd v1.17). It's cit3 in re4qie4. It just wasn't in yiqie4 (which zhy_list has as an exception anyway)
@@ -153,10 +154,10 @@ def adjust_jyutping_for_pinyin(hanzi,jyutping,pinyin):
def jyutping_to_lau(j):
j = S(j).lower().replace("j","y").replace("z","j")
for k,v in jlRep: j=j.replace(k,v)
- return j.lower().replace("aa","a").replace("ohek","euk")
+ return j.lower().replace("ohek","euk")
def jyutping_to_lau_java(jyutpingNo=2,lauNo=1):
# for annogen.py 3.29+ --annotation-postprocess to ship Jyutping and generate Lau at runtime
- return 'if(annotNo=='+str(jyutpingNo)+'||annotNo=='+str(lauNo)+'){m=Pattern.compile("").matcher(r);sb=new StringBuffer();while(m.find()){String r2=(annotNo=='+str(jyutpingNo)+'?m.group(1).replaceAll("([1-7])(.)","$1$2"):(m.group(1)+" ").toLowerCase().replace("j","y").replace("z","j")'+''.join('.replace("'+k+'","'+v+'")' for k,v in jlRep)+'.toLowerCase().replace("aa","a").replace("ohek","euk").replaceAll("([1-7])","$1-").replace("- "," ").replaceAll(" $","")),tmp=m.group(1).substring(0,1);if(annotNo=='+str(lauNo)+'&&tmp.equals(tmp.toUpperCase()))r2=r2.substring(0,1).toUpperCase()+r2.substring(1);m.appendReplacement(sb,"");}m.appendTail(sb); r=sb.toString();}' # TODO: can probably go faster with mapping for some of this
+ return 'if(annotNo=='+str(jyutpingNo)+'||annotNo=='+str(lauNo)+'){m=Pattern.compile("").matcher(r);sb=new StringBuffer();while(m.find()){String r2=(annotNo=='+str(jyutpingNo)+'?m.group(1).replaceAll("([1-7])(.)","$1$2"):(m.group(1)+" ").toLowerCase().replace("j","y").replace("z","j")'+''.join('.replace("'+k+'","'+v+'")' for k,v in jlRep)+'.toLowerCase().replace("ohek","euk").replaceAll("([1-7])","$1-").replace("- "," ").replaceAll(" $","")),tmp=m.group(1).substring(0,1);if(annotNo=='+str(lauNo)+'&&tmp.equals(tmp.toUpperCase()))r2=r2.substring(0,1).toUpperCase()+r2.substring(1);m.appendReplacement(sb,"");}m.appendTail(sb); r=sb.toString();}' # TODO: can probably go faster with mapping for some of this
def incomplete_lau_to_jyutping(l):
# incomplete: assumes Lau didn't do the "aa" -> "a" rule
l = S(l).lower().replace("euk","ohek")
@@ -236,7 +237,10 @@ def mysub(z,l):
z = re.sub(re.escape(x)+r"(.)",r"\1"+y,z)
return z
if type(u"")==type(""): U=str # Python 3
- else: U=unicode # Python 2
+ else: # Python 2
+ def U(x):
+ try: return x.decode('utf-8') # might be an emoji pass-through
+ except: return x # already Unicode
return unicodedata.normalize('NFC',mysub(U(jyutping_to_yale_TeX(j).replace(r"\i{}","i").replace(r"\I{}","I")),[(r"\`",u"\u0300"),(r"\'",u"\u0301"),(r"\=",u"\u0304")])).encode('utf-8')
def superscript_digits_TeX(j):
@@ -291,6 +295,9 @@ def songSubst(l):
pinyin = pinyin.decode('utf-8')
if pinyin and not (pinyin,) in cache:
pinyin_dryrun.add(pinyin)
+ for w in pinyin.split():
+ for h in w.split('-'):
+ pinyin_dryrun.add(h)
dryrun_mode = False
for l in lines:
if '#' in l: l,pinyin = l.split('#')
@@ -300,7 +307,7 @@ def songSubst(l):
elif pinyin:
jyutping = adjust_jyutping_for_pinyin(l,jyutping,pinyin)
groupLens = [0]
- for syl,space in re.findall('([A-Za-z]*[1-5])( *)',py2nums(pinyin)):
+ for syl,space in re.findall('([A-Za-z]*[1-5])( *)',' '.join('-'.join(py2nums(h) for h in w.split('-')) for w in pinyin.split())): # doing it this way so we're not relying on espeak transliterate_multiple to preserve spacing and hyphenation
groupLens[-1] += 1
if space: groupLens.append(0)
if not groupLens[-1]: groupLens=groupLens[:-1]
diff --git a/server/email-lesson.sh b/server/email-lesson.sh
index 8406ee7..17e0d95 100755
--- a/server/email-lesson.sh
+++ b/server/email-lesson.sh
@@ -3,9 +3,9 @@
# email-lesson.sh: a script that can help you to
# automatically distribute daily Gradint lessons
# to students using a web server with reminder
-# emails. Version 1.15
+# emails. Version 1.16
-# (C) 2007-2010,2020-2022 Silas S. Brown, License: GPL
+# (C) 2007-2010,2020-2022,2024 Silas S. Brown, License: GPL
# 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
@@ -39,7 +39,7 @@ elif which mutt >/dev/null 2>/dev/null; then DefaultMailProg="mutt -x"
else DefaultMailProg="ssh example.org mail"
fi
-if test "a$1" == "a--run"; then
+if [ "$1" == "--run" ]; then
set -o pipefail # make sure errors in pipes are reported
if ! [ -d email_lesson_users ]; then
echo "Error: script does not seem to have been set up yet"
@@ -61,14 +61,14 @@ if test "a$1" == "a--run"; then
while true; do ssh -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS -n -o ControlMaster=yes $ControlPath $(echo "$PUBLIC_HTML"|sed -e 's/:.*//') sleep 86400; sleep 10; done & MasterPid=$!
else unset MasterPid
fi
- (while ! bash -c "$CAT_LOGS_COMMAND"; do echo "cat-logs failed, re-trying in 61 seconds" 1>&2;sleep 61; done) | grep '/user\.' > "$TMPDIR/._email_lesson_logs"
+ (while ! bash -c "$CAT_LOGS_COMMAND"; do echo "cat-logs failed, re-trying in 61 seconds" >&2;sleep 61; done) | grep '/user\.' > "$TMPDIR/._email_lesson_logs"
# (note: sleeping odd numbers of seconds so we can tell where it is if it gets stuck in one of these loops)
Users="$(echo user.*)"
cd ..
unset NeedRunMirror
for U in $Users; do
. email_lesson_users/config
- if ! test "a$GLOBAL_GRADINT_OPTIONS" == a; then GLOBAL_GRADINT_OPTIONS="$GLOBAL_GRADINT_OPTIONS ;"; fi
+ if [ "$GLOBAL_GRADINT_OPTIONS" ]; then GLOBAL_GRADINT_OPTIONS="$GLOBAL_GRADINT_OPTIONS ;"; fi
# set some (but not all!) variables to defaults in case not set in profile
SUBJECT_LINE="$DEFAULT_SUBJECT_LINE"
FORGOT_YESTERDAY="$DEFAULT_FORGOT_YESTERDAY"
@@ -85,7 +85,7 @@ if test "a$1" == "a--run"; then
mv "email_lesson_users/$U/profile.removeCR" "email_lesson_users/$U/profile"
fi
. "email_lesson_users/$U/profile"
- if test "a$Use_M3U" == ayes; then FILE_TYPE_2=m3u
+ if [ "$Use_M3U" == yes ]; then FILE_TYPE_2=m3u
else FILE_TYPE_2=$FILE_TYPE; fi
if echo "$MailProg" | grep ssh >/dev/null; then
# ssh discards a level of quoting, so we need to be more careful
@@ -94,7 +94,7 @@ if test "a$1" == "a--run"; then
Extra_Mailprog_Params2="\"$Extra_Mailprog_Params2\""
fi
if [ -e "email_lesson_users/$U/lastdate" ]; then
- if test "$(cat "email_lesson_users/$U/lastdate")" == "$(date +%Y%m%d)"; then
+ if [ "$(cat "email_lesson_users/$U/lastdate")" == "$(date +%Y%m%d)" ]; then
# still on same day - do nothing with this user this time
continue
fi
@@ -114,10 +114,10 @@ if test "a$1" == "a--run"; then
fi
else Did_Download=1; fi
rm -f "email_lesson_users/$U/rollback"
- if test $Did_Download == 0; then
+ if [ $Did_Download == 0 ]; then
# send a reminder
DaysOld="$(python -c "import os,time;print(int((time.time()-os.stat('email_lesson_users/$U/lastdate').st_mtime)/3600/24))")"
- if test $DaysOld -lt 5 || test $(date +%u) == 1; then # (remind only on Mondays if not checked for 5 days, to avoid filling up inboxes when people are away and can't get to email)
+ if [ $DaysOld -lt 5 ] || [ $(date +%u) == 1 ]; then # (remind only on Mondays if not checked for 5 days, to avoid filling up inboxes when people are away and can't get to email)
while ! $MailProg -s "$SUBJECT_LINE" "$STUDENT_EMAIL" "$Extra_Mailprog_Params1" "$Extra_Mailprog_Params2" </dev/null; then OUTDIR=$TMPDIR
else OUTDIR=$PUBLIC_HTML; fi
USER_GRADINT_OPTIONS="$GLOBAL_GRADINT_OPTIONS $GRADINT_OPTIONS samplesDirectory='email_lesson_users/$U/samples'; progressFile='email_lesson_users/$U/progress.txt'; pickledProgressFile='email_lesson_users/$U/progress.bin'; vocabFile='email_lesson_users/$U/vocab.txt';saveLesson='';loadLesson=0;progressFileBackup='email_lesson_users/$U/progress.bak';outputFile="
@@ -147,14 +147,14 @@ do echo "mail sending failed; retrying in 62 seconds"; sleep 62; done; fi
tail -$NumLines "email_lesson_users/$U/podcasts-to-send" > "email_lesson_users/$U/podcasts-to-send2"
mv "email_lesson_users/$U/podcasts-to-send" "email_lesson_users/$U/podcasts-to-send.old"
mv "email_lesson_users/$U/podcasts-to-send2" "email_lesson_users/$U/podcasts-to-send"
- if test $NumLines == 0; then
+ if [ $NumLines == 0 ]; then
echo "$U" | $MailProg -s Warning:email-lesson-run-out-of-podcasts $ADMIN_EMAIL
fi
else rm -f "email_lesson_users/$U/podcasts-to-send.old" # won't be a rollback after this
fi
- if test "$ENCODE_ON_REMOTE_HOST" == 1; then
+ if [ "$ENCODE_ON_REMOTE_HOST" == 1 ]; then
ToSleep=123
- while ! if test "a$Send_Podcast_Instead" == a; then
+ while ! if [ ! "$Send_Podcast_Instead" ]; then
python gradint.py "$USER_GRADINT_OPTIONS '-.sh'" "$TMPDIR/__stderr" | ssh -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $ControlPath $(echo "$PUBLIC_HTML"|sed -e 's/:.*//') "mkdir -p $REMOTE_WORKING_DIR; cd $REMOTE_WORKING_DIR; cat > __gradint.sh;chmod +x __gradint.sh;PATH=$SOX_PATH ./__gradint.sh|$ENCODING_COMMAND $(echo $PUBLIC_HTML|sed -e 's/[^:]*://')/$U-$CurDate.$FILE_TYPE;rm -f __gradint.sh";
else
cd "email_lesson_users/$U" ; cat "$Send_Podcast_Instead" | ssh -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $ControlPath $(echo "$PUBLIC_HTML"|sed -e 's/:.*//') "cat > $(echo $PUBLIC_HTML|sed -e 's/[^:]*://')/$U-$CurDate.$FILE_TYPE"; cd ../..;
@@ -166,18 +166,18 @@ do echo "mail sending failed; retrying in 62 seconds"; sleep 62; done; fi
sleep $ToSleep ; ToSleep=$[$ToSleep*1.5] # (increasing-time retries)
done
rm "$TMPDIR/__stderr"
- if test "a$Use_M3U" == ayes; then
+ if [ "$Use_M3U" == yes ]; then
while ! ssh -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $ControlPath $(echo "$PUBLIC_HTML"|sed -e 's/:.*//') "echo $OUTSIDE_LOCATION/$U-$CurDate.$FILE_TYPE > $(echo $PUBLIC_HTML|sed -e 's/[^:]*://')/$U-$CurDate.m3u"; do sleep 63; done
fi
else # not ENCODE_ON_REMOTE_HOST
- if ! test "a$Send_Podcast_Instead" == a; then
+ if [ "$Send_Podcast_Instead" ]; then
(cd "email_lesson_users/$U" ; cat "$Send_Podcast_Instead") > "$OUTDIR/$U-$CurDate.$FILE_TYPE"
elif ! python gradint.py "$USER_GRADINT_OPTIONS '$OUTDIR/$U-$CurDate.$FILE_TYPE'" "$OUTDIR/$U-$CurDate.m3u"
fi
if echo "$PUBLIC_HTML" | grep : >/dev/null; then
@@ -200,14 +200,14 @@ EOF
do echo "mail sending failed; retrying in 65 seconds"; sleep 65; done
echo "$CurDate" > "email_lesson_users/$U/lastdate"
unset AdminNote
- if test "a$Send_Podcast_Instead" == a; then
- if test "$(zgrep -H -m 1 lessonsLeft "email_lesson_users/$U/progress.txt"|sed -e 's/.*=//')" == 0; then AdminNote="Note: $U has run out of new words"; fi
+ if [ "$Send_Podcast_Instead" == a ]; then
+ if [ "$(zgrep -H -m 1 lessonsLeft "email_lesson_users/$U/progress.txt"|sed -e 's/.*=//')" == 0 ]; then AdminNote="Note: $U has run out of new words"; fi
elif ! [ -e "email_lesson_users/$U/podcasts-to-send" ]; then AdminNote="Note: $U has run out of podcasts"; fi
- if ! test "a$AdminNote" == a; then
+ if [ "$AdminNote" ]; then
while ! echo "$AdminNote"|$MailProg -s gradint-user-ran-out "$ADMIN_EMAIL"; do echo "Mail sending failed; retrying in 67 seconds"; sleep 67; done
fi
done # end of per-user loop
- if test "a$NeedRunMirror" == "a1" && ! test "a$PUBLIC_HTML_MIRROR_COMMAND" == a; then
+ if [ "$NeedRunMirror" == "1" ] && [ "$PUBLIC_HTML_MIRROR_COMMAND" ]; then
while ! $PUBLIC_HTML_MIRROR_COMMAND; do
echo "PUBLIC_HTML_MIRROR_COMMAND failed; retrying in 79 seconds"
echo As subject | $MailProg -s "PUBLIC_HTML_MIRROR_COMMAND failed, will retry" "$ADMIN_EMAIL" || true # ignore errors
@@ -215,9 +215,9 @@ do echo "mail sending failed; retrying in 65 seconds"; sleep 65; done
done
fi
rm -f "$TMPDIR/._email_lesson_logs"
- if ! test a$MasterPid == a; then
+ if [ $MasterPid ] ; then
kill $MasterPid
- kill $(ps axwww|grep "$TMPDIR/__gradint_ctrl"|sed -e 's/^ *//' -e 's/ .*//') 2>/dev/null
+ kill $(pgrep -f "$TMPDIR/__gradint_ctrl") 2>/dev/null
rm -f "$TMPDIR/__gradint_ctrl" # in case ssh doesn't
fi
rm -f "$Gradint_Dir/.email-lesson-running"
@@ -227,7 +227,7 @@ fi
echo "After setting up users, run this script daily with --run on the command line."
echo "As --run was not specified, it will now go into setup mode."
# Setup:
-if test "a$EDITOR" == a; then
+if ! [ "$EDITOR" ]; then
echo "Error: No EDITOR environment variable set"; exit 1
fi
if ! [ -e email_lesson_users/config ]; then
@@ -286,7 +286,7 @@ while true; do
echo "Type a user alias (or just press Enter) to add a new user, or Ctrl-C to quit"
read Alias
ID=$(mktemp -d user.$(python -c 'import random; print(random.random())')XXXXXX) # (newer versions of mktemp allow more than 6 X's so the python step isn't necessary, but just in case we want to make sure that it's hard to guess the ID)
- if ! test "a$Alias" == a; then ln -s "$ID" "$Alias"; fi
+ if [ "$Alias" ]; then ln -s "$ID" "$Alias"; fi
cd "$ID" || exit 1
cat > profile <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
+ langFullName[abbr]=name.replace("_","-")
# Try to work out probable default language:
lang = os.environ.get("HTTP_ACCEPT_LANGUAGE","")
if lang:
@@ -74,8 +80,10 @@ reinit_gradint()
def main():
if "id" in query: # e.g. from redirectHomeKeepCookie
- os.environ["HTTP_COOKIE"]="id="+query.getfirst("id")
- print ('Set-Cookie: id=' + query.getfirst("id")+'; expires=Wed, 1 Dec 2036 23:59:59 GMT')
+ 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)
filetype=""
if "filetype" in query: filetype=query.getfirst("filetype")
@@ -95,19 +103,19 @@ def main():
gradint.justSynthesize="0"
if "l2w" in query and query.getfirst("l2w"):
gradint.startBrowser=lambda *args:0
- if query.getfirst("l2")=="zh" and gradint.sanityCheck(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 sanityCheck in js, if so don't call HTML5 audio, then we can have an on-screen message here)
+ 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('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 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.getfirst("l2w") and "l1w" in query and query.getfirst("l1w"):
gradint.startBrowser=lambda *args:0
- if query.getfirst("l2")=="zh": scmsg=gradint.sanityCheck(query.getfirst("l2w"),"zh")
- else: scmsg=None
- if scmsg: htmlOut(gradint.B(scmsg)+gradint.B(backLink))
+ 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('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
+ 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):
@@ -124,7 +132,7 @@ def main():
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.getfirst("l1"),query.getfirst("l2")) and not query.getfirst("clang")=="ignore-unchanged": return htmlOut('You must change the settings before pressing the Change Languages button.'+backLink) # (external scripts can set clang=ignore-unchanged)
+ 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
@@ -142,12 +150,24 @@ def main():
try: v=open(gradint.vocabFile).read()
except: v="" # (shouldn't get here unless they hack URLs)
htmlOut('',"Text edit your vocab list")
- elif "lesson" in query: # make lesson
+ elif "lesson" in query: # make lesson ("Start lesson" button)
setup_userID()
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=""
@@ -199,8 +219,10 @@ 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"] # will be http:// or https:// as appropriate
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('