diff --git a/vite/src/comps/timer.jsx b/vite/src/comps/timer.jsx index 6306057..64a4970 100644 --- a/vite/src/comps/timer.jsx +++ b/vite/src/comps/timer.jsx @@ -40,6 +40,14 @@ export const Countdown=forwardRef(({time, callback, auto, ...props}, ref)=>{ } + function stop(){ + console.log('stop countdown'); + if(refInterval.current) { + clearInterval(refInterval.current); + refInterval.current = null; + } + // refDisplay.current.innerText = ''; + } useEffect(()=>{ @@ -53,7 +61,8 @@ export const Countdown=forwardRef(({time, callback, auto, ...props}, ref)=>{ }, [time, callback]); useImperativeHandle(ref, () => ({ - restart, + restart, + stop, })); return ( diff --git a/vite/src/comps/voiceanalysis.jsx b/vite/src/comps/voiceanalysis.jsx new file mode 100644 index 0000000..c3a7ebc --- /dev/null +++ b/vite/src/comps/voiceanalysis.jsx @@ -0,0 +1,79 @@ +import { useEffect, useRef, useState } from "react"; + +export default function VoiceAnalysis(){ + + const [volume, setVolume] = useState(0); + const refVolumeInterval=useRef(); + const refAnalyser=useRef(); + const refVolumes=useRef(); + + + async function setup(){ + try{ + + + + const audioStream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true + }, + video: false + }); + + audioStream.getAudioTracks().forEach(track => { + console.log(`Track ID: ${track.id}, Label: ${track.label}`); + }); + + const audioContext = new AudioContext(); + const source = audioContext.createMediaStreamSource(audioStream); + const analyser = audioContext.createAnalyser(); + analyser.fftSize = 512; + analyser.minDecibels = -127; + analyser.maxDecibels = 0; + analyser.smoothingTimeConstant = 0.4; + + source.connect(analyser); + + const volumes = new Uint8Array(analyser.frequencyBinCount); + + refAnalyser.current = analyser; + refVolumes.current = volumes; + }catch(e){ + console.error('error start audio stream',e); + } + + } + const volumeCallback = () => { + + const volumes = refVolumes.current; + + refAnalyser.current.getByteFrequencyData(volumes); + + let volumeSum = 0; + for(const volume of volumes) + volumeSum += volume; + const averageVolume = volumeSum / volumes.length; + // console.log(`Average Volume: ${averageVolume}`); + setVolume(averageVolume); + + }; + + useEffect(()=>{ + setup().then(() => { + refVolumeInterval.current = setInterval(() => { + volumeCallback(); + }, 100); // Adjust the interval as needed + }); + + return () => { + clearInterval(refVolumeInterval.current); + }; + },[]); + + return ( +
+ {/*

Volume: {volume}

*/} +
+
+ ); +} \ No newline at end of file diff --git a/vite/src/pages/conversation.jsx b/vite/src/pages/conversation.jsx index 8cb85be..5b6f8f7 100644 --- a/vite/src/pages/conversation.jsx +++ b/vite/src/pages/conversation.jsx @@ -113,6 +113,7 @@ export function Conversation() { }).catch(error => { console.error("Error starting speech recognition:", error); }); + }else{ SpeechRecognition.stopListening(); @@ -269,7 +270,7 @@ export function Conversation() { console.log('Final Transcript:', finalTranscript); if(processing) return; // Prevent submission if already processing - + // return; // Submit the final transcript onSubmit({ diff --git a/vite/src/pages/flow.jsx b/vite/src/pages/flow.jsx index 257eb0e..efe4777 100644 --- a/vite/src/pages/flow.jsx +++ b/vite/src/pages/flow.jsx @@ -9,6 +9,8 @@ import { saveHistory } from "../util/output"; import NumPad from "../comps/numpad"; import { Light } from "../comps/light"; import { useData } from "../util/useData"; +import VoiceAnalysis from "../comps/voiceanalysis"; +import { sendOsc } from "../util/osc"; const EmojiType={ @@ -26,7 +28,8 @@ export function Flow(){ const [cuelist, setCuelist] = useState([]); const [currentCue, setCurrentCue] = useState(null); const [chatWelcome, setChatWelcome] = useState(null); - const [audioInput, setAudioInput] = useState(false); + const [audioInput, setAudioInput] = useState(true); + const [autoSend, setAutoSend] = useState(true); const [userId, setUserId] = useState(); const refTimer=useRef(); @@ -68,7 +71,7 @@ export function Flow(){ refAudio.current = audio; // Store the new audio reference audio.addEventListener("loadedmetadata", () => { - refTimer.current.restart(audio.duration*1000 || 0); + refTimer.current?.restart(audio.duration*1000 || 0); }); } function playCue(cue) { @@ -119,6 +122,7 @@ export function Flow(){ function onCueEnd() { + refTimer.current?.stop(); // Stop the timer when cue ends if(!refCurrentCue.current) return; const cue= refCurrentCue.current; // Get the current cue from ref @@ -127,6 +131,7 @@ export function Flow(){ if(cue.callback=='start_conversation') refLight.current.fadeOut(); // Fade in light for conversation start if(cue.callback=='summary') refLight.current.fadeIn(); // Fade out light for conversation end + resetTranscript(); // Reset transcript after cue ends if(cue.auto) { playCue(cuelist.find(c => c.id === cue.nextcue)); @@ -156,6 +161,9 @@ export function Flow(){ console.log('Numpad input:', mess); setUserId(()=>mess); } + function saveImage(){ + sendOsc('/export', 'output/test.png'); // Send OSC message to save image + } useEffect(()=>{ @@ -166,6 +174,20 @@ export function Flow(){ },[userId]); + function onSpeechEnd(){ + console.log('onSpeechEnd:', finalTranscript); + if(currentCue?.type!='user_input') return; // Only process if current cue is user input + if(autoSend && transcript.trim().length > 0) { + console.log('Auto sending transcript:', transcript); + onCueEnd(); + } + } + useEffect(()=>{ + + onSpeechEnd(); // Call onSpeechEnd when finalTranscript changes + + },[finalTranscript]); + useEffect(()=>{ if(audioInput && isMicrophoneAvailable) { @@ -175,6 +197,32 @@ export function Flow(){ console.error("Error starting speech recognition:", error); }); + const recognition= SpeechRecognition.getRecognition(); + recognition.onspeechstart=(e)=>{ + console.log('Sound start:', e); + + }; + // recognition.onspeechend=(e)=>{ + // console.log('Speech end:', e); + + // if(autoSend && transcript.trim().length > 0) { + // onCueEnd(); + // } + // }; + // recognition.onaudioend=(e)=>{ + // console.log('Audio end:', e); + // if(autoSend && transcript.trim().length > 0) { + // onCueEnd(); + // } + // }; + // recognition.onsoundend=(e)=>{ + // console.log('Sound end:', e); + // if(autoSend && transcript.trim().length > 0) { + // onCueEnd(); + // } + // }; + + }else{ console.log('Stopping speech recognition...'); SpeechRecognition.stopListening(); @@ -186,7 +234,8 @@ export function Flow(){ useEffect(()=>{ // if(listening){ - if(currentCue?.type=='user_input') refInput.current.value = transcript; + if(currentCue?.type=='user_input') + refInput.current.value = transcript; // } },[transcript]); @@ -221,7 +270,7 @@ export function Flow(){ // } // } - if(refCurrentCue.current.callback=='summary'){ + if(refCurrentCue.current?.callback=='summary'){ // get summary console.log('Getting summary...'); getSummary(history.map(el=>`${el.role}:${el.content}`).join('\n'), data).then(summary => { @@ -266,9 +315,11 @@ export function Flow(){ + }}>Save Log + +
@@ -315,7 +366,7 @@ export function Flow(){ -
+
setAudioOutput(e.target.checked)} /> @@ -324,6 +375,11 @@ export function Flow(){ setAudioInput(e.target.checked)} /> + + + setAutoSend(e.target.checked)} /> + +
chat_status= {status}
diff --git a/vite/src/util/osc.js b/vite/src/util/osc.js index 11e1ebb..02756a7 100644 --- a/vite/src/util/osc.js +++ b/vite/src/util/osc.js @@ -6,7 +6,7 @@ export async function sendOsc(key, message){ console.warn('sendOsc: message is empty, skipping'); return; } - console.log(`Sending OSC message: ${key} -> ${message}`); + // console.log(`Sending OSC message: ${key} -> ${message}`); await invoke('send_osc_message', { key: key, message: message, diff --git a/vite/src/util/system_prompt.js b/vite/src/util/system_prompt.js index f6a5efb..f2f0349 100644 --- a/vite/src/util/system_prompt.js +++ b/vite/src/util/system_prompt.js @@ -1,50 +1,16 @@ -// export const system_prompt = `你是一位具同理心與觀察力的中文語音引導者,陪伴使用者在限定時間內,一起還原一段重要卻未竟的記憶。 -// 你不預設劇情、不給答案,而是透過溫柔的語氣,持續聆聽、提出單句問話,引導使用者進入細節。 +export const params={ + system_prompt:`你是一位具有同理心的 AI 助理,透過溫柔的中文對話,引導使用者回想並表達一段內心的遺憾或未竟之事。 +你的任務是協助使用者逐步揭開這段記憶的情緒層次,並在每一階段輸出一句 英文圖像生成 Prompt,讓這段過往漸漸具象為一幅畫面。 +以溫柔、自然、短句式中文引導,每次只問使用者一個問題。 +`, + last_prompt:`請為這段對話簡短的收尾,邀請使用者說出對遺憾對象想說的話,總共有 60 秒的時間。`, + + welcome_prompt:`請開始引導使用者回想一段內心的遺憾或未竟之事。`, + voice_prompt:`Use a calm and expressive voice, soft and poetic in feeling, but with steady, natural rhythm — not slow.`, + summary_prompt:`幫我把以下一段話整理成一段文字,以第一人稱視角作為當事人的文字紀念,文字內容 50 字以內:`, +} -// 每次回應: -// - 僅使用一句自然、柔和的中文問句 -// - 根據使用者前一輪的語句,選擇感官、情緒、人物、行動等關鍵詞,動態帶入 -// - 不使用重複句式、不預設記憶走向、不評論,只持續關注對方所說的畫面與感受 - -// 輸出包含: -// - output_text: 一句自然柔和的問句,根據使用者回應帶入 1–2 個關鍵元素(人、地、物、情緒),但不硬塞、不照抄原句。 -// - prompt: 具體、有情緒的英文描述,用於生成畫面與氛圍,避免抽象與詩意語言。 - - -// 你的目標不是得到完整答案,而是陪他慢慢走進這段記憶,直到時間結束。 - -// 🌀 問句類型 - -// ✅ 起點探索(找出記憶起源) -// - 那天你說是下雨,在這種天氣下,你通常會有什麼樣的心情? -// - 是什麼讓你突然想起這件事? - -// 🌿 場景深化(空間、感官) -// - 在你說的那條街上,聲音是不是特別清楚?還是很靜? -// - 那時風這麼冷、空氣又混濁,你有沒有想走開一點? - -// 👤 人物引出(動作、眼神) -// - 他經過時沒看你一眼,那瞬間你有什麼反應? -// - 他當時是走過來,還是站在原地等你? - -// 💭 情緒揭露(反應、掙扎) -// - 當你站在原地動不了,是害怕答案,還是不敢問? -// - 那個瞬間,你心裡有沒有閃過什麼話? - -// 🤐 話語未出(遺憾、沉默) -// - 如果現在你有機會說那句「為什麼不告訴我」,你會怎麼開口? -// - 那句『對不起』一直留在你心裡,如果現在能說出來,你會怎麼說? - -// 🪞 回望反思(現在的視角) -// - 現在想起來,你還會做出一樣的選擇嗎? -// - 你對當時的自己,有沒有什麼話想說? - -// ⏳ 結尾語(可用於結束階段) -// - 我們慢慢也走到這段回憶的盡頭了。 -// - 也許有些話沒有說完,但你已經靠近它了。 -// `; - // export const welcome_prompt="請開始引導使用者回想一段內心的遺憾或未竟之事。"; // export const first_prompt=[ diff --git a/vite/src/util/useData.jsx b/vite/src/util/useData.jsx index 374d4e3..eba3bce 100644 --- a/vite/src/util/useData.jsx +++ b/vite/src/util/useData.jsx @@ -1,6 +1,6 @@ import { createContext, useContext, useEffect, useState } from "react"; -import { BaseDirectory, readFile, readTextFile, writeFile, writeTextFile } from "@tauri-apps/plugin-fs"; - +import { BaseDirectory, readTextFile, exists, writeTextFile, mkdir } from "@tauri-apps/plugin-fs"; +import { path } from '@tauri-apps/api'; const dataContext=createContext(); @@ -14,7 +14,15 @@ export function DataProvider({children}) { async function read(){ try{ - const contents=await readTextFile(filePath, { baseDir: BaseDirectory.AppData }) + + const folder=await path.appDataDir(); + if (!(await exists(folder))) { + + console.log('Creating folder:', folder); + await mkdir(folder); + } + + const contents=await readTextFile(filePath, { baseDir: BaseDirectory.AppData }); const output=await JSON.parse(contents); console.log("File read successfully:", output);