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