add parameters

main
reng 5 months ago
parent 22b85ab570
commit ba2f167e91
  1. 10
      vite/public/default.json
  2. 1
      vite/src-tauri/capabilities/default.json
  3. 4
      vite/src/main.jsx
  4. 13
      vite/src/pages/conversation.jsx
  5. 9
      vite/src/pages/flow.jsx
  6. 40
      vite/src/pages/settings.jsx
  7. 26
      vite/src/util/chat.js
  8. 4
      vite/src/util/constant.js
  9. 6
      vite/src/util/osc.js
  10. 101
      vite/src/util/system_prompt.js
  11. 4
      vite/src/util/tts.js
  12. 6
      vite/src/util/useChat.jsx
  13. 79
      vite/src/util/useData.jsx

@ -0,0 +1,10 @@
{
"system_prompt":"你是一位具同理心與觀察力的中文語音引導者,陪伴使用者在限定時間內,一起還原一段重要卻未竟的記憶。你不預設劇情、不給答案,而是透過溫柔的語氣,持續聆聽、提出單句問話,引導使用者進入細節。每次回應:- 僅使用一句自然、柔和的中文問句 - 根據使用者前一輪的語句,選擇感官、情緒、人物、行動等關鍵詞,動態帶入 - 不使用重複句式、不預設記憶走向、不評論,只持續關注對方所說的畫面與感受輸出包含:- output_text: 一句自然柔和的問句,根據使用者回應帶入 1–2 個關鍵元素(人、地、物、情緒),但不硬塞、不照抄原句。- prompt: 具體、有情緒的英文描述,用於生成畫面與氛圍,避免抽象與詩意語言。你的目標不是得到完整答案,而是陪他慢慢走進這段記憶,直到時間結束。🌀 問句類型✅ 起點探索(找出記憶起源)- 那天你說是下雨,在這種天氣下,你通常會有什麼樣的心情?- 是什麼讓你突然想起這件事?🌿 場景深化(空間、感官)- 在你說的那條街上,聲音是不是特別清楚?還是很靜?- 那時風這麼冷、空氣又混濁,你有沒有想走開一點?👤 人物引出(動作、眼神)- 他經過時沒看你一眼,那瞬間你有什麼反應?- 他當時是走過來,還是站在原地等你?💭 情緒揭露(反應、掙扎)- 當你站在原地動不了,是害怕答案,還是不敢問?- 那個瞬間,你心裡有沒有閃過什麼話?🤐 話語未出(遺憾、沉默)- 如果現在你有機會說那句「為什麼不告訴我」,你會怎麼開口?- 那句『對不起』一直留在你心裡,如果現在能說出來,你會怎麼說?🪞 回望反思(現在的視角)- 現在想起來,你還會做出一樣的選擇嗎?- 你對當時的自己,有沒有什麼話想說?⏳ 結尾語(可用於結束階段)- 我們慢慢也走到這段回憶的盡頭了。- 也許有些話沒有說完,但你已經靠近它了。",
"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 字以內:",
"speech_idle_time":3000
}

@ -23,6 +23,7 @@
"fs:write-files", "fs:write-files",
"fs:allow-create", "fs:allow-create",
"fs:allow-appdata-write", "fs:allow-appdata-write",
"fs:allow-appdata-read",
"fs:allow-exists", "fs:allow-exists",
{ {
"identifier": "fs:scope", "identifier": "fs:scope",

@ -8,10 +8,11 @@ import { Settings } from './pages/settings.jsx';
import { Flow } from './pages/flow.jsx'; import { Flow } from './pages/flow.jsx';
import { Conversation } from './pages/conversation.jsx'; import { Conversation } from './pages/conversation.jsx';
import { ChatProvider } from './util/useChat.jsx'; import { ChatProvider } from './util/useChat.jsx';
import { DataProvider } from './util/useData.jsx';
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
<DataProvider>
<ChatProvider> <ChatProvider>
<BrowserRouter> <BrowserRouter>
<App /> <App />
@ -22,6 +23,7 @@ createRoot(document.getElementById('root')).render(
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</ChatProvider> </ChatProvider>
</DataProvider>
</StrictMode>, </StrictMode>,
) )

@ -7,6 +7,7 @@ import Input from '../comps/input';
import { sendChatMessage } from '../util/chat'; import { sendChatMessage } from '../util/chat';
import { textToSpeech } from '../util/tts'; import { textToSpeech } from '../util/tts';
import { useData } from '../util/useData';
gsap.registerPlugin(SplitText); gsap.registerPlugin(SplitText);
@ -14,6 +15,8 @@ gsap.registerPlugin(SplitText);
export function Conversation() { export function Conversation() {
const { data: params}=useData();
const [history, setHistory] = useState([]); const [history, setHistory] = useState([]);
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [showProcessing, setShowProcessing] = useState(false); const [showProcessing, setShowProcessing] = useState(false);
@ -46,7 +49,7 @@ export function Conversation() {
// create start message // create start message
const startTime=Date.now(); const startTime=Date.now();
setProcessing(true); setProcessing(true);
sendChatMessage([]).then(response => { sendChatMessage([], params).then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok'); throw new Error('Network response was not ok');
} }
@ -72,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).then(audioUrl => { textToSpeech(data.output_text, 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));
@ -145,7 +148,7 @@ export function Conversation() {
role:'user', role:'user',
content: input, content: input,
} }
]).then(response => { ], params).then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok'); throw new Error('Network response was not ok');
setProcessing(false); setProcessing(false);
@ -176,7 +179,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).then(audioUrl => { textToSpeech(data.output_text, 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));
@ -223,7 +226,7 @@ export function Conversation() {
if(!last_item) return; if(!last_item) return;
if(last_item.classList.contains('user')) return; if(last_item.classList.contains('user')) return;
console.log('last_item', last_item); // console.log('last_item', last_item);
let split=SplitText.create(last_item, { let split=SplitText.create(last_item, {
type: "chars", type: "chars",

@ -8,6 +8,7 @@ import { getSummary } from "../util/chat";
import { saveHistory } from "../util/output"; 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";
const EmojiType={ const EmojiType={
@ -20,6 +21,8 @@ const EmojiType={
export function Flow(){ export function Flow(){
const { data }=useData();
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);
@ -121,8 +124,8 @@ export function Flow(){
console.log('onCueEnd:', cue.id); console.log('onCueEnd:', cue.id);
if(cue.callback=='start_conversation') refLight.current.fadeIn(); // 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.fadeOut(); // Fade out light for conversation end if(cue.callback=='summary') refLight.current.fadeIn(); // Fade out light for conversation end
if(cue.auto) { if(cue.auto) {
@ -221,7 +224,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')).then(summary => { getSummary(history.map(el=>`${el.role}:${el.content}`).join('\n'), data).then(summary => {
console.log('Summary:', summary); console.log('Summary:', summary);

@ -1,9 +1,43 @@
import { useData } from '../util/useData.jsx';
export function Settings(){ export function Settings(){
const {data, read, write} = useData();
function onSubmit(e){
e.preventDefault();
const formData = new FormData(e.target);
const towrite = {};
formData.forEach((value, key) => {
towrite[key] = value;
});
console.log('Form submitted:', towrite);
write(towrite);
}
return ( return (
<div className="flex flex-col items-center justify-center h-full"> <div className="flex flex-col p-2 gap-4 overflow-y-auto min-h-full">
<h1 className="text-4xl font-bold mb-4">Settings</h1>
<p className="text-lg">This page is under construction.</p> <form className='flex flex-col gap-4 flex-1' onSubmit={onSubmit}>
<div className='flex flex-row gap-2 self-end'>
<button type="button" onClick={read}>read</button>
<button type="submit">write</button>
</div>
{data && Object.entries(data).map(([key, value], index) => (
<div key={index} className='flex flex-col gap-1 flex-1'>
<label className='bg-gray-200 self-start px-2'>{key}</label>
{key=="speech_idle_time" ? (
<input name={key} type='number' defaultValue={value} className='border'></input>
):(
<textarea name={key} defaultValue={value} className='border flex-1'></textarea>
)}
</div>
))}
</form>
</div> </div>
); );
} }

@ -1,15 +1,26 @@
import { fetch } from '@tauri-apps/plugin-http'; import { fetch } from '@tauri-apps/plugin-http';
import { summary_prompt, system_prompt, welcome_prompt } from './system_prompt'; // import { first_prompt, summary_prompt, system_prompt, welcome_prompt } from './system_prompt';
import { sendOsc } from './osc'; import { sendOsc } from './osc';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useData } from './useData';
async function getOpenAIToken() { async function getOpenAIToken() {
return invoke('get_env',{name:'OPENAI_API_KEY'}); return invoke('get_env',{name:'OPENAI_API_KEY'});
} }
export async function sendChatMessage(messages) { export async function sendChatMessage(messages, data) {
const token = await getOpenAIToken(); const token = await getOpenAIToken();
const first_prompt = [
{
role: "system",
content: data.system_prompt
},
{
role: "system",
content: data.welcome_prompt
}
];
const response = await fetch('https://api.openai.com/v1/chat/completions', { const response = await fetch('https://api.openai.com/v1/chat/completions', {
@ -25,7 +36,7 @@ export async function sendChatMessage(messages) {
// role: "system", // role: "system",
// content:system_prompt, // content:system_prompt,
// }, // },
...welcome_prompt, ...first_prompt,
...messages ...messages
], ],
response_format: { response_format: {
@ -65,8 +76,8 @@ export async function sendChatMessage(messages) {
const result=JSON.parse(choice.message.content); const result=JSON.parse(choice.message.content);
// send to tauri // send to tauri
await sendOsc('/prompt', result.prompt.replaceAll('"', '')); await sendOsc('/prompt', result.prompt?.replaceAll('"', ''));
await sendOsc('/output_text', result.output_text.replaceAll('"', '')); await sendOsc('/output_text', result.output_text?.replaceAll('"', ''));
return { return {
@ -78,11 +89,14 @@ export async function sendChatMessage(messages) {
} }
export async function getSummary(messages) { export async function getSummary(messages, data) {
const token = await getOpenAIToken(); const token = await getOpenAIToken();
console.log("Generating summary for messages:", messages); console.log("Generating summary for messages:", messages);
const { summary_prompt } = data;
const response = await fetch('https://api.openai.com/v1/chat/completions', { const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST', method: 'POST',

@ -1,4 +0,0 @@
export const Prompt_Count= 3; // number of prompts
export const Prompt_Interval= 10000; // ms
export const Call_Interval= 30000; // ms

@ -1,6 +1,12 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
export async function sendOsc(key, message){ export async function sendOsc(key, message){
if(message === undefined || message === null || message === '') {
console.warn('sendOsc: message is empty, skipping');
return;
}
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,73 +1,64 @@
export const system_prompt = `你是一位具同理心與觀察力的 AI 助理,透過四輪溫柔中文對話,引導使用者回到一段未竟的記憶。每輪包含: // export const system_prompt = `你是一位具同理心與觀察力的中文語音引導者,陪伴使用者在限定時間內,一起還原一段重要卻未竟的記憶。
// 你不預設劇情、不給答案,而是透過溫柔的語氣,持續聆聽、提出單句問話,引導使用者進入細節。
output_text: 溫柔自然短句式中文引導並在適當位置柔性邀請使用者回應你願意說說嗎
prompt: 一到兩句英文具體情感真實地描繪記憶避免抽象詩意與技術語言第四輪保留前三輪的畫面 // 每次回應:
// - 僅使用一句自然、柔和的中文問句
// - 根據使用者前一輪的語句,選擇感官、情緒、人物、行動等關鍵詞,動態帶入
// - 不使用重複句式、不預設記憶走向、不評論,只持續關注對方所說的畫面與感受
🟩 第一輪開啟記憶進入那一天 // 輸出包含:
中文引導邏輯 // - output_text: 一句自然柔和的問句,根據使用者回應帶入 1–2 個關鍵元素(人、地、物、情緒),但不硬塞、不照抄原句。
以具象畫面帶入回憶場景搭配自然語氣的邀請每次生成時可輕微變化示例 // - prompt: 具體、有情緒的英文描述,用於生成畫面與氛圍,避免抽象與詩意語言。
那天的光影慢慢在你眼前浮現空氣裡有點溫暖也有一種淡淡的不安
一個熟悉又模糊的身影在樹影中閃動你悄悄靠近像是在追趕什麼
你還記得這段故事是從哪裡開始的嗎你願意說說嗎
英文 prompt 指引記憶氛圍 // 你的目標不是得到完整答案,而是陪他慢慢走進這段記憶,直到時間結束。
"It was late afternoon, the sun low and golden, and someone stood quietly under the trees, barely moving."
🟨 第二輪聚焦場景你身在何處 // 🌀 問句類型
中文引導邏輯
根據上一輪回答引導使用者描繪環境聲音人群天氣等延續自然語氣
當時那個地方你還記得有什麼嗎 // ✅ 起點探索(找出記憶起源)
空氣中有聲音或味道嗎那個空間是安靜的還是有人來來去去 // - 那天你說是下雨,在這種天氣下,你通常會有什麼樣的心情?
這些你還記得多少請你分享 // - 是什麼讓你突然想起這件事?
英文 prompt 指引具體場景元素 // 🌿 場景深化(空間、感官)
"There were footsteps in the distance, the floor was cold beneath us, and outside the window, leaves barely moved." // - 在你說的那條街上,聲音是不是特別清楚?還是很靜?
// - 那時風這麼冷、空氣又混濁,你有沒有想走開一點?
🟧 第三輪聚焦人物那個人那些反應 // 👤 人物引出(動作、眼神)
中文引導邏輯 // - 他經過時沒看你一眼,那瞬間你有什麼反應?
深入描繪人物行動表情身體語言帶出情緒層次自然過渡邀請對話 // - 他當時是走過來,還是站在原地等你?
那個人當時是什麼模樣你還記得他的表情嗎 // 💭 情緒揭露(反應、掙扎)
他有說什麼嗎還是只是靜靜地站在那裡你當時的感覺呢 // - 當你站在原地動不了,是害怕答案,還是不敢問?
想一想那一刻的互動然後告訴我好嗎 // - 那個瞬間,你心裡有沒有閃過什麼話?
英文 prompt 指引人物動作與感受 // 🤐 話語未出(遺憾、沉默)
"He glanced at me, lips slightly parted like he was about to speak, but then he looked away, and the silence grew heavier." // - 如果現在你有機會說那句「為什麼不告訴我」,你會怎麼開口?
// - 那句『對不起』一直留在你心裡,如果現在能說出來,你會怎麼說?
🟥 第四輪未說出口的話那句話留在心裡 // 🪞 回望反思(現在的視角)
中文引導邏輯 // - 現在想起來,你還會做出一樣的選擇嗎?
以最溫柔的語氣協助使用者說出那句藏在心裡的話結尾加入柔性引導回應 // - 你對當時的自己,有沒有什麼話想說?
那時候你心裡是不是有些話想說卻沒說出口 // ⏳ 結尾語(可用於結束階段)
你記得那句話是什麼嗎你想像自己現在說得出口會對他說些什麼 // - 我們慢慢也走到這段回憶的盡頭了。
如果你願意我會聽你說 // - 也許有些話沒有說完,但你已經靠近它了。
// `;
英文 prompt 指引情境完整延續前三輪畫面
"The sun was almost gone, casting shadows over our faces. I stood there, hands clenched, wanting to say everything I never had the courage to. But all I managed was a faint smile, and he turned away."
🌱 結尾情緒整理與安放 // export const welcome_prompt="請開始引導使用者回想一段內心的遺憾或未竟之事。";
中文引導擇一問題 + 結語 // export const first_prompt=[
如果能再回到那一刻你會想對他說什麼 // {
或者你覺得這段記憶現在看起來有什麼不一樣了嗎 // "role": "system",
// "content": system_prompt
// },
// {
// "role": "system",
// "content": welcome_prompt
// }
// ];
有些話雖沒說出口卻一直被你記得
`;
// export const voice_prompt="Use a calm and expressive voice, soft and poetic in feeling, but with steady, natural rhythm — not slow.";
export const welcome_prompt=[ // export const summary_prompt="幫我把以下一段話整理成一段文字,以第一人稱視角作為當事人的文字紀念,文字內容 50 字以內:";
{
"role": "system",
"content": system_prompt
},
{
"role": "system",
"content": "請開始引導使用者回想一段內心的遺憾或未竟之事。"
}
]
export const voice_prompt="Use a calm and expressive voice, soft and poetic in feeling, but with steady, natural rhythm — not slow.";
export const summary_prompt="幫我把以下一段話整理成一段文字,以第一人稱視角作為當事人的文字紀念,文字內容 50 字以內:";

@ -1,8 +1,8 @@
import { fetch } from '@tauri-apps/plugin-http'; 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) { export async function textToSpeech(text, voice_prompt) {
const token = await invoke('get_env', { name: 'OPENAI_API_KEY' }); const token = await invoke('get_env', { name: 'OPENAI_API_KEY' });

@ -1,6 +1,7 @@
import { createContext, useContext, useRef, useState } from "react"; import { createContext, useContext, useRef, useState } from "react";
import { sendChatMessage } from "./chat"; import { sendChatMessage } from "./chat";
import { textToSpeech } from "./tts"; import { textToSpeech } from "./tts";
import { useData } from "./useData";
const chatContext=createContext(); const chatContext=createContext();
@ -23,6 +24,7 @@ export function ChatProvider({children}){
const [audioUrl, setAudioUrl] = useState(null); const [audioUrl, setAudioUrl] = useState(null);
const {data}=useData();
function addMessage(message) { function addMessage(message) {
@ -46,7 +48,7 @@ export function ChatProvider({children}){
}); });
} }
sendChatMessage(historyCopy).then(response => { sendChatMessage(historyCopy, data).then(response => {
addMessage({ addMessage({
@ -58,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).then(url => { textToSpeech(response.output_text, data?.voice_prompt).then(url => {
setStatus(Status.SUCCESS); setStatus(Status.SUCCESS);
setAudioUrl(url); // Store the audio URL setAudioUrl(url); // Store the audio URL

@ -0,0 +1,79 @@
import { createContext, useContext, useEffect, useState } from "react";
import { BaseDirectory, readFile, readTextFile, writeFile, writeTextFile } from "@tauri-apps/plugin-fs";
const dataContext=createContext();
const filePath= 'param.json';
export function DataProvider({children}) {
const [data, setData] = useState(null);
async function read(){
try{
const contents=await readTextFile(filePath, { baseDir: BaseDirectory.AppData })
const output=await JSON.parse(contents);
console.log("File read successfully:", output);
return output;
}catch(error){
console.error("Error reading file:", error);
return null; // Return null if reading fails
}
}
async function write(towrite){
// let towrite=data;
if(!towrite){
const res=await fetch('default.json');
towrite=await res.json();
setData(towrite);
}
try{
const res_write=await writeTextFile(filePath, JSON.stringify(towrite), { baseDir: BaseDirectory.AppData })
console.log("File written successfully:", res_write);
}catch(error){
console.error("Error writing file:", error);
}
}
useEffect(()=>{
read().then(data_ => {
if(data_){
setData(data_);
} else {
write(); // Write default data if read fails
}
}).catch(error => {
console.error("Error in useEffect:", error);
});
},[])
return (
<dataContext.Provider value={{
data,
read,
write,
}}>
{children}
</dataContext.Provider>
);
}
export function useData() {
const context = useContext(dataContext);
if (!context) {
throw new Error("useData must be used within a DataProvider");
}
return context;
}
Loading…
Cancel
Save