diff --git a/speech/talk.js b/speech/talk.js
new file mode 100644
index 0000000000000000000000000000000000000000..c6b57660b4ba1fe70437b081813aef20d0e3fdfc
--- /dev/null
+++ b/speech/talk.js
@@ -0,0 +1,289 @@
+// jshint esversion: 6
+	const machine = document.location.search.indexOf('machine=')>-1? document.location.search.split('machine=')[1].split('&')[0]: 'fermi';
+	initSearch('../' + machine + '_lattice.json');
+	window.openData = [];
+	const t0 = + new Date();
+	let mode = '';
+	if (document.location.search.indexOf('background=')>-1) {
+		const a = document.location.search.split('background=')[1].split('&')[0];
+		$('body').css('background-color', +a > 0? '#'+a: a);
+	}
+	const lang = document.location.search.indexOf('lang=')>-1 && document.location.search.split('lang=')[1].split('&')[0]=='en'? 'en-US': 'it-IT'; // BCP 47 language
+	const host = document.location.search.indexOf('talk=')>-1? document.location.search.split('talk=')[1].split('&')[0].replace('ee', 'pcl-elettra-cre-0').replace('ef','pcl-elettra-crf-0'): '';
+	function switchLocale(newlang) {document.location = './talk.php?lang='+newlang+(host.length>0? '&talk='+host: '');}
+	let locale = {};
+	function myload() {
+		fetch('./talk_locale.json').then((response) => {return response.json();}).then((rlocale) => {
+			locale = rlocale;
+			$('#title').html(rlocale[lang].talkto + host);
+			$('#micna').attr('title', rlocale[lang].micna);
+			$('#miclabel').html(rlocale[lang].miclabel + '   ');
+		});
+	}
+	// https://stackoverflow.com/questions/64405532/why-speechsynthesisutterance-is-not-working-on-chrome
+	// It's because in Chrome speech synthasis requires user interaction before it speaks e.g. a button click.
+	// https://stackoverflow.com/questions/50490304/how-to-make-audio-autoplay-on-chrome
+	const synth = window.speechSynthesis;
+	const voices = synth.getVoices();
+	const voiceLocale = {'it-IT': false, 'en-US': false};
+	for (let i=voices.length-1; i>=0; i--) {
+		if (voices[i].lang=='it-IT') voiceLocale['it-IT'] = voices[i];
+		if (voices[i].lang=='en-US') voiceLocale['en-US'] = voices[i];
+	}
+	let oldText = '';
+	let oldTime = 0;
+	function speakGenerated(textValue) {
+		if (textValue !== '') {
+			stopRec();
+			if (synth.speaking) {console.error(locale.lang.speaking);showLog(locale.lang.speaking2);return;}
+			const utterThis = new SpeechSynthesisUtterance(textValue);
+			utterThis.onend = function (event) {startRec(); console.log('SpeechSynthesisUtterance.onend');showLog('..');};
+			utterThis.onerror = function (event) {console.error('SpeechSynthesisUtterance.onerror', event.error, event);showLog(locale[lang].onerror+JSON.stringify(event.error));};
+			utterThis.voice = voiceLocale[lang]===false? voices[0]: voiceLocale[lang];
+			utterThis.pitch = 0.1;
+			utterThis.rate = 1;
+			utterThis.lang = lang;
+			showLog(lang);
+			synth.speak(utterThis);
+		}
+	}
+	let keepAlive = false;
+	const names = [];
+	let aliveTimer = -1;
+	function startRec() {
+		keepAlive = true;
+		aliveTimer = -1;
+		// showLog('start');
+		recognition.start();
+		document.getElementById('micstart').style.display = 'inline';
+		document.getElementById('micstop').style.display = 'none';
+	}
+	function stopRec() {
+		if (typeof window.SpeechRecognition == 'undefined') {$("#micstart").hide();$("#micna").show();}
+		keepAlive = false;
+		recognition.stop();
+	}
+	function commandlistinfo() {
+		alert(locale[lang].commandlistinfo);
+	}
+	if (document.location.search.indexOf('debug')==-1) $('#log').hide();
+	let speechStat = '';
+	let firstRun = true;
+	function showLog(currentToken, p) {
+		console.log(p);
+		console.trace(currentToken);
+		// Show log in console and UI.
+		const logElement = document.querySelector('#log');
+		logElement.textContent = logElement.textContent+'\n'+JSON.stringify(currentToken);
+	}
+	let finalTranscript = '';
+	let recognition = new window.SpeechRecognition();
+	recognition.maxAlternatives = 10;
+	recognition.continuous = true;
+	recognition.interimResults = true;
+	recognition.lang = lang;
+	recognition.maxAlternatives = 3;
+	showLog('starting...'+'\n');
+	/* recognition.onresult = (event) => {
+					  // The SpeechRecognitionEvent results property returns a SpeechRecognitionResultList object
+					  // The SpeechRecognitionResultList object contains SpeechRecognitionResult objects.
+					  // It has a getter so it can be accessed like an array
+					  // The first [0] returns the SpeechRecognitionResult at position 0.
+					  // Each SpeechRecognitionResult object contains SpeechRecognitionAlternative objects
+					  // that contain individual results.
+					  // These also have getters so they can be accessed like arrays.
+					  // The second [0] returns the SpeechRecognitionAlternative at position 0.
+					  // We then return the transcript property of the SpeechRecognitionAlternative object
+					  const color = event.results[0][0].transcript;
+					  diagnostic.textContent = `Result received: ${color}.`;
+					  bg.style.backgroundColor = color;
+					};*/
+	recognition.onerror = function(event) {
+		showLog(locale[lang].errorname + event.error+', '+event.message+', '+event.lineno+'\n');
+		showLog(locale[lang].errormessage + JSON.stringify(event.error));
+		console.log(event);
+		startRec();
+	};
+	recognition.onend = function(event) {
+		showLog('.');
+		// document.getElementById('miclabel').style.display = 'none';
+		document.getElementById('micna').style.display = 'none';
+		document.getElementById('micstart').style.display = 'none';
+		document.getElementById('micstop').style.display = 'inline';
+		if (keepAlive && aliveTimer==-1) aliveTimer = setTimeout(startRec, 200);
+	};
+	recognition.onstart = function(event) {
+		if (document.location.search.indexOf('debug')==-1) $('#log').hide();
+		if (firstRun) {
+			firstRun = false;
+			recognition.stop();
+			return;
+		}
+		showLog('-');
+		document.getElementById('miclabel').style.display = 'none';
+		document.getElementById('micna').style.display = 'none';
+		document.getElementById('micstart').style.display = 'inline';
+		document.getElementById('micstop').style.display = 'none';
+	};
+	function detect_token(transcript, token) {
+		const t = token.split(';');
+		for (let i in t) {
+			if (transcript.indexOf(t[i])>-1) return transcript.indexOf(t[i]) + t[i].length+1;
+		}
+		return -1;
+	}
+	function quick_add(transcript) {
+		const t = + new Date();
+		if ((transcript==oldText || (transcript=='te' && oldText.indexOf('te')>-1)) && t-oldTime<1500) return;
+		oldTime = t;
+		oldText = transcript;
+		speechStat = '';
+		let txt = transcript.toLowerCase();
+		if (txt.length>0) {
+			const tok = txt.split(' ')[0];
+			if (locale[lang].open.indexOf(tok)>-1) {
+				mode = 'open';
+				txt = txt.replace(tok, '').replace(' ', '');
+			}
+			if (mode=='open' && txt.length>0) {
+				const snd = './talk.php?open='+txt.toLowerCase().replace('.', '')+'&token='+document.location.search.split('?d=')[1].split('&')[0]+"&lang="+lang+"&host="+document.location.search.split('host=')[1].split('&')[0];
+				showLog(snd);
+				showLog(' host: '+host);
+				fetch(snd).then((response) => {return response.json();}).then((rlocale) => {
+					if (rlocale.length>=3 && rlocale[0].ws) {
+						$('#openTable').html('<tr><td colspan="5">' + locale[lang].openSelect + '</td></tr>');
+						for (let i=0; i<3; i++) {
+							$('#openTable').html($('#openTable').html()+'<tr><td>'+(i+1)+'</td><td>'+rlocale[i].title+'</td><td>'+rlocale[i].description+'</td><td>'+rlocale[i].exename+'</td></tr>');
+							window.openData.push(rlocale[i]);
+						}
+					}
+					window.openData.push(txt);								
+					showLog('window.openData1');
+					showLog(window.openData);
+					mode = 'openTable';
+					txt = '';
+				});
+			}
+			if (mode=='openTable' && txt.length>0) {
+				showLog('window.openData2');
+				showLog(window.openData);
+				showLog(txt);
+				txt = txt.replace('numero ', '');
+				let num = -1;
+				if (txt.indexOf(locale[lang].zero)>-1) num = -1;
+				if (txt.indexOf(locale[lang].one)>-1) num = 0;
+				if (txt.indexOf(locale[lang].two)>-1) num = 1;
+				if (txt.indexOf(locale[lang].three)>-1) num = 2;
+				if (!isNaN(txt)) num = txt - 1;
+				showLog(num);
+				const exe = (window.openData[num].path? window.openData[num].path: '/runtime/bin/')+ window.openData[num].exename + '&host=' + document.location.search.split('host=')[1].split('&')[0];
+				showLog(exe);
+				if (num>-1) {
+					showLog("https://puma-01.elettra.eu/knob/launcher.php?open="+exe);
+					fetch("https://puma-01.elettra.eu/knob/launcher.php?open="+exe)
+						.then((response) => {return response.json();})
+						.then((rlocale) => {showLog(rlocale);});
+					showLog("./talk.php?speech="+window.openData[3]+"&lang="+lang+"&id="+window.openData[num].id);
+					fetch("./talk.php?speech="+window.openData[3]+"&lang="+lang+"&id="+window.openData[num].id)
+						.then((response) => {return response.json();})
+						.then((rlocale) => {showLog(rlocale);});
+				}
+				mode = '';
+				$('#openTable').html('');
+			}
+			if (locale[lang].search.indexOf(tok)>-1) {
+				mode = 'search';
+				// txt = txt.replace(tok+' ', '').replace('ing', 'inj').replace('inch', 'inj').split(' ').join('_').split('.').join('_').toUpperCase();
+				txt = txt.replace(tok+' ', '').split(' ').join('_').split('.').join('_').toUpperCase();
+				const token = document.location.search.indexOf('?d=')>-1? '&token='+document.location.search.split('?d=')[1].split('&')[0]: '';
+				const snd = './talk.php?search='+txt+token+"&lang="+lang;
+				$('#openTable').html('<tr><td colspan="5">' + locale[lang].searchCorrect + '</td></tr>');
+				$('#openTable').html('<tr><td>'+txt+'</td><td><input id="sname" class="sname"></input></td><td><button class="btn btn-primary" onClick="findComponent($(\"#sname\").val)">'+locale[lang].searchSubmit+'</button></td></tr>');
+				$(function() {$(".sname").autocomplete({source: names, select: function(event, ui) {findComponent(ui.item.value, txt); return false;}});});
+				showLog(snd);
+				fetch(snd).then((response) => {return response.text();}).then((rlocale) => {showLog(rlocale); /*alert(rlocale);*/});
+				mode = '';
+			}
+			if (locale[lang].list.indexOf(tok)>-1) {
+				mode = 'list';
+				txt = txt.replace(tok, '').replace(' ', '');
+			}
+			if (mode=='list') {
+				const snd = './talk.php?list='+tok+'&token='+document.location.search.split('?d=')[1].split('&')[0];
+				fetch(snd).then((response) => {return response.json();}).then((rlocale) => {showLog(rlocale); alert(rlocale);});
+				mode = '';
+			}
+			showLog('mode: '+mode+', tok: '+tok);
+		}
+		return;
+		// transcript.replace('sky ','');
+	}
+
+	function findComponent(txt, speech) {
+		console.log(txt);
+		const token = document.location.search.indexOf('?d=')>-1? '&token='+document.location.search.split('?d=')[1].split('&')[0]: '';
+		const snd = './talk.php?search='+txt+token+"&lang="+lang+"&speech="+speech;
+		showLog(snd);
+		fetch(snd).then((response) => {return response.text();}).then((rlocale) => {showLog(rlocale); /*alert(rlocale);*/});
+	}
+	if (!String.prototype.replaceAll) {
+		String.prototype.replaceAll = function(search, replace) {
+			return this.split(search).join(replace);
+		};
+	}
+	function extractId(name) {
+		if (name.indexOf('(')>-1) {
+			let tok = name.split('(');
+			return [tok[0].replaceAll('.','_').replaceAll(' ','_').replace(/_+$/, ''), tok[1].replaceAll('.','_').replaceAll(' ','_').replaceAll(')','').replace(/_+$/, '')];
+		}
+		return [name.replaceAll('.','_').replaceAll(' ','_').replace(/_+$/, ''), false];
+	}
+	function initLattice(sections) { 
+		for (let j=0; j<sections.length; j++) { // if(i>1) break;
+			const components = sections[j].components;
+			if (components) for (let i=0; i<components.length; i++) {
+				const id = extractId(components[i].name);
+				names.push(id[0]);
+				if (components[i].embedded) {
+					for (let j=0; j<components[i].embedded.length; j++)  {names.push(components[i].embedded[j]);}
+				}
+			}
+		}
+	}
+	function initSearch(latticeFile) {
+		fetch(latticeFile).then((response) => {return response.json();}).then((flattice) => {
+			lattice = flattice;
+			if (Object.keys(lattice).length>0) {
+				for (let i in lattice) {
+					if (lattice[i].sections) initLattice(lattice[i].sections);
+				}
+			}
+		});
+	}
+
+	recognition.onresult = (event) => {
+		let interimTranscript = '';
+		for (let i = event.resultIndex, len = event.results.length; i < len; i++) {
+			let transcript = event.results[i][0].transcript;
+			// const dt = + new Time() - t0;
+			if (event.results[i][0].transcript.length) showLog(' mode '+mode+', i: '+i+', transcripts: '+event.results[i][0].transcript+(event.results[i][1]? ', transcripts: '+event.results[i][1].transcript:'')+'\n');
+			if (event.results[i].isFinal) {
+				quick_add(transcript.toLowerCase());
+				var meno = transcript.toLowerCase().indexOf('meno ');
+				console.log(meno, transcript.substring(meno+5,meno+6), transcript.substring(meno+5,meno+6) % 1);
+				if (meno>-1 && transcript.substring(meno+5,meno+6) % 1 === 0) {
+					console.log('replace');
+					transcript = transcript.replace(transcript.substring(meno,meno+5), '-');
+				}
+				// finalTranscript += transcript+'<br>\\n';
+				if (transcript.length>0) finalTranscript = transcript;
+			} else {
+				interimTranscript += transcript+'<br>\\n';
+			}
+			document.getElementById('transcriptDiv').innerHTML = '<i style=\"color:#ddd;\">' + interimTranscript + '</i>';
+		}
+		// document.getElementById('transcriptDiv').innerHTML = finalTranscript + '<i style=\"color:#00d;\">' + interimTranscript + '</i>';
+		document.getElementById('transcriptDiv').innerHTML =  '<i style=\"color:#00d;\">' + finalTranscript + '</i>';
+	};
+	showLog('start'+'\n');
+	recognition.start();