1 module hcspeech.hcspeech; 2 3 import std.algorithm; 4 import std.path; 5 import std.range; 6 import std.string; 7 8 import file = std.file; 9 import io = std.stdio; 10 11 import hexchat.plugin; 12 import speech.synthesis; 13 14 import hcspeech.base, hcspeech.commands; 15 16 /* 17 * ========================================= 18 * Command Setup 19 * ========================================= 20 */ 21 struct TTSAction 22 { 23 alias void function(in char[][], in char[][]) Callback; 24 25 Callback callback; 26 string usage; 27 size_t minArgs; 28 } 29 30 __gshared TTSAction[string] actions; 31 __gshared string[] actionList; // For ordered help output. 32 33 void addAction(string name, TTSAction.Callback callback, string usage, size_t minArgs = 0) 34 { 35 actions[name] = TTSAction(callback, usage, minArgs); 36 actionList ~= usage; 37 } 38 39 void addHelpSeparator() 40 { 41 actionList ~= ""; 42 } 43 44 shared static this() 45 { 46 addAction("help", &helpCommand, helpUsage); 47 addHelpSeparator(); 48 49 addAction("toggle", &toggleCommand, toggleUsage); 50 addAction("add", &addCommand, addUsage, 1); 51 addAction("remove", &removeCommand, removeUsage, 1); 52 addAction("list", &listCommand, listUsage); 53 addHelpSeparator(); 54 55 addAction("voicelist", &voiceListCommand, voiceListUsage); 56 addAction("assign", &assignCommand, assignUsage, 2); 57 addAction("unassign", &unassignCommand, unassignUsage, 1); 58 addAction("assignedvoices", &assignedVoicesCommand, assignedVoicesUsage); 59 addHelpSeparator(); 60 61 addAction("volume", &volumeCommand, volumeUsage); 62 addAction("rate", &rateCommand, rateUsage); 63 } 64 65 immutable ttsUsage = "Usage: TTS [action [args]], main TTS command. " ~ 66 "Toggles TTS for the current channel when given no arguments. " ~ 67 `Use "/TTS help" to see available actions.`; 68 69 EatMode ttsCommand(in char[][] words, in char[][] words_eol) 70 { 71 if(words.length == 1) 72 { 73 toggleCommand(words[1 .. $], words_eol[1 .. $]); 74 } 75 else 76 { 77 auto actionName = words[1]; 78 79 if(auto action = toLower(actionName) in actions) 80 { 81 auto args = words[1 .. $]; 82 auto args_eol = words_eol[1 .. $]; 83 84 if(args.length - 1 >= action.minArgs) 85 action.callback(args, args_eol); 86 else 87 { 88 writefln("Usage: %s", action.usage); 89 writefln(`Action "%s" requires at least %s argument(s).`, actionName, action.minArgs); 90 } 91 } 92 else 93 { 94 writefln(`Unknown TTS action "%s". Use "/TTS HELP" to see usage.`, actionName); 95 } 96 } 97 return EatMode.all; 98 } 99 100 immutable helpUsage = "HELP [action], display help information."; 101 102 void helpCommand(in char[][] words, in char[][] words_eol) 103 { 104 if(words.length > 1) 105 { 106 auto actionName = words[1]; 107 if(auto action = toLower(actionName) in actions) 108 { 109 writefln("Usage: %s", action.usage); 110 } 111 else 112 { 113 writefln(`Unknown TTS action "%s". Use "/TTS HELP" to see usage.`, actionName); 114 } 115 } 116 else 117 { 118 writefln("Usage: TTS [action [arguments]], perform the specified Text To Speech related action."); 119 writefln(""); 120 foreach(usage; actionList) 121 writefln(" %s", usage); 122 writefln(""); 123 writefln("When no action is specified, the TOGGLE action is performed."); 124 } 125 } 126 127 /* 128 * ========================================= 129 * TTS Hooks 130 * ========================================= 131 */ 132 EatMode onMessage(in char[][] words, in char[][] words_eol) 133 { 134 auto channelName = words[2]; 135 136 auto position = ttsChannels.find!((channel, name) => channel.name == name)(channelName); 137 if(position.empty) 138 return EatMode.none; 139 140 auto channel = position.front; 141 142 auto user = parseUser(words[0][1 .. $]); 143 const(char)[] message = words_eol[3]; 144 145 if(message.length > 0 && message[0] == ':') 146 message = message[1 .. $]; 147 148 tts.voice = getUserVoice(channel, user.nick); 149 tts.queue(message); 150 151 return EatMode.none; 152 } 153 154 /* 155 * ========================================= 156 * Persistent Settings 157 * ========================================= 158 */ 159 string configFilePath() 160 { 161 return buildPath(getInfo("xchatdir"), "hcspeech.conf"); 162 } 163 164 PluginInfo pluginInfo; 165 166 void loadSettings() 167 { 168 auto path = configFilePath(); 169 if(!file.exists(path)) 170 return; 171 172 auto file = io.File(path, "r"); 173 174 string nick; 175 176 foreach(line; file.byLine()) 177 { 178 auto stripped = line.strip(); 179 if(stripped.empty) 180 continue; 181 182 auto key = stripped.munch("^=").strip(); 183 184 stripped.popFront(); 185 auto value = stripped.strip(); 186 187 switch(key) 188 { 189 case "nick": 190 nick = value.idup; 191 break; 192 case "voice": 193 assert(nick !is null); 194 195 if(auto voice = voiceByName(value)) 196 specifiedUserVoices[nick] = *voice; 197 else 198 writefln(`No voice found for "%s", removing entry for user %s.`, value, nick); 199 break; 200 default: 201 } 202 } 203 } 204 205 void saveSettings() 206 { 207 auto file = io.File(configFilePath(), "w"); 208 209 void writeKey(in char[] key, in char[] value) 210 { 211 file.writefln("%s = %s", key, value); 212 } 213 214 writeKey("plugin_version", pluginInfo.version_); 215 file.writeln(); 216 217 foreach(nick, voice; specifiedUserVoices) 218 { 219 writeKey("nick", nick); 220 writeKey("voice", voice.name); 221 file.writeln(); 222 } 223 } 224 225 /* 226 * ========================================= 227 * Initialization 228 * ========================================= 229 */ 230 void init(ref PluginInfo info) 231 { 232 info.name = "hcspeech"; 233 info.description = "Text To Speech"; 234 info.version_ = (cast(string)import("version.txt")).chomp.stripLeft('v'); 235 pluginInfo = info; 236 237 tts = Synthesizer.create(); 238 allVoices = voiceList().array(); 239 240 loadSettings(); 241 242 hookCommand("tts", &ttsCommand, ttsUsage); 243 244 hookServer("PRIVMSG", &onMessage); 245 246 writefln("Text To Speech plugin (hcspeech %s) successfully loaded", info.version_); 247 } 248 249 void shutdown() 250 { 251 saveSettings(); 252 } 253 254 version(Windows) 255 { 256 import core.sys.windows.dll; 257 mixin SimpleDllMain; 258 } 259 260 mixin Plugin!(init, shutdown); 261