update voice & update flow

main
reng 5 months ago
parent 644c9b175b
commit e94ac148f2
  1. BIN
      vite/public/assets/0721/onyx/q4-1.mp3
  2. BIN
      vite/public/assets/0721/onyx/q4-2.mp3
  3. BIN
      vite/public/assets/0721/onyx/q4.mp3
  4. BIN
      vite/public/assets/0721/onyx/q5.mp3
  5. BIN
      vite/public/assets/0721/shimmer/q4-1.mp3
  6. BIN
      vite/public/assets/0721/shimmer/q4-2.mp3
  7. BIN
      vite/public/assets/0721/shimmer/q4.mp3
  8. BIN
      vite/public/assets/0721/shimmer/q5.mp3
  9. 50
      vite/public/cuelist_free.json
  10. 37
      vite/src-tauri/Cargo.lock
  11. 1
      vite/src-tauri/Cargo.toml
  12. 29
      vite/src-tauri/src/lib.rs
  13. 2
      vite/src/App.jsx
  14. 25
      vite/src/comps/light.jsx
  15. 35
      vite/src/pages/flow_free.jsx
  16. 6
      vite/src/util/osc.js
  17. 4
      vite/src/util/useChat.jsx

@ -31,9 +31,9 @@
"id": 4, "id": 4,
"name": "Q4", "name": "Q4",
"type": "phone", "type": "phone",
"description": "Guide to call", "description": "引導撥號",
"auto": false, "auto": false,
"audioFile": "assets/q4.mp3", "audioFile": "assets/0721/onyx/q4.mp3",
"nextcue": 4.1, "nextcue": 4.1,
"callback":"numpad" "callback":"numpad"
}, },
@ -41,42 +41,60 @@
"id": 4.1, "id": 4.1,
"name": "Q4.1", "name": "Q4.1",
"type": "phone", "type": "phone",
"description": "Guide to construct scene", "description": "電話開頭",
"auto": true, "auto": true,
"audioFile": "assets/q4-1.mp3", "audioFile": "assets/0721/onyx/q4-1.mp3",
"nextcue": 4.2, "nextcue": 4.2
"status":"intro"
}, },
{ {
"id": 4.2, "id": 4.2,
"name": "Q4.2", "name": "Q4.2",
"type": "phone",
"description": "示範影片,引導回憶",
"auto": true,
"audioFile": "assets/0721/onyx/q4-2.mp3",
"nextcue": 4.3,
"status":"intro"
},
{
"id": 4.3,
"name": "Q4.3",
"type": "chat", "type": "chat",
"description": "chat", "description": "chat",
"auto": true, "auto": true,
"nextcue": 4.3, "nextcue": 4.4,
"duration": 90, "duration": 90,
"status":"go" "status":"go"
}, },
{ {
"id": 4.3, "id": 4.4,
"name": "Q4.3", "name": "Q4.4",
"type": "chat_end", "type": "chat_end",
"description": "chat end", "description": "對話收尾",
"auto": true, "auto": true,
"nextcue": 5 "nextcue": 5.1
}, },
{ {
"id": 5, "id": 5.1,
"name": "Q5", "name": "Q5.1",
"type": "phone",
"description": "引導打給遺憾對象",
"auto": true,
"audioFile": "assets/0721/onyx/q5.mp3",
"nextcue": 5.2
},
{
"id": 5.2,
"name": "Q5.2",
"type": "user_input", "type": "user_input",
"description": "call", "description": "call",
"duration": 60, "duration": 60,
"auto": true, "auto": true,
"nextcue": 5.1 "nextcue": 5.3
}, },
{ {
"id": 5.1, "id": 5.3,
"name": "Q5.1", "name": "Q5.3",
"type": "summary", "type": "summary",
"description": "summary", "description": "summary",
"auto": true, "auto": true,

@ -95,6 +95,7 @@ name = "app"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"dotenv", "dotenv",
"enttecopendmx",
"log", "log",
"rosc", "rosc",
"serde", "serde",
@ -1025,6 +1026,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enttecopendmx"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439b5a6167b4d55c80838155742c65c5159f76ee4acd25324bdeb142828aa0c5"
dependencies = [
"libftd2xx",
]
[[package]] [[package]]
name = "enumflags2" name = "enumflags2"
version = "0.7.12" version = "0.7.12"
@ -2137,6 +2147,27 @@ version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libftd2xx"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b3be3aa1917532fb3918dde5fda1c82d7169d2bbd2e79353bac2cd3985e68a"
dependencies = [
"libftd2xx-ffi",
"log",
"paste",
"static_assertions",
]
[[package]]
name = "libftd2xx-ffi"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd82d23ae0cbbf0fb65ad0310b474e45ddfdece8aa03a34130c59c04333c701"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.7.4" version = "0.7.4"
@ -2716,6 +2747,12 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "pathdiff" name = "pathdiff"
version = "0.2.3" version = "0.2.3"

@ -31,3 +31,4 @@ webview2-com = "0.37.0"
windows = "0.61.1" windows = "0.61.1"
tauri-plugin-fs = "2" tauri-plugin-fs = "2"
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
enttecopendmx ="0.1.0"

@ -85,7 +85,8 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
get_env, get_env,
send_osc_message, send_osc_message,
reset_permission reset_permission,
send_dmx_message
]) ])
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())
.setup(|app| { .setup(|app| {
@ -101,3 +102,29 @@ pub fn run() {
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }
static mut LIGHT: Option<enttecopendmx::EnttecOpenDMX> = None;
#[tauri::command]
fn send_dmx_message(message: &str) -> Result<(), String> {
println!("Sending DMX message: {}", message);
let val = message.parse::<u8>().map_err(|e| e.to_string())?;
unsafe {
if LIGHT.is_none() {
let mut dmx = enttecopendmx::EnttecOpenDMX::new().map_err(|e| e.to_string())?;
dmx.open().map_err(|e| e.to_string())?;
LIGHT = Some(dmx);
}
if let Some(ref mut dmx) = LIGHT {
dmx.set_channel(1, val);
dmx.render().unwrap();
} else {
return Err("DMX interface not available".into());
}
}
Ok(())
}

@ -9,7 +9,7 @@ function App() {
return ( return (
<div className='w-full flex flex-row gap-2 justify-center py-2 px-8 *:bg-pink-200 *:px-2'> <div className='w-full flex flex-row gap-2 justify-center py-2 px-8 *:bg-pink-200 *:px-2'>
<a href="/">Conversation</a> <a href="/">Conversation</a>
<a href="/flow">Flow</a> {/* <a href="/flow">Flow</a> */}
<a href="/free-flow">Free Flow</a> <a href="/free-flow">Free Flow</a>
<a href="/settings">Settings</a> <a href="/settings">Settings</a>
</div> </div>

@ -1,8 +1,8 @@
import gsap from "gsap" import gsap from "gsap"
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react" import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"
import { OSC_ADDRESS, sendOsc } from "../util/osc"; import { invoke } from '@tauri-apps/api/core';
const FADE_TIME=3; const FADE_TIME=5;
export const Light=forwardRef((props, ref)=>{ export const Light=forwardRef((props, ref)=>{
@ -18,7 +18,16 @@ export const Light=forwardRef((props, ref)=>{
duration: time, duration: time,
onUpdate: () => { onUpdate: () => {
// console.log(refVal.current.val); // console.log(refVal.current.val);
sendOsc(OSC_ADDRESS.LIGHT, refVal.current.val.toString()); // sendOsc(OSC_ADDRESS.LIGHT, refVal.current.val.toString());
invoke('send_dmx_message', {
message: Math.floor(refVal.current.val * 255).toString(),
}).then(() => {
console.log(`dmx message sent: ${Math.floor(refVal.current.val * 255)}`);
}).catch((error) => {
console.error('Error sending DMX message:', error);
});
if(refContainer.current) if(refContainer.current)
refContainer.current.style.background= `rgba(0, 255, 0, ${refVal.current.val})`; // Update background color based on value refContainer.current.style.background= `rgba(0, 255, 0, ${refVal.current.val})`; // Update background color based on value
}, },
@ -41,15 +50,15 @@ export const Light=forwardRef((props, ref)=>{
},[]); },[]);
return ( return (
<div ref={refContainer} className="flex flex-row gap-4 !w-auto p-1 border"> <div ref={refContainer} className="grid grid-cols-2 justify-between p-1 border *:overflow-hidden">
<span className="flex flex-col justify-between"> {/* <span className="flex flex-col justify-between"> */}
<label className="text-center">Light</label> <label className="text-center">Light</label>
<input ref={refInput} type="text" className="border w-[5vw]" defaultValue={5}/> <input ref={refInput} type="text" className="border w-[5vw]" defaultValue={5}/>
</span> {/* </span>
<span className="flex flex-col justify-between"> <span className="flex flex-col justify-between"> */}
<button onClick={()=>fade(0,1)}>fadeIn</button> <button onClick={()=>fade(0,1)}>fadeIn</button>
<button onClick={()=>fade(1,0)}>fadeOut</button> <button onClick={()=>fade(1,0)}>fadeOut</button>
</span> {/* </span> */}
</div> </div>
) )
}); });

@ -28,6 +28,11 @@ const ChatStatus={
Processing: 'processing', Processing: 'processing',
} }
const Voice={
ONYX: 'onyx',
SHIMMER: 'shimmer',
}
export function FreeFlow(){ export function FreeFlow(){
const { data }=useData(); const { data }=useData();
@ -39,6 +44,8 @@ export function FreeFlow(){
const [autoSend, setAutoSend] = useState(true); const [autoSend, setAutoSend] = useState(true);
const [userId, setUserId] = useState(); const [userId, setUserId] = useState();
const [summary, setSummary] = useState(null); const [summary, setSummary] = useState(null);
const [voice, setVoice] = useState(Voice.ONYX);
const [chatStatus, setChatStatus] = useState(ChatStatus.System); // System, User, Processing const [chatStatus, setChatStatus] = useState(ChatStatus.System); // System, User, Processing
@ -78,7 +85,11 @@ export function FreeFlow(){
refAudio.current.pause(); // Stop any currently playing audio refAudio.current.pause(); // Stop any currently playing audio
} }
const audio = new Audio(url); let audioUrl = url;
if(voice==Voice.SHIMMER) audioUrl = url.replace(Voice.ONYX, Voice.SHIMMER);
console.log('Using voice:', voice, 'for audio:', audioUrl);
const audio = new Audio(audioUrl);
audio.loop=refCurrentCue.current?.loop || false; // Set loop if defined in cue audio.loop=refCurrentCue.current?.loop || false; // Set loop if defined in cue
audio.play().catch(error => { audio.play().catch(error => {
console.error("Audio playback error:", error); console.error("Audio playback error:", error);
@ -122,14 +133,14 @@ export function FreeFlow(){
// Special case for starting a conversation // Special case for starting a conversation
resetTranscript(); resetTranscript();
console.log('Starting conversation...'); console.log('Starting conversation...');
sendMessage(); sendMessage(null, false, false, voice); // Send initial message with voice
setChatWelcome(true); setChatWelcome(true);
resetData(); // Reset data for new conversation resetData(); // Reset data for new conversation
break; break;
case 'chat_end': case 'chat_end':
const message= refInput.current?.value?.trim(); const message= refInput.current?.value?.trim();
console.log('Ending conversation with message:', message); console.log('Ending conversation with message:', message);
sendMessage(message, false, true); sendMessage(message, false, true, voice);
setChatWelcome(false); setChatWelcome(false);
break; break;
@ -246,7 +257,7 @@ export function FreeFlow(){
const message= refInput.current?.value?.trim(); const message= refInput.current?.value?.trim();
if(message && message.length>0) { if(message && message.length>0) {
console.log('Ending conversation with message:', message); console.log('Ending conversation with message:', message);
sendMessage(message, false, false); sendMessage(message, false, false, voice);
setChatWelcome(false); setChatWelcome(false);
setChatStatus(ChatStatus.Processing); // Set chat status to Processing setChatStatus(ChatStatus.Processing); // Set chat status to Processing
@ -341,19 +352,25 @@ export function FreeFlow(){
<main className="items-start"> <main className="items-start">
<section className="flex-1 flex flex-col gap-2 self-stretch overflow-hidden"> <section className="flex-1 flex flex-col gap-2 self-stretch overflow-hidden">
<DebugControl/> <DebugControl/>
<div className="w-full p-2 flex flex-row justify-center gap-2 *:w-[12vw] *:h-[5rem]"> <div className="w-full p-2 grid grid-cols-4 gap-2 items-stretch justify-center *:max-h-[5rem]">
<div className="bg-gray-100 text-4xl font-bold mb-4 flex justify-center items-center"> <div className="bg-gray-100 text-4xl font-bold mb-4 flex justify-center items-center">
{refCurrentCue.current?.name} {refCurrentCue.current?.name}
</div> </div>
<Countdown ref={refTimer} /> <Countdown ref={refTimer} />
<button className="!bg-red-300" onClick={onStop}>Stop</button> <button className="!bg-red-300" onClick={onStop}>Stop</button>
<button className="!bg-yellow-300" onClick={()=>{
saveHistory(history); {/* <button onClick={saveImage}>Save image</button> */}
}}>Save Log</button>
<button onClick={saveImage}>Save image</button>
<NumPad onSend={onNumpad} /> <NumPad onSend={onNumpad} />
<Light ref={refLight} /> <Light ref={refLight} />
<VoiceAnalysis/> <VoiceAnalysis/>
<div className="flex flex-col">
<label className="text-center">Voice</label>
<button className={`${voice==Voice.ONYX && 'bg-gray-300'}`} onClick={() => setVoice(Voice.ONYX)}>Onyx</button>
<button className={`${voice==Voice.SHIMMER && 'bg-gray-300'}`} onClick={() => setVoice(Voice.SHIMMER)}>Shimmer</button>
</div>
<button className="!bg-yellow-300" onClick={()=>{
saveHistory(history);
}}>Save Log</button>
</div> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<table className="border-collapse **:border-y w-full **:p-2"> <table className="border-collapse **:border-y w-full **:p-2">

@ -20,6 +20,8 @@ export async function sendOsc(key, message){
console.warn('sendOsc: message is empty, skipping'); console.warn('sendOsc: message is empty, skipping');
return; return;
} }
try{
console.log(`Sending OSC message: ${key} -> ${message}`); console.log(`Sending OSC message: ${key} -> ${message}`);
await invoke('send_osc_message', { await invoke('send_osc_message', {
key: key, key: key,
@ -27,6 +29,9 @@ export async function sendOsc(key, message){
host:`0.0.0.0:0`, host:`0.0.0.0:0`,
target: '127.0.0.1:9000', target: '127.0.0.1:9000',
}); });
}catch (error){
console.error('Error sending OSC message:', error);
}
} }
@ -44,6 +49,5 @@ export async function updatePrompt(prompt) {
}); });
}catch(error){ }catch(error){
console.error('Error updating prompt:', error); console.error('Error updating prompt:', error);
throw error;
} }
} }

@ -35,7 +35,7 @@ export function ChatProvider({children}){
} }
function sendMessage(message, force_no_audio=false, isLastMessage=false) { function sendMessage(message, force_no_audio=false, isLastMessage=false, voice=null) {
console.log('Sending chat message:', message); console.log('Sending chat message:', message);
setStatus(Status.PROCESSING_TEXT); setStatus(Status.PROCESSING_TEXT);
@ -60,7 +60,7 @@ export function ChatProvider({children}){
if(response.output_text && (!force_no_audio && audioOutput)){ if(response.output_text && (!force_no_audio && audioOutput)){
setStatus(Status.PROCESSING_AUDIO); setStatus(Status.PROCESSING_AUDIO);
textToSpeech(response.output_text, data?.voice, data?.voice_prompt).then(url => { textToSpeech(response.output_text, voice||data?.voice, data?.voice_prompt).then(url => {
setStatus(Status.SUCCESS); setStatus(Status.SUCCESS);
setAudioUrl(url); // Store the audio URL setAudioUrl(url); // Store the audio URL

Loading…
Cancel
Save