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 2871 additions and 1084 deletions
......@@ -6,7 +6,7 @@
# - report is written to standard output so you can include
# it in a script that makes some larger HTML page
# v1.1 (C) 2007, 2009 Silas S. Brown, License: GPL
# v1.12 (C) 2007, 2009. 2021-22 Silas S. Brown, License: GPL
if ! pwd|grep email_lesson_users >/dev/null; then
echo "This script should be run from an email_lesson_users directory (see email-lesson.sh)"
......@@ -14,13 +14,13 @@ if ! pwd|grep email_lesson_users >/dev/null; then
fi
echo '<TABLE>'
touch -d 0:00 /dev/shm/.midnight 2>/dev/null || touch -d 0:00 /tmp/.midnight
if test -e /dev/shm/.midnight; then export Midnight=/dev/shm/.midnight; else export Midnight=/tmp/.midnight; fi
for P in $(ls --color=none -t */progress.txt */podcasts-to-send 2>/dev/null); do
if test $P -nt $Midnight; then export Em="*";else unset Em; fi
if echo $P | grep podcasts-to-send$ >/dev/null; then
zgrep -H -m 1 . $P|grep -v ^user\.|sed -e 's,/.*:,</TD><TD COLSPAN=4>,' -e "s/^/<TR><TD>$Em/" -e "s,$,</TD></TR>,"
if [ -e /dev/shm/.midnight ]; then Midnight=/dev/shm/.midnight; else Midnight=/tmp/.midnight; fi
for P in $(ls --color=none -t -- */progress.txt */podcasts-to-send 2>/dev/null); do
if test "$P" -nt $Midnight; then Em="*";else unset Em; fi
if echo "$P" | grep podcasts-to-send$ >/dev/null; then
zgrep -H -m 1 . "$P"|grep -v ^user\.|sed -e 's,/.*:,</TD><TD COLSPAN=4>,' -e "s/^/<TR><TD>$Em/" -e "s,$,</TD></TR>,"
else
zgrep -H -m 1 lessonsLeft $P|grep -v user\.|sed -e 's,/.*#,,' -e "s/^/<TR><TD>$Em/" -e "s, ,</TD><TD>,g" -e "s,$,</TD></TR>," -e "s/=/: /g"
zgrep -H -m 1 lessonsLeft "$P"|grep -v user\.|sed -e 's,/.*#,,' -e "s/^/<TR><TD>$Em/" -e "s, ,</TD><TD>,g" -e "s,$,</TD></TR>," -e "s/=/: /g"
fi
done
rm $Midnight
......
......@@ -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.1128
# emails. Version 1.16
# (C) 2007-2010 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
......@@ -17,110 +17,110 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
export DEFAULT_SUBJECT_LINE="Vocabulary practice (automatic message from gradint)"
export DEFAULT_FORGOT_YESTERDAY="You forgot your lesson yesterday.
DEFAULT_SUBJECT_LINE="Vocabulary practice (automatic message from gradint)"
DEFAULT_FORGOT_YESTERDAY="You forgot your lesson yesterday.
Please remember to download your lesson from"
# (NB include the words "you forgot" so that it's obvious this is a reminder not an additional lesson)
export DEFAULT_EXPLAIN_FORGOT="Please try to hear one lesson every day. If you download that lesson today,
DEFAULT_EXPLAIN_FORGOT="Please try to hear one lesson every day. If you download that lesson today,
this program will make the next one for tomorrow."
export DEFAULT_NEW_LESSON="Your lesson for today is at"
export DEFAULT_LISTEN_TODAY="Please download and listen to it today."
export DEFAULT_AUTO_MESSAGE="This is an automatic message from the gradint program.
DEFAULT_NEW_LESSON="Your lesson for today is at"
DEFAULT_LISTEN_TODAY="Please download and listen to it today."
DEFAULT_AUTO_MESSAGE="This is an automatic message from the gradint program.
Any problems, requests, or if you no longer wish to receive these emails,
let me know."
if ! test -e gradint.py; then
if ! [ -e gradint.py ]; then
echo "Error: This script should ALWAYS be run in the gradint directory."
exit 1
fi
if which mail >/dev/null 2>/dev/null; then export DefaultMailProg=mail
elif which mutt >/dev/null 2>/dev/null; then export DefaultMailProg="mutt -x"
else export DefaultMailProg="ssh example.org mail"
if which mail >/dev/null 2>/dev/null; then DefaultMailProg=mail
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 ! test -d email_lesson_users; then
if ! [ -d email_lesson_users ]; then
echo "Error: script does not seem to have been set up yet"
exit 1
fi
export Gradint_Dir=$(pwd)
cd email_lesson_users
Gradint_Dir=$(pwd)
cd email_lesson_users || exit
. config
if test -e $Gradint_Dir/.email-lesson-running; then
export Msg="Another email-lesson.sh --run is running - exitting. (Remove $Gradint_Dir/.email-lesson-running if this isn't the case.)"
if [ -e "$Gradint_Dir/.email-lesson-running" ]; then
Msg="Another email-lesson.sh --run is running - exitting. (Remove $Gradint_Dir/.email-lesson-running if this isn't the case.)"
echo "$Msg"
echo "$Msg"|$MailProg -s email-lesson-not-running $ADMIN_EMAIL # don't worry about retrying that
exit 1
fi
touch $Gradint_Dir/.email-lesson-running
if echo $PUBLIC_HTML | grep : >/dev/null && man ssh 2>/dev/null | grep ControlMaster >/dev/null; then
touch "$Gradint_Dir/.email-lesson-running"
if echo "$PUBLIC_HTML" | grep : >/dev/null && man ssh 2>/dev/null | grep ControlMaster >/dev/null; then
# this version of ssh is new enough to support ControlPath, and PUBLIC_HTML indicates a remote host, so let's do it all through one connection
export ControlPath="-o ControlPath=$TMPDIR/__gradint_ctrl"
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 & export MasterPid=$!
ControlPath="-o ControlPath=$TMPDIR/__gradint_ctrl"
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)
export Users="$(echo user.*)"
Users="$(echo user.*)"
cd ..
unset NeedRunMirror
for U in $Users; do
. email_lesson_users/config
if ! test "a$GLOBAL_GRADINT_OPTIONS" == a; then export 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
export SUBJECT_LINE="$DEFAULT_SUBJECT_LINE"
export FORGOT_YESTERDAY="$DEFAULT_FORGOT_YESTERDAY"
export LISTEN_TODAY="$DEFAULT_LISTEN_TODAY"
export NEW_LESSON="$DEFAULT_NEW_LESSON"
export EXPLAIN_FORGOT="$DEFAULT_EXPLAIN_FORGOT"
export AUTO_MESSAGE="$DEFAULT_AUTO_MESSAGE"
SUBJECT_LINE="$DEFAULT_SUBJECT_LINE"
FORGOT_YESTERDAY="$DEFAULT_FORGOT_YESTERDAY"
LISTEN_TODAY="$DEFAULT_LISTEN_TODAY"
NEW_LESSON="$DEFAULT_NEW_LESSON"
EXPLAIN_FORGOT="$DEFAULT_EXPLAIN_FORGOT"
AUTO_MESSAGE="$DEFAULT_AUTO_MESSAGE"
unset Extra_Mailprog_Params1 Extra_Mailprog_Params2 GRADINT_OPTIONS
export Use_M3U=no
export FILE_TYPE=mp3
if grep $'\r' email_lesson_users/$U/profile >/dev/null; then
Use_M3U=no
FILE_TYPE=mp3
if grep $'\r' "email_lesson_users/$U/profile" >/dev/null; then
# Oops, someone edited profile in a DOS line-endings editor (e.g. Wenlin on WINE for CJK stuff). DOS line endings can mess up Extra_Mailprog_Params settings.
cat email_lesson_users/$U/profile | tr -d $'\r' > email_lesson_users/$U/profile.removeCR
mv email_lesson_users/$U/profile.removeCR email_lesson_users/$U/profile
tr -d $'\r' < "email_lesson_users/$U/profile" > email_lesson_users/$U/profile.removeCR
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 export FILE_TYPE_2=m3u
else export FILE_TYPE_2=$FILE_TYPE; fi
. "email_lesson_users/$U/profile"
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
export SUBJECT_LINE="\"$SUBJECT_LINE\""
export Extra_Mailprog_Params1="\"$Extra_Mailprog_Params1\""
export Extra_Mailprog_Params2="\"$Extra_Mailprog_Params2\""
SUBJECT_LINE="\"$SUBJECT_LINE\""
Extra_Mailprog_Params1="\"$Extra_Mailprog_Params1\""
Extra_Mailprog_Params2="\"$Extra_Mailprog_Params2\""
fi
if test -e email_lesson_users/$U/lastdate; then
if test "$(cat email_lesson_users/$U/lastdate)" == "$(date +%Y%m%d)"; then
if [ -e "email_lesson_users/$U/lastdate" ]; 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
if ! grep $U-$(cat email_lesson_users/$U/lastdate)\. $TMPDIR/._email_lesson_logs >/dev/null
if ! grep "$U-$(cat email_lesson_users/$U/lastdate)"\. "$TMPDIR/._email_lesson_logs" >/dev/null
# (don't add $FILE_TYPE after \. in case it has been changed)
then
export Did_Download=0
if test -e email_lesson_users/$U/rollback; then
if test -e email_lesson_users/$U/progress.bak; then
mv email_lesson_users/$U/progress.bak email_lesson_users/$U/progress.txt
rm -f email_lesson_users/$U/progress.bin
export Did_Download=1 # (well actually they didn't, but we're rolling back)
Did_Download=0
if [ -e "email_lesson_users/$U/rollback" ]; then
if [ -e "email_lesson_users/$U/progress.bak" ]; then
mv "email_lesson_users/$U/progress.bak" "email_lesson_users/$U/progress.txt"
rm -f "email_lesson_users/$U/progress.bin"
Did_Download=1 # (well actually they didn't, but we're rolling back)
fi # else can't rollback, as no progress.bak
if test -e email_lesson_users/$U/podcasts-to-send.old; then
mv email_lesson_users/$U/podcasts-to-send.old email_lesson_users/$U/podcasts-to-send
if [ -e "email_lesson_users/$U/podcasts-to-send.old" ]; then
mv "email_lesson_users/$U/podcasts-to-send.old" "email_lesson_users/$U/podcasts-to-send"
fi
fi
else export Did_Download=1; fi
rm -f email_lesson_users/$U/rollback
if test $Did_Download == 0; then
else Did_Download=1; fi
rm -f "email_lesson_users/$U/rollback"
if [ $Did_Download == 0 ]; then
# send a reminder
export 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)
while ! $MailProg -s "$SUBJECT_LINE" $STUDENT_EMAIL "$Extra_Mailprog_Params1" "$Extra_Mailprog_Params2" <<EOF
DaysOld="$(python -c "import os,time;print(int((time.time()-os.stat('email_lesson_users/$U/lastdate').st_mtime)/3600/24))")"
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" <<EOF
$FORGOT_YESTERDAY
$OUTSIDE_LOCATION/$U-$(cat email_lesson_users/$U/lastdate).$FILE_TYPE_2
$OUTSIDE_LOCATION/$U-$(cat "email_lesson_users/$U/lastdate").$FILE_TYPE_2
$EXPLAIN_FORGOT
$AUTO_MESSAGE
......@@ -129,68 +129,68 @@ do echo "mail sending failed; retrying in 62 seconds"; sleep 62; done; fi
continue
else
# delete the previous lesson
if echo $PUBLIC_HTML | grep : >/dev/null; then ssh -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $ControlPath $(echo $PUBLIC_HTML|sed -e 's/:.*//') rm $(echo $PUBLIC_HTML|sed -e 's/[^:]*://')/$U-$(cat email_lesson_users/$U/lastdate).*
else rm $PUBLIC_HTML/$U-$(cat email_lesson_users/$U/lastdate).*; fi
if echo "$PUBLIC_HTML" | grep : >/dev/null; then ssh -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $ControlPath $(echo "$PUBLIC_HTML"|sed -e 's/:.*//') rm "$(echo "$PUBLIC_HTML"|sed -e 's/[^:]*://')/$U-$(cat "email_lesson_users/$U/lastdate").*"
else rm $PUBLIC_HTML/$U-$(cat "email_lesson_users/$U/lastdate").*; fi
# (.* because .$FILE_TYPE and possibly .m3u as well)
fi
fi
export CurDate=$(date +%Y%m%d)
if ! test "a$GRADINT_OPTIONS" == a; then export GRADINT_OPTIONS="$GRADINT_OPTIONS ;"; fi
if echo $PUBLIC_HTML | grep : >/dev/null; then export OUTDIR=$TMPDIR
else export OUTDIR=$PUBLIC_HTML; fi
export 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="
CurDate=$(date +%Y%m%d)
if [ "$GRADINT_OPTIONS" ]; then GRADINT_OPTIONS="$GRADINT_OPTIONS ;"; fi
if echo "$PUBLIC_HTML" | grep : >/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="
# (note: we DO keep progressFileBackup, because it can be useful if the server goes down and the MP3's need to be re-generated or something)
unset Send_Podcast_Instead
if test -s email_lesson_users/$U/podcasts-to-send; then
export Send_Podcast_Instead="$(head -1 email_lesson_users/$U/podcasts-to-send)"
export NumLines=$(echo $(cat email_lesson_users/$U/podcasts-to-send|wc -l)-1|bc)
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
echo $U | $MailProg -s Warning:email-lesson-run-out-of-podcasts $ADMIN_EMAIL
if [ -s "email_lesson_users/$U/podcasts-to-send" ]; then
Send_Podcast_Instead="$(head -1 email_lesson_users/$U/podcasts-to-send)"
NumLines=$[$(cat "email_lesson_users/$U/podcasts-to-send"|wc -l)-1]
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 [ $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
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
export ToSleep=123
while ! if test "a$Send_Podcast_Instead" == a; then
python gradint.py "$USER_GRADINT_OPTIONS '-.sh'" </dev/null 2>$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";
if [ "$ENCODE_ON_REMOTE_HOST" == 1 ]; then
ToSleep=123
while ! if [ ! "$Send_Podcast_Instead" ]; then
python gradint.py "$USER_GRADINT_OPTIONS '-.sh'" </dev/null 2>"$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 ../..;
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 ../..;
fi; do
# (</dev/null so exceptions don't get stuck on 'press enter to continue' to a temp stderr if running from a terminal)
$MailProg -s gradint-to-ssh-failed,-will-retry $ADMIN_EMAIL < $TMPDIR/__stderr
$MailProg -s gradint-to-ssh-failed,-will-retry $ADMIN_EMAIL < "$TMPDIR/__stderr"
# (no spaces in subj so no need to decide whether to single or double quote)
# (don't worry about mail errors - if net is totally down that's ok, admin needs to know if it's a gradint bug causing infinite loop)
sleep $ToSleep ; export ToSleep=$(echo $ToSleep*1.5|bc) # (increasing-time retries)
sleep $ToSleep ; ToSleep=$[$ToSleep*1.5] # (increasing-time retries)
done
rm $TMPDIR/__stderr
if test "a$Use_M3U" == ayes; 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
rm "$TMPDIR/__stderr"
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
(cd email_lesson_users/$U ; cat "$Send_Podcast_Instead") > "$OUTDIR/$U-$CurDate.$FILE_TYPE"
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'" </dev/null; then
echo "Errors from gradint itself (not ssh/network); skipping this user."
echo "Failed on $U, check output " | $MailProg -s gradint-failed $ADMIN_EMAIL
continue
fi
if test "a$Use_M3U" == ayes; then
echo $OUTSIDE_LOCATION/$U-$CurDate.$FILE_TYPE > $OUTDIR/$U-$CurDate.m3u
if [ "$Use_M3U" == yes ]; then
echo "$OUTSIDE_LOCATION/$U-$CurDate.$FILE_TYPE" > "$OUTDIR/$U-$CurDate.m3u"
fi
if echo $PUBLIC_HTML | grep : >/dev/null; then
while ! scp $ControlPath -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $OUTDIR/$U-$CurDate.* $PUBLIC_HTML/; do
if echo "$PUBLIC_HTML" | grep : >/dev/null; then
while ! scp $ControlPath -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $OUTDIR/$U-$CurDate.* "$PUBLIC_HTML/"; do
echo "scp failed; re-trying in 60 seconds"
sleep 64
done
rm $OUTDIR/$U-$CurDate.*
rm "$OUTDIR/$U-$CurDate".*
fi
fi
export NeedRunMirror=1
if ! test -e email_lesson_users/$U/progress.bak; then touch email_lesson_users/$U/progress.bak; fi # so rollback works after 1st lesson
while ! $MailProg -s "$SUBJECT_LINE" $STUDENT_EMAIL "$Extra_Mailprog_Params1" "$Extra_Mailprog_Params2" <<EOF
NeedRunMirror=1
if ! [ -e "email_lesson_users/$U/progress.bak" ]; then touch "email_lesson_users/$U/progress.bak"; fi # so rollback works after 1st lesson
while ! $MailProg -s "$SUBJECT_LINE" "$STUDENT_EMAIL" "$Extra_Mailprog_Params1" "$Extra_Mailprog_Params2" <<EOF
$NEW_LESSON
$OUTSIDE_LOCATION/$U-$CurDate.$FILE_TYPE_2
$LISTEN_TODAY
......@@ -198,27 +198,27 @@ $LISTEN_TODAY
$AUTO_MESSAGE
EOF
do echo "mail sending failed; retrying in 65 seconds"; sleep 65; done
echo $CurDate > email_lesson_users/$U/lastdate
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 export AdminNote="Note: $U has run out of new words"; fi
elif ! test -e email_lesson_users/$U/podcasts-to-send; then export AdminNote="Note: $U has run out of podcasts"; fi
if ! test "a$AdminNote" == a; then
while ! echo "$AdminNote"|$MailProg -s gradint-user-ran-out $ADMIN_EMAIL; do echo "Mail sending failed; retrying in 67 seconds"; sleep 67; done
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 [ "$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
echo As subject | $MailProg -s "PUBLIC_HTML_MIRROR_COMMAND failed, will retry" "$ADMIN_EMAIL" || true # ignore errors
sleep 79
done
fi
rm -f $TMPDIR/._email_lesson_logs
if ! test a$MasterPid == a; then
rm -f "$TMPDIR/._email_lesson_logs"
if [ $MasterPid ] ; then
kill $MasterPid
kill $(ps axwww|grep $TMPDIR/__gradint_ctrl|sed -e 's/^ *//' -e 's/ .*//') 2>/dev/null
rm -f $TMPDIR/__gradint_ctrl # in case ssh doesn't
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"
exit 0
......@@ -227,10 +227,10 @@ 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 ! test -e email_lesson_users/config; then
if ! [ -e email_lesson_users/config ]; then
echo "It seems the email_lesson_users directory is not set up"
echo "Press Enter to create a new one,
or Ctrl-C to quit if you're in the wrong directory"
......@@ -238,11 +238,11 @@ if ! test -e email_lesson_users/config; then
mkdir email_lesson_users || exit 1
cat > email_lesson_users/config <<EOF
# You need to edit this file.
export GLOBAL_GRADINT_OPTIONS="" # if set, will be added to all gradint command lines (e.g. to set synthCache if it's not in advanced.txt)
export MailProg="$DefaultMailProg" # mail, or mutt -x, or ssh some.host mail, or whatever
export PUBLIC_HTML=~/public_html # where to put files on the WWW. If it contains a : then scp will be used to copy them there.
export OUTSIDE_LOCATION=http://$(hostname -f)/~$(whoami) # where they appear from outside
export CAT_LOGS_COMMAND="false" # Please change this to a command that cats the
GLOBAL_GRADINT_OPTIONS="" # if set, will be added to all gradint command lines (e.g. to set synthCache if it's not in advanced.txt)
MailProg="$DefaultMailProg" # mail, or mutt -x, or ssh some.host mail, or whatever
PUBLIC_HTML=~/public_html # where to put files on the WWW. If it contains a : then scp will be used to copy them there.
OUTSIDE_LOCATION=http://$(hostname -f)/~$(whoami) # where they appear from outside
CAT_LOGS_COMMAND="false" # Please change this to a command that cats the
# server logs for at least the last 48 hours. (On some systems you may need
# to make the script suid root.) It is used to check that the users have
# downloads their lessons and remind them if not.
......@@ -254,29 +254,29 @@ export CAT_LOGS_COMMAND="false" # Please change this to a command that cats the
# control connection (\$ControlPath will expand to
# nothing on systems with old ssh's that don't support this)
export PUBLIC_HTML_EXTRA_SSH_OPTIONS="" # if set and PUBLIC_HTML is on a remote host, these options will be added to all ssh and scp commands to that host - use this for things like specifying an alternative identity file with -i
PUBLIC_HTML_EXTRA_SSH_OPTIONS="" # if set and PUBLIC_HTML is on a remote host, these options will be added to all ssh and scp commands to that host - use this for things like specifying an alternative identity file with -i
export PUBLIC_HTML_MIRROR_COMMAND="" # if set, will be run after any new lessons are written to PUBLIC_HTML.
PUBLIC_HTML_MIRROR_COMMAND="" # if set, will be run after any new lessons are written to PUBLIC_HTML.
# This is for unusual setups where PUBLIC_HTML is not the real public_html directory but some command can be run to mirror its contents to the real one (perhaps on a remote server that cannot take passwordless SSH from here; of course you'd need to set up an alternative way of getting the files across and the log entries back).
# Note: Do not add >/dev/null or similar redirects to PUBLIC_HTML_MIRROR_COMMAND as some versions of bash will give an error.
export TMPDIR=/tmp # or /dev/shm or whatever
export ENCODE_ON_REMOTE_HOST=0 # if 1, will ssh to the remote host
ENCODE_ON_REMOTE_HOST=0 # if 1, will ssh to the remote host
# that's specified in PUBLIC_HTML (which *must* be host:path in this case)
# and will run an encoding command *there*, instead of encoding
# locally and copying up. This is useful if the local machine is the
# only place gradint can run but it can't encode (e.g. Linux server running on NAS device).
# If you set the above to 1 then you also need to set these options:
export REMOTE_WORKING_DIR=. # directory to change to on remote host e.g. /tmp/gradint (will create with mkdir -p if does not exist)
REMOTE_WORKING_DIR=. # directory to change to on remote host e.g. /tmp/gradint (will create with mkdir -p if does not exist)
# (make sure $PUBLIC_HTML etc is absolute or is relative to $REMOTE_WORKING_DIR) (don't use spaces in these pathnames)
export SOX_PATH=$PATH
SOX_PATH=$PATH
# make sure the above includes the remote host's "sox" as well as basic commands
export ENCODING_COMMAND="lame --vbr-new -V 9 -"
ENCODING_COMMAND="lame --vbr-new -V 9 -"
# (used only if ENCODE_ON_REMOTE_HOST is set)
# (include the full path for that if necessary; SOX_PATH will NOT be searched)
# (set options for encode wav from stdin & output to the file specified on nxt parameter. No shell quoting.)
export ADMIN_EMAIL=admin@example.com # to report errors
ADMIN_EMAIL=admin@example.com # to report errors
EOF
cd email_lesson_users; $EDITOR config; cd ..
echo "Created email_lesson_users/config"
......@@ -285,16 +285,16 @@ cd email_lesson_users
while true; do
echo "Type a user alias (or just press Enter) to add a new user, or Ctrl-C to quit"
read Alias
export 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
cd $ID
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 [ "$Alias" ]; then ln -s "$ID" "$Alias"; fi
cd "$ID" || exit 1
cat > profile <<EOF
# You need to edit the settings in this file.
export STUDENT_EMAIL=student@example.org # change to student's email address
STUDENT_EMAIL=student@example.org # change to student's email address
export GRADINT_OPTIONS="" # extra gradint command-line options, for example to
# specify a different first and second language
export FILE_TYPE=mp3 # change to something else if you want
export Use_M3U=no # if yes, sends a .m3u link to the student
FILE_TYPE=mp3 # change to something else if you want
Use_M3U=no # if yes, sends a .m3u link to the student
# instead of sending the file link directly. Use this if
# the student needs to stream over a slow link, but note
# that it makes offline listening one step more complicated.
......@@ -324,33 +324,33 @@ export Use_M3U=no # if yes, sends a .m3u link to the student
# students that the lessons will now come from a different address.
# Optional settings for customising the text of the message:
export SUBJECT_LINE="$DEFAULT_SUBJECT_LINE"
export FORGOT_YESTERDAY="$DEFAULT_FORGOT_YESTERDAY"
export LISTEN_TODAY="$DEFAULT_LISTEN_TODAY"
export NEW_LESSON="$DEFAULT_NEW_LESSON"
export EXPLAIN_FORGOT="$DEFAULT_EXPLAIN_FORGOT"
export AUTO_MESSAGE="$DEFAULT_AUTO_MESSAGE"
export Extra_Mailprog_Params1=""
export Extra_Mailprog_Params2=""
SUBJECT_LINE="$DEFAULT_SUBJECT_LINE"
FORGOT_YESTERDAY="$DEFAULT_FORGOT_YESTERDAY"
LISTEN_TODAY="$DEFAULT_LISTEN_TODAY"
NEW_LESSON="$DEFAULT_NEW_LESSON"
EXPLAIN_FORGOT="$DEFAULT_EXPLAIN_FORGOT"
AUTO_MESSAGE="$DEFAULT_AUTO_MESSAGE"
Extra_Mailprog_Params1=""
Extra_Mailprog_Params2=""
# You may need to set Extra_Mailprog_Params to extra parameters
# if the subject or text includes characters that need to be sent
# in a specific charset. For example, to send in Chinese GB2312
# with Mutt, you can do this:
# in a specific charset. For example, to send Chinese (Simplified)
# in UTF-8 with Mutt, you can do this:
# export GRADINT_OPTIONS="firstLanguage='zh'; secondLanguage='en'; otherLanguages=[]"
# export LANG=C
# export Extra_Mailprog_Params1="-e"
# export Extra_Mailprog_Params2="set charset='gb2312'; set send_charset='gb2312'"
# export SUBJECT_LINE="英文词汇练习 (English vocabulary practice)"
# export FORGOT_YESTERDAY="你忘记了昨天的课 (you forgot your lesson yesterday).
# 请记得下载 (please remember to download) :"
# export EXPLAIN_FORGOT="请试图天天听一课 (please try to hear one lesson every day)
# 如果你今天下载, 这个软件要明天给你另一个课.
# Extra_Mailprog_Params1="-e"
# Extra_Mailprog_Params2="set charset='utf-8'; set send_charset='utf-8'"
# SUBJECT_LINE="英文词汇练习 (English vocabulary practice)"
# FORGOT_YESTERDAY="你忘记了昨天的课 (you forgot your lesson yesterday).
# 请记得下载 (please remember to download) :"
# EXPLAIN_FORGOT="请试图天天听一课 (please try to hear one lesson every day)
# 如果你今天下载, 这个软件要明天给你另一个课.
# (If you download that lesson today,
# this program will make the next one for tomorrow.)"
# export NEW_LESSON="今天的课在以下的网址 (your lesson for today is at)"
# export LISTEN_TODAY="请你今天下载而听 (please download and listen to it today)."
# export AUTO_MESSAGE="这个电邮是软件写的 (this is an automatic message from the gradint program).
# 假如你有问题, 请告诉我 (any problems, let me know)."
# NEW_LESSON="今天的课在以下的网址 (your lesson for today is at)"
# LISTEN_TODAY="请你今天下载而听 (please download and listen to it today)."
# AUTO_MESSAGE="这个电邮是软件写的 (this is an automatic message from the gradint program).
# 假如你有问题, 请告诉我 (any problems, let me know)."
# You can also override *some* of the email_lesson_users/config
# options on a per-user basis by putting them here,
......
#!/usr/bin/env python
# (Python 2 or Python 3)
# espeak.cgi - a CGI script for the eSpeak speech synthesizer
# (c) 2008,2011 Silas S. Brown, License: GPL
version="1.122"
# (c) 2008,2011,2020 Silas S. Brown, License: GPL
version="1.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
......@@ -31,15 +32,28 @@ max_input_size = 1000
minSpeed, defaultSpeed, maxSpeed, speedStep = 80,170,370,30 # (NB check defaultSpeed=minSpeed+integer*speedStep)
import os,commands,re,sys
if commands.getoutput("which espeak 2>/dev/null"): prog="espeak"
elif commands.getoutput("which speak 2>/dev/null"): prog="speak"
import os,re,sys
try: from commands import getoutput # Python 2
except: from subprocess import getoutput # Python 3
if getoutput("which espeak 2>/dev/null"): prog="espeak"
elif getoutput("which speak 2>/dev/null"): prog="speak"
else: raise Exception("Cannot find espeak")
lang = f.getfirst("l",default_language)
def S(x):
if type("")==type(u""): # Python 3
try: return x.decode('utf-8') # in case byte-string
except: pass
return x
lang = S(f.getfirst("l",default_language))
if len(lang)>10 or not re.match("^[a-z0-9-+]*$",lang): lang=default_language
voiceDir=os.environ.get("ESPEAK_DATA_PATH","/usr/share/espeak-data")+"/voices"
if "ESPEAK_DATA_PATH" in os.environ: voiceDir = os.environ["ESPEAK_DATA_PATH"]
elif os.path.exists("/usr/share/espeak-data"): voiceDir = "/usr/share/espeak-data"
elif os.path.exists("/usr/lib/x86_64-linux-gnu/espeak-data"): voiceDir = "/usr/lib/x86_64-linux-gnu/espeak-data"
else:
print ("Content-type: text-plain\n\nUnable to find ESPEAK_DATA_PATH")
raise SystemExit
voiceDir += "/voices"
variants = os.listdir(voiceDir+"/!v")
if "whisper" in variants and "wisper" in variants: variants.remove("wisper")
......@@ -55,16 +69,17 @@ try: speed=int(speed)
except: speed=defaultSpeed
if speed<minSpeed or speed>maxSpeed: speed=defaultSpeed
t = f.getfirst("t","")
t = S(f.getfirst("t",""))
if len(t)>max_input_size: t=""
else:
try: t.decode('utf-8')
except: t="" # not valid utf-8
if not type(t)==type(u""):
try: t.decode('utf-8')
except: t="" # not valid utf-8
if chr(0) in t: t="" # just in case
if len(t)>stream_if_input_bigger_than:
# streaming - will need sox to convert
if not commands.getoutput("which sox 2>/dev/null"): raise Exception("Cannot find sox")
if not getoutput("which sox 2>/dev/null"): raise Exception("Cannot find sox")
fname=None
else:
# not streaming (so can fill in length etc) - will need a writable file in a private tmp directory, preferably in memory
......@@ -80,15 +95,16 @@ else:
open(fname2,"w") # raising exception if it's unwritable (try changing to a suitable directory)
# in case espeak can't find a utf-8 locale
loc=commands.getoutput("locale -a|grep -i 'utf-*8$'|head -1").strip()
loc=getoutput("locale -a|grep -i 'utf-*8$'|head -1").strip()
if loc: os.environ["LC_CTYPE"]=loc
def getName(f):
f = S(f)
o=open(f)
line=""
for t in range(10):
line=o.readline()
if "name" in line: return line.split()[1]
line=getBuf(o).readline()
if u"name".encode('latin1') in line: return S(line.split()[1])
return f[f.rindex("/")+1:] # assumes it'll be a full pathname
def isDirectory(directory):
oldDir = os.getcwd()
......@@ -99,10 +115,21 @@ def isDirectory(directory):
os.chdir(oldDir)
return ret
def getBuf(f):
if hasattr(f,"buffer"): return f.buffer # Python 3
else: return f # Python 2
def doPipe(P,t):
if type("")==type(u""): # Python 3
P = os.popen(P,"w")
if type(t)==type(u""): P.write(t)
else: P.buffer.write(t)
P.close()
else: os.popen(P,"wb").write(t) # Python 2
if t and f.getfirst("qx",""):
sys.stdout.write("Content-Type: text/plain; charset=utf-8\n\n")
sys.stdout.flush() # help mathopd
os.popen(prog+" -v "+lang+" -q -X -m 2>/dev/null","wb").write(t)
doPipe(prog+" -v "+lang+" -q -X -m 2>/dev/null",t)
elif t:
prog_with_params = prog+" -v "+lang+"+"+variant+" -s "+str(speed)+" -m"
# TODO -p 0-99 default 50 (pitch adjustment)
......@@ -111,17 +138,18 @@ elif t:
sys.stdout.write("Content-Type: audio/basic\nContent-Disposition: attachment; filename=\""+t+"_"+lang+".au\"\n\n") # using .au instead of .wav because Windows Media Player doesn't like incorrect length fields in wav. And make sure it's attachment otherwise Mac OS QuickTime etc can have problems when server is slow
# problem is, WILL NEED CONVERTING for gradint (unless want to use "sox" on the Windows version before playing via winsound) (but the espeak no-length wav files will probably be wrong on that anyway). Should be OK because we're doing this only in the case of len(t)>stream_if_input_bigger_than.
sys.stdout.flush() # help mathopd
os.popen(prog_with_params+" --stdout 2>/dev/null | sox -t wav - -t au - 2>/dev/null","wb").write(t)
else:
os.popen(prog_with_params+" -w "+fname2+" 2>/dev/null","wb").write(t)
doPipe(prog_with_params+" --stdout 2>/dev/null | sox -t wav - -t au - 2>/dev/null",t)
else: # not streaming
doPipe(prog_with_params+" -w "+fname2+" 2>/dev/null",t)
sys.stdout.write("Content-Type: audio/wav\nContent-Disposition: attachment; filename=\""+t+"_"+lang+".wav\"\n\n")
sys.stdout.write(open(fname2,"rb").read())
sys.stdout.flush()
getBuf(sys.stdout).write(open(fname2,"rb").read())
else:
sys.stdout.write("Content-Type: text/html; charset=utf-8\n\n<HTML><BODY>") # (specify utf-8 here in case accept-charset is not recognised, e.g. some versions of IE6)
banner = commands.getoutput(prog+" --help|head -3").strip()
sys.stdout.write('Content-Type: text/html; charset=utf-8\n\n<HTML><head><meta name="viewport" content="width=device-width"></head><BODY>') # (specify utf-8 here in case accept-charset is not recognised, e.g. some versions of IE6)
banner = S(getoutput(prog+" --help|head -3").strip())
sys.stdout.write("This is espeak.cgi version "+version+", using <A HREF=http://espeak.sourceforge.net/>eSpeak</A> "+" ".join(banner.split()[1:]))
if not loc: sys.stdout.write("<br>Warning: could not find a UTF-8 locale; espeak may malfunction on some languages")
warnings=commands.getoutput(prog+" -q -x .").strip() # make sure any warnings about locales are output
warnings=S(getoutput(prog+" -q -x .").strip()) # make sure any warnings about locales are output
if warnings: sys.stdout.write("<br>"+warnings)
sys.stdout.write("<FORM method=post accept-charset=UTF-8>Text or SSML: <INPUT TYPE=text NAME=t STYLE='width:80%'><br>Language: <SELECT NAME=l>")
ld=os.listdir(voiceDir)
......@@ -148,7 +176,7 @@ else:
if v==variant: sys.stdout.write(" SELECTED")
sys.stdout.write(">"+name+"</OPTION>")
sys.stdout.write("</SELECT> Speed: <SELECT NAME=s>")
for ss in range(minSpeed,maxSpeed,speedStep)+[maxSpeed]:
for ss in list(range(minSpeed,maxSpeed,speedStep))+[maxSpeed]:
sys.stdout.write("<OPTION VALUE="+str(ss))
if ss==speed: sys.stdout.write(" SELECTED")
sys.stdout.write(">"+str(ss)+"</OPTION>")
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# (either Python 2 or Python 3)
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
# 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.
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, 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
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"]
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 = []
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)
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.&nbsp; 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")
if not filetype in ["mp3","ogg","wav"]: filetype="mp3"
for k in query.keys():
if k.startswith("del-"):
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.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.getfirst("l2w"):
gradint.startBrowser=lambda *args:0
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.getfirst("l2w") and "l1w" in query and query.getfirst("l1w"):
gradint.startBrowser=lambda *args:0
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):
if x[0]=='W':
try:
int(x[1:])
return True
except: pass
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.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.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()
gradint.updateSettingsFile(gradint.settingsFile,{"firstLanguage": gradint.secondLanguage,"secondLanguage":gradint.firstLanguage})
redirectHomeKeepCookie(dirID)
elif "editsave" in query: # save 'vocab'
dirID = setup_userID()
if "vocab" in query: vocab=query.getfirst("vocab")
else: vocab="" # user blanked it
open(gradint.vocabFile,"w").write(vocab)
redirectHomeKeepCookie(dirID)
elif "edit" in query: # show the edit form
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=placeholder value="Cancel"></form>',"Text edit your vocab list")
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=""
gradint.updateSettingsFile(gradint.settingsFile,{"voiceOption":""})
listVocab(True)
elif "vopt" in query: # set voice option
setup_userID()
for v in gradint.guiVoiceOptions:
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.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
if allLinesHaveEquals(lines):
paras[i] = authorWordList(lines,l1,l2)
need_h5a = True
# TODO else some wiki markup for links etc ? (but you can alter the HTML after)
if need_h5a: h5astr = h5a()
else: h5astr = ""
htmlOut(HTML_and_preview(h5astr+encodeAmp('<p>'.join(paras))),"HTML result",links=0)
else: return False
return True
def allLinesHaveEquals(lines):
if not lines: return False
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):
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 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))
return ''.join(r)
def encodeAmp(uniStr):
# HTML-ampersand encode when we don't know if the server will be utf-8 after copy/paste
r=[]
for c in uniStr:
if ord(c)>126: r.append("&#"+str(ord(c))+";")
else: r.append(c)
return ''.join(r)
def HTML_and_preview(code): return '<h2>HTML code</h2><textarea style="width:100%%;height:40%%" rows=7 cols=50>%s</textarea><h2>Preview</h2>%s' % (code.replace('&','&amp;').replace('<','&lt;'),code)
def justSynth(text,lang="",filetype=""):
if lang: lang = lang.replace("#","").replace('"','')+" "
gradint.justSynthesize=lang+text.replace("#","").replace('"','')
if not filetype in ["mp3","ogg","wav"]: filetype="mp3"
serveAudio(stream = len(text)>80, filetype=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='+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):
if noGTranslate: print ("Google: notranslate")
print ("Content-type: text/html; charset=utf-8\n")
if title_extra: title_extra=": "+title_extra
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="//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>
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 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") # 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 ("")
sys.stdout.flush()
gradint.write_to_stdout = 1
gradint.outputFile="-."+filetype ; gradint.setSoundCollector(gradint.SoundCollector())
mainOrSynth()
else:
gradint.write_to_stdout = 0
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):
if out: dirID=setup_userID()
if not (gradint.firstLanguage,gradint.secondLanguage) == (l1,l2):
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(withLocalise('This word is already in your list.')+backLink)
return
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="+quote(os.environ["HTTP_REFERER"])
else: extra=""
redirectHomeKeepCookie(dirID,extra)
def redirectHomeKeepCookie(dirID,extra=""):
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(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,span=0):
r=gradint.localise(x)
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><!--
function h5a(link,endFunc) { if (document.createElement) {
var ae = document.createElement('audio');
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+'">'
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"',"")+'>'
body += '<input type=hidden name=curVopt value="'+gradint.voiceOption+'">' # ignored by gradint.cgi but needed by browser cache to ensuer 'change voice option and press Speak again' works
body += '<br>'
# must have autocomplete=off if capturing keycode 13
if gotVoiceOptions: cacheInfo="&curVopt="+gradint.voiceOption
else: cacheInfo=""
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)
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 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>"+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",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.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><!--\ndocument.forms[0].l2w.focus()\n//--></script>')
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 = getCookieId()
need_write = (userID and not os.path.exists(dirName+'/'+userID+'-settings.txt')) # maybe it got cleaned up
if not userID:
while True:
userID = str(random.random())[2:]
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') # 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
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.07 (c) 2011, 2020-21, 2025 Silas S. Brown. License: GPL
# Example use:
# 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
mergeIn = gradint.scanSamples()+gradint.parseSynthVocab(gradint.vocabFile)
oldProg = gradint.ProgressDatabase(alsoScan=0)
oldProg.data += oldProg.unavail # because it might be available in newProg
gradint.mergeProgress(oldProg.data,mergeIn)
opd = {}
for tries,l1,l2 in oldProg.data:
key = gradint.norm_filelist(l1,l2)
if tries: opd[key]=tries
del oldProg
gradint.progressFile = newpf
newProg = gradint.ProgressDatabase(alsoScan=0)
gradint.mergeProgress(newProg.data,mergeIn)
del mergeIn
changes = [] ; count=0
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,S(l1),S(l2)))
count += 1
del newProg,opd
changes.sort()
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):
r = real_h5aCode(filename)
if r:
global had_h5a
if not had_h5a:
had_h5a = True
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))) {
if (link.href) ae.setAttribute('src', link.href);
else ae.setAttribute('src', link);
ae.play();
return false; } } return true; }
//--></script>""")
return r
def real_h5aCode(filename):
if filename.endswith(gradint.dotmp3): return ' onClick="javascript:return h5a(this,\'audio/mpeg\')"'
elif filename.endswith(gradint.dotwav): return ' onClick="javascript:return h5a(this,\'audio/wav\')"'
else: return ""
def wrappable(f):
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 = 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:
if ensureTxt:
if not v.endswith(gradint.dottxt): continue
elif v.endswith(gradint.dottxt): continue
if not os.sep in l: return v
return l[:l.rindex(os.sep)+1]+v
def link(l):
if type(l)==type([]): return link(l[-1])
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=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 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)
# (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
sys.stdout.write("<HEAD><META HTTP_EQUIV=Content-type CONTENT=\"text/html; charset=utf-8\"></HEAD>\n") # (assume utf8 in case there's any hanzi in lily, or in espeak cantonese voice or whatever - 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: sys.stdout.write("<P>")
if not justHadP: print ("<P>")
justHadP=1 ; continue
if not justHadP: sys.stdout.write("<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"):
sys.stdout.write("<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("<!--"): sys.stdout.write("<small>#</small> ")
sys.stdout.write(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,8 +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)
sys.stdout.write(" = ".join(out))
print (" = ".join(out))
justHadP=0
print ("</body></html>")
File moved
../gradint.py:
make -C .. gradint.py
# This file is part of the source code of
# gradint v0.9974 (c) 2002-2011 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))
......@@ -294,13 +288,16 @@ class Lesson(object):
global sequenceIDs_to_cancel ; sequenceIDs_to_cancel = {}
global copy_of_runner_events ; copy_of_runner_events = []
global lessonStartTime ; lessonStartTime = 0 # will be set to time.time() on 1st event
# make the runner as late as possible
if soundCollector: runner = sched.scheduler(collector_time,collector_sleep)
else: runner = sched.scheduler(time.time,mysleep)
for (t,event) in self.events: copy_of_runner_events.append((event,runner.enter(t,1,play,(event,)),t))
# TODO what if Brief Interrupt appears during that events loop and someone presses it (will act as a Cancel and go back to main)
try: runner.run()
except KeyboardInterrupt: handleInterrupt()
disable_lid(0)
try:
# make the runner as late as possible
if soundCollector: runner = sched.scheduler(collector_time,collector_sleep)
else: runner = sched.scheduler(time.time,mysleep)
for (t,event) in self.events: copy_of_runner_events.append((event,runner.enter(t,1,play,(event,)),t))
# TODO what if Brief Interrupt appears during that events loop and someone presses it (will act as a Cancel and go back to main)
try: runner.run()
except KeyboardInterrupt: handleInterrupt()
finally: disable_lid(1)
runner = None
if soundCollector: soundCollector.finished()
if logFileHandle: logFileHandle.close()
......@@ -312,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.9974 (c) 2002-2011 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
......@@ -244,67 +256,67 @@ def parseSynthVocab(fname,forGUI=0):
if not fname: return []
langs = [secondLanguage,firstLanguage] ; someLangsUnknown = 0 ; maxsplit = 1
ret = []
count = 1 ; doLimit = 0 ; limitNo = 0 ; doPoetry = 0
count = 1 ; doLimit = 0 ; limitNo = 0 ; doPoetry = disablePoem = 0
lastPromptAndWord = None
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 = True,None
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,52 +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):
f=strCount+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=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: lastPromptAndWord=None # if one of the lines can't be synth'd, don't connect the lines before/after it
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)
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)
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*.
......@@ -367,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:
......@@ -379,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]]
......@@ -392,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,
......@@ -405,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.9974 (c) 2002-2011 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
......@@ -12,7 +12,7 @@
# Start of frontend.py - Tk and other front-ends
def interrupt_instructions():
if soundCollector or app or appuifw: return ""
if soundCollector or app or appuifw or android: return ""
elif msvcrt: return "\nPress Space if you have to interrupt the lesson."
elif riscos_sound: return "\nLesson interruption not yet implemented on RISC OS. If you stop the program before the end of the lesson, your progress will be lost. Sorry about that."
elif winCEsound: return "\nLesson interruption not implemented on\nWinCE without GUI. Can't stop, sorry!"
......@@ -23,18 +23,29 @@ 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 app and hasattr(app.todo,"alert"): time.sleep(0.5)
while True:
try:
if not hasattr(app.todo,"alert"): break
except: break # app destroyed
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)
......@@ -45,7 +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()
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)
......@@ -59,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
......@@ -67,48 +85,60 @@ def getYN(msg,defaultIfEof="n"):
def primitive_synthloop():
global justSynthesize,warnings_printed
lang = None
interactive = appuifw or not hasattr(sys.stdin,"isatty") or sys.stdin.isatty()
interactive = appuifw or winCEsound or android or not hasattr(sys.stdin,"isatty") or sys.stdin.isatty()
if interactive: interactive=cond(winCEsound and warnings_printed,"(see warnings under this window) Say:","Say: ") # (WinCE uses an input box so need to repeat the warnings if any - but can't because prompt is size-limited, so need to say move the window.)
else: interactive="" # no prompt on the raw_input (we might be doing outputFile="-" as well)
while True:
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:
try: justSynthesize=raw_input(interactive)
except EOFError: break
if (winCEsound or riscos_sound) and not justSynthesize: break # because no way to send EOF (and we won't be taking i/p from a file)
if android:
justSynthesize = android.dialogGetInput("Gradint",interactive).result
if type(justSynthesize)==type(u""): justSynthesize=justSynthesize.encode("utf-8")
else:
try: justSynthesize=raw_input(interactive)
except EOFError: break
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 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()
t.add(u"".join(warnings_printed))
appuifw.app.body = t
elif android: waitOnMessage("") # (makeToast doesn't stay around for very long)
# else they'll have already been printed
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: import webbrowser
except: webbrowser=0
if webbrowser:
try:
import webbrowser
g=webbrowser.get()
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)
# else don't risk it - it might be text-mode and unsuitable for multitask-with-gradint
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(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)
......@@ -120,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 = []
......@@ -140,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)
......@@ -175,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>')
......@@ -193,14 +228,16 @@ def selectAllButNumber(e): # hack for recording.py - select all but any number a
def addTextBox(row,wide=0):
text = Tkinter.StringVar(row)
entry = Tkinter.Entry(row,textvariable=text)
if winsound or mingw32 or cygwin or macsound: entry.bind('<Button-3>',CXVMenu)
if macsound: entry.bind('<Control-Button-1>',CXVMenu)
entry.bind('<ButtonRelease-3>',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
......@@ -209,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):
......@@ -224,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)
......@@ -262,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]:
......@@ -277,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"})
......@@ -286,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)
......@@ -302,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("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")
......@@ -317,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
......@@ -338,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:
......@@ -355,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):
......@@ -373,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)
......@@ -382,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})
......@@ -426,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)
......@@ -438,16 +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:
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")
......@@ -459,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'
......@@ -601,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)
......@@ -610,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)
......@@ -674,7 +736,8 @@ def startTk():
self.Minutes.set(mins)
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.MakeLessonButton.bind('<FocusIn>',(lambda *args:app.after(10,lambda *args:app.MinsEntry.selection_clear())))
self.lastOutTo=-1 # so it updates the Start Lesson button if needed
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
......@@ -705,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"):
......@@ -731,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:
......@@ -752,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"):
......@@ -771,17 +839,25 @@ def startTk():
self.ChangeButton.pack_forget()
self.change_button_shown = 0
del self.ListBox # so doesn't sync lists, or assume Cancel button is a Clear button
if hasattr(self,"cacheManagementButtons"):
for b in self.cacheManagementButtons: b.pack_forget()
del self.cacheManagementButtons,self.cacheManagementOptions
app.master.title(appTitle)
self.CancelRow.pack_forget() ; self.Version.pack_forget()
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 = []
......@@ -857,14 +933,14 @@ 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")
row4right = addRightRow(self.row4)
self.EditVocabButton = addButton(row4right,"",self.openVocabFile,{"side":"left"},status="This button lets you edit your\nvocab collection in "+textEditorName)
if not GUI_omit_settings: addButton(row4right,"advanced"+dottxt,self.openAdvancedTxt,{"side":"left"},status="Press this button to learn multiple languages\nor change advanced settings for synthesis etc")
if not GUI_omit_settings: addButton(row4right,"advanced"+dottxt,self.openAdvancedTxt,{"side":"left"},status="Press this button to change voices,\nlearn multiple languages, etc")
self.make_lesson_row()
else: # no text editor, but can at least have Recorded Words button now we have a built-in manager
self.make_lesson_row()
......@@ -905,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):
......@@ -916,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
......@@ -935,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 that can be computer voiced:")+"\n"+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()
......@@ -958,7 +1035,7 @@ def startTk():
self.L2Text.set(secondLanguage)
self.L1Label["text"] = localise("Your first language")+":"
self.L2Label["text"] = localise("second")+":"
self.TestTextButton["text"] = localise("Speak")
self.TestTextButton["text"] = localise("Speak") ; self.lastOutTo=-1 # so updates to "To WAV" etc if necessary
if hasattr(self,"userNo") and intor0(self.userNo.get()): gui_vocabFile_name="vocab file" # don't expose which user number they are because that might change
elif len(vocabFile)>15 and os.sep in vocabFile: gui_vocabFile_name=vocabFile[vocabFile.rindex(os.sep)+1:]
else: gui_vocabFile_name=vocabFile
......@@ -1003,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):
......@@ -1055,7 +1135,7 @@ def startTk():
appThread(Application)
def hanzi_only(unitext): return u"".join(filter(lambda x:0x3000<ord(x)<0xa700 or ord(x)>=0x10000, list(unitext)))
def hanzi_and_punc(unitext): return u"".join(filter(lambda x:0x3000<ord(x)<0xa700 or ord(x)>=0x10000 or x in '.,?;":\'()[]!0123456789-', list(remove_tone_numbers(fix_compatibility(unitext)))))
def hanzi_and_punc(unitext): return u"".join(filter(lambda x:0x3000<ord(x)<0xa700 or ord(x)>=0x10000 or x in '.,?;:\'()[]!0123456789-', list(remove_tone_numbers(fix_compatibility(unitext))))) # no " as it could be from SGML markup
# (exclusion of 3000 in above is deliberate, otherwise get problems with hanzi spaces being taken out by fix-compat+strip hence a non-functional 'delete non-hanzi' button appears)
def guiVocabList(parsedVocab):
# This needs to be fast. Have tried writing interatively rather than filter and map, and assume stuff is NOT already unicode (so just decode rather than call ensure_unicode) + now assuming no !synth: (but can still run with .txt etc)
......@@ -1072,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)
......@@ -1138,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"
......@@ -1149,21 +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-open are available, can we tell which one the user prefers? currently using gnome-open)
# (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 ["gedit","nedit","kedit","xedit"]:
for editor in ["leafpad","featherpad","gedit","nedit","kedit","xedit"]:
if got_program(editor):
textEditorName=textEditorCommand=editor
textEditorWaits = 1
......@@ -1173,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
......@@ -1193,31 +1283,35 @@ 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 += "&"
os.system(cmd)
else:
msg = "Don't know how to start the file explorer. Please open the %s directory (in %s)" % (dir,os.getcwd())
msg = ""
if not dir.startswith(os.sep): msg=" (in %s)" % os.getcwd()
msg = "Don't know how to start the file explorer. Please open the %s directory%s" % (dir,msg)
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."))
......@@ -1232,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?
......@@ -1245,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()
......@@ -1259,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
......@@ -1269,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
......@@ -1279,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"))
......@@ -1291,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))
......@@ -1336,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
......@@ -1358,6 +1495,13 @@ fi""")
def gui_event_loop():
app.todo.set_main_menu = 1 ; braveUser = 0
global disable_once_per_day
if disable_once_per_day==2:
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
try: os.remove("background"+dottxt)
except: pass
if orig_onceperday&2: check_for_slacking()
while app:
while not hasattr(app,"menu_response"):
......@@ -1370,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()
......@@ -1391,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:
......@@ -1412,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.")
......@@ -1439,35 +1586,39 @@ 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:
control.set(t) ; app.toRestore.append((control,t,restoreTo))
doControl(text1,secondLanguage,app.Text1)
def doSynth():
gui_outputTo_start() ; just_synthesize() ; gui_outputTo_end()
def doSynth(openDir=True):
gui_outputTo_start() ; just_synthesize() ; gui_outputTo_end(openDir)
global justSynthesize ; justSynthesize = ""
if app: app.unset_watch_cursor = 1 # otherwise was closed by the close box
if ask_teacherMode and text1 and text2: # 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 getYN("Also speak the %s?" % firstLanguage):
doControl(text2,firstLanguage,app.Text2)
doSynth()
else:
doControl(text2,firstLanguage,app.Text2)
if text1 and text2:
if app and hasattr(app,"outputTo") and app.outputTo.get() and not app.outputTo.get()=="0":
if getYN("Save %s and %s to separate files?" % (secondLanguage,firstLanguage)): doSynth(False)
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):
continue
doControl(text2,firstLanguage,app.Text2)
doSynth()
elif menu_response=="mp3web":
url=[] ; text1 = asUnicode(app.Text1.get())
for c in list(text1.encode("utf-8")):
......@@ -1481,14 +1632,15 @@ def gui_event_loop():
for f in os.listdir(d): dd[d+os.sep+f]=1
return dd,found
oldLs,found = scanDirs()
if not found: app.todo.alert=localise("Please set downloadsDirs in advanced"+dottxt)
if downloadsDirs and not found: app.todo.alert=localise("Please set downloadsDirs in advanced"+dottxt)
elif not url: app.todo.alert=localise("You need to type a word in the box before you can press this button")
elif not startBrowser(mp3web.replace("$Word","".join(url)).replace("$Lang",secondLanguage)): app.todo.alert = localise("Can't start the web browser")
else:
elif downloadsDirs:
waitOnMessage(localise("If the word is there, download it. When you press OK, Gradint will check for downloads."))
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:
......@@ -1521,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)
......@@ -1557,12 +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+"~") 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":
......@@ -1575,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:
......@@ -1600,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)
......@@ -1638,7 +1796,6 @@ def transliterates_differently(text,lang):
translit=synth.transliterate(lang,text,forPartials=0)
if translit and not translit==text: return translit
gui_output_counter = 1
def gui_outputTo_start():
if hasattr(app,"outputTo") and app.outputTo.get() and not app.outputTo.get()=="0":
global outputFile,gui_output_directory,oldGID ; outputFile=None
......@@ -1651,21 +1808,27 @@ def gui_outputTo_start():
if type(gui_output_directory)==type([]): gui_output_directory=gui_output_directory[-1]
try: os.mkdir(gui_output_directory)
except: pass
global gui_output_counter
gui_output_counter = 1 # now local because we also got prefix
if justSynthesize:
if B('#') in B(justSynthesize)[1:]: prefix=B("") # multiple languages
else: # prefix the language that's being synth'd
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+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()
global need_run_media_encoder
if out_type=="wma" or (out_type=="aac" and not got_program("faac")):
if out_type=="wma" or (out_type=="aac" and not (got_program("neroAacEnc") or got_program("faac"))):
need_run_media_encoder = (out_type,outputFile)
out_type="wav" ; outputFile=os.tempnam()+dotwav
else: need_run_media_encoder = 0
setSoundCollector(SoundCollector())
global waitBeforeStart, waitBeforeStart_old
waitBeforeStart_old = waitBeforeStart ; waitBeforeStart = 0
def gui_outputTo_end():
def gui_outputTo_end(openDir=True):
global outputFile, waitBeforeStart, oldGID, gui_output_directory
if outputFile:
no_output = not soundCollector.tell() # probably 'no words to put in the lesson'
......@@ -1686,9 +1849,10 @@ def gui_outputTo_end():
# 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)
......@@ -1699,16 +1863,20 @@ def gui_outputTo_end():
no_output = 1
outputFile=None
waitBeforeStart = waitBeforeStart_old
if not no_output: openDirectory(gui_output_directory)
if openDir and not no_output: openDirectory(gui_output_directory)
try: gui_output_directory = oldGID
except: pass
def main():
global useTK,justSynthesize,waitBeforeStart,traceback,appTitle
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: rest_of_main()
else:
app = None # not False anymore
if not appuifw and not android: # REALLY output them to stderr
for w in warnings_toprint: show_warning(w)
warnings_toprint = [] ; rest_of_main()
def rest_of_main():
global useTK,justSynthesize,waitBeforeStart,traceback,appTitle,saveProgress,RM_running
exitStatus = 0 ; RM_running = 1
......@@ -1718,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
......@@ -1755,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
......@@ -1768,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")
else: 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.9974 (c) 2002-2011 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,64 +87,102 @@ 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:
if app or appuifw or android:
if getYN("I/O fault when saving progress. Retry?"): continue
# TODO else try to restore the backup?
else: raise
break
if not partial: self.saved_completely = 1
if not app and not appuifw: show_info("done\n")
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,17 +204,17 @@ 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
if changed: self.save(partial=1)
elif app==None and not appuifw: show_info("No sequences were fully complete so no changes saved\n")
elif app==None and not appuifw and not android: show_info("No sequences were fully complete so no changes saved\n")
self.promptsData,self.data = curPD,curDat
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,24 +455,24 @@ 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=z[zf:].lower() # 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
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(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 (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,noext=noext:denumber_synth(noext(z),1),y))
else: y=denumber_synth(noext(y),1)
return (x,y)
def mergeProgress(progList,scan):
# Merges a progress database with a samples scan, to
# pick up any new samples that were added since last
# time. Appends to progList. Return value see below.
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))
else: x=denumber_synth(noext(x),1)
if type(y)==type([]): y=tuple(map(lambda z:denumber_synth(noext(z),1),y))
else: y=denumber_synth(noext(y),1)
return (x,y)
proglistDict = {} ; scanlistDict = {} ; n = 0
while n<len(progList):
i,j,k = progList[n]
......@@ -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.9974 (c) 2002-2011 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:
......@@ -31,24 +36,44 @@ def doOneLesson(dbase):
waitOnMessage(msg+interrupt_instructions())
#time.sleep(5)
time.sleep(2) # less confusing for beginners
elif not app and not appuifw: show_info(msg+interrupt_instructions()+"\n",True)
elif not app and not appuifw and not android: show_info(msg+interrupt_instructions()+"\n",True)
if startFunction: startFunction()
if app:
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
if not app and not app==None: break # close box pressed
if not waitBeforeStart or not getYN(cond(not askAgain_explain and (not dbase or not saveProgress or dbase.saved_completely),"Hear this lesson again?",askAgain_explain+"Start this lesson again?")): break
def disable_lid(restore): # for portable netbooks (like eee), can close lid & keep listening
if unix:
if app and not outputFile:
global oldLid,warnedAC
try: warnedAC
except: warnedAC=0
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 [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])
def lesson_loop():
......@@ -65,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.9974 (c) 2002-2011 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
......@@ -98,57 +105,63 @@ def fileToEvent(fname,dirBase=None):
transTbl = "TRANS"+extsep+"TBL"
if mp3web: # synth-cache must exist
if not synthCache: synthCache = "synth-cache"
if not isDirectory(synthCache): os.mkdir(synthCache)
try: os.mkdir(synthCache)
except: pass # already exists, temporarily-dangling symlink, etc
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):
# if justQueryCache (used by the GUI), return value is (synthCache_transtbl key, result if any). If key starts with _, we got a sporadic one.
# if justQueryCache (used by the GUI), return value is (synthCache_transtbl key, result if any). If key starts with _, we got a sporadic one. (If =2, query sporadic ones but also try partials, used for can_be_synthesized)
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)
if justQueryCache: useSporadic=1
if justQueryCache: useSporadic,tryHarder=1,0
else: tryHarder = not get_synth_if_possible(lang,0) # (tryHarder = always use sporadic if can't synth from partials, because there's no other synth to fall back on)
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: ret=(k,ret)
if justQueryCache==1: ret=(k,ret)
if init=="_":
if useSporadic==-1: useSporadic=decide_subst_synth(text)
if useSporadic: return ret
elif tryHarder: tryHarder=ret
else: return ret
if justQueryCache: 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 justQueryCache==1: return 0,0
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"]:
......@@ -161,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
......@@ -184,27 +197,29 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
global last_partials_transliteration
last_partials_transliteration=translit
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
except: NICcount=1
if NICcount>20: pass
elif NICcount==20: show_info("Further 'not in cache' warnings turned off\n",True) # (TODO configurable? important on S60 etc)
elif NICcount==20: show_info("Further 'not in cache' warnings turned off\n",True) # (important on S60 etc; TODO configurable?)
else: show_info("Not in cache: "+r+"\n",True)
def can_be_synthesized(fname,dirBase=None,lang=None):
if dirBase==None: dirBase=samplesDirectory
if dirBase: dirBase += os.sep
if not lang: lang = languageof(fname)
if get_synth_if_possible(lang,0): return True
elif synthcache_lookup(fname,dirBase,1,lang=lang): return True
elif synthcache_lookup(fname,dirBase,1,2,lang): return True
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:
......@@ -212,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
......@@ -224,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 = []
......@@ -245,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
......@@ -260,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)
......@@ -274,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 = []
......@@ -283,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 = []
......@@ -303,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)
......@@ -316,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)
......@@ -359,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:
......@@ -395,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)
......@@ -431,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
......@@ -446,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.9974 (c) 2002-2011 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,24 +41,26 @@ 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,"\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)
elif appuifw:
t=appuifw.Text() ; t.add(labelText)
appuifw.app.body = t
elif android: android.makeToast(labelText) # TODO alternatives? method to cancel lessons etc would be nice
elif not (riscos_sound or winCEsound): # (we don't have a way of updating a clock or progress indicator on those)
global doLabelLastLen
try: doLabelLastLen
......@@ -93,86 +95,107 @@ if ask_teacherMode:
old_mysleep(1)
def maybe_unicode(label):
if app or appuifw:
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_qtplay = 1 # should be bundled
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"):
if macsound and sox_formats.find("Rosetta")>-1:
try: u""+sox_formats
except: sox_formats="Please install Rosetta (from the Mac OS X optional CD) to run sox." # just in case there are encoding problems with localisation
show_warning(sox_formats.replace("sox.","some of the utilities Gradint uses.")+" Otherwise expect problems!") # (TODO need to check espeak separately in case they've compiled it x86, see in synth.py)
# TODO document a way to install Rosetta without the CD? (e.g. downloading a PowerPC-only Abiword binary seems to do it)
got_qtplay = 0
else: show_warning("SOX found, but it can't handle WAV files. Ubuntu users please install libsox-fmt-all.")
if macsound: got_afplay = got_program("afplay") # 10.5+, use in preference to the bundled qtplay which requires PowerPC or Rosetta
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:
......@@ -185,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"
......@@ -194,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('"','\\"')+'"' # 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)) # ?
......@@ -237,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
......@@ -246,65 +281,80 @@ 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 and not hasattr(self,"isTemp"): open(self.file) # ensure ready for reading
if paranoid_file_management:
if not hasattr(self,"isTemp"): open(self.file) # ensure ready for reading
fileType=soundFileType(self.file)
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
sound = audio.Sound.open(fname)
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?
finally: sound.stop()
sound.close() # (probably not worth keeping it open for repeats - there may be a limit to how many can be open)
return
elif fileType=="mp3" and madplay_program and not macsound and not playProgram=="aplay":
elif android:
fname = self.file
if not B(fname[0])==B('/'): fname=B(os.getcwd()+'/')+fname
android.mediaPlay("file://"+S(fname))
return
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 and got_qtplay:
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,))
elif macsound:
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 | ..
......@@ -313,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?
......@@ -322,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),
......@@ -356,19 +408,25 @@ def rough_guess_mp3_length(fname):
try:
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 or scan beyond 128 bytes.
# It should be fairly quick though.)
head=open(fname).read(128)
i=head.find('\xFF')
b=ord(head[i+1])
# and doesn't even check all sync bits. It should be fairly quick though.)
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(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):
......@@ -377,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
##########################################################
......@@ -394,27 +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=="aac": 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)
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)),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).
# Typical file sizes for a 30-minute lesson: OGG 2.7M, MP3 3.1M, MP2 3.4M, AAC 3.7M (all +/- at least 0.1M), WAV 152M
# 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
......@@ -425,17 +503,17 @@ 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")
self.theLen += outfile_writeFile(self.o,handle)
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")
def addBeeps(self,gap):
......@@ -444,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)
......@@ -465,32 +543,38 @@ 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()
except IOError: outfile_write_error()
def outfile_writeFile(o,handle):
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)
assert theLen, "No data: check for sox crash" # TODO report what file was being fed to sox; 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 "version " in lameVer:
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 "alpha" in lameVer 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?
......@@ -505,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
# -----------------------------------------------------
......@@ -518,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
......@@ -543,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)
......@@ -551,11 +639,11 @@ 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)
offset, length = self.bytesWritten, outfile_writeFile(self.o,handle,file)
self.bytesWritten += length
# dd is more efficient when copying large chunks - try to align to 1k
first_few_bytes = min(length,(1024-(offset%1024))%1024)
......@@ -575,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:
......@@ -590,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.")
......@@ -621,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()
......@@ -655,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.9974 (c) 2002-2011 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
......@@ -31,25 +31,33 @@ class MicInput(InputSource):
self.rate = max(rates)
else: self.rate = None
def startRec(self,outFile,lastStopRecVal=None):
if not self.rate:
app.todo.alert="Cannot record on this system (try aoss?)"
return
self.sound = tkSnack.Sound(file=outFile, rate=self.rate, channels=1, encoding="Lin16")
self.sound.record()
def stopRec(self): self.sound.stop()
if not self.rate: return self.err("Cannot record on this system (try aoss?)")
try: self.sound = tkSnack.Sound(file=outFile, rate=self.rate, channels=1, encoding="Lin16")
except: return self.err("Cannot write to sound file '"+outFile+"' with tkSnack")
try: self.sound.record()
except: # e.g. waveInOpen failed on Windows 7 (driver problems?)
self.err("sound.record() failed")
try: self.sound.stop()
except: pass
try: os.remove(outFile)
except: pass
del self.sound
def err(self,msg): app.todo.alert=msg
def stopRec(self):
if hasattr(self,"sound"): self.sound.stop()
class PlayerInput(InputSource): # play to speakers while recording to various destinations
def __init__(self,fileToPlay,startNow=True,startTime=0): # (if startNow=False, starts when you start recording)
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)
......@@ -78,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
......@@ -124,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
......@@ -200,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)
......@@ -222,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])
......@@ -252,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
......@@ -277,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):
......@@ -315,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
......@@ -328,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"):
......@@ -346,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)
......@@ -390,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()
......@@ -398,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
......@@ -419,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
......@@ -444,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
......@@ -455,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()
......@@ -514,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)
......@@ -522,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:
......@@ -530,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()
......@@ -555,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
......@@ -577,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()
......@@ -596,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")
......@@ -635,17 +664,23 @@ 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)
if paranoid_file_management and tkSnack.audio.playLatency()<500: tkSnack.audio.playLatency(500) # at least 500ms latency if we're paranoid_file_management, just in case (since tkSnack.audio.elapsedTime does not account for hold-ups)
if paranoid_file_management:
if tkSnack.audio.playLatency()<500: tkSnack.audio.playLatency(500) # at least 500ms latency if we're paranoid_file_management, just in case (since tkSnack.audio.elapsedTime does not account for hold-ups)
self.snack_initialized = True
if not hasattr(app,"scanrow"):
app.scanrow = Tkinter.StringVar(app) # (keep StringVars in app not self to avoid d'tor confusion when close box pressed)
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)
......@@ -653,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 [
......@@ -669,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
......@@ -681,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)
......@@ -701,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)
......@@ -731,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
......@@ -767,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()
......@@ -784,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"))
......@@ -803,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("_"):])
......@@ -826,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.9974 (c) 2002-2011 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.9974 (c) 2002-2011 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,14 +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
......@@ -64,24 +68,87 @@ def unzip_and_delete(f,specificFiles="",ignore_fail=0):
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 works_on_this_platform(self):
if not (macsound and fileExists("/usr/bin/say")): return False
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\"" % (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\"" % (fname,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
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():
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")
......@@ -91,21 +158,31 @@ 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")
def guess_length(self,lang,text): return quickGuess(len(text),12) # TODO need a better estimate
def play(self,lang,text): audio.say(text)
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)
class AndroidSynth(Synth):
def __init__(self): Synth.__init__(self)
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)
if winsound or mingw32: toNull=" > nul"
else: toNull=" >/dev/null" # stdout only, not stderr, because we want to see any errors that happen
......@@ -127,9 +204,10 @@ class PttsSynth(Synth):
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)
# 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)
......@@ -138,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
......@@ -162,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'
......@@ -175,13 +253,18 @@ class PttsSynth(Synth):
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)
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")
r=p.close()
elif lang=='zh':
self.sapi_unicode("VW Lily",self.preparePinyinPhrase(text),fname,16000,speed=sapiSpeeds.get(lang,None))
r=self.sapi_unicode("VW Lily",self.preparePinyinPhrase(text),fname,16000,speed=sapiSpeeds.get(lang,None))
self.restore_lily_dict()
else: r=0 # shouldn't get here
os.chdir(oldcwd)
d = sapi_sox_bug_workaround(read(fname)); open(fname,"wb").write(d)
assert not r,"ptts.exe failed"
d = sapi_sox_bug_workaround(read(fname)); write(fname,d)
if cygwin: os.system("chmod -x '"+fname+"'")
return fname
def preparePinyinPhrase(self,pinyin):
......@@ -215,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")
......@@ -227,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
......@@ -241,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)
......@@ -269,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"]
......@@ -297,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):
......@@ -320,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 = []
......@@ -334,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):
......@@ -342,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 ['en'] # 'zh' is sort-of OK, but if the user has explicitly installed another synth for it then had probably better defer to that
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:
......@@ -387,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:
......@@ -397,11 +497,9 @@ class ESpeakSynth(Synth):
return True
return False
else: # not windows or cygwin
if macsound and not got_qtplay: return False # Rosetta required (TODO need to check espeak separately in case they've compiled it x86)
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
......@@ -409,26 +507,38 @@ 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)
return quickGuess(l,12)+cond(winCEsound,1.3,0) # TODO need a better estimate. Overhead on 195MHz Vario (baseline?) >1sec (1.3 seems just about ok)
latency = 0
if winCEsound: latency = 1.3 # TODO need a better estimate. Overhead on 195MHz Vario (baseline?) >1sec (1.3 seems just about ok)
elif unix:
if espeak_pipe_through and not outputFile:
if not hasattr(self,"latency"):
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: 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)
def winCE_run(self,parameters,expectedOutputFile,infileToDel=None):
self.winCE_start(parameters)
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)
......@@ -437,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:
......@@ -450,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]
......@@ -462,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])
......@@ -519,38 +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[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 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(l[7])-ord('a')])
else: r.append(l[7])
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
......@@ -575,12 +723,13 @@ class ESpeakSynth(Synth):
finally:
if hasattr(self,"winCEhint"): os.unlink(self.makefile(lang,"")) # make sure to clean up on interrupt
return ret
elif winsound or mingw32 or cygwin:
# Windows command line is not always 100% UTF-8 safe, so we'd better use a pipe. (Command line ok on other platforms, and must do it on riscos - no pipes.) (espeak_pipe_through only needs supporting on non-Windows - it's for aplay etc)
p=os.popen(self.program+cond(text.find("</")>-1," -m","")+' -v%s -a%d' % (espeak_language_aliases.get(lang,lang),100*soundVolume),"wb")
p.write(text+"\n") ; return p.close()
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)
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(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
......@@ -590,7 +739,8 @@ class ESpeakSynth(Synth):
return fname
fname = os.tempnam()+dotwav
oldcwd=os.getcwd()
sysCommand = cond(winCEsound,"",self.program)+cond(text.find("</")>-1," -m","")+' -w %s -v%s' % (changeToDirOf(fname,1),espeak_language_aliases.get(lang,lang))
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
elif riscos_sound: os.system(sysCommand+' '+shell_escape(text))
......@@ -601,11 +751,11 @@ class ESpeakSynth(Synth):
self.winCE_start(sysCommand+' -f '+fnameIn)
else: self.winCE_run(sysCommand+' -f '+fnameIn,fname,fnameIn)
else:
# we can make it asynchronously
if cygwin: sysCommand='echo "'+text.replace('"','\\"')+'"|'+sysCommand # (still need to pipe)
else: sysCommand += (' "'+text.replace('"','\\"')+'"')
# 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=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
......@@ -616,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
......@@ -630,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
......@@ -669,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
......@@ -734,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
......@@ -782,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):
......@@ -817,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
......@@ -833,15 +1042,17 @@ class GeneralFileSynth(Synth):
else: os.rename(f,fname)
return fname
all_synth_classes = [GeneralSynth,GeneralFileSynth] # first so user can override
for s in synth_priorities.split():
if s.lower()=="espeak": all_synth_classes.append(ESpeakSynth)
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)
elif s.lower()=="macos":
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]
all_synth_classes += [FestivalSynth,FliteSynth,OldRiscosSynth,S60Synth,AndroidSynth]
prefer_espeak = prefer_espeak.split()
viable_synths = []
warned_about_nosynth = {}
......@@ -851,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:
......@@ -863,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?)"
......@@ -889,27 +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):
self.text = text ; self.synthesizer = synthesizer
assert text,"Trying to speak zero-length text"
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.)
......@@ -944,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:
......@@ -954,7 +1173,7 @@ class SynthEvent(Event):
except IOError:
time.sleep(1) ; continue
return e.play()
raise IOError("IOError after 10 tries")
raise IOError("IOError after 10 tries on "+repr(self.sound))
else:
assert (not soundCollector) and hasattr(self.synthesizer,"play"),"Should have called will_be_played before playing offline"
return self.synthesizer.play(self.language,self.modifiedText)
......@@ -1004,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
......@@ -1014,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
......@@ -1024,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:
......@@ -1049,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
......@@ -1073,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