chore: tts

Signed-off-by: Neko Ayaka <neko@ayaka.moe>
This commit is contained in:
Neko Ayaka
2024-12-02 20:49:31 +08:00
parent be4cbce93e
commit 8b1338c123
6 changed files with 139 additions and 11 deletions
+11 -3
View File
@@ -37,6 +37,7 @@ const nowSpeakingAvatarBorderOpacityMax = 100
const openAiApiKey = useLocalStorage('openai-api-key', '')
const openAiApiBaseURL = useLocalStorage('openai-api-base-url', '')
const openAIModel = useLocalStorage<{ id: string, name?: string }>('openai-model', { id: 'openai/gpt-3.5-turbo', name: 'OpenAI GPT3.5 Turbo' })
const elevenLabsApiKey = useLocalStorage('elevenlabs-api-key', '')
const { setupOpenAI, streamSpeech, stream, models } = useLLM()
const { audioContext, calculateVolume } = useAudioContext()
@@ -102,7 +103,7 @@ const ttsQueue = useQueue<string>({
handlers: [
async (ctx) => {
const now = Date.now()
const res = await streamSpeech(ctx.data)
const res = await streamSpeech(ctx.data, elevenLabsApiKey.value)
const elapsed = Date.now() - now
// eslint-disable-next-line no-console
@@ -253,6 +254,13 @@ onUnmounted(() => {
<template>
<div max-h="[100vh]" h-full p="2" flex="~ col">
<div space-x="2" flex="~ row" w-full>
<div flex="~ row" w-full>
<input
v-model="openAiApiBaseURL"
placeholder="Input your API base URL"
p="2" bg="zinc-100 dark:zinc-700" w-full rounded-lg outline-none
>
</div>
<div flex="~ row" w-full>
<input
v-model="openAiApiKey"
@@ -262,8 +270,8 @@ onUnmounted(() => {
</div>
<div flex="~ row" w-full>
<input
v-model="openAiApiBaseURL"
placeholder="Input your API base URL"
v-model="elevenLabsApiKey"
placeholder="Input your ElevenLabs API key"
p="2" bg="zinc-100 dark:zinc-700" w-full rounded-lg outline-none
>
</div>
+2 -1
View File
@@ -35,13 +35,14 @@ export const useLLM = defineStore('llm', () => {
return await openAI.value.models.list()
}
async function streamSpeech(text: string) {
async function streamSpeech(text: string, apiKey: string) {
if (!text || !text.trim())
throw new Error('Text is required')
return await ofetch('/api/v1/llm/voice/text-to-speech', {
body: {
text,
apiKey,
},
method: 'POST',
cache: 'no-cache',
+3
View File
@@ -62,5 +62,8 @@
"vue-tsc": "^2.1.10",
"yauzl": "^3.2.0",
"zod": "^3.23.8"
},
"dependencies": {
"elevenlabs": "^0.18.1"
}
}
+93
View File
@@ -7,6 +7,10 @@ settings:
importers:
.:
dependencies:
elevenlabs:
specifier: ^0.18.1
version: 0.18.1
devDependencies:
'@ai-sdk/openai':
specifier: ^1.0.5
@@ -2640,6 +2644,9 @@ packages:
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
command-exists@1.2.9:
resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -2974,6 +2981,9 @@ packages:
electron-to-chromium@1.5.67:
resolution: {integrity: sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ==}
elevenlabs@0.18.1:
resolution: {integrity: sha512-JHdgqPyRJFwpo6jfJy8UaVWmt5yQ/SMVsUWAHOng0QpeeI0UxxYMrIR1TcpIXCaFkk4+MOgk8neJu2K1JIFaoA==}
email-addresses@3.1.0:
resolution: {integrity: sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==}
@@ -3302,6 +3312,10 @@ packages:
resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==}
engines: {node: '>=18.0.0'}
execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
execa@7.2.0:
resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==}
engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
@@ -3411,6 +3425,10 @@ packages:
form-data-encoder@1.7.2:
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
form-data-encoder@4.0.2:
resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==}
engines: {node: '>= 18'}
form-data@4.0.1:
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
engines: {node: '>= 6'}
@@ -3419,6 +3437,10 @@ packages:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
formdata-node@6.0.3:
resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==}
engines: {node: '>= 18'}
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@@ -3660,6 +3682,10 @@ packages:
httpxy@0.1.5:
resolution: {integrity: sha512-hqLDO+rfststuyEUTWObQK6zHEEmZ/kaIP2/zclGGZn6X8h/ESTWg+WKecQ/e5k4nPswjzZD+q2VqZIbr15CoQ==}
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
human-signals@4.3.1:
resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==}
engines: {node: '>=14.18.0'}
@@ -4299,6 +4325,10 @@ packages:
engines: {node: '>=16'}
hasBin: true
mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
mimic-fn@4.0.0:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
@@ -4505,6 +4535,10 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
onetime@6.0.0:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
@@ -4929,6 +4963,10 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qs@6.11.2:
resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==}
engines: {node: '>=0.6'}
qs@6.13.1:
resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==}
engines: {node: '>=0.6'}
@@ -5330,6 +5368,10 @@ packages:
resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==}
engines: {node: '>=10'}
strip-final-newline@2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
strip-final-newline@3.0.0:
resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
engines: {node: '>=12'}
@@ -5717,6 +5759,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-join@4.0.1:
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
url@0.11.4:
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
engines: {node: '>= 0.4'}
@@ -9097,6 +9142,8 @@ snapshots:
comma-separated-tokens@2.0.3: {}
command-exists@1.2.9: {}
commander@2.20.3: {}
commander@7.2.0: {}
@@ -9386,6 +9433,20 @@ snapshots:
electron-to-chromium@1.5.67: {}
elevenlabs@0.18.1:
dependencies:
command-exists: 1.2.9
execa: 5.1.1
form-data: 4.0.1
form-data-encoder: 4.0.2
formdata-node: 6.0.3
node-fetch: 2.7.0
qs: 6.11.2
readable-stream: 4.5.2
url-join: 4.0.1
transitivePeerDependencies:
- encoding
email-addresses@3.1.0: {}
emoji-regex@8.0.0: {}
@@ -9906,6 +9967,18 @@ snapshots:
eventsource-parser@3.0.0: {}
execa@5.1.1:
dependencies:
cross-spawn: 7.0.6
get-stream: 6.0.1
human-signals: 2.1.0
is-stream: 2.0.1
merge-stream: 2.0.0
npm-run-path: 4.0.1
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
execa@7.2.0:
dependencies:
cross-spawn: 7.0.6
@@ -10033,6 +10106,8 @@ snapshots:
form-data-encoder@1.7.2: {}
form-data-encoder@4.0.2: {}
form-data@4.0.1:
dependencies:
asynckit: 0.4.0
@@ -10044,6 +10119,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
formdata-node@6.0.3: {}
fraction.js@4.3.7: {}
fresh@0.5.2: {}
@@ -10332,6 +10409,8 @@ snapshots:
httpxy@0.1.5: {}
human-signals@2.1.0: {}
human-signals@4.3.1: {}
human-signals@5.0.0: {}
@@ -11110,6 +11189,8 @@ snapshots:
mime@4.0.4: {}
mimic-fn@2.1.0: {}
mimic-fn@4.0.0: {}
min-indent@1.0.1: {}
@@ -11465,6 +11546,10 @@ snapshots:
dependencies:
wrappy: 1.0.2
onetime@5.1.2:
dependencies:
mimic-fn: 2.1.0
onetime@6.0.0:
dependencies:
mimic-fn: 4.0.0
@@ -11852,6 +11937,10 @@ snapshots:
punycode@2.3.1: {}
qs@6.11.2:
dependencies:
side-channel: 1.0.6
qs@6.13.1:
dependencies:
side-channel: 1.0.6
@@ -12341,6 +12430,8 @@ snapshots:
strip-comments@2.0.1: {}
strip-final-newline@2.0.0: {}
strip-final-newline@3.0.0: {}
strip-indent@3.0.0:
@@ -12776,6 +12867,8 @@ snapshots:
dependencies:
punycode: 2.3.1
url-join@4.0.1: {}
url@0.11.4:
dependencies:
punycode: 1.4.1
-7
View File
@@ -1,7 +0,0 @@
const startAt = Date.now()
let count = 0
export default defineEventHandler(() => ({
pageview: count++,
startAt,
}))
+30
View File
@@ -0,0 +1,30 @@
import { ElevenLabsClient } from 'elevenlabs'
export default defineEventHandler(async (event) => {
const body = await readBody<{ text: string, apiKey: string }>(event)
const client = new ElevenLabsClient({
apiKey: body.apiKey,
})
const res = await client.generate({
// voice: 'ShanShan',
// Quite good for English
voice: 'Myriam',
// Beatrice is not 'childish' like the others
// voice: 'Beatrice',
text: body.text,
stream: true,
model_id: 'eleven_multilingual_v2',
voice_settings: {
stability: 0.4,
similarity_boost: 0.5,
},
})
// Set headers for streaming
event.node.res.setHeader('Content-Type', 'audio/mpeg')
event.node.res.setHeader('Transfer-Encoding', 'chunked')
// res is NodeJS.ReadableStream
return sendStream(event, res)
})