main
reng 5 months ago
parent 2757230ef7
commit 7f4f065538
  1. 9
      vite/public/cuelist.json
  2. 9
      vite/public/cuelist_free.json
  3. 2
      vite/src-tauri/capabilities/default.json
  4. 1
      vite/src-tauri/src/lib.rs
  5. 20
      vite/src/comps/debug.jsx
  6. 4
      vite/src/comps/light.jsx
  7. 2
      vite/src/index.css
  8. 4
      vite/src/pages/conversation.jsx
  9. 6
      vite/src/pages/flow.jsx
  10. 22
      vite/src/pages/flow_free.jsx
  11. 13
      vite/src/util/chat.js
  12. 39
      vite/src/util/osc.js
  13. 6
      vite/src/util/system_prompt.js
  14. 4
      vite/src/util/tts.js
  15. 2
      vite/src/util/useChat.jsx

@ -6,7 +6,8 @@
"type": "space", "type": "space",
"description": "Annonce", "description": "Annonce",
"audioFile": "assets/q1.mp3", "audioFile": "assets/q1.mp3",
"loop": true "loop": true,
"status":"reset"
}, },
{ {
"id": 2, "id": 2,
@ -42,7 +43,8 @@
"description": "Guide to construct scene", "description": "Guide to construct scene",
"auto": true, "auto": true,
"audioFile": "assets/q4-1.mp3", "audioFile": "assets/q4-1.mp3",
"nextcue": 4.2 "nextcue": 4.2,
"status":"go"
}, },
{ {
"id": 4.2, "id": 4.2,
@ -136,7 +138,8 @@
"name": "Q6", "name": "Q6",
"type": "space", "type": "space",
"description": "Ending", "description": "Ending",
"audioFile": "assets/q6.mp3" "audioFile": "assets/q6.mp3",
"status":"end"
} }
] ]
} }

@ -6,7 +6,8 @@
"type": "space", "type": "space",
"description": "Annonce", "description": "Annonce",
"audioFile": "assets/q1.mp3", "audioFile": "assets/q1.mp3",
"loop": true "loop": true,
"status":"reset"
}, },
{ {
"id": 2, "id": 2,
@ -42,7 +43,8 @@
"description": "Guide to construct scene", "description": "Guide to construct scene",
"auto": true, "auto": true,
"audioFile": "assets/q4-1.mp3", "audioFile": "assets/q4-1.mp3",
"nextcue": 4.2 "nextcue": 4.2,
"status":"go"
}, },
{ {
"id": 4.2, "id": 4.2,
@ -84,7 +86,8 @@
"name": "Q6", "name": "Q6",
"type": "space", "type": "space",
"description": "Ending", "description": "Ending",
"audioFile": "assets/q6.mp3" "audioFile": "assets/q6.mp3",
"status":"end"
} }
] ]
} }

@ -17,6 +17,8 @@
"allow": [ "allow": [
{ {
"url": "https://*.openai.com" "url": "https://*.openai.com"
},{
"url":"http://localhost:34800"
} }
] ]
}, },

@ -50,6 +50,7 @@ async fn send_osc_message(
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
fn reset_permission(origin: &str, app: AppHandle) { fn reset_permission(origin: &str, app: AppHandle) {
let webview = app.get_webview_window("main").unwrap(); let webview = app.get_webview_window("main").unwrap();

@ -0,0 +1,20 @@
import { sendOsc, OSC_ADDRESS, updatePrompt } from "../util/osc"
const TEST_PROMPT='a hazy memory of a {{ Scene }}, seen through soft atmospheric blur, distant silhouettes and faded contours, pastel light and cinematic haze, (analog film texture), (shallow depth of field:1.3), shallow depth of field, memory fragment effect, light leak, subtle grain, chromatic aberration, surreal glow, in muted warm tones, cinematic framing,';
export function DebugControl(){
return (
<div className="flex flex-row gap-2 [&>button]:rounded-full [&>button]:bg-white bg-gray-200 p-2 w-full justify-center">
<button onClick={() => sendOsc(OSC_ADDRESS.STATUS, 'reset')}>reset</button>
<button onClick={() => sendOsc(OSC_ADDRESS.STATUS, 'go')}>go</button>
<button onClick={() => sendOsc(OSC_ADDRESS.STATUS, 'end')}>end</button>
<div className="flex flex-col gap-1">
<button className="btn btn-success" onClick={() => updatePrompt('Random dogs')}>Test Prompt</button>
<button className="btn btn-success" onClick={() => updatePrompt(TEST_PROMPT)}>Test Prompt</button>
</div>
</div>
)
}

@ -1,6 +1,6 @@
import gsap from "gsap" import gsap from "gsap"
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react" import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"
import { sendOsc } from "../util/osc"; import { OSC_ADDRESS, sendOsc } from "../util/osc";
const FADE_TIME=3; const FADE_TIME=3;
@ -18,7 +18,7 @@ export const Light=forwardRef((props, ref)=>{
duration: time, duration: time,
onUpdate: () => { onUpdate: () => {
// console.log(refVal.current.val); // console.log(refVal.current.val);
sendOsc('/light', refVal.current.val.toString()); sendOsc(OSC_ADDRESS.LIGHT, refVal.current.val.toString());
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
}, },

@ -3,7 +3,7 @@
@layer base{ @layer base{
button{ button{
@apply rounded-full border-4 px-2 !bg-orange-200 cursor-pointer font-bold; @apply rounded-full border-2 px-2 bg-orange-200 cursor-pointer font-bold;
} }
} }

@ -75,7 +75,7 @@ export function Conversation() {
}else{ }else{
console.log('create speech:', data.output_text); console.log('create speech:', data.output_text);
textToSpeech(data.output_text, params?.voice_prompt).then(audioUrl => { textToSpeech(data.output_text, params?.voice, params?.voice_prompt).then(audioUrl => {
const audio = new Audio(audioUrl); const audio = new Audio(audioUrl);
console.log('play audio...', new Date(Date.now()-startTime).toISOString().slice(11, 19)); console.log('play audio...', new Date(Date.now()-startTime).toISOString().slice(11, 19));
@ -183,7 +183,7 @@ export function Conversation() {
}else{ }else{
// tts // tts
console.log('create speech:', data.output_text); console.log('create speech:', data.output_text);
textToSpeech(data.output_text, params?.voice_prompt).then(audioUrl => { textToSpeech(data.output_text, params?.voice, params?.voice_prompt).then(audioUrl => {
const audio = new Audio(audioUrl); const audio = new Audio(audioUrl);
console.log('play audio...', new Date(Date.now()-startTime).toISOString().slice(11, 19)); console.log('play audio...', new Date(Date.now()-startTime).toISOString().slice(11, 19));

@ -10,7 +10,7 @@ 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 VoiceAnalysis from "../comps/voiceanalysis";
import { sendOsc } from "../util/osc"; import { sendOsc, OSC_ADDRESS } from "../util/osc";
const EmojiType={ const EmojiType={
@ -108,6 +108,9 @@ export function Flow(){
} }
} }
if(cue.status){
sendOsc(OSC_ADDRESS.STATUS, cue.status); // Send OSC status message
}
if(cue.audioFile){ if(cue.audioFile){
playAudio(cue.audioFile); playAudio(cue.audioFile);
@ -177,6 +180,7 @@ export function Flow(){
function onSpeechEnd(){ function onSpeechEnd(){
console.log('onSpeechEnd:', finalTranscript); console.log('onSpeechEnd:', finalTranscript);
if(currentCue?.type!='user_input') return; // Only process if current cue is user input if(currentCue?.type!='user_input') return; // Only process if current cue is user input
if(autoSend && transcript.trim().length > 0) { if(autoSend && transcript.trim().length > 0) {
console.log('Auto sending transcript:', transcript); console.log('Auto sending transcript:', transcript);
onCueEnd(); onCueEnd();

@ -10,8 +10,8 @@ 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 VoiceAnalysis from "../comps/voiceanalysis";
import { sendOsc } from "../util/osc"; import { sendOsc, OSC_ADDRESS } from "../util/osc";
import { set } from "zod/v4"; import { DebugControl } from "../comps/debug";
const EmojiType={ const EmojiType={
@ -138,6 +138,8 @@ export function FreeFlow(){
} }
if(cue.audioFile){ if(cue.audioFile){
playAudio(cue.audioFile); playAudio(cue.audioFile);
} }
@ -147,6 +149,17 @@ export function FreeFlow(){
onCueEnd(cue); onCueEnd(cue);
}); });
} }
// control unity
if(cue.status){
sendOsc(OSC_ADDRESS.STATUS, cue.status); // Send OSC status message
}
if(cue.type=='chat'){
sendOsc(OSC_ADDRESS.COUNTDOWN, cue.duration || 0); // Send OSC countdown message
}else{
sendOsc(OSC_ADDRESS.COUNTDOWN, 0); // Reset countdown for non-chat cues
}
} }
function onCueEnd() { function onCueEnd() {
@ -272,6 +285,7 @@ export function FreeFlow(){
useEffect(()=>{ useEffect(()=>{
resetTranscript(); resetTranscript();
sendOsc(OSC_ADDRESS.INPUT, chatStatus);
},[chatStatus]); },[chatStatus]);
@ -308,7 +322,7 @@ export function FreeFlow(){
return ( return (
<main className="items-center"> <main className="items-center">
<DebugControl/>
<div className="w-full p-2 flex flex-row justify-center gap-2 *:w-[12vw] *:h-[12vw]"> <div className="w-full p-2 flex flex-row justify-center gap-2 *:w-[12vw] *:h-[12vw]">
<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}
@ -364,7 +378,7 @@ export function FreeFlow(){
{msg.prompt && <div className="text-xs bg-gray-200">{msg.prompt}</div>} {msg.prompt && <div className="text-xs bg-gray-200">{msg.prompt}</div>}
</div> </div>
))} ))}
{summary && <div className="w-full self-center bg-blue-200 px-2">{summary}</div>} {summary && <div className="w-full self-center bg-blue-200 px-2">{JSON.stringify(summary)}</div>}
</div> </div>
<textarea ref={refInput} name="message" rows={2} <textarea ref={refInput} name="message" rows={2}
className={`w-full border-1 resize-none p-2 disabled:bg-gray-500`} className={`w-full border-1 resize-none p-2 disabled:bg-gray-500`}

@ -1,6 +1,6 @@
import { fetch } from '@tauri-apps/plugin-http'; import { fetch } from '@tauri-apps/plugin-http';
import { params } from './system_prompt'; import { params } from './system_prompt';
import { sendOsc } from './osc'; import { sendOsc, OSC_ADDRESS, updatePrompt } from './osc';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useData } from './useData'; import { useData } from './useData';
@ -97,9 +97,12 @@ export async function sendChatMessage(messages, data, isLastMessage = false) {
// console.log("Generated response:", choice.message); // console.log("Generated response:", choice.message);
const result=JSON.parse(choice.message.content); const result=JSON.parse(choice.message.content);
// send to tauri // send to python & unity
await sendOsc('/prompt', result.prompt?.replaceAll('"', '')); const prompt = result.prompt?.replaceAll('"', '');
await sendOsc('/output_text', result.output_text?.replaceAll('"', '')); await updatePrompt(prompt);
await sendOsc(OSC_ADDRESS.PROMPT, prompt);
// TODO: send to python
return { return {
@ -153,7 +156,7 @@ export async function getSummary(messages, data) {
const result=choice.message.content; const result=choice.message.content;
// send to tauri // send to tauri
await sendOsc('/summary', result); await sendOsc(OSC_ADDRESS.SUMMARY, result);
return { return {

@ -1,4 +1,18 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { fetch } from '@tauri-apps/plugin-http';
export const OSC_ADDRESS={
LIGHT: '/light',
STATUS: '/status',
INPUT: '/input',
COUNTDOWN: '/countdown',
VOLUME: '/volume',
SCRIPT: '/script',
SUMMARY: '/summary',
PROMPT: '/prompt',
}
export async function sendOsc(key, message){ export async function sendOsc(key, message){
@ -6,11 +20,30 @@ 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.toString(),
host:`0.0.0.0:0`, host:`0.0.0.0:0`,
target: '127.0.0.1:8787', target: '127.0.0.1:9000',
});
}
// send to python fastapi
export async function updatePrompt(prompt) {
console.log(`Updating prompt: ${prompt}`);
try{
await fetch('http://localhost:34800/api/update/prompt', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ prompt })
}); });
}catch(error){
console.error('Error updating prompt:', error);
throw error;
}
} }

@ -6,8 +6,10 @@ export const params={
last_prompt:`請用一句話為這段對話簡短的收尾,並邀請使用者在 60 秒的時間內,說出對遺憾對象想說的話`, last_prompt:`請用一句話為這段對話簡短的收尾,並邀請使用者在 60 秒的時間內,說出對遺憾對象想說的話`,
welcome_prompt:`請開始引導使用者回想一段內心的遺憾或未竟之事。`, welcome_prompt:`請開始引導使用者回想一段內心的遺憾或未竟之事。`,
voice_prompt:`使用台灣語境的繁體中文,有同理心,柔和的口氣,不要有質問感,語速自然流暢,稍微慢。用冥想的方式,引導使用者回想記憶。`,
voice:"nova", voice_prompt:`Voice Affect: Low, hushed, and suspenseful; convey tension and intrigue.\n\nTone: Deeply serious and mysterious, maintaining an undercurrent of unease throughout.\n\nPacing: Slow, deliberate, pausing slightly after suspenseful moments to heighten drama.\n\nEmotion: Restrained yet intense—voice should subtly tremble or tighten at key suspenseful points.\n\nEmphasis: Highlight sensory descriptions (\"footsteps echoed,\" \"heart hammering,\" \"shadows melting into darkness\") to amplify atmosphere.\n\nPronunciation: Slightly elongated vowels and softened consonants for an eerie, haunting effect.\n\nPauses: Insert meaningful pauses after phrases like \"only shadows melting into darkness,\" and especially before the final line, to enhance suspense dramatically.`,
voice:"onyx",
summary_prompt:`幫我把以下一段話整理成一段文字,以第一人稱視角作為當事人的文字紀念,文字內容 50 字以內:`, summary_prompt:`幫我把以下一段話整理成一段文字,以第一人稱視角作為當事人的文字紀念,文字內容 50 字以內:`,
} }

@ -2,7 +2,7 @@ import { fetch } from '@tauri-apps/plugin-http';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
// import { voice_prompt } from './system_prompt'; // import { voice_prompt } from './system_prompt';
export async function textToSpeech(text, voice_prompt) { export async function textToSpeech(text, voice, voice_prompt) {
const token = await invoke('get_env', { name: 'OPENAI_API_KEY' }); const token = await invoke('get_env', { name: 'OPENAI_API_KEY' });
@ -15,7 +15,7 @@ export async function textToSpeech(text, voice_prompt) {
body: JSON.stringify({ body: JSON.stringify({
model: 'gpt-4o-mini-tts',//'tts-1', model: 'gpt-4o-mini-tts',//'tts-1',
input: text, input: text,
voice: "nova", voice: voice,
instructions: voice_prompt, instructions: voice_prompt,
response_format:'opus' response_format:'opus'
}), }),

@ -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_prompt).then(url => { textToSpeech(response.output_text, 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