changeset 46:cc20eebaa74a

use openai tts
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 14 Aug 2025 11:27:34 +0900
parents fef7a5c65cfb
children 3cd6f36c81d4
files courses/j1.txt src/chat.html.luan src/chat.js src/chats.html.luan src/edit_course.html.luan src/lang_courses.html.luan src/lib/Chat.luan src/lib/Course.luan src/lib/Shared.luan src/lib/Utils.luan src/lib/languages.luan src/new_chat.red.luan src/private/Config_sample.luan src/private/tools/chat.html.luan src/save_chat.js.luan src/save_course.js.luan src/site.js src/tts.mp3.luan src/your_courses.html.luan
diffstat 19 files changed, 75 insertions(+), 116 deletions(-) [+]
line wrap: on
line diff
--- a/courses/j1.txt	Wed Aug 13 10:31:24 2025 +0900
+++ b/courses/j1.txt	Thu Aug 14 11:27:34 2025 +0900
@@ -34,3 +34,5 @@
 
 
 You can start.
+
+Only use Japanese and English.
--- a/src/chat.html.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/chat.html.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -16,7 +16,8 @@
 local current_user = User.current or error()
 local Chat = require "site:/lib/Chat.luan"
 local get_chat_by_id = Chat.get_by_id or error()
-local languages = require "site:/lib/languages.luan"
+local Utils = require "site:/lib/Utils.luan"
+local capitalize = Utils.capitalize or error()
 
 
 return function()
@@ -79,18 +80,10 @@
 				</p>
 				<p><%= chat.language_name() %></p>
 				<p>
-					<label>Region</label><br> 
-					<select name=language_region>
-<%	for _, region in ipairs(languages[chat.language].regions) do %>
-						<option value="<%=region.code%>"><%=region.name%></option>
-<%	end %>
-					<select>
-				</p>
-				<p>
 					<label>Voice</label><br> 
 					<select name=voice>
 <%	for _, voice in ipairs(voices) do %>
-						<option value="<%=voice.code%>"><%=voice.name%></option>
+						<option value="<%=voice%>"><%=capitalize(voice)%></option>
 <%	end %>
 					<select>
 				</p>
--- a/src/chat.js	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/chat.js	Thu Aug 14 11:27:34 2025 +0900
@@ -3,11 +3,10 @@
 let chat;
 
 function setChat(newChat) {
-	let audioChanged = chat && (chat.language_region != newChat.language_region || chat.voice != newChat.voice);
+	let audioChanged = chat && chat.voice != newChat.voice;
 	chat = newChat;
 	document.querySelector('[content] [name]').textContent = chat.name;
 	if(audioChanged) {
-		let lang = `lang=${chat.language_region}&`;
 		let voice = `voice=${chat.voice}&`;
 		let audios = document.querySelectorAll('audio[src]');
 		for( let audio of audios ) {
@@ -22,7 +21,6 @@
 function editChat(name) {
 	let dialog = document.querySelector('dialog[edit]');
 	dialog.querySelector('input[name=name]').value = chat.name;
-	dialog.querySelector('select[name=language_region]').value = chat.language_region;
 	dialog.querySelector('select[name=voice]').value = chat.voice;
 	dialog.querySelector('input[name=show_text]').checked = chat.show_text;
 	dialog.querySelector('input[name=autoplay]').checked = chat.autoplay;
@@ -67,7 +65,7 @@
 }
 
 function handleChatMarkdown() {
-	handleMarkdown(chat.language_region,chat.voice);
+	handleMarkdown(chat.voice,chat.tts_instructions);
 }
 
 function scrollToEnd() {
@@ -100,7 +98,7 @@
 	textarea.parentNode.scrollIntoViewIfNeeded(false);
 	if( !audio )
 		audio = document.querySelector('div[buttons] audio');
-	audio.src = `/tts.mp3?lang=${chat.language_region}&voice=${chat.voice}&text=${encodeURIComponent(textarea.value)}`;
+	audio.src = `/tts.mp3?voice=${chat.voice}&instructions=${encodeURIComponent(chat.tts_instructions)}&text=${encodeURIComponent(textarea.value)}`;
 }
 
 function askAi() {
--- a/src/chats.html.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/chats.html.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -7,11 +7,11 @@
 local Shared = require "site:/lib/Shared.luan"
 local head = Shared.head or error()
 local header = Shared.header or error()
+local languages = Shared.languages or error()
 local User = require "site:/lib/User.luan"
 local current_user = User.current_required or error()
 local Chat = require "site:/lib/Chat.luan"
 local chat_search = Chat.search or error()
-local languages = require "site:/lib/languages.luan"
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "chats.html"
 
@@ -42,11 +42,10 @@
 			<h1>Your Chats</h1>
 			<form action="lang_courses.html">
 				<select name=language>
-<%	for _, lang in pairs(languages) do
-		local code = lang.code
+<%	for code, name in pairs(languages) do
 		local selected = code==select_language and "selected" or ""
 %>
-					<option value="<%=code%>" <%=selected%> ><%=lang.name%></option>
+					<option value="<%=code%>" <%=selected%> ><%=name%></option>
 <%	end %>
 				</select>
 				<input type=submit value="new chat">
--- a/src/edit_course.html.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/edit_course.html.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -72,6 +72,9 @@
 			<h4>AI first message (optional)</h4>
 			<textarea name=ai_first_message oninput="fixTextarea(event.target)"><%=html_encode(course.ai_first_message or "")%></textarea>
 
+			<h4>Text to speech instructions</h4>
+			<textarea name=tts_instructions oninput="fixTextarea(event.target)"><%=html_encode(course.tts_instructions or "")%></textarea>
+
 			<input type=submit>
 
 			<hr>
--- a/src/lang_courses.html.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/lang_courses.html.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -6,9 +6,9 @@
 local Shared = require "site:/lib/Shared.luan"
 local head = Shared.head or error()
 local header = Shared.header or error()
+local languages = Shared.languages or error()
 local Course = require "site:/lib/Course.luan"
 local course_search = Course.search or error()
-local languages = require "site:/lib/languages.luan"
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "chats.html"
 
@@ -35,7 +35,7 @@
 	<body>
 <%		header() %>
 		<div content>
-			<h1><%=languages[language].name%> Courses</h1>
+			<h1><%=languages[language]%> Courses</h1>
 			<form action="edit_course.html">
 				<input type=hidden name=language value="<%=language%>">
 				<input type=submit value="new course">
--- a/src/lib/Chat.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/lib/Chat.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -10,11 +10,11 @@
 local Db = require "site:/lib/Db.luan"
 local run_in_transaction = Db.run_in_transaction or error()
 local Ai_chat = require "site:/lib/ai/claude/Ai_chat.luan"
-local languages = require "site:/lib/languages.luan"
 local Course = require "site:/lib/Course.luan"
 local get_course_by_id = Course.get_by_id or error()
 local Shared = require "site:/lib/Shared.luan"
 local voices = Shared.voices or error()
+local languages = Shared.languages or error()
 
 
 local Chat = {}
@@ -29,7 +29,7 @@
 		name = doc.name
 		ai_thread = doc.ai_thread
 		language = doc.language
-		language_region = doc.language_region
+		tts_instructions = doc.tts_instructions
 		voice = doc.voice
 		show_text = doc.show_text == "true"
 		autoplay = doc.autoplay == "true"
@@ -47,7 +47,7 @@
 		name = chat.name or error()
 		ai_thread = chat.ai_thread or error()
 		language = chat.language or error()
-		language_region = chat.language_region or error()
+		tts_instructions = chat.tts_instructions -- or error()
 		voice = chat.voice or error()
 		show_text = chat.show_text and "true" or "false"
 		autoplay = chat.autoplay and "true" or "false"
@@ -55,14 +55,9 @@
 	}
 end
 
-local function first_region(language)
-	return languages[language].regions[1].code
-end
-
 function Chat.new(chat)
 	chat.updated = chat.updated or time_now()
-	chat.language_region = chat.language_region or first_region(chat.language)
-	chat.voice = chat.voice or voices[1].code
+	chat.voice = chat.voice or voices[1]
 	if chat.show_text==nil then chat.show_text = true end
 	if chat.autoplay==nil then chat.autoplay = true end
 
@@ -83,8 +78,8 @@
 	function chat.info()
 		return {
 			id = chat.id
-			language_region = chat.language_region
 			voice = chat.voice
+			tts_instructions = chat.tts_instructions
 			name = chat.name
 			show_text = chat.show_text
 			autoplay = chat.autoplay
@@ -124,7 +119,7 @@
 	end
 
 	function chat.language_name()
-		return languages[chat.language].name
+		return languages[chat.language]
 	end
 
 	return chat
--- a/src/lib/Course.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/lib/Course.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -6,7 +6,8 @@
 local Html = require "luan:Html.luan"
 local html_encode = Html.encode or error()
 local Db = require "site:/lib/Db.luan"
-local languages = require "site:/lib/languages.luan"
+local Shared = require "site:/lib/Shared.luan"
+local languages = Shared.languages or error()
 
 
 local Course = {}
@@ -21,6 +22,7 @@
 		name = doc.name
 		ai_system_prompt = doc.ai_system_prompt
 		ai_first_message = doc.ai_first_message
+		tts_instructions = doc.tts_instructions
 	}
 end
 
@@ -34,6 +36,7 @@
 		name = course.name or error()
 		ai_system_prompt = course.ai_system_prompt or error()
 		ai_first_message = course.ai_first_message
+		tts_instructions = course.tts_instructions
 	}
 end
 
@@ -50,7 +53,7 @@
 	end
 
 	function course.language_name()
-		return languages[course.language].name
+		return languages[course.language]
 	end
 
 	return course
--- a/src/lib/Shared.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/lib/Shared.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -62,15 +62,23 @@
 	end )
 end
 
+Shared.languages = {
+	en = "English"
+	jp = "Japanese"
+	ko = "Korean"
+}
+
 Shared.voices = {
-	{
-		name = "Brandon"
-		code = "en-US-BrandonMultilingualNeural"
-	}
-	{
-		name = "Jenny"
-		code = "en-US-JennyMultilingualNeural"
-	}
+	"onyx"
+	"sage"
+	"alloy"
+	"ash"
+	"ballad"
+	"coral"
+	"echo"
+	"fable"
+	"nova"
+	"shimmer"
 }
 
 return Shared
--- a/src/lib/Utils.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/lib/Utils.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -2,6 +2,9 @@
 local error = Luan.error
 local pairs = Luan.pairs or error()
 local type = Luan.type or error()
+local String = require "luan:String.luan"
+local to_upper = String.upper or error()
+local substring = String.sub or error()
 local Http = require "luan:http/Http.luan"
 
 
@@ -47,4 +50,8 @@
 end
 ]]
 
+function Utils.capitalize(s)
+	return to_upper(substring(s,1,1))..substring(s,2)
+end
+
 return Utils
--- a/src/lib/languages.luan	Wed Aug 13 10:31:24 2025 +0900
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local pairs = Luan.pairs or error()
-local ipairs = Luan.ipairs or error()
-
-
--- https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts
-
-local regions = {
-	GB = "United Kingdom"
-	JP = "Japan"
-	KR = "Korea"
-	US = "United States"
-}
-
-local languages = {
-	en = {
-		name = "English"
-		regions = {"US","GB"}
-	}
-	jp = {
-		name = "Japanese"
-		regions = {"JP"}
-	}
-	ko = {
-		name = "Korean"
-		regions = {"KR"}
-	}
-}
-
-for code, info in pairs(languages) do
-	info.code = code
-	local t = {nil}
-	for _, region in ipairs(info.regions) do
-		t[#t+1] = {
-			code = code.."-"..region
-			name = regions[region] or error(region)
-		}
-	end
-	info.regions = t
-end
-
-return languages
--- a/src/new_chat.red.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/new_chat.red.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -20,6 +20,7 @@
 		course_id = course.id
 		name = course.name
 		language = course.language
+		tts_instructions = course.tts_instructions
 		ai_thread = ai_init(course.ai_system_prompt)
 	}
 	chat.save()
--- a/src/private/Config_sample.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/private/Config_sample.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -8,10 +8,6 @@
 	claude = {
 		key = "sk-xxx"
 	}
-	azure_tts = {
-		key = "xxx"
-		region = "eastus"
-	}
 	chatgpt = {
 		key = "sk-xxx"
 	}
--- a/src/private/tools/chat.html.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/private/tools/chat.html.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -41,7 +41,7 @@
 		</div>
 <%	if process_markdown then %>
 		<script>
-			handleMarkdown(<%=json_string(chat.language_region)%>,<%=json_string(chat.voice)%>);
+			handleMarkdown(<%=json_string(chat.voice)%>,<%=json_string(chat.tts_instructions)%>);
 		</script>
 <%	end %>
 	</body>
--- a/src/save_chat.js.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/save_chat.js.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -17,7 +17,6 @@
 return function()
 	local chat = Http.request.parameters.chat or error()
 	local name = Http.request.parameters.name or error()
-	local language_region = Http.request.parameters.language_region or error()
 	local voice = Http.request.parameters.voice or error()
 	local show_text = Http.request.parameters.show_text
 	local autoplay = Http.request.parameters.autoplay
@@ -26,7 +25,6 @@
 		chat = get_chat_by_id(chat) or error()
 		chat.user_id == current_user().id or error()
 		chat.name = name
-		chat.language_region = language_region
 		chat.voice = voice
 		chat.show_text = show_text ~= nil
 		chat.autoplay = autoplay ~= nil
--- a/src/save_course.js.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/save_course.js.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -30,6 +30,7 @@
 		course.name = parameters.name or error()
 		course.ai_system_prompt = parameters.ai_system_prompt or error()
 		course.ai_first_message = parameters.ai_first_message or error()
+		course.tts_instructions = parameters.tts_instructions or error()
 		course.updated = time_now()
 		course.save()
 	end )
--- a/src/site.js	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/site.js	Thu Aug 14 11:27:34 2025 +0900
@@ -151,7 +151,7 @@
 	}
 }
 
-function handleMarkdown(lang,voice) {
+function handleMarkdown(voice,instructions) {
 	let converter = window.markdownit();
 	let divs = document.querySelectorAll('[markdown]');
 	for( let div of divs ) {
@@ -166,7 +166,7 @@
 				rt.remove();
 			}
 			//console.log(mdDiv.textContent);
-			parent.querySelector('audio').src = `/tts.mp3?lang=${lang}&voice=${voice}&text=${encodeURIComponent(mdDiv.textContent)}`;
+			parent.querySelector('audio').src = `/tts.mp3?voice=${voice}&instructions=${encodeURIComponent(instructions)}&text=${encodeURIComponent(mdDiv.textContent)}`;
 		}
 	}
 }
--- a/src/tts.mp3.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/tts.mp3.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -1,43 +1,42 @@
 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 json_string = Parsers.json_string 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 key = Config.chatgpt.key or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "tts.js"
 
 
--- https://learn.microsoft.com/en-us/azure/ai-services/speech-service/index-text-to-speech
+-- https://platform.openai.com/docs/guides/text-to-speech
 
-local region = Config.azure_tts.region or error()
-local url = "https://"..region..".tts.speech.microsoft.com/cognitiveservices/v1"
+local url = "https://api.openai.com/v1/audio/speech"
 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"
+	Authorization = "Bearer "..key
+	["Content-Type"] = "application/json"
 }
 
-local function text_to_speech(lang,voice,text)
-	local xml = `%>
-<speak version='1.0' xml:lang='<%=lang%>'>
-    <voice name='<%=voice%>'>
-<%=		xml_encode(text) %>
-    </voice>
-</speak>
-<%	`
+local function text_to_speech(voice,instructions,text)
 	local options = {
 		method = "POST"
 		headers = headers
-		content = xml
+		content = json_string{
+			model = "gpt-4o-mini-tts"
+			voice = voice
+			input = text
+			instructions = instructions
+		}
 	}
 	return uri(url,options)
 end
 
 return function()
-	local lang = Http.request.parameters.lang or error()
 	local voice = Http.request.parameters.voice or error()
+	local instructions = Http.request.parameters.instructions or error()
 	local text = Http.request.parameters.text or error()
-	local input = text_to_speech(lang,voice,text)
+	local input = text_to_speech(voice,instructions,text)
 	Http.response.binary_writer().write_from(input)
 end
--- a/src/your_courses.html.luan	Wed Aug 13 10:31:24 2025 +0900
+++ b/src/your_courses.html.luan	Thu Aug 14 11:27:34 2025 +0900
@@ -7,11 +7,11 @@
 local Shared = require "site:/lib/Shared.luan"
 local head = Shared.head or error()
 local header = Shared.header or error()
+local languages = Shared.languages or error()
 local User = require "site:/lib/User.luan"
 local current_user = User.current_required or error()
 local Course = require "site:/lib/Course.luan"
 local course_search = Course.search or error()
-local languages = require "site:/lib/languages.luan"
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "chats.html"
 
@@ -42,11 +42,10 @@
 			<h1>Your Courses</h1>
 			<form action="edit_course.html">
 				<select name=language>
-<%	for _, lang in pairs(languages) do
-		local code = lang.code
+<%	for code, name in pairs(languages) do
 		local selected = code==select_language and "selected" or ""
 %>
-					<option value="<%=code%>" <%=selected%> ><%=lang.name%></option>
+					<option value="<%=code%>" <%=selected%> ><%=name%></option>
 <%	end %>
 				</select>
 				<input type=submit value="new course">