node-red-contrib-yandex-station-management 0.2.7 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -8
- package/nodes/get.js +28 -16
- package/nodes/in.js +30 -18
- package/nodes/local-out.html +245 -133
- package/nodes/local-out.js +107 -11
- package/nodes/locales/en-US/local-out.html +46 -0
- package/nodes/locales/en-US/local-out.json +72 -0
- package/nodes/locales/ru-RU/local-out.html +43 -0
- package/nodes/locales/ru-RU/local-out.json +69 -0
- package/nodes/yandex-login.js +44 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
С помощью модуля можно через локальное API управлять вопсроизведением на устройсвах Яндекса:
|
|
3
3
|
- Яндекс Станция(протестировано)
|
|
4
4
|
- Яндекс Станция мини(протестировано)
|
|
5
|
+
- Яндекс Станция мини 2 с экраном(протестировано)
|
|
6
|
+
- Яндекс Станция лайт(протестировано)
|
|
5
7
|
- Яндекс Станци Макс(протестировано)
|
|
6
8
|
- Яндекс Модуль(не протестировано)
|
|
9
|
+
- Яндекс Модуль - 2 (в процессе тестирования)
|
|
7
10
|
- JBL Link Music(не протестировано)
|
|
8
11
|
- JBL Link Portable(протестировано)
|
|
9
12
|
|
|
@@ -14,7 +17,7 @@
|
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
Для работы требуется токен от Яндекс.Музыки.
|
|
17
|
-
В модуле в экспериментальном режиме реализована возможность получения токена из логин-пароля(Спасибо слать [сюда](https://github.com/twocolors)). Если получение токена не отрабатывает, то стоит попробовать включить и отключить двух-факторную аутентификацию в настройках Яндекса. [Источник](https://github.com/AlexxIT/YandexStation/issues/103). Убедиться в безопасности использования учетных данных
|
|
20
|
+
В модуле в экспериментальном режиме реализована возможность получения токена из логин-пароля(Спасибо слать [сюда](https://github.com/twocolors)). Если получение токена не отрабатывает, то стоит попробовать включить и отключить двух-факторную аутентификацию в настройках Яндекса. [Источник](https://github.com/AlexxIT/YandexStation/issues/103). Убедиться в безопасности использования учетных данных можно, посмотрев [код](./nodes/yandex-login.html)
|
|
18
21
|
|
|
19
22
|
Второй из варинатов его получения описан в [FAQ](#faq)
|
|
20
23
|
|
|
@@ -36,7 +39,9 @@
|
|
|
36
39
|
## Первоначальная настройка.
|
|
37
40
|
После установки для начала работы добавить любую ноду, ввести учетные данные(токен) в раздел Login, сохранить и нажать Deploy(обязательно!). Как получить токен - написано в FAQ.
|
|
38
41
|
|
|
39
|
-
После деплоя в настройках ноды в поле Station должны появиться станции доступные для управления.
|
|
42
|
+
После деплоя в настройках ноды в поле Station должны появиться станции доступные для управления.
|
|
43
|
+
|
|
44
|
+
Если станция не появилась в списке, то можно подождать пару минут или перезапустить Node-Red.
|
|
40
45
|
|
|
41
46
|
## Описание возможностей и сценариев использования.
|
|
42
47
|
### Нода Station
|
|
@@ -109,13 +114,54 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
109
114
|
Отправка команды, вместо того, чтобы говорить ее колонке голосом: "Включи свет", "Включи музыку", "Включи мой плейлист", "Отключись через 15 минут" и так далее.
|
|
110
115
|
|
|
111
116
|
#### TTS.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
117
|
+
Воспроизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.
|
|
118
|
+
Параметры для TTS могут задаваться как в настройках, так и некоторые из них могут быть переопределены входящим сообщением.
|
|
119
|
+
- Text. Откуда и какой текст проговорить.
|
|
120
|
+
- из входящего сообщения (msg.payload по-умолчанию)
|
|
121
|
+
- строго заданную строку
|
|
122
|
+
- переменную из flow или global context
|
|
123
|
+
- JSON: выбрать сообщение случайным образом из массива вида ["один", "два", "три"].
|
|
124
|
+
- Voice. Выбор голоса для генерации. Может быть переопределено через msg.voice
|
|
125
|
+
- Effect. Эффекты для генерации голоса. Может быть переопределено через msg.effect
|
|
126
|
+
-
|
|
127
|
+
Есть ряд опций:
|
|
128
|
+
- Volume. Позволяет произносить фразу заданной громкостью. Если не выбрано, то фраза произносится с текущим уровнем громкости. После произнесения уровень громкости вернется в изначальный. Может быть переопределено через msg.volume
|
|
129
|
+
- Whisper. Позволяет произнести фразу шептом.. Переопределяется через msg.whisper
|
|
130
|
+
- Prevent listening. Если выбрано, то колонка после воспроизведения не "слушает", что ей ответят. Может быть переопределено через msg.prevent_listening
|
|
131
|
+
- Pause while TTS. Ставит воспроизведение плеера на паузу на время речи. Воспроизведение будет продолжено, только если что-то играло на момент поступления команды. Может быть переопределено через msg.pause_music
|
|
116
132
|
Все опции комбинируемы между собой.
|
|
117
133
|
|
|
118
|
-
|
|
134
|
+
##### Добавление голосу жизни и красок.
|
|
135
|
+
###### Раставляйте ударения
|
|
136
|
+
При необходимости ударные гласные в словах следует отмечать знаком «+», например:
|
|
137
|
+
|
|
138
|
+
остр+ота
|
|
139
|
+
м+ука
|
|
140
|
+
|
|
141
|
+
##### Разделяйте слова
|
|
142
|
+
Длинные слова можно разбить на слова покороче и проставить ударения для каждого из этих коротких слов, например:
|
|
143
|
+
|
|
144
|
+
мн+ого пр+офильный
|
|
145
|
+
с+еми пал+атинск
|
|
146
|
+
|
|
147
|
+
##### Меняйте написание слов
|
|
148
|
+
Некоторые слова можно попробовать писать так, как они слышатся:
|
|
149
|
+
|
|
150
|
+
«ненастный» — нен+асный
|
|
151
|
+
«пожалуйста» — пож+алуста
|
|
152
|
+
|
|
153
|
+
###### Добавляйте паузы
|
|
154
|
+
Чтобы задать паузу между словами, используйте синтаксис sil <[ количество_миллисекунд ]>. Например:
|
|
155
|
+
|
|
156
|
+
смелость sil <[500]> город+а берёт
|
|
157
|
+
|
|
158
|
+
Каждый отделенный пробелами пунктуационный знак обозначается паузой в 50-100 мс.
|
|
159
|
+
|
|
160
|
+
###### Добавляйте [звуки из библиотеки](https://yandex.ru/dev/dialogs/alice/doc/sounds-docpage/)
|
|
161
|
+
|
|
162
|
+
<speaker audio=\"alice-sounds-game-win-1.opus\"> У вас получилось!
|
|
163
|
+
|
|
164
|
+
|
|
119
165
|
|
|
120
166
|
|
|
121
167
|
#### Homekit Formatted.
|
|
@@ -191,6 +237,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
191
237
|
}
|
|
192
238
|
```
|
|
193
239
|
10. Отправить "Текст" для TTS.
|
|
240
|
+
Больше не работает!
|
|
194
241
|
```json
|
|
195
242
|
{
|
|
196
243
|
"command" : "sendText",
|
|
@@ -259,7 +306,10 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
259
306
|
```json
|
|
260
307
|
"value": "<speaker audio='alice-sounds-game-win-1.opus'>У вас получилось!"
|
|
261
308
|
```
|
|
262
|
-
|
|
309
|
+
- Говорить шепотом
|
|
310
|
+
```json
|
|
311
|
+
"value": "<speaker is_whisper="true"'>Я говорю тихо-тихо!"
|
|
312
|
+
```
|
|
263
313
|
- Совмещение эффектов
|
|
264
314
|
```json
|
|
265
315
|
"value": "<speaker voice='kostya' audio='alice-sounds-game-win-1.opus' effect='megaphone'>добро пожаловать"
|
package/nodes/get.js
CHANGED
|
@@ -10,7 +10,7 @@ module.exports = function(RED) {
|
|
|
10
10
|
node.lastState = {};
|
|
11
11
|
node.status({});
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
function debugMessage(text){
|
|
15
15
|
if (node.debugFlag) {
|
|
16
16
|
node.log(text);
|
|
@@ -21,25 +21,37 @@ module.exports = function(RED) {
|
|
|
21
21
|
if (node.output == 'status') {
|
|
22
22
|
inputMsg.payload = message;
|
|
23
23
|
} else if (node.output == 'homekit') {
|
|
24
|
-
|
|
25
|
-
(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
try {
|
|
25
|
+
if (node.homekitFormat == 'speaker') {
|
|
26
|
+
let ConfiguredName = `${(message.playerState.subtitle) ? message.playerState.subtitle : 'No Artist'} - ${(message.playerState.title) ? message.playerState.title : 'No Track Name'}`;
|
|
27
|
+
let title = `${message.playerState.title}`;
|
|
28
|
+
if (ConfiguredName.length > 64 && title.length > 0 && title.length <= 64) {
|
|
29
|
+
ConfiguredName = title;
|
|
30
|
+
} else {
|
|
31
|
+
ConfiguredName = title.substr(0, 61) + `...`;
|
|
32
|
+
}
|
|
33
|
+
(message.playerState)? inputMsg.payload = {
|
|
34
|
+
"CurrentMediaState": (message.playing) ? 0 : 1,
|
|
35
|
+
"ConfiguredName": ConfiguredName
|
|
36
|
+
} :inputMsg.payload = {
|
|
37
|
+
"CurrentMediaState": (message.playing) ? 0 : 1,
|
|
38
|
+
"ConfiguredName": `No Artist - No Track Name`
|
|
39
|
+
}
|
|
40
|
+
}else if (node.homekitFormat == 'tv') {
|
|
41
|
+
inputMsg.payload = {
|
|
42
|
+
"Active": (message.playing) ? 1 : 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
debugMessage(`Error while preparing payload: `+ e);
|
|
37
48
|
}
|
|
49
|
+
|
|
38
50
|
}
|
|
39
51
|
return inputMsg;
|
|
40
52
|
|
|
41
53
|
}
|
|
42
|
-
|
|
54
|
+
|
|
43
55
|
|
|
44
56
|
node.onStatus = function(data) {
|
|
45
57
|
if (data) {
|
|
@@ -57,7 +69,7 @@ module.exports = function(RED) {
|
|
|
57
69
|
node.onClose = function(){
|
|
58
70
|
node.controller.removeListener(`message_${node.station}`, node.onMessage)
|
|
59
71
|
}
|
|
60
|
-
|
|
72
|
+
|
|
61
73
|
node.on('input', node.onInput);
|
|
62
74
|
|
|
63
75
|
node.on('close', node.onClose)
|
package/nodes/in.js
CHANGED
|
@@ -10,9 +10,9 @@ module.exports = function(RED) {
|
|
|
10
10
|
node.homekitFormat = config.homekitFormat;
|
|
11
11
|
node.lastMessage = {};
|
|
12
12
|
node.status({});
|
|
13
|
-
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
|
|
16
16
|
debugMessage(`Node settings: ID: ${node.station}, Output Format: ${node.output}, HK: ${node.homekitFormat}`);
|
|
17
17
|
function debugMessage(text){
|
|
18
18
|
if (node.debugFlag) {
|
|
@@ -26,20 +26,32 @@ module.exports = function(RED) {
|
|
|
26
26
|
if (node.output == 'status') {
|
|
27
27
|
payload = {'payload': message}
|
|
28
28
|
} else if (node.output == 'homekit') {
|
|
29
|
-
|
|
30
|
-
(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
try {
|
|
30
|
+
if (node.homekitFormat == 'speaker') {
|
|
31
|
+
let ConfiguredName = `${(message.playerState.subtitle) ? message.playerState.subtitle : 'No Artist'} - ${(message.playerState.title) ? message.playerState.title : 'No Track Name'}`;
|
|
32
|
+
let title = `${message.playerState.title}`;
|
|
33
|
+
if (ConfiguredName.length > 64 && title.length > 0 && title.length <= 64) {
|
|
34
|
+
ConfiguredName = title;
|
|
35
|
+
} else {
|
|
36
|
+
ConfiguredName = title.substr(0, 61) + `...`;
|
|
37
|
+
}
|
|
38
|
+
(message.playerState)? payload = {'payload': {
|
|
39
|
+
"CurrentMediaState": (message.playing) ? 0 : 1,
|
|
40
|
+
"ConfiguredName": ConfiguredName
|
|
41
|
+
} }:payload = {'payload': {
|
|
42
|
+
"CurrentMediaState": (message.playing) ? 0 : 1,
|
|
43
|
+
"ConfiguredName": `No Artists - No Track Name`
|
|
44
|
+
} }
|
|
45
|
+
}else if (node.homekitFormat == 'tv') {
|
|
46
|
+
payload = {'payload': {
|
|
47
|
+
"Active": (message.playing) ? 1 : 0
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch(e) {
|
|
52
|
+
debugMessage(`Error while preparing payload: `+ e)
|
|
42
53
|
}
|
|
54
|
+
|
|
43
55
|
}
|
|
44
56
|
return payload;
|
|
45
57
|
|
|
@@ -50,8 +62,8 @@ module.exports = function(RED) {
|
|
|
50
62
|
if ((JSON.stringify(node.lastMessage.payload) != JSON.stringify(message.payload))) {
|
|
51
63
|
node.send(message)
|
|
52
64
|
node.lastMessage = message
|
|
53
|
-
debugMessage(`Sended message to HK: ${JSON.stringify(message)}`);
|
|
54
|
-
|
|
65
|
+
debugMessage(`Sended message to HK: ${JSON.stringify(message)}`);
|
|
66
|
+
|
|
55
67
|
}
|
|
56
68
|
} else {
|
|
57
69
|
node.send(message);
|
|
@@ -77,7 +89,7 @@ module.exports = function(RED) {
|
|
|
77
89
|
node.controller.on(`message_${node.station}`, node.onMessage);
|
|
78
90
|
node.controller.on(`statusUpdate_${node.station}`, node.onStatus);
|
|
79
91
|
}
|
|
80
|
-
|
|
92
|
+
|
|
81
93
|
node.on('close', node.onClose);
|
|
82
94
|
|
|
83
95
|
|
package/nodes/local-out.html
CHANGED
|
@@ -1,169 +1,281 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
2
|
+
|
|
3
|
+
RED.nodes.registerType('alice-local-out', {
|
|
4
|
+
category: 'Yandex Station',
|
|
5
|
+
color: '#b89fcc',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: {value: ''},
|
|
8
|
+
token: {
|
|
9
|
+
type: 'yandex-login',
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
station_id: {
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
debugFlag: {
|
|
16
|
+
value: false,
|
|
17
|
+
},
|
|
18
|
+
input: {
|
|
19
|
+
value: 'command',
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
payload: {
|
|
23
|
+
value: 'payload',
|
|
24
|
+
},
|
|
25
|
+
payloadType: {
|
|
26
|
+
value: 'msg',
|
|
27
|
+
},
|
|
28
|
+
volume: {},
|
|
29
|
+
volumeFlag: {
|
|
30
|
+
value: false,
|
|
31
|
+
},
|
|
32
|
+
stopListening: {
|
|
33
|
+
value: true,
|
|
34
|
+
},
|
|
35
|
+
pauseMusic: {
|
|
36
|
+
value: false,
|
|
37
|
+
},
|
|
38
|
+
noTrack: {},
|
|
39
|
+
whisper: {
|
|
40
|
+
value: false,
|
|
41
|
+
},
|
|
42
|
+
ttsVoice: {
|
|
43
|
+
value: null,
|
|
44
|
+
},
|
|
45
|
+
ttsEffect: {
|
|
46
|
+
value: null,
|
|
47
|
+
required: false,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
inputs: 1,
|
|
51
|
+
outputs: 0,
|
|
52
|
+
icon: 'station.png',
|
|
53
|
+
label: function() {
|
|
54
|
+
return this.name || this.station_id;
|
|
55
|
+
},
|
|
56
|
+
paletteLabel: 'Yandex OUT',
|
|
57
|
+
oneditprepare: onOpen,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function onOpen() {
|
|
61
|
+
var command = $('#node-input-input').val();
|
|
62
|
+
|
|
63
|
+
$('#node-input-payload').typedInput({
|
|
64
|
+
types: ['msg', 'str', 'flow', 'global', 'json'],
|
|
65
|
+
default: 'msg',
|
|
66
|
+
value: 'payload',
|
|
67
|
+
typeField: $('#node-input-payloadType'),
|
|
43
68
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
$("#node-input-ttsEffect").typedInput({type:"ttsEffect", types:[{
|
|
73
|
+
value: 'ttsEffect',
|
|
74
|
+
multiple: false,
|
|
75
|
+
options: [
|
|
76
|
+
{ value: "", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.none")},
|
|
77
|
+
{ value: "behind_the_wall", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.behind_the_wall")},
|
|
78
|
+
{ value: "hamster", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.hamster")},
|
|
79
|
+
{ value: "megaphone", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.megaphone")},
|
|
80
|
+
{ value: "pitch_down", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.pitch_down")},
|
|
81
|
+
{ value: "psychodelic", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.psychodelic")},
|
|
82
|
+
{ value: "pulse", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.pulse")},
|
|
83
|
+
{ value: "train_announce", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.train_announce")}
|
|
84
|
+
]
|
|
85
|
+
}]})
|
|
86
|
+
|
|
87
|
+
let config = RED.nodes.node($('#node-input-token').val());
|
|
88
|
+
let selector = $('#node-input-station_id');
|
|
89
|
+
selector.empty();
|
|
90
|
+
let currentId = this.station_id;
|
|
91
|
+
$.getJSON('yandexdevices_' + config.id, function(data) {
|
|
92
|
+
data.devices.forEach(device => {
|
|
93
|
+
selector.append(`<option value="${device.id}">
|
|
52
94
|
${device.name}(${device.id})
|
|
53
|
-
</option>`)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
$('#node-input-volume').on('change', function() {
|
|
93
|
-
$('#volume-level').text(`${parseFloat($('#node-input-volume').val())*100}`)
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
function updateDevices() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
95
|
+
</option>`);
|
|
96
|
+
$(`#node-input-station_id :contains(${currentId})`).attr('selected', 'selected');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
$('.command_options').hide();
|
|
101
|
+
$('.command_options-' + command).show();
|
|
102
|
+
|
|
103
|
+
$('#node-input-input').on('change', function(type, value) {
|
|
104
|
+
$('.command_options').hide();
|
|
105
|
+
$('.command_options-' + $(this).val()).show();
|
|
106
|
+
|
|
107
|
+
if ($(this).val() == 'tts') {
|
|
108
|
+
if ($('#node-input-volumeFlag').prop('checked')) {
|
|
109
|
+
$('#node-input-volume').show();
|
|
110
|
+
$('#range-label').show();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
$('#node-input-volumeFlag').on('change', function(type, value) {
|
|
116
|
+
let val = $(this).val();
|
|
117
|
+
if ($('#node-input-input').val() == 'tts' && $(this).prop('checked')) {
|
|
118
|
+
$('#node-input-volume').show();
|
|
119
|
+
$('#range-label').show();
|
|
120
|
+
|
|
121
|
+
} else if ($('#node-input-input').val() == 'tts' && !$(this).prop('checked')) {
|
|
122
|
+
$('#node-input-volume').hide();
|
|
123
|
+
$('#range-label').hide();
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
$('#node-input-volume').on('change', function() {
|
|
128
|
+
$('#volume-level').text(`${parseFloat($('#node-input-volume').val())}`);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
100
131
|
</script>
|
|
101
132
|
|
|
102
133
|
<script type="text/html" data-template-name="alice-local-out">
|
|
103
134
|
<style>
|
|
104
|
-
.label
|
|
105
|
-
|
|
106
|
-
|
|
135
|
+
label.label {
|
|
136
|
+
line-height: 1em;
|
|
137
|
+
}
|
|
138
|
+
label.label-long {
|
|
139
|
+
min-width: 150px;
|
|
140
|
+
width: 20%;
|
|
141
|
+
}
|
|
142
|
+
.online {
|
|
143
|
+
display: inline-block;
|
|
144
|
+
width: auto;
|
|
145
|
+
vertical-align: middle;
|
|
146
|
+
}
|
|
147
|
+
</style>
|
|
107
148
|
|
|
108
149
|
<div class="form-row">
|
|
109
|
-
<label for="node-input-name"><i class="fa fa-bookmark"></i
|
|
110
|
-
<input type="text" id="node-input-name"
|
|
150
|
+
<label for="node-input-name"><i class="fa fa-bookmark"></i> <span data-i18n="label.name"></span></label>
|
|
151
|
+
<input type="text" id="node-input-name">
|
|
111
152
|
</div>
|
|
112
153
|
<div class="form-row">
|
|
113
|
-
<label for="node-input-token"><i class="fa fa-globe"></i
|
|
114
|
-
<input type="text" id="node-input-token" placeholder="
|
|
154
|
+
<label for="node-input-token"><i class="fa fa-globe"></i> <span data-i18n="label.login"></span></label>
|
|
155
|
+
<input type="text" id="node-input-token" placeholder="">
|
|
115
156
|
</div>
|
|
116
157
|
<div class="form-row">
|
|
117
|
-
<label for="node-input-station_id"><i class="fa fa-database"
|
|
158
|
+
<label for="node-input-station_id"><i class="fa fa-database"></i> <span data-i18n="label.station"></span></label>
|
|
118
159
|
<div style="display: inline-block;position: relative;width: 70%;height: 20px;">
|
|
119
|
-
<div style="position: absolute;left:
|
|
160
|
+
<div style="position: absolute;left: 0; right: 40px;">
|
|
120
161
|
<select id="node-input-station_id" data-single="true" style="width: 100%"></select>
|
|
121
|
-
</div>
|
|
162
|
+
</div>
|
|
122
163
|
<div style="text-align: end; display: inline; float: right">
|
|
123
|
-
<button onclick="onOpen()" class="red-ui-button" style="position: absolute;right: 0px;top: 0px;"
|
|
164
|
+
<button onclick="onOpen()" class="red-ui-button" style="position: absolute;right: 0px;top: 0px;">
|
|
165
|
+
<i class="fa fa-refresh"></i></button>
|
|
124
166
|
</div>
|
|
125
167
|
</div>
|
|
126
168
|
</div>
|
|
127
169
|
<div class="form-row">
|
|
128
|
-
<label for="node-input-input"><i class="fa fa-arrow-circle-o-right"
|
|
170
|
+
<label for="node-input-input"><i class="fa fa-arrow-circle-o-right"></i> <span data-i18n="label.command"></span></label>
|
|
129
171
|
<div style="display: inline-block;position: relative;width: 70%;height: 20px;">
|
|
130
172
|
<div style="position: absolute;left: 0px; right: 0px;">
|
|
131
173
|
<select id="node-input-input" data-single="true" style="width: 100%">
|
|
132
|
-
<option value="command"
|
|
133
|
-
<option value="voice"
|
|
134
|
-
<option value="tts"
|
|
135
|
-
<option value="homekit"
|
|
136
|
-
<option value="
|
|
137
|
-
<option value="
|
|
174
|
+
<option value="command" data-i18n="command.player"></option>
|
|
175
|
+
<option value="voice" data-i18n="command.voice"></option>
|
|
176
|
+
<option value="tts" data-i18n="command.tts"></option>
|
|
177
|
+
<option value="homekit" data-i18n="command.homekit"></option>
|
|
178
|
+
<option value="stopListening" data-i18n="command.stop_listening"></option>
|
|
179
|
+
<option value="raw" data-i18n="command.raw"></option>
|
|
138
180
|
</select>
|
|
139
|
-
|
|
181
|
+
</div>
|
|
140
182
|
</div>
|
|
141
183
|
</div>
|
|
142
184
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<
|
|
146
|
-
<input type="
|
|
185
|
+
<!--for homekit-->
|
|
186
|
+
<div class="form-row command_options command_options-homekit">
|
|
187
|
+
<label for="node-input-noTrack" class="label-long" style="display: inline-block"><i class="fa fa-podcast"></i> <span data-i18n="label.default_command"></span></label>
|
|
188
|
+
<input type="text" id="node-input-noTrack" data-i18n="[placeholder]placeholder.play_music" style="display: inline-block;left: 0px; right: 0px; width: 60%; vertical-align: middle;">
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!--for tts-->
|
|
192
|
+
<div class="form-row command_options command_options-tts">
|
|
193
|
+
<label for="node-input-payload" class="l-width"><i class="fa fa-envelope"></i> <span data-i18n="label.text"></span></label>
|
|
194
|
+
<input type="text" id="node-input-payload" style="width:70%">
|
|
195
|
+
<input type="hidden" id="node-input-payloadType">
|
|
196
|
+
</div>
|
|
197
|
+
<div class="form-row command_options command_options-tts">
|
|
198
|
+
<label class="label" for='node-input-ttsVoice'>
|
|
199
|
+
<i class='fa fa-user'></i> <span data-i18n="label.voice"></span>
|
|
200
|
+
<div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.voice</div></label>
|
|
201
|
+
</label>
|
|
202
|
+
<div style="display: inline-block;position: relative;width: 70%;height: 30px;">
|
|
203
|
+
<div style="position: absolute;left: 0px; right: 0px;">
|
|
204
|
+
<select id="node-input-ttsVoice" style="width: 100%;vertical-align: top;">
|
|
205
|
+
<option value="" data-i18n="voice.default"></option>
|
|
206
|
+
<!-- <option value="alena" data-i18n="voice.alena"></option>-->
|
|
207
|
+
<option value="alyss" data-i18n="voice.alyss"></option>
|
|
208
|
+
<option value="anton_samokhvalov" data-i18n="voice.anton_samokhvalov"></option>
|
|
209
|
+
<option value="dude" data-i18n="voice.dude"></option>
|
|
210
|
+
<option value="ermil" data-i18n="voice.ermil"></option>
|
|
211
|
+
<option value="ermilov" data-i18n="voice.ermilov"></option>
|
|
212
|
+
<option value="ermil_with_tuning" data-i18n="voice.ermil_with_tuning"></option>
|
|
213
|
+
<option value="erkanyavas" data-i18n="voice.erkanyavas"></option>
|
|
214
|
+
<!-- <option value="filipp" data-i18n="voice.filipp"></option>-->
|
|
215
|
+
<option value="jane" data-i18n="voice.jane"></option>
|
|
216
|
+
<option value="kolya" data-i18n="voice.kolya"></option>
|
|
217
|
+
<option value="kostya" data-i18n="voice.kostya"></option>
|
|
218
|
+
<option value="levitan" data-i18n="voice.levitan"></option>
|
|
219
|
+
<option value="nastya" data-i18n="voice.nastya"></option>
|
|
220
|
+
<option value="nick" data-i18n="voice.nick"></option>
|
|
221
|
+
<option value="oksana" data-i18n="voice.oksana"></option>
|
|
222
|
+
<option value="omazh" data-i18n="voice.omazh"></option>
|
|
223
|
+
<option value="robot" data-i18n="voice.robot"></option>
|
|
224
|
+
<option value="sasha" data-i18n="voice.sasha"></option>
|
|
225
|
+
<option value="silaerkan" data-i18n="voice.silaerkan"></option>
|
|
226
|
+
<option value="smoky" data-i18n="voice.smoky"></option>
|
|
227
|
+
<option value="tanya" data-i18n="voice.tanya"></option>
|
|
228
|
+
<option value="tatyana_abramova" data-i18n="voice.tatyana_abramova"></option>
|
|
229
|
+
<option value="zahar" data-i18n="voice.zahar"></option>
|
|
230
|
+
<option value="zhenya" data-i18n="voice.zhenya"></option>
|
|
231
|
+
<option value="zombie" data-i18n="voice.zombie"></option>
|
|
232
|
+
<option value="voicesearch" data-i18n="voice.voicesearch"></option>
|
|
233
|
+
</select>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="form-row command_options command_options-tts">
|
|
238
|
+
<label for="node-input-ttsEffect" class="label l-width" style="vertical-align: bottom;">
|
|
239
|
+
<i class="fa fa-magic"></i> <span data-i18n="label.effect"></span>
|
|
240
|
+
<div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.effect</div></label>
|
|
241
|
+
</label>
|
|
242
|
+
<input type="text" id="node-input-ttsEffect">
|
|
243
|
+
</div>
|
|
244
|
+
<div class="form-row command_options command_options-tts">
|
|
245
|
+
<label for="node-input-volume" class="label label-long">
|
|
246
|
+
<i class="fa fa-volume-up"></i> <span data-i18n="label.volume"></span>
|
|
247
|
+
<div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.volume</div></label>
|
|
248
|
+
</label>
|
|
249
|
+
<input type="checkbox" id="node-input-volumeFlag" style="display: inline-block; width: auto; vertical-align: top;">
|
|
250
|
+
<input type="range" id="node-input-volume" name="volume" min="0" max="100" step="1" style="display: inline-block; width: 150px; margin-left: 10px; vertical-align: middle;">
|
|
147
251
|
<div id="range-label" class='online'><span id="volume-level" class="online"></span><span class="online">%</span></div>
|
|
148
252
|
</div>
|
|
149
|
-
<div class="form-row
|
|
150
|
-
<label for="node-input-
|
|
151
|
-
|
|
253
|
+
<div class="form-row command_options command_options-tts">
|
|
254
|
+
<label for="node-input-whisper" class="label label-long">
|
|
255
|
+
<i class="fa fa-deaf"></i> <span data-i18n="label.whisper"></span>
|
|
256
|
+
<div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.whisper</div></label>
|
|
257
|
+
</label>
|
|
258
|
+
<input type="checkbox" id="node-input-whisper" style="display: inline-block; width: auto; vertical-align: top;">
|
|
152
259
|
</div>
|
|
153
|
-
<div class="form-row
|
|
154
|
-
<label for="node-input-
|
|
155
|
-
|
|
260
|
+
<div class="form-row command_options command_options-tts">
|
|
261
|
+
<label for="node-input-stopListening" class="label label-long">
|
|
262
|
+
<i class="fa fa-deaf"></i> <span data-i18n="label.prevent_listening"></span>
|
|
263
|
+
<div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.prevent_listening</div></label>
|
|
264
|
+
</label>
|
|
265
|
+
<input type="checkbox" id="node-input-stopListening" style="display: inline-block; width: auto; vertical-align: top;">
|
|
156
266
|
</div>
|
|
157
|
-
<div class="form-row
|
|
158
|
-
<label for="node-input-
|
|
159
|
-
|
|
267
|
+
<div class="form-row command_options command_options-tts">
|
|
268
|
+
<label for="node-input-pauseMusic" class="label label-long">
|
|
269
|
+
<i class="fa fa-pause"></i> <span data-i18n="label.pause_while_tts"></span>
|
|
270
|
+
<div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.pause_music</div></label>
|
|
271
|
+
</label>
|
|
272
|
+
<input type="checkbox" id="node-input-pauseMusic" style="display: inline-block; width: auto; vertical-align: top;">
|
|
160
273
|
</div>
|
|
274
|
+
|
|
275
|
+
|
|
161
276
|
<div class="form-row" style="vertical-align:bottom;">
|
|
162
|
-
<label for='node-input-debugFlag'><i class='fa fa-share-square'
|
|
163
|
-
<input type="checkbox" id="node-input-debugFlag" checked="checked" style="display: inline-block; width: auto; vertical-align: top;">
|
|
277
|
+
<label for='node-input-debugFlag' class="label-long"><i class='fa fa-share-square'></i> <span data-i18n="label.debug"></span></label>
|
|
278
|
+
<input type="checkbox" id="node-input-debugFlag" checked="checked" style="display: inline-block; width: auto; vertical-align: top;">
|
|
164
279
|
</div>
|
|
165
280
|
</script>
|
|
166
281
|
|
|
167
|
-
<script type="text/html" data-help-name="alice-local-out">
|
|
168
|
-
<p> Output node for Yandex Station local management</p>
|
|
169
|
-
</script>
|
package/nodes/local-out.js
CHANGED
|
@@ -2,7 +2,9 @@ module.exports = function(RED) {
|
|
|
2
2
|
function AliceLocalOutNode(config) {
|
|
3
3
|
RED.nodes.createNode(this,config);
|
|
4
4
|
let node = this;
|
|
5
|
+
node.config = config;
|
|
5
6
|
node.controller = RED.nodes.getNode(config.token);
|
|
7
|
+
|
|
6
8
|
node.input = config.input;
|
|
7
9
|
node.station = config.station_id;
|
|
8
10
|
node.debugFlag = config.debugFlag;
|
|
@@ -11,6 +13,9 @@ module.exports = function(RED) {
|
|
|
11
13
|
node.stopListening = config.stopListening;
|
|
12
14
|
node.noTrackPhrase = config.noTrack;
|
|
13
15
|
node.pauseMusic = config.pauseMusic;
|
|
16
|
+
node.ttsVoice = config.ttsVoice;
|
|
17
|
+
node.ttsEffect = config.ttsEffect;
|
|
18
|
+
node.whisper = config.whisper;
|
|
14
19
|
node.status({});
|
|
15
20
|
|
|
16
21
|
function debugMessage(text){
|
|
@@ -19,15 +24,106 @@ module.exports = function(RED) {
|
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
debugMessage(node.station);
|
|
22
|
-
node.on('input', (
|
|
23
|
-
debugMessage(`input: ${JSON.stringify(
|
|
24
|
-
|
|
25
|
-
if (node.stopListening) {data.stopListening = node.stopListening}
|
|
26
|
-
if (node.noTrackPhrase) {data.noTrackPhrase = node.noTrackPhrase}
|
|
27
|
-
if (node.pauseMusic) {data.pauseMusic = node.pauseMusic}
|
|
27
|
+
node.on('input', (input) => {
|
|
28
|
+
debugMessage(`input: ${JSON.stringify(input)}`)
|
|
29
|
+
|
|
28
30
|
if (node.station) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
var data = {};
|
|
32
|
+
//data.payload = input.payload;
|
|
33
|
+
|
|
34
|
+
//apply node's config
|
|
35
|
+
if (node.volumeFlag) {data.volume = node.volume/100}
|
|
36
|
+
if (node.whisper) {data.whisper = node.whisper}
|
|
37
|
+
if (node.stopListening) {data.stopListening = node.stopListening}
|
|
38
|
+
if (node.noTrackPhrase) {data.noTrackPhrase = node.noTrackPhrase}
|
|
39
|
+
if (node.pauseMusic) {data.pauseMusic = node.pauseMusic}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
//redefine options from input
|
|
43
|
+
if ("volume" in input) {data.volume = input.volume/100}
|
|
44
|
+
if ("whisper" in input) {data.whisper = input.whisper?true:false}
|
|
45
|
+
if ("voice" in input) {node.ttsVoice = input.voice}
|
|
46
|
+
if ("effect" in input) {node.ttsEffect = input.effect}
|
|
47
|
+
if ("prevent_listening" in input) {node.noTrackPhrase = input.prevent_listening}
|
|
48
|
+
if ("pause_music" in input) {data.pauseMusic = input.pause_music}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if ('tts' === node.input) {
|
|
52
|
+
let payload;
|
|
53
|
+
switch (node.config.payloadType) {
|
|
54
|
+
case 'flow': {
|
|
55
|
+
if (typeof(node.context().flow.get(node.config.payload)) != "undefined") {
|
|
56
|
+
payload = node.context().flow.get(node.config.payload)
|
|
57
|
+
} else {
|
|
58
|
+
debugMessage('Empty flow context with key '+ node.config.payload)
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case 'global': {
|
|
63
|
+
if (typeof(node.context().global.get(node.config.payload)) != "undefined") {
|
|
64
|
+
payload = node.context().global.get(node.config.payload)
|
|
65
|
+
} else {
|
|
66
|
+
debugMessage('Empty global context with key '+ node.config.payload)
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
case 'str': {
|
|
72
|
+
payload = node.config.payload;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'json': {
|
|
76
|
+
let arr = [];
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
arr = JSON.parse(node.config.payload)
|
|
80
|
+
payload = arr[(Math.random() * arr.length) | 0];
|
|
81
|
+
} catch (e) {
|
|
82
|
+
debugMessage("Error on parsing input JSON: "+ e);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'msg': {
|
|
88
|
+
payload = node.input[node.config.payload]
|
|
89
|
+
}
|
|
90
|
+
default: {
|
|
91
|
+
payload = input[node.config.payload];
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (typeof(payload) != "undefined" ) {
|
|
96
|
+
data.payload = payload;
|
|
97
|
+
if (node.ttsVoice) {
|
|
98
|
+
data.payload = "<speaker voice='" + node.ttsVoice + "'>" + data.payload;
|
|
99
|
+
}
|
|
100
|
+
if (node.ttsEffect) {
|
|
101
|
+
let effectsArr = node.ttsEffect.split(',');
|
|
102
|
+
for (let ind in effectsArr) {
|
|
103
|
+
data.payload = "<speaker effect='" + effectsArr[ind] + "'>" + data.payload;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (data.whisper) {
|
|
108
|
+
data.payload = "<speaker is_whisper='"+data.whisper+"'>" + data.payload;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
data.payload = ""
|
|
112
|
+
}
|
|
113
|
+
if (data.payload.length > 0) {node.controller.sendMessage(node.station, node.input, data)
|
|
114
|
+
debugMessage(`Sending data: station: ${node.station}, input type: ${node.input}, data: ${JSON.stringify(data)}`);
|
|
115
|
+
} else {
|
|
116
|
+
debugMessage("Nothing to send. Check input and parameters")
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
data.payload = input.payload;
|
|
120
|
+
data.hap = input.hap;
|
|
121
|
+
node.controller.sendMessage(node.station, node.input, data);
|
|
122
|
+
debugMessage(`Sending data: station: ${node.station}, input type: ${node.input}, data: ${JSON.stringify(data)}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
} else {
|
|
126
|
+
debugMessage('node.station is empty');
|
|
31
127
|
}
|
|
32
128
|
});
|
|
33
129
|
|
|
@@ -37,10 +133,10 @@ module.exports = function(RED) {
|
|
|
37
133
|
node.status({fill: data.color,shape:"dot",text: data.text});
|
|
38
134
|
}
|
|
39
135
|
}
|
|
40
|
-
|
|
136
|
+
|
|
41
137
|
|
|
42
138
|
node.on('close', () => {
|
|
43
|
-
node.controller.removeListener(`statusUpdate_${node.station}`, node.onStatus)
|
|
139
|
+
node.controller.removeListener(`statusUpdate_${node.station}`, node.onStatus)
|
|
44
140
|
});
|
|
45
141
|
|
|
46
142
|
if (node.controller) {
|
|
@@ -49,4 +145,4 @@ module.exports = function(RED) {
|
|
|
49
145
|
}
|
|
50
146
|
}
|
|
51
147
|
RED.nodes.registerType("alice-local-out",AliceLocalOutNode);
|
|
52
|
-
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="alice-local-out">
|
|
2
|
+
<p>Output node for Yandex Station local management</p>
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
<h3>TTS</h3>
|
|
6
|
+
<p>Воспросизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.</p>
|
|
7
|
+
<dl class="message-properties">
|
|
8
|
+
<dt>Текст</dt><dd>Откуда брать текст: msg.payload, flow, global или Json - выбрать сообщение случайным образом из массива вида <code>["один", "два", "три"]</code>.</dd>
|
|
9
|
+
<dt class="optional">Whisper</dt><dd>Ответить шёпотом.</dd>
|
|
10
|
+
<dt class="optional">Эффект</dt><dd>Наложить эффект на голос.</dd>
|
|
11
|
+
<dt class="optional">Громкость</dt><dd>Позволяет произносить фразу заданной громкостью. Если не выбрано, то фраза произносится с текущим уровнем громкости. После произнесения, уровень громкости вернется в изначальный.</dd>
|
|
12
|
+
<dt class="optional">Voice</dt><dd>Say a phrase in a whisper.</dd>
|
|
13
|
+
<dt class="optional">Не ждать ответ</dt><dd>Если выбрано, то колонка, после воспроизведения, не "слушает", что ей ответят.</dd>
|
|
14
|
+
<dt class="optional">Плеер на паузу</dt><dd>Ставит воспроизведение плеера на паузу на время речи. Воспроизведение будет продолжено, только если что-то играло на момент поступления команды.</dd>
|
|
15
|
+
</dl>
|
|
16
|
+
|
|
17
|
+
<h4>Отмечайте ударения</h4>
|
|
18
|
+
<p>При необходимости ударные гласные в словах следует отмечать знаком «+», например:</p>
|
|
19
|
+
<ul>
|
|
20
|
+
<li><code>остр+ота</code></li>
|
|
21
|
+
<li><code>м+ука</code></li>
|
|
22
|
+
</ul>
|
|
23
|
+
|
|
24
|
+
<h4>Разделяйте слова</h4>
|
|
25
|
+
<p>Длинные слова можно разбить на слова покороче и проставить ударения для каждого из этих коротких слов, например:</p>
|
|
26
|
+
<ul>
|
|
27
|
+
<li><code>мн+ого пр+офильный</code></li>
|
|
28
|
+
<li><code>с+еми пал+атинск</code></li>
|
|
29
|
+
</ul>
|
|
30
|
+
|
|
31
|
+
<h4>Меняйте написание слов</h4>
|
|
32
|
+
<p>Некоторые слова можно попробовать писать так, как они слышатся:</p>
|
|
33
|
+
<ul>
|
|
34
|
+
<li><code>«ненастный» — нен+асный</code></li>
|
|
35
|
+
<li><code>«пожалуйста» — пож+алуста</code></li>
|
|
36
|
+
</ul>
|
|
37
|
+
|
|
38
|
+
<h4>Добавляйте паузы</h4>
|
|
39
|
+
<p>Чтобы задать паузу между словами, используйте синтаксис sil <[ количество_миллисекунд ]>. Например:</p>
|
|
40
|
+
<ul>
|
|
41
|
+
<li><code>смелость sil <[500]> город+а берёт</code></li>
|
|
42
|
+
</ul>
|
|
43
|
+
<p>Каждый отделенный пробелами пунктуационный знак обозначается паузой в 50-100 мс.</p>
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"label": {
|
|
3
|
+
"name": "Name",
|
|
4
|
+
"text": "Text",
|
|
5
|
+
"effect": "Effect",
|
|
6
|
+
"login": "Login",
|
|
7
|
+
"station": "Station",
|
|
8
|
+
"voice": "Voice",
|
|
9
|
+
"debug": "Debug",
|
|
10
|
+
"command": "Command",
|
|
11
|
+
"volume": "Volume",
|
|
12
|
+
"prevent_listening": "Prevent listening",
|
|
13
|
+
"pause_while_tts": "Pause while TTS",
|
|
14
|
+
"whisper": "Whisper",
|
|
15
|
+
"default_command": "Default command"
|
|
16
|
+
},
|
|
17
|
+
"placeholder": {
|
|
18
|
+
"name": "Name",
|
|
19
|
+
"play_music": "Play music"
|
|
20
|
+
},
|
|
21
|
+
"command": {
|
|
22
|
+
"player": "Player",
|
|
23
|
+
"tts": "Text-to-Speech",
|
|
24
|
+
"voice": "Voice",
|
|
25
|
+
"raw": "Raw",
|
|
26
|
+
"stop_listening": "Stop listening",
|
|
27
|
+
"homekit": "Homekit"
|
|
28
|
+
},
|
|
29
|
+
"voice": {
|
|
30
|
+
"default": "Default",
|
|
31
|
+
"alena": "Alena",
|
|
32
|
+
"alyss": "Alyss",
|
|
33
|
+
"anton_samokhvalov": "Anton Samokhvalov",
|
|
34
|
+
"dude": "Dude",
|
|
35
|
+
"ermil": "Ermil",
|
|
36
|
+
"ermilov": "Ermilov",
|
|
37
|
+
"ermil_with_tuning": "Ermil (tuning)",
|
|
38
|
+
"erkanyavas": "Erkanyavas",
|
|
39
|
+
"filipp": "Filipp",
|
|
40
|
+
"jane": "Jane",
|
|
41
|
+
"kolya": "Kolya",
|
|
42
|
+
"kostya": "Kostya",
|
|
43
|
+
"levitan": "Levitan",
|
|
44
|
+
"nastya": "Nastya",
|
|
45
|
+
"nick": "Nick",
|
|
46
|
+
"oksana": "Oksana",
|
|
47
|
+
"omazh": "Omazh",
|
|
48
|
+
"robot": "Robot",
|
|
49
|
+
"sasha": "Sasha",
|
|
50
|
+
"silaerkan": "Silaerkan",
|
|
51
|
+
"smoky": "Smoky",
|
|
52
|
+
"tanya": "Tanya",
|
|
53
|
+
"tatyana_abramova": "Tatyana Abramova",
|
|
54
|
+
"zahar": "Zahar",
|
|
55
|
+
"zhenya": "Zhenya",
|
|
56
|
+
"zombie": "Zombie",
|
|
57
|
+
"voicesearch": "Voicesearch"
|
|
58
|
+
},
|
|
59
|
+
"effect": {
|
|
60
|
+
"none": "None",
|
|
61
|
+
"behind_the_wall": "behind_the_wall",
|
|
62
|
+
"hamster": "hamster",
|
|
63
|
+
"megaphone": "megaphone",
|
|
64
|
+
"pitch_down": "pitch_down",
|
|
65
|
+
"psychodelic": "psychodelic",
|
|
66
|
+
"pulse": "pulse",
|
|
67
|
+
"train_announce": "train_announce"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="alice-local-out">
|
|
2
|
+
<p>Команда для яндекс станции</p>
|
|
3
|
+
|
|
4
|
+
<h3>TTS</h3>
|
|
5
|
+
<p>Воспросизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.</p>
|
|
6
|
+
<dl class="message-properties">
|
|
7
|
+
<dt>Текст</dt><dd>Откуда брать текст: msg.payload, flow, global или Json - выбрать сообщение случайным образом из массива вида <code>["один", "два", "три"]</code>.</dd>
|
|
8
|
+
<dt class="optional">Голос</dt><dd>Изменить голос.</dd>
|
|
9
|
+
<dt class="optional">Эффект</dt><dd>Наложить эффект на голос.</dd>
|
|
10
|
+
<dt class="optional">Громкость</dt><dd>Позволяет произносить фразу заданной громкостью. Если не выбрано, то фраза произносится с текущим уровнем громкости. После произнесения, уровень громкости вернется в изначальный.</dd>
|
|
11
|
+
<dt class="optional">Шёпот</dt><dd>Ответить шёпотом.</dd>
|
|
12
|
+
<dt class="optional">Не ждать ответ</dt><dd>Если выбрано, то колонка, после воспроизведения, не "слушает", что ей ответят.</dd>
|
|
13
|
+
<dt class="optional">Плеер на паузу</dt><dd>Ставит воспроизведение плеера на паузу на время речи. Воспроизведение будет продолжено, только если что-то играло на момент поступления команды.</dd>
|
|
14
|
+
</dl>
|
|
15
|
+
|
|
16
|
+
<h4>Отмечайте ударения</h4>
|
|
17
|
+
<p>При необходимости ударные гласные в словах следует отмечать знаком «+», например:</p>
|
|
18
|
+
<ul>
|
|
19
|
+
<li><code>остр+ота</code></li>
|
|
20
|
+
<li><code>м+ука</code></li>
|
|
21
|
+
</ul>
|
|
22
|
+
|
|
23
|
+
<h4>Разделяйте слова</h4>
|
|
24
|
+
<p>Длинные слова можно разбить на слова покороче и проставить ударения для каждого из этих коротких слов, например:</p>
|
|
25
|
+
<ul>
|
|
26
|
+
<li><code>мн+ого пр+офильный</code></li>
|
|
27
|
+
<li><code>с+еми пал+атинск</code></li>
|
|
28
|
+
</ul>
|
|
29
|
+
|
|
30
|
+
<h4>Меняйте написание слов</h4>
|
|
31
|
+
<p>Некоторые слова можно попробовать писать так, как они слышатся:</p>
|
|
32
|
+
<ul>
|
|
33
|
+
<li><code>«ненастный» — нен+асный</code></li>
|
|
34
|
+
<li><code>«пожалуйста» — пож+алуста</code></li>
|
|
35
|
+
</ul>
|
|
36
|
+
|
|
37
|
+
<h4>Добавляйте паузы</h4>
|
|
38
|
+
<p>Чтобы задать паузу между словами, используйте синтаксис sil <[ количество_миллисекунд ]>. Например:</p>
|
|
39
|
+
<ul>
|
|
40
|
+
<li><code>смелость sil <[500]> город+а берёт</code></li>
|
|
41
|
+
</ul>
|
|
42
|
+
<p>Каждый отделенный пробелами пунктуационный знак обозначается паузой в 50-100 мс.</p>
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"label": {
|
|
3
|
+
"name": "Название",
|
|
4
|
+
"text": "Текст",
|
|
5
|
+
"effect": "Эффект",
|
|
6
|
+
"login": "Логин",
|
|
7
|
+
"station": "Станция",
|
|
8
|
+
"voice": "Голос",
|
|
9
|
+
"debug": "Дебаг",
|
|
10
|
+
"command": "Команда",
|
|
11
|
+
"volume": "Громкость",
|
|
12
|
+
"prevent_listening": "Не ждать ответ",
|
|
13
|
+
"pause_while_tts": "Плеер на паузу",
|
|
14
|
+
"whisper": "Шёпот",
|
|
15
|
+
"default_command": "Команда"
|
|
16
|
+
},
|
|
17
|
+
"placeholder": {
|
|
18
|
+
"name": "Название",
|
|
19
|
+
"play_music": "Включи музыку"
|
|
20
|
+
},
|
|
21
|
+
"command": {
|
|
22
|
+
"player": "Плеер",
|
|
23
|
+
"tts": "Синтез речи из текста",
|
|
24
|
+
"voice": "Голосовая команда",
|
|
25
|
+
"stop_listening": "Перестать слушать",
|
|
26
|
+
"homekit": "Homekit",
|
|
27
|
+
"raw": "Сырая команда"
|
|
28
|
+
},
|
|
29
|
+
"voice": {
|
|
30
|
+
"default": "Алиса (стандартный)",
|
|
31
|
+
"alena": "Алёна (alena)",
|
|
32
|
+
"alyss": "Элис (alyss)",
|
|
33
|
+
"anton_samokhvalov": "Антон Самохвалов (anton_samokhvalov)",
|
|
34
|
+
"dude": "Чувак (dude)",
|
|
35
|
+
"ermil": "Ермил (ermil)",
|
|
36
|
+
"ermilov": "Ermilov (ermilov)",
|
|
37
|
+
"ermil_with_tuning": "Ермил Т (ermil_with_tuning)",
|
|
38
|
+
"erkanyavas": "Ерканявас (erkanyavas)",
|
|
39
|
+
"filipp": "Филипп (filipp)",
|
|
40
|
+
"jane": "Джейн (jane)",
|
|
41
|
+
"kolya": "Коля (kolya)",
|
|
42
|
+
"kostya": "Костя (kostya)",
|
|
43
|
+
"levitan": "Левитан (levitan)",
|
|
44
|
+
"nastya": "Настя (nastya)",
|
|
45
|
+
"nick": "Ник (nick)",
|
|
46
|
+
"oksana": "Оксана (oksana)",
|
|
47
|
+
"omazh": "Омаж (omazh)",
|
|
48
|
+
"robot": "Робот (robot)",
|
|
49
|
+
"sasha": "Саша (sasha)",
|
|
50
|
+
"silaerkan": "Силаеркан (silaerkan)",
|
|
51
|
+
"smoky": "Смоки (smoky)",
|
|
52
|
+
"tanya": "Таня (tanya)",
|
|
53
|
+
"tatyana_abramova": "Татьяна Абрамова (tatyana_abramova)",
|
|
54
|
+
"zahar": "Захар (zahar)",
|
|
55
|
+
"zhenya": "Женя (zhenya)",
|
|
56
|
+
"zombie": "Зомби (zombie)",
|
|
57
|
+
"voicesearch": "voicesearch"
|
|
58
|
+
},
|
|
59
|
+
"effect": {
|
|
60
|
+
"none": "Без эффекта",
|
|
61
|
+
"behind_the_wall": "Из-за стены (behind_the_wall)",
|
|
62
|
+
"hamster": "Хомяк (hamster)",
|
|
63
|
+
"megaphone": "Мегафон (megaphone)",
|
|
64
|
+
"pitch_down": "Низкий (pitch_down)",
|
|
65
|
+
"psychodelic": "Психоделический (psychodelic)",
|
|
66
|
+
"pulse": "С прерыванием (pulse)",
|
|
67
|
+
"train_announce": "Громкоговоритель (train_announce)"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/nodes/yandex-login.js
CHANGED
|
@@ -45,7 +45,7 @@ module.exports = function(RED) {
|
|
|
45
45
|
debugMessage(`Ready event fo ${device.id}`);
|
|
46
46
|
node.emit(`deviceReady`, device);
|
|
47
47
|
node.readyList.push({ 'name': device.name, 'id': device.id, 'platform': device.platform, 'address': device.address, 'port': device.port, 'host': device.host, 'parameters': device.parameters});
|
|
48
|
-
node.emit('refreshHttp', node.activeStationList, node.readyList)
|
|
48
|
+
node.emit('refreshHttp', node.activeStationList, node.readyList);
|
|
49
49
|
statusUpdate({"color": "yellow", "text": "connecting..."}, device);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -97,15 +97,19 @@ module.exports = function(RED) {
|
|
|
97
97
|
});
|
|
98
98
|
//node.emit('refreshHttp', node.activeStationList, node.readyList)
|
|
99
99
|
deviceListProcessing(node.deviceList)
|
|
100
|
+
try {
|
|
101
|
+
discoverDevices(node.deviceList)
|
|
102
|
+
.then(() => {
|
|
103
|
+
//debugMessage(`calling processing for ${node.deviceList.length} devices`);
|
|
104
|
+
deviceListProcessing(node.deviceList)
|
|
105
|
+
|
|
106
|
+
});
|
|
107
|
+
//debugMessage(node.id);
|
|
108
|
+
return node.deviceList;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
debugMessage(`Error while searching: ${error}`);
|
|
111
|
+
}
|
|
100
112
|
|
|
101
|
-
discoverDevices(node.deviceList)
|
|
102
|
-
.then(() => {
|
|
103
|
-
//debugMessage(`calling processing for ${node.deviceList.length} devices`);
|
|
104
|
-
deviceListProcessing(node.deviceList)
|
|
105
|
-
|
|
106
|
-
});
|
|
107
|
-
//debugMessage(node.id);
|
|
108
|
-
return node.deviceList;
|
|
109
113
|
})
|
|
110
114
|
.catch(function (err) {
|
|
111
115
|
//node.emit('refreshHttp', node.readyList);
|
|
@@ -122,20 +126,28 @@ module.exports = function(RED) {
|
|
|
122
126
|
await mDnsSd.discover({
|
|
123
127
|
name: '_yandexio._tcp.local'
|
|
124
128
|
}).then((result) => {
|
|
125
|
-
|
|
129
|
+
debugMessage(`MDNS. Found ${result.length} devices`);
|
|
130
|
+
node.emit('refreshHttpDNS', result);
|
|
126
131
|
if (result.length != 0){
|
|
127
132
|
for (const device of deviceList) {
|
|
128
133
|
result.forEach(element => {
|
|
129
134
|
let checkConnType = device.parameters.network || {}
|
|
130
135
|
//если режим автоопределения адреса или набор параметров пустой, то записывать значния из результатов mdns поиска
|
|
131
136
|
if (checkConnType.mode == "auto" || JSON.stringify(checkConnType) == "{}") {
|
|
132
|
-
|
|
133
|
-
let
|
|
137
|
+
//need remove to REGEXP!
|
|
138
|
+
//let mdnsId = element.fqdn.split("-")[1].split(".")[0];
|
|
139
|
+
let srvEls = element.packet.answers.find(el => el.type == "SRV") || element.packet.additionals.find(el => el.type == "SRV");
|
|
140
|
+
let txtEls = element.packet.answers.find(el => el.type == "TXT") || element.packet.additionals.find(el => el.type == "TXT");
|
|
134
141
|
if (typeof(txtEls) !== 'undefined' ) {
|
|
135
142
|
if (txtEls.rdata.deviceId == device.id) {
|
|
136
143
|
device.address = element.address;
|
|
137
144
|
device.port = element.service.port;
|
|
138
|
-
|
|
145
|
+
try {
|
|
146
|
+
device.host = srvEls.rdata.target;
|
|
147
|
+
} catch(e) {
|
|
148
|
+
debugMessage(`Error searching hostname in mDNS answer`)
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
}
|
|
140
152
|
}
|
|
141
153
|
}
|
|
@@ -200,8 +212,8 @@ module.exports = function(RED) {
|
|
|
200
212
|
function connect(device) {
|
|
201
213
|
//connect only if !device.ws
|
|
202
214
|
//debugMessage(`device.ws = ${JSON.stringify(device.ws)}`);
|
|
203
|
-
if (device.connection == true || typeof(device.connection) == "undefined") {
|
|
204
|
-
debugMessage(`Connecting to device ${device.id}. ws is ${JSON.stringify(device.ws)}`)
|
|
215
|
+
if ( (device.connection == true || typeof(device.connection) == "undefined") && node.listenerCount(`statusUpdate_${device.id}`) > 0 ) {
|
|
216
|
+
debugMessage(`Connecting to device ${device.id}. ws is ${JSON.stringify(device.ws)}. Listeners: ` + node.listenerCount(`statusUpdate_${device.id}`));
|
|
205
217
|
if (!device.ws) {
|
|
206
218
|
debugMessage('recieving conversation token...');
|
|
207
219
|
getLocalToken(device)
|
|
@@ -246,7 +258,7 @@ module.exports = function(RED) {
|
|
|
246
258
|
//}
|
|
247
259
|
}
|
|
248
260
|
} else {
|
|
249
|
-
debugMessage(`${device.id} connection is disabled by settings in manager node ${device.manager}`)
|
|
261
|
+
debugMessage(`${device.id} connection is disabled by settings in manager node ${device.manager} or you have not use any node for this station`)
|
|
250
262
|
statusUpdate({"color": "red", "text": "disconnected"}, device);
|
|
251
263
|
device.timer = setTimeout(connect, 60000, device);
|
|
252
264
|
|
|
@@ -275,16 +287,19 @@ module.exports = function(RED) {
|
|
|
275
287
|
device.watchDog = setTimeout(() => device.ws.close(), 10000);
|
|
276
288
|
device.pingInterval = setInterval(onPing,300,device);
|
|
277
289
|
clearTimeout(device.timer);
|
|
290
|
+
debugMessage(`readyState: ${device.ws.readyState}`)
|
|
278
291
|
});
|
|
279
292
|
device.ws.on('message', function incoming(data) {
|
|
280
293
|
//debugMessage(`${device.id}: ${JSON.stringify(data)}`);
|
|
281
294
|
let dataRecieved = JSON.parse(data);
|
|
282
295
|
device.lastState = dataRecieved.state;
|
|
296
|
+
device.fullMessage = JSON.stringify(dataRecieved);
|
|
283
297
|
//debugMessage(checkSheduler(device, JSON.parse(data).sentTime));
|
|
284
298
|
node.emit(`message_${device.id}`, device.lastState);
|
|
285
299
|
if (device.lastState.aliceState == 'LISTENING' && device.waitForListening) {node.emit(`stopListening`, device)}
|
|
286
300
|
if (device.lastState.aliceState == 'LISTENING' && device.playAfterTTS) {node.emit('startPlay', device)}
|
|
287
301
|
if (device.lastState.aliceState == 'LISTENING' && device.waitForIdle) {node.emit('setVolume', device)}
|
|
302
|
+
//if (device.lastState.aliceState != 'SPEAKING' && device.ttsBuffer.length > 0) {node.emit('nextTts', device)}
|
|
288
303
|
// if (device.parameters.hasOwnProperty(sheduler)) {
|
|
289
304
|
// let resultSheduler = checkSheduler(device, dataRecieved.sentTime)
|
|
290
305
|
// device.canPlay = resultSheduler[0]
|
|
@@ -313,6 +328,7 @@ module.exports = function(RED) {
|
|
|
313
328
|
//device.ws.on('ping', function);
|
|
314
329
|
device.ws.on('close', function close(code, reason){
|
|
315
330
|
statusUpdate({"color": "red", "text": "disconnected"}, device);
|
|
331
|
+
//debugMessage(`readyState: ${device.ws.readyState}`)
|
|
316
332
|
device.lastState = {};
|
|
317
333
|
clearTimeout(device.watchDog);
|
|
318
334
|
switch(code) {
|
|
@@ -499,10 +515,11 @@ module.exports = function(RED) {
|
|
|
499
515
|
return result;
|
|
500
516
|
break;
|
|
501
517
|
case 'homekit':
|
|
502
|
-
debugMessage('HAP: ' + JSON.stringify(message
|
|
503
|
-
if (message.hap
|
|
518
|
+
debugMessage('HAP: ' + JSON.stringify(message) + ' PL: ' + JSON.stringify(message.payload) );
|
|
519
|
+
if ("session" in message.hap) {
|
|
504
520
|
switch(JSON.stringify(message.payload)){
|
|
505
521
|
case '{"TargetMediaState":1}':
|
|
522
|
+
return messageConstructor('command', {'payload': 'stop'})
|
|
506
523
|
case '{"Active":0}':
|
|
507
524
|
return messageConstructor('command', {'payload': 'stop'})
|
|
508
525
|
case '{"TargetMediaState":0}':
|
|
@@ -758,6 +775,12 @@ module.exports = function(RED) {
|
|
|
758
775
|
res.json({"devices": activeList});
|
|
759
776
|
});
|
|
760
777
|
});
|
|
778
|
+
|
|
779
|
+
node.on('refreshHttpDNS', function(dnsList) {
|
|
780
|
+
RED.httpAdmin.get("/mdns/"+node.id, RED.auth.needsPermission('yandex-login.read'), function(req,res) {
|
|
781
|
+
res.json({"SearchResult": dnsList});
|
|
782
|
+
});
|
|
783
|
+
});
|
|
761
784
|
node.on('close', onClose)
|
|
762
785
|
|
|
763
786
|
|
|
@@ -772,7 +795,7 @@ module.exports = function(RED) {
|
|
|
772
795
|
let device = searchDeviceByID(id);
|
|
773
796
|
if (device) {
|
|
774
797
|
|
|
775
|
-
res.json({"id": device.id,"name": device.name, "platform": device.platform, "address": device.address, "port": device.port, "manager": device.manager, "ws": device.ws, "parameters": device.parameters});
|
|
798
|
+
res.json({"id": device.id,"name": device.name, "platform": device.platform, "address": device.address, "port": device.port, "manager": device.manager, "ws": device.ws, "parameters": device.parameters, "fullMessage": device.fullMessage });
|
|
776
799
|
} else {
|
|
777
800
|
res.json({"error": 'no device found'});
|
|
778
801
|
}
|
|
@@ -780,8 +803,9 @@ module.exports = function(RED) {
|
|
|
780
803
|
|
|
781
804
|
// main init
|
|
782
805
|
if (typeof(node.token) !== 'undefined') {
|
|
806
|
+
debugMessage(`Starting server with id ${node.id}`)
|
|
783
807
|
getDevices(node.token);
|
|
784
|
-
node.interval = setInterval(getDevices,
|
|
808
|
+
node.interval = setInterval(getDevices, 60000, node.token);
|
|
785
809
|
}
|
|
786
810
|
|
|
787
811
|
|