diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 0566095..f941c1a 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/app/pages/index.vue b/app/pages/index.vue index 4bb09b9..bfc2793 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,22 +1,22 @@ diff --git a/nuxt.config.ts b/nuxt.config.ts index 03a6f79..996c978 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -2,5 +2,6 @@ export default defineNuxtConfig({ compatibilityDate: '2025-07-15', devtools: { enabled: true }, - modules: ['vuetify-nuxt-module', '@nuxtjs/tailwindcss', 'nuxt-aos'] -}) \ No newline at end of file + modules: ['vuetify-nuxt-module', '@nuxtjs/tailwindcss', 'nuxt-aos'], + aos: { once: true, duration: 600, easing: 'ease-out' } +}) diff --git a/package.json b/package.json index dcbb777..c94055d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dotenv": "^17.2.2", "nuxt": "^4.1.1", "nuxt-aos": "1.2.5", - "openai": "^5.20.2", + "openai": "^4.66.0", "vue": "^3.5.21", "vue-router": "^4.5.1", "vuetify": "3.9.0-beta.1", diff --git a/server/api/ptt/reply.post.ts b/server/api/ptt/reply.post.ts index d0345aa..9cdfd12 100644 --- a/server/api/ptt/reply.post.ts +++ b/server/api/ptt/reply.post.ts @@ -1,35 +1,62 @@ import { readMultipartFormData, createError } from "h3"; -import { openai, LLM_MODEL, TTS_MODEL, atcReplyPrompt } from "../../utils/openai"; +import { LLM_MODEL, TTS_MODEL, atcReplyPrompt, openai } from "../../utils/openai"; import { applyRadioEffect } from "../../utils/radio"; -import { writeFile, rm } from "node:fs/promises"; +import { writeFile, rm, readFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { toFile } from "openai/uploads"; // ⟵ NEU export default defineEventHandler(async (event) => { const parts = await readMultipartFormData(event); - const mode = parts?.find(p=>p.name==="mode")?.data?.toString("utf8") || "text"; - const audio = parts?.find(p=>p.type && p.data); - if (!audio) throw createError({ statusCode:400, statusMessage:"No audio" }); + if (!parts) throw createError({ statusCode: 400, statusMessage: "No form-data" }); - const file = new File([audio.data], audio.filename || "audio.webm", { type: audio.type || "audio/webm" }); + const mode = (parts.find(p => p.name === "mode")?.data?.toString("utf8") || "text").toLowerCase(); + const audio = parts.find(p => p.type && p.data); + if (!audio) throw createError({ statusCode: 400, statusMessage: "No audio file" }); + + // ✅ saubere Datei-Erzeugung + const file = await toFile( + audio.data, + audio.filename || "ptt.webm", + { type: audio.type || "audio/webm" } + ); + + // 1) Transkribieren const tr = await openai.audio.transcriptions.create({ model: "whisper-1", file }); - const pilotText = tr.text.trim(); + const pilotText = tr.text?.trim() || ""; + if (!pilotText) throw createError({ statusCode: 400, statusMessage: "Empty transcription" }); - const resp = await openai.responses.create({ model: LLM_MODEL, input: atcReplyPrompt(pilotText) }); + // 2) ATC-Antwort + const resp = await openai.responses.create({ + model: LLM_MODEL, + input: atcReplyPrompt(pilotText), + }); const replyText = resp.output_text?.trim() || ""; + if (!replyText) throw createError({ statusCode: 500, statusMessage: "LLM empty" }); + // 3) Optional TTS + Funk if (mode === "tts") { const clean = join(tmpdir(), `tts-${randomUUID()}.wav`); const radio = join(tmpdir(), `radio-${randomUUID()}.wav`); - const tts = await openai.audio.speech.create({ model: TTS_MODEL, voice: "alloy", format: "wav", input: replyText }); + + const tts = await openai.audio.speech.create({ + model: TTS_MODEL, + voice: "alloy", + format: "wav", + input: replyText, + }); + await writeFile(clean, Buffer.from(await tts.arrayBuffer())); await applyRadioEffect(clean, radio); - const data = await import("node:fs/promises").then(fs => fs.readFile(radio)); - await rm(clean).catch(()=>{}); + + const data = await readFile(radio); const b64 = Buffer.from(data).toString("base64"); - await rm(radio).catch(()=>{}); - return { pilotText, replyText, audio:{ mime:"audio/wav", base64:b64 } }; + rm(clean).catch(() => {}); + rm(radio).catch(() => {}); + + return { pilotText, replyText, audio: { mime: "audio/wav", base64: b64 } }; } + return { pilotText, replyText }; }); diff --git a/server/api/ptt/transcribe.post.ts b/server/api/ptt/transcribe.post.ts index 1cbf79b..c712cac 100644 --- a/server/api/ptt/transcribe.post.ts +++ b/server/api/ptt/transcribe.post.ts @@ -1,10 +1,24 @@ import { readMultipartFormData, createError } from "h3"; import { openai } from "../../utils/openai"; +import { toFile } from "openai/uploads"; // ⟵ NEU + export default defineEventHandler(async (event) => { const parts = await readMultipartFormData(event); - const audio = parts?.find(p=>p.type && p.data); - if (!audio) throw createError({ statusCode:400, statusMessage:"No audio" }); - const file = new File([audio.data], audio.filename || "audio.webm", { type: audio.type || "audio/webm" }); - const tr = await openai.audio.transcriptions.create({ model: "whisper-1", file }); + if (!parts) throw createError({ statusCode: 400, statusMessage: "No form-data" }); + + const audio = parts.find(p => p.type && p.data); + if (!audio) throw createError({ statusCode: 400, statusMessage: "No audio file" }); + + const file = await toFile( + audio.data, // Buffer + audio.filename || "ptt.webm", // ⟵ mit Endung + { type: audio.type || "audio/webm" } // ⟵ korrekter MIME + ); + + const tr = await openai.audio.transcriptions.create({ + model: "whisper-1", + file + }); + return { text: tr.text }; }); diff --git a/yarn.lock b/yarn.lock index 131c5d3..8461dcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2059,6 +2059,34 @@ __metadata: languageName: node linkType: hard +"@types/node-fetch@npm:^2.6.4": + version: 2.6.13 + resolution: "@types/node-fetch@npm:2.6.13" + dependencies: + "@types/node": "npm:*" + form-data: "npm:^4.0.4" + checksum: 10c0/6313c89f62c50bd0513a6839cdff0a06727ac5495ccbb2eeda51bb2bbbc4f3c0a76c0393a491b7610af703d3d2deb6cf60e37e59c81ceeca803ffde745dbf309 + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 24.4.0 + resolution: "@types/node@npm:24.4.0" + dependencies: + undici-types: "npm:~7.11.0" + checksum: 10c0/3aefa4c6b006b3478f2d28a48d830ab7350ce24efdaf352011418e3ee17ed7683ca9c0a1840e770685ce78f413344badafa91e034be02488efe2eb6b612bd542 + languageName: node + linkType: hard + +"@types/node@npm:^18.11.18": + version: 18.19.124 + resolution: "@types/node@npm:18.19.124" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/a1b1aa2d21a0b0dec473b9b15e048a25f86cd26f7fc1b9036b43b838ee50b33534518c503f12c9a86be4cebb0f957fd3583fee9c78372ece973a1081c9fc7966 + languageName: node + linkType: hard + "@types/parse-path@npm:^7.0.0": version: 7.0.3 resolution: "@types/parse-path@npm:7.0.3" @@ -2454,6 +2482,15 @@ __metadata: languageName: node linkType: hard +"agentkeepalive@npm:^4.2.1": + version: 4.6.0 + resolution: "agentkeepalive@npm:4.6.0" + dependencies: + humanize-ms: "npm:^1.2.1" + checksum: 10c0/235c182432f75046835b05f239708107138a40103deee23b6a08caee5136873709155753b394ec212e49e60e94a378189562cb01347765515cff61b692c69187 + languageName: node + linkType: hard + "alien-signals@npm:^2.0.5": version: 2.0.7 resolution: "alien-signals@npm:2.0.7" @@ -2597,6 +2634,13 @@ __metadata: languageName: node linkType: hard +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d + languageName: node + linkType: hard + "at-least-node@npm:^1.0.0": version: 1.0.0 resolution: "at-least-node@npm:1.0.0" @@ -3009,6 +3053,15 @@ __metadata: languageName: node linkType: hard +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 + languageName: node + linkType: hard + "commander@npm:^11.1.0": version: 11.1.0 resolution: "commander@npm:11.1.0" @@ -3450,6 +3503,13 @@ __metadata: languageName: node linkType: hard +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 + languageName: node + linkType: hard + "delegates@npm:^1.0.0": version: 1.0.0 resolution: "delegates@npm:1.0.0" @@ -3738,6 +3798,18 @@ __metadata: languageName: node linkType: hard +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.2" + checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af + languageName: node + linkType: hard + "esbuild@npm:^0.20.2 || ^0.21.0 || ^0.22.0 || ^0.23.0": version: 0.23.1 resolution: "esbuild@npm:0.23.1" @@ -4090,6 +4162,36 @@ __metadata: languageName: node linkType: hard +"form-data-encoder@npm:1.7.2": + version: 1.7.2 + resolution: "form-data-encoder@npm:1.7.2" + checksum: 10c0/56553768037b6d55d9de524f97fe70555f0e415e781cb56fc457a68263de3d40fadea2304d4beef2d40b1a851269bd7854e42c362107071892cb5238debe9464 + languageName: node + linkType: hard + +"form-data@npm:^4.0.4": + version: 4.0.4 + resolution: "form-data@npm:4.0.4" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695 + languageName: node + linkType: hard + +"formdata-node@npm:^4.3.2": + version: 4.4.1 + resolution: "formdata-node@npm:4.4.1" + dependencies: + node-domexception: "npm:1.0.0" + web-streams-polyfill: "npm:4.0.0-beta.3" + checksum: 10c0/74151e7b228ffb33b565cec69182694ad07cc3fdd9126a8240468bb70a8ba66e97e097072b60bcb08729b24c7ce3fd3e0bd7f1f80df6f9f662b9656786e76f6a + languageName: node + linkType: hard + "fraction.js@npm:^4.3.7": version: 4.3.7 resolution: "fraction.js@npm:4.3.7" @@ -4186,7 +4288,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: @@ -4541,6 +4643,15 @@ __metadata: languageName: node linkType: hard +"humanize-ms@npm:^1.2.1": + version: 1.2.1 + resolution: "humanize-ms@npm:1.2.1" + dependencies: + ms: "npm:^2.0.0" + checksum: 10c0/f34a2c20161d02303c2807badec2f3b49cbfbbb409abd4f95a07377ae01cfe6b59e3d15ac609cffcd8f2521f0eb37b7e1091acf65da99aa2a4f1ad63c21e7e7a + languageName: node + linkType: hard + "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -5362,7 +5473,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.18, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -5550,7 +5661,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.1.1, ms@npm:^2.1.3": +"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 @@ -5716,6 +5827,13 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b + languageName: node + linkType: hard + "node-fetch-native@npm:^1.6.4, node-fetch-native@npm:^1.6.6, node-fetch-native@npm:^1.6.7": version: 1.6.7 resolution: "node-fetch-native@npm:1.6.7" @@ -6056,9 +6174,17 @@ __metadata: languageName: node linkType: hard -"openai@npm:^5.20.2": - version: 5.20.2 - resolution: "openai@npm:5.20.2" +"openai@npm:^4.66.0": + version: 4.104.0 + resolution: "openai@npm:4.104.0" + dependencies: + "@types/node": "npm:^18.11.18" + "@types/node-fetch": "npm:^2.6.4" + abort-controller: "npm:^3.0.0" + agentkeepalive: "npm:^4.2.1" + form-data-encoder: "npm:1.7.2" + formdata-node: "npm:^4.3.2" + node-fetch: "npm:^2.6.7" peerDependencies: ws: ^8.18.0 zod: ^3.23.8 @@ -6069,7 +6195,7 @@ __metadata: optional: true bin: openai: bin/cli - checksum: 10c0/9a8004d64feadfeba0cc15ccfdce6d1863be06dbaef7c1137aea606a4825657e77803cb106ccaf2913303ee1c1b55bc599fde08afb5158726e69011dfb38f6e3 + checksum: 10c0/c4f2e837684ed96b8cec58c65a584646d667c69918f29052775e2e8c05ff5c860d8b58214a7770bc6895ca8602480420c1db6a5392dd250179eb0b91c2b19a2f languageName: node linkType: hard @@ -6081,7 +6207,7 @@ __metadata: dotenv: "npm:^17.2.2" nuxt: "npm:^4.1.1" nuxt-aos: "npm:1.2.5" - openai: "npm:^5.20.2" + openai: "npm:^4.66.0" vue: "npm:^3.5.21" vue-router: "npm:^4.5.1" vuetify: "npm:3.9.0-beta.1" @@ -8048,6 +8174,20 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 + languageName: node + linkType: hard + +"undici-types@npm:~7.11.0": + version: 7.11.0 + resolution: "undici-types@npm:7.11.0" + checksum: 10c0/24ff69baeaebc7d09f4dae68564df850b4ff561810148ff6c37d3fc64dd1460c55e59e604420495e73b1fb48594a1c98e69f6732086a24e01b88f2d926378af1 + languageName: node + linkType: hard + "unenv@npm:^2.0.0-rc.21": version: 2.0.0-rc.21 resolution: "unenv@npm:2.0.0-rc.21" @@ -8645,6 +8785,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:4.0.0-beta.3": + version: 4.0.0-beta.3 + resolution: "web-streams-polyfill@npm:4.0.0-beta.3" + checksum: 10c0/a9596779db2766990117ed3a158e0b0e9f69b887a6d6ba0779940259e95f99dc3922e534acc3e5a117b5f5905300f527d6fbf8a9f0957faf1d8e585ce3452e8e + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1"