support tts

This commit is contained in:
2023-11-07 13:50:49 +08:00
parent 63a8331250
commit 15054eec85
4 changed files with 148 additions and 60 deletions

View File

@@ -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
)
);
}

View File

@@ -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>

View File

@@ -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
View 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>
);
}