@@ -15,10 +15,26 @@ export const voicerecNode = {
1515 color : '#ffb6c1' , // light pink
1616 icon : true ,
1717 faChar : '\uf130' , // microphone
18- inputs : 0 ,
18+ inputs : 1 ,
1919 outputs : 1 ,
2020
2121 defaults : {
22+ mode : {
23+ type : 'select' ,
24+ default : 'continuous' ,
25+ label : 'Mode' ,
26+ options : [
27+ { value : 'continuous' , label : 'Continuous' } ,
28+ { value : 'push-to-talk' , label : 'Push-to-talk' }
29+ ] ,
30+ description : 'Continuous: always listening. Push-to-talk: only records while receiving "push" signal.'
31+ } ,
32+ topic : {
33+ type : 'string' ,
34+ default : '' ,
35+ label : 'Topic' ,
36+ description : 'Optional topic for p2p routing. Leave empty for default "voicerec" topic.'
37+ } ,
2238 lang : { type : 'select' , default : 'en-US' , label : 'Language' , options : [
2339 { value : 'en-US' , label : 'English (US)' } ,
2440 { value : 'en-GB' , label : 'English (UK)' } ,
@@ -34,7 +50,7 @@ export const voicerecNode = {
3450 continuous : {
3551 type : 'boolean' ,
3652 default : true ,
37- label : 'Continuous ' ,
53+ label : 'Auto-restart ' ,
3854 description : 'Keep listening after each result instead of stopping. Turn off for single-phrase recognition.'
3955 } ,
4056 interimResults : {
@@ -46,11 +62,21 @@ export const voicerecNode = {
4662 } ,
4763
4864 messageInterface : {
65+ reads : {
66+ payload : {
67+ type : 'string' ,
68+ description : 'Control signal: "start", "stop", "push", or "release"'
69+ }
70+ } ,
4971 writes : {
5072 payload : {
5173 type : 'string' ,
5274 description : 'Transcribed text'
5375 } ,
76+ topic : {
77+ type : 'string' ,
78+ description : 'Topic for p2p routing (from config or default "voicerec")'
79+ } ,
5480 confidence : {
5581 type : 'number' ,
5682 description : 'Recognition confidence (0-1)'
@@ -63,19 +89,25 @@ export const voicerecNode = {
6389 } ,
6490
6591 mainThread : ( ( ) => {
66- // Track recognition instance and session ID per node
92+ // Track recognition instance, session ID, and config per node
6793 const voiceRecognitions = new Map ( ) ;
6894 const nodeSessions = new Map ( ) ; // nodeId -> current session ID
95+ const nodeConfigs = new Map ( ) ; // nodeId -> { topic, mode, autoRestart }
6996
7097 return {
71- start ( peerRef , nodeId , { lang, continuous : _continuous , interimResults } , PN ) {
98+ start ( peerRef , nodeId , { lang, topic , mode , autoRestart , interimResults } , PN ) {
7299 const SpeechRecognition = window . SpeechRecognition || window . webkitSpeechRecognition ;
73100 if ( ! SpeechRecognition ) {
74101 peerRef . current . methods . emitEvent ( nodeId , 'status' , { text : 'Not supported' , fill : 'red' } ) ;
75102 peerRef . current . methods . emitEvent ( nodeId , 'error' , 'Speech recognition not supported' ) ;
76103 return ;
77104 }
78105
106+ // Store config for this node (topic defaults to 'voicerec' if empty)
107+ const effectiveTopic = topic && topic . trim ( ) ? topic . trim ( ) : 'voicerec' ;
108+ const isPushToTalk = mode === 'push-to-talk' ;
109+ nodeConfigs . set ( nodeId , { topic : effectiveTopic , mode, autoRestart : ! isPushToTalk && autoRestart } ) ;
110+
79111 // Create a new session ID - this invalidates any previous session's onend handlers
80112 const sessionId = Date . now ( ) + Math . random ( ) ;
81113 nodeSessions . set ( nodeId , sessionId ) ;
@@ -100,18 +132,20 @@ export const voicerecNode = {
100132 recognition . onstart = ( ) => {
101133 // Only update status if this is still the active session
102134 if ( nodeSessions . get ( nodeId ) === sessionId ) {
103- peerRef . current . methods . emitEvent ( nodeId , 'status' , { text : 'Listening' , fill : 'green' } ) ;
135+ const statusText = isPushToTalk ? 'Recording' : 'Listening' ;
136+ peerRef . current . methods . emitEvent ( nodeId , 'status' , { text : statusText , fill : 'green' } ) ;
104137 }
105138 } ;
106139
107140 recognition . onresult = ( event ) => {
108141 // Only send results if this is still the active session
109142 if ( nodeSessions . get ( nodeId ) !== sessionId ) return ;
110143
144+ const config = nodeConfigs . get ( nodeId ) || { topic : 'voicerec' } ;
111145 const result = event . results [ 0 ] [ 0 ] ;
112146 peerRef . current . methods . sendResult ( nodeId , {
113147 payload : result . transcript ,
114- topic : 'voicerec' ,
148+ topic : config . topic ,
115149 confidence : result . confidence ,
116150 isFinal : true
117151 } ) ;
@@ -121,7 +155,7 @@ export const voicerecNode = {
121155 // Ignore errors from stale sessions
122156 if ( nodeSessions . get ( nodeId ) !== sessionId ) return ;
123157
124- // 'no-speech' means silence timeout - just restart
158+ // 'no-speech' means silence timeout - just restart (unless PTT)
125159 if ( event . error === 'no-speech' ) {
126160 return ;
127161 }
@@ -140,6 +174,14 @@ export const voicerecNode = {
140174 return ; // Stale session, ignore
141175 }
142176
177+ const config = nodeConfigs . get ( nodeId ) || { autoRestart : true } ;
178+
179+ // In push-to-talk mode, don't auto-restart - wait for next push
180+ if ( ! config . autoRestart ) {
181+ peerRef . current . methods . emitEvent ( nodeId , 'status' , { text : 'Ready' , fill : 'yellow' } ) ;
182+ return ;
183+ }
184+
143185 // Restart with small delay to prevent rapid loop
144186 setTimeout ( ( ) => {
145187 // Double-check we're still the active session after delay
@@ -178,16 +220,33 @@ export const voicerecNode = {
178220 < >
179221 < p > Speech recognition - converts speech to text using the Web Speech API.</ p >
180222
223+ < h5 > Modes</ h5 >
224+ < ul >
225+ < li > < strong > Continuous</ strong > - Always listening, auto-restarts after each result</ li >
226+ < li > < strong > Push-to-talk</ strong > - Only records while receiving "push" signal, stops on "release"</ li >
227+ </ ul >
228+
181229 < h5 > Options</ h5 >
182230 < ul >
231+ < li > < strong > Mode</ strong > - Continuous or push-to-talk operation</ li >
232+ < li > < strong > Topic</ strong > - Optional topic for p2p routing (default: "voicerec")</ li >
183233 < li > < strong > Language</ strong > - Recognition language</ li >
184- < li > < strong > Continuous </ strong > - Keep listening after first result</ li >
234+ < li > < strong > Auto-restart </ strong > - Keep listening after first result (continuous mode only) </ li >
185235 < li > < strong > Interim Results</ strong > - Output partial results while speaking</ li >
186236 </ ul >
187237
238+ < h5 > Input</ h5 >
239+ < ul >
240+ < li > < code > "start"</ code > - Start listening</ li >
241+ < li > < code > "stop"</ code > - Stop listening</ li >
242+ < li > < code > "push"</ code > - Start recording (push-to-talk mode)</ li >
243+ < li > < code > "release"</ code > - Stop recording (push-to-talk mode)</ li >
244+ </ ul >
245+
188246 < h5 > Output</ h5 >
189247 < ul >
190248 < li > < code > msg.payload</ code > - Transcribed text</ li >
249+ < li > < code > msg.topic</ code > - Topic for p2p routing</ li >
191250 < li > < code > msg.confidence</ code > - Recognition confidence (0-1)</ li >
192251 < li > < code > msg.isFinal</ code > - Whether this is a final or interim result</ li >
193252 </ ul >
0 commit comments