+
+
+
{
diff --git a/app/stores/llm.ts b/app/stores/llm.ts
index 191fd1b..1d2c891 100644
--- a/app/stores/llm.ts
+++ b/app/stores/llm.ts
@@ -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',
diff --git a/package.json b/package.json
index 42f0046..a630afc 100644
--- a/package.json
+++ b/package.json
@@ -62,5 +62,8 @@
"vue-tsc": "^2.1.10",
"yauzl": "^3.2.0",
"zod": "^3.23.8"
+ },
+ "dependencies": {
+ "elevenlabs": "^0.18.1"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c2e9689..3496260 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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
diff --git a/server/api/pageview.ts b/server/api/pageview.ts
deleted file mode 100644
index e364c58..0000000
--- a/server/api/pageview.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-const startAt = Date.now()
-let count = 0
-
-export default defineEventHandler(() => ({
- pageview: count++,
- startAt,
-}))
diff --git a/server/api/v1/llm/voice/text-to-speech.ts b/server/api/v1/llm/voice/text-to-speech.ts
new file mode 100644
index 0000000..553ea53
--- /dev/null
+++ b/server/api/v1/llm/voice/text-to-speech.ts
@@ -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)
+})