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. 24
      vite/src/main.jsx
  4. 15
      vite/src/pages/conversation.jsx
  5. 11
      vite/src/pages/flow.jsx
  6. 40
      vite/src/pages/settings.jsx
  7. 28
      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:allow-create",
"fs:allow-appdata-write",
"fs:allow-appdata-read",
"fs:allow-exists",
{
"identifier": "fs:scope",

@ -8,20 +8,22 @@ import { Settings } from './pages/settings.jsx';
import { Flow } from './pages/flow.jsx';
import { Conversation } from './pages/conversation.jsx';
import { ChatProvider } from './util/useChat.jsx';
import { DataProvider } from './util/useData.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<ChatProvider>
<BrowserRouter>
<App />
<Routes>
<Route path="/" element={<Conversation />} />
<Route path="/flow" element={<Flow />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</BrowserRouter>
</ChatProvider>
<DataProvider>
<ChatProvider>
<BrowserRouter>
<App />
<Routes>
<Route path="/" element={<Conversation />} />
<Route path="/flow" element={<Flow />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</BrowserRouter>
</ChatProvider>
</DataProvider>
</StrictMode>,
)

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

@ -8,6 +8,7 @@ import { getSummary } from "../util/chat";
import { saveHistory } from "../util/output";
import NumPad from "../comps/numpad";
import { Light } from "../comps/light";
import { useData } from "../util/useData";
const EmojiType={
@ -20,6 +21,8 @@ const EmojiType={
export function Flow(){
const { data }=useData();
const [cuelist, setCuelist] = useState([]);
const [currentCue, setCurrentCue] = useState(null);
const [chatWelcome, setChatWelcome] = useState(null);
@ -79,7 +82,7 @@ export function Flow(){
if(parseFloat(cue.id)<=4.2){
// Special case for starting a conversation
console.log('clear conversation...');
reset();
reset();
}
if(cue.type=='chat'){
@ -121,8 +124,8 @@ export function Flow(){
console.log('onCueEnd:', cue.id);
if(cue.callback=='start_conversation') refLight.current.fadeIn(); // Fade in light for conversation start
if(cue.callback=='summary') refLight.current.fadeOut(); // Fade out light for conversation end
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.auto) {
@ -221,7 +224,7 @@ export function Flow(){
if(refCurrentCue.current.callback=='summary'){
// get 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);

@ -1,9 +1,43 @@
import { useData } from '../util/useData.jsx';
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 (
<div className="flex flex-col items-center justify-center h-full">
<h1 className="text-4xl font-bold mb-4">Settings</h1>
<p className="text-lg">This page is under construction.</p>
<div className="flex flex-col p-2 gap-4 overflow-y-auto min-h-full">
<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>
);
}

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

@ -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';
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', {
key: key,
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=[
{
"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 字以內:";
// export const summary_prompt="幫我把以下一段話整理成一段文字,以第一人稱視角作為當事人的文字紀念,文字內容 50 字以內:";

@ -1,8 +1,8 @@
import { fetch } from '@tauri-apps/plugin-http';
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' });

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