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;
|
develop_mode: boolean;
|
||||||
whisper_api: string;
|
whisper_api: string;
|
||||||
whisper_key: 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";
|
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
|
||||||
@@ -56,7 +60,11 @@ const newChatStore = (
|
|||||||
temperature = 0.7,
|
temperature = 0.7,
|
||||||
dev = false,
|
dev = false,
|
||||||
whisper_api = "",
|
whisper_api = "",
|
||||||
whisper_key = ""
|
whisper_key = "",
|
||||||
|
tts_api = "",
|
||||||
|
tts_key = "",
|
||||||
|
tts_speed = 1.0,
|
||||||
|
tts_speed_enabled = false
|
||||||
): ChatStore => {
|
): ChatStore => {
|
||||||
return {
|
return {
|
||||||
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
|
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
|
||||||
@@ -81,7 +89,11 @@ const newChatStore = (
|
|||||||
develop_mode: getDefaultParams("dev", dev),
|
develop_mode: getDefaultParams("dev", dev),
|
||||||
whisper_api: getDefaultParams("whisper-api", whisper_api),
|
whisper_api: getDefaultParams("whisper-api", whisper_api),
|
||||||
whisper_key: getDefaultParams("whisper-key", whisper_key),
|
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.temperature,
|
||||||
!!chatStore.develop_mode,
|
!!chatStore.develop_mode,
|
||||||
chatStore.whisper_api,
|
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.systemMessageContent,
|
||||||
chatStore.apiEndpoint,
|
chatStore.apiEndpoint,
|
||||||
chatStore.streamMode,
|
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 { ChatStore, ChatStoreMessage } from "./app";
|
||||||
import { calculate_token_length } from "./chatgpt";
|
import { calculate_token_length } from "./chatgpt";
|
||||||
import Markdown from "preact-markdown";
|
import Markdown from "preact-markdown";
|
||||||
|
import TTSButton from "./tts";
|
||||||
|
|
||||||
interface EditMessageProps {
|
interface EditMessageProps {
|
||||||
chat: ChatStoreMessage;
|
chat: ChatStoreMessage;
|
||||||
@@ -163,6 +164,13 @@ export default function Message(props: Props) {
|
|||||||
<div className="w-full flex justify-between">
|
<div className="w-full flex justify-between">
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
<button onClick={() => setShowEdit(true)}>🖋</button>
|
<button onClick={() => setShowEdit(true)}>🖋</button>
|
||||||
|
{chatStore.tts_api && chatStore.tts_key && (
|
||||||
|
<TTSButton
|
||||||
|
chatStore={chatStore}
|
||||||
|
text={chat.content}
|
||||||
|
setChatStore={setChatStore}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<CopyIcon />
|
<CopyIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ import { TemplateChatStore } from "./chatbox";
|
|||||||
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||||
import p from "preact-markdown";
|
import p from "preact-markdown";
|
||||||
|
|
||||||
|
const TTS_VOICES: string[] = [
|
||||||
|
"alloy",
|
||||||
|
"echo",
|
||||||
|
"fable",
|
||||||
|
"onyx",
|
||||||
|
"nova",
|
||||||
|
"shimmer",
|
||||||
|
];
|
||||||
|
|
||||||
const Help = (props: { children: any; help: string }) => {
|
const Help = (props: { children: any; help: string }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -71,7 +80,13 @@ const LongInput = (props: {
|
|||||||
const Input = (props: {
|
const Input = (props: {
|
||||||
chatStore: ChatStore;
|
chatStore: ChatStore;
|
||||||
setChatStore: (cs: ChatStore) => void;
|
setChatStore: (cs: ChatStore) => void;
|
||||||
field: "apiKey" | "apiEndpoint" | "whisper_api" | "whisper_key";
|
field:
|
||||||
|
| "apiKey"
|
||||||
|
| "apiEndpoint"
|
||||||
|
| "whisper_api"
|
||||||
|
| "whisper_key"
|
||||||
|
| "tts_api"
|
||||||
|
| "tts_key";
|
||||||
help: string;
|
help: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [hideInput, setHideInput] = useState(true);
|
const [hideInput, setHideInput] = useState(true);
|
||||||
@@ -101,8 +116,10 @@ const Input = (props: {
|
|||||||
const Slicer = (props: {
|
const Slicer = (props: {
|
||||||
chatStore: ChatStore;
|
chatStore: ChatStore;
|
||||||
setChatStore: (cs: ChatStore) => void;
|
setChatStore: (cs: ChatStore) => void;
|
||||||
field: "temperature" | "top_p";
|
field: "temperature" | "top_p" | "tts_speed";
|
||||||
help: string;
|
help: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
}) => {
|
}) => {
|
||||||
const enable_filed_name: "temperature_enabled" | "top_p_enabled" =
|
const enable_filed_name: "temperature_enabled" | "top_p_enabled" =
|
||||||
`${props.field}_enabled` as any;
|
`${props.field}_enabled` as any;
|
||||||
@@ -139,8 +156,8 @@ const Slicer = (props: {
|
|||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
className="m-2 p-2 border rounded focus w-28"
|
className="m-2 p-2 border rounded focus w-28"
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min={props.min}
|
||||||
max="1"
|
max={props.max}
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={props.chatStore[props.field]}
|
value={props.chatStore[props.field]}
|
||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
@@ -387,8 +404,8 @@ export default (props: {
|
|||||||
readOnly={true}
|
readOnly={true}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Slicer field="temperature" help="温度" {...props} />
|
<Slicer field="temperature" min={0} max={1} help="温度" {...props} />
|
||||||
<Slicer field="top_p" help="top_p" {...props} />
|
<Slicer field="top_p" min={0} max={1} help="top_p" {...props} />
|
||||||
<Number
|
<Number
|
||||||
field="presence_penalty"
|
field="presence_penalty"
|
||||||
help="presence_penalty"
|
help="presence_penalty"
|
||||||
@@ -401,61 +418,47 @@ export default (props: {
|
|||||||
readOnly={false}
|
readOnly={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
field="whisper_api"
|
|
||||||
help="Whisper 语言转文字服务,填入此api才会开启,默认为 https://api.openai.com/v1/audio/transriptions"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<Input
|
<Input
|
||||||
field="whisper_key"
|
field="whisper_key"
|
||||||
help="用于 Whisper 服务的 key,默认为 上方使用的OPENAI key,可在此单独配置专用key"
|
help="用于 Whisper 服务的 key,默认为 上方使用的OPENAI key,可在此单独配置专用key"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
field="whisper_api"
|
||||||
|
help="Whisper 语言转文字服务,填入此api才会开启,默认为 https://api.openai.com/v1/audio/transriptions"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
<p className="flex justify-between">
|
<Input field="tts_key" help="tts service api key" {...props} />
|
||||||
<label className="m-2 p-2">{Tr("Audio Device")}</label>
|
<Input
|
||||||
{devices.length === 0 && (
|
field="tts_api"
|
||||||
<button
|
help="tts api, eg. https://api.openai.com/v1/audio/speech"
|
||||||
className="p-2 m-2 rounded bg-emerald-500"
|
{...props}
|
||||||
onClick={async () => {
|
/>
|
||||||
const ds: MediaDeviceInfo[] = (
|
<Help help="tts voice style">
|
||||||
await navigator.mediaDevices.enumerateDevices()
|
<label className="m-2 p-2">TTS Voice</label>
|
||||||
).filter((device) => device.kind === "audioinput");
|
|
||||||
|
|
||||||
setDevices([...ds]);
|
|
||||||
console.log("devices", ds);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.chatStore.audioDeviceID
|
|
||||||
? props.chatStore.audioDeviceID
|
|
||||||
: Tr("default")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{devices.length > 0 && (
|
|
||||||
<select
|
<select
|
||||||
value={
|
className="m-2 p-2"
|
||||||
props.chatStore.audioDeviceID
|
value={props.chatStore.tts_voice}
|
||||||
? props.chatStore.audioDeviceID
|
|
||||||
: "default"
|
|
||||||
}
|
|
||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
const value = event.target.value;
|
const voice = event.target.value as string;
|
||||||
if (!value || value == "default") {
|
props.chatStore.tts_voice = voice;
|
||||||
props.chatStore.audioDeviceID = "";
|
|
||||||
props.setChatStore({ ...props.chatStore });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
props.chatStore.audioDeviceID = value;
|
|
||||||
props.setChatStore({ ...props.chatStore });
|
props.setChatStore({ ...props.chatStore });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value={"default"}>{Tr("default")}</option>
|
{TTS_VOICES.map((opt) => (
|
||||||
{devices.map((device) => (
|
<option value={opt}>{opt}</option>
|
||||||
<option value={device.deviceId}>{device.deviceId}</option>
|
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)}
|
</Help>
|
||||||
</p>
|
<Slicer
|
||||||
|
min={0.25}
|
||||||
|
max={4.0}
|
||||||
|
field="tts_speed"
|
||||||
|
help={"TTS Speed"}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<p className="m-2 p-2">
|
<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