support tts
This commit is contained in:
34
src/app.tsx
34
src/app.tsx
@@ -43,7 +43,11 @@ export interface ChatStore {
|
||||
develop_mode: boolean;
|
||||
whisper_api: string;
|
||||
whisper_key: string;
|
||||
audioDeviceID: string;
|
||||
tts_api: string;
|
||||
tts_key: string;
|
||||
tts_voice: string;
|
||||
tts_speed: number;
|
||||
tts_speed_enabled: boolean;
|
||||
}
|
||||
|
||||
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
|
||||
@@ -56,7 +60,11 @@ const newChatStore = (
|
||||
temperature = 0.7,
|
||||
dev = false,
|
||||
whisper_api = "",
|
||||
whisper_key = ""
|
||||
whisper_key = "",
|
||||
tts_api = "",
|
||||
tts_key = "",
|
||||
tts_speed = 1.0,
|
||||
tts_speed_enabled = false
|
||||
): ChatStore => {
|
||||
return {
|
||||
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
|
||||
@@ -81,7 +89,11 @@ const newChatStore = (
|
||||
develop_mode: getDefaultParams("dev", dev),
|
||||
whisper_api: getDefaultParams("whisper-api", whisper_api),
|
||||
whisper_key: getDefaultParams("whisper-key", whisper_key),
|
||||
audioDeviceID: "",
|
||||
tts_api: getDefaultParams("tts-api", tts_api),
|
||||
tts_key: getDefaultParams("tts-key", tts_key),
|
||||
tts_voice: "alloy",
|
||||
tts_speed: tts_speed,
|
||||
tts_speed_enabled: tts_speed_enabled,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -215,7 +227,11 @@ export function App() {
|
||||
chatStore.temperature,
|
||||
!!chatStore.develop_mode,
|
||||
chatStore.whisper_api,
|
||||
chatStore.whisper_key
|
||||
chatStore.whisper_key,
|
||||
chatStore.tts_api,
|
||||
chatStore.tts_key,
|
||||
chatStore.tts_speed,
|
||||
chatStore.tts_speed_enabled
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -296,7 +312,15 @@ export function App() {
|
||||
chatStore.systemMessageContent,
|
||||
chatStore.apiEndpoint,
|
||||
chatStore.streamMode,
|
||||
chatStore.model
|
||||
chatStore.model,
|
||||
chatStore.temperature,
|
||||
!!chatStore.develop_mode,
|
||||
chatStore.whisper_api,
|
||||
chatStore.whisper_key,
|
||||
chatStore.tts_api,
|
||||
chatStore.tts_key,
|
||||
chatStore.tts_speed,
|
||||
chatStore.tts_speed_enabled
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState, useEffect, StateUpdater } from "preact/hooks";
|
||||
import { ChatStore, ChatStoreMessage } from "./app";
|
||||
import { calculate_token_length } from "./chatgpt";
|
||||
import Markdown from "preact-markdown";
|
||||
import TTSButton from "./tts";
|
||||
|
||||
interface EditMessageProps {
|
||||
chat: ChatStoreMessage;
|
||||
@@ -163,6 +164,13 @@ export default function Message(props: Props) {
|
||||
<div className="w-full flex justify-between">
|
||||
<DeleteIcon />
|
||||
<button onClick={() => setShowEdit(true)}>🖋</button>
|
||||
{chatStore.tts_api && chatStore.tts_key && (
|
||||
<TTSButton
|
||||
chatStore={chatStore}
|
||||
text={chat.content}
|
||||
setChatStore={setChatStore}
|
||||
/>
|
||||
)}
|
||||
<CopyIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,15 @@ import { TemplateChatStore } from "./chatbox";
|
||||
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
import p from "preact-markdown";
|
||||
|
||||
const TTS_VOICES: string[] = [
|
||||
"alloy",
|
||||
"echo",
|
||||
"fable",
|
||||
"onyx",
|
||||
"nova",
|
||||
"shimmer",
|
||||
];
|
||||
|
||||
const Help = (props: { children: any; help: string }) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -71,7 +80,13 @@ const LongInput = (props: {
|
||||
const Input = (props: {
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
field: "apiKey" | "apiEndpoint" | "whisper_api" | "whisper_key";
|
||||
field:
|
||||
| "apiKey"
|
||||
| "apiEndpoint"
|
||||
| "whisper_api"
|
||||
| "whisper_key"
|
||||
| "tts_api"
|
||||
| "tts_key";
|
||||
help: string;
|
||||
}) => {
|
||||
const [hideInput, setHideInput] = useState(true);
|
||||
@@ -101,8 +116,10 @@ const Input = (props: {
|
||||
const Slicer = (props: {
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
field: "temperature" | "top_p";
|
||||
field: "temperature" | "top_p" | "tts_speed";
|
||||
help: string;
|
||||
min: number;
|
||||
max: number;
|
||||
}) => {
|
||||
const enable_filed_name: "temperature_enabled" | "top_p_enabled" =
|
||||
`${props.field}_enabled` as any;
|
||||
@@ -139,8 +156,8 @@ const Slicer = (props: {
|
||||
disabled={!enabled}
|
||||
className="m-2 p-2 border rounded focus w-28"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
step="0.01"
|
||||
value={props.chatStore[props.field]}
|
||||
onChange={(event: any) => {
|
||||
@@ -387,8 +404,8 @@ export default (props: {
|
||||
readOnly={true}
|
||||
{...props}
|
||||
/>
|
||||
<Slicer field="temperature" help="温度" {...props} />
|
||||
<Slicer field="top_p" help="top_p" {...props} />
|
||||
<Slicer field="temperature" min={0} max={1} help="温度" {...props} />
|
||||
<Slicer field="top_p" min={0} max={1} help="top_p" {...props} />
|
||||
<Number
|
||||
field="presence_penalty"
|
||||
help="presence_penalty"
|
||||
@@ -401,61 +418,47 @@ export default (props: {
|
||||
readOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
<Input
|
||||
field="whisper_api"
|
||||
help="Whisper 语言转文字服务,填入此api才会开启,默认为 https://api.openai.com/v1/audio/transriptions"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<Input
|
||||
field="whisper_key"
|
||||
help="用于 Whisper 服务的 key,默认为 上方使用的OPENAI key,可在此单独配置专用key"
|
||||
{...props}
|
||||
/>
|
||||
<Input
|
||||
field="whisper_api"
|
||||
help="Whisper 语言转文字服务,填入此api才会开启,默认为 https://api.openai.com/v1/audio/transriptions"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<p className="flex justify-between">
|
||||
<label className="m-2 p-2">{Tr("Audio Device")}</label>
|
||||
{devices.length === 0 && (
|
||||
<button
|
||||
className="p-2 m-2 rounded bg-emerald-500"
|
||||
onClick={async () => {
|
||||
const ds: MediaDeviceInfo[] = (
|
||||
await navigator.mediaDevices.enumerateDevices()
|
||||
).filter((device) => device.kind === "audioinput");
|
||||
|
||||
setDevices([...ds]);
|
||||
console.log("devices", ds);
|
||||
}}
|
||||
>
|
||||
{props.chatStore.audioDeviceID
|
||||
? props.chatStore.audioDeviceID
|
||||
: Tr("default")}
|
||||
</button>
|
||||
)}
|
||||
{devices.length > 0 && (
|
||||
<Input field="tts_key" help="tts service api key" {...props} />
|
||||
<Input
|
||||
field="tts_api"
|
||||
help="tts api, eg. https://api.openai.com/v1/audio/speech"
|
||||
{...props}
|
||||
/>
|
||||
<Help help="tts voice style">
|
||||
<label className="m-2 p-2">TTS Voice</label>
|
||||
<select
|
||||
value={
|
||||
props.chatStore.audioDeviceID
|
||||
? props.chatStore.audioDeviceID
|
||||
: "default"
|
||||
}
|
||||
className="m-2 p-2"
|
||||
value={props.chatStore.tts_voice}
|
||||
onChange={(event: any) => {
|
||||
const value = event.target.value;
|
||||
if (!value || value == "default") {
|
||||
props.chatStore.audioDeviceID = "";
|
||||
props.setChatStore({ ...props.chatStore });
|
||||
return;
|
||||
}
|
||||
props.chatStore.audioDeviceID = value;
|
||||
const voice = event.target.value as string;
|
||||
props.chatStore.tts_voice = voice;
|
||||
props.setChatStore({ ...props.chatStore });
|
||||
}}
|
||||
>
|
||||
<option value={"default"}>{Tr("default")}</option>
|
||||
{devices.map((device) => (
|
||||
<option value={device.deviceId}>{device.deviceId}</option>
|
||||
{TTS_VOICES.map((opt) => (
|
||||
<option value={opt}>{opt}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</p>
|
||||
</Help>
|
||||
<Slicer
|
||||
min={0.25}
|
||||
max={4.0}
|
||||
field="tts_speed"
|
||||
help={"TTS Speed"}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<p className="m-2 p-2">
|
||||
|
||||
53
src/tts.tsx
Normal file
53
src/tts.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ChatStore, addTotalCost } from "./app";
|
||||
|
||||
interface TTSProps {
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
text: string;
|
||||
}
|
||||
export default function TTSButton(props: TTSProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
const api = props.chatStore.tts_api;
|
||||
const api_key = props.chatStore.tts_key;
|
||||
const model = "tts-1";
|
||||
const input = props.text;
|
||||
const voice = "alloy";
|
||||
|
||||
const body: Record<string, any> = {
|
||||
model,
|
||||
input,
|
||||
voice,
|
||||
response_format: "opus",
|
||||
};
|
||||
if (props.chatStore.tts_speed_enabled) {
|
||||
body["speed"] = props.chatStore.tts_speed;
|
||||
}
|
||||
|
||||
fetch(api, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${api_key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
// update price
|
||||
const cost = (props.text.length * 0.015) / 1000;
|
||||
props.chatStore.cost += cost;
|
||||
addTotalCost(cost);
|
||||
props.setChatStore({ ...props.chatStore });
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const audio = new Audio(url);
|
||||
audio.play();
|
||||
});
|
||||
}}
|
||||
>
|
||||
🔈
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user