changeset 12:2d4b3f003ec2

tts
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 24 Jul 2025 22:14:49 -0600
parents 003a90ce72d7
children 65bd7e245c63
files .hgignore src/chat.js src/lib/ai/claude/Chat.luan src/private/Config_sample.luan src/site.js src/tts.mp3.luan
diffstat 6 files changed, 63 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue Jul 22 15:30:10 2025 -0600
+++ b/.hgignore	Thu Jul 24 22:14:49 2025 -0600
@@ -5,3 +5,4 @@
 private/Config.luan
 mine/
 .DS_Store
+test/
--- a/src/chat.js	Tue Jul 22 15:30:10 2025 -0600
+++ b/src/chat.js	Thu Jul 24 22:14:49 2025 -0600
@@ -52,6 +52,11 @@
 		scroll.scrollTo(0,scroll.scrollHeight);
 	});
 */
+	let audios = document.querySelectorAll('audio');
+	if( audios.length >= 1 ) {
+		let audio = audios[audios.length-1];
+		audio.play();
+	}
 }
 
 const isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
--- a/src/lib/ai/claude/Chat.luan	Tue Jul 22 15:30:10 2025 -0600
+++ b/src/lib/ai/claude/Chat.luan	Thu Jul 24 22:14:49 2025 -0600
@@ -96,16 +96,7 @@
 	messages[#messages+1] = {
 		role = "assistant"
 		content = [[
-- 1
-- 2
-- 3
-- 4
-- 5
-- 6
-- 7
-- 8
-- 9
-- 10
+uhuh
 ]]
 	}
 	if true then
--- a/src/private/Config_sample.luan	Tue Jul 22 15:30:10 2025 -0600
+++ b/src/private/Config_sample.luan	Thu Jul 24 22:14:49 2025 -0600
@@ -8,4 +8,8 @@
 	claude = {
 		key = "sk-xxx"
 	}
+	azure_tts = {
+		key = "xxx"
+		region = "eastus"
+	}
 }
--- a/src/site.js	Tue Jul 22 15:30:10 2025 -0600
+++ b/src/site.js	Thu Jul 24 22:14:49 2025 -0600
@@ -119,7 +119,11 @@
 	window.scrollTo( 0, lastY );
 }
 
+
 // requires markdown-it
+
+let mdDiv = document.createElement('div');
+
 function handleMarkdown() {
 	let converter = window.markdownit({html: true});
 	let divs = document.querySelectorAll('[markdown]');
@@ -127,6 +131,15 @@
 		let text = div.textContent;
 		text = text.replace(/\{([^|}]+)\|([^|}]+)\}/g, '<ruby>$1<rt>$2</rt></ruby>');
 		text = converter.render(text);
+		if( div.getAttribute('role')==='assistant' ) {
+			mdDiv.innerHTML = text;
+			let rts = mdDiv.querySelectorAll('rt');
+			for( let rt of rts ) {
+				rt.remove();
+			}
+			//console.log(mdDiv.textContent);
+			text += `\n<p><audio controls preload=none src="/tts.mp3?text=${encodeURIComponent(mdDiv.textContent)}"></p>\n`;
+		}
 		div.innerHTML = text;
 		div.removeAttribute('markdown');
 	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tts.mp3.luan	Thu Jul 24 22:14:49 2025 -0600
@@ -0,0 +1,39 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local xml_encode = Parsers.xml_encode or error()
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local Http = require "luan:http/Http.luan"
+local Config = require "site:/private/Config.luan"
+
+
+local region = Config.azure_tts.region or error()
+local url = "https://"..region..".tts.speech.microsoft.com/cognitiveservices/v1"
+local headers = {
+	["Ocp-Apim-Subscription-Key"] = Config.azure_tts.key or error()
+	["Content-Type"] = "application/ssml+xml"
+	["X-Microsoft-OutputFormat"] = "audio-16khz-128kbitrate-mono-mp3"
+}
+
+local function text_to_speech(lang,text)
+	local xml = `%>
+<speak version='1.0' xml:lang='<%=lang%>'>
+    <voice name='en-US-BrandonMultilingualNeural'>
+<%=		xml_encode(text) %>
+    </voice>
+</speak>
+<%	`
+	local options = {
+		method = "POST"
+		headers = headers
+		content = xml
+	}
+	return uri(url,options)
+end
+
+return function()
+	local text = Http.request.parameters.text or error()
+	local input = text_to_speech("ja-JP",text)
+	Http.response.binary_writer().write_from(input)
+end