main
reng 4 months ago
parent 1dd02a36ca
commit 6b263c35c1
  1. BIN
      vite/public/assets/ai/ai-01-1.mp3
  2. BIN
      vite/public/assets/ai/ai-01-2.mp3
  3. BIN
      vite/public/assets/ai/sfx-03-ai-03.mp3
  4. BIN
      vite/public/assets/ai/sfx-06-ai-04-record-03-ai-05-sfx-07.mp3
  5. BIN
      vite/public/assets/ai/sfx-08-record-04-ai-06.mp3
  6. BIN
      vite/public/assets/record/record-05.mp3
  7. 87
      vite/public/cuelist_demo2.json
  8. 4
      vite/src/comps/numpad.jsx
  9. 124
      vite/src/pages/flow_free.jsx
  10. 6
      vite/src/util/osc.js
  11. 37
      vite/src/util/useUser.jsx

Binary file not shown.

Binary file not shown.

@ -33,22 +33,21 @@
"name": "Q4", "name": "Q4",
"type": "phone", "type": "phone",
"description": "引導撥號", "description": "引導撥號",
"auto": false, "auto": true,
"audioFile": "assets/ai/ai-01.mp3", "audioFile": "assets/ai/ai-01-1.mp3",
"nextcue": 4.01, "nextcue": 4.01
"callback":"numpad",
"numpad_type":"userid",
"hint":"輸入兩位數號碼\n輸入完成送出,請按井字\n取消輸入,請按米字"
}, },
{ {
"id": 4.01, "id": 4.01,
"name": "Q4.01", "name": "Q4.01",
"type": "phone", "type": "phone",
"description": "撥接音效", "description": "撥",
"auto": true, "auto": false,
"audioFile": "assets/sfx/sfx-03.mp3", "audioFile": "assets/ai/ai-01-2.mp3",
"nextcue": 4.1, "nextcue": 4.1,
"hint":"輸入完成" "callback":"numpad",
"numpad_type":"userid",
"hint":"輸入兩位數號碼\n輸入完成送出,請按井字\n取消輸入,請按米字"
}, },
{ {
"id": 4.1, "id": 4.1,
@ -56,7 +55,7 @@
"type": "phone", "type": "phone",
"description": "輸入完成,請描述腦中的記憶畫面", "description": "輸入完成,請描述腦中的記憶畫面",
"auto": true, "auto": true,
"audioFile": "assets/ai/ai-03.mp3", "audioFile": "assets/ai/sfx-03-ai-03.mp3",
"nextcue": 4.11 "nextcue": 4.11
}, },
{ {
@ -76,55 +75,19 @@
"type": "chat", "type": "chat",
"description": "chat", "description": "chat",
"auto": true, "auto": true,
"nextcue": 4.21, "nextcue": 4.3,
"duration": 90, "duration": 20,
"status":"go" "status":"go"
}, },
{
"id": 4.21,
"name": "Q4.21",
"type": "phone",
"description": "裝置完成音效",
"auto": true,
"audioFile": "assets/sfx/sfx-06.mp3",
"nextcue": 4.3
},
{ {
"id": 4.3, "id": 4.3,
"name": "Q4.3", "name": "Q4.3",
"type": "phone", "type": "phone",
"description": "記憶提取完成", "description": "記憶提取完成",
"auto": true, "auto": true,
"audioFile": "assets/ai/ai-04.mp3", "audioFile": "assets/ai/sfx-06-ai-04-record-03-ai-05-sfx-07.mp3",
"nextcue": 4.4
},
{
"id": 4.4,
"name": "Q4.4",
"type": "phone",
"description": "準備打電話",
"audioFile": "assets/record/record-03.mp3",
"auto": true,
"nextcue": 4.5
},
{
"id": 4.5,
"name": "Q4.5",
"type": "phone",
"description": "電話將自動接通",
"audioFile": "assets/ai/ai-05.mp3",
"auto": true,
"nextcue": 4.51
},
{
"id": 4.51,
"name": "Q4.51",
"type": "phone",
"description": "接通音效",
"audioFile": "assets/sfx/sfx-07.mp3",
"auto": true,
"nextcue": 5.1, "nextcue": 5.1,
"hint":"電話已接通,請留言" "hint":"電話已接通"
}, },
{ {
"id": 5.1, "id": 5.1,
@ -133,33 +96,15 @@
"description": "call", "description": "call",
"duration": 30, "duration": 30,
"auto": true, "auto": true,
"nextcue": 5.11
},
{
"id": 5.11,
"name": "Q5.11",
"type": "summary",
"description": "結束逼",
"auto": true,
"audioFile": "assets/sfx/sfx-08.mp3",
"nextcue": 5.2, "nextcue": 5.2,
"hint":"通話結束" "callback":"summary"
}, },
{ {
"id": 5.2, "id": 5.2,
"name": "Q5.2", "name": "Q5.2",
"type": "phone", "type": "phone",
"description": "保留刪除",
"auto": true,
"nextcue": 5.3,
"audioFile": "assets/record/record-04.mp3"
},
{
"id": 5.3,
"name": "Q5.3",
"type": "phone",
"description": "保留刪除操作說明", "description": "保留刪除操作說明",
"audioFile": "assets/ai/ai-06.mp3", "audioFile": "assets/ai/sfx-08-record-04-ai-06.mp3",
"callback":"numpad", "callback":"numpad",
"numpad_type":"choice", "numpad_type":"choice",
"auto": false, "auto": false,

@ -62,8 +62,8 @@ export default function NumPad({onSend, disabled, type}){
} }
break; break;
case NUMPAD_TYPE.PASSWORD: case NUMPAD_TYPE.PASSWORD:
if(num>=0 && num<=999){ if(refInput.current.length==3){
onSend(num.toString().padStart(3, '0')); onSend(refInput.current);
refAudio.current[KEY_ENTER]?.play(); refAudio.current[KEY_ENTER]?.play();
}else{ }else{
refAudio.current['error']?.play(); refAudio.current['error']?.play();

@ -33,6 +33,7 @@ const ChatStatus={
Clear: 'clear', Clear: 'clear',
End: 'end', End: 'end',
Playing: 'playing', Playing: 'playing',
Message: 'message',
} }
const Voice={ const Voice={
@ -50,10 +51,12 @@ export function FreeFlow(){
const [audioInput, setAudioInput] = useState(true); const [audioInput, setAudioInput] = useState(true);
const [autoSend, setAutoSend] = useState(true); const [autoSend, setAutoSend] = useState(true);
const [chatStatus, setChatStatus] = useState(ChatStatus.System); // System, User, Processing const [chatStatus, setChatStatus] = useState(ChatStatus.System); // System, User, Processing
const { userId, setUserId, getFileId, setPassword, reset:resetUser, uploadHistory, setSummary, summary,setChoice } = useUser(); const { userId, setUserId, getFileId, setPassword, reset:resetUser, uploadHistory, setSummary, summary,setChoice, getUploadFolder,getDataId } = useUser();
const refTimer=useRef(); const refTimer=useRef();
const refAudio=useRef(); const refAudio=useRef();
const refAudioPrompt=useRef();
const refInput=useRef(); const refInput=useRef();
const refLight=useRef(); const refLight=useRef();
@ -91,12 +94,12 @@ export function FreeFlow(){
//TODO: if cue end, don't play audio //TODO: if cue end, don't play audio
if(refCurrentCue.current?.type=='chat'){ if(refCurrentCue.current?.type=='chat'){
if(refChatCueEnd.current) { // if(refChatCueEnd.current) {
console.log('Chat cue has ended, not playing audio:', url); // console.log('Chat cue has ended, not playing audio:', url);
setChatStatus(ChatStatus.Clear); // Reset chat status to Clear // setChatStatus(ChatStatus.Clear); // Reset chat status to Clear
onCueEnd(); // onCueEnd();
return; // return;
} // }
// if audio time larger than cue remaining time, don't play audio // if audio time larger than cue remaining time, don't play audio
} }
@ -152,12 +155,17 @@ export function FreeFlow(){
updatePrompt(prompt); updatePrompt(prompt);
sendOsc(OSC_ADDRESS.PROMPT, prompt); sendOsc(OSC_ADDRESS.PROMPT, prompt);
} // play audio for prompt
refAudioPrompt.current?.play().catch(error => {
console.error("Audio prompt playback error:", error);
});
if(refChatCueEnd.current){
console.log('Talking ended, ending current cue');
onCueEnd(); // End the current cue if chat cue has ended
} }
// if(refChatCueEnd.current){
// console.log('Talking ended, ending current cue');
// onCueEnd(); // End the current cue if chat cue has ended
// }
} }
} }
@ -186,6 +194,9 @@ export function FreeFlow(){
} }
// clear unity hint
sendOsc(OSC_ADDRESS.HINT, ''); // Clear hint message
sendOsc(OSC_ADDRESS.INPUT, ''); // Clear input message
switch(cue.type){ switch(cue.type){
case 'chat': case 'chat':
@ -204,31 +215,30 @@ export function FreeFlow(){
setChatWelcome(false); setChatWelcome(false);
setChatStatus(ChatStatus.Clear); setChatStatus(ChatStatus.Clear);
break; break;
case 'summary': // case 'summary':
console.log('Getting summary...'); // console.log('Getting summary...');
setChatStatus(ChatStatus.Clear); // Set chat status to Processing
getSummary(history.map(el=>`${el.role}:${el.content}`).join('\n'), data).then(summary_ => {
console.log('Summary:', summary_); // setChatStatus(ChatStatus.Clear); // Set chat status to Processing
onCueEnd(); // End the current cue after getting summary // getSummary(history.map(el=>`${el.role}:${el.content}`).join('\n'), data).then(summary_ => {
setSummary(summary_?.result); // console.log('Summary:', summary_);
refContainer.current.scrollTop = refContainer.current.scrollHeight; // Scroll to bottom // onCueEnd(); // End the current cue after getting summary
// sendOsc(OSC_ADDRESS.SUMMARY, summary_?.result); // setSummary(summary_?.result);
// sendOsc(OSC_ADDRESS.EXPORT, `${getFileId()}#${summary_?.result}#${password}`); // Save summary with file ID // refContainer.current.scrollTop = refContainer.current.scrollHeight; // Scroll to bottom
}).catch(error => { // }).catch(error => {
console.error('Error getting summary:', error); // console.error('Error getting summary:', error);
}); // });
break; // break;
case 'user_input': case 'user_input':
console.log('User input cue, setting chat status to User'); setChatStatus(ChatStatus.Message); // Set chat status to User
setChatStatus(ChatStatus.Clear); // Set chat status to User for user input cues
resetTranscript(); // Reset transcript for user input resetTranscript(); // Reset transcript for user input
break; break;
default:
setChatStatus(ChatStatus.Clear);
break;
} }
@ -275,17 +285,19 @@ export function FreeFlow(){
const cue= refCurrentCue.current; // Get the current cue from ref const cue= refCurrentCue.current; // Get the current cue from ref
if(cue.type=='chat'){ if(cue.type=='chat'){
if(chatStatus==ChatStatus.System) { // if(chatStatus==ChatStatus.System) {
console.log('Still talking...'); // console.log('Still talking...');
refChatCueEnd.current=true; // refChatCueEnd.current=true;
return; // return;
} // }
console.log('save chat history:', history);
uploadHistory(history); // Save chat history when cue ends uploadHistory(history); // Save chat history when cue ends
} }
if(cue.callback==OSC_ADDRESS.DISCARD) { if(cue.callback==OSC_ADDRESS.DISCARD) {
sendOsc(OSC_ADDRESS.CHOICE, OSC_ADDRESS.DISCARD); // Send OSC discard message sendOsc(OSC_ADDRESS.CHOICE, OSC_ADDRESS.DISCARD); // Send OSC discard message
setPassword(); setPassword();
sendOsc(OSC_ADDRESS.EXPORT, `${getUploadFolder()}#${getFileId()}#${summary}#${mess}`);
} }
if(cue.hint!=null){ if(cue.hint!=null){
@ -294,6 +306,22 @@ export function FreeFlow(){
sendOsc(OSC_ADDRESS.HINT, ''); // Clear hint message sendOsc(OSC_ADDRESS.HINT, ''); // Clear hint message
} }
if(cue.callback=='summary') {
console.log('Getting summary...');
getSummary(history.map(el=>`${el.role}:${el.content}`).join('\n'), data).then(summary_ => {
console.log('Summary:', summary_);
onCueEnd(); // End the current cue after getting summary
setSummary(summary_?.result);
refContainer.current.scrollTop = refContainer.current.scrollHeight; // Scroll to bottom
}).catch(error => {
console.error('Error getting summary:', error);
});
}
refAudio.current?.pause(); // Pause any playing audio refAudio.current?.pause(); // Pause any playing audio
console.log('onCueEnd:', cue.id); console.log('onCueEnd:', cue.id);
@ -339,7 +367,7 @@ export function FreeFlow(){
case NUMPAD_TYPE.PASSWORD: case NUMPAD_TYPE.PASSWORD:
setPassword(()=>mess); setPassword(()=>mess);
// sendOsc(OSC_ADDRESS.PASSWORD, mess); // Send OSC password message // sendOsc(OSC_ADDRESS.PASSWORD, mess); // Send OSC password message
sendOsc(OSC_ADDRESS.EXPORT, `${getFileId()}#${summary}#${mess}`); sendOsc(OSC_ADDRESS.EXPORT, `${getUploadFolder()}#${getDataId()}#${summary}#${getFileId(mess)}`);
sendOsc(OSC_ADDRESS.CHOICE, OSC_ADDRESS.SAVE); // Send OSC save choice message sendOsc(OSC_ADDRESS.CHOICE, OSC_ADDRESS.SAVE); // Send OSC save choice message
break; break;
} }
@ -474,7 +502,27 @@ export function FreeFlow(){
useEffect(()=>{ useEffect(()=>{
resetTranscript(); resetTranscript();
sendOsc(OSC_ADDRESS.INPUT, chatStatus);
let text='';
switch(chatStatus) {
case ChatStatus.System:
text = '等我一下\n換我說囉';
break;
case ChatStatus.User:
text = '換你說了';
break;
case ChatStatus.Processing:
text = '記憶讀取中';
break;
case ChatStatus.Message:
text = '請留言';
break;
case ChatStatus.Clear:
default:
text = '';
break;
}
sendOsc(OSC_ADDRESS.INPUT, text);
},[chatStatus]); },[chatStatus]);
@ -506,6 +554,9 @@ export function FreeFlow(){
console.error('Error fetching cuelist:', error); console.error('Error fetching cuelist:', error);
}); });
refAudioPrompt.current = new Audio('assets/sfx/sfx-05.mp3'); // Load audio prompt if available
},[]); },[]);
@ -591,7 +642,7 @@ export function FreeFlow(){
</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`}
disabled={chatStatus!=ChatStatus.User}></textarea> disabled={chatStatus!=ChatStatus.User && chatStatus!=ChatStatus.Message}></textarea>
<div className="flex flex-row justify-end gap-2 flex-wrap"> <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>
@ -606,6 +657,7 @@ export function FreeFlow(){
<input type='checkbox' checked={autoSend} onChange={(e) => setAutoSend(e.target.checked)} /> <input type='checkbox' checked={autoSend} onChange={(e) => setAutoSend(e.target.checked)} />
</span> </span>
<button onClick={resetTranscript}>reset transcript</button> <button onClick={resetTranscript}>reset transcript</button>
<button onClick={sendMessage} disabled={chatStatus!=ChatStatus.User && chatStatus!=ChatStatus.Message}>Send</button>
<div className="rounded-2xl bg-gray-300 self-end px-4 tracking-widest">api_status= {status}</div> <div className="rounded-2xl bg-gray-300 self-end px-4 tracking-widest">api_status= {status}</div>
<div className="rounded-2xl bg-gray-300 self-end px-4 tracking-widest">chat_status= {chatStatus}</div> <div className="rounded-2xl bg-gray-300 self-end px-4 tracking-widest">chat_status= {chatStatus}</div>

@ -25,9 +25,9 @@ export async function sendOsc(key, message){
console.warn('sendOsc: message is empty, skipping'); console.warn('sendOsc: message is empty, skipping');
return; return;
} }
if(key!=OSC_ADDRESS.HINT && message === '') { // if(key!=OSC_ADDRESS.HINT && message === '') {
return; // return;
} // }
try{ try{
console.log(`Sending OSC message: ${key} -> ${message}`); console.log(`Sending OSC message: ${key} -> ${message}`);

@ -4,7 +4,7 @@ import { updateUser } from "./backend";
const userContext=createContext(); const userContext=createContext();
const SesstionTime={ const SessionTime={
A:["12:00", "13:30"], A:["12:00", "13:30"],
B:["13:30", "15:00"], B:["13:30", "15:00"],
C:["15:00", "17:30"], C:["15:00", "17:30"],
@ -23,14 +23,25 @@ export function UserProvider({children}) {
const [summary, setSummary] = useState(null); const [summary, setSummary] = useState(null);
function getFileId(){ function getFileId(pass){
if(!userId) return `test-${moment().format("YYYYMMDDmmss")}-${sessionId}`; if(!userId) return `${password||moment().format('hhmmss')}_testuser`;
return `${moment().format("YYYYMMDD").substring(2)}-${sessionId}-${userId}`; return `${password||pass||''}_${userId}`;
} }
function getDataId(){
if(!userId) return `${moment().format('YYYYMM')}/${moment().format("MMDD")}/${sessionId==null? SessionTime[sessionId][0]:'no-sesion'}/testuser_${moment().format('hhmmss')}`;
return `${moment().format('YYYYMM')}/${moment().format("MMDD")}/${sessionId==null? SessionTime[sessionId][0]:'no-sesion'}/${userId}`;
}
function getUploadFolder(){
return `${moment().format("YYYYMM")}/${moment().format("MMDD")}/${sessionId==null? SessionTime[sessionId][0]:'no-session'}`;
}
function reset(){ function reset(){
setUserId(null); setUserId(null);
setSessionId(null); setSessionId(null);
setPassword(null); setPassword(null);
setChoice(null);
setSummary(null);
} }
function saveHistory(history) { function saveHistory(history) {
@ -39,7 +50,7 @@ export function UserProvider({children}) {
const data={ const data={
history, history,
} }
updateUser(getFileId(), data) updateUser('user/'+getDataId(), data)
.then(() => { .then(() => {
console.log("History saved successfully"); console.log("History saved successfully");
}) })
@ -55,17 +66,17 @@ export function UserProvider({children}) {
const data={ const data={
userId, userId,
sessionId, // sessionId,
password, password,
choice, choice,
summary, summary,
date: moment().format("YYYY-MM-DD"), date: moment().format("YYYY-MM-DD hh:mm:ss"),
fileId: getFileId(), fileId: getFileId(),
}; };
console.log("Updating user data:", data); console.log("Updating user data:", data);
updateUser(getFileId(), data) updateUser('user/'+getDataId(), data)
.then(() => { .then(() => {
console.log("User data updated successfully"); console.log("User data updated successfully");
}) })
@ -73,13 +84,13 @@ export function UserProvider({children}) {
console.error("Error updating user data:", error); console.error("Error updating user data:", error);
}); });
},[password, sessionId, choice, summary]); },[password, choice, summary]);
useEffect(() => { useEffect(() => {
let symbol=''; let symbol='';
for(const [key, value] of Object.entries(SesstionTime)){ for(const [key, value] of Object.entries(SessionTime)){
const start=moment(value[0], "HH:mm"); const start=moment(value[0], "HH:mm");
const end=moment(value[1], "HH:mm"); const end=moment(value[1], "HH:mm");
@ -95,11 +106,11 @@ export function UserProvider({children}) {
return ( return (
<userContext.Provider value={{ <userContext.Provider value={{
userId, setUserId, getFileId, userId, setUserId, getFileId, getUploadFolder,
reset, uploadHistory: saveHistory, setChoice, reset, uploadHistory: saveHistory, setChoice,
setSummary, summary, setSummary, summary, getDataId,
setPassword}}> setPassword}}>
{children} {children}
</userContext.Provider> </userContext.Provider>
); );
} }

Loading…
Cancel
Save