ULTRACOMBOS-DEV 5 months ago
parent ba2f167e91
commit a1f20d0121
  1. 9
      vite/src/comps/timer.jsx
  2. 79
      vite/src/comps/voiceanalysis.jsx
  3. 3
      vite/src/pages/conversation.jsx
  4. 66
      vite/src/pages/flow.jsx
  5. 2
      vite/src/util/osc.js
  6. 56
      vite/src/util/system_prompt.js
  7. 14
      vite/src/util/useData.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(()=>{ useEffect(()=>{
@ -54,6 +62,7 @@ export const Countdown=forwardRef(({time, callback, auto, ...props}, ref)=>{
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
restart, restart,
stop,
})); }));
return ( return (

@ -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 (
<div className="voice-analysis border flex flex-col justify-end">
{/* <p>Volume: {volume}</p> */}
<div className="w-full bg-amber-600" style={{ height: `${volume/127*100}%` }}></div>
</div>
);
}

@ -114,6 +114,7 @@ export function Conversation() {
console.error("Error starting speech recognition:", error); console.error("Error starting speech recognition:", error);
}); });
}else{ }else{
SpeechRecognition.stopListening(); SpeechRecognition.stopListening();
} }
@ -269,7 +270,7 @@ export function Conversation() {
console.log('Final Transcript:', finalTranscript); console.log('Final Transcript:', finalTranscript);
if(processing) return; // Prevent submission if already processing if(processing) return; // Prevent submission if already processing
// return;
// Submit the final transcript // Submit the final transcript
onSubmit({ onSubmit({

@ -9,6 +9,8 @@ import { saveHistory } from "../util/output";
import NumPad from "../comps/numpad"; import NumPad from "../comps/numpad";
import { Light } from "../comps/light"; import { Light } from "../comps/light";
import { useData } from "../util/useData"; import { useData } from "../util/useData";
import VoiceAnalysis from "../comps/voiceanalysis";
import { sendOsc } from "../util/osc";
const EmojiType={ const EmojiType={
@ -26,7 +28,8 @@ export function Flow(){
const [cuelist, setCuelist] = useState([]); const [cuelist, setCuelist] = useState([]);
const [currentCue, setCurrentCue] = useState(null); const [currentCue, setCurrentCue] = useState(null);
const [chatWelcome, setChatWelcome] = 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 [userId, setUserId] = useState();
const refTimer=useRef(); const refTimer=useRef();
@ -68,7 +71,7 @@ export function Flow(){
refAudio.current = audio; // Store the new audio reference refAudio.current = audio; // Store the new audio reference
audio.addEventListener("loadedmetadata", () => { audio.addEventListener("loadedmetadata", () => {
refTimer.current.restart(audio.duration*1000 || 0); refTimer.current?.restart(audio.duration*1000 || 0);
}); });
} }
function playCue(cue) { function playCue(cue) {
@ -119,6 +122,7 @@ export function Flow(){
function onCueEnd() { function onCueEnd() {
refTimer.current?.stop(); // Stop the timer when cue ends
if(!refCurrentCue.current) return; if(!refCurrentCue.current) return;
const cue= refCurrentCue.current; // Get the current cue from ref 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=='start_conversation') refLight.current.fadeOut(); // Fade in light for conversation start
if(cue.callback=='summary') refLight.current.fadeIn(); // Fade out light for conversation end if(cue.callback=='summary') refLight.current.fadeIn(); // Fade out light for conversation end
resetTranscript(); // Reset transcript after cue ends
if(cue.auto) { if(cue.auto) {
playCue(cuelist.find(c => c.id === cue.nextcue)); playCue(cuelist.find(c => c.id === cue.nextcue));
@ -156,6 +161,9 @@ export function Flow(){
console.log('Numpad input:', mess); console.log('Numpad input:', mess);
setUserId(()=>mess); setUserId(()=>mess);
} }
function saveImage(){
sendOsc('/export', 'output/test.png'); // Send OSC message to save image
}
useEffect(()=>{ useEffect(()=>{
@ -166,6 +174,20 @@ export function Flow(){
},[userId]); },[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(()=>{ useEffect(()=>{
if(audioInput && isMicrophoneAvailable) { if(audioInput && isMicrophoneAvailable) {
@ -175,6 +197,32 @@ export function Flow(){
console.error("Error starting speech recognition:", error); 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{ }else{
console.log('Stopping speech recognition...'); console.log('Stopping speech recognition...');
SpeechRecognition.stopListening(); SpeechRecognition.stopListening();
@ -186,7 +234,8 @@ export function Flow(){
useEffect(()=>{ useEffect(()=>{
// if(listening){ // if(listening){
if(currentCue?.type=='user_input') refInput.current.value = transcript; if(currentCue?.type=='user_input')
refInput.current.value = transcript;
// } // }
},[transcript]); },[transcript]);
@ -221,7 +270,7 @@ export function Flow(){
// } // }
// } // }
if(refCurrentCue.current.callback=='summary'){ if(refCurrentCue.current?.callback=='summary'){
// get summary // get summary
console.log('Getting summary...'); console.log('Getting summary...');
getSummary(history.map(el=>`${el.role}:${el.content}`).join('\n'), data).then(summary => { getSummary(history.map(el=>`${el.role}:${el.content}`).join('\n'), data).then(summary => {
@ -267,8 +316,10 @@ export function Flow(){
<button className="!bg-yellow-300" onClick={()=>{ <button className="!bg-yellow-300" onClick={()=>{
saveHistory(history); saveHistory(history);
}}>Save Log</button> }}>Save Log</button>
<button onClick={saveImage}>Save image</button>
<NumPad onSend={onNumpad} /> <NumPad onSend={onNumpad} />
<Light ref={refLight} /> <Light ref={refLight} />
<VoiceAnalysis/>
</div> </div>
<div className=" max-h-[33vh] overflow-y-auto"> <div className=" max-h-[33vh] overflow-y-auto">
<table className="border-collapse **:border-y w-full **:p-2"> <table className="border-collapse **:border-y w-full **:p-2">
@ -315,7 +366,7 @@ export function Flow(){
<textarea ref={refInput} name="message" rows={2} <textarea ref={refInput} name="message" rows={2}
className={`w-full border-1 resize-none p-2 ${currentCue?.type!='user_input'? 'bg-gray-500':''}`} className={`w-full border-1 resize-none p-2 ${currentCue?.type!='user_input'? 'bg-gray-500':''}`}
disabled={currentCue?.type!='user_input'}></textarea> disabled={currentCue?.type!='user_input'}></textarea>
<div className="flex flex-row justify-end gap-2"> <div className="flex flex-row justify-end gap-2 flex-wrap">
<span className="flex flex-row gap-1"> <span className="flex flex-row gap-1">
<label>audio_output</label> <label>audio_output</label>
<input type='checkbox' checked={audioOutput} onChange={(e) => setAudioOutput(e.target.checked)} /> <input type='checkbox' checked={audioOutput} onChange={(e) => setAudioOutput(e.target.checked)} />
@ -324,6 +375,11 @@ export function Flow(){
<label>audio_input</label> <label>audio_input</label>
<input type='checkbox' checked={audioInput} onChange={(e) => setAudioInput(e.target.checked)} /> <input type='checkbox' checked={audioInput} onChange={(e) => setAudioInput(e.target.checked)} />
</span> </span>
<span className="flex flex-row gap-1">
<label>auto_send</label>
<input type='checkbox' checked={autoSend} onChange={(e) => setAutoSend(e.target.checked)} />
</span>
<button onClick={resetTranscript}>reset transcript</button>
<div className="rounded-2xl bg-gray-300 self-end px-4 tracking-widest">chat_status= {status}</div> <div className="rounded-2xl bg-gray-300 self-end px-4 tracking-widest">chat_status= {status}</div>
</div> </div>

@ -6,7 +6,7 @@ export async function sendOsc(key, message){
console.warn('sendOsc: message is empty, skipping'); console.warn('sendOsc: message is empty, skipping');
return; return;
} }
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,
message: message, message: message,

@ -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 welcome_prompt="請開始引導使用者回想一段內心的遺憾或未竟之事。";
// export const first_prompt=[ // export const first_prompt=[

@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useState } from "react"; 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(); const dataContext=createContext();
@ -14,7 +14,15 @@ export function DataProvider({children}) {
async function read(){ async function read(){
try{ 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); const output=await JSON.parse(contents);
console.log("File read successfully:", output); console.log("File read successfully:", output);

Loading…
Cancel
Save