1 module hcspeech.commands;
2 
3 import std.algorithm;
4 import std.array;
5 import std.conv;
6 import std.range;
7 import std.string;
8 
9 import hexchat.plugin;
10 
11 import speech.synthesis;
12 
13 import hcspeech.base;
14 
15 /*
16  * =========================================
17  * Commands
18  * =========================================
19  */
20 // TTS context management
21 immutable toggleUsage = "TOGGLE, toggle TTS for the current channel.";
22 immutable addUsage = "ADD <channel>, start TTS in the specified channel.";
23 immutable removeUsage = "REMOVE <channel>, stop TTS in the specified channel.";
24 immutable listUsage = "LIST, list channels with TTS running.";
25 
26 void toggleCommand(in char[][] words, in char[][] words_eol)
27 {
28 	auto channel = getInfo("channel");
29 	auto position = ttsChannels.countUntil!((channel, name) => channel.name == name)(channel);
30 	if(position == -1)
31 	{
32 		ttsChannels ~= ChannelInfo(channel);
33 		writefln("Started TTS in %s.", channel);
34 	}
35 	else
36 	{
37 		ttsChannels = ttsChannels.remove!(SwapStrategy.unstable)(position);
38 		writefln("Stopped TTS in %s.", channel);
39 	}
40 }
41 
42 void addCommand(in char[][] words, in char[][] words_eol)
43 {
44 	auto channel = words[1];
45 
46 	auto search = ttsChannels.find!((channel, name) => channel.name == name)(channel);
47 	if(search.empty)
48 	{
49 		ttsChannels ~= ChannelInfo(channel.idup);
50 		writefln("Started TTS in %s.", channel);
51 	}
52 	else
53 	{
54 		writefln("TTS is already enabled in %s.", channel);
55 	}
56 }
57 
58 void removeCommand(in char[][] words, in char[][] words_eol)
59 {
60 	auto channel = words[1];
61 
62 	auto position = ttsChannels.countUntil!((channel, name) => channel.name == name)(channel);
63 	if(position != -1)
64 	{
65 		ttsChannels = ttsChannels.remove!(SwapStrategy.unstable)(position);
66 		writefln("Stopped TTS in %s.", channel);
67 	}
68 	else
69 	{
70 		writefln("%s is not in the list of TTS channels.", channel);
71 	}
72 }
73 
74 void listCommand(in char[][] words, in char[][] words_eol)
75 {
76 	if(ttsChannels.empty)
77 		writefln("TTS is not enabled for any channels.");
78 	else
79 		writefln("%(%s%|, %)", ttsChannels.map!(channel => channel.name));
80 }
81 
82 // Voice management
83 immutable voiceListUsage =      "VOICELIST, list available TTS voices.";
84 
85 immutable assignUsage =         "ASSIGN <nick> <voice name>, " ~
86                                 "assign a specific voice to a user by voice name.";
87 
88 immutable unassignUsage =       "UNASSIGN <nick>, unassign any previously " ~
89                                 "assigned voice for the specified user.";
90 
91 immutable assignedVoicesUsage = "ASSIGNEDVOICES [nick filter], list assigned voices.";
92 
93 
94 void voiceListCommand(in char[][] words, in char[][] words_eol)
95 {
96 	writefln("%-(%s%|, %)", allVoices.map!(v => v.name).enumerate(1).map!(pair => format("#%s %s", pair[0], pair[1])));
97 }
98 
99 void assignCommand(in char[][] words, in char[][] words_eol)
100 {
101 	auto nick = words[1];
102 	auto voiceSpecifier = words_eol[2];
103 
104 	auto voices = findVoices(voiceSpecifier);
105 	if(voices.empty)
106 	{
107 		writefln(`No voice found for specifier "%s". Use "/TTS VOICELIST" to list available voices.`, voiceSpecifier);
108 		return;
109 	}
110 
111 	auto firstVoice = voices.front;
112 	voices.popFront();
113 
114 	if(voices.empty) // Only one voice found
115 	{
116 		specifiedUserVoices[nick.idup] = firstVoice;
117 		writefln("Assigned voice to %s: %s", nick, firstVoice.name);
118 	}
119 	else // Ambiguous search
120 	{
121 		writefln("Specified name matches multiple voices. Which did you mean?");
122 		writefln("%-(%s%|, %)", only(firstVoice).chain(voices).map!(v => v.name));
123 	}
124 }
125 
126 void unassignCommand(in char[][] words, in char[][] words_eol)
127 {
128 	auto nick = words[1];
129 	if(nick in specifiedUserVoices)
130 	{
131 		specifiedUserVoices.remove(nick.idup); // Ew, idup :(
132 		writefln("Voice cleared for user %s.", nick);
133 	}
134 	else
135 	{
136 		writefln("User %s doesn't have a manually assigned voice.", nick);
137 	}
138 }
139 
140 void assignedVoicesCommand(in char[][] words, in char[][] words_eol)
141 {
142 	if(words.length < 2)
143 	{
144 		if(specifiedUserVoices.length == 0)
145 			writefln("There are no assigned voices.");
146 		else
147 			writefln("%-(%s%|, %)", specifiedUserVoices.byPair.map!(pair => format("%s: %s", pair[0], pair[1].name)));
148 	}
149 	else
150 	{
151 		auto matches = specifiedUserVoices.byPair.filter!(pair => pair[0].asLowerCase.canFind(words[1].asLowerCase));
152 
153 		if(matches.empty)
154 			writefln(`Found no assigned voices with the search "%s".`, words[1]);
155 		else
156 			writefln("%-(%s%|, %)", matches.map!(pair => format("%s: %s", pair[0], pair[1].name)));
157 	}
158 }
159 
160 // TTS configuration management
161 immutable volumeUsage = "VOLUME [new volume in the range 0-100], set or display TTS volume.";
162 immutable rateUsage = "RATE [new rate], set or display TTS rate.";
163 
164 void volumeCommand(in char[][] words, in char[][] words_eol)
165 {
166 	if(words.length > 1)
167 	{
168 		uint volume;
169 		try volume = to!uint(words[1]);
170 		catch(ConvException e)
171 		{
172 			writefln("Volume must be a positive number.");
173 			return;
174 		}
175 
176 		if(volume > 100)
177 			writefln("Volume level must be in the range 0-100.");
178 		else
179 		{
180 			tts.volume = volume;
181 			writefln("TTS volume is now %s/100.", tts.volume);
182 		}
183 	}
184 	else
185 	{
186 		writefln("TTS volume is %s/100.", tts.volume);
187 	}
188 }
189 
190 void rateCommand(in char[][] words, in char[][] words_eol)
191 {
192 	if(words.length > 1)
193 	{
194 		int rate;
195 		try rate = to!int(words[1]);
196 		catch(ConvException e)
197 		{
198 			writefln("Rate must be a number.");
199 			return;
200 		}
201 
202 		tts.rate = rate;
203 		writefln("TTS rate is now %s.", tts.rate);
204 	}
205 	else
206 	{
207 		writefln("TTS rate is %s.", tts.rate);
208 	}
209 }
210