From 4ab0d7f7da55b567f018b2b4e2f2f494a82b467f Mon Sep 17 00:00:00 2001 From: Adrian Cochrane Date: Tue, 1 Jun 2021 19:20:06 +1200 Subject: [PATCH] Initialize Voice2JSON for speech recognition! --- rhapsode.cabal | 2 +- src/CExports.hs | 6 +++--- src/Links.hs | 38 ++++++++++++++++++++++++++++++++++--- src/main.c | 50 +++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/rhapsode.cabal b/rhapsode.cabal index a223f26..5a0cac0 100644 --- a/rhapsode.cabal +++ b/rhapsode.cabal @@ -65,7 +65,7 @@ library network-uri, stylist >= 2.4 && <3, css-syntax, xml-conduit-stylist >= 2.3 && <3, scientific, async, hurl >= 2, filepath, temporary, - file-embed >= 0.0.9 && < 0.1, time, parallel >= 1 + file-embed >= 0.0.9 && < 0.1, time, parallel >= 1, process -- Directories containing source files. hs-source-dirs: src diff --git a/src/CExports.hs b/src/CExports.hs index b755683..1162a30 100644 --- a/src/CExports.hs +++ b/src/CExports.hs @@ -38,10 +38,10 @@ import Foreign.Marshal.Array --pair a b = (a, b) -- FIXME: Segfaults, was intended for the sake of easy concurrency. -foreign export ccall c_docLinksAndRendering :: StablePtr Session -> StablePtr Page -> Bool -> IO (CArray CString) +foreign export ccall c_docLinksAndRendering :: StablePtr Session -> StablePtr Page -> Bool -> CString -> IO (CArray CString) -c_docLinksAndRendering c_session c_page rewriteUrls = do - c_links <- c_extractLinks c_page +c_docLinksAndRendering c_session c_page rewriteUrls c_v2jProfile = do + c_links <- c_extractLinks c_page c_v2jProfile ssml <- c_renderDoc c_session c_page rewriteUrls -- (c_links, ssml) <- c_extractLinks c_page `concurrently` c_renderDoc c_session c_page rewriteUrls nil <- newCString "" diff --git a/src/Links.hs b/src/Links.hs index 9b0ce8b..914df0c 100644 --- a/src/Links.hs +++ b/src/Links.hs @@ -29,6 +29,10 @@ import qualified Data.Set as Set import Data.List (nub, intercalate) import Control.Concurrent (forkIO) +-- For Voice2Json's sentences.ini +import Data.Char +import System.Process (callProcess) + data Link = Link { label :: Text, title :: Text, @@ -208,14 +212,34 @@ updateSuggestions page = do createDirectoryIfMissing True dir Prelude.writeFile path $ unlines $ map unwords suggestions' +------- +--- Voice2Json language models +------- + +-- | Output links to a Voice2Json sentences.ini grammar. +outputSentences _ "" = return () +outputSentences links dir = do + Prelude.writeFile (dir "sentences.ini") $ unlines sentences + callProcess "voice2json" ["voice2json", "--profile", dir, "train-profile"] + where + sentences = "[links]" : [ + unwords $ words $ filter validChar line -- Enforce valid sentences.ini syntax. + | line@(_:_) <- map (unpack . label) links ++ map (unpack . title) links ++ map (show . href) links + ] + -- | Can this character appear in a sentences.ini rule without escaping? + validChar ch = not (isAscii ch) || isSpace ch || isAlphaNum ch + -- C API -foreign export ccall c_extractLinks :: StablePtr Page -> IO (CArray CString) +foreign export ccall c_extractLinks :: StablePtr Page -> CString -> IO (CArray CString) -c_extractLinks c_page = do +c_extractLinks c_page c_v2jProfile = do page <- deRefStablePtr c_page + v2jProfile <- peekCString c_v2jProfile forkIO $ updateSuggestions page -- background process for useful navigation aid. bookmarks <- readBookmarks - ret <- forM (linksFromPage page ++ extractLinks bookmarks) $ \link -> do + let links = linksFromPage page ++ extractLinks bookmarks + forkIO $ outputSentences links v2jProfile + ret <- forM links $ \link -> do c_label <- text2cstring $ strip $ label link c_title <- text2cstring $ strip $ title link c_href <- newCString $ uriToString id (href link) "" @@ -248,3 +272,11 @@ c_formatLink c_label c_title c_url = do audio src = el "audio" [("src", pack src)] [] prosody attrs txt = el "prosody" attrs [NodeContent txt] style field mode inner = el "tts:style" [("field", field), ("mode", mode)] [NodeElement inner] + +foreign export ccall c_dataDir :: CString -> IO CString + +-- | Used to find Voice2Json profile +c_dataDir c_subdir = do + subdir <- peekCString c_subdir + cache <- getXdgDirectory XdgData "rhapsode" + newCString (cache subdir) diff --git a/src/main.c b/src/main.c index 2b89f0b..ec17e0b 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,9 @@ #include #include +#include +#include + #include "HsFFI.h" #include #include @@ -31,10 +34,11 @@ struct session *c_enableLogging(struct session*); void c_writeLog(char*, struct session*); char *c_renderDoc(struct session*, struct page*, _Bool); -char **c_extractLinks(struct page*); +char **c_extractLinks(struct page*, char *v2jProfile); char **c_docLinksAndRendering(struct session*, struct page*, _Bool); // FIXME segfaults. int c_ssmlHasMark(char*, char*); char *c_formatLink(char *label, char *title, char *url); +char *c_dataDir(char *subdir); char *c_lastVisited(char*); @@ -322,16 +326,25 @@ FILE *parse_opt_file() { return ret; } +int dir_exists(char *path) { + DIR *dir = opendir(path); + if (dir) closedir(dir); + else return ENOENT == errno; + return 1; +} + int main(int argc, char **argv) { int speak_err = 0; struct session *session = NULL; struct page *referer = NULL; int use_espeak = 0; + int validate_v2j_profile = 0; char *logpath = NULL; char *mimes; FILE *fd_ssml = NULL; FILE *fd_links = NULL; + char *v2j_profile = c_dataDir("voice2json"); hs_init(&argc, &argv); @@ -344,9 +357,9 @@ int main(int argc, char **argv) { int c; opterr = 0; #ifdef WITH_SPEECHD - while ((c = getopt(argc, argv, "xs::l::L:kKw::dh")) != -1) { + while ((c = getopt(argc, argv, "xs::l::L:kKvVw::dh")) != -1) { #else - while ((c = getopt(argc, argv, "xs::l::kKw::h")) != -1) { + while ((c = getopt(argc, argv, "xs::l::kKv::Vw::h")) != -1) { #endif switch (c) { case 'x': @@ -367,6 +380,14 @@ int main(int argc, char **argv) { case 'k': read_keyboard = 1; break; + case 'V': + v2j_profile = ""; + validate_v2j_profile = 0; + break; + case 'v': + if (optarg != NULL) v2j_profile = optarg; + validate_v2j_profile = 1; + break; case 'w': use_espeak = 1; path_wav = optarg; @@ -387,8 +408,10 @@ int main(int argc, char **argv) { fprintf(stderr, "\t\t\thttps://xkcd.com/1692/\n"); fprintf(stderr, "\t-l\tlinks\tWrite extracted links to specifed file or stdout as TSV.\n"); fprintf(stderr, "\t-L\tlog\tWrite (append) network request timing to specified filepath.\n"); - fprintf(stderr, "\t-k\tkeyboard\tRead arrow key navigation & links from stdin. Default behaviour, noop.\n"); - fprintf(stderr, "\t-K\tDon't read input from stdin."); + fprintf(stderr, "\t-k\tkeyboard in\tRead arrow key navigation & links from stdin. Default behaviour, noop.\n"); + fprintf(stderr, "\t-K\t\tDon't read input from stdin."); + fprintf(stderr, "\t-v\tvoice in\tEnsure voice input is enabled & optionally sets the Voice2JSON profile.\n"); + fprintf(stderr, "\t-V\t\tDon't listen for voice input.\n"); fprintf(stderr, "\t-w\t.wav\tWrite an audio recording of the webpage, or (DEFAULT) immediately output through speakers.\n"); #ifdef WITH_SPEECHD fprintf(stderr, "\t-d\tSpeechD\tSchedule page read via the SpeechD daemon. (BROKEN)\n"); @@ -431,6 +454,21 @@ int main(int argc, char **argv) { referer = c_initialReferer(); if (use_espeak) speak_err = speak_initialize(); + + struct stat sb; + if (validate_v2j_profile && dir_exists(v2j_profile)) { + // TODO Would packagers want a way to configure autodownloading of these profiles? + fprintf(stderr, "Voice2JSON profile is uninitialized! Voice recognition is now disabled.\n"); + fprintf(stderr, "Please download a profile from: http://voice2json.org/#supported-languages\n"); + fprintf(stderr, "And save the download in: %s\n", v2j_profile); + if (use_espeak) { + speak_text("Voice2JSON profile is uninitialized! Voice recognition is now disabled.", + espeakRATE, 0); + } + + v2j_profile = ""; + } + char *ssml, **links, *uri; int read_links = 0; if (optind >= argc) { @@ -451,8 +489,8 @@ read_uri: if (logpath != NULL) session = c_enableLogging(session); struct page *page = c_fetchURL(session, mimes, referer, uri); + links = c_extractLinks(page, v2j_profile); ssml = c_renderDoc(session, page, use_espeak); - links = c_extractLinks(page); if (logpath != NULL) c_writeLog(logpath, session); -- 2.30.2