diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f4abd1d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + name: Type-check and build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Type-check + run: npm run check + + - name: Build + run: npm run build diff --git a/client/src/components/shared/LiveAudioDiscernment.tsx b/client/src/components/shared/LiveAudioDiscernment.tsx index 4f4e664..84edba4 100644 --- a/client/src/components/shared/LiveAudioDiscernment.tsx +++ b/client/src/components/shared/LiveAudioDiscernment.tsx @@ -356,7 +356,7 @@ export function LiveAudioDiscernment() { const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); - const streamIntervalRef = useRef(null); + const streamIntervalRef = useRef | null>(null); const handleConsent = () => { localStorage.setItem(CONSENT_STORAGE_KEY, "true"); diff --git a/package.json b/package.json index 6645069..50de9bb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "rest-express", "version": "1.0.0", "type": "module", - "license": "MIT", + "license": "BUSL-1.1", "scripts": { "dev:client": "vite dev --port 5000", "dev": "NODE_ENV=development tsx server/index.ts", diff --git a/server/replit_integrations/audio/index.ts b/server/replit_integrations/audio/index.ts index 8d2a257..0999dd8 100644 --- a/server/replit_integrations/audio/index.ts +++ b/server/replit_integrations/audio/index.ts @@ -1,4 +1,3 @@ -export { registerAudioRoutes } from "./routes"; export { openai, detectAudioFormat, diff --git a/server/replit_integrations/audio/routes.ts b/server/replit_integrations/audio/routes.ts deleted file mode 100644 index 6feaff0..0000000 --- a/server/replit_integrations/audio/routes.ts +++ /dev/null @@ -1,85 +0,0 @@ -import express, { type Express, type Request, type Response } from "express"; -import { chatStorage } from "../chat/storage"; -import { openai, speechToText, ensureCompatibleFormat } from "./client"; - -// Body parser with 50MB limit for audio payloads -const audioBodyParser = express.json({ limit: "50mb" }); - -export function registerAudioRoutes(app: Express): void { - // Send voice message and get streaming audio response - // Auto-detects audio format and converts WebM/MP4/OGG to WAV - // Uses gpt-4o-mini-transcribe for STT, gpt-audio for voice response - app.post("/api/voice-conversations/:id/messages", audioBodyParser, async (req: Request, res: Response) => { - try { - const conversationId = parseInt(req.params.id as string); - const { audio, voice = "alloy" } = req.body; - - if (!audio) { - return res.status(400).json({ error: "Audio data (base64) is required" }); - } - - // 1. Auto-detect format and convert to OpenAI-compatible format - const rawBuffer = Buffer.from(audio, "base64"); - const { buffer: audioBuffer, format: inputFormat } = await ensureCompatibleFormat(rawBuffer); - - // 2. Transcribe user audio - const userTranscript = await speechToText(audioBuffer, inputFormat); - - // 3. Save user message - await chatStorage.createMessage(conversationId, "user", userTranscript); - - // 4. Get conversation history - const existingMessages = await chatStorage.getMessagesByConversation(conversationId); - const chatHistory = existingMessages.map((m) => ({ - role: m.role as "user" | "assistant", - content: m.content, - })); - - // 5. Set up SSE - res.setHeader("Content-Type", "text/event-stream"); - res.setHeader("Cache-Control", "no-cache"); - res.setHeader("Connection", "keep-alive"); - - res.write(`data: ${JSON.stringify({ type: "user_transcript", data: userTranscript })}\n\n`); - - // 6. Stream audio response from gpt-audio - const stream = await openai.chat.completions.create({ - model: "gpt-audio", - modalities: ["text", "audio"], - audio: { voice, format: "pcm16" }, - messages: chatHistory, - stream: true, - }); - - let assistantTranscript = ""; - - for await (const chunk of stream) { - const delta = chunk.choices?.[0]?.delta as any; - if (!delta) continue; - - if (delta?.audio?.transcript) { - assistantTranscript += delta.audio.transcript; - res.write(`data: ${JSON.stringify({ type: "transcript", data: delta.audio.transcript })}\n\n`); - } - - if (delta?.audio?.data) { - res.write(`data: ${JSON.stringify({ type: "audio", data: delta.audio.data })}\n\n`); - } - } - - // 7. Save assistant message - await chatStorage.createMessage(conversationId, "assistant", assistantTranscript); - - res.write(`data: ${JSON.stringify({ type: "done", transcript: assistantTranscript })}\n\n`); - res.end(); - } catch (error) { - console.error("Error processing voice message:", error); - if (res.headersSent) { - res.write(`data: ${JSON.stringify({ type: "error", error: "Failed to process voice message" })}\n\n`); - res.end(); - } else { - res.status(500).json({ error: "Failed to process voice message" }); - } - } - }); -}