Festival supports text to speech for raw text files. If you are not interested in using Festival in any other way except as black box for rendering text as speech, the following method is probably what you want.
festival --tts myfile
This will say the contents of `myfile'. Alternatively text may be submitted on standard input
echo hello world | festival --tts cat myfile | festival --tts
Festival supports the notion of text modes where the text file
type may be identified, allowing Festival to process the file in an
appropriate way. Currently only two types are considered stable:
STML
and raw
, but other types such as email
,
HTML
, Latex
, etc. are being developed and discussed below.
This follows the idea of buffer modes in Emacs where a file's type can
be utilized to best display the text. Text mode may also be selected
based on a filename's extension.
Within the command interpreter the function tts
is used
to render files as text; it takes a filename and the text mode
as arguments.
Text to speech works by first tokenizing the file and chunking the
tokens into utterances. The definition of utterance breaks is
determined by the utterance tree in variable eou_tree
. A default
version is given in `lib/tts.scm'. This uses a decision tree to
determine what signifies an utterance break. Obviously blank lines are
probably the most reliable, followed by certain punctuation. The
confusion of the use of periods for both sentence breaks and
abbreviations requires some more heuristics to best guess their
different use. The following tree is currently used which
works better than simply using punctuation.
(defvar eou_tree '((n.whitespace matches ".*\n.*\n\\(.\\|\n\\)*") ;; 2 or more newlines ((1)) ((punc in ("?" ":" "!")) ((1)) ((punc is ".") ;; This is to distinguish abbreviations vs periods ;; These are heuristics ((name matches "\\(.*\\..*\\|[A-Z][A-Za-z]?[A-Za-z]?\\|etc\\)") ((n.whitespace is " ") ((0)) ;; if abbrev single space isn't enough for break ((n.name matches "[A-Z].*") ((1)) ((0)))) ((n.whitespace is " ") ;; if it doesn't look like an abbreviation ((n.name matches "[A-Z].*") ;; single space and non-cap is no break ((1)) ((0))) ((1)))) ((0)))))
The token items this is applied to will always (except in the
end of file case) include one following token, so look ahead is
possible. The "n." and "p." and "p.p." prefixes allow access to the
surrounding token context. The features name
, whitespace
and punc
allow access to the contents of the token itself. At
present there is no way to access the lexicon form this tree which
unfortunately might be useful if certain abbreviations were identified
as such there.
Note these are heuristics and written by hand not trained from data, though problems have been fixed as they have been observed in data. The above rules may make mistakes where abbreviations appear at end of lines, and when improper spacing and capitalization is used. This is probably worth changing, for modes where more casual text appears, such as email messages and USENET news messages. A possible improvement could be made by analysing a text to find out its basic threshold of utterance break (i.e. if no full stop, two spaces, followed by a capitalized word sequences appear and the text is of a reasonable length then look for other criteria for utterance breaks).
Ultimately what we are trying to do is to chunk the text into utterances that can be synthesized quickly and start to play them quickly to minimise the time someone has to wait for the first sound when starting synthesis. Thus it would be better if this chunking were done on prosodic phrases rather than chunks more similar to linguistic sentences. Prosodic phrases are bounded in size, while sentences are not.
We do not believe that all texts are of the same type. Often information about the general contents of file will aid synthesis greatly. For example in Latex files we do not want to here "left brace, backslash e m" before each emphasized word, nor do we want to necessarily hear formating commands. Festival offers a basic method for specifying customization rules depending on the mode of the text. By type we are following the notion of modes in Emacs and eventually will allow customization at a similar level.
Modes are specified as the third argument to the function tts
.
When using the Emacs interface to Festival the buffer mode is
automatically passed as the text mode. If the mode is not supported a
warning message is printed and the raw text mode is used.
Our initial text mode implementation allows configuration both in C++ and in Scheme. Obviously in C++ almost anything can be done but it is not as easy to reconfigure without recompilation. Here we will discuss those modes which can be fully configured at run time.
A text mode may contain the following
xml
the file is read through the built in XML
parser rxp
. Alternatively if analysis mode is xxml
the
filter should an SGML normalising parser and the output is processed in
a way suitable for it. Any other value is ignored.
These mode specific parameters are specified in the a-list
held in tts_text_modes
.
When using Festival in Emacs the emacs buffer mode is passed to Festival as the text mode.
Note that above mechanism is not really designed to be re-entrant, this should be addressed in later versions.
Following the use of auto-selection of mode in Emacs, Festival can
auto-select the text mode based on the filename given when no explicit
mode is given. The Lisp variable auto-text-mode-alist
is a list
of dotted pairs of regular expression and mode name. For example
to specify that the email
mode is to be used for files ending
in `.email' we would add to the current auto-text-mode-alist
as follows
(set! auto-text-mode-alist (cons (cons "\\.email$" 'email) auto-text-mode-alist))
If the function tts
is called with a mode other than nil
that mode overrides any specified by the auto-text-mode-alist
.
The mode fundamental
is the explicit "null" mode, it is used
when no mode is specified in the function tts
, and match
is found in auto-text-mode-alist
or the specified mode
is not found.
By convention if a requested text model is not found in
tts_text_modes
the file `MODENAME-mode' will be
required
. Therefore if you have the file
`MODENAME-mode.scm' in your library then it will be automatically
loaded on reference. Modes may be quite large and it is not necessary
have Festival load them all at start up time.
Because of the auto-text-mode-alist
and the auto loading
of currently undefined text modes you can use Festival like
festival --tts example.email
Festival with automatically synthesize `example.email' in text
mode email
.
If you add your own personal text modes you should do the following.
Suppose you've written an HTML mode. You have named it
`html-mode.scm' and put it in `/home/awb/lib/festival/'. In
your `.festivalrc' first identify you're personal Festival library
directory by adding it to lib-path
.
(set! lib-path (cons "/home/awb/lib/festival/" lib-path))
Then add the definition to the auto-text-mode-alist
that file names ending `.html' or `.htm' should
be read in HTML mode.
(set! auto-text-mode-alist (cons (cons "\\.html?$" 'html) auto-text-mode-alist))
Then you may synthesize an HTML file either from Scheme
(tts "example.html" nil)
Or from the shell command line
festival --tts example.html
Anyone familiar with modes in Emacs should recognise that the process of adding a new text mode to Festival is very similar to adding a new buffer mode to Emacs.
Here is a short example of a tts mode for reading email messages. It is by no means complete but is a start at showing how you can customize tts modes without writing new C++ code.
The first task is to define a filter that will take a saved mail message and remove extraneous headers and just leave the from line, subject and body of the message. The filter program is given a file name as its first argument and should output the result on standard out. For our purposes we will do this as a shell script.
#!/bin/sh # Email filter for Festival tts mode # usage: email_filter mail_message >tidied_mail_message grep "^From: " $1 echo grep "^Subject: " $1 echo # delete up to first blank line (i.e. the header) sed '1,/^$/ d' $1
Next we define the email init function, which will be called when we start this mode. What we will do is save the current token to words function and slot in our own new one. We can then restore the previous one when we exit.
(define (email_init_func) "Called on starting email text mode." (set! email_previous_t2w_func token_to_words) (set! english_token_to_words email_token_to_words) (set! token_to_words email_token_to_words))
Note that both english_token_to_words
and
token_to_words
should be set to ensure that our new
token to word function is still used when we change voices.
The corresponding end function puts the token to words function back.
(define (email_exit_func) "Called on exit email text mode." (set! english_token_to_words email_previous_t2w_func) (set! token_to_words email_previous_t2w_func))
Now we can define the email specific token to words function. In this example we deal with two specific cases. First we deal with the common form of email addresses so that the angle brackets are not pronounced. The second points are to recognise quoted text and immediately change the the speaker to the alternative speaker.
(define (email_token_to_words token name) "Email specific token to word rules." (cond
This first condition identifies the token as a bracketed email address
and removes the brackets and splits the token into name
and IP address. Note that we recursively call the function
email_previous_t2w_func
on the email name and IP address
so that they will be pronounced properly. Note that because that
function returns a list of words we need to append them together.
((string-matches name "<.*.*>") (append (email_previous_t2w_func token (string-after (string-before name "@") "<")) (cons "at" (email_previous_t2w_func token (string-before (string-after name "@") ">")))))
Our next condition deals with identifying a greater than sign being used as a quote marker. When we detect this we select the alternative speaker, even though it may already be selected. We then return no words so the quote marker is not spoken. The following condition finds greater than signs which are the first token on a line.
((and (string-matches name ">") (string-matches (item.feat token "whitespace") "[ \t\n]*\n *")) (voice_don_diphone) nil ;; return nothing to say )
If it doesn't match any of these we can go ahead and use the builtin token to words function Actually, we call the function that was set before we entered this mode to ensure any other specific rules still remain. But before that we need to check if we've had a newline with doesn't start with a greater than sign. In that case we switch back to the primary speaker.
(t ;; for all other cases (if (string-matches (item.feat token "whitespace") ".*\n[ \t\n]*") (voice_rab_diphone)) (email_previous_t2w_func token name))))
In addition to these we have to actually declare the text mode. This we do by adding to any existing modes as follows.
(set! tts_text_modes (cons (list 'email ;; mode name (list ;; email mode params (list 'init_func email_init_func) (list 'exit_func email_exit_func) '(filter "email_filter"))) tts_text_modes))
This will now allow simple email messages to be dealt with in a mode specific way.
An example mail message is included in `examples/ex1.email'. To hear the result of the above text mode start Festival, load in the email mode descriptions, and call TTS on the example file.
(tts ".../examples/ex1.email" 'email)
The above is very short of a real email mode but does illustrate how one might go about building one. It should be reiterated that text modes are new in Festival and their most effective form has not been discovered yet. This will improve with time and experience.
Go to the first, previous, next, last section, table of contents.