M rhapsode.cabal => rhapsode.cabal +1 -1
@@ 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
M src/CExports.hs => src/CExports.hs +3 -3
@@ 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 ""
M src/Links.hs => src/Links.hs +35 -3
@@ 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)
M src/main.c => src/main.c +44 -6
@@ 6,6 6,9 @@
#include <termios.h>
#include <limits.h>
+#include <dirent.h>
+#include <errno.h>
+
#include "HsFFI.h"
#include <espeak-ng/espeak_ng.h>
#include <sndfile.h>
@@ 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);