packages feed

tttool-1.4: src/TextToSpeech.hs

{-# LANGUAGE NondecreasingIndentation #-}

module TextToSpeech where

import System.Directory
import System.IO
import System.FilePath
import System.Process
import System.Exit
import Control.Monad
import Data.Hashable
import Text.Printf
import Control.Exception
import System.IO.Error
import System.Environment
import System.Info (os)
import System.Environment.Executable

import Language

ttsFileName lang txt =
    "tts-cache" </> "tts-" ++ map go (shorten txt) ++ "-" ++ ppLang lang <.>  "ogg"
  where go '/'         = '_'
        go c | c < ' ' = '_'
        go c           = c
        shorten x | length x > 20 = printf "%s-%016X" (take 20 x) (hash x)
        shorten x                 = x


pico :: Language -> FilePath -> String -> (String, [String])
pico lang tmp txt =
   ("pico2wave", ["--wave", tmp, "--lang", l, txt])
  where
    l = case lang of 
            Language "en" -> "en-GB"
            Language "de" -> "de-DE"
            Language "fr" -> "fr-FR"
            Language s    -> s

espeak :: Language -> FilePath -> String -> (String, [String])
espeak lang tmp txt =
 ("espeak", ["-v", l, "-w", tmp, "-s", "120", txt])
  where
    l = case lang of
            Language s    -> s

espeak_contrib :: FilePath -> Language -> FilePath -> String -> (String, [String])
espeak_contrib myDir lang tmp txt =
 (myDir </> "contrib" </> "espeak", ["-v", l, "-w", tmp, "-s", "120", txt])
  where
    l = case lang of
            Language s    -> s


engines :: FilePath -> Language -> FilePath -> String -> [(String, [String])]
engines myDir l ft txt =
    [ pico l ft txt
    , espeak l ft txt
    , espeak_contrib myDir l ft txt
    ]

oggenc :: FilePath -> FilePath -> (String, [String])
oggenc from to = ("oggenc", ["-Q", "-o", to, from])

oggenc_contrib :: FilePath -> FilePath -> (String, [String])
oggenc_contrib from to = ("contrib/oggenc", ["-Q", "-o", to, from])

encoders :: FilePath -> FilePath -> [(String, [String])]
encoders from to =
    [ oggenc from to
    , oggenc_contrib from to
    ]

tryPrograms [] e = e
tryPrograms ((c,args):es) e = do
    -- Missing programs cause exceptions on Windows, but error 127 on Linux.
    -- Try to handle both here.
    r <- tryJust (guard . isDoesNotExistError) $ do
        env <- getEnvironment
        let env' | os == "mingw32" = ("ESPEAK_DATA_PATH", "contrib") : env
                 | otherwise       = env
        ph <- runProcess  c args Nothing (Just env') Nothing Nothing Nothing
        ret <- waitForProcess ph
        if ret == ExitSuccess then return True
        else if ret == ExitFailure 127 then return False
        else do
            putStrLn $ "Failed to execute \"" ++ c ++ "\" (" ++ show ret ++ ")"
            exitFailure
    case r of
       Right True -> return ()
       _ -> tryPrograms es e

textToSpeech :: Language -> String -> IO ()
textToSpeech lang txt = do
    ex <- doesFileExist fn
    if ex then return () else do

    createDirectoryIfMissing True (takeDirectory fn)

    putStrLn $ "Speaking \"" ++ txt ++ "\"."  
    (tmp,h) <- openTempFile (takeDirectory fn) (takeBaseName fn <.> "wav")
    hClose h

    (myDir,_) <- splitExecutablePath

    tryPrograms (engines myDir lang tmp txt) $ do
        putStrLn "No suitable text-to-speech-engine found."
        putStrLn "Do you have libttspico-utils or espeak installed?"

    tryPrograms (encoders tmp fn) $ do
        putStrLn "Could not find \"oggenc\"."
        putStrLn "Do you have vorbis-tools installed?"

    removeFile tmp
    return ()
  where
    fn = ttsFileName lang txt