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 2452 additions and 898 deletions
#!/bin/bash #!/bin/bash
# email-lesson-archive.sh - archive an old email-lesson user # email-lesson-archive.sh - archive an old email-lesson user
# (C) 2008 Silas S. Brown, License: GPL # (C) 2008,2021-22 Silas S. Brown, License: GPL
if ! pwd|grep email_lesson_users >/dev/null; then 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)" echo "This script should be run from an email_lesson_users directory (see email-lesson.sh)"
...@@ -13,29 +13,29 @@ if test "a$1" == a; then ...@@ -13,29 +13,29 @@ if test "a$1" == a; then
fi fi
. config . config
while ! test "a$1" == a; do while ! test "a$1" == a; do
if test -e "$1"; then if [ -e "$1" ]; then
unset U; unset Links unset U; unset Links
if echo "$1"|grep "^user.0*" >/dev/null; then if echo "$1"|grep "^user.0*" >/dev/null; then
# specifying by user.0* id # specifying by user.0* id
export U=$1 export U=$1
export Links=$(find . -maxdepth 1 -lname $U) Links=$(find . -maxdepth 1 -lname "$U")
elif ls -l --color=none "$1"|grep ' -> ' >/dev/null; then elif find "$1" -maxdepth 0 -type l|grep . >/dev/null; then
# specifying by symlink # specifying by symlink
export Links=$1 Links=$1
export U=$(ls -l --color=none "$1"|sed -e 's/.* -> //') U=$(ls -l --color=none "$1"|sed -e 's/.* -> //')
else echo "Warning: can't make sense of username $1"; fi else echo "Warning: can't make sense of username $1"; fi
if ! test "a$U" == a; then if ! [ "a$U" == a ]; then
if test -e $U/lastdate; then if [ -e "$U/lastdate" ]; then
if test "a$Links" == a; then export Shortname=$U; else export Shortname=$Links; fi if test "a$Links" == a; then export Shortname=$U; else export Shortname=$Links; fi
if echo $PUBLIC_HTML | grep : >/dev/null; then if echo "$PUBLIC_HTML" | grep : >/dev/null; then
ssh $PUBLIC_HTML_EXTRA_SSH_OPTIONS $(echo $PUBLIC_HTML|sed -e 's/:.*//') rm -v $(echo $PUBLIC_HTML|sed -e 's/[^:]*://')/$U-$(cat $U/lastdate).* ssh $PUBLIC_HTML_EXTRA_SSH_OPTIONS "$(echo "$PUBLIC_HTML"|sed -e 's/:.*//')" rm -v "$(echo "$PUBLIC_HTML"|sed -e 's/[^:]*://')/$U-$(cat $U/lastdate).*"
else rm -v $PUBLIC_HTML/$U-$(cat $U/lastdate).* else rm -v "$PUBLIC_HTML/$U-$(cat "$U/lastdate")".*
fi fi
fi fi
tar -jcvf $Shortname.tbz $U $Links tar -jcvf "$Shortname.tbz" "$U" $Links
mkdir -p old mkdir -p old
mv -v --backup=numbered $Shortname.tbz old/ mv -v --backup=numbered "$Shortname.tbz" old/
rm -rf $U $Links rm -rf "$U" $Links
fi fi
else echo "Warning: User $1 does not exist"; fi else echo "Warning: User $1 does not exist"; fi
shift; done shift; done
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
# - report is written to standard output so you can include # - report is written to standard output so you can include
# it in a script that makes some larger HTML page # 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 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)" 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 ...@@ -14,13 +14,13 @@ if ! pwd|grep email_lesson_users >/dev/null; then
fi fi
echo '<TABLE>' echo '<TABLE>'
touch -d 0:00 /dev/shm/.midnight 2>/dev/null || touch -d 0:00 /tmp/.midnight 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 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 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 test "$P" -nt $Midnight; then Em="*";else unset Em; fi
if echo $P | grep podcasts-to-send$ >/dev/null; then 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>," 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 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 fi
done done
rm $Midnight rm $Midnight
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
# email-lesson.sh: a script that can help you to # email-lesson.sh: a script that can help you to
# automatically distribute daily Gradint lessons # automatically distribute daily Gradint lessons
# to students using a web server with reminder # 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 # 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 # it under the terms of the GNU General Public License as published by
...@@ -17,110 +17,110 @@ ...@@ -17,110 +17,110 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
export DEFAULT_SUBJECT_LINE="Vocabulary practice (automatic message from gradint)" DEFAULT_SUBJECT_LINE="Vocabulary practice (automatic message from gradint)"
export DEFAULT_FORGOT_YESTERDAY="You forgot your lesson yesterday. DEFAULT_FORGOT_YESTERDAY="You forgot your lesson yesterday.
Please remember to download your lesson from" 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) # (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." this program will make the next one for tomorrow."
export DEFAULT_NEW_LESSON="Your lesson for today is at" DEFAULT_NEW_LESSON="Your lesson for today is at"
export DEFAULT_LISTEN_TODAY="Please download and listen to it today." DEFAULT_LISTEN_TODAY="Please download and listen to it today."
export DEFAULT_AUTO_MESSAGE="This is an automatic message from the gradint program. 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, Any problems, requests, or if you no longer wish to receive these emails,
let me know." 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." echo "Error: This script should ALWAYS be run in the gradint directory."
exit 1 exit 1
fi fi
if which mail >/dev/null 2>/dev/null; then export DefaultMailProg=mail if which mail >/dev/null 2>/dev/null; then DefaultMailProg=mail
elif which mutt >/dev/null 2>/dev/null; then export DefaultMailProg="mutt -x" elif which mutt >/dev/null 2>/dev/null; then DefaultMailProg="mutt -x"
else export DefaultMailProg="ssh example.org mail" else DefaultMailProg="ssh example.org mail"
fi fi
if test "a$1" == "a--run"; then if [ "$1" == "--run" ]; then
set -o pipefail # make sure errors in pipes are reported 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" echo "Error: script does not seem to have been set up yet"
exit 1 exit 1
fi fi
export Gradint_Dir=$(pwd) Gradint_Dir=$(pwd)
cd email_lesson_users cd email_lesson_users || exit
. config . config
if test -e $Gradint_Dir/.email-lesson-running; then if [ -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.)" 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"
echo "$Msg"|$MailProg -s email-lesson-not-running $ADMIN_EMAIL # don't worry about retrying that echo "$Msg"|$MailProg -s email-lesson-not-running $ADMIN_EMAIL # don't worry about retrying that
exit 1 exit 1
fi fi
touch $Gradint_Dir/.email-lesson-running touch "$Gradint_Dir/.email-lesson-running"
if echo $PUBLIC_HTML | grep : >/dev/null && man ssh 2>/dev/null | grep ControlMaster >/dev/null; then 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 # 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" 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=$! 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 else unset MasterPid
fi 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) # (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 .. cd ..
unset NeedRunMirror unset NeedRunMirror
for U in $Users; do for U in $Users; do
. email_lesson_users/config . 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 # set some (but not all!) variables to defaults in case not set in profile
export SUBJECT_LINE="$DEFAULT_SUBJECT_LINE" SUBJECT_LINE="$DEFAULT_SUBJECT_LINE"
export FORGOT_YESTERDAY="$DEFAULT_FORGOT_YESTERDAY" FORGOT_YESTERDAY="$DEFAULT_FORGOT_YESTERDAY"
export LISTEN_TODAY="$DEFAULT_LISTEN_TODAY" LISTEN_TODAY="$DEFAULT_LISTEN_TODAY"
export NEW_LESSON="$DEFAULT_NEW_LESSON" NEW_LESSON="$DEFAULT_NEW_LESSON"
export EXPLAIN_FORGOT="$DEFAULT_EXPLAIN_FORGOT" EXPLAIN_FORGOT="$DEFAULT_EXPLAIN_FORGOT"
export AUTO_MESSAGE="$DEFAULT_AUTO_MESSAGE" AUTO_MESSAGE="$DEFAULT_AUTO_MESSAGE"
unset Extra_Mailprog_Params1 Extra_Mailprog_Params2 GRADINT_OPTIONS unset Extra_Mailprog_Params1 Extra_Mailprog_Params2 GRADINT_OPTIONS
export Use_M3U=no Use_M3U=no
export FILE_TYPE=mp3 FILE_TYPE=mp3
if grep $'\r' email_lesson_users/$U/profile >/dev/null; then 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. # 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 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 mv "email_lesson_users/$U/profile.removeCR" "email_lesson_users/$U/profile"
fi fi
. email_lesson_users/$U/profile . "email_lesson_users/$U/profile"
if test "a$Use_M3U" == ayes; then export FILE_TYPE_2=m3u if [ "$Use_M3U" == yes ]; then FILE_TYPE_2=m3u
else export FILE_TYPE_2=$FILE_TYPE; fi else FILE_TYPE_2=$FILE_TYPE; fi
if echo "$MailProg" | grep ssh >/dev/null; then if echo "$MailProg" | grep ssh >/dev/null; then
# ssh discards a level of quoting, so we need to be more careful # ssh discards a level of quoting, so we need to be more careful
export SUBJECT_LINE="\"$SUBJECT_LINE\"" SUBJECT_LINE="\"$SUBJECT_LINE\""
export Extra_Mailprog_Params1="\"$Extra_Mailprog_Params1\"" Extra_Mailprog_Params1="\"$Extra_Mailprog_Params1\""
export Extra_Mailprog_Params2="\"$Extra_Mailprog_Params2\"" Extra_Mailprog_Params2="\"$Extra_Mailprog_Params2\""
fi fi
if test -e email_lesson_users/$U/lastdate; then if [ -e "email_lesson_users/$U/lastdate" ]; then
if test "$(cat email_lesson_users/$U/lastdate)" == "$(date +%Y%m%d)"; then if [ "$(cat "email_lesson_users/$U/lastdate")" == "$(date +%Y%m%d)" ]; then
# still on same day - do nothing with this user this time # still on same day - do nothing with this user this time
continue continue
fi 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) # (don't add $FILE_TYPE after \. in case it has been changed)
then then
export Did_Download=0 Did_Download=0
if test -e email_lesson_users/$U/rollback; then if [ -e "email_lesson_users/$U/rollback" ]; then
if test -e email_lesson_users/$U/progress.bak; then if [ -e "email_lesson_users/$U/progress.bak" ]; then
mv email_lesson_users/$U/progress.bak email_lesson_users/$U/progress.txt mv "email_lesson_users/$U/progress.bak" "email_lesson_users/$U/progress.txt"
rm -f email_lesson_users/$U/progress.bin rm -f "email_lesson_users/$U/progress.bin"
export Did_Download=1 # (well actually they didn't, but we're rolling back) Did_Download=1 # (well actually they didn't, but we're rolling back)
fi # else can't rollback, as no progress.bak fi # else can't rollback, as no progress.bak
if test -e email_lesson_users/$U/podcasts-to-send.old; then 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 mv "email_lesson_users/$U/podcasts-to-send.old" "email_lesson_users/$U/podcasts-to-send"
fi fi
fi fi
else export Did_Download=1; fi else Did_Download=1; fi
rm -f email_lesson_users/$U/rollback rm -f "email_lesson_users/$U/rollback"
if test $Did_Download == 0; then if [ $Did_Download == 0 ]; then
# send a reminder # 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)")" DaysOld="$(python -c "import os,time;print(int((time.time()-os.stat('email_lesson_users/$U/lastdate').st_mtime)/3600/24))")"
if test $DaysOld -lt 5 || test $(date +%u) == 1; then # (remind only on Mondays if not checked for 5 days, to avoid filling up inboxes when people are away and can't get to email) if [ $DaysOld -lt 5 ] || [ $(date +%u) == 1 ]; then # (remind only on Mondays if not checked for 5 days, to avoid filling up inboxes when people are away and can't get to email)
while ! $MailProg -s "$SUBJECT_LINE" $STUDENT_EMAIL "$Extra_Mailprog_Params1" "$Extra_Mailprog_Params2" <<EOF while ! $MailProg -s "$SUBJECT_LINE" "$STUDENT_EMAIL" "$Extra_Mailprog_Params1" "$Extra_Mailprog_Params2" <<EOF
$FORGOT_YESTERDAY $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 $EXPLAIN_FORGOT
$AUTO_MESSAGE $AUTO_MESSAGE
...@@ -129,68 +129,68 @@ do echo "mail sending failed; retrying in 62 seconds"; sleep 62; done; fi ...@@ -129,68 +129,68 @@ do echo "mail sending failed; retrying in 62 seconds"; sleep 62; done; fi
continue continue
else else
# delete the previous lesson # 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).* 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 else rm $PUBLIC_HTML/$U-$(cat "email_lesson_users/$U/lastdate").*; fi
# (.* because .$FILE_TYPE and possibly .m3u as well) # (.* because .$FILE_TYPE and possibly .m3u as well)
fi fi
fi fi
export CurDate=$(date +%Y%m%d) CurDate=$(date +%Y%m%d)
if ! test "a$GRADINT_OPTIONS" == a; then export GRADINT_OPTIONS="$GRADINT_OPTIONS ;"; fi if [ "$GRADINT_OPTIONS" ]; then GRADINT_OPTIONS="$GRADINT_OPTIONS ;"; fi
if echo $PUBLIC_HTML | grep : >/dev/null; then export OUTDIR=$TMPDIR if echo "$PUBLIC_HTML" | grep : >/dev/null; then OUTDIR=$TMPDIR
else export OUTDIR=$PUBLIC_HTML; fi else 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=" 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) # (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 unset Send_Podcast_Instead
if test -s email_lesson_users/$U/podcasts-to-send; then if [ -s "email_lesson_users/$U/podcasts-to-send" ]; then
export Send_Podcast_Instead="$(head -1 email_lesson_users/$U/podcasts-to-send)" 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) 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 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-send" "email_lesson_users/$U/podcasts-to-send.old"
mv email_lesson_users/$U/podcasts-to-send2 email_lesson_users/$U/podcasts-to-send mv "email_lesson_users/$U/podcasts-to-send2" "email_lesson_users/$U/podcasts-to-send"
if test $NumLines == 0; then if [ $NumLines == 0 ]; then
echo $U | $MailProg -s Warning:email-lesson-run-out-of-podcasts $ADMIN_EMAIL echo "$U" | $MailProg -s Warning:email-lesson-run-out-of-podcasts $ADMIN_EMAIL
fi 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 fi
if test "$ENCODE_ON_REMOTE_HOST" == 1; then if [ "$ENCODE_ON_REMOTE_HOST" == 1 ]; then
export ToSleep=123 ToSleep=123
while ! if test "a$Send_Podcast_Instead" == a; then 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"; 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 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 fi; do
# (</dev/null so exceptions don't get stuck on 'press enter to continue' to a temp stderr if running from a terminal) # (</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) # (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) # (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 done
rm $TMPDIR/__stderr rm "$TMPDIR/__stderr"
if test "a$Use_M3U" == ayes; then if [ "$Use_M3U" == yes ]; then
while ! ssh -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $ControlPath $(echo $PUBLIC_HTML|sed -e 's/:.*//') "echo $OUTSIDE_LOCATION/$U-$CurDate.$FILE_TYPE > $(echo $PUBLIC_HTML|sed -e 's/[^:]*://')/$U-$CurDate.m3u"; do sleep 63; done 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 fi
else # not ENCODE_ON_REMOTE_HOST else # not ENCODE_ON_REMOTE_HOST
if ! test "a$Send_Podcast_Instead" == a; then if [ "$Send_Podcast_Instead" ]; then
(cd email_lesson_users/$U ; cat "$Send_Podcast_Instead") > "$OUTDIR/$U-$CurDate.$FILE_TYPE" (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 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 "Errors from gradint itself (not ssh/network); skipping this user."
echo "Failed on $U, check output " | $MailProg -s gradint-failed $ADMIN_EMAIL echo "Failed on $U, check output " | $MailProg -s gradint-failed $ADMIN_EMAIL
continue continue
fi fi
if test "a$Use_M3U" == ayes; then if [ "$Use_M3U" == yes ]; then
echo $OUTSIDE_LOCATION/$U-$CurDate.$FILE_TYPE > $OUTDIR/$U-$CurDate.m3u echo "$OUTSIDE_LOCATION/$U-$CurDate.$FILE_TYPE" > "$OUTDIR/$U-$CurDate.m3u"
fi fi
if echo $PUBLIC_HTML | grep : >/dev/null; then if echo "$PUBLIC_HTML" | grep : >/dev/null; then
while ! scp $ControlPath -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $OUTDIR/$U-$CurDate.* $PUBLIC_HTML/; do while ! scp $ControlPath -C $PUBLIC_HTML_EXTRA_SSH_OPTIONS $OUTDIR/$U-$CurDate.* "$PUBLIC_HTML/"; do
echo "scp failed; re-trying in 60 seconds" echo "scp failed; re-trying in 60 seconds"
sleep 64 sleep 64
done done
rm $OUTDIR/$U-$CurDate.* rm "$OUTDIR/$U-$CurDate".*
fi fi
fi fi
export NeedRunMirror=1 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 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 while ! $MailProg -s "$SUBJECT_LINE" "$STUDENT_EMAIL" "$Extra_Mailprog_Params1" "$Extra_Mailprog_Params2" <<EOF
$NEW_LESSON $NEW_LESSON
$OUTSIDE_LOCATION/$U-$CurDate.$FILE_TYPE_2 $OUTSIDE_LOCATION/$U-$CurDate.$FILE_TYPE_2
$LISTEN_TODAY $LISTEN_TODAY
...@@ -198,27 +198,27 @@ $LISTEN_TODAY ...@@ -198,27 +198,27 @@ $LISTEN_TODAY
$AUTO_MESSAGE $AUTO_MESSAGE
EOF EOF
do echo "mail sending failed; retrying in 65 seconds"; sleep 65; done 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 unset AdminNote
if test "a$Send_Podcast_Instead" == a; then if [ "$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 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 ! test -e email_lesson_users/$U/podcasts-to-send; then export AdminNote="Note: $U has run out of podcasts"; fi elif ! [ -e "email_lesson_users/$U/podcasts-to-send" ]; then AdminNote="Note: $U has run out of podcasts"; fi
if ! test "a$AdminNote" == a; then if [ "$AdminNote" ]; then
while ! echo "$AdminNote"|$MailProg -s gradint-user-ran-out $ADMIN_EMAIL; do echo "Mail sending failed; retrying in 67 seconds"; sleep 67; done while ! echo "$AdminNote"|$MailProg -s gradint-user-ran-out "$ADMIN_EMAIL"; do echo "Mail sending failed; retrying in 67 seconds"; sleep 67; done
fi fi
done # end of per-user loop 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 while ! $PUBLIC_HTML_MIRROR_COMMAND; do
echo "PUBLIC_HTML_MIRROR_COMMAND failed; retrying in 79 seconds" 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 sleep 79
done done
fi fi
rm -f $TMPDIR/._email_lesson_logs rm -f "$TMPDIR/._email_lesson_logs"
if ! test a$MasterPid == a; then if [ $MasterPid ] ; then
kill $MasterPid kill $MasterPid
kill $(ps axwww|grep $TMPDIR/__gradint_ctrl|sed -e 's/^ *//' -e 's/ .*//') 2>/dev/null kill $(pgrep -f "$TMPDIR/__gradint_ctrl") 2>/dev/null
rm -f $TMPDIR/__gradint_ctrl # in case ssh doesn't rm -f "$TMPDIR/__gradint_ctrl" # in case ssh doesn't
fi fi
rm -f "$Gradint_Dir/.email-lesson-running" rm -f "$Gradint_Dir/.email-lesson-running"
exit 0 exit 0
...@@ -227,10 +227,10 @@ fi ...@@ -227,10 +227,10 @@ fi
echo "After setting up users, run this script daily with --run on the command line." 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." echo "As --run was not specified, it will now go into setup mode."
# Setup: # Setup:
if test "a$EDITOR" == a; then if ! [ "$EDITOR" ]; then
echo "Error: No EDITOR environment variable set"; exit 1 echo "Error: No EDITOR environment variable set"; exit 1
fi 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 "It seems the email_lesson_users directory is not set up"
echo "Press Enter to create a new one, echo "Press Enter to create a new one,
or Ctrl-C to quit if you're in the wrong directory" or Ctrl-C to quit if you're in the wrong directory"
...@@ -238,11 +238,11 @@ if ! test -e email_lesson_users/config; then ...@@ -238,11 +238,11 @@ if ! test -e email_lesson_users/config; then
mkdir email_lesson_users || exit 1 mkdir email_lesson_users || exit 1
cat > email_lesson_users/config <<EOF cat > email_lesson_users/config <<EOF
# You need to edit this file. # 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) 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 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. 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 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 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 # 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 # to make the script suid root.) It is used to check that the users have
# downloads their lessons and remind them if not. # 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 ...@@ -254,29 +254,29 @@ export CAT_LOGS_COMMAND="false" # Please change this to a command that cats the
# control connection (\$ControlPath will expand to # control connection (\$ControlPath will expand to
# nothing on systems with old ssh's that don't support this) # 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). # 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. # 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 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) # that's specified in PUBLIC_HTML (which *must* be host:path in this case)
# and will run an encoding command *there*, instead of encoding # and will run an encoding command *there*, instead of encoding
# locally and copying up. This is useful if the local machine is the # 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). # 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: # 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) # (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 # 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) # (used only if ENCODE_ON_REMOTE_HOST is set)
# (include the full path for that if necessary; SOX_PATH will NOT be searched) # (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.) # (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 EOF
cd email_lesson_users; $EDITOR config; cd .. cd email_lesson_users; $EDITOR config; cd ..
echo "Created email_lesson_users/config" echo "Created email_lesson_users/config"
...@@ -285,16 +285,16 @@ cd email_lesson_users ...@@ -285,16 +285,16 @@ cd email_lesson_users
while true; do while true; do
echo "Type a user alias (or just press Enter) to add a new user, or Ctrl-C to quit" echo "Type a user alias (or just press Enter) to add a new user, or Ctrl-C to quit"
read Alias 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) ID=$(mktemp -d user.$(python -c 'import random; print(random.random())')XXXXXX) # (newer versions of mktemp allow more than 6 X's so the python step isn't necessary, but just in case we want to make sure that it's hard to guess the ID)
if ! test "a$Alias" == a; then ln -s $ID "$Alias"; fi if [ "$Alias" ]; then ln -s "$ID" "$Alias"; fi
cd $ID cd "$ID" || exit 1
cat > profile <<EOF cat > profile <<EOF
# You need to edit the settings in this file. # 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 export GRADINT_OPTIONS="" # extra gradint command-line options, for example to
# specify a different first and second language # specify a different first and second language
export FILE_TYPE=mp3 # change to something else if you want FILE_TYPE=mp3 # change to something else if you want
export Use_M3U=no # if yes, sends a .m3u link to the student Use_M3U=no # if yes, sends a .m3u link to the student
# instead of sending the file link directly. Use this if # instead of sending the file link directly. Use this if
# the student needs to stream over a slow link, but note # the student needs to stream over a slow link, but note
# that it makes offline listening one step more complicated. # 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 ...@@ -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. # students that the lessons will now come from a different address.
# Optional settings for customising the text of the message: # Optional settings for customising the text of the message:
export SUBJECT_LINE="$DEFAULT_SUBJECT_LINE" SUBJECT_LINE="$DEFAULT_SUBJECT_LINE"
export FORGOT_YESTERDAY="$DEFAULT_FORGOT_YESTERDAY" FORGOT_YESTERDAY="$DEFAULT_FORGOT_YESTERDAY"
export LISTEN_TODAY="$DEFAULT_LISTEN_TODAY" LISTEN_TODAY="$DEFAULT_LISTEN_TODAY"
export NEW_LESSON="$DEFAULT_NEW_LESSON" NEW_LESSON="$DEFAULT_NEW_LESSON"
export EXPLAIN_FORGOT="$DEFAULT_EXPLAIN_FORGOT" EXPLAIN_FORGOT="$DEFAULT_EXPLAIN_FORGOT"
export AUTO_MESSAGE="$DEFAULT_AUTO_MESSAGE" AUTO_MESSAGE="$DEFAULT_AUTO_MESSAGE"
export Extra_Mailprog_Params1="" Extra_Mailprog_Params1=""
export Extra_Mailprog_Params2="" Extra_Mailprog_Params2=""
# You may need to set Extra_Mailprog_Params to extra parameters # You may need to set Extra_Mailprog_Params to extra parameters
# if the subject or text includes characters that need to be sent # if the subject or text includes characters that need to be sent
# in a specific charset. For example, to send in Chinese GB2312 # in a specific charset. For example, to send Chinese (Simplified)
# with Mutt, you can do this: # in UTF-8 with Mutt, you can do this:
# export GRADINT_OPTIONS="firstLanguage='zh'; secondLanguage='en'; otherLanguages=[]" # export GRADINT_OPTIONS="firstLanguage='zh'; secondLanguage='en'; otherLanguages=[]"
# export LANG=C # export LANG=C
# export Extra_Mailprog_Params1="-e" # Extra_Mailprog_Params1="-e"
# export Extra_Mailprog_Params2="set charset='gb2312'; set send_charset='gb2312'" # Extra_Mailprog_Params2="set charset='utf-8'; set send_charset='utf-8'"
# export SUBJECT_LINE="英文词汇练习 (English vocabulary practice)" # SUBJECT_LINE="英文词汇练习 (English vocabulary practice)"
# export FORGOT_YESTERDAY="你忘记了昨天的课 (you forgot your lesson yesterday). # FORGOT_YESTERDAY="你忘记了昨天的课 (you forgot your lesson yesterday).
# 请记得下载 (please remember to download) :" # 请记得下载 (please remember to download) :"
# export EXPLAIN_FORGOT="请试图天天听一课 (please try to hear one lesson every day) # EXPLAIN_FORGOT="请试图天天听一课 (please try to hear one lesson every day)
# 如果你今天下载, 这个软件要明天给你另一个课. # 如果你今天下载, 这个软件要明天给你另一个课.
# (If you download that lesson today, # (If you download that lesson today,
# this program will make the next one for tomorrow.)" # this program will make the next one for tomorrow.)"
# export NEW_LESSON="今天的课在以下的网址 (your lesson for today is at)" # NEW_LESSON="今天的课在以下的网址 (your lesson for today is at)"
# export LISTEN_TODAY="请你今天下载而听 (please download and listen to it today)." # LISTEN_TODAY="请你今天下载而听 (please download and listen to it today)."
# export AUTO_MESSAGE="这个电邮是软件写的 (this is an automatic message from the gradint program). # AUTO_MESSAGE="这个电邮是软件写的 (this is an automatic message from the gradint program).
# 假如你有问题, 请告诉我 (any problems, let me know)." # 假如你有问题, 请告诉我 (any problems, let me know)."
# You can also override *some* of the email_lesson_users/config # You can also override *some* of the email_lesson_users/config
# options on a per-user basis by putting them here, # options on a per-user basis by putting them here,
......
#!/usr/bin/env python #!/usr/bin/env python
# (Python 2 or Python 3)
# espeak.cgi - a CGI script for the eSpeak speech synthesizer # espeak.cgi - a CGI script for the eSpeak speech synthesizer
# (c) 2008,2011 Silas S. Brown, License: GPL # (c) 2008,2011,2020,2025 Silas S. Brown, License: GPL
version="1.122" version="1.31"
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
...@@ -22,6 +23,9 @@ version="1.122" ...@@ -22,6 +23,9 @@ version="1.122"
# import os ; os.environ["PATH"]="/my/espeak/path:"+os.environ["PATH"] # import os ; os.environ["PATH"]="/my/espeak/path:"+os.environ["PATH"]
# ) # )
# If your Python is 3.13 or above (expected in Ubuntu 2.26 LTS)
# then you will need: pip install legacy-cgi
import cgi, cgitb ; cgitb.enable() import cgi, cgitb ; cgitb.enable()
f = cgi.FieldStorage() f = cgi.FieldStorage()
...@@ -31,15 +35,28 @@ max_input_size = 1000 ...@@ -31,15 +35,28 @@ max_input_size = 1000
minSpeed, defaultSpeed, maxSpeed, speedStep = 80,170,370,30 # (NB check defaultSpeed=minSpeed+integer*speedStep) minSpeed, defaultSpeed, maxSpeed, speedStep = 80,170,370,30 # (NB check defaultSpeed=minSpeed+integer*speedStep)
import os,commands,re,sys import os,re,sys
if commands.getoutput("which espeak 2>/dev/null"): prog="espeak" try: from commands import getoutput # Python 2
elif commands.getoutput("which speak 2>/dev/null"): prog="speak" 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") 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 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") variants = os.listdir(voiceDir+"/!v")
if "whisper" in variants and "wisper" in variants: variants.remove("wisper") if "whisper" in variants and "wisper" in variants: variants.remove("wisper")
...@@ -55,16 +72,17 @@ try: speed=int(speed) ...@@ -55,16 +72,17 @@ try: speed=int(speed)
except: speed=defaultSpeed except: speed=defaultSpeed
if speed<minSpeed or speed>maxSpeed: 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="" if len(t)>max_input_size: t=""
else: else:
try: t.decode('utf-8') if not type(t)==type(u""):
except: t="" # not valid utf-8 try: t.decode('utf-8')
except: t="" # not valid utf-8
if chr(0) in t: t="" # just in case if chr(0) in t: t="" # just in case
if len(t)>stream_if_input_bigger_than: if len(t)>stream_if_input_bigger_than:
# streaming - will need sox to convert # 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 fname=None
else: else:
# not streaming (so can fill in length etc) - will need a writable file in a private tmp directory, preferably in memory # not streaming (so can fill in length etc) - will need a writable file in a private tmp directory, preferably in memory
...@@ -80,15 +98,16 @@ else: ...@@ -80,15 +98,16 @@ else:
open(fname2,"w") # raising exception if it's unwritable (try changing to a suitable directory) 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 # 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 if loc: os.environ["LC_CTYPE"]=loc
def getName(f): def getName(f):
f = S(f)
o=open(f) o=open(f)
line="" line=""
for t in range(10): for t in range(10):
line=o.readline() line=getBuf(o).readline()
if "name" in line: return line.split()[1] if u"name".encode('latin1') in line: return S(line.split()[1])
return f[f.rindex("/")+1:] # assumes it'll be a full pathname return f[f.rindex("/")+1:] # assumes it'll be a full pathname
def isDirectory(directory): def isDirectory(directory):
oldDir = os.getcwd() oldDir = os.getcwd()
...@@ -99,10 +118,21 @@ def isDirectory(directory): ...@@ -99,10 +118,21 @@ def isDirectory(directory):
os.chdir(oldDir) os.chdir(oldDir)
return ret 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",""): if t and f.getfirst("qx",""):
sys.stdout.write("Content-Type: text/plain; charset=utf-8\n\n") sys.stdout.write("Content-Type: text/plain; charset=utf-8\n\n")
sys.stdout.flush() # help mathopd 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: elif t:
prog_with_params = prog+" -v "+lang+"+"+variant+" -s "+str(speed)+" -m" prog_with_params = prog+" -v "+lang+"+"+variant+" -s "+str(speed)+" -m"
# TODO -p 0-99 default 50 (pitch adjustment) # TODO -p 0-99 default 50 (pitch adjustment)
...@@ -111,19 +141,20 @@ elif t: ...@@ -111,19 +141,20 @@ 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 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. # 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 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) doPipe(prog_with_params+" --stdout 2>/dev/null | sox -t wav - -t au - 2>/dev/null",t)
else: else: # not streaming
os.popen(prog_with_params+" -w "+fname2+" 2>/dev/null","wb").write(t) 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("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: 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) 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 = commands.getoutput(prog+" --help|head -3").strip() 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:])) 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") 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) 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>") 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) ld=os.listdir(voiceDir)
directories = {} directories = {}
for f in ld[:]: for f in ld[:]:
...@@ -135,22 +166,22 @@ else: ...@@ -135,22 +166,22 @@ else:
directories[f2]=f directories[f2]=f
ld.sort() ld.sort()
for f in ld: for f in ld:
sys.stdout.write("<OPTION VALUE="+f) sys.stdout.write('<option value="'+f+'"')
if f==lang: sys.stdout.write(" SELECTED") if f==lang: sys.stdout.write(" selected")
if f in directories: name=getName(voiceDir+"/"+directories[f]+"/"+f) if f in directories: name=getName(voiceDir+"/"+directories[f]+"/"+f)
else: name=getName(voiceDir+"/"+f) else: name=getName(voiceDir+"/"+f)
sys.stdout.write(">"+f+" ("+name+")</OPTION>") sys.stdout.write(">"+f+" ("+name+")</option>")
sys.stdout.write("</SELECT> Voice: <SELECT NAME=v>") sys.stdout.write('</select> Voice: <select name="v">')
for v in variants: for v in variants:
if v=="default": name="default" if v=="default": name="default"
else: name=getName(voiceDir+"/!v/"+v) else: name=getName(voiceDir+"/!v/"+v)
sys.stdout.write("<OPTION VALUE="+v) sys.stdout.write('<option value="'+v+'"')
if v==variant: sys.stdout.write(" SELECTED") if v==variant: sys.stdout.write(" selected")
sys.stdout.write(">"+name+"</OPTION>") sys.stdout.write(">"+name+"</option>")
sys.stdout.write("</SELECT> Speed: <SELECT NAME=s>") 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)) sys.stdout.write('<option value="'+str(ss)+'"')
if ss==speed: sys.stdout.write(" SELECTED") if ss==speed: sys.stdout.write(" selected")
sys.stdout.write(">"+str(ss)+"</OPTION>") sys.stdout.write(">"+str(ss)+"</option>")
sys.stdout.write("</SELECT> <INPUT TYPE=submit NAME=qx VALUE=\"View phonemes\"><center><big><INPUT TYPE=submit VALUE=SPEAK></big></center></FORM></BODY></HTML>") sys.stdout.write('</select> <input type="submit" name="qx" value="View phonemes"><center><big><input type="submit" value="SPEAK"></big></center></form></body></html>')
if fname: os.system("rm -rf \""+fname+"\"") # clean up temp dir if fname: os.system("rm -rf \""+fname+"\"") # clean up temp dir
#!/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.
# If your Python is 3.13 or above (expected in Ubuntu 2.26 LTS)
# then you will need: pip install legacy-cgi
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 # if this fails, see note above
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: # safety-check-progressfile.py:
# The purpose of this script is to check # The purpose of this script is to check
# progress.txt for safety. Because it's # progress.txt for safety. Because it's
......
#!/bin/bash #!/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. # Works as an "indexing" CGI.
# To set up in Apache, make .htaccess with: # To set up in Apache, make .htaccess with:
...@@ -28,7 +28,11 @@ if ! test "a$REQUEST_URI" == a; then ...@@ -28,7 +28,11 @@ if ! test "a$REQUEST_URI" == a; then
cd "/home/ssb22/public_html/$(echo "$REQUEST_URI"|sed -e 's/?.*//')" 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 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 if test "$QUERY_STRING" == zip || test "a$(echo "$REQUEST_URI"|sed -e 's/.*?//')" == azip; then
echo Content-type: application/zip echo Content-type: application/zip
...@@ -66,8 +70,8 @@ function h5a(link) { ...@@ -66,8 +70,8 @@ function h5a(link) {
//--></script> //--></script>
EOF EOF
for N in *; do for N in *; do
export Size=$(du -h --apparent-size -s "$N"|cut -f1) 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 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>" echo "<LI><A HREF=\"$N\" onClick=\"javascript:return h5a(this)\">$N</A> $Size</LI>"
done done
echo "</UL></BODY></HTML>" echo "</UL></BODY></HTML>"
......
...@@ -13,34 +13,35 @@ ...@@ -13,34 +13,35 @@
# be used. E.g.: export ESPEAK_CGI_URL="/~userID/espeak.cgi" # be used. E.g.: export ESPEAK_CGI_URL="/~userID/espeak.cgi"
# (TODO: this script ignores the possibility of synthesizing phrases from partials) # (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 * from gradint import *
if not synthCache: synthCache_contents = [] if not synthCache: synthCache_contents = []
langs=[secondLanguage,firstLanguage] langs=[secondLanguage,firstLanguage]
o=open(vocabFile,"rU") o=open(vocabFile,"rU")
justHadP=1 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?) print ('<html><HEAD><META HTTP-EQUIV=Content-type CONTENT="text/html; charset=utf-8"><meta name="viewport" content="width=device-width"></HEAD><body>') # (assume utf8 in case there's any hanzi, but TODO what if using another charset for another language?)
for l in o.readlines(): for l in o:
l2=l.lower() l2=l.lower()
if l2.startswith("set language ") or l2.startswith("set languages "): langs=l.split()[2:] if l2.startswith("set language ") or l2.startswith("set languages "): langs=l.split()[2:]
if not l.strip(): if not l.strip():
# blank line # blank line
if not justHadP: sys.stdout.write("<P>") if not justHadP: print ("<P>")
justHadP=1 ; continue 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"): 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("#"): elif l2.startswith("#"):
# comment (and may be part of multi-line comment) # comment (and may be part of multi-line comment)
if not l[1:].strip().startswith("<!--"): sys.stdout.write("<small>#</small> ") if not l[1:].strip().startswith("<!--"): print ("<small>#</small> ")
sys.stdout.write(l[1:]) print (l[1:])
else: else:
# vocab line # vocab line
langsAndWords=zip(langs,map(lambda x:x.strip(),l.split("="))) langsAndWords=zip(langs,map(lambda x:x.strip(),l.split("=")))
out = [] out = []
for lang,word in langsAndWords: 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 found = 0
for fn2 in [fname,fname.replace(dotwav,dotmp3)]: for fn2 in [fname,fname.replace(dotwav,dotmp3)]:
if fn2 in synthCache_contents: if fn2 in synthCache_contents:
...@@ -48,8 +49,10 @@ for l in o.readlines(): ...@@ -48,8 +49,10 @@ for l in o.readlines():
found = 1 ; break found = 1 ; break
if not found: if not found:
if os.getenv("ESPEAK_CGI_URL"): if os.getenv("ESPEAK_CGI_URL"):
import urllib try: from urllib import urlencode # Python 2
out.append("<A HREF=\""+os.getenv("ESPEAK_CGI_URL")+"?"+urllib.urlencode({"t":word,"l":lang})+"\">"+word+"</A>") 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) else: out.append(word)
sys.stdout.write(" = ".join(out)) print (" = ".join(out))
justHadP=0 justHadP=0
print ("</body></html>")
File moved
../gradint.py:
make -C .. gradint.py
# This file is part of the source code of # This file is part of the source code of Gradint
# gradint v0.9974 (c) 2002-2011 Silas S. Brown. GPL v3+. # (c) Silas S. Brown.
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or # the Free Software Foundation; either version 3 of the License, or
...@@ -23,17 +23,14 @@ ...@@ -23,17 +23,14 @@
def initialGlue(): return Glue(0,maxLenOfLesson) def initialGlue(): return Glue(0,maxLenOfLesson)
try: import bisect try: from bisect import insort
except: except:
class bisect: pass
bisect=bisect()
def insort(l,item): def insort(l,item):
l.append(item) ; l.sort() l.append(item) ; l.sort()
bisect.insort = insort
class Schedule(object): class Schedule(object):
# A sorted list of (start,finish) times that are booked # A sorted list of (start,finish) times that are booked
def __init__(self): self.bookedList = [] 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 earliestAllowedEvent = 0 # for "don't start before" hacks, so can keep all initial glue starting at 0
...@@ -123,10 +120,7 @@ class Glue (GlueOrEvent): ...@@ -123,10 +120,7 @@ class Glue (GlueOrEvent):
def __init__(self,length,plusMinus): def __init__(self,length,plusMinus):
GlueOrEvent.__init__(self,length,plusMinus,1) GlueOrEvent.__init__(self,length,plusMinus,1)
def sgn(a): def sgn(a): return [1,-1][a<0]
# Not all versions of Python have this built-in
if a: return a/abs(a)
else: return 1
class StretchedTooFar(Exception): pass class StretchedTooFar(Exception): pass
class GluedEvent(object): class GluedEvent(object):
...@@ -244,9 +238,9 @@ class Lesson(object): ...@@ -244,9 +238,9 @@ class Lesson(object):
else: else:
# less confusing message for a beginner # 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")) 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) bookIn(gluedEventList,self.schedule)
if not gluedListTracker==None: gluedListTracker.append(gluedEventList) if not gluedListTracker==None and canTrack: gluedListTracker.append(gluedEventList)
glueStart = 0 ; lastI = None glueStart = 0 ; lastI = None
for i in gluedEventList: for i in gluedEventList:
i.event.setOnLeaves("sequenceID",self.eventListCounter) # for max_lateness stuff i.event.setOnLeaves("sequenceID",self.eventListCounter) # for max_lateness stuff
...@@ -268,7 +262,7 @@ class Lesson(object): ...@@ -268,7 +262,7 @@ class Lesson(object):
latenessCap = {} ; nextStart = 0 latenessCap = {} ; nextStart = 0
for t,event in self.events: for t,event in self.events:
if nextStart: 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 nextStart = t
if not hasattr(event,"importance"): continue # (wasn't added via addSequence, probably not a normal lesson) 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)) event.max_lateness=min(event.max_lateness,latenessCap.get(event.importance,maxLenOfLesson))
...@@ -294,13 +288,16 @@ class Lesson(object): ...@@ -294,13 +288,16 @@ class Lesson(object):
global sequenceIDs_to_cancel ; sequenceIDs_to_cancel = {} global sequenceIDs_to_cancel ; sequenceIDs_to_cancel = {}
global copy_of_runner_events ; copy_of_runner_events = [] global copy_of_runner_events ; copy_of_runner_events = []
global lessonStartTime ; lessonStartTime = 0 # will be set to time.time() on 1st event global lessonStartTime ; lessonStartTime = 0 # will be set to time.time() on 1st event
# make the runner as late as possible disable_lid(0)
if soundCollector: runner = sched.scheduler(collector_time,collector_sleep) try:
else: runner = sched.scheduler(time.time,mysleep) # make the runner as late as possible
for (t,event) in self.events: copy_of_runner_events.append((event,runner.enter(t,1,play,(event,)),t)) if soundCollector: runner = sched.scheduler(collector_time,collector_sleep)
# TODO what if Brief Interrupt appears during that events loop and someone presses it (will act as a Cancel and go back to main) else: runner = sched.scheduler(time.time,mysleep)
try: runner.run() for (t,event) in self.events: copy_of_runner_events.append((event,runner.enter(t,1,play,(event,)),t))
except KeyboardInterrupt: handleInterrupt() # 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 runner = None
if soundCollector: soundCollector.finished() if soundCollector: soundCollector.finished()
if logFileHandle: logFileHandle.close() if logFileHandle: logFileHandle.close()
...@@ -312,17 +309,17 @@ def decide_subst_synth(cache_fname): ...@@ -312,17 +309,17 @@ def decide_subst_synth(cache_fname):
def subst_some_synth_for_synthcache(events): def subst_some_synth_for_synthcache(events):
# turn SOME synthcache events back into synth events (for testing new synths etc) # turn SOME synthcache events back into synth events (for testing new synths etc)
reverse_transTbl = {} 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)): for i in range(len(events)):
if hasattr(events[i][1],"file") and events[i][1].file.startswith(synthCache+os.sep): 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) 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([]): if type(synthCache_test_mode)==type([]):
found=0 found=0
for str in synthCache_test_mode: 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 found=1 ; break
if found: continue if found: continue
lang = languageof(cache_fname) 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 # This file is part of the source code of Gradint
# gradint v0.9974 (c) 2002-2011 Silas S. Brown. GPL v3+. # (c) Silas S. Brown.
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or # the Free Software Foundation; either version 3 of the License, or
...@@ -17,7 +17,7 @@ def init_scanSamples(): ...@@ -17,7 +17,7 @@ def init_scanSamples():
dirsWithIntros = [] dirsWithIntros = []
filesWithExplanations = {} filesWithExplanations = {}
singleLinePoems = {} # keys are any poem files which are single line only, so as to avoid saying 'beginning' in prompts 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 init_scanSamples() ; emptyCheck_hack = 0
def scanSamples(directory=None): def scanSamples(directory=None):
if not directory: directory=samplesDirectory if not directory: directory=samplesDirectory
...@@ -59,7 +59,7 @@ def import_recordings(destDir=None): ...@@ -59,7 +59,7 @@ def import_recordings(destDir=None):
if checkFirst: if checkFirst:
for lang in [firstLanguage,secondLanguage]: for lang in [firstLanguage,secondLanguage]:
for ext in [dotwav,dotmp3]: 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 continue
if not destDir: if not destDir:
if not getYN("Import the recordings that are in "+importDir+"?"): break if not getYN("Import the recordings that are in "+importDir+"?"): break
...@@ -74,7 +74,7 @@ def import_recordings(destDir=None): ...@@ -74,7 +74,7 @@ def import_recordings(destDir=None):
try: try:
import shutil import shutil
shutil.copy2(importDir+os.sep+f,destDir+os.sep+f) 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) os.remove(importDir+os.sep+f)
numFound += 1 numFound += 1
if numFound: open(destDir+os.sep+"settings"+dottxt,"w").write("firstLanguage=\""+firstLanguage+"\"\nsecondLanguage=\""+secondLanguage+"\"\n") if numFound: open(destDir+os.sep+"settings"+dottxt,"w").write("firstLanguage=\""+firstLanguage+"\"\nsecondLanguage=\""+secondLanguage+"\"\n")
...@@ -83,7 +83,7 @@ def import_recordings(destDir=None): ...@@ -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) 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 # 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} d={"firstLanguage":firstLanguage,"secondLanguage":secondLanguage}
exec x in d exec (x,d)
return d["secondLanguage"],d["firstLanguage"] return d["secondLanguage"],d["firstLanguage"]
def check_has_variants(directory,ls): def check_has_variants(directory,ls):
if directory==promptsDirectory: return True if directory==promptsDirectory: return True
...@@ -101,9 +101,9 @@ def getLsDic(directory): ...@@ -101,9 +101,9 @@ def getLsDic(directory):
if not (directory.find(exclude_from_scan)==-1): return {} if not (directory.find(exclude_from_scan)==-1): return {}
try: ls = os.listdir(directory) try: ls = os.listdir(directory)
except: return {} # (can run without a 'samples' directory at all if just doing synth) 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) # 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 if oddLanguage==evenLanguage: oddLanguage,evenLanguage="_"+oddLanguage,"-meaning_"+evenLanguage # if user sets languages the same, assume they want -meaning prompts
else: oddLanguage,evenLanguage="_"+oddLanguage,"_"+evenLanguage else: oddLanguage,evenLanguage="_"+oddLanguage,"_"+evenLanguage
for f in ls: for f in ls:
...@@ -115,15 +115,15 @@ def getLsDic(directory): ...@@ -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.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) os.remove(directory+os.sep+"settings"+dottxt)
ls = os.listdir(directory) 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) has_variants = check_has_variants(directory,ls)
for file in ls: for file in ls:
filelower = file.lower() 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 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)>-1: languageOverride=file[file.find("_")+1:file.find("_",file.find("_")+1)] 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 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("_")>=0 and can_be_synthesized(file,directory,languageOverride)) or filelower.endswith(dotwav) or filelower.endswith(dotmp3): val = file
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
else: else:
val = "" 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 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): ...@@ -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) 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') # (+ 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 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) 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: if has_variants:
ls=list2set(ls) ; newVs = [] ls=list2set(ls)
for k,v in lsDic.items(): newVs = {} # variantFiles keys we added or changed
for k,v in list(lsDic.items()):
# check for _lang_variant.ext and take out the _variant, # check for _lang_variant.ext and take out the _variant,
# but keep them in variantFiles dict for fileToEvent to put back # 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("_") last_ = v.rfind("_")
if last_==-1: continue if last_==-1: continue
penult_ = v.rfind("_",0,last_) penult_ = v.rfind("_",0,last_)
if penult_==-1: continue if penult_==-1: continue
del lsDic[k] # Now k = filename without extension but including a variant number, and v = full filename
newK,newV = k[:k.rfind("_")], v[:v.rfind("_")]+v[v.rfind(extsep):] del lsDic[k] # we don't want variant numbers in lsDic, we want them in variantFiles instead
if not newK in lsDic: lsDic[newK] = newV 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)
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) new_dirV = B(directory)+B(os.sep)+B(newV)
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. if not checkIn(newK,lsDic): # filename without variant number doesn't exist (for any extension)
old_dirV = directory+os.sep+lsDic[newK] 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] d = variantFiles[old_dirV]
del variantFiles[old_dirV] del variantFiles[old_dirV]
lsDic[newK] = newV variantFiles[new_dirV] = d
variantFiles[directory+os.sep+newV] = 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
lsDic[newK] = newV # just add to the previous key variantFiles[new_dirV].append(v)
else: newV = lsDic[newK] if checkIn(old_dirV,newVs):
dir_newV = directory+os.sep+newV del newVs[old_dirV]
if not dir_newV in variantFiles: newVs[new_dirV] = 1
variantFiles[dir_newV] = [] lsDic[newK] = newV
if newV in ls: variantFiles[dir_newV].append(newV) # the no-variants name is also a valid option else: # filename without variant number does exist (or we made the key), and we need to add new variant
variantFiles[dir_newV].append(v) newV = lsDic[newK]
newVs.append(dir_newV) new_dirV = B(directory)+B(os.sep)+B(newV)
for v in newVs: random.shuffle(variantFiles[v]) 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 return lsDic
def scanSamples_inner(directory,retVal,doLimit): def scanSamples_inner(directory,retVal,doLimit):
...@@ -169,14 +181,14 @@ def scanSamples_inner(directory,retVal,doLimit): ...@@ -169,14 +181,14 @@ def scanSamples_inner(directory,retVal,doLimit):
secLangSuffix = "_"+secondLanguage secLangSuffix = "_"+secondLanguage
lsDic = getLsDic(directory) lsDic = getLsDic(directory)
intro = intro_filename+"_"+firstLanguage intro = intro_filename+"_"+firstLanguage
if intro in lsDic: dirsWithIntros.append((directory[len(samplesDirectory)+len(os.sep):],lsDic[intro])) if checkIn(intro,lsDic): dirsWithIntros.append((directory[len(samplesDirectory)+len(os.sep):],lsDic[intro]))
if not doLimit: doLimit = limit_filename in lsDic if not doLimit: doLimit = checkIn(limit_filename,lsDic)
doPoetry = poetry_filename in lsDic doPoetry = checkIn(poetry_filename,lsDic)
if doPoetry: if doPoetry:
# check which language the poetry is to be in (could be L1-to-L2, L2-to-L3, L2-only, or L3-only) # 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 = "" ret = ""
for file,withExt in lsDic.items(): for file,withExt in list(lsDic.items()):
if withExt: if withExt:
if file.endswith(secLangSuffix): ret=secLangSuffix # but stay in the loop if file.endswith(secLangSuffix): ret=secLangSuffix # but stay in the loop
elif (not file.endswith(firstLangSuffix)): elif (not file.endswith(firstLangSuffix)):
...@@ -184,11 +196,11 @@ def scanSamples_inner(directory,retVal,doLimit): ...@@ -184,11 +196,11 @@ def scanSamples_inner(directory,retVal,doLimit):
for l in otherLanguages: for l in otherLanguages:
if not l in llist and file.endswith("_"+l): return "_"+l if not l in llist and file.endswith("_"+l): return "_"+l
return ret 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 prefix = directory[len(samplesDirectory)+cond(samplesDirectory,len(os.sep),0):] # the directory relative to samplesDirectory
if prefix: prefix += os.sep if prefix: prefix += os.sep
lastFile = None # for doPoetry lastFile = None # for doPoetry
items = lsDic.items() ; items.sort() items = list(lsDic.items()) ; items.sort()
for file,withExt in items: for file,withExt in items:
swapWithPrompt = 0 swapWithPrompt = 0
if not withExt: if not withExt:
...@@ -204,7 +216,7 @@ def scanSamples_inner(directory,retVal,doLimit): ...@@ -204,7 +216,7 @@ def scanSamples_inner(directory,retVal,doLimit):
wordSuffix=None wordSuffix=None
for l in otherLanguages: for l in otherLanguages:
if not l in [firstLanguage,secondLanguage] and file.endswith("_"+l): 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 wordSuffix="_"+l ; break
if not wordSuffix: continue # can't do anything with this file if not wordSuffix: continue # can't do anything with this file
if swapWithPrompt or firstLanguage==secondLanguage: promptFile=None if swapWithPrompt or firstLanguage==secondLanguage: promptFile=None
...@@ -227,7 +239,7 @@ def scanSamples_inner(directory,retVal,doLimit): ...@@ -227,7 +239,7 @@ def scanSamples_inner(directory,retVal,doLimit):
# poetry without first-language prompts # poetry without first-language prompts
if lastFile: if lastFile:
promptToAdd = prefix+lastFile[-1] promptToAdd = prefix+lastFile[-1]
if promptToAdd in singleLinePoems: del singleLinePoems[promptToAdd] if checkIn(promptToAdd,singleLinePoems): del singleLinePoems[promptToAdd]
else: else:
promptToAdd = prefix+withExt # 1st line is its own prompt promptToAdd = prefix+withExt # 1st line is its own prompt
singleLinePoems[promptToAdd]=1 singleLinePoems[promptToAdd]=1
...@@ -236,7 +248,7 @@ def scanSamples_inner(directory,retVal,doLimit): ...@@ -236,7 +248,7 @@ def scanSamples_inner(directory,retVal,doLimit):
retVal.append((0,promptToAdd,prefix+withExt)) retVal.append((0,promptToAdd,prefix+withExt))
if emptyCheck_hack: return if emptyCheck_hack: return
if explanationFile: filesWithExplanations[prefix+withExt]=explanationFile if explanationFile: filesWithExplanations[prefix+withExt]=explanationFile
if doLimit: limitedFiles[prefix+withExt]=prefix if doLimit: limitedFiles[B(prefix+withExt)]=prefix
lastFile = [promptFile,withExt] 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 cache_maintenance_mode=0 # hack so cache-synth.py etc can cache promptless words for use in justSynthesize, and words in prompts themselves
...@@ -249,62 +261,62 @@ def parseSynthVocab(fname,forGUI=0): ...@@ -249,62 +261,62 @@ def parseSynthVocab(fname,forGUI=0):
if not fileExists(fname): return [] if not fileExists(fname): return []
if not emptyCheck_hack: doLabel("Reading "+fname) if not emptyCheck_hack: doLabel("Reading "+fname)
allLangs = list2set([firstLanguage,secondLanguage]+otherLanguages) 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) # 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 if not l: continue
canProcess = 0 ; l2=l.strip(wsp) canProcess = 0 ; l2=bwspstrip(l)
if not l2 or l2[0]=='#': continue if not l2 or l2[0:1]==B('#'): continue
l2=l2.lower() l2=l2.lower()
if l2.startswith("set language ") or l2.startswith("set languages "): if l2.startswith(B("set language ")) or l2.startswith(B("set languages ")):
langs=l.split()[2:] ; someLangsUnknown = 0 langs=map(S,l.split()[2:]) ; someLangsUnknown = 0
maxsplit = len(langs)-1 maxsplit = len(langs)-1
for l in langs: for l in langs:
if not l in allLangs: someLangsUnknown = 1 if not checkIn(l,allLangs): someLangsUnknown = 1
elif l2.startswith("limit on"): elif l2.startswith(B("limit on")):
doLimit = 1 ; limitNo += 1 doLimit = 1 ; limitNo += 1
elif l2.startswith("limit off"): doLimit = 0 elif l2.startswith(B("limit off")): doLimit = 0
elif l2.startswith("begin poetry"): doPoetry,lastPromptAndWord,disablePoem = True,None,False elif l2.startswith(B("begin poetry")): doPoetry,lastPromptAndWord,disablePoem = True,None,False
elif l2.startswith("end poetry"): doPoetry = lastPromptAndWord = None elif l2.startswith(B("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("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 else: canProcess=1
if not canProcess: continue 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="" if forGUI: strCount=""
else: else:
strCount = "%05d!synth:" % (count,) strCount = "%05d!synth:" % (count,)
count += 1 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) # (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 # 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 = {} if firstLanguage==secondLanguage: langsAlreadySeen = {}
else: langsAlreadySeen = {firstLanguage:True} else: langsAlreadySeen = {firstLanguage:True}
def findPrompt(): def findPrompt(langsAndWords,langsAlreadySeen,doPoetry,strCount):
i=0 i=0
while i<len(langsAndWords): while i<len(langsAndWords):
lang,word = langsAndWords[i] ; i += 1 lang,word = langsAndWords[i] ; i += 1
isReminder = cache_maintenance_mode and len(langsAndWords)==1 and not doPoetry 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 if not word: continue
elif word[0] in wsp or word[-1] in wsp: word=word.strip(wsp) # avoid call if unnecessary elif word[0:1] in bwsp or word[-1:] in bwsp: word=bwspstrip(word) # avoid call if unnecessary
return strCount+word+"_"+lang, cond(isReminder,0,i) return B(strCount)+word+B("_"+lang), cond(isReminder,0,i)
langsAlreadySeen[lang]=True langsAlreadySeen[lang]=True
return None,0 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 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: 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) prompt_L1only = prompt # before we possibly change it into a list etc. (Actually not necessarily L1 see above, but usually is)
if doPoetry: if doPoetry:
if prompt and lastPromptAndWord: if prompt and lastPromptAndWord:
if lastPromptAndWord[0]: prompt=[lastPromptAndWord[0],prompt,lastPromptAndWord[1]] # L1 for line 1, L1 for line2, L2 for line 1 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=[lastPromptAndWord[1],prompt] # line 1 doesn't have L1 but line 2 does, so have L2 for line 1 + L1 for line 2 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: elif not prompt:
if lastPromptAndWord: if lastPromptAndWord:
prompt=lastPromptAndWord[-1] prompt=lastPromptAndWord[-1]
if lastPromptAndWord[-1] in singleLinePoems: del singleLinePoems[lastPromptAndWord[-1]] if checkIn(lastPromptAndWord[-1],singleLinePoems): del singleLinePoems[lastPromptAndWord[-1]]
else: else:
prompt = 1 # file itself (see below) prompt = 1 # file itself (see below)
if prompt: if prompt:
...@@ -312,53 +324,55 @@ def parseSynthVocab(fname,forGUI=0): ...@@ -312,53 +324,55 @@ def parseSynthVocab(fname,forGUI=0):
while i<len(langsAndWords): while i<len(langsAndWords):
lang,word = langsAndWords[i] ; i+=1 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 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 elif word[0:1] in bwsp or word[-1:] in bwsp: word=bwspstrip(word) # avoid call if unnecessary
if lang in getsynth_cache or can_be_synthesized("!synth:"+word+"_"+lang): if checkIn(lang,getsynth_cache) or can_be_synthesized(B("!synth:")+word+B("_"+lang)):
if not (doPoetry and disablePoem): if not (doPoetry and disablePoem):
f=strCount+word+"_"+lang f=B(strCount)+word+B("_"+lang)
if prompt==1 or prompt==f: # a file with itself as the prompt (either explicitly or by omitting any other prompt) if prompt==1 or prompt==f: # a file with itself as the prompt (either explicitly or by omitting any other prompt)
prompt=f prompt=f
singleLinePoems[f]=1 singleLinePoems[f]=1
ret.append((0,prompt,f)) ret.append((0,S(prompt),S(f)))
if emptyCheck_hack: return ret 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] if doPoetry: lastPromptAndWord = [prompt_L1only,f]
elif doPoetry: disablePoem=1 # if one of the lines can't be synth'd, disable the rest of the poem (otherwise get wrongly connected lines, disconnected lines, or re-introduction of isolated lines that were previously part of a poem but can't be synth'd on this platform) elif doPoetry: disablePoem=1 # if one of the lines can't be synth'd, disable the rest of the poem (otherwise get wrongly connected lines, disconnected lines, or re-introduction of isolated lines that were previously part of a poem but can't be synth'd on this platform)
if not lastPromptAndWord==None: doPoetry = 1 # just processed a "poetry vocab line:" (lastPromptAndWord is either the real last prompt and word, or 0 if we were at the start) if not lastPromptAndWord==None: doPoetry = 1 # just processed a "poetry vocab line" (lastPromptAndWord is either the real last prompt and word, or 0 if we were at the start)
return ret return ret
def sanitise_otherLanguages(): def sanitise_otherLanguages():
for l in otherFirstLanguages: for l in otherFirstLanguages:
if not l in otherLanguages: otherLanguages.append(l) if not checkIn(l,otherLanguages): otherLanguages.append(l)
for l in otherLanguages: 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() sanitise_otherLanguages()
# Prompt file syntax: word_language.wav # Prompt file syntax: word_language.wav
# or: word_language_2.wav .. (alternatives chosen at random) # or: word_language_2.wav .. (alternatives chosen at random)
# ('word' can also be a language name) # ('word' can also be a language name)
class PromptException(Exception): class MessageException(Exception):
def __init__(self,message): self.message = message def __init__(self,message): self.message = message
def __repr__(self): return self.message def __repr__(self): return self.message
class PromptException(MessageException): pass
auto_advancedPrompt=0 # used by gradint.cgi
class AvailablePrompts(object): class AvailablePrompts(object):
reservedPrefixes = list2set(map(lambda x:x.lower(),["whatmean","meaningis","repeatAfterMe","sayAgain","longPause","begin","end",firstLanguage,secondLanguage] + possible_otherLanguages)) reservedPrefixes = list2set(map(lambda x:x.lower(),["whatmean","meaningis","repeatAfterMe","sayAgain","longPause","begin","end",firstLanguage,secondLanguage] + possible_otherLanguages))
def __init__(self): def __init__(self):
self.lsDic = getLsDic(promptsDirectory) self.lsDic = getLsDic(promptsDirectory)
self.prefixes = {} 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 if v: self.prefixes[k[:k.rfind("_")]]=1 # delete language
else: del self.lsDic[k] # !poetry etc doesn't make sense in prompts 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 self.user_is_advanced = None
def getRandomPromptList(self,promptsData,language): def getRandomPromptList(self,promptsData,language):
random.shuffle(self.prefixes) random.shuffle(self.prefixes)
for p in self.prefixes: for p in self.prefixes:
if p.lower() in self.reservedPrefixes: continue if checkIn(p.lower(),self.reservedPrefixes): continue
try: try:
theList = self.getPromptList(p,promptsData,language) theList = self.getPromptList(p,promptsData,language)
return theList return theList
except PromptException: pass 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): def getPromptList(self,prefix,promptsData,language):
# used for introducing foreign-language prompts to # used for introducing foreign-language prompts to
# beginners. language is the suffix of the language we're *learning*. # beginners. language is the suffix of the language we're *learning*.
...@@ -368,11 +382,11 @@ class AvailablePrompts(object): ...@@ -368,11 +382,11 @@ class AvailablePrompts(object):
if p > advancedPromptThreshold2: if p > advancedPromptThreshold2:
self.user_is_advanced = 1 ; break # got a reasonably advanced user self.user_is_advanced = 1 ; break # got a reasonably advanced user
beginnerPrompt = prefix+"_"+firstLanguage 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 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 else: beginnerPrompt = None
advancedPrompt = prefix+"_"+language advancedPrompt = prefix+"_"+language
if not advancedPrompt in self.lsDic: if not checkIn(advancedPrompt,self.lsDic):
# Must use beginnerPrompt # Must use beginnerPrompt
if beginnerPrompt: r=[self.lsDic[beginnerPrompt]] if beginnerPrompt: r=[self.lsDic[beginnerPrompt]]
else: else:
...@@ -380,7 +394,7 @@ class AvailablePrompts(object): ...@@ -380,7 +394,7 @@ class AvailablePrompts(object):
else: raise PromptException("Can't find "+prefix+"_"+language+", "+prefix+"_"+firstLanguage+" or "+prefix+"_"+secondLanguage) else: raise PromptException("Can't find "+prefix+"_"+language+", "+prefix+"_"+firstLanguage+" or "+prefix+"_"+secondLanguage)
elif not beginnerPrompt: elif not beginnerPrompt:
# Must use advancedPrompt # 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]] 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,advancedPromptThreshold,advancedPromptThreshold2): r=[self.lsDic[advancedPrompt]]
elif promptsData.get(advancedPrompt,0) >= cond(language==secondLanguage,transitionPromptThreshold,transitionPromptThreshold2): r=[self.lsDic[advancedPrompt], self.lsDic[beginnerPrompt]] elif promptsData.get(advancedPrompt,0) >= cond(language==secondLanguage,transitionPromptThreshold,transitionPromptThreshold2): r=[self.lsDic[advancedPrompt], self.lsDic[beginnerPrompt]]
...@@ -393,7 +407,7 @@ class AvailablePrompts(object): ...@@ -393,7 +407,7 @@ class AvailablePrompts(object):
# Increment advancedPrompt, taking care not to go # Increment advancedPrompt, taking care not to go
# past the threshold if it's not available yet # past the threshold if it's not available yet
adv = promptsData.get(advancedPrompt,0) 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 adv += 1
promptsData[advancedPrompt] = adv promptsData[advancedPrompt] = adv
# and finally, # and finally,
...@@ -406,14 +420,14 @@ def introductions(zhFile,progressData): ...@@ -406,14 +420,14 @@ def introductions(zhFile,progressData):
for d,fname in dirsWithIntros[:]: for d,fname in dirsWithIntros[:]:
found = 0 found = 0
for p in progressData: 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 # this dir has already been introduced
found=1 ; dirsWithIntros.remove((d,fname)) ; break found=1 ; dirsWithIntros.remove((d,fname)) ; break
if found: continue 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 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): 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 # This file is part of the source code of Gradint
# gradint v0.9974 (c) 2002-2011 Silas S. Brown. GPL v3+. # (c) Silas S. Brown.
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or # the Free Software Foundation; either version 3 of the License, or
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# Start of frontend.py - Tk and other front-ends # Start of frontend.py - Tk and other front-ends
def interrupt_instructions(): 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 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 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!" elif winCEsound: return "\nLesson interruption not implemented on\nWinCE without GUI. Can't stop, sorry!"
...@@ -23,12 +23,19 @@ appTitle += time.strftime(" %A") # in case leave 2+ instances on the desktop ...@@ -23,12 +23,19 @@ appTitle += time.strftime(" %A") # in case leave 2+ instances on the desktop
def waitOnMessage(msg): def waitOnMessage(msg):
global warnings_printed global warnings_printed
if type(msg)==type(u""): msg2=msg.encode("utf-8") 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: 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) 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: 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 app.todo.alert = "".join(warnings_printed)+msg
while True: while True:
try: try:
...@@ -37,8 +44,8 @@ def waitOnMessage(msg): ...@@ -37,8 +44,8 @@ def waitOnMessage(msg):
time.sleep(0.5) time.sleep(0.5)
if not (winsound or winCEsound or mingw32 or cygwin): show_info("OK\n",True) if not (winsound or winCEsound or mingw32 or cygwin): show_info("OK\n",True)
else: 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 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+"\n\n"+cond(winCEsound,"Press OK to continue\n","Press Enter to continue\n")) 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) 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: 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) 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)
...@@ -49,7 +56,14 @@ def waitOnMessage(msg): ...@@ -49,7 +56,14 @@ def waitOnMessage(msg):
def getYN(msg,defaultIfEof="n"): def getYN(msg,defaultIfEof="n"):
if appuifw: if appuifw:
appuifw.app.body = None 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: elif app:
app.todo.question = localise(msg) app.todo.question = localise(msg)
while app and not hasattr(app,"answer_given"): time.sleep(0.5) while app and not hasattr(app,"answer_given"): time.sleep(0.5)
...@@ -63,7 +77,7 @@ def getYN(msg,defaultIfEof="n"): ...@@ -63,7 +77,7 @@ def getYN(msg,defaultIfEof="n"):
while not ans=='y' and not ans=='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,)) try: ans = raw_input("%s\nPress y for yes, or n for no. Then press Enter. --> " % (msg,))
except EOFError: except EOFError:
ans=defaultIfEof ; print ans ans=defaultIfEof ; print (ans)
clearScreen() # less confusing for beginners clearScreen() # less confusing for beginners
if ans=='y': return 1 if ans=='y': return 1
return 0 return 0
...@@ -71,52 +85,60 @@ def getYN(msg,defaultIfEof="n"): ...@@ -71,52 +85,60 @@ def getYN(msg,defaultIfEof="n"):
def primitive_synthloop(): def primitive_synthloop():
global justSynthesize,warnings_printed global justSynthesize,warnings_printed
lang = None lang = None
interactive = appuifw or winCEsound 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.) 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) else: interactive="" # no prompt on the raw_input (we might be doing outputFile="-" as well)
while True: while True:
old_js = justSynthesize old_js = justSynthesize
if appuifw: if appuifw:
if not justSynthesize: justSynthesize="" 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") if justSynthesize: justSynthesize=justSynthesize.encode("utf-8")
else: break else: break
else: else:
try: justSynthesize=raw_input(interactive) if android:
except EOFError: break justSynthesize = android.dialogGetInput("Gradint",interactive).result
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 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: if interactive and not readline:
interactive="('a' for again) Say: " interactive="('a' for again) Say: "
if justSynthesize=="a": justSynthesize=old_js if B(justSynthesize)==B("a"): justSynthesize=old_js
oldLang = lang oldLang = lang
if justSynthesize: lang = just_synthesize(interactive,lang) if justSynthesize: lang = S(just_synthesize(interactive,lang))
# and see if it transliterates: # and see if it transliterates:
if justSynthesize and lang and not "#" in justSynthesize: if justSynthesize and lang and not B('#') in B(justSynthesize):
if justSynthesize.startswith(lang+" "): if B(justSynthesize).startswith(B(lang)+B(" ")):
t = transliterates_differently(justSynthesize[len(lang+" "):],lang) t = transliterates_differently(justSynthesize[len(lang+" "):],lang)
if t: t=lang+" "+t if t: t=lang+" "+t
else: t = transliterates_differently(justSynthesize,lang) else: t = transliterates_differently(justSynthesize,lang)
if t: if t:
if appuifw: justSynthesize = 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 warnings_printed: # at end not beginning, because don't want to overwrite the info message if appuifw
if appuifw: if appuifw:
t=appuifw.Text() t=appuifw.Text()
t.add(u"".join(warnings_printed)) t.add(u"".join(warnings_printed))
appuifw.app.body = t appuifw.app.body = t
elif android: waitOnMessage("") # (makeToast doesn't stay around for very long)
# else they'll have already been printed # else they'll have already been printed
warnings_printed = [] warnings_printed = []
if not lang: lang=oldLang 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 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 if winCEsound: return None # user might be paying per byte! + difficult to switch back if no Alt-Tab program
try: import webbrowser try:
except: webbrowser=0 import webbrowser
if webbrowser:
g=webbrowser.get() 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)): except: g=0
return g.open_new(url) 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)):
# else don't risk it - it might be text-mode and unsuitable for multitask-with-gradint 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) 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) # (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)
...@@ -128,7 +150,7 @@ def clearScreen(): ...@@ -128,7 +150,7 @@ def clearScreen():
warnings_printed = [] warnings_printed = []
return return
if winsound or mingw32: os.system("cls") 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 return True
cancelledFiles = [] cancelledFiles = []
...@@ -148,17 +170,17 @@ def handleInterrupt(): # called only if there was an interrupt while the runner ...@@ -148,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 # 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 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)) 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 tkNumWordsToShow = 10 # the default number of list-box items
def addStatus(widget,status,mouseOnly=0): 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). # 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) # (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) widget.bind('<Leave>',app.restore_statusline)
if not mouseOnly: 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) widget.bind('<FocusOut>',app.restore_statusline)
def makeButton(parent,text,command): def makeButton(parent,text,command):
button = Tkinter.Button(parent) button = Tkinter.Button(parent)
...@@ -183,12 +205,17 @@ def addLabel(row,label): ...@@ -183,12 +205,17 @@ def addLabel(row,label):
def CXVMenu(e): # callback for right-click def CXVMenu(e): # callback for right-click
e.widget.focus() e.widget.focus()
m=Tkinter.Menu(None, tearoff=0, takefocus=0) m=Tkinter.Menu(None, tearoff=0, takefocus=0)
ctrl=cond(macsound,"<Command-","<Control-") if macsound:
m.add_command(label="Cut",command=(lambda e=e: e.widget.event_generate(ctrl+'x>'))) cut,copy,paste = "<<Cut>>","<<Copy>>","<<Paste>>"
m.add_command(label="Copy",command=(lambda e=e: e.widget.event_generate(ctrl+'-c>'))) else:
m.add_command(label="Paste",command=(lambda e=e: e.widget.event_generate(ctrl+'-v>'))) ctrl="<Control-"
m.add_command(label="Delete",command=(lambda e=e: e.widget.event_generate('<Delete>'))) cut,copy,paste = ctrl+'x>',ctrl+'c>',ctrl+'v>'
m.add_command(label="Select All",command=(lambda e=e: selectAll(e))) 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") m.tk_popup(e.x_root-3, e.y_root+3,entry="0")
def selectAll(e): def selectAll(e):
e.widget.event_generate('<Home>') e.widget.event_generate('<Home>')
...@@ -202,13 +229,15 @@ def addTextBox(row,wide=0): ...@@ -202,13 +229,15 @@ def addTextBox(row,wide=0):
text = Tkinter.StringVar(row) text = Tkinter.StringVar(row)
entry = Tkinter.Entry(row,textvariable=text) entry = Tkinter.Entry(row,textvariable=text)
entry.bind('<ButtonRelease-3>',CXVMenu) entry.bind('<ButtonRelease-3>',CXVMenu)
if macsound: entry.bind('<Control-ButtonRelease-1>',CXVMenu) if macsound:
entry.bind('<Control-ButtonRelease-1>',CXVMenu)
entry.bind('<ButtonRelease-2>',CXVMenu)
if winCEsound: if winCEsound:
if WMstandard: # non-numeric inputs no good on WMstandard Tkinter if WMstandard: # non-numeric inputs no good on WMstandard Tkinter
def doRawInput(text,entry): def doRawInput(text,entry):
app.input_to_set = text app.input_to_set = text
app.menu_response="input" 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 if wide: # put help in 1st wide textbox
global had_doRawInput global had_doRawInput
try: had_doRawInput try: had_doRawInput
...@@ -217,10 +246,10 @@ def addTextBox(row,wide=0): ...@@ -217,10 +246,10 @@ def addTextBox(row,wide=0):
text.set("(Push OK to type A-Z)") # (if changing this message, change it below too) text.set("(Push OK to type A-Z)") # (if changing this message, change it below too)
class E: pass class E: pass
e=E() ; e.widget = entry 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. 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() 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 global lastDblclkAdvisory,lastDblclk
lastDblclkAdvisory=lastDblclk=0 lastDblclkAdvisory=lastDblclk=0
def pasteInstructions(t): def pasteInstructions(t):
...@@ -232,8 +261,8 @@ def addTextBox(row,wide=0): ...@@ -232,8 +261,8 @@ def addTextBox(row,wide=0):
def doPaste(text,entry): def doPaste(text,entry):
text.set(entry.selection_get(selection="CLIPBOARD")) text.set(entry.selection_get(selection="CLIPBOARD"))
global lastDblclk ; lastDblclk=time.time() global lastDblclk ; lastDblclk=time.time()
entry.bind('<ButtonRelease-1>',lambda e:pasteInstructions(time.time()-getattr(entry,"buttonPressTime",time.time()))) 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(text,entry)) 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: # 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)))) entry.bind(cond(macsound,'<Command-a>','<Control-a>'),(lambda e:e.widget.after(10,lambda e=e:selectAll(e))))
bindUpDown(entry,False) bindUpDown(entry,False)
...@@ -270,14 +299,14 @@ def make_output_row(parent): ...@@ -270,14 +299,14 @@ def make_output_row(parent):
# if there aren't any options then return None # if there aren't any options then return None
# we also put script-variant selection here, if any # we also put script-variant selection here, if any
row = None row = None
def getRow(row): def getRow(row,parent):
if not row: if not row:
row = Tkinter.Frame(parent) row = Tkinter.Frame(parent)
row.pack(fill=Tkinter.X,expand=1) row.pack(fill=Tkinter.X,expand=1)
return row return row
GUIlang = GUI_languages.get(firstLanguage,firstLanguage) GUIlang = GUI_languages.get(firstLanguage,firstLanguage)
if "@variants-"+GUIlang in GUI_translations: # the firstLanguage has script variants if checkIn("@variants-"+GUIlang,GUI_translations): # the firstLanguage has script variants
row=getRow(row) row=getRow(row,parent)
if not hasattr(app,"scriptVariant"): app.scriptVariant = Tkinter.StringVar(app) if not hasattr(app,"scriptVariant"): app.scriptVariant = Tkinter.StringVar(app)
count = 0 count = 0
for variant in GUI_translations["@variants-"+GUIlang]: for variant in GUI_translations["@variants-"+GUIlang]:
...@@ -285,7 +314,7 @@ def make_output_row(parent): ...@@ -285,7 +314,7 @@ def make_output_row(parent):
count += 1 count += 1
app.scriptVariant.set(str(scriptVariants.get(GUIlang,0))) app.scriptVariant.set(str(scriptVariants.get(GUIlang,0)))
if synth_partials_voices and guiVoiceOptions: if synth_partials_voices and guiVoiceOptions:
row=getRow(row) row=getRow(row,parent)
if not hasattr(app,"voiceOption"): app.voiceOption = Tkinter.StringVar(app) 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"}) 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"}) 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"})
...@@ -294,9 +323,9 @@ def make_output_row(parent): ...@@ -294,9 +323,9 @@ def make_output_row(parent):
if not hasattr(app,"outputTo"): if not hasattr(app,"outputTo"):
app.outputTo = Tkinter.StringVar(app) # NB app not parent (as parent is no longer app) 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 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 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() ftu = fileType.upper()
t = Tkinter.Radiobutton(rightrow, text=cond(forceRadio,""," ")+ftu+" ", variable=app.outputTo, value=fileType, indicatoron=forceRadio) t = Tkinter.Radiobutton(rightrow, text=cond(forceRadio,""," ")+ftu+" ", variable=app.outputTo, value=fileType, indicatoron=forceRadio)
bindUpDown(t,True) bindUpDown(t,True)
...@@ -310,13 +339,13 @@ def make_output_row(parent): ...@@ -310,13 +339,13 @@ def make_output_row(parent):
addStatus(t,"Select this to send all sounds to\nthe speaker, not to files on disk") addStatus(t,"Select this to send all sounds to\nthe speaker, not to files on disk")
bindUpDown(t,True) bindUpDown(t,True)
t.pack({"side":"left"}) t.pack({"side":"left"})
if got_program("lame"): addFiletypeButton("mp3") if got_program("lame"): addFiletypeButton("mp3",rightrow)
if got_windows_encoder: addFiletypeButton("wma") if got_windows_encoder: addFiletypeButton("wma",rightrow)
if got_program("faac") or got_program("afconvert"): addFiletypeButton("aac") if got_program("neroAacEnc") or got_program("faac") or got_program("afconvert"): addFiletypeButton("aac",rightrow)
if got_program("oggenc"): addFiletypeButton("ogg") if got_program("oggenc") or got_program("oggenc2"): addFiletypeButton("ogg",rightrow)
if got_program("toolame"): addFiletypeButton("mp2") if got_program("toolame"): addFiletypeButton("mp2",rightrow)
if got_program("speexenc"): addFiletypeButton("spx") if got_program("speexenc"): addFiletypeButton("spx",rightrow)
addFiletypeButton("wav") addFiletypeButton("wav",rightrow)
# "Get MP3 encoder" and "Get WMA encoder" changed to "MP3..." and "WMA..." to save width (+ no localisation necessary) # "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) 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") # (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")
...@@ -325,19 +354,19 @@ def make_output_row(parent): ...@@ -325,19 +354,19 @@ def make_output_row(parent):
def updateSettingsFile(fname,newVals): def updateSettingsFile(fname,newVals):
# leaves comments etc intact, but TODO does not cope with changing variables that have been split over multiple lines # leaves comments etc intact, but TODO does not cope with changing variables that have been split over multiple lines
replacement_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=[] except IOError: oldLines=[]
for l in oldLines: for l in oldLines:
found=0 found=0
for k in newVals.keys(): for k in list(newVals.keys()):
if l.startswith(k): if l.startswith(B(k)):
replacement_lines.append(k+"="+repr(newVals[k])) replacement_lines.append(B(k+"="+repr(newVals[k])))
del newVals[k] del newVals[k]
found=1 found=1
if not found: replacement_lines.append(l) if not found: replacement_lines.append(l)
for k,v in newVals.items(): replacement_lines.append(k+"="+repr(v)) for k,v in list(newVals.items()): replacement_lines.append(B(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 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
open(fname,"w").write("\n".join(replacement_lines)) writeB(open(fname,"w"),B("\n").join(replacement_lines))
def asUnicode(x): # for handling the return value of Tkinter entry.get() def asUnicode(x): # for handling the return value of Tkinter entry.get()
try: return u""+x # original behaviour try: return u""+x # original behaviour
...@@ -346,16 +375,27 @@ def asUnicode(x): # for handling the return value of Tkinter entry.get() ...@@ -346,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) 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): def setupScrollbar(parent,rowNo):
onLeft = winCEsound or olpc
s = Tkinter.Scrollbar(parent,takefocus=0) 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=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) s.config(command=c.yview)
scrolledFrame=Tkinter.Frame(c) ; c.create_window(0,0,window=scrolledFrame,anchor="nw") 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) # 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")) app.ScrollUpHandler = lambda e=None,c=c:c.yview("scroll","-1","units")
scrolledFrame.bind_all('<Button-5>',lambda *args:c.yview("scroll","1","units")) app.ScrollDownHandler = lambda e=None,c=c: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?) 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 return scrolledFrame, c
# GUI presets buttons: # GUI presets buttons:
...@@ -363,8 +403,8 @@ shortDescriptionName = "short-description"+dottxt ...@@ -363,8 +403,8 @@ shortDescriptionName = "short-description"+dottxt
longDescriptionName = "long-description"+dottxt longDescriptionName = "long-description"+dottxt
class ExtraButton(object): class ExtraButton(object):
def __init__(self,directory): def __init__(self,directory):
self.shortDescription = u8strip(read(directory+os.sep+shortDescriptionName)).strip(wsp) self.shortDescription = wspstrip(u8strip(read(directory+os.sep+shortDescriptionName)))
if fileExists(directory+os.sep+longDescriptionName): self.longDescription = u8strip(read(directory+os.sep+longDescriptionName)).strip(wsp) if fileExists(directory+os.sep+longDescriptionName): self.longDescription = wspstrip(u8strip(read(directory+os.sep+longDescriptionName)))
else: self.longDescription = self.shortDescription else: self.longDescription = self.shortDescription
self.directory = directory self.directory = directory
def add(self): def add(self):
...@@ -381,7 +421,7 @@ class ExtraButton(object): ...@@ -381,7 +421,7 @@ class ExtraButton(object):
try: ls = os.listdir(samplesDirectory) try: ls = os.listdir(samplesDirectory)
except: os.mkdir(samplesDirectory) except: os.mkdir(samplesDirectory)
name1=newName name1=newName
while newName in ls: newName+="1" while checkIn(newName,ls): newName+="1"
name2=newName name2=newName
newName = samplesDirectory+os.sep+newName newName = samplesDirectory+os.sep+newName
os.rename(self.directory,newName) os.rename(self.directory,newName)
...@@ -390,13 +430,13 @@ class ExtraButton(object): ...@@ -390,13 +430,13 @@ class ExtraButton(object):
which_collection = localise(" has been added to your collection.") which_collection = localise(" has been added to your collection.")
o=open(vocabFile,"a") o=open(vocabFile,"a")
o.write("# --- BEGIN "+self.shortDescription+" ---\n") 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") o.write("# ----- END "+self.shortDescription+" ---\n")
if hasattr(app,"vocabList"): del app.vocabList # so re-reads if hasattr(app,"vocabList"): del app.vocabList # so re-reads
os.remove(newName+os.sep+"add-to-vocab"+dottxt) os.remove(newName+os.sep+"add-to-vocab"+dottxt)
if fileExists(newName+os.sep+"add-to-languages"+dottxt): if fileExists(newName+os.sep+"add-to-languages"+dottxt):
changed = 0 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: if not lang in [firstLanguage,secondLanguage]+otherLanguages:
otherLanguages.append(lang) ; changed = 1 otherLanguages.append(lang) ; changed = 1
if changed: sanitise_otherLanguages(), updateSettingsFile("advanced"+dottxt,{"otherLanguages":otherLanguages,"possible_otherLanguages":possible_otherLanguages}) if changed: sanitise_otherLanguages(), updateSettingsFile("advanced"+dottxt,{"otherLanguages":otherLanguages,"possible_otherLanguages":possible_otherLanguages})
...@@ -434,8 +474,8 @@ def focusButton(button): ...@@ -434,8 +474,8 @@ def focusButton(button):
try: button.config(state=state) try: button.config(state=state)
except: pass # maybe not a button except: pass # maybe not a button
for t in range(250,1000,250): # (NB avoid epilepsy's 5-30Hz!) for t in range(250,1000,250): # (NB avoid epilepsy's 5-30Hz!)
app.after(t,lambda *args:flashButton(button,"active")) app.after(t,lambda e=None,flashButton=flashButton,button=button:flashButton(button,"active"))
app.after(t+150,lambda *args:flashButton(button,"normal")) 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) # (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) if WMstandard: GUI_omit_statusline = 1 # unlikely to be room (and can disrupt nav)
...@@ -446,16 +486,24 @@ def startTk(): ...@@ -446,16 +486,24 @@ def startTk():
Tkinter.Frame.__init__(self, master) Tkinter.Frame.__init__(self, master)
class EmptyClass: pass class EmptyClass: pass
self.todo = EmptyClass() ; self.toRestore = [] self.todo = EmptyClass() ; self.toRestore = []
self.ScrollUpHandler = self.ScrollDownHandler = lambda e=None:True
global app ; app = self global app ; app = self
make_extra_buttons_waiting_list() make_extra_buttons_waiting_list()
if olpc: self.master.option_add('*font',cond(extra_buttons_waiting_list,'Helvetica 9','Helvetica 14')) 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? 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.) 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.extra_button_callables = []
self.pack(fill=Tkinter.BOTH,expand=1) self.pack(fill=Tkinter.BOTH,expand=1)
self.leftPanel = Tkinter.Frame(self) 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.rightPanel = None # for now
self.cancelling = 0 # guard against multiple presses of Cancel self.cancelling = 0 # guard against multiple presses of Cancel
self.Label = Tkinter.Label(self.leftPanel,text="Please wait a moment") self.Label = Tkinter.Label(self.leftPanel,text="Please wait a moment")
...@@ -467,33 +515,37 @@ def startTk(): ...@@ -467,33 +515,37 @@ def startTk():
nominalSize = intor0(f[-1]) nominalSize = intor0(f[-1])
if nominalSize: f=" ".join(f[:-1])+" %d" if nominalSize: f=" ".join(f[:-1])+" %d"
else: # Tk 8.5+ ? else: # Tk 8.5+ ?
f=str(self.tk.eval('set font [font actual '+' '.join(f)+']')).split() for f2 in ['set font [font actual '+' '.join(f)+']', # Tk 8.5
upNext = 0 'set font [font actual default]']: # Tk 8.6
for i in range(len(f)): f=str(self.tk.eval(f2)).split()
upNext = 0
for i in range(len(f)):
if f[i]=="-size": upNext=1 if f[i]=="-size": upNext=1
elif upNext: elif upNext:
nominalSize=intor0(f[i]) nominalSize=intor0(f[i])
if nominalSize<0: nominalSize,f[i] = -nominalSize,"-%d" if nominalSize<0: nominalSize,f[i] = -nominalSize,"-%d"
else: f[i]="%d" else: f[i]="%d"
break 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) 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"])) 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. # 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.) 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) largeNominalSize = int(nominalSize*self.Label.winfo_screenheight()/approx_lines_per_screen_when_large/pixelSize)
if largeNominalSize >= nominalSize+3: if largeNominalSize >= nominalSize+3:
self.bigPrintFont = f % largeNominalSize self.bigPrintFont = f % largeNominalSize
self.bigPrintMult = largeNominalSize*1.0/nominalSize
if GUI_always_big_print: if GUI_always_big_print:
self.master.option_add('*font',self.bigPrintFont) self.bigPrint0()
self.Label["font"]=self.bigPrintFont
del self.bigPrintFont ; self.isBigPrint=1
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) 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 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 if winCEsound and ask_teacherMode: self.Label["font"]="Helvetica 16" # might make it slightly easier
self.remake_cancel_button(localise("Cancel lesson")) self.remake_cancel_button(localise("Cancel lesson"))
self.Cancel.focus() # (default focus if we don't add anything else, e.g. reader) 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) self.Version = Tkinter.Label(self.leftPanel,text=self.copyright_string)
addStatus(self.Version,self.copyright_string) addStatus(self.Version,self.copyright_string)
if olpc: self.Version["font"]='Helvetica 9' if olpc: self.Version["font"]='Helvetica 9'
...@@ -609,7 +661,9 @@ def startTk(): ...@@ -609,7 +661,9 @@ def startTk():
updateUserRow(1) updateUserRow(1)
if hasattr(self,"bigPrintFont"): if hasattr(self,"bigPrintFont"):
self.BigPrintButton = addButton(self.leftPanel,localise("Big print"),self.bigPrint) 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")) self.remake_cancel_button(localise("Quit"))
if not GUI_omit_statusline: self.Version.pack(fill=Tkinter.X,expand=1) 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) 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)
...@@ -618,7 +672,7 @@ def startTk(): ...@@ -618,7 +672,7 @@ def startTk():
self.restore_copyright() self.restore_copyright()
if hasattr(self.todo,"alert"): 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) # 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 del self.todo.alert
if hasattr(self.todo,"question"): if hasattr(self.todo,"question"):
self.answer_given = tkMessageBox.askyesno(self.master.title(),self.todo.question) self.answer_given = tkMessageBox.askyesno(self.master.title(),self.todo.question)
...@@ -682,7 +736,8 @@ def startTk(): ...@@ -682,7 +736,8 @@ def startTk():
self.Minutes.set(mins) self.Minutes.set(mins)
addLabel(self.LessonRow,localise("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=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): def sync_listbox_etc(self):
if not hasattr(self,"vocabList"): if not hasattr(self,"vocabList"):
if hasattr(self,"needVocablist"): return # already waiting for main thread to make one if hasattr(self,"needVocablist"): return # already waiting for main thread to make one
...@@ -713,18 +768,18 @@ def startTk(): ...@@ -713,18 +768,18 @@ def startTk():
if synthCache: if synthCache:
cacheManagementOptions = [] # (text, oldKey, newKey, oldFile, newFile) cacheManagementOptions = [] # (text, oldKey, newKey, oldFile, newFile)
for t,l in [(text1.encode('utf-8'),secondLanguage),(text2.encode('utf-8'),firstLanguage)]: 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 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 (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 k in synthCache_transtbl and k[0]=="_": cacheManagementOptions.append(("Keep in "+l+" cache",k,k[1:],0,0)) if checkIn(k,synthCache_transtbl) and B(k[:1])==B("_"): 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:])) elif B(k[:1])==B("_"): 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(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: cacheManagementOptions.append(("Reject from "+l+" cache",0,0,f,"__rejected_"+f))
else: else:
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("!synth:__rejected__"+t+"_"+l,justQueryCache=1) if not f: k,f = synthcache_lookup(B("!synth:__rejected__")+t+B("_"+l),justQueryCache=1)
if f: 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:])) 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)) elif l==secondLanguage and mp3web and not ';' in t: cacheManagementOptions.append(("Get from "+mp3webName,0,0,0,0))
if not hasattr(self,"cacheManagementOptions"): if not hasattr(self,"cacheManagementOptions"):
...@@ -739,16 +794,16 @@ def startTk(): ...@@ -739,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 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) 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 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 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): def doSynthcacheManagement(self,oldKey,newKey,oldFname,newFname):
# should be a quick operation - might as well do it in the GUI thread # should be a quick operation - might as well do it in the GUI thread
if (oldKey,oldFname) == (0,0): # special for mp3web if (oldKey,oldFname) == (0,0): # special for mp3web
self.menu_response="mp3web" ; return self.menu_response="mp3web" ; return
if oldKey in synthCache_transtbl: if checkIn(oldKey,synthCache_transtbl):
if newKey: synthCache_transtbl[newKey]=synthCache_transtbl[oldKey] if newKey: synthCache_transtbl[newKey]=synthCache_transtbl[oldKey]
else: del 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: if oldFname:
del synthCache_contents[oldFname] del synthCache_contents[oldFname]
if newFname: if newFname:
...@@ -760,7 +815,12 @@ def startTk(): ...@@ -760,7 +815,12 @@ def startTk():
for control,current,restoreTo in self.toRestore: for control,current,restoreTo in self.toRestore:
if asUnicode(control.get())==current: control.set(restoreTo) if asUnicode(control.get())==current: control.set(restoreTo)
self.toRestore = [] 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): def thin_down_for_lesson(self):
if hasattr(self,"OutputRow"): self.OutputRow.pack_forget() if hasattr(self,"OutputRow"): self.OutputRow.pack_forget()
if hasattr(self,"CopyFromButton"): if hasattr(self,"CopyFromButton"):
...@@ -787,12 +847,17 @@ def startTk(): ...@@ -787,12 +847,17 @@ def startTk():
self.Label.pack() ; self.CancelRow.pack() 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.Label["text"] = "Working..." # (to be replaced by time indication on real-time, not on output-to-file)
self.Cancel["text"] = localise("Quit") self.Cancel["text"] = localise("Quit")
def bigPrint(self,*args): def bigPrint0(self):
self.thin_down_for_lesson()
self.master.option_add('*font',self.bigPrintFont) 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.) 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 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 if self.rightPanel: # oops, need to re-construct it
global extra_buttons_waiting_list global extra_buttons_waiting_list
extra_buttons_waiting_list = [] extra_buttons_waiting_list = []
...@@ -868,14 +933,14 @@ def startTk(): ...@@ -868,14 +933,14 @@ def startTk():
self.L2Entry.bind('<Return>',self.changeLanguages) 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)))) 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 = 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.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 *args:app.after(10,lambda *args:app.L1Entry.selection_clear()))) # for backwards tabbing 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 GUI_omit_settings and (vocabFile==user0[1] or fileExists(vocabFile)): self.row3.pack_forget()
if textEditorCommand: 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") 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) row4right = addRightRow(self.row4)
self.EditVocabButton = addButton(row4right,"",self.openVocabFile,{"side":"left"},status="This button lets you edit your\nvocab collection in "+textEditorName) 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() self.make_lesson_row()
else: # no text editor, but can at least have Recorded Words button now we have a built-in manager else: # no text editor, but can at least have Recorded Words button now we have a built-in manager
self.make_lesson_row() self.make_lesson_row()
...@@ -916,8 +981,8 @@ def startTk(): ...@@ -916,8 +981,8 @@ def startTk():
m=Tkinter.Menu(None, tearoff=0, takefocus=0) m=Tkinter.Menu(None, tearoff=0, takefocus=0)
for i in range(len(lastUserNames)): for i in range(len(lastUserNames)):
if lastUserNames[i] and not i==intor0(self.userNo.get()): 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))) 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.setToOpen((addUserToFname(user0[0],i),addUserToFname(user0[0],intor0(self.userNo.get())))))) 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") 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 setToOpen(self,toOpen): self.menu_response,self.toOpen = "samplesCopy",toOpen
def copyVocabFrom(self,userNo): def copyVocabFrom(self,userNo):
...@@ -927,11 +992,12 @@ def startTk(): ...@@ -927,11 +992,12 @@ def startTk():
select_userNumber(intor0(self.userNo.get()),updateGUI=0) select_userNumber(intor0(self.userNo.get()),updateGUI=0)
vCurrent = list2set(vocabLinesWithLangs()) vCurrent = list2set(vocabLinesWithLangs())
o=appendVocabFileInRightLanguages() o=appendVocabFileInRightLanguages()
if not o: return # IOError
langs = (secondLanguage,firstLanguage) langs = (secondLanguage,firstLanguage)
for newLangs,line in vCopyFrom: for newLangs,line in vCopyFrom:
if (newLangs,line) in vCurrent: continue # already got it if checkIn((newLangs,line),vCurrent): continue # already got it
if not newLangs==langs: o.write("SET LANGUAGES "+" ".join(list(newLangs))+"\n") if not newLangs==langs: o.write(B("SET LANGUAGES ")+B(" ").join(list(newLangs))+B("\n"))
o.write(line+"\n") o.write(B(line)+B("\n"))
langs = newLangs langs = newLangs
o.close() o.close()
if hasattr(self,"vocabList"): del self.vocabList # re-read if hasattr(self,"vocabList"): del self.vocabList # re-read
...@@ -946,15 +1012,15 @@ def startTk(): ...@@ -946,15 +1012,15 @@ def startTk():
global firstLanguage,secondLanguage global firstLanguage,secondLanguage
firstLanguage1=asUnicode(self.L1Text.get()).encode('utf-8') firstLanguage1=asUnicode(self.L1Text.get()).encode('utf-8')
secondLanguage1=asUnicode(self.L2Text.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() 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.") 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 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.)") else: self.todo.alert = msg+" "+localise("(Sorry, a list of these is not available on this system - check eSpeak installation.)")
return 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) 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 = firstLanguage1,secondLanguage1 firstLanguage,secondLanguage = S(firstLanguage1),S(secondLanguage1)
updateSettingsFile(settingsFile,{"firstLanguage":firstLanguage,"secondLanguage":secondLanguage}) updateSettingsFile(settingsFile,{"firstLanguage":firstLanguage,"secondLanguage":secondLanguage})
if need_redisplay: if need_redisplay:
self.thin_down_for_lesson() self.thin_down_for_lesson()
...@@ -1014,7 +1080,10 @@ def startTk(): ...@@ -1014,7 +1080,10 @@ def startTk():
self.menu_response="add" 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 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()) 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 t1==text1: self.Text1.set(t1)
if not t2==text2: self.Text2.set(t2) if not t2==text2: self.Text2.set(t2)
def getEncoder(self,*args): def getEncoder(self,*args):
...@@ -1066,7 +1135,7 @@ def startTk(): ...@@ -1066,7 +1135,7 @@ def startTk():
appThread(Application) appThread(Application)
def hanzi_only(unitext): return u"".join(filter(lambda x:0x3000<ord(x)<0xa700 or ord(x)>=0x10000, list(unitext))) 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) # (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): 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) # 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)
...@@ -1083,24 +1152,26 @@ def guiVocabList(parsedVocab): ...@@ -1083,24 +1152,26 @@ def guiVocabList(parsedVocab):
if b.endswith(fl2): b=b[:fl2Len] if b.endswith(fl2): b=b[:fl2Len]
elif b.endswith(fl3): b=readText(b) elif b.endswith(fl3): b=readText(b)
else: continue else: continue
ret.append((unicode(c,"utf-8"),unicode(b,"utf-8"))) ret.append((ensure_unicode(c),ensure_unicode(b)))
return ret return ret
def readText(l): # see utils/transliterate.py (running guiVocabList on txt files from scanSamples) def readText(l): # see utils/transliterate.py (running guiVocabList on txt files from scanSamples)
l = samplesDirectory+os.sep+l l = B(samplesDirectory)+B(os.sep)+B(l)
if l in variantFiles: # oops. just read the 1st .txt variant if checkIn(l,variantFiles): # oops. just read the 1st .txt variant
if os.sep in l: lp=(l+os.sep)[:l.rfind(os.sep)]+os.sep if B(os.sep) in l: lp=(l+B(os.sep))[:l.rfind(B(os.sep))]+B(os.sep)
else: lp = "" else: lp = B("")
varList = filter(lambda x:x.endswith(dottxt),variantFiles[l]) 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? 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] l = lp + varList[0]
return u8strip(read(l)).strip(wsp) return bwspstrip(u8strip(read(l)))
def singular(number,s): def singular(number,s):
s=localise(s) s=localise(s)
if firstLanguage=="en" and number==1 and s[-1]=="s": return s[:-1] if firstLanguage=="en" and number==1 and s[-1]=="s": return s[:-1]
return s return s
def localise(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) GUIlang = GUI_languages.get(firstLanguage,firstLanguage)
if scriptVariants.get(GUIlang,0): s2 = d.get(GUIlang+str(scriptVariants[GUIlang]+1),0) if scriptVariants.get(GUIlang,0): s2 = d.get(GUIlang+str(scriptVariants[GUIlang]+1),0)
if not s2: s2 = d.get(GUIlang,s) if not s2: s2 = d.get(GUIlang,s)
...@@ -1149,10 +1220,7 @@ if useTK: ...@@ -1149,10 +1220,7 @@ if useTK:
textEditorCommand=explorerCommand=None textEditorCommand=explorerCommand=None
if winsound or mingw32 or cygwin: if winsound or mingw32 or cygwin:
textEditorName="Notepad" ; textEditorWaits=1 textEditorName="Notepad" ; textEditorWaits=1
# Try Notepad++ first, otherwise plain notepad textEditorCommand="notepad"
textEditorCommand = programFiles+os.sep+"Notepad++"+os.sep+"notepad++.exe"
if fileExists(textEditorCommand): textEditorCommand='"'+textEditorCommand+'" -multiInst -notabbar -nosession'
else: textEditorCommand="notepad"
explorerCommand="explorer" explorerCommand="explorer"
elif macsound: elif macsound:
textEditorName="TextEdit" textEditorName="TextEdit"
...@@ -1160,21 +1228,27 @@ if useTK: ...@@ -1160,21 +1228,27 @@ if useTK:
if got_program("bbedit"): if got_program("bbedit"):
textEditorName="bbedit" textEditorName="bbedit"
textEditorCommand="bbedit -w" ; textEditorWaits=1 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 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" else: explorerCommand="open"
elif unix: 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 # looks like we're in a KDE session and can use the kfmclient command
textEditorCommand=explorerCommand="kfmclient exec" textEditorCommand=explorerCommand="kfmclient exec"
elif not olpc and got_program("gnome-open"): elif not olpc and got_program("gnome-open"):
textEditorCommand=explorerCommand="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"): elif got_program("rox"):
# rox is available - try using that to open directories # rox is available - try using that to open directories
# (better not use it for editor as it might not be configured) # (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" explorerCommand="rox"
# anyway, see if we can find a nice editor # 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): if got_program(editor):
textEditorName=textEditorCommand=editor textEditorName=textEditorCommand=editor
textEditorWaits = 1 textEditorWaits = 1
...@@ -1184,7 +1258,12 @@ if useTK: ...@@ -1184,7 +1258,12 @@ if useTK:
break break
# End of finding editor - now start GUI # End of finding editor - now start GUI
try: 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 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: if olpc:
def interrupt_main(): os.kill(os.getpid(),2) # sigint def interrupt_main(): os.kill(os.getpid(),2) # sigint
...@@ -1204,31 +1283,35 @@ if useTK: ...@@ -1204,31 +1283,35 @@ if useTK:
def openDirectory(dir,inGuiThread=0): def openDirectory(dir,inGuiThread=0):
if winCEsound: if winCEsound:
if not dir[0]=="\\": dir=os.getcwd()+cwd_addSep+dir # must be absolute 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: elif explorerCommand:
if ' ' in dir: dir='"'+dir+'"'
cmd = explorerCommand+" "+dir cmd = explorerCommand+" "+dir
if winsound or mingw32: cmd="start "+cmd # (not needed on XP but is on Vista) if winsound or mingw32: cmd="start "+cmd # (not needed on XP but is on Vista)
elif unix: cmd += "&" elif unix: cmd += "&"
os.system(cmd) os.system(cmd)
else: 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) if inGuiThread: tkMessageBox.showinfo(app.master.title(),msg)
else: waitOnMessage(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 not text: return # always OK empty strings
if pauseOnError: if pauseOnError:
ret = sanityCheck(text,language) ret = generalCheck(text,language)
if ret: waitOnMessage(ret) if ret: waitOnMessage(ret)
return ret return ret
if language=="zh": if language=="zh":
allDigits = True allDigits = True ; text=B(text)
for t in text: for i in xrange(len(text)):
t = text[i:i+1]
if ord(t)>127: return # got hanzi or tone marks if ord(t)>127: return # got hanzi or tone marks
if t in "12345": return # got tone numbers if t in B("12345"): return # got tone numbers
if t not in "0123456789. ": allDigits = False if t not in B("0123456789. "): allDigits = False
if allDigits: return 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(): 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.")) if fileExists(progressFile): checkAge(progressFile,localise("It has been %d days since your last Gradint lesson. Please try to have one every day."))
...@@ -1243,12 +1326,12 @@ def checkAge(fname,message): ...@@ -1243,12 +1326,12 @@ def checkAge(fname,message):
if days>=5 and (days%5)==0: waitOnMessage(message % days) if days>=5 and (days%5)==0: waitOnMessage(message % days)
def s60_addVocab(): 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: 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 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 if not result: return # cancelled
l2,l1 = result # guaranteed to both be populated 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"") l2=appuifw.query(label1,"text",u"")
if not l2: return # cancelled if not l2: return # cancelled
# TODO detect duplicates like Tk GUI does? # TODO detect duplicates like Tk GUI does?
...@@ -1256,7 +1339,7 @@ def s60_addVocab(): ...@@ -1256,7 +1339,7 @@ def s60_addVocab():
appendVocabFileInRightLanguages().write((l2+"="+l1+"\n").encode("utf-8")) appendVocabFileInRightLanguages().write((l2+"="+l1+"\n").encode("utf-8"))
def s60_changeLang(): def s60_changeLang():
global firstLanguage,secondLanguage 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 if not result: return # cancelled
l1,l2 = result l1,l2 = result
firstLanguage,secondLanguage = l1.encode('utf-8').lower(),l2.encode('utf-8').lower() firstLanguage,secondLanguage = l1.encode('utf-8').lower(),l2.encode('utf-8').lower()
...@@ -1270,7 +1353,7 @@ def s60_runLesson(): ...@@ -1270,7 +1353,7 @@ def s60_runLesson():
def s60_viewVocab(): def s60_viewVocab():
global justSynthesize global justSynthesize
doLabel("Reading your vocab list, please wait...") 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.") if not vList: return waitOnMessage("Your computer-voiced vocab list is empty.")
while True: while True:
appuifw.app.body = None appuifw.app.body = None
...@@ -1280,8 +1363,8 @@ def s60_viewVocab(): ...@@ -1280,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]) 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: if action==0 or action==1:
doLabel("Speaking...") doLabel("Speaking...")
justSynthesize = secondLanguage+" "+l2.encode('utf-8') justSynthesize = B(secondLanguage)+B(" ")+l2.encode('utf-8')
if action==1: justSynthesize += ("#"+firstLanguage+" "+l1.encode('utf-8')) if action==1: justSynthesize += (B('#')+B(firstLanguage)+B(" ")+l1.encode('utf-8'))
just_synthesize() just_synthesize()
justSynthesize = "" justSynthesize = ""
elif action==5: pass elif action==5: pass
...@@ -1290,11 +1373,11 @@ def s60_viewVocab(): ...@@ -1290,11 +1373,11 @@ def s60_viewVocab():
oldL1,oldL2 = l1,l2 oldL1,oldL2 = l1,l2
if action==2: if action==2:
first=1 first=1
while first or (l2 and sanityCheck(l2.encode('utf-8'),secondLanguage,1)): while first or (l2 and generalCheck(l2.encode('utf-8'),secondLanguage,1)):
first=0 ; l2=appuifw.query(u""+secondLanguage,"text",l2) first=0 ; l2=appuifw.query(ensure_unicode(secondLanguage),"text",l2)
if not l2: continue if not l2: continue
elif action==3: elif action==3:
l1 = appuifw.query(u""+firstLanguage,"text",l1) l1 = appuifw.query(ensure_unicode(firstLanguage),"text",l1)
if not l1: continue if not l1: continue
doLabel("Processing") doLabel("Processing")
delOrReplace(oldL2,oldL1,l2,l1,cond(action==4,"delete","replace")) delOrReplace(oldL2,oldL1,l2,l1,cond(action==4,"delete","replace"))
...@@ -1302,36 +1385,81 @@ def s60_viewVocab(): ...@@ -1302,36 +1385,81 @@ def s60_viewVocab():
del vList[sel] del vList[sel]
if not vList: return # empty if not vList: return # empty
else: vList[sel] = l2+"="+l1 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"): def delOrReplace(L2toDel,L1toDel,newL2,newL1,action="delete"):
langs = [secondLanguage,firstLanguage] langs = [secondLanguage,firstLanguage]
v=u8strip(read(vocabFile)).replace("\r\n","\n").replace("\r","\n") v=u8strip(read(vocabFile)).replace(B("\r\n"),B("\n")).replace(B("\r"),B("\n"))
o=open(vocabFile,"w") ; found = 0 if paranoid_file_management:
if last_u8strip_found_BOM: o.write('\xef\xbb\xbf') # re-write it fname = os.tempnam()
v=v.split("\n") 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 if v and not v[-1]: v=v[:-1] # don't add an extra blank line at end
for l in v: for l in v:
l2=l.lower() l2=l.lower()
if l2.startswith("set language ") or l2.startswith("set languages "): if l2.startswith(B("set language ")) or l2.startswith(B("set languages ")):
langs=l.split()[2:] ; o.write(l+"\n") ; continue langs=map(S,l.split()[2:]) ; writeB(o,l+B("\n")) ; continue
thisLine=map(lambda x:x.strip(wsp),l.split("=",len(langs)-1)) 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')]): 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 # delete this line. and maybe replace it
found = 1 found = 1
if action=="replace": if action=="replace":
if langs==[secondLanguage,firstLanguage]: o.write(newL2.encode("utf-8")+"="+newL1.encode("utf-8")+"\n") if langs==[secondLanguage,firstLanguage]: writeB(o,newL2.encode("utf-8")+B("=")+newL1.encode("utf-8")+B("\n"))
else: o.write(newL1.encode("utf-8")+"="+newL2.encode("utf-8")+"\n") else: writeB(o,newL1.encode("utf-8")+B("=")+newL2.encode("utf-8")+B("\n"))
else: o.write(l+"\n") else: writeB(o,l+B("\n"))
o.close() o.close()
if paranoid_file_management:
write(vocabFile,read(fname))
os.remove(fname)
return found 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(): def s60_main_menu():
while True: while True:
appuifw.app.body = None # NOT text saying version no etc - has distracting blinking cursor appuifw.app.body = None # NOT text saying version no etc - has distracting blinking cursor
menu=[] menu=[]
if maybeCanSynth(secondLanguage): 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) doVocab = maybeCanSynth(firstLanguage)
if doVocab: menu.append((u"Add word to my vocab",s60_addVocab)) if doVocab: menu.append((u"Add word to my vocab",s60_addVocab))
menu.append((u"Make lesson from vocab",s60_runLesson)) menu.append((u"Make lesson from vocab",s60_runLesson))
...@@ -1347,20 +1475,18 @@ def s60_main_menu(): ...@@ -1347,20 +1475,18 @@ def s60_main_menu():
def downloadLAME(): def downloadLAME():
# Sourceforge keep making this harder! # 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 # Removed code to check for latest version, as we
if ! test -e lame*.tar.gz; then # can't use v3.100 due to Lame bug 488.
export Link="$($Curl "http://sourceforge.net/project/showfiles.php?group_id=290&package_id=309"|grep tar.gz|head -1)" return not system("""if which curl >/dev/null 2>/dev/null; then Curl="curl -L"; else Curl="wget -O -"; fi
echo "Got HTML: $Link" 1>&2 if ! [ -e lame*.tar.gz ]; then
export Link="$(echo "$Link"|sed -e 's,href="/,href="http://sourceforge.net/,' -e 's/.*http:/http:/' -e 's/.tar.gz.*/.tar.gz/')" if ! $Curl "https://sourceforge.net/projects/lame/files/lame/3.99/lame-3.99.5.tar.gz/download" > lame.tar.gz; then
echo "Following link to $Link" 1>&2
if ! $Curl "$Link" > lame.tar.gz; then
rm -f lame.tar.gz; exit 1 rm -f lame.tar.gz; exit 1
fi fi
if grep downloads.sourceforge lame.tar.gz 2>/dev/null; then if grep downloads.sourceforge lame.tar.gz 2>/dev/null; then
export Link="$(cat lame.tar.gz|grep downloads.sourceforge|head -1)" Link="$(cat lame.tar.gz|grep downloads.sourceforge|head -1)"
echo "Got HTML 2: $Link" 1>&2 echo "Got HTML: $Link" >&2
export Link="$(echo "$Link"|sed -e 's/.*http/http/' -e 's,.*/projects,http://sourceforge.net/projects,' -e 's/".*//')" 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 echo "Following link to $Link" >&2
if ! $Curl "$Link" > lame.tar.gz; then if ! $Curl "$Link" > lame.tar.gz; then
rm -f lame.tar.gz; exit 1 rm -f lame.tar.gz; exit 1
fi fi
...@@ -1369,6 +1495,13 @@ fi""") ...@@ -1369,6 +1495,13 @@ fi""")
def gui_event_loop(): def gui_event_loop():
app.todo.set_main_menu = 1 ; braveUser = 0 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() if orig_onceperday&2: check_for_slacking()
while app: while app:
while not hasattr(app,"menu_response"): while not hasattr(app,"menu_response"):
...@@ -1381,13 +1514,16 @@ def gui_event_loop(): ...@@ -1381,13 +1514,16 @@ def gui_event_loop():
if emulated_interruptMain: check_for_interrupts() if emulated_interruptMain: check_for_interrupts()
time.sleep(0.3) time.sleep(0.3)
menu_response = app.menu_response menu_response = app.menu_response
del app.menu_response
if menu_response=="input": # WMstandard if menu_response=="input": # WMstandard
app.todo.input_response=raw_input() app.todo.input_response=raw_input()
elif menu_response=="go": elif menu_response=="go":
gui_outputTo_start() gui_outputTo_start()
if not soundCollector: app.todo.add_briefinterrupt_button = 1 if not soundCollector: app.todo.add_briefinterrupt_button = 1
try: lesson_loop() 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) 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) if app and not soundCollector: app.todo.remove_briefinterrupt_button = 1 # (not app if it's closed by the close box)
gui_outputTo_end() gui_outputTo_end()
...@@ -1402,7 +1538,7 @@ def gui_event_loop(): ...@@ -1402,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 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 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 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.") waitOnMessage("When you've finished editing "+app.fileToEdit+", close it and start gradint again.")
return return
elif textEditorCommand: elif textEditorCommand:
...@@ -1423,7 +1559,7 @@ def gui_event_loop(): ...@@ -1423,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.") 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) newContents = read(fileToEdit)
if not newContents==oldContents: 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: if app.fileToEdit==vocabFile:
app.wordsExist=1 ; del app.vocabList # re-read 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.") else: waitOnMessage("The changes you made to "+app.fileToEdit+" will take effect when you quit Gradint and start it again.")
...@@ -1450,35 +1586,39 @@ def gui_event_loop(): ...@@ -1450,35 +1586,39 @@ def gui_event_loop():
text1 = asUnicode(app.Text1.get()).encode('utf-8') ; text2 = asUnicode(app.Text2.get()).encode('utf-8') 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." 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: else:
msg=sanityCheck(text1,secondLanguage) if text1.startswith(B('#')): msg="" # see below
if msg: app.todo.alert=u""+msg else: msg=generalCheck(text1,secondLanguage)
if msg: app.todo.alert=ensure_unicode(msg)
else: else:
app.set_watch_cursor = 1 ; app.toRestore = [] app.set_watch_cursor = 1 ; app.toRestore = []
global justSynthesize ; justSynthesize = "" global justSynthesize ; justSynthesize = ""
def doControl(text,lang,control): def doControl(text,lang,control):
global justSynthesize global justSynthesize ; text=B(text)
restoreTo = asUnicode(control.get()) restoreTo = asUnicode(control.get())
if text: 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)
if can_be_synthesized("!synth:"+text+"_"+lang): justSynthesize += ("#"+lang+" "+text) elif text:
else: app.todo.alert="Cannot find a synthesizer that can say '"+text+"' in language '"+lang+"' on this system" if can_be_synthesized(B("!synth:")+text+B("_")+B(lang)):
t=transliterates_differently(text,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 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 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: else:
control.set(t) ; app.toRestore.append((control,t,restoreTo)) control.set(t) ; app.toRestore.append((control,t,restoreTo))
doControl(text1,secondLanguage,app.Text1) doControl(text1,secondLanguage,app.Text1)
def doSynth(): def doSynth(openDir=True):
gui_outputTo_start() ; just_synthesize() ; gui_outputTo_end() gui_outputTo_start() ; just_synthesize() ; gui_outputTo_end(openDir)
global justSynthesize ; justSynthesize = "" global justSynthesize ; justSynthesize = ""
if app: app.unset_watch_cursor = 1 # otherwise was closed by the close box 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) if text1 and text2:
doSynth() if app and hasattr(app,"outputTo") and app.outputTo.get() and not app.outputTo.get()=="0":
if app and getYN("Also speak the %s?" % firstLanguage): if getYN("Save %s and %s to separate files?" % (secondLanguage,firstLanguage)): doSynth(False)
doControl(text2,firstLanguage,app.Text2) 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()
else:
doControl(text2,firstLanguage,app.Text2)
doSynth() doSynth()
if app and not getYN("Also speak the %s?" % firstLanguage):
continue
doControl(text2,firstLanguage,app.Text2)
doSynth()
elif menu_response=="mp3web": elif menu_response=="mp3web":
url=[] ; text1 = asUnicode(app.Text1.get()) url=[] ; text1 = asUnicode(app.Text1.get())
for c in list(text1.encode("utf-8")): for c in list(text1.encode("utf-8")):
...@@ -1492,15 +1632,15 @@ def gui_event_loop(): ...@@ -1492,15 +1632,15 @@ def gui_event_loop():
for f in os.listdir(d): dd[d+os.sep+f]=1 for f in os.listdir(d): dd[d+os.sep+f]=1
return dd,found return dd,found
oldLs,found = scanDirs() 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 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") 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.")) waitOnMessage(localise("If the word is there, download it. When you press OK, Gradint will check for downloads."))
if not app: break if not app: break
found=0 found=0
for f in scanDirs()[0].keys(): 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) 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() uf=scFile=text1.encode("utf-8")+"_"+secondLanguage+f[-4:].lower()
try: try:
...@@ -1533,30 +1673,29 @@ def gui_event_loop(): ...@@ -1533,30 +1673,29 @@ def gui_event_loop():
if not getYN("Download failed. Try again?"): break if not getYN("Download failed. Try again?"): break
if worked: if worked:
app.setLabel("Compiling...") 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 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" 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? # Trying to add a word that's already there - do we interpret this as a progress adjustment?
app.set_watch_cursor = 1 app.set_watch_cursor = 1
t1,t2 = asUnicode(app.Text1.get()),asUnicode(app.Text2.get()) t1,t2 = asUnicode(app.Text1.get()),asUnicode(app.Text2.get())
lang2,lang1=t1.lower(),t2.lower() # because it's .lower()'d in progress.txt lang2,lang1=t1.lower(),t2.lower() # because it's .lower()'d in progress.txt
d = ProgressDatabase(0) d = ProgressDatabase(0)
l1find = "!synth:"+lang1.encode('utf-8')+"_"+firstLanguage l1find = S(B("!synth:")+lang1.encode('utf-8')+B("_"+firstLanguage))
found = 0 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]: for listToCheck in [d.data,d.unavail]:
if found: break if found: break
for item in listToCheck: 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 if not item[0]: break # not done yet - as not-found
newItem0 = reviseCount(item[0])
app.unset_watch_cursor = 1 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))): if getYN(msg+" "+localise("Repeat count is %d. Reduce this to %d for extra revision?" % (item[0],newItem0))):
app.set_watch_cursor = 1 app.set_watch_cursor = 1
listToCheck.remove(item) listToCheck.remove(item)
...@@ -1569,12 +1708,15 @@ def gui_event_loop(): ...@@ -1569,12 +1708,15 @@ def gui_event_loop():
app.todo.alert=msg+" "+localise("Repeat count is 0, so we cannot reduce it for extra revision.") app.todo.alert=msg+" "+localise("Repeat count is 0, so we cannot reduce it for extra revision.")
elif menu_response=="add": elif menu_response=="add":
text1 = asUnicode(app.Text1.get()).encode('utf-8') ; text2 = asUnicode(app.Text2.get()).encode('utf-8') text1 = asUnicode(app.Text1.get()).encode('utf-8') ; text2 = asUnicode(app.Text2.get()).encode('utf-8')
msg=sanityCheck(text1,secondLanguage) msg=generalCheck(text1,secondLanguage)
if msg: app.todo.alert=u""+msg if msg: app.todo.alert=ensure_unicode(msg)
else: else:
o=appendVocabFileInRightLanguages() 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() 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))) if hasattr(app,"vocabList"): app.vocabList.append((ensure_unicode(text1),ensure_unicode(text2)))
app.todo.clear_text_boxes=app.wordsExist=1 app.todo.clear_text_boxes=app.wordsExist=1
elif menu_response=="delete" or menu_response=="replace": elif menu_response=="delete" or menu_response=="replace":
...@@ -1587,19 +1729,19 @@ def gui_event_loop(): ...@@ -1587,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) if found and menu_response=="replace": # maybe hack progress.txt as well (taken out of the above loop for better failsafe)
d = ProgressDatabase(0) d = ProgressDatabase(0)
lang2,lang1=lang2.lower(),lang1.lower() # because it's .lower()'d in progress.txt 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: 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 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 app.set_watch_cursor = 1 ; break
d.data.remove(item) 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([]): if type(item[1])==type([]):
l = item[1] l = item[1]
l[l.index(l1find)] = l1replace l[l.index(l1find)] = l1replace
else: l=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) d.data.append(item)
app.set_watch_cursor = 1 app.set_watch_cursor = 1
for i2 in d.unavail: for i2 in d.unavail:
...@@ -1612,35 +1754,39 @@ def gui_event_loop(): ...@@ -1612,35 +1754,39 @@ def gui_event_loop():
app.todo.clear_text_boxes=1 app.todo.clear_text_boxes=1
app.unset_watch_cursor = 1 app.unset_watch_cursor = 1
if not found: app.todo.alert = "OOPS: Item to delete/replace was not found in "+vocabFile 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 def vocabLinesWithLangs(): # used for merging different users' vocab files
langs = [secondLanguage,firstLanguage] ; ret = [] langs = [secondLanguage,firstLanguage] ; ret = []
try: v=u8strip(read(vocabFile)).replace("\r","\n") try: v=u8strip(read(vocabFile)).replace(B("\r"),B("\n"))
except IOError: v="" except IOError: v=B("")
for l in v.split("\n"): for l in v.split(B("\n")):
l2=l.lower() 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) elif l: ret.append((tuple(langs),l)) # TODO what about blank lines? (currently they'd be considered duplicates)
return ret return ret
def appendVocabFileInRightLanguages(): def appendVocabFileInRightLanguages():
# check if we need a SET LANGUAGE # check if we need a SET LANGUAGE
langs = [secondLanguage,firstLanguage] langs = [secondLanguage,firstLanguage]
try: v=u8strip(read(vocabFile)).replace("\r","\n") try: v=u8strip(read(vocabFile)).replace(B("\r"),B("\n"))
except IOError: v="" except IOError: v=B("")
for l in v.split("\n"): for l in v.split(B("\n")):
l2=l.lower() 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 ")):
o=open(vocabFile,"a") langs=l.split()[2:]
if not v.endswith("\n"): o.write("\n") for i in range(len(langs)): langs[i]=S(langs[i])
if not langs==[secondLanguage,firstLanguage]: o.write("SET LANGUAGES "+secondLanguage+" "+firstLanguage+"\n") 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 return o
def transliterates_differently(text,lang): def transliterates_differently(text,lang):
global last_partials_transliteration ; last_partials_transliteration=None 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 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 partials_are_sporadic = o
if last_partials_transliteration and not last_partials_transliteration==text: return last_partials_transliteration 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) else: return # (don't try to translit. if was in synth cache - will have no idea which synth did it)
...@@ -1650,7 +1796,6 @@ def transliterates_differently(text,lang): ...@@ -1650,7 +1796,6 @@ def transliterates_differently(text,lang):
translit=synth.transliterate(lang,text,forPartials=0) translit=synth.transliterate(lang,text,forPartials=0)
if translit and not translit==text: return translit if translit and not translit==text: return translit
gui_output_counter = 1
def gui_outputTo_start(): def gui_outputTo_start():
if hasattr(app,"outputTo") and app.outputTo.get() and not app.outputTo.get()=="0": if hasattr(app,"outputTo") and app.outputTo.get() and not app.outputTo.get()=="0":
global outputFile,gui_output_directory,oldGID ; outputFile=None global outputFile,gui_output_directory,oldGID ; outputFile=None
...@@ -1663,21 +1808,27 @@ def gui_outputTo_start(): ...@@ -1663,21 +1808,27 @@ def gui_outputTo_start():
if type(gui_output_directory)==type([]): gui_output_directory=gui_output_directory[-1] if type(gui_output_directory)==type([]): gui_output_directory=gui_output_directory[-1]
try: os.mkdir(gui_output_directory) try: os.mkdir(gui_output_directory)
except: pass 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): 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 gui_output_counter += 1
global write_to_stdout ; write_to_stdout = 0 global write_to_stdout ; write_to_stdout = 0
global out_type ; out_type = app.outputTo.get() global out_type ; out_type = app.outputTo.get()
global need_run_media_encoder 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) need_run_media_encoder = (out_type,outputFile)
out_type="wav" ; outputFile=os.tempnam()+dotwav out_type="wav" ; outputFile=os.tempnam()+dotwav
else: need_run_media_encoder = 0 else: need_run_media_encoder = 0
setSoundCollector(SoundCollector()) setSoundCollector(SoundCollector())
global waitBeforeStart, waitBeforeStart_old global waitBeforeStart, waitBeforeStart_old
waitBeforeStart_old = waitBeforeStart ; waitBeforeStart = 0 waitBeforeStart_old = waitBeforeStart ; waitBeforeStart = 0
def gui_outputTo_end(): def gui_outputTo_end(openDir=True):
global outputFile, waitBeforeStart, oldGID, gui_output_directory global outputFile, waitBeforeStart, oldGID, gui_output_directory
if outputFile: if outputFile:
no_output = not soundCollector.tell() # probably 'no words to put in the lesson' no_output = not soundCollector.tell() # probably 'no words to put in the lesson'
...@@ -1698,9 +1849,10 @@ def gui_outputTo_end(): ...@@ -1698,9 +1849,10 @@ def gui_outputTo_end():
# NB we're passing this to cmd, NOT bash: # 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" 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. 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 else: assert 0
if cygwin: 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 cmd="echo '"+cmd+" && exit' | cmd" # seems the only way to get it to work on cygwin
system(cmd) system(cmd)
os.remove(outputFile) os.remove(outputFile)
...@@ -1711,16 +1863,20 @@ def gui_outputTo_end(): ...@@ -1711,16 +1863,20 @@ def gui_outputTo_end():
no_output = 1 no_output = 1
outputFile=None outputFile=None
waitBeforeStart = waitBeforeStart_old 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 try: gui_output_directory = oldGID
except: pass except: pass
def main(): def main():
global useTK,justSynthesize,waitBeforeStart,traceback,appTitle global useTK,justSynthesize,waitBeforeStart,traceback,appTitle,app,warnings_toprint
if useTK: 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() 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(): def rest_of_main():
global useTK,justSynthesize,waitBeforeStart,traceback,appTitle,saveProgress,RM_running global useTK,justSynthesize,waitBeforeStart,traceback,appTitle,saveProgress,RM_running
exitStatus = 0 ; RM_running = 1 exitStatus = 0 ; RM_running = 1
...@@ -1730,20 +1886,27 @@ def rest_of_main(): ...@@ -1730,20 +1886,27 @@ def rest_of_main():
except NameError: ceLowMemory=0 except NameError: ceLowMemory=0
if ceLowMemory and getYN("Low memory! Python may crash. Turn off progress saving for safety?"): saveProgress=0 if ceLowMemory and getYN("Low memory! Python may crash. Turn off progress saving for safety?"): saveProgress=0
if justSynthesize=="-": primitive_synthloop() if B(justSynthesize)==B("-"): primitive_synthloop()
elif justSynthesize and justSynthesize[-1]=='*': elif justSynthesize and B(justSynthesize)[-1:]==B('*'):
justSynthesize=justSynthesize[:-1] justSynthesize=justSynthesize[:-1]
waitBeforeStart = 0 waitBeforeStart = 0
just_synthesize() ; lesson_loop() just_synthesize() ; lesson_loop()
elif justSynthesize: just_synthesize() elif justSynthesize: just_synthesize()
elif app and waitBeforeStart: gui_event_loop() elif app and waitBeforeStart: gui_event_loop()
elif appuifw: s60_main_menu() elif appuifw: s60_main_menu()
elif android: android_main_menu()
else: lesson_loop() else: lesson_loop()
except SystemExit: pass except SystemExit:
e = sys.exc_info()[1]
exitStatus = e.code
except KeyboardInterrupt: pass except KeyboardInterrupt: pass
except PromptException,prEx: except PromptException:
prEx = sys.exc_info()[1]
waitOnMessage("\nProblem finding prompts:\n"+prEx.message+"\n") waitOnMessage("\nProblem finding prompts:\n"+prEx.message+"\n")
exitStatus = 1 exitStatus = 1
except MessageException:
mEx = sys.exc_info()[1]
waitOnMessage(mEx.message+"\n") ; exitStatus = 1
except: 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() 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 try: import traceback
...@@ -1767,7 +1930,7 @@ def rest_of_main(): ...@@ -1767,7 +1930,7 @@ def rest_of_main():
except: pass except: pass
waitOnMessage(w.strip()) waitOnMessage(w.strip())
if not useTK: 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 elif traceback: traceback.print_exc() # will be wrong if there was an error in speaking
exitStatus = 1 exitStatus = 1
if appuifw: raw_input() # so traceback stays visible if appuifw: raw_input() # so traceback stays visible
...@@ -1780,7 +1943,10 @@ def rest_of_main(): ...@@ -1780,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 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 appuifw: appuifw.app.set_exit()
elif riscos_sound: show_info("You may now close this Task Window.\n") 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 RM_running = 0
if exitStatus: sys.exit(exitStatus) if exitStatus: sys.exit(exitStatus)
......
# This file is part of the source code of # This file is part of the source code of Gradint
# gradint v0.9974 (c) 2002-2011 Silas S. Brown. GPL v3+. # (c) Silas S. Brown.
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or # the Free Software Foundation; either version 3 of the License, or
...@@ -25,29 +25,34 @@ class ProgressDatabase(object): ...@@ -25,29 +25,34 @@ class ProgressDatabase(object):
self.unavail = mergeProgress(self.data,scanSamples()+parseSynthVocab(vocabFile)) self.unavail = mergeProgress(self.data,scanSamples()+parseSynthVocab(vocabFile))
if not cache_maintenance_mode: if not cache_maintenance_mode:
doLabel("Checking transliterations") doLabel("Checking transliterations")
global tList # for Python 2.1
tList = {} tList = {}
def addVs(ff,dirBase): def addVs(ff,dirBase): # add variants of ff to tList which we might need to transliterate
if dirBase: dirBase += os.sep dirBase,ff = B(dirBase),B(ff)
if dirBase+ff in variantFiles: if dirBase: dirBase += B(os.sep)
if os.sep in ff: ffpath=ff[:ff.rfind(os.sep)+1] if checkIn(dirBase+ff,variantFiles):
else: ffpath="" if B(os.sep) in ff: ffpath=ff[:ff.rfind(B(os.sep))+1]
variantList=map(lambda x:ffpath+x,variantFiles[dirBase+ff]) else: ffpath=B("")
variantList=map(lambda x,f=ffpath:f+B(x),variantFiles[dirBase+ff])
else: variantList = [ff] else: variantList = [ff]
l=languageof(ff) l=languageof(ff)
for f in variantList: for f in variantList:
if f.lower().endswith(dottxt): text=u8strip(read(dirBase+f)).strip(wsp) f = B(f)
elif f.find("!synth")==-1: continue # don't need to translit. filenames of wav's etc 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) else: text = textof(f)
if not l in tList: tList[l]={} if not checkIn(l,tList): tList[l]={}
tList[l][text]=1 tList[l][text]=1
for ff in availablePrompts.lsDic.values(): addVs(ff,promptsDirectory) for ff in availablePrompts.lsDic.values(): addVs(ff,promptsDirectory)
for _,l1,l2 in self.data: for _,l1,l2 in self.data:
if not type(l1)==type([]): l1=[l1] if not type(l1)==type([]): l1=[l1]
for ff in l1+[l2]: addVs(ff,samplesDirectory) for ff in l1+[l2]: addVs(ff,samplesDirectory)
doLabel("Transliterating") doLabel("Transliterating")
for lang,dic in tList.items(): for lang,dic in list(tList.items()):
s = get_synth_if_possible(lang,0) 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 self.didScan = alsoScan
def _load_from_binary(self): def _load_from_binary(self):
if pickledProgressFile and fileExists(pickledProgressFile): if pickledProgressFile and fileExists(pickledProgressFile):
...@@ -55,11 +60,12 @@ class ProgressDatabase(object): ...@@ -55,11 +60,12 @@ class ProgressDatabase(object):
global firstLanguage, secondLanguage, otherLanguages global firstLanguage, secondLanguage, otherLanguages
if compress_progress_file or (unix and got_program("gzip")): if compress_progress_file or (unix and got_program("gzip")):
if paranoid_file_management: open(pickledProgressFile) # ensure ready 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") else: f=open(pickledProgressFile,"rb")
try: thingsToSet, tup = pickle.Unpickler(f).load() try: thingsToSet, tup = pickle.Unpickler(f).load()
except: return False # probably moved to a different Python version or something except: return False # probably moved to a different Python version or something
exec(thingsToSet) exec(thingsToSet)
self._py3_fix()
return True return True
# otherwise drop out and return None # otherwise drop out and return None
def _load_from_text(self,fromString=0): def _load_from_text(self,fromString=0):
...@@ -67,11 +73,11 @@ class ProgressDatabase(object): ...@@ -67,11 +73,11 @@ class ProgressDatabase(object):
elif fileExists(progressFile): elif fileExists(progressFile):
if compress_progress_file or (unix and got_program("gzip")): if compress_progress_file or (unix and got_program("gzip")):
if paranoid_file_management: open(progressFile) # ensure ready 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 = read(progressFile)
else: expr = None else: expr = None
if expr: 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). # 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 global firstLanguage, secondLanguage, otherLanguages
try: self.data = eval(expr) try: self.data = eval(expr)
...@@ -81,64 +87,102 @@ class ProgressDatabase(object): ...@@ -81,64 +87,102 @@ class ProgressDatabase(object):
except: codeop = 0 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 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 = [] 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) lineCache.append(l)
if lineCache[-1].endswith(","): continue # no point trying to compile if it's obviously incomplete if lineCache[-1].endswith(B(",")): continue # no point trying to compile if it's obviously incomplete
code = codeop.compile_command("\n".join(lineCache)) code = codeop.compile_command("# coding=utf-8\n"+S(B("\n").join(lineCache)))
if code: if code:
lineCache = [] lineCache = []
exec code exec(code)
else: exec(expr) else: exec(B("# coding=utf-8\n")+expr)
del expr del expr
# Remove legacy extentions in promptsData (needed only when loading from text, as this was before pickledProgressFile was added) # 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): if k.endswith(dotwav) or k.endswith(dotmp3):
self.promptsData[k[:-len(dotwav)]]=self.promptsData[k] self.promptsData[k[:-len(dotwav)]]=self.promptsData[k]
del 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): def save(self,partial=0):
if need_say_where_put_progress: show_info("Saving "+cond(partial,"partial ","")+"progress to "+progressFile+"... ") if need_say_where_put_progress: show_info("Saving "+cond(partial,"partial ","")+"progress to "+progressFile+"... ")
else: show_info("Saving "+cond(partial,"partial ","")+"progress... ") else: show_info("Saving "+cond(partial,"partial ","")+"progress... ")
self._py3_fix_on_save()
global progressFileBackup global progressFileBackup
# Remove 0-repeated items (helps editing by hand) # Remove 0-repeated items (helps editing by hand)
data = [] # don't use self.data - may want to make another lesson after saving data = [] # don't use self.data - may want to make another lesson after saving
for a,b,c in self.data: for a,b,c in self.data:
if a: data.append(denumber_filelists(a,b,c)) 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: if progressFileBackup:
try: try:
import shutil import shutil
shutil.copy2(progressFile,progressFileBackup) # preserve timestamp etc if shutil is available shutil.copy2(progressFile,progressFileBackup) # preserve timestamp etc if shutil is available
except: except:
try: try: write(progressFileBackup,read(progressFile))
open(progressFileBackup,'wb').write(read(progressFile))
except IOError: pass # maybe progressFile not made yet except IOError: pass # maybe progressFile not made yet
progressFileBackup = None progressFileBackup = None
while True: while True:
try: 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') 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(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.) 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) prettyPrintLongList(f,"self.data",data)
f.write("self.promptsData=") ; pprint.PrettyPrinter(indent=2,width=60,stream=f).pprint(self.promptsData) f.write("self.promptsData=") ; pprint.PrettyPrinter(indent=2,width=60,stream=f).pprint(self.promptsData)
prettyPrintLongList(f,"self.unavail",self.unavail) 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) 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) 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 if getYN("I/O fault when saving progress. Retry?"): continue
# TODO else try to restore the backup? # TODO else try to restore the backup?
else: raise else: raise
break break
if not partial: self.saved_completely = 1 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) def save_binary(self,data): # save a pickled version if possible (no error if not)
if not (pickledProgressFile and pickle): return if not (pickledProgressFile and pickle): return
self._py3_fix_on_save()
try: 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') 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))) 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) 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): def savePartial(self,filesNotPlayed):
curPD,curDat = self.promptsData, self.data[:] # in case want to save a more complete one later curPD,curDat = self.promptsData, self.data[:] # in case want to save a more complete one later
...@@ -146,7 +190,7 @@ class ProgressDatabase(object): ...@@ -146,7 +190,7 @@ class ProgressDatabase(object):
if hasattr(self,"previous_filesNotPlayed"): if hasattr(self,"previous_filesNotPlayed"):
i=0 i=0
while i<len(filesNotPlayed): 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) 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) self.previous_filesNotPlayed = filesNotPlayed = list2set(filesNotPlayed)
if not filesNotPlayed: if not filesNotPlayed:
...@@ -160,17 +204,17 @@ class ProgressDatabase(object): ...@@ -160,17 +204,17 @@ class ProgressDatabase(object):
l.append(self.data[i][2]) l.append(self.data[i][2])
found=0 found=0
for ii in l: for ii in l:
if ii in filesNotPlayed: if checkIn(ii,filesNotPlayed):
self.data[i] = self.oldData[i] self.data[i] = self.oldData[i]
found=1 ; break found=1 ; break
if not found and not self.data[i] == self.oldData[i]: changed = 1 if not found and not self.data[i] == self.oldData[i]: changed = 1
if changed: self.save(partial=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 self.promptsData,self.data = curPD,curDat
def makeLesson(self): def makeLesson(self):
global maxLenOfLesson global maxLenOfLesson
self.l = Lesson() 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.oldData = self.data[:] # for handling interrupts & partial progress saves
self.exclude = {} ; self.do_as_poem = {} self.exclude = {} ; self.do_as_poem = {}
# First priority: Recently-learned old words # First priority: Recently-learned old words
...@@ -192,28 +236,34 @@ class ProgressDatabase(object): ...@@ -192,28 +236,34 @@ class ProgressDatabase(object):
for p in poems: for p in poems:
for l in p: self.do_as_poem[self.responseIndex[l]] = p for l in p: self.do_as_poem[self.responseIndex[l]] = p
self.addToLesson(reallyKnownThreshold,-1,1,1,-1) 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 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 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: if orderlessCommentsToAdd:
for c in orderlessCommentsToAdd: for c in orderlessCommentsToAdd:
try: try:
l.addSequence([GluedEvent(Glue(1,maxLenOfLesson),fileToEvent(c,""))]) l.addSequence([GluedEvent(Glue(1,maxLenOfLesson),fileToEvent(c,""))],False)
except StretchedTooFar: except StretchedTooFar:
sys.stderr.write("Was trying to add %s\n" % (c,)) show_info(("Was trying to add %s\n" % (c,)),True)
raise raise
# Add note on "long pause", for beginners # Add note on "long pause", for beginners
longpause = "longpause_"+firstLanguage 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] 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: try:
def PauseEvent(): return fileToEvent(availablePrompts.lsDic[longpause],promptsDirectory) def PauseEvent(longpause): return fileToEvent(availablePrompts.lsDic[longpause],promptsDirectory)
firstPauseMsg = PauseEvent() firstPauseMsg = PauseEvent(longpause)
# the 1st potentially-awkward pause is likely to be a beepThreshold-length one # 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: 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 self.promptsData[longpause] = 1
except StretchedTooFar: pass except StretchedTooFar: pass
maxLenOfLesson = o maxLenOfLesson = o
...@@ -246,7 +296,7 @@ class ProgressDatabase(object): ...@@ -246,7 +296,7 @@ class ProgressDatabase(object):
numFailures = 0 ; startTime = time.time() # for not taking too long numFailures = 0 ; startTime = time.time() # for not taking too long
for i in xrange(len(self.data)): for i in xrange(len(self.data)):
if maxNumToAdd>-1 and numberAdded >= maxNumToAdd: break # too many 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] (timesDone,promptFile,zhFile)=self.data[i]
if timesDone < minTimesDone or (maxTimesDone>=0 and timesDone > maxTimesDone): continue # out of range this time 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) if timesDone >= knownThreshold: thisNumToTry = min(random.choice([2,3,4]),numToTry)
...@@ -255,7 +305,7 @@ class ProgressDatabase(object): ...@@ -255,7 +305,7 @@ class ProgressDatabase(object):
# dropping it at random # dropping it at random
self.exclude[i] = 1 # pretend we've done it self.exclude[i] = 1 # pretend we've done it
continue 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 # 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 self.try_add_poem(self.do_as_poem[i]) ; continue
oldPromptsData = self.promptsData.copy() oldPromptsData = self.promptsData.copy()
...@@ -264,8 +314,8 @@ class ProgressDatabase(object): ...@@ -264,8 +314,8 @@ class ProgressDatabase(object):
global earliestAllowedEvent ; earliestAllowedEvent = 0 global earliestAllowedEvent ; earliestAllowedEvent = 0
if not timesDone and type(promptFile)==type([]): 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 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(): for f,t in list(newWordTimes.items()):
if f in promptFile: earliestAllowedEvent = max(earliestAllowedEvent,t) 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) 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) 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. 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): ...@@ -314,7 +364,7 @@ class ProgressDatabase(object):
covered = 0 covered = 0
for timesDone,promptFile,zhFile in self.data: for timesDone,promptFile,zhFile in self.data:
if timesDone: covered += 1 if timesDone: covered += 1
x = (covered > 1000) # arbitrary x = (covered > veryExperiencedThreshold)
self.cached_very_experienced = x self.cached_very_experienced = x
return x return x
def message(self): def message(self):
...@@ -323,8 +373,8 @@ class ProgressDatabase(object): ...@@ -323,8 +373,8 @@ class ProgressDatabase(object):
for timesDone,promptFile,zhFile in self.data: for timesDone,promptFile,zhFile in self.data:
if timesDone: if timesDone:
covered += 1 covered += 1
if zhFile.find(exclude_from_coverage)==-1: actualCovered += 1 if B(zhFile).find(B(exclude_from_coverage))==-1: actualCovered += 1
if zhFile.find(exclude_from_coverage)==-1: actualTotal += 1 if B(zhFile).find(B(exclude_from_coverage))==-1: actualTotal += 1
l=cond(app,localise,lambda x:x) l=cond(app,localise,lambda x:x)
toRet = (l("You have %d words in your collection") % total) toRet = (l("You have %d words in your collection") % total)
if not total==actualTotal: toRet += (" (actually %d)" % actualTotal) if not total==actualTotal: toRet += (" (actually %d)" % actualTotal)
...@@ -339,15 +389,23 @@ def prettyPrintLongList(f,thing,data): ...@@ -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) 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) else: p=pprint.PrettyPrinter(indent=2,width=60,stream=f)
for start in range(0,len(data),step): 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+"+=") if start: f.write(thing+"+=")
else: f.write(thing+"=") else: f.write(thing+"=")
if p: if p:
t = time.time() 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 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 else: # faster version - not quite as pretty
f.write("[") 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") f.write("]\n")
def calcDropLevel(timesDone): def calcDropLevel(timesDone):
...@@ -372,15 +430,23 @@ def cmpfunc(x,y): ...@@ -372,15 +430,23 @@ def cmpfunc(x,y):
if r: return r # skipping the rest if x[0]!=y[0] 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) 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): def my_toString(x):
if type(x)==type([]): return "".join(x) if type(x)==type([]): return B("").join(map(B,x))
else: return x else: return B(x)
x2 = (my_toString(x[1]).replace(os.sep,chr(0)), my_toString(x[2]).replace(os.sep,chr(0))) 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(os.sep,chr(0)), my_toString(y[2]).replace(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) return cmpfunc_test(x2,y2)
def cmpfunc_test(x,y): def cmpfunc_test(x,y):
if x < y: return -1 try:
elif x > y: return 1 if x < y: return -1
else: return 0 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): def denumber_filelists(r,x,y):
if type(x)==type([]): x=map(lambda z:denumber_synth(z),x) if type(x)==type([]): x=map(lambda z:denumber_synth(z),x)
...@@ -389,18 +455,18 @@ def denumber_filelists(r,x,y): ...@@ -389,18 +455,18 @@ def denumber_filelists(r,x,y):
else: y=denumber_synth(y) else: y=denumber_synth(y)
return (r,x,y) return (r,x,y)
def denumber_synth(z,also_norm_extsep=0): def denumber_synth(z,also_norm_extsep=0):
zf = z.find("!synth:") z=B(z) ; zf = z.find(B("!synth:"))
if zf>-1: if zf>=0:
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) z=lower(z[zf:]) # so ignores the priority-number it had (because the vocab.txt file might have been re-organised hence changing all the numbers). Also a .lower() so case changes don't change progress. (Old versions of gradint said .lower() when parsing vocab.txt, but this can cause problems with things like Mc[A-Z].. in English espeak)
if z.endswith(dotwav) or z.endswith(dotmp3): return z[:z.rindex(extsep)] # remove legacy extensions from synth vocab 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("\\","/").replace(".","/") # so compares equally across platforms with os.sep and extsep differences 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 return z
def norm_filelist(x,y): 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 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:denumber_synth(noext(z),1),x)) if type(x)==type([]): x=tuple(map(lambda z,noext=noext:denumber_synth(noext(z),1),x))
else: x=denumber_synth(noext(x),1) else: x=denumber_synth(noext(x),1)
if type(y)==type([]): y=tuple(map(lambda z:denumber_synth(noext(z),1),y)) if type(y)==type([]): y=tuple(map(lambda z,noext=noext:denumber_synth(noext(z),1),y))
else: y=denumber_synth(noext(y),1) else: y=denumber_synth(noext(y),1)
return (x,y) return (x,y)
def mergeProgress(progList,scan): def mergeProgress(progList,scan):
...@@ -426,7 +492,7 @@ def mergeProgress(progList,scan): ...@@ -426,7 +492,7 @@ def mergeProgress(progList,scan):
renames = {} renames = {}
for (_,j,k) in scan: for (_,j,k) in scan:
key = norm_filelist(j,k) 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: # 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) 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]): 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): ...@@ -439,16 +505,16 @@ def mergeProgress(progList,scan):
while ki>lastDirsep and not "0"<=normK[ki]<="9": ki -= 1 while ki>lastDirsep and not "0"<=normK[ki]<="9": ki -= 1
if ki<=lastDirsep: break if ki<=lastDirsep: break
key2 = (key[0][:ki+1]+key[0][key[0].rindex("_"):],key[1][:ki+1]+key[1][key[1].rindex("_"):]) key2 = (key[0][:ki+1]+key[0][key[0].rindex("_"):],key[1][:ki+1]+key[1][key[1].rindex("_"):])
if key2 in proglistDict: if checkIn(key2,proglistDict):
if not key2 in renames: renames[key2] = [] if not checkIn(key2,renames): renames[key2] = []
renames[key2].append((j,k)) renames[key2].append((j,k))
found=1 ; break found=1 ; break
while ki>lastDirsep and "0"<=normK[ki]<="9": ki -= 1 while ki>lastDirsep and "0"<=normK[ki]<="9": ki -= 1
if not found: progList.append((0,j,k)) # new item if not found: progList.append((0,j,k)) # new item
else: progList.append((0,j,k)) # ditto else: progList.append((0,j,k)) # ditto
scanlistDict[key]=1 scanlistDict[key]=1
for k,v in renames.items(): for k,v in list(renames.items()):
if k in scanlistDict or len(v)>1: # can't make sense of this one - just add the new stuff 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)) for jj,kk in v: progList.append((0,jj,kk))
else: progList[proglistDict[k]]=(progList[proglistDict[k]][0],v[0][0],v[0][1]) else: progList[proglistDict[k]]=(progList[proglistDict[k]][0],v[0][0],v[0][1])
# finally, separate off any with non-0 progress that are # finally, separate off any with non-0 progress that are
...@@ -458,7 +524,7 @@ def mergeProgress(progList,scan): ...@@ -458,7 +524,7 @@ def mergeProgress(progList,scan):
n = 0 ; unavailList = [] n = 0 ; unavailList = []
while n<len(progList): while n<len(progList):
i,j,k = progList[n] 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)) unavailList.append((i,j,k))
del progList[n] del progList[n]
else: n += 1 else: n += 1
...@@ -514,11 +580,11 @@ def jitter(list): ...@@ -514,11 +580,11 @@ def jitter(list):
# need to work on them more quickly, and can limit manually # need to work on them more quickly, and can limit manually
limitCounts = {} ; i = 0 ; imax = len(list) limitCounts = {} ; i = 0 ; imax = len(list)
while i < imax: 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" # if not languageof(list[i][2])==secondLanguage: countNo="other-langs"
# else: # else:
countNo = limitedFiles[list[i][-1]] countNo = limitedFiles[list[i][-1]]
if not countNo in limitCounts: limitCounts [countNo] = 0 if not checkIn(countNo,limitCounts): limitCounts [countNo] = 0
limitCounts [countNo] += 1 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) # (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): 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): ...@@ -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) 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 hasPreviousLine[response]=True
poems = [] 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 poemLines = [] ; line = poemFirstLine
poem_is_viable = True poem_is_viable = True
while True: while True:
poemLines.append(line) 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 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] line = nextLineDic[line]
if poem_is_viable: poems.append(poemLines) if poem_is_viable: poems.append(poemLines)
return poems, responseIndex return poems, responseIndex
# This file is part of the source code of # This file is part of the source code of Gradint
# gradint v0.9974 (c) 2002-2011 Silas S. Brown. GPL v3+. # (c) Silas S. Brown.
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or # the Free Software Foundation; either version 3 of the License, or
...@@ -18,7 +18,12 @@ def doOneLesson(dbase): ...@@ -18,7 +18,12 @@ def doOneLesson(dbase):
lesson = dbase.makeLesson() lesson = dbase.makeLesson()
else: else:
soFar = "Re-loading saved lesson, so not scanning collection." 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() else: lesson=pickle.Unpickler(open(saveLesson,'rb')).load()
if app and not dbase: app.setNotFirstTime() if app and not dbase: app.setNotFirstTime()
while 1: while 1:
...@@ -31,24 +36,44 @@ def doOneLesson(dbase): ...@@ -31,24 +36,44 @@ def doOneLesson(dbase):
waitOnMessage(msg+interrupt_instructions()) waitOnMessage(msg+interrupt_instructions())
#time.sleep(5) #time.sleep(5)
time.sleep(2) # less confusing for beginners 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 startFunction: startFunction()
if app: if app:
app.setLabel("Starting lesson") app.setLabel("Starting lesson")
app.cancelling = 0 app.cancelling = 0
lesson.play() 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 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) if cancelledFiles: dbase.savePartial(cancelledFiles)
else: dbase.save() 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 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 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) else: pickle.Pickler(open(saveLesson,"wb"),-1).dump(lesson)
saveLesson = None # so saves only the first when doing multiple lessons saveLesson = None # so saves only the first when doing multiple lessons
if justSaveLesson: break if justSaveLesson: break
if not app and not app==None: break # close box pressed 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 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]) if loadLesson==-1: loadLesson=(fileExists(saveLesson) and time.localtime(os.stat(saveLesson).st_mtime)[:3]==time.localtime()[:3])
def lesson_loop(): def lesson_loop():
...@@ -65,7 +90,7 @@ def lesson_loop(): ...@@ -65,7 +90,7 @@ def lesson_loop():
dbase = ProgressDatabase() dbase = ProgressDatabase()
if not dbase.data: if not dbase.data:
msg = "There are no words to put in the lesson." msg = "There are no words to put in the lesson."
if app or appuifw: if app or appuifw or android:
drop_to_synthloop = False drop_to_synthloop = False
msg = localise(msg)+"\n"+localise("Please add some words first.") msg = localise(msg)+"\n"+localise("Please add some words first.")
else: else:
......
# This file is part of the source code of # This file is part of the source code of Gradint
# gradint v0.9974 (c) 2002-2011 Silas S. Brown. GPL v3+. # (c) Silas S. Brown.
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or # the Free Software Foundation; either version 3 of the License, or
...@@ -17,7 +17,7 @@ def filesToEvents(files,dirBase=None): ...@@ -17,7 +17,7 @@ def filesToEvents(files,dirBase=None):
# (e.g. when learning poetry, may have a composite # (e.g. when learning poetry, may have a composite
# prompt) # prompt)
if not type(files)==type([]): files = [files] 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): class Partials_Synth(Synth):
# text is really a list of lists of filenames # text is really a list of lists of filenames
...@@ -40,11 +40,12 @@ class Partials_Synth(Synth): ...@@ -40,11 +40,12 @@ class Partials_Synth(Synth):
# the first syllable in 1st list can optionally be the header file to use # the first syllable in 1st list can optionally be the header file to use
fname = os.tempnam()+dotwav fname = os.tempnam()+dotwav
o=open(fname,"wb") 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: for phrase in text:
datFileInUse = 0 ; assert type(phrase)==type([]) datFileInUse = 0 ; assert type(phrase)==type([])
for f in phrase: for f in phrase:
if f in audioDataPartials: f = S(f)
if checkIn(f,audioDataPartials):
datFile,offset,size = audioDataPartials[f] datFile,offset,size = audioDataPartials[f]
if not datFileInUse: datFileInUse = open(partialsDirectory+os.sep+datFile,"rb") if not datFileInUse: datFileInUse = open(partialsDirectory+os.sep+datFile,"rb")
datFileInUse.seek(offset) ; o.write(datFileInUse.read(size)) datFileInUse.seek(offset) ; o.write(datFileInUse.read(size))
...@@ -58,16 +59,20 @@ class Partials_Synth(Synth): ...@@ -58,16 +59,20 @@ class Partials_Synth(Synth):
def fileToEvent(fname,dirBase=None): def fileToEvent(fname,dirBase=None):
if dirBase==None: dirBase=samplesDirectory if dirBase==None: dirBase=samplesDirectory
if dirBase: dirBase += os.sep dirBase,fname = B(dirBase),B(fname)
orig_fname = fname if dirBase: dirBase += B(os.sep)
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:] orig_DB,orig_fname = dirBase,fname
if "_" in fname: lang = languageof(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 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 variantFiles[dirBase+fname]=variantFiles[dirBase+fname][1:]+[variantFiles[dirBase+fname][0]] # cycle through the random order of variants
fname=variantFiles[dirBase+fname][0] fname=B(variantFiles[dirBase+fname][0])
if fname.lower().endswith(dottxt) and "_" in fname: fname = "!synth:"+u8strip(read(dirBase+fname)).strip(wsp)+'_'+lang if fname.lower().endswith(B(dottxt)) and B("_") in fname:
if fname.find("!synth:")>-1: 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) s = synthcache_lookup(fname)
if type(s)==type([]): # trying to synth from partials 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: 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): ...@@ -88,9 +93,11 @@ def fileToEvent(fname,dirBase=None):
e.append(Event(betweenPhrasePause)) e.append(Event(betweenPhrasePause))
e=CompositeEvent(e[:-1]) # omit trailing pause 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) 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 elif s: e=SampleEvent(synthCache+os.sep+S(s)) # single file in synth cache
else: e=synth_event(languageof(fname),textof(fname)) else:
e.is_prompt=(dirBase==promptsDirectory+os.sep) 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) else: e=SampleEvent(dirBase+fname)
e.setOnLeaves('wordToCancel',orig_fname) e.setOnLeaves('wordToCancel',orig_fname)
return e return e
...@@ -98,27 +105,30 @@ def fileToEvent(fname,dirBase=None): ...@@ -98,27 +105,30 @@ def fileToEvent(fname,dirBase=None):
transTbl = "TRANS"+extsep+"TBL" transTbl = "TRANS"+extsep+"TBL"
if mp3web: # synth-cache must exist if mp3web: # synth-cache must exist
if not synthCache: synthCache = "synth-cache" 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: if synthCache:
# this listdir() call can take ages on rpcemu if it's large # this listdir() call can take ages on rpcemu if it's large
if riscos_sound: show_info("Reading synthCache... ") 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 = [] except: synthCache_contents = synthCache = []
for i in synthCache_contents: for i in synthCache_contents:
if i.upper()==transTbl: # in case it's a different case if i.upper()==B(transTbl): # in case it's a different case
transTbl=i ; break transTbl=S(i) ; break
synthCache_contents = list2dict(synthCache_contents) # NOT 2set, as the GUI can delete things from it synthCache_contents = list2dict(synthCache_contents) # NOT 2set, as the GUI can delete things from it
if riscos_sound: show_info("done\n") if riscos_sound: show_info("done\n")
synthCache_transtbl = {} 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 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(): for l in open(synthCache+os.sep+transTbl,'rb').readlines():
v,k = l.strip(wsp).split(None,1) v,k = bwspstrip(l).split(None,1)
if v in ensure_nodups: del synthCache_transtbl[ensure_nodups[v]] if checkIn(v,ensure_nodups): del synthCache_transtbl[ensure_nodups[v]]
ensure_nodups[v]=k ; synthCache_transtbl[k]=v ensure_nodups[v]=k ; synthCache_transtbl[k]=v
del ensure_nodups 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 last_partials_transliteration = None
synth_partials_cache = {} ; scl_disable_recursion = 0 synth_partials_cache = {} ; scl_disable_recursion = 0
def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=None): def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=None):
...@@ -126,8 +136,9 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non ...@@ -126,8 +136,9 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
if dirBase==None: dirBase=samplesDirectory if dirBase==None: dirBase=samplesDirectory
if dirBase: dirBase += os.sep if dirBase: dirBase += os.sep
if not lang: lang = languageof(fname) if not lang: lang = languageof(fname)
if fname.lower().endswith(dottxt): fname = B(fname)
try: fname = fname[:fname.rfind("_")]+"!synth:"+u8strip(read(dirBase+fname)).strip(wsp)+"_"+lang 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 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) text = textof(fname)
useSporadic = -1 # undecided (no point accumulating counters for potentially-unbounded input) useSporadic = -1 # undecided (no point accumulating counters for potentially-unbounded input)
...@@ -136,10 +147,10 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non ...@@ -136,10 +147,10 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
if synthCache: if synthCache:
for init in "_","": for init in "_","":
for ext in "wav","mp3": for ext in "wav","mp3":
k=init+text.lower()+"_"+lang+extsep+ext k=B(init)+text.lower()+B("_"+lang+extsep+ext)
s=synthCache_transtbl.get(k,k) s=B(synthCache_transtbl.get(k,k))
if s in synthCache_contents: ret=s if checkIn(s,synthCache_contents): ret=s
elif s.lower().endswith(dotwav) and s[:-len(dotwav)]+dotmp3 in synthCache_contents: ret=s[:-len(dotwav)]+dotmp3 elif s.lower().endswith(B(dotwav)) and checkIn(s[:-len(dotwav)]+B(dotmp3),synthCache_contents): ret=s[:-len(dotwav)]+B(dotmp3)
else: ret=0 else: ret=0
if ret: if ret:
if justQueryCache==1: ret=(k,ret) if justQueryCache==1: ret=(k,ret)
...@@ -149,8 +160,8 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non ...@@ -149,8 +160,8 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
elif tryHarder: tryHarder=ret elif tryHarder: tryHarder=ret
else: return ret else: return ret
if justQueryCache==1: return 0,0 if justQueryCache==1: return 0,0
if lang not in synth_partials_voices: l,translit=None,None # don't bother trying to transliterate here if there aren't even any partials for that language 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 (lang,text) not in synth_partials_cache: elif not checkIn((lang,text),synth_partials_cache):
# See if we can transliterate the text first. # See if we can transliterate the text first.
synth,translit = get_synth_if_possible(lang,0,to_transliterate=True),None synth,translit = get_synth_if_possible(lang,0,to_transliterate=True),None
if espeak_language_aliases.get(lang,lang) in ["zhy","zh-yue"]: if espeak_language_aliases.get(lang,lang) in ["zhy","zh-yue"]:
...@@ -163,17 +174,17 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non ...@@ -163,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 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()) 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 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 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?) 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.) 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 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') 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=[] l=[]
scl_disable_recursion = 1 scl_disable_recursion = 1
for phrase in filter(lambda x:x,t2.split(chr(0))): 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 if type(ll)==type([]): l += ll
else: l.append(synth_event(lang,phrase,0)) else: l.append(synth_event(lang,phrase,0))
scl_disable_recursion = 0 scl_disable_recursion = 0
...@@ -188,7 +199,7 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non ...@@ -188,7 +199,7 @@ def synthcache_lookup(fname,dirBase=None,printErrors=0,justQueryCache=0,lang=Non
if l: return l if l: return l
if tryHarder and not tryHarder==True: return tryHarder if tryHarder and not tryHarder==True: return tryHarder
if printErrors and synthCache and not (app and winsound): 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]+"..." if len(r)>100: r=r[:100]+"..."
global NICcount global NICcount
try: NICcount += 1 try: NICcount += 1
...@@ -205,9 +216,10 @@ def can_be_synthesized(fname,dirBase=None,lang=None): ...@@ -205,9 +216,10 @@ def can_be_synthesized(fname,dirBase=None,lang=None):
else: return get_synth_if_possible(lang) # and this time print the warning else: return get_synth_if_possible(lang) # and this time print the warning
def stripPuncEtc(text): 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 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,"") text = B(text)
for t in ".!?:;": text=text.replace(t,",") for t in " -_'\"()[]": text=text.replace(B(t),B(""))
return filter(lambda x:x,text.split(",")) 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"]: for zipToCheck in ["yali-voice","yali-lower","cameron-voice"]:
if riscos_sound: if riscos_sound:
...@@ -215,7 +227,10 @@ for zipToCheck in ["yali-voice","yali-lower","cameron-voice"]: ...@@ -215,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) 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]: for d in [os.getcwd()+cwd_addSep,".."+os.sep,samplesDirectory+os.sep]:
f=d+zipToCheck+".exe" 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) # 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 non_normal_filenames = {} ; using_unicode_filenames=0
...@@ -227,7 +242,7 @@ def filename2unicode(f): ...@@ -227,7 +242,7 @@ def filename2unicode(f):
def u8_or_raw(s): def u8_or_raw(s):
try: return unicode(s,"utf-8") 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) 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") try: return unicode(f.replace("_u","\\u").replace("_U","\\u"),"unicode_escape")
except UnicodeDecodeError: # oops, need to be more careful except UnicodeDecodeError: # oops, need to be more careful
ret = [] ret = []
...@@ -248,7 +263,7 @@ def filename2unicode(f): ...@@ -248,7 +263,7 @@ def filename2unicode(f):
return u return u
def unicode2filename(u): def unicode2filename(u):
if using_unicode_filenames: return 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") f=u.encode("unicode_escape").replace("\\u","_u")
for unsafe_char in "?+*<=": f=f.replace(unsafe_char,"_u%04x" % ord(unsafe_char)) for unsafe_char in "?+*<=": f=f.replace(unsafe_char,"_u%04x" % ord(unsafe_char))
return f return f
...@@ -263,7 +278,7 @@ if partialsDirectory and isDirectory(partialsDirectory): ...@@ -263,7 +278,7 @@ if partialsDirectory and isDirectory(partialsDirectory):
try: try:
ela = espeak_language_aliases ela = espeak_language_aliases
format,values = pickle.Unpickler(open(partials_cache_file,"rb")).load() 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,[] if not (ela==espeak_language_aliases and dirsToStat[0][0]==partialsDirectory): espeak_language_aliases,dirsToStat=ela,[]
del ela,format,values 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) except MemoryError: raise # has been known on winCEsound when we're a library module (so previous memory check didn't happen)
...@@ -277,7 +292,7 @@ if partialsDirectory and isDirectory(partialsDirectory): ...@@ -277,7 +292,7 @@ if partialsDirectory and isDirectory(partialsDirectory):
langs = os.listdir(partialsDirectory) langs = os.listdir(partialsDirectory)
dirsToStat.append((partialsDirectory,os.stat(partialsDirectory))) dirsToStat.append((partialsDirectory,os.stat(partialsDirectory)))
audioDataPartials = {} ; synth_partials_voices = {} audioDataPartials = {} ; synth_partials_voices = {}
partials_raw_mode = "header"+dotwav in langs partials_raw_mode = checkIn("header"+dotwav,langs)
for l in langs: for l in langs:
try: voices = os.listdir(partialsDirectory+os.sep+l) try: voices = os.listdir(partialsDirectory+os.sep+l)
except: voices = [] except: voices = []
...@@ -286,7 +301,7 @@ if partialsDirectory and isDirectory(partialsDirectory): ...@@ -286,7 +301,7 @@ if partialsDirectory and isDirectory(partialsDirectory):
for v in voices: for v in voices:
if "-" in v and v[:v.index("-")] in voices: if "-" in v and v[:v.index("-")] in voices:
suffix=v[v.index("-"):] 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 start,mid,end = [],[],[] ; flags=0
try: files = os.listdir(partialsDirectory+os.sep+l+os.sep+v) try: files = os.listdir(partialsDirectory+os.sep+l+os.sep+v)
except: files = [] except: files = []
...@@ -306,10 +321,11 @@ if partialsDirectory and isDirectory(partialsDirectory): ...@@ -306,10 +321,11 @@ if partialsDirectory and isDirectory(partialsDirectory):
while True: while True:
fftell = ff.tell() fftell = ff.tell()
char = ff.read(1) char = ff.read(1)
if not "0"<=char<="9": break if not B("0")<=char<=B("9"): break
size,fname = (char+ff.readline(256)).strip(wsp).split(None,1) size,fname = bwspstrip(char+ff.readline(256)).split(None,1)
try: size=int(size) try: size=int(size)
except: break # binary just happened to start with "0"-"9" except: break # binary just happened to start with "0"-"9"
fname = S(fname)
addFile(fname) addFile(fname)
amend.append(l+os.sep+v+os.sep+fname) amend.append(l+os.sep+v+os.sep+fname)
audioDataPartials[l+os.sep+v+os.sep+fname] = (f,offset,size) audioDataPartials[l+os.sep+v+os.sep+fname] = (f,offset,size)
...@@ -319,41 +335,41 @@ if partialsDirectory and isDirectory(partialsDirectory): ...@@ -319,41 +335,41 @@ if partialsDirectory and isDirectory(partialsDirectory):
if partials_raw_mode: if partials_raw_mode:
if not f.endswith(extsep+"raw"): return if not f.endswith(extsep+"raw"): return
elif not f.endswith(dotwav) or f.endswith(dotmp3): 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' if f.find("-s")>=0 or f.find("-i")>=0: start.append(f) # 'start' or 'initial'
elif not "-" in f or f.find('-m')>-1: mid.append(f) elif not "-" in f or f.find('-m')>=0: mid.append(f)
elif f.find('-e')>-1 or f.find('-f')>-1: end.append(f) # 'end' or 'finish' elif f.find('-e')>=0 or f.find('-f')>=0: end.append(f) # 'end' or 'finish'
for f in files: addFile(f) for f in files: addFile(f)
def byReverseLength(a,b): return len(b)-len(a) 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. 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 {} if not l: return {}
l2 = [] ; kLen = len(l[0]) l2 = [] ; kLen = len(l[0])
for i in l: for i in l:
if "-" in i: key=i[:i.index("-")] if "-" in i: key=i[:i.index("-")]
else: key=i[:i.rindex(extsep)] 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') key = filename2unicode(key).encode('utf-8')
l2.append((key,i)) l2.append((key,i))
kLen=min(kLen,len(key)) kLen=min(kLen,len(key))
l = {} l = {}
for k,i in l2: 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)) l[k[:kLen]].append((k,i))
return l return l
thisLangVoices.append((v,toDict(start),toDict(mid),toDict(end),flags)) thisLangVoices.append((v,toDict(start),toDict(mid),toDict(end),flags))
synth_partials_voices[l] = thisLangVoices 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 riscos_sound or winCEsound: show_info("done\n")
if pickle: if pickle:
try: pickle.Pickler(open(partials_cache_file,"wb"),-1).dump((partialsCacheFormat,eval(partialsCacheFormat))) 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 IOError: pass # ignore write errors as it's only a cache
except OSError: pass except OSError: pass
if partials_raw_mode: if partials_raw_mode:
(wtype,wrate,wchannels,wframes,wbits) = sndhdr.what(partialsDirectory+os.sep+"header"+dotwav) (wtype,wrate,wchannels,wframes,wbits) = swhat(partialsDirectory+os.sep+"header"+dotwav)
partials_raw_0bytes = int(betweenPhrasePause*wrate)*wchannels*(wbits/8) partials_raw_0bytes = int(betweenPhrasePause*wrate)*wchannels*int(wbits/8)
else: synth_partials_voices,partials_raw_mode = {},None 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): def partials_langname(lang):
lang = espeak_language_aliases.get(lang,lang) lang = espeak_language_aliases.get(lang,lang)
...@@ -362,29 +378,30 @@ def partials_langname(lang): ...@@ -362,29 +378,30 @@ def partials_langname(lang):
def synth_from_partials(text,lang,voice=None,isStart=1): def synth_from_partials(text,lang,voice=None,isStart=1):
lang = partials_langname(lang) 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) if lang=="zh": # hack for Mandarin - higher tone 5 after a tone 3 (and ma5 after 4 or 5 also)
lastNum = None lastNum = None
for i in range(len(text)): for i in range(len(text)):
if text[i] in "123456": if text[i:i+1] in B("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]==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) # see if we got a "tone 6" (higher tone 5)
# don't worry too much if we haven't # 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 if r: return r
else: break else: break
elif lastNum: break # don't look beyond 1st 2 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 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 needCalibrated=False
if lang=="zh": # hack for Mandarin - avoid consecutive 1st tones on non-calibrated voices 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) # (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 lastNum=None
for c in text: for i in xrange(len(text)):
if c=="1" and lastNum=="1": 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) 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 # end of hack for Mandarin
vTry = synth_partials_voices[lang] vTry = synth_partials_voices[lang]
if voiceOption: if voiceOption:
...@@ -398,12 +415,13 @@ def synth_from_partials(text,lang,voice=None,isStart=1): ...@@ -398,12 +415,13 @@ def synth_from_partials(text,lang,voice=None,isStart=1):
r = synth_from_partials(text,lang,v) r = synth_from_partials(text,lang,v)
if r: if r:
if partials_raw_mode and v[-1]&2: r.insert(0,"header"+dotwav) 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 return None
dir, start, mid, end, flags = voice dir, start, mid, end, flags = voice
def lookup_dic(text,dic): def lookup_dic(text,dic):
text = S(text)
if dic: 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 if text.startswith(k): return k,v
return None,None return None,None
if not text: return [] # (shouldn't happen) if not text: return [] # (shouldn't happen)
...@@ -434,12 +452,12 @@ def optimise_partial_playing(ce): ...@@ -434,12 +452,12 @@ def optimise_partial_playing(ce):
for e in ce.eventList[1:]: for e in ce.eventList[1:]:
if not soundFileType(e.file)==fileType: return ce # must be all the same type for this optimisation if not soundFileType(e.file)==fileType: return ce # must be all the same type for this optimisation
s = None 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 # 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) 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(madplay_program+' -q -A $Vol$'+''.join(map(lambda x:' "'+x.file+'"', ce.eventList)),True) else: s=ShellEvent(mp3Player+' -q -A $Vol$'+''.join(map(lambda x:' "'+x.file+'"', ce.eventList)),True)
s.VolReplace="soundVolume_dB" 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) # 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) # (don't try to do that if different formats - the low-end hardware may not take the rate conversion)
ok=gotSox ok=gotSox
...@@ -449,21 +467,25 @@ def optimise_partial_playing(ce): ...@@ -449,21 +467,25 @@ def optimise_partial_playing(ce):
if not simplified_header(e.file)==format: if not simplified_header(e.file)==format:
ok=False ; break ok=False ; break
if ok: 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" 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: if s:
s.length = ce.length s.length = ce.length
s.equivalent_event_list = ce.eventList s.equivalent_event_list = ce.eventList
return s return s
else: return ce # can't figure out an optimisation in these circumstances else: return ce # can't figure out an optimisation in these circumstances
def simplified_header(fname): 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 # ignore num frames i.e. h[3], just compare formats
if h: return h[:3]+h[4:] if h: return h[:3]+h[4:]
def optimise_partial_playing_list(ceList): 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. # 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 format = None ; l = [] ; theLen = 0
for ce in ceList: for ce in ceList:
for e in ce.eventList: for e in ce.eventList:
......
# This file is part of the source code of # This file is part of the source code of Gradint
# gradint v0.9974 (c) 2002-2011 Silas S. Brown. GPL v3+. # (c) Silas S. Brown.
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or # the Free Software Foundation; either version 3 of the License, or
...@@ -27,7 +27,7 @@ def play(event): ...@@ -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) 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 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,"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" play_error = "firstTime"
while play_error and time.time()<=timeout_time: # use <= rather than < in case we have only 1sec precision while play_error and time.time()<=timeout_time: # use <= rather than < in case we have only 1sec precision
if not play_error=="firstTime": if not play_error=="firstTime":
...@@ -41,24 +41,26 @@ def play(event): ...@@ -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,"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 if hasattr(event,"endseq"): wordsLeft[event.endseq] -= 1
del copy_of_runner_events[0] del copy_of_runner_events[0]
if soundCollector: doLabel("%d%% completed" % (soundCollector.tell()*100/lessonLen)) if soundCollector: return 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), + say how many already cancelled (for diagnosing results of interruptions caused by phone events etc on those platforms)
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]
new,old=wordsLeft[True],wordsLeft[False] if new: line2="%d new " % new
if new: line2="%d new " % new if old:
if old: if line2: line2 += ("+ %d old " % old)
if line2: line2 += ("+ %d old " % old) else: line2="%d old words " % old
else: line2="%d old words " % old elif new: line2 += "words "
elif new: line2 += "words " if line2:
if line2: line2=cond(app or appuifw,"\n",", ")+line2+"remain" line2=cond(app or appuifw or android,"\n",", ")+line2+"remain"
if not lessonStartTime: lessonStartTime = time.time() # the actual time of the FIRST event (don't set it before as there may be delays). (we're setting this at the END of the 1st event - the extra margin should be ok, and can help with start-of-lesson problems with slow disks.) if cancelledFiles: line2 += "\n("+str(len(cancelledFiles))+" cancelled)"
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 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): def doLabel(labelText):
labelText = ensure_unicode(labelText) labelText = ensure_unicode(labelText)
if app: app.setLabel(labelText) if app: app.setLabel(labelText)
elif appuifw: elif appuifw:
t=appuifw.Text() ; t.add(labelText) t=appuifw.Text() ; t.add(labelText)
appuifw.app.body = t 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) elif not (riscos_sound or winCEsound): # (we don't have a way of updating a clock or progress indicator on those)
global doLabelLastLen global doLabelLastLen
try: doLabelLastLen try: doLabelLastLen
...@@ -93,86 +95,107 @@ if ask_teacherMode: ...@@ -93,86 +95,107 @@ if ask_teacherMode:
old_mysleep(1) old_mysleep(1)
def maybe_unicode(label): def maybe_unicode(label):
if app or appuifw: if app or appuifw or android:
try: return unicode(label,'utf-8') try: return unicode(label,'utf-8')
except: return label # ?? except: return label # e.g. Python 3
else: return repr(label) else: return repr(label)
madplay_program = 0 madplay_path = None
if (winsound or mingw32) and fileExists("madplay.exe"): madplay_program = "madplay.exe" 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"): elif unix and hasattr(os,"popen"):
madplay_program = os.popen("PATH=$PATH:. which madplay 2>/dev/null").read().strip(wsp) madplay_path = os.popen("PATH=$PATH:. which madplay 2>/dev/null").read()
if not fileExists(cond(cygwin,madplay_program+".exe",madplay_program)): madplay_program=0 # in case of a Unix 'which' returning error on stdout try: madplay_path = wspstrip(madplay_path)
if madplay_program and not winsound and not mingw32: madplay_program='"'+madplay_program+'"' # in case there's spaces etc in the 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): def intor0(v):
try: return int(v) try: return int(v)
except ValueError: return 0 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_effect=""
sox_8bit, sox_16bit, sox_ignoreLen = "-b", "-w", "" 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?) # 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)) soundVolume_dB = math.log(soundVolume)*(-6/math.log(0.5))
if unix: if unix:
if macsound: got_qtplay = 1 # should be bundled if macsound: got_afplay = got_program("afplay") # 10.5+, use in preference to the bundled qtplay which requires PowerPC or Rosetta
sox_formats=os.popen("sox --help 2>&1").read() # NOT .lower() yet def sox_check():
if sox_formats.lower().startswith("sox: sox v"): global sox_8bit, sox_16bit, sox_ignoreLen, sox_signed, sox_formats
soxMaj = intor0(sox_formats[10:sox_formats.index('.')]) sox_formats=os.popen("sox --help 2>&1").read() # NOT .lower() yet
if soxMaj>=14: sf2 = ' '.join(sox_formats.lower().split())
sox_8bit, sox_16bit = "-1", "-2" # see comment above if sf2.startswith("sox: sox v"):
if soxMaj==14 and sox_formats[13]<'3': pass if sf2[10]==' ': soxMaj=15 # guess (broken HomeBrew install)
else: sox_ignoreLen = "|sox --ignore-length -t wav - -t wav - 2>/dev/null" else: soxMaj = intor0(sf2[10:sf2.index('.')])
if sox_formats.lower().find("wav")>-1: gotSox=1 else: soxMaj=0
else: if soxMaj>=14:
gotSox=0 if soxMaj==14 and sf2[13]<'3': pass
if got_program("sox"): else: sox_ignoreLen = "|sox --ignore-length -t wav - -t wav - 2>/dev/null"
if macsound and sox_formats.find("Rosetta")>-1: if soxMaj==14 and sf2[13]<'4': sox_8bit, sox_16bit = "-1", "-2" # see comment above
try: u""+sox_formats 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)
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 return sf2.find("wav")>=0, sf2.find("mp3")>=0
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) gotSox,soxMp3 = sox_check()
# TODO document a way to install Rosetta without the CD? (e.g. downloading a PowerPC-only Abiword binary seems to do it) if macsound:
got_qtplay = 0 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)
else: show_warning("SOX found, but it can't handle WAV files. Ubuntu users please install libsox-fmt-all.") 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") else: gotSox = got_program("sox")
wavPlayer_override = not (not wavPlayer)
if winsound or mingw32: 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 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? # 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: 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 not soundVolume==1: sox_effect=" vol "+str(soundVolume)
if sox_effect and not gotSox: if sox_effect and not gotSox:
show_warning("Warning: trying to adjust soundVolume when 'sox' is not on the system might not work") 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) # (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: if not oss_sound_device:
dsps_to_check = [] dsps_to_check = []
if sox_formats.find("ossdsp")>-1: dsps_to_check += ["/dev/sound/dsp","/dev/dsp"] if sox_formats.find("ossdsp")>=0:
if sox_formats.find("sunau")>-1: dsps_to_check += ["/dev/audio"] 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: for dsp in dsps_to_check:
if fileExists_stat(dsp): if fileExists_stat(dsp):
oss_sound_device = 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 break
if sox_formats.find("-q")>-1: sox_type="-q "+sox_type if sox_formats.find("-q")>=0: sox_type="-q "+sox_type
# Try to find playProgram (and maybe mpg123, for use if no madplay or mp3-playing playProgram) if not wavPlayer:
if oss_sound_device and not cygwin and gotSox: playProgram = "sox" 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) 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 wavPlayer = "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 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"): 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 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: 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) 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: for otherProgram in otherPrograms:
if got_program(otherProgram): if got_program(otherProgram):
playProgram = otherProgram wavPlayer = otherProgram
break break
if not cygwin and not madplay_program: if not cygwin and not mp3Player:
for mpg in ["mpg123","mpg321","mad123","mplayer"]: for mpg in ["mpg123","mpg321","mad123","mplayer"]:
if got_program(mpg): if got_program(mpg):
mpg123 = mpg ; break mp3Player = 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","")) 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 = ((playProgram or winsound or riscos_sound or mingw32) and not (mpg123 or gotSox or madplay_program)) may_need_mp3_warning = ((wavPlayer or winsound or riscos_sound or mingw32) and not (mp3Player or gotSox))
def maybe_warn_mp3(): def maybe_warn_mp3():
global may_need_mp3_warning global may_need_mp3_warning
if may_need_mp3_warning: if may_need_mp3_warning:
...@@ -185,7 +208,7 @@ sox_same_endian = sox_little_endian = "" ...@@ -185,7 +208,7 @@ sox_same_endian = sox_little_endian = ""
if gotSox and unix: if gotSox and unix:
# should only have to run this test if macsound (don't bother on NSLU2's etc): # 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) # (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" sox_little_endian = " -x"
if not big_endian: sox_same_endian = " -x" if not big_endian: sox_same_endian = " -x"
elif big_endian: sox_little_endian = " -x" elif big_endian: sox_little_endian = " -x"
...@@ -194,35 +217,47 @@ def changeToDirOf(file,winsound_also=0): ...@@ -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) # 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). # 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) # 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 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 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: elif (cygwin or ((winsound or mingw32) and winsound_also)) and os.sep in file:
os.chdir(file[:file.rfind(os.sep)]) os.chdir(file[:file.rfind(os.sep)])
return file[file.rfind(os.sep)+1:] 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): 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 # 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 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)
if unix and ';' in cmd: cmd='/bin/bash -c "'+cmd.replace('\\','\\\\').replace('"','\\"')+'"' # not /bin/sh if it's complex 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) try: r=os.popen(cmd)
except: return os.system(cmd) # too many file descriptors open or something except: return os.system(cmd) # too many file descriptors open or something
r.read() ; return r.close() r.read() ; return r.close()
signal=0
if unix: 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() os.setpgrp()
import signal import signal
def siggrp(sigNo,*args): def siggrp(sigNo,*args):
os.killpg(os.getpgrp(),sigNo) signal.signal(sigNo,signal.SIG_IGN)
os.abort() # in case still here os.killpg(os.getpgrp(),sigNo) # players etc
raise KeyboardInterrupt # clean up, rm tempfiles etc
signal.signal(signal.SIGTERM,siggrp) signal.signal(signal.SIGTERM,siggrp)
else: signal=0 except: pass
# Event(len) gives a pause of that length # Event(len) gives a pause of that length
# SampleEvent extends this to actually play something: # SampleEvent extends this to actually play something:
def soundFileType(file): 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" else: return "wav"
def lessonIsTight(): return maxLenOfLesson <= 10*60 * min(1.8,max(1,maxNewWords/5.0)) # ? def lessonIsTight(): return maxLenOfLesson <= 10*60 * min(1.8,max(1,maxNewWords/5.0)) # ?
...@@ -237,8 +272,8 @@ class SampleEvent(Event): ...@@ -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) 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) Event.__init__(self,approxLen)
def __repr__(self): def __repr__(self):
if use_unicode_filenames: return self.file.encode('utf-8') if use_unicode_filenames: return self.file.encode('utf-8') # winCEsound, will be Python 2
else: return self.file else: return S(self.file)
def __del__(self): def __del__(self):
if hasattr(self,"isTemp"): if hasattr(self,"isTemp"):
import time,os # in case gc'd import time,os # in case gc'd
...@@ -246,65 +281,80 @@ class SampleEvent(Event): ...@@ -246,65 +281,80 @@ class SampleEvent(Event):
try: return os.unlink(self.file) try: return os.unlink(self.file)
except: time.sleep(0.2) # may have taken extra time for the player to load 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 ?? 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 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) fileType=soundFileType(self.file)
if soundCollector: soundCollector.addFile(self.file,self.exactLen) if soundCollector: soundCollector.addFile(self.file,self.exactLen)
elif appuifw: elif appuifw:
fname = self.file fname = self.file
if not fname[1]==":": fname=os.getcwd()+cwd_addSep+fname # must be full drive:\path if not B(fname[1:2])==B(":"): fname=B(os.getcwd()+cwd_addSep)+B(fname) # must be full drive:\path
sound = audio.Sound.open(ensure_unicode(fname)) sound = audio.Sound.open(ensure_unicode(fname))
sound.play() sound.play()
try: time.sleep(self.length) # TODO or exactLen? try: time.sleep(self.length) # TODO or exactLen?
finally: sound.stop() finally: sound.stop()
sound.close() # (probably not worth keeping it open for repeats - there may be a limit to how many can be open) sound.close() # (probably not worth keeping it open for repeats - there may be a limit to how many can be open)
return 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() 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) os.chdir(oldcwd)
return play_error return play_error
elif winCEsound and fileType=="mp3": elif winCEsound and fileType=="mp3":
# we can handle MP3 on WinCE by opening in Media Player. Too bad it ignores requests to run minimized. # we can handle MP3 on WinCE by opening in Media Player. Too bad it ignores requests to run minimized.
fname = self.file fname = self.file
if not fname[0]=="\\": fname=os.getcwd()+cwd_addSep+fname # must be full path 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=u""+fname))) r=not ctypes.cdll.coredll.ShellExecuteEx(ctypes.byref(ShellExecuteInfo(60,File=ensure_unicode(fname))))
time.sleep(self.length) # exactLen may not be enough 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) if fileType=="mp3": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file else: file=self.file
try: try:
if winsound: winsound.PlaySound(file,winsound.SND_FILENAME) if winsound: winsound.PlaySound(file,winsound.SND_FILENAME)
else: # winCEsound else: # winCEsound
fname = self.file fname = self.file
if not fname[0]=="\\": fname=os.getcwd()+cwd_addSep+fname # must be full path if not B(fname[0])==B("\\"): fname=os.getcwd()+cwd_addSep+fname # must be full path
ctypes.cdll.coredll.sndPlaySoundW(u""+fname,1) # 0=sync 1=async 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. time.sleep(self.exactLen) # if async. Async seems to be better at avoiding crashes on some handhelds.
except RuntimeError: return 1 except RuntimeError: return 1
elif macsound and got_qtplay: elif macsound:
try: unicode(self.file,"ascii") if got_afplay: player="afplay"
except UnicodeDecodeError: # qtplay can't always handle non-ASCII else: player="qtplay"
t=os.tempnam()+self.file[self.file.rindex(extsep):] try: unicode(self.file,"ascii")
open(t,"w").write(open(self.file).read()) except UnicodeDecodeError: # Mac command line can't always handle non-ASCII
ret=system("qtplay \"%s\"" % (t,)) t=os.tempnam()+self.file[self.file.rindex(extsep):]
os.remove(t) write(t,open(self.file).read())
return ret ret=system(player+" \"%s\"" % (t,))
return system("qtplay \"%s\"" % (self.file,)) os.remove(t)
return ret
return system(player+" \"%s\"" % (S(self.file),))
elif riscos_sound: 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 else: file=self.file
system("PlayIt_Play \"%s\"" % (file,)) global playit_loaded
elif playProgram.find('sndrec32')>-1: 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) if fileType=="mp3": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file else: file=self.file
oldDir = os.getcwd() oldDir = os.getcwd()
t=time.time() t=time.time()
os.system(playProgram+' "'+changeToDirOf(file)+'"') # don't need to call our version of system() here os.system(wavPlayer+' "'+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.) 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) 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 fileType=="mp3" and mp3Player and not sox_effect and not (wavPlayer=="aplay" and mp3Player==madplay_path): return system(mp3Player+' "'+S(self.file)+'"')
elif playProgram=="sox": elif wavPlayer=="sox" and (soxMp3 or not fileType=="mp3"):
# To make it more difficult: # 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 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 | .. # sox 14.0 on Cygwin - bug when input is from redirection, unless using cat | ..
...@@ -313,7 +363,7 @@ class SampleEvent(Event): ...@@ -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) # 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 # + 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() 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 if play_error: return play_error
else: else:
# no error, but did it take long enough? # no error, but did it take long enough?
...@@ -322,18 +372,20 @@ class SampleEvent(Event): ...@@ -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 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 if not app: show_info("play didn't take long enough - maybe ") # .. problem playing sound
return 1 return 1
elif playProgram=="aplay" and ((not fileType=="mp3") or madplay_program or gotSox): elif wavPlayer=="aplay" and ((not fileType=="mp3") or madplay_path 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) 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 "'+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 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)) # (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 # 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 # 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 fileType=="mp3" and mp3Player and not sox_effect: return system(mp3Player+' "'+S(self.file)+'"')
elif playProgram: elif wavPlayer:
if fileType=="mp3" and not playProgram=="mplayer": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file) if fileType=="mp3" and not wavPlayer=="mplayer": file=theMp3FileCache.decode_mp3_to_tmpfile(self.file)
else: file=self.file else: file=S(self.file)
return system(playProgram+' "'+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') else: show_warning("Don't know how to play \""+self.file+'" on this system')
br_tab=[(0 , 0 , 0 , 0 , 0), br_tab=[(0 , 0 , 0 , 0 , 0),
...@@ -356,19 +408,25 @@ def rough_guess_mp3_length(fname): ...@@ -356,19 +408,25 @@ def rough_guess_mp3_length(fname):
try: try:
maybe_warn_mp3() # in case there's no mp3 player maybe_warn_mp3() # in case there's no mp3 player
# (NB this is only a rough guess because it doesn't support VBR # (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. # and doesn't even check all sync bits. It should be fairly quick though.)
# It should be fairly quick though.) o = open(fname,"rb") ; i = -1
head=open(fname).read(128) while True:
i=head.find('\xFF') head=o.read(512)
b=ord(head[i+1]) 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) layer = 4-((b&6)>>1)
if b&24 == 24: # bits are 11 - MPEG version is 1 if b&24 == 24: # bits are 11 - MPEG version is 1
column = layer-1 # MPEG 1 layer 1, 2 or 3 column = layer-1 # MPEG 1 layer 1, 2 or 3
elif layer==1: column = 3 # MPEG 2+ layer 1 elif layer==1: column = 3 # MPEG 2+ layer 1
else: column = 4 # MPEG 2+ layer 2+ 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 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)) except IndexError: raise Exception("Invalid MP3 header in file "+repr(fname))
def filelen(fname): def filelen(fname):
...@@ -377,15 +435,30 @@ def filelen(fname): ...@@ -377,15 +435,30 @@ def filelen(fname):
return fileLen return fileLen
def lengthOfSound(file): 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) 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): def pcmlen(file):
header = sndhdr.what(file) header = swhat(file)
if not header: raise IOError("Problem opening file '%s'" % (file,))
(wtype,wrate,wchannels,wframes,wbits) = header (wtype,wrate,wchannels,wframes,wbits) = header
divisor = wrate*wchannels*wbits/8 # do NOT optimise with (wbits>>3), because wbits could be 4 if android:
if not divisor: raise IOError("Cannot parse sample format of '%s'" % (file,)) 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 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): ...@@ -394,27 +467,32 @@ class SoundCollector(object):
def __init__(self): def __init__(self):
self.rate = 44100 # so ok for oggenc etc self.rate = 44100 # so ok for oggenc etc
if out_type=="raw" and write_to_stdout: self.o=sys.stdout if out_type=="raw" and write_to_stdout: self.o=sys.stdout
elif out_type=="ogg": self.o=os.popen("oggenc -o \"%s\" -r -C 1 -q 0 -" % (cond(write_to_stdout,"-",outputFile),),"wb") # oggenc assumes little-endian, which is what we're going to give it elif out_type=="ogg": self.o=os.popen(oggenc()+" -o \"%s\" -r -C 1 -q 0 -" % (cond(write_to_stdout,"-",outputFile),),popenWB) # oggenc assumes little-endian, which is what we're going to give it
elif out_type=="aac": 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=="aac":
elif out_type=="mp3": self.o=os.popen("lame -r%s%s -m m --vbr-new -V 9 - \"%s\"" % (lame_endian_parameters(),lame_quiet(),cond(write_to_stdout,"-",outputFile)),"wb") # (TODO check that old versions of lame won't complain about the --vbr-new switch. And some very old hardware players may insist on MPEG-1 rather than MPEG-2, which would need different parameters) if got_program("neroAacEnc"): self.o=os.popen("sox %s - -t wav - | neroAacEnc -br 32000 -if - -of \"%s\"" % (self.soxParams(),cond(write_to_stdout,"-",outputFile)),popenWB) # (TODO optionally use -2pass, on a physical input file like the afconvert code)
else: self.o=os.popen("faac -b 32 -P%s -C 1 -o \"%s\" -" % (cond(big_endian,""," -X"),cond(write_to_stdout,"-",outputFile)),popenWB) # (TODO check that faac on big-endian needs the -X removed when we're giving it little-endian. It SHOULD if the compile is endian-dependent.)
elif out_type=="mp3": self.o=os.popen("lame -r%s%s -m m --vbr-new -V 9 - \"%s\"" % (lame_endian_parameters(),lame_quiet(),cond(write_to_stdout,"-",outputFile)),popenWB) # (TODO check that old versions of lame won't complain about the --vbr-new switch. And some very old hardware players may insist on MPEG-1 rather than MPEG-2, which would need different parameters)
# Older versions of gradint used BladeEnc, with these settings: "BladeEnc -br 48 -mono -rawmono STDIN \"%s\"", but lame gives much smaller files (e.g. 3.1M instead of 11M) - it handles the silences more efficiently for a start). # 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) # (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": 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.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": elif out_type=="mp2":
self.rate = 22050 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": elif not out_type=="raw":
if out_type=="wav": self.rate=22050 # try not to take TOO much disk space 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") else: self.o = open(outputFile,"wb")
self.theLen = 0 self.theLen = 0
self.silences = [] self.silences = []
def soxParams(self): def soxParams(self):
# Have 16-bit mono, signed, little-endian # 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): def tell(self):
# How many seconds have we had? (2 because 16-bit) # How many seconds have we had? (2 because 16-bit)
return 1.0*self.theLen/self.rate/2 return 1.0*self.theLen/self.rate/2
...@@ -425,16 +503,16 @@ class SoundCollector(object): ...@@ -425,16 +503,16 @@ class SoundCollector(object):
sampleNo = int(0.5+seconds*self.rate) sampleNo = int(0.5+seconds*self.rate)
if not sampleNo: sampleNo=1 # so don't lock on rounding errors if not sampleNo: sampleNo=1 # so don't lock on rounding errors
byteNo = sampleNo*2 # since 16-bit byteNo = sampleNo*2 # since 16-bit
outfile_writeBytes(self.o,"\0"*byteNo) outfile_writeBytes(self.o,chr(0)*byteNo)
self.theLen += byteNo self.theLen += byteNo
def addFile(self,file,length): def addFile(self,file,length): # length ignored in this version
fileType=soundFileType(file) 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: 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") handle=open("tmp0","rb")
elif winsound or mingw32: handle = os.popen(("sox -t %s - %s - < \"%s\"" % (fileType,self.soxParams(),file)),"rb") elif winsound or mingw32: handle = os.popen(("sox -t %s - %s - < \"%s\"" % (fileType,self.soxParams(),file)),popenRB)
else: handle = os.popen(("cat \"%s\" | sox -t %s - %s -" % (file,fileType,self.soxParams())),"rb") else: handle = os.popen(("cat \"%s\" | sox -t %s - %s -" % (S(file),fileType,self.soxParams())),popenRB)
self.theLen += outfile_writeFile(self.o,handle,file) self.theLen += outfile_writeFile(self.o,handle,file)
if riscos_sound: if riscos_sound:
handle.close() ; os.unlink("tmp0") handle.close() ; os.unlink("tmp0")
...@@ -444,9 +522,9 @@ class SoundCollector(object): ...@@ -444,9 +522,9 @@ class SoundCollector(object):
t1 = self.tell() t1 = self.tell()
self.addSilence(betweenBeeps/2.0) self.addSilence(betweenBeeps/2.0)
if riscos_sound: if riscos_sound:
os.system(beepCmd() % (self.soxParams(),"tmp0")) system(beepCmd(self.soxParams(),"tmp0"))
data=read("tmp0") ; os.unlink("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) outfile_writeBytes(self.o,data)
self.theLen += len(data) self.theLen += len(data)
self.addSilence(betweenBeeps/2.0) self.addSilence(betweenBeeps/2.0)
...@@ -465,7 +543,7 @@ class SoundCollector(object): ...@@ -465,7 +543,7 @@ class SoundCollector(object):
if not app: show_info("Lengths of silences: %s (total %s)\n" % (self.silences,ttl)) if not app: show_info("Lengths of silences: %s (total %s)\n" % (self.silences,ttl))
if not outputFile=="-": outfile_close(self.o) if not outputFile=="-": outfile_close(self.o)
def outfile_writeBytes(o,bytes): def outfile_writeBytes(o,bytes):
try: o.write(bytes) try: writeB(o,bytes)
except IOError: outfile_write_error() except IOError: outfile_write_error()
def outfile_close(o): def outfile_close(o):
try: o.close() try: o.close()
...@@ -473,24 +551,30 @@ def outfile_close(o): ...@@ -473,24 +551,30 @@ def outfile_close(o):
def outfile_writeFile(o,handle,filename): def outfile_writeFile(o,handle,filename):
data,theLen = 1,0 data,theLen = 1,0
while data: while data:
data = handle.read(102400) data = readB(handle,102400)
outfile_writeBytes(o,data) outfile_writeBytes(o,data)
theLen += len(data) theLen += len(data)
assert theLen, "No data when reading "+filename+": check for sox crash" # TODO if it's from EkhoSynth it could be a buggy version of Ekho if not B(filename).startswith(B(partialsDirectory+os.sep)): assert theLen, "No data when reading "+S(filename)+": check for sox crash" # (but allow empty partials e.g. r5. TODO if it's from EkhoSynth it could be a buggy version of Ekho)
return theLen 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 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(): 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. # 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.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. # 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.) # (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() lameVer = os.popen("lame --version").read()
if "version " in lameVer: if lameVer.find("version ")>=0:
lameVer = lameVer[lameVer.index("version "):].split()[1] 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) # 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" return " --little-endian"
# otherwise fall-through to older lame behaviour: # 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? 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 ...@@ -505,11 +589,15 @@ beepType = 0
beepCmds = ["sox -t nul - %s %s synth trapetz 880 trim 0 0:0.05", 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"]*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"] "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 global beepType
r = beepCmds[beepType] r = beepCmds[beepType]
beepType += 1 beepType += 1
if beepType==len(beepCmds): beepType=0 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 return r
# ----------------------------------------------------- # -----------------------------------------------------
...@@ -518,14 +606,14 @@ def beepCmd(): ...@@ -518,14 +606,14 @@ def beepCmd():
class ShSoundCollector(object): class ShSoundCollector(object):
def __init__(self): def __init__(self):
self.file2command = {} 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 self.seconds = self.lastProgress = 0
if write_to_stdout: self.o=sys.stdout if write_to_stdout: self.o=sys.stdout
else: self.o = open(outputFile,"wb") else: self.o = open(outputFile,"wb")
start = """#!/bin/bash start = """#!/bin/bash
if echo "$0"|grep / >/dev/null; then export S="$0"; else export S=$(which "$0"); fi if echo "$0"|grep / >/dev/null; then S="$0"; else S=$(which "$0"); fi
export P="-t raw %s -s -r 44100 -c 1" 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) 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) outfile_writeBytes(self.o,start)
self.bytesWritten = len(start) # need to keep a count because it might be stdout 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 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 ...@@ -543,7 +631,7 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor
while gap > betweenBeeps+0.05: while gap > betweenBeeps+0.05:
t1 = self.tell() t1 = self.tell()
self.addSilence(betweenBeeps/2.0) self.addSilence(betweenBeeps/2.0)
self.commands.append(beepCmd() % ("$P","-")) self.commands.append(beepCmd("$P","-"))
self.seconds += 0.05 self.seconds += 0.05
self.addSilence(betweenBeeps/2.0) self.addSilence(betweenBeeps/2.0)
gap -= (self.tell()-t1) gap -= (self.tell()-t1)
...@@ -551,9 +639,9 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor ...@@ -551,9 +639,9 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor
def addFile(self,file,length): def addFile(self,file,length):
fileType=soundFileType(file) fileType=soundFileType(file)
self.seconds += length 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 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") else: handle = open(file,"rb")
offset, length = self.bytesWritten, outfile_writeFile(self.o,handle,file) offset, length = self.bytesWritten, outfile_writeFile(self.o,handle,file)
self.bytesWritten += length self.bytesWritten += length
...@@ -575,7 +663,7 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor ...@@ -575,7 +663,7 @@ tail -1 "$S" | bash\nexit\n""" % (sox_16bit,) # S=script P=params for sox (ignor
def finished(self): def finished(self):
if outputFile_appendSilence: self.addSilence(outputFile_appendSilence,False) if outputFile_appendSilence: self.addSilence(outputFile_appendSilence,False)
outfile_writeBytes(self.o,"\n") # so "tail" has a start of a line 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") for c in self.commands: outfile_writeBytes(self.o,c+"\n")
outfile_writeBytes(self.o,"tail -%d \"$S\" | bash\n" % (len(self.commands)+1)) outfile_writeBytes(self.o,"tail -%d \"$S\" | bash\n" % (len(self.commands)+1))
if not write_to_stdout: if not write_to_stdout:
...@@ -590,30 +678,44 @@ def dd_command(offset,length): ...@@ -590,30 +678,44 @@ def dd_command(offset,length):
warned_about_sox_decode = 0 warned_about_sox_decode = 0
def warn_sox_decode(): def warn_sox_decode():
global warned_about_sox_decode global warned_about_sox_decode
if not warned_about_sox_decode and not sox_ignoreLen: if not warned_about_sox_decode:
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 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 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: if riscos_sound:
warn_sox_decode() warn_sox_decode() # TODO: can use madplay or AMPlay to decode if correctly installed
os.system("sox -t mp3 \""+file+"\" -t wav"+cond(compress_SH," "+sox_8bit,"")+" tmp0") system("sox -t mp3 \""+file+"\" -t wav"+cond(compress_SH," "+sox_8bit,"")+" tmp0")
data=read("tmp0") ; os.unlink("tmp0") data=read("tmp0") ; os.unlink("tmp0")
return data 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() 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() tfil = os.tempnam()+dotwav
else: d=os.popen("mpg123 -q -w - \""+changeToDirOf(file)+"\"","rb").read() system("mpg123 -q -w \""+tfil+"\" \""+changeToDirOf(file)+"\"")
os.chdir(oldDir) if compress_SH and gotSox: dat = readB(os.popen("sox \""+tfil+"\" -t wav "+sox_8bit+" - ",popenRB))
# fix length (especially if it's mpg123) else: dat = open(tfil,"rb").read()
wavLen = len(d)-8 ; datLen = wavLen-36 # assumes no other chunks os.unlink(tfil) ; os.chdir(oldDir) ; return dat
if datLen<0: raise IOError("decode_mp3 got bad wav") # better than ValueError for the chr() in the following line elif macsound and got_program("afconvert"):
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("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: elif unix:
if gotSox: if soxMp3:
warn_sox_decode() 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: 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 "" 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.") 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): ...@@ -621,14 +723,17 @@ def decode_mp3(file):
class Mp3FileCache(object): class Mp3FileCache(object):
def __init__(self): self.fileCache = {} def __init__(self): self.fileCache = {}
def __del__(self): def __del__(self):
import os # as it might already have been gc'd try: import os # as it might already have been gc'd
for v in self.fileCache.values(): except: pass
try:
for v in self.fileCache.values():
try: os.remove(v) try: os.remove(v)
except: pass # somebody may have removed it already except: pass # somebody may have removed it already
except: pass
def decode_mp3_to_tmpfile(self,file): 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 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] return self.fileCache[file]
theMp3FileCache = Mp3FileCache() theMp3FileCache = Mp3FileCache()
...@@ -655,7 +760,7 @@ if outputFile: ...@@ -655,7 +760,7 @@ if outputFile:
if out_type=="sh": soundCollector,sample_table_hack = ShSoundCollector(), 1 if out_type=="sh": soundCollector,sample_table_hack = ShSoundCollector(), 1
else: soundCollector = SoundCollector() else: soundCollector = SoundCollector()
waitBeforeStart = 0 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) 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_time(): return soundCollector.tell()
def collector_sleep(s): soundCollector.addSilence(s) def collector_sleep(s): soundCollector.addSilence(s)
......
# This file is part of the source code of # This file is part of the source code of Gradint
# gradint v0.9974 (c) 2002-2011 Silas S. Brown. GPL v3+. # (c) Silas S. Brown.
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or # the Free Software Foundation; either version 3 of the License, or
...@@ -31,25 +31,33 @@ class MicInput(InputSource): ...@@ -31,25 +31,33 @@ class MicInput(InputSource):
self.rate = max(rates) self.rate = max(rates)
else: self.rate = None else: self.rate = None
def startRec(self,outFile,lastStopRecVal=None): def startRec(self,outFile,lastStopRecVal=None):
if not self.rate: if not self.rate: return self.err("Cannot record on this system (try aoss?)")
app.todo.alert="Cannot record on this system (try aoss?)" try: self.sound = tkSnack.Sound(file=outFile, rate=self.rate, channels=1, encoding="Lin16")
return except: return self.err("Cannot write to sound file '"+outFile+"' with tkSnack")
self.sound = tkSnack.Sound(file=outFile, rate=self.rate, channels=1, encoding="Lin16") try: self.sound.record()
self.sound.record() except: # e.g. waveInOpen failed on Windows 7 (driver problems?)
def stopRec(self): self.sound.stop() 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 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) def __init__(self,fileToPlay,startNow=True,startTime=0): # (if startNow=False, starts when you start recording)
global paranoid_file_management global paranoid_file_management
if use_unicode_filenames: fileToPlay=ensure_unicode(fileToPlay) if use_unicode_filenames: fileToPlay=ensure_unicode(fileToPlay)
else: 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 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 paranoid_file_management: # try to ensure it's ready for reading
if filelen(fileToPlay)<1048576: if filelen(fileToPlay)<1048576:
# only small - copy to temp 1st # only small - copy to temp 1st
self.fileToDel = os.tempnam()+fileToPlay[fileToPlay.rfind(extsep):] self.fileToDel = os.tempnam()+fileToPlay[fileToPlay.rfind(extsep):]
open(self.fileToDel,"wb").write(read(fileToPlay)) write(self.fileToDel,read(fileToPlay))
fileToPlay=self.fileToDel fileToPlay=self.fileToDel
else: open(fileToPlay) 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) 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 ...@@ -78,11 +86,12 @@ class PlayerInput(InputSource): # play to speakers while recording to various de
elapsedTime = self.elapsedTime() elapsedTime = self.elapsedTime()
if elapsedTime>=self.length-self.startSample*1.0/self.sampleRate: self.close() if elapsedTime>=self.length-self.startSample*1.0/self.sampleRate: self.close()
else: else:
import thread try: import thread
def stopMe(self,thread_id): 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)) time.sleep(max(0.5,self.length-self.startSample*1.0/self.sampleRate-elapsedTime))
self.autostop(thread_id) self.autostop(thread_id)
thread.start_new_thread(stopMe,(self,thread_id)) thread.start_new_thread(stopMe,(self,thread_id,elapsedTime))
def elapsedTime(self): def elapsedTime(self):
try: t=tkSnack.audio.elapsedTime() try: t=tkSnack.audio.elapsedTime()
except: t=0.0 except: t=0.0
...@@ -124,35 +133,16 @@ class PlayerInput(InputSource): # play to speakers while recording to various de ...@@ -124,35 +133,16 @@ class PlayerInput(InputSource): # play to speakers while recording to various de
theRecorderControls.current_recordFrom_button = theRecorderControls.old_recordFrom_button theRecorderControls.current_recordFrom_button = theRecorderControls.old_recordFrom_button
app.todo.undoRecordFrom=True # we might not be the GUI thread app.todo.undoRecordFrom=True # we might not be the GUI thread
if not tkSnack: if useTK and not tkSnack:
if macsound: # might still be able to use Audio Recorder if unix and isDirectory("/dev/snd") and got_program("arecord"): # can record via ALSA
if fileExists("AudioRecorder.zip"): unzip_and_delete("AudioRecorder.zip") recParams = (os.P_NOWAIT,got_program("arecord"),"arecord","-f","S16_LE","-r","22050")
if fileExists("Audio Recorder.app/plist"): # Audio Recorder with our special preferences list elif gotSox: recParams = (os.P_NOWAIT,got_program("sox"),"sox","-d","-r","22050","-c","1")+tuple(sox_16bit.split()+sox_signed.split())
runAudioRecorderYet = 0 else: recParams = None
def MacStartRecording(): if recParams:
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)
del MicInput del MicInput
class MicInput(InputSource): class MicInput(InputSource):
def startRec(self,outFile,lastStopRecVal=0.5): 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 time.sleep(lastStopRecVal) # allow process to start
def stopRec(self): def stopRec(self):
os.kill(self.pid,2) # INT os.kill(self.pid,2) # INT
...@@ -200,7 +190,7 @@ def wavToMp3(directory): ...@@ -200,7 +190,7 @@ def wavToMp3(directory):
def makeMp3Zips(baseDir,outDir,zipNo=0,direc=None): def makeMp3Zips(baseDir,outDir,zipNo=0,direc=None):
zipSplitThreshold = 5*1048576 # to be safe (as will split when it goes OVER that) 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: elif not direc:
for f in os.listdir(baseDir): zipNo = makeMp3Zips(baseDir,outDir,zipNo,f) 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) elif isDirectory(baseDir+os.sep+direc): zipNo = makeMp3Zips(baseDir+os.sep+direc,outDir,zipNo)
...@@ -222,7 +212,7 @@ def getAmplify(directory): ...@@ -222,7 +212,7 @@ def getAmplify(directory):
for f in os.listdir(directory): for f in os.listdir(directory):
factor = None factor = None
if f.endswith(dotwav) and not system("sox \""+directory+os.sep+f+"\" "+out2nul+" stat 2> \""+statfile+"\""): 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 l.startswith("Volume adjustment:"): factor=l.split()[2]
if not factor: continue if not factor: continue
tmplist.append([float(factor),f,factor]) tmplist.append([float(factor),f,factor])
...@@ -252,7 +242,7 @@ def doAmplify(directory,fileList,factor): ...@@ -252,7 +242,7 @@ def doAmplify(directory,fileList,factor):
class ButtonScrollingMixin(object): class ButtonScrollingMixin(object):
# expects self.ourCanvas # expects self.ourCanvas
def bindFocusIn(self,b): 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"): if not hasattr(app,"gotFocusInHandler"):
# (see scrollIntoView method's use of justGotFocusIn) # (see scrollIntoView method's use of justGotFocusIn)
app.gotFocusInHandler=1 app.gotFocusInHandler=1
...@@ -277,7 +267,7 @@ class ButtonScrollingMixin(object): ...@@ -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 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") elif by < cy: self.ourCanvas.yview("scroll","-1","units")
else: return # done 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): class RecorderControls(ButtonScrollingMixin):
def __init__(self): def __init__(self):
...@@ -315,7 +305,7 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -315,7 +305,7 @@ class RecorderControls(ButtonScrollingMixin):
self.frame.pack_forget() self.frame.pack_forget()
theISM.setInputSource(None) theISM.setInputSource(None)
def addButton(self,row,col,text,command,colspan=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) b = makeButton(self.grid,text=text,command=command)
self.bindFocusIn(b) self.bindFocusIn(b)
self.coords2buttons[(row,col)] = b self.coords2buttons[(row,col)] = b
...@@ -328,17 +318,56 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -328,17 +318,56 @@ class RecorderControls(ButtonScrollingMixin):
if col: self.coords2buttons[(row,col)].grid(row=row,column=col,columnspan=colspan) 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") else: self.coords2buttons[(row,col)].grid(row=row,column=0,columnspan=colspan,sticky="w")
def addLabel(self,row,col,utext): def addLabel(self,row,col,utext):
if (row,col) in self.coords2buttons: self.coords2buttons[(row,col)].grid_forget() if checkIn((row,col),self.coords2buttons): self.coords2buttons[(row,col)].grid_forget()
self.coords2buttons[(row,col)] = self.makeLabel_lenLimit(utext) rc = self.coords2buttons[(row,col)] = self.makeLabel_lenLimit(utext)
self.coords2buttons[(row,col)].grid(row=row,column=col,sticky="w") rc.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 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 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): 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 except IOError: return False
l = self.makeLabel_lenLimit(ftext) l = self.makeLabel_lenLimit(ftext)
l.grid(row=row,column=col,columnspan=2,sticky="w") 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) return True # do NOT put it in self.coords2buttons (not to do with space bar stuff etc)
def startSynthEdit(self,l,row,col,filename): def startSynthEdit(self,l,row,col,filename):
if hasattr(self,"renameToCancel"): if hasattr(self,"renameToCancel"):
...@@ -346,29 +375,29 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -346,29 +375,29 @@ class RecorderControls(ButtonScrollingMixin):
self.cancelRename(rr,cc) self.cancelRename(rr,cc)
if l: l.grid_forget() if l: l.grid_forget()
editText,editEntry = addTextBox(self.grid,"nopack") 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 except IOError: pass
editEntry.grid(row=row,column=col,sticky='we',columnspan=2) editEntry.grid(row=row,column=col,sticky='we',columnspan=2)
editEntry.bind('<Return>',lambda *args:self.doEdit(editText,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 *args:self.cancelEdit(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) focusButton(editEntry)
if hasattr(self.coords2buttons.get((row-1,col+1),""),"is_synth_label"): if hasattr(self.coords2buttons.get((row-1,col+1),""),"is_synth_label"):
self.addLabel(row-1,col+1,localise("(synth'd)")) self.addLabel(row-1,col+1,localise("(synth'd)"))
self.coords2buttons[(row-1,col+1)].is_synth_label = True self.coords2buttons[(row-1,col+1)].is_synth_label = True
def doEdit(self,editText,editEntry,row,col,filename): def doEdit(self,editText,editEntry,row,col,filename):
text = asUnicode(editText.get()).encode("utf-8").strip(wsp) text = bwspstrip(asUnicode(editText.get()).encode("utf-8"))
if text: open(filename,"w").write(text+"\n") if text: writeB(open(filename,"w"),text+B("\n"))
else: else:
try: os.remove(filename) try: os.remove(filename)
except: pass except: pass
self.cancelEdit(editEntry,row,col,filename) 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): def cancelEdit(self,editEntry,row,col,filename):
editEntry.grid_forget() editEntry.grid_forget()
labelAdded = self.addSynthLabel(filename,row,col) labelAdded = self.addSynthLabel(filename,row,col)
if hasattr(self.coords2buttons.get((row-1,col+1),""),"is_synth_label"): if hasattr(self.coords2buttons.get((row-1,col+1),""),"is_synth_label"):
if labelAdded: self.addLabel(row-1,col+1,localise("(synth'd)")) 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 self.coords2buttons[(row-1,col+1)].is_synth_label = True
def amplify(self,*args): 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) 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): ...@@ -390,7 +419,7 @@ class RecorderControls(ButtonScrollingMixin):
numZips = makeMp3Zips(self.currentDir,self.currentDir+os.sep+"zips") numZips = makeMp3Zips(self.currentDir,self.currentDir+os.sep+"zips")
if numZips: if numZips:
openDirectory(self.currentDir+os.sep+"zips",1) 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("You may now send the zip file by email.")
else: app.todo.alert=localise("No recordings found") else: app.todo.alert=localise("No recordings found")
self.undraw() ; self.draw() self.undraw() ; self.draw()
...@@ -398,11 +427,11 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -398,11 +427,11 @@ class RecorderControls(ButtonScrollingMixin):
if hasattr(self,"renameToCancel"): if hasattr(self,"renameToCancel"):
rr,cc = self.renameToCancel rr,cc = self.renameToCancel
self.cancelRename(rr,cc) 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 app.todo.alert=self.renamevar_msg
return return
self.renameToCancel = (row,col) 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") renameText,renameEntry = addTextBox(self.grid,"nopack")
renameEntry['width']=min(8,len(filename)+2) renameEntry['width']=min(8,len(filename)+2)
renameEntry.theText = renameText renameEntry.theText = renameText
...@@ -419,20 +448,20 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -419,20 +448,20 @@ class RecorderControls(ButtonScrollingMixin):
selectAllFunc = selectAll selectAllFunc = selectAll
class E: pass class E: pass
e=E() ; e.widget = renameEntry e=E() ; e.widget = renameEntry
self.ourCanvas.after(50,lambda *args:(e.widget.focus(),self.scrollIntoView(e.widget),selectAllFunc(e))) 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 *args:self.doRename(row,col)) renameEntry.bind('<Return>',lambda e=None,self=self,row=row,col=col:self.doRename(row,col))
renameEntry.bind('<Escape>',lambda *args:self.cancelRename(row,col)) renameEntry.bind('<Escape>',lambda e=None,self=self,row=row,col=col:self.cancelRename(row,col))
def doRename(self,row,col): def doRename(self,row,col):
if hasattr(self,"renameToCancel"): del self.renameToCancel if hasattr(self,"renameToCancel"): del self.renameToCancel
try: origName = self.coords2buttons[(row,col)].origName try: origName = self.coords2buttons[(row,col)].origName
except AttributeError: return # event must have fired twice for some reason? 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: for newName in newNames:
if not origName: # extra lines - need to get their origNames if not origName: # extra lines - need to get their origNames
if row==self.addMoreRow: self.addMore() 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"] 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 app.todo.alert=self.renamevar_msg
break break
if len(newNames)>1 and not '0'<=newName[0]<='9': # multiline paste and not numbered - we'd better keep the original number 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): ...@@ -444,7 +473,7 @@ class RecorderControls(ButtonScrollingMixin):
except: except:
tkMessageBox.showinfo(app.master.title(),localise("Could not rename %s to %s") % (origName,newName)) tkMessageBox.showinfo(app.master.title(),localise("Could not rename %s to %s") % (origName,newName))
return 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 else: # not a directory - rename individual files
self.doStop() # just in case 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 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): ...@@ -455,53 +484,53 @@ class RecorderControls(ButtonScrollingMixin):
except: 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) 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 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 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 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) 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) # 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 origName = None # get any others from the form
row += 1 row += 1
if len(newNames)==1 and row<self.addMoreRow: # put cursor on the next one 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: if row<self.addMoreRow:
origName=self.coords2buttons[(row,col)]["text"] origName=self.coords2buttons[(row,col)]["text"]
if not isDirectory(unicode2filename(self.currentDir+os.sep+origName)): self.startRename(row,0,origName) if not isDirectory(unicode2filename(self.currentDir+os.sep+origName)): self.startRename(row,0,origName)
def cancelRename(self,row,col): def cancelRename(self,row,col):
if hasattr(self,"renameToCancel"): del self.renameToCancel if hasattr(self,"renameToCancel"): del self.renameToCancel
origName = self.coords2buttons[(row,col)].origName 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) 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 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 if not os.sep in filename: filename = self.currentDir+os.sep+filename
recFilename = filename recFilename = filename
if recFilename.lower().endswith(dotmp3): recFilename=recFilename[:-len(dotmp3)]+dotwav # always record in WAV; can compress to MP3 after if recFilename.lower().endswith(dotmp3): recFilename=recFilename[:-len(dotmp3)]+dotwav # always record in WAV; can compress to MP3 after
if state: # exists if state: # sound file 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: 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.doStop(),theISM.setInputSource(PlayerInput(f,not self.syncFlag)),self.setSync(False)))) 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): 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: else:
self.addLabel(row,3+3*languageNo,"") self.addLabel(row,3+3*languageNo,"")
self.need_reRecord_enabler = not (not tkSnack) self.need_reRecord_enabler = not (not tkSnack)
else: # does not exist else: # sound file does not exist
synthFilename = filename[:filename.rfind(extsep)]+dottxt synthFilename = filename[:filename.rfind(extsep)]+dottxt
if txtExists=="unknown": txtExists=fileExists(synthFilename) if txtExists=="unknown": txtExists=fileExists(synthFilename)
if txtExists: self.addLabel(row,2+3*languageNo,localise("(synth'd)")) 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)")) else: self.addLabel(row,2+3*languageNo,localise("(empty)"))
self.coords2buttons[(row,2+3*languageNo)].is_synth_label = True self.coords2buttons[(row,2+3*languageNo)].is_synth_label = True
if winCEsound and not tkSnack: self.addLabel(row,3+3*languageNo,"") 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): def add_addMore_button(self):
if winCEsound and not tkSnack: pass # no 'add more words' button on WinCE; use PocketPC record button instead 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)) 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 *args:self.global_rerecord()),colspan=2) 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 *args:self.newFolder()),colspan=3) self.addButton(self.addMoreRow,4,text=localise("New folder"),command=(lambda e=None,self=self:self.newFolder()),colspan=3)
def del_addMore_button(self): def del_addMore_button(self):
if (self.addMoreRow,0) in self.coords2buttons: self.coords2buttons[(self.addMoreRow,0)].grid_forget() # old 'add more' button if checkIn((self.addMoreRow,0),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,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 self.coords2buttons[(self.addMoreRow,4)].grid_forget() # old 'new folder' button
def addMore(self,*args): def addMore(self,*args):
self.del_addMore_button() self.del_addMore_button()
...@@ -514,7 +543,7 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -514,7 +543,7 @@ class RecorderControls(ButtonScrollingMixin):
self.gridLabel(lang,self.addMoreRow) self.gridLabel(lang,self.addMoreRow)
self.addMoreRow += 2 ; self.maxPrefix += 1 self.addMoreRow += 2 ; self.maxPrefix += 1
self.add_addMore_button() 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): 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.")) 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) theISM.startRecording(filename)
...@@ -522,7 +551,7 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -522,7 +551,7 @@ class RecorderControls(ButtonScrollingMixin):
self.coords2buttons[(row,3+3*languageNo)]["text"]=localise("Stop") self.coords2buttons[(row,3+3*languageNo)]["text"]=localise("Stop")
self.updateForStopOrChange() self.updateForStopOrChange()
self.currentRecording = (filename,row,languageNo) 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" if app.scanrow.get()=="2": # "stop"
focusButton(self.coords2buttons[(row,3+3*languageNo)]) focusButton(self.coords2buttons[(row,3+3*languageNo)])
else: else:
...@@ -530,14 +559,14 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -530,14 +559,14 @@ class RecorderControls(ButtonScrollingMixin):
if app.scanrow.get()=="1": # move along 1st if app.scanrow.get()=="1": # move along 1st
while languageNo+1<len(self.languagesToDraw): while languageNo+1<len(self.languagesToDraw):
languageNo += 1 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)]) focusButton(self.coords2buttons[(row,3+3*languageNo)])
return return
languageNo = 0 # start of the row languageNo = 0 # start of the row
# fall-through - vertical movement # fall-through - vertical movement
for r in [row+1,row+2]: for r in [row+1,row+2]:
if r==self.addMoreRow: self.addMore() 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)]) return focusButton(self.coords2buttons[(r,3+3*languageNo)])
def doStop(self,*args): def doStop(self,*args):
theISM.stopRecording() theISM.stopRecording()
...@@ -555,9 +584,9 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -555,9 +584,9 @@ class RecorderControls(ButtonScrollingMixin):
if hasattr(self,"oldCanvasBbox") and bbox==self.oldCanvasBbox: pass if hasattr(self,"oldCanvasBbox") and bbox==self.oldCanvasBbox: pass
else: else:
self.oldCanvasBbox = bbox 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) 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 setSync(self,syncFlag): self.syncFlag = syncFlag
def newFolder(self,*args): def newFolder(self,*args):
count=0 count=0
...@@ -577,12 +606,12 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -577,12 +606,12 @@ class RecorderControls(ButtonScrollingMixin):
self.doStop() self.doStop()
theISM.setInputSource(PlayerInput(filename,not self.syncFlag)) theISM.setInputSource(PlayerInput(filename,not self.syncFlag))
self.current_recordFrom_button = (row, self.coords2buttons[(row,0)]) 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 col = 1
for inc in [-30, -5, 5, 30]: for inc in [-30, -5, 5, 30]:
if inc<0: text="<"+str(-inc) if inc<0: text="<"+str(-inc)
else: 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 col += 1
def handleSkip(self,filename,i): def handleSkip(self,filename,i):
self.protect_currentRecordFrom() self.protect_currentRecordFrom()
...@@ -596,7 +625,7 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -596,7 +625,7 @@ class RecorderControls(ButtonScrollingMixin):
if hasattr(self,"current_recordFrom_button") and self.current_recordFrom_button: if hasattr(self,"current_recordFrom_button") and self.current_recordFrom_button:
row, button = self.current_recordFrom_button row, button = self.current_recordFrom_button
for col in range(1+3*len(self.languagesToDraw)): 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() self.coords2buttons[(row,col)].grid_forget()
del self.coords2buttons[(row,col)] del self.coords2buttons[(row,col)]
button.grid(row=row,column=0,columnspan=1+3*len(self.languagesToDraw),sticky="w") button.grid(row=row,column=0,columnspan=1+3*len(self.languagesToDraw),sticky="w")
...@@ -635,17 +664,23 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -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!) 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")) 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:])) 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 not self.snack_initialized:
if tkSnack and not tkSnack=="MicOnly": if tkSnack and not tkSnack=="MicOnly":
tkSnack.initializeSnack(app) 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 self.snack_initialized = True
if not hasattr(app,"scanrow"): 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 = Tkinter.StringVar(app) # (keep StringVars in app not self to avoid d'tor confusion when close box pressed)
app.scanrow.set("0") app.scanrow.set("0")
self.reconfigure_scrollbar() self.reconfigure_scrollbar()
if tkSnack: theISM.setInputSource(MicInput()) 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) 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): ...@@ -653,6 +688,15 @@ class RecorderControls(ButtonScrollingMixin):
else: else:
r = Tkinter.Frame(self.frame) r = Tkinter.Frame(self.frame)
r.grid(row=1,sticky="e",columnspan=2) 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() Tkinter.Label(r,text=localise("Action of spacebar during recording")).pack()
r=Tkinter.Frame(r) ; r.pack() r=Tkinter.Frame(r) ; r.pack()
for button in [ for button in [
...@@ -669,7 +713,7 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -669,7 +713,7 @@ class RecorderControls(ButtonScrollingMixin):
maxPrefix = 0 ; self.has_recordFrom_buttons = False maxPrefix = 0 ; self.has_recordFrom_buttons = False
if not self.currentDir==samplesDirectory and os.sep in self.currentDir: 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 curRow += 1
l = os.listdir(self.currentDir) l = os.listdir(self.currentDir)
def cmpfunc(a,b): # sort alphabetically but ensure L2 comes before L1 for tab order def cmpfunc(a,b): # sort alphabetically but ensure L2 comes before L1 for tab order
...@@ -681,19 +725,19 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -681,19 +725,19 @@ class RecorderControls(ButtonScrollingMixin):
if a>b: return 1 if a>b: return 1
elif b>a: return -1 elif b>a: return -1
else: return 0 else: return 0
l.sort(cmpfunc) sort(l,cmpfunc)
self.has_variants = check_has_variants(self.currentDir,l) self.has_variants = check_has_variants(self.currentDir,l)
allLangs = list2set([firstLanguage,secondLanguage]+possible_otherLanguages) allLangs = list2set([firstLanguage,secondLanguage]+possible_otherLanguages)
hadDirectories = False hadDirectories = False
for fname in l: for fname in l:
flwr = fname.lower() ; isMeaning=0 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 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 else: languageOverride=None
if isDirectory(self.currentDir+os.sep+fname): if isDirectory(self.currentDir+os.sep+fname):
if not flwr in ["zips","utils","advanced utilities"]: # NOT "prompts", that can be browsed if not flwr in ["zips","utils","advanced utilities"]: # NOT "prompts", that can be browsed
newDir = self.currentDir+os.sep+fname 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 ? # TODO if _disabled have an Enable button ?
# if not have a Disable ?? # if not have a Disable ??
# (NB though the above button will have a column span) # (NB though the above button will have a column span)
...@@ -701,12 +745,14 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -701,12 +745,14 @@ class RecorderControls(ButtonScrollingMixin):
focusButton(self.coords2buttons[(curRow,0)]) focusButton(self.coords2buttons[(curRow,0)])
dirToHighlight = None # done dirToHighlight = None # done
curRow += 1 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) 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=u8strip(read(self.currentDir+os.sep+fname+os.sep+shortDescriptionName)).strip(wsp) 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 else: description=None
if description: if description:
l = Tkinter.Label(self.grid,text=" "+description,wraplength=self.ourCanvas.winfo_screenwidth()) try: sbarWidth = app.sbarWidth
l.grid(row=curRow,column=0,columnspan=1+3*len(self.languagesToDraw),sticky="w") 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 curRow += 1
if not flwr=="prompts": hadDirectories = True 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) 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): ...@@ -731,24 +777,24 @@ class RecorderControls(ButtonScrollingMixin):
if iprefix>maxPrefix: maxPrefix=iprefix # max existing numerical prefix 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 (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)) self.addLabel(curRow,0,utext=filename2unicode(prefix))
foundTxt = {} foundTxt = {}
for lang in self.languagesToDraw: 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 prefix2row[prefix] = curRow
for lang in self.languagesToDraw: # preserve tab order for lang in self.languagesToDraw: # preserve tab order
if lang==languageOverride and not flwr.endswith(dottxt): if lang==languageOverride and not flwr.endswith(dottxt):
self.updateFile(fname,curRow,self.languagesToDraw.index(lang),state=1) self.updateFile(fname,curRow,self.languagesToDraw.index(lang),state=1)
languageOverride=None # so not done again 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) self.gridLabel(lang,curRow)
for filename,col in foundTxt.values(): self.addSynthLabel(filename,curRow+1,col) for filename,col in foundTxt.values(): self.addSynthLabel(filename,curRow+1,col)
curRow += 2 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) 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 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 self.has_recordFrom_buttons = True
curRow += 1 curRow += 1
self.addMoreRow = curRow ; self.maxPrefix = maxPrefix+1 self.addMoreRow = curRow ; self.maxPrefix = maxPrefix+1
...@@ -767,14 +813,24 @@ class RecorderControls(ButtonScrollingMixin): ...@@ -767,14 +813,24 @@ class RecorderControls(ButtonScrollingMixin):
addButton(r2,localise("Record from file"),self.do_recordFromFile,"left") 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 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) # 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") 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" 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." 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) # (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 def doRecWords(): # called from GUI thread
if hasattr(app,"LessonRow"): app.thin_down_for_lesson() # else recorderMode if hasattr(app,"LessonRow"): app.thin_down_for_lesson() # else recorderMode
app.Label.pack_forget() ; app.CancelRow.pack_forget() app.Label.pack_forget() ; app.CancelRow.pack_forget()
...@@ -784,18 +840,39 @@ def doRecWords(): # called from GUI thread ...@@ -784,18 +840,39 @@ def doRecWords(): # called from GUI thread
theRecorderControls.draw() theRecorderControls.draw()
app.wordsExist = 1 # well not necessarily, but see comments re "Create word list" 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 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" if secondLanguage==firstLanguage: l1Suffix, l1Display = firstLanguage+"-meaning_"+firstLanguage, "meaning"
else: l1Suffix, l1Display = firstLanguage, firstLanguage else: l1Suffix, l1Display = firstLanguage, firstLanguage
while True: while True:
l2 = s60_recordFile(secondLanguage) l2 = recFunc(secondLanguage)
if not l2: return if not l2: return
l1 = None l1 = None
while not l1: 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: else:
l1txt = appuifw.query(u""+firstLanguage+" text:","text") l1txt = inputFunc(u""+firstLanguage+" text:")
if l1txt: if l1txt:
l1 = "newfile_"+firstLanguage+dottxt l1 = "newfile_"+firstLanguage+dottxt
open(l1,"w").write(l1txt.encode("utf-8")) open(l1,"w").write(l1txt.encode("utf-8"))
...@@ -803,21 +880,22 @@ def s60_recordWord(): ...@@ -803,21 +880,22 @@ def s60_recordWord():
os.remove(l2) ; break os.remove(l2) ; break
if not l1: continue if not l1: continue
ls = list2set(os.listdir(samplesDirectory)) ls = list2set(os.listdir(samplesDirectory))
def inLs(prefix): def inLs(prefix,ls):
for ext in [dotwav,dotmp3,dottxt]: for l in ls:
for l in [firstLanguage,secondLanguage]: if l.startswith(prefix) and len(l) > len(prefix) and l[len(prefix)] not in "0123456789": return True
if prefix+"_"+l+ext in ls: return 1 global recCount
c = 1 try: recCount += 1
while inLs("%02d" % c): c += 1 except: recCount = 1
origPrefix = prefix = u""+("%02d" % c) while inLs("%02d" % recCount,ls): recCount += 1
origPrefix = prefix = ensure_unicode("%02d" % recCount)
while True: while True:
prefix = appuifw.query(u"Filename:","text",prefix) prefix = inputFunc(u"Filename:",prefix)
if not prefix: # pressed cancel ?? if not prefix: # pressed cancel ??
if getYN("Discard this recording?"): if getYN("Discard this recording?"):
os.remove(l1) ; os.remove(l2) ; return recCount-=1;os.remove(l1);os.remove(l2);return
else: else:
prefix = origPrefix ; continue 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 if samplesDirectory: prefix=samplesDirectory+os.sep+prefix
os.rename(l1,prefix+l1[l1.index("_"):]) os.rename(l1,prefix+l1[l1.index("_"):])
os.rename(l2,prefix+l2[l2.index("_"):]) os.rename(l2,prefix+l2[l2.index("_"):])
...@@ -826,15 +904,15 @@ def s60_recordFile(language): ...@@ -826,15 +904,15 @@ def s60_recordFile(language):
fname = "newfile_"+language+dotwav fname = "newfile_"+language+dotwav
while True: while True:
S=audio.Sound.open(os.getcwd()+os.sep+fname) S=audio.Sound.open(os.getcwd()+os.sep+fname)
def forgetS(): def forgetS(fname,S):
S.close() S.close()
try: os.remove(fname) try: os.remove(fname)
except: pass 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() S.record()
ret = getYN("Press OK to stop") ; S.stop() ret = getYN("Press OK to stop") ; S.stop()
if not ret: if not ret:
forgetS() ; continue forgetS(fname,S) ; continue
S.play() S.play()
ret = getYN("Are you happy with this?") ret = getYN("Are you happy with this?")
S.stop() ; S.close() S.stop() ; S.close()
......
# This file is part of the source code of # This file is part of the source code of Gradint
# gradint v0.9974 (c) 2002-2011 Silas S. Brown. GPL v3+. # (c) Silas S. Brown.
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or # the Free Software Foundation; either version 3 of the License, or
...@@ -42,7 +42,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData): ...@@ -42,7 +42,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData):
# work out number of repetitions needed. not sure if this should be configurable somewhere. # work out number of repetitions needed. not sure if this should be configurable somewhere.
first_repeat_is_unessential = 0 first_repeat_is_unessential = 0
if not numTimesBefore: # New word. If there are L2 variants, introduce them all if possible. 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 if numVariants>1 and lessonIsTight(): numVariants = 1 # hack
numRepeats = numVariants + cond(numVariants>=cond(availablePrompts.user_is_advanced,2,3),0,1) numRepeats = numVariants + cond(numVariants>=cond(availablePrompts.user_is_advanced,2,3),0,1)
elif numTimesBefore == 1: numRepeats = 3 elif numTimesBefore == 1: numRepeats = 3
...@@ -53,7 +53,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData): ...@@ -53,7 +53,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData):
else: numRepeats = 1 else: numRepeats = 1
if numRepeats==1: if numRepeats==1:
k,f = synthcache_lookup(zhFile,justQueryCache=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? # Hack: an experimental cache entry but only 1 repetition - what do we do?
c=random.choice([1,2,3]) c=random.choice([1,2,3])
if c==1: pass # do nothing if c==1: pass # do nothing
...@@ -65,7 +65,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData): ...@@ -65,7 +65,7 @@ def anticipation(promptFile,zhFile,numTimesBefore,promptsData):
theList = [] theList = []
if instrIsPrefix: theList = instructions if instrIsPrefix: theList = instructions
theList.append(promptEvent) 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. # 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))) theList = theList + map(lambda x:fileToEvent(x,promptsDirectory), availablePrompts.getPromptList("begin",promptsData,languageof(zhFile)))
if not instrIsPrefix: theList += instructions if not instrIsPrefix: theList += instructions
...@@ -111,10 +111,11 @@ def reverseAnticipation(promptFile,zhFile,promptsData): ...@@ -111,10 +111,11 @@ def reverseAnticipation(promptFile,zhFile,promptsData):
return CompositeEvent(theList) return CompositeEvent(theList)
def languageof(file): def languageof(file):
assert "_" in file, "no _ in %s" % (file,) file = B(file)
s=file[file.rindex("_")+1:] assert B("_") in file, "no _ in %s" % (repr(file),)
if extsep in s: return s[:s.rindex(extsep)] s=file[file.rindex(B("_"))+1:]
else: return s if B(extsep) in s: return S(s[:s.rindex(B(extsep))])
else: return S(s)
def commentSequence(): def commentSequence():
sequence = [] sequence = []
...@@ -128,7 +129,7 @@ def anticipationSequence(promptFile,zhFile,start,to,promptsData,introList): ...@@ -128,7 +129,7 @@ def anticipationSequence(promptFile,zhFile,start,to,promptsData,introList):
# (try number from 'start' to 'to', EXCLUDING 'to') # (try number from 'start' to 'to', EXCLUDING 'to')
sequence = [] sequence = []
# First one has initialGlue() whatever the value of 'start' is # 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? # *** 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) # (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) firstItem = reverseAnticipation(promptFile,zhFile,promptsData)
......