From 6d2509c258f35bd110c23cc1c85c8bf7ccf277e4 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 31 Mar 2026 20:16:54 +0200 Subject: [PATCH] feat(memoro): add deployment infrastructure and migrate web services to new Hono server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile for audio-server (Bun + ffmpeg) - docker-compose.macmini.yml entries for memoro-server (3015) and memoro-audio-server (3016) - Dev commands: dev:memoro:server, dev:memoro:audio-server, dev:memoro:app, dev:memoro:full - MEMORO_* env vars in .env.development - web: add PUBLIC_MEMORO_SERVER_URL env var to env.ts and .env.example - web: rewrite transcriptionService → POST /api/v1/memos (new server path) - web: rewrite spaceService → /api/v1/spaces/* (aligned with actual Hono routes) - server: fix callAudioServer param name audioPath (was filePath) in memos.ts Co-Authored-By: Claude Sonnet 4.6 --- .env.development | 35 + apps/memoro/apps/audio-server/Dockerfile | 19 + apps/memoro/apps/server/src/routes/memos.ts | 4 +- apps/memoro/apps/web/.env.example | 5 +- apps/memoro/apps/web/src/lib/config/env.ts | 8 +- .../apps/web/src/lib/services/spaceService.ts | 891 ++++-------------- .../src/lib/services/transcriptionService.ts | 111 +-- docker-compose.macmini.yml | 70 ++ package.json | 5 + 9 files changed, 359 insertions(+), 789 deletions(-) create mode 100644 apps/memoro/apps/audio-server/Dockerfile diff --git a/.env.development b/.env.development index a7fb37182..a68195eb9 100644 --- a/.env.development +++ b/.env.development @@ -420,6 +420,41 @@ CITYCORNERS_BACKEND_PORT=3025 CITYCORNERS_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/citycorners CITYCORNERS_WEB_PORT=5196 +# ============================================ +# MEMORO PROJECT +# ============================================ + +# Server ports +MEMORO_SERVER_PORT=3015 +MEMORO_AUDIO_SERVER_PORT=3016 +MEMORO_SERVER_URL=http://localhost:3015 +MEMORO_AUDIO_SERVER_URL=http://localhost:3016 + +# Shared service key (server ↔ audio-server communication) +MEMORO_SERVICE_KEY=dev-memoro-service-key-change-in-prod + +# Supabase (Memoro has its own Supabase project) +MEMORO_SUPABASE_URL=https://your-memoro-project.supabase.co +MEMORO_SUPABASE_SERVICE_KEY=your-memoro-supabase-service-role-key + +# Azure Speech Services (load-balanced across up to 4 keys) +AZURE_SPEECH_KEY_1=your-azure-speech-key-1 +AZURE_SPEECH_KEY_2= +AZURE_SPEECH_KEY_3= +AZURE_SPEECH_KEY_4= +AZURE_SPEECH_REGION=germanywestcentral +AZURE_SPEECH_ENDPOINT=https://germanywestcentral.api.cognitive.microsoft.com + +# Azure Blob Storage (for batch transcription jobs) +AZURE_STORAGE_ACCOUNT_NAME=your-storage-account +AZURE_STORAGE_ACCOUNT_KEY=your-storage-account-key +AZURE_STORAGE_CONTAINER=memoro-batch-audio + +# Azure OpenAI (headline/Q&A generation fallback) +AZURE_OPENAI_KEY=your-azure-openai-key +AZURE_OPENAI_ENDPOINT=https://memoroseopenai.openai.azure.com/ +AZURE_OPENAI_DEPLOYMENT=gpt-4o + # GPU Server (Windows PC with RTX 3090) GPU_API_KEY=sk-gpu-cf483ede1e05e28fba5e56c94cd3c24e7c245e57816d3e86 GPU_SERVER_URL=https://gpu.mana.how diff --git a/apps/memoro/apps/audio-server/Dockerfile b/apps/memoro/apps/audio-server/Dockerfile new file mode 100644 index 000000000..a4ca04def --- /dev/null +++ b/apps/memoro/apps/audio-server/Dockerfile @@ -0,0 +1,19 @@ +FROM oven/bun:1 AS production + +# Install ffmpeg +RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY package.json bun.lock* ./ +RUN bun install --frozen-lockfile 2>/dev/null || bun install + +COPY src ./src +COPY tsconfig.json ./ + +EXPOSE 3016 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD bun -e "fetch('http://localhost:3016/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +CMD ["bun", "run", "src/index.ts"] diff --git a/apps/memoro/apps/server/src/routes/memos.ts b/apps/memoro/apps/server/src/routes/memos.ts index fb418af6b..d17fc1548 100644 --- a/apps/memoro/apps/server/src/routes/memos.ts +++ b/apps/memoro/apps/server/src/routes/memos.ts @@ -119,7 +119,7 @@ memoRoutes.post('/:id/append', async (c) => { callAudioServer({ memoId, userId, - filePath: body.filePath, + audioPath: body.filePath, duration: body.duration, recordingIndex, recordingLanguages: body.recordingLanguages, @@ -158,7 +158,7 @@ memoRoutes.post('/:id/retry-transcription', async (c) => { await updateMemoProcessingStatus(memoId, 'transcription', 'pending'); queueMicrotask(() => { - callAudioServer({ memoId, userId, filePath, duration }).catch((err) => + callAudioServer({ memoId, userId, audioPath: filePath, duration }).catch((err) => console.error(`[memos] Retry transcription failed: ${err}`) ); }); diff --git a/apps/memoro/apps/web/.env.example b/apps/memoro/apps/web/.env.example index 4d0d0aa90..630b31e6b 100644 --- a/apps/memoro/apps/web/.env.example +++ b/apps/memoro/apps/web/.env.example @@ -5,7 +5,10 @@ PUBLIC_SUPABASE_URL=your-supabase-url PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key -# Middleware Service URLs (required for transcription) +# Memoro Server (new Hono/Bun backend — replaces middleware) +PUBLIC_MEMORO_SERVER_URL=http://localhost:3015 + +# Middleware Service URLs (legacy — kept during transition) PUBLIC_MEMORO_MIDDLEWARE_URL=https://your-memoro-middleware-url PUBLIC_MANA_MIDDLEWARE_URL=https://your-mana-middleware-url PUBLIC_MIDDLEWARE_APP_ID=your-middleware-app-id diff --git a/apps/memoro/apps/web/src/lib/config/env.ts b/apps/memoro/apps/web/src/lib/config/env.ts index 912016f9e..9532fd3f0 100644 --- a/apps/memoro/apps/web/src/lib/config/env.ts +++ b/apps/memoro/apps/web/src/lib/config/env.ts @@ -7,6 +7,7 @@ import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, PUBLIC_MEMORO_MIDDLEWARE_URL, + PUBLIC_MEMORO_SERVER_URL, PUBLIC_MANA_MIDDLEWARE_URL, PUBLIC_MIDDLEWARE_APP_ID, PUBLIC_STORAGE_BUCKET, @@ -25,7 +26,12 @@ export const env = { anonKey: PUBLIC_SUPABASE_ANON_KEY, }, - // Middleware APIs + // API servers + server: { + memoroUrl: PUBLIC_MEMORO_SERVER_URL, + }, + + // Middleware APIs (legacy — kept for authService compatibility during migration) middleware: { memoroUrl: PUBLIC_MEMORO_MIDDLEWARE_URL, manaUrl: PUBLIC_MANA_MIDDLEWARE_URL, diff --git a/apps/memoro/apps/web/src/lib/services/spaceService.ts b/apps/memoro/apps/web/src/lib/services/spaceService.ts index 4cc233f31..b072d0f73 100644 --- a/apps/memoro/apps/web/src/lib/services/spaceService.ts +++ b/apps/memoro/apps/web/src/lib/services/spaceService.ts @@ -1,13 +1,14 @@ /** * Space Service for Memoro Web - * Handles space management and collaboration features - * - * Pattern adapted from memoro_app/features/spaces/services/spaceService.ts + * Handles space management and collaboration via memoro-server (Hono/Bun). */ import { env } from '$lib/config/env'; +const API = () => env.server.memoroUrl.replace(/\/$/, ''); + // Types + export interface Memo { id: string; title: string; @@ -60,15 +61,11 @@ export interface Space { }; }; }; - apps?: { - name: string; - slug: string; - }; + apps?: { name: string; slug: string }; } export interface CreateSpaceRequest { name: string; - appId?: string; description?: string; color?: string; } @@ -79,702 +76,196 @@ export interface UpdateSpaceRequest { color?: string; } -class SpaceService { - private apiUrl: string; - private appId: string; +// ── Helpers ──────────────────────────────────────────────────────────────────── - constructor() { - this.apiUrl = env.middleware.memoroUrl.replace(/\/$/, ''); - this.appId = env.middleware.appId; - } +function headers(token: string) { + return { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; +} - /** - * Get all spaces for current user from the unified API - */ - async getSpaces(appToken: string): Promise { +async function throwOnError(response: Response, fallback: string) { + if (!response.ok) { + let msg = fallback; try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - const response = await fetch(`${this.apiUrl}/memoro/spaces`, { - method: 'GET', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `Error fetching spaces: ${response.statusText}`); - } - - const data = await response.json(); - console.log(data); - - // Transform the response to match our Space interface - return data.spaces.map((space: any) => ({ - id: space.id, - name: space.name, - description: space.description || '', - memoCount: space.memo_count || 0, - isDefault: space.is_default || false, - color: space.color || '#4CAF50', - created_at: space.created_at, - updated_at: space.updated_at, - owner_id: space.owner_id, - app_id: space.app_id, - credits: space.credits || 0, - roles: space.roles, - isOwner: space.isOwner || false, - })); - } catch (error) { - console.error('Failed to fetch spaces:', error); - throw error; - } - } - - /** - * Get a specific space by ID - */ - async getSpace(spaceId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, { - method: 'GET', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `Error fetching space: ${response.statusText}`); - } - - const data = await response.json(); - - // Transform the response to match our Space interface - return { - id: data.space.id, - name: data.space.name, - description: data.space.description || '', - memoCount: data.space.memo_count || 0, - isDefault: data.space.is_default || false, - color: data.space.color || '#4CAF50', - created_at: data.space.created_at, - updated_at: data.space.updated_at, - owner_id: data.space.owner_id, - app_id: data.space.app_id, - credits: data.space.credits || 0, - roles: data.space.roles, - apps: data.space.apps, - isOwner: data.space.isOwner || false, - }; - } catch (error) { - console.error(`Failed to fetch space ${spaceId}:`, error); - throw error; - } - } - - /** - * Create a new space - */ - async createSpace(spaceData: CreateSpaceRequest, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - // Ensure appId is set - if (!spaceData.appId) { - spaceData.appId = this.appId; - } - - // Prepare request data according to API docs - const requestData = { - name: spaceData.name, - }; - - const response = await fetch(`${this.apiUrl}/memoro/spaces`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestData), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `Error creating space: ${response.statusText}`); - } - - const data = await response.json(); - console.log('Create space response:', JSON.stringify(data, null, 2)); - - // Handle both formats: {success, spaceId} or {space: {success, spaceId}} - const success = data.success || (data.space && data.space.success); - const spaceId = data.spaceId || (data.space && data.space.spaceId); - - if (!success) { - throw new Error(data.error || (data.space && data.space.error) || 'Failed to create space'); - } - - if (!spaceId) { - throw new Error('No space ID returned from server'); - } - - try { - // Try to get the new space details - return await this.getSpace(spaceId, appToken); - } catch (fetchError) { - console.log('Could not fetch newly created space details. Creating fallback space object.'); - - // Create a fallback space object with minimal information - return { - id: spaceId, - name: spaceData.name, - description: '', - memoCount: 0, - isDefault: false, - color: spaceData.color || '#4CAF50', - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }; - } - } catch (error) { - console.error('Failed to create space:', error); - throw error; - } - } - - /** - * Update an existing space - */ - async updateSpace( - spaceId: string, - spaceData: UpdateSpaceRequest, - appToken: string - ): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, { - method: 'PUT', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(spaceData), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `Error updating space: ${response.statusText}`); - } - - const data = await response.json(); - - if (!data.success) { - throw new Error(data.error || 'Failed to update space'); - } - - // Get the updated space details - return this.getSpace(spaceId, appToken); - } catch (error) { - console.error(`Failed to update space ${spaceId}:`, error); - throw error; - } - } - - /** - * Delete a space - */ - async deleteSpace(spaceId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.log(`Deleting space at: ${this.apiUrl}/memoro/spaces/${spaceId}`); - - const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - console.error(`Error response status: ${response.status}`); - let errorMessage = `Error deleting space: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - const data = await response.json(); - - if (!data.success) { - throw new Error(data.error || 'Failed to delete space'); - } - - return true; - } catch (error) { - console.error(`Failed to delete space ${spaceId}:`, error); - throw error; - } - } - - /** - * Leave a space (for non-owners) - */ - async leaveSpace(spaceId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.log(`Leaving space at: ${this.apiUrl}/memoro/spaces/${spaceId}/leave`); - - const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/leave`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - console.error(`Error response status: ${response.status}`); - let errorMessage = `Error leaving space: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - const data = await response.json(); - - if (!data.success) { - throw new Error(data.error || 'Failed to leave space'); - } - - return true; - } catch (error) { - console.error(`Failed to leave space ${spaceId}:`, error); - throw error; - } - } - - /** - * Get all memos for a specific space - */ - async getSpaceMemos(spaceId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.debug( - `Fetching memos for space ${spaceId} from ${this.apiUrl}/memoro/spaces/${spaceId}/memos` - ); - - const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/memos`, { - method: 'GET', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - let errorMessage = `Error fetching space memos: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.error || errorMessage; - } catch (jsonError) { - // If response is not JSON, use the status text - } - throw new Error(errorMessage); - } - - const data = await response.json(); - return data.memos || []; - } catch (error) { - console.error(`Failed to fetch memos for space ${spaceId}:`, error); - throw error; - } - } - - /** - * Link a memo to a space - */ - async linkMemoToSpace(memoId: string, spaceId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.log(`Linking memo ${memoId} to space ${spaceId}`); - - const response = await fetch(`${this.apiUrl}/memoro/link-memo`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ memoId, spaceId }), - }); - - if (!response.ok) { - console.error(`Error response status: ${response.status}`); - let errorMessage = `Error linking memo to space: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - const data = await response.json(); - return data.success; - } catch (error) { - console.error(`Failed to link memo ${memoId} to space ${spaceId}:`, error); - throw error; - } - } - - /** - * Unlink a memo from a space - */ - async unlinkMemoFromSpace(memoId: string, spaceId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.log(`Unlinking memo ${memoId} from space ${spaceId}`); - - const response = await fetch(`${this.apiUrl}/memoro/unlink-memo`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ memoId, spaceId }), - }); - - if (!response.ok) { - console.error(`Error response status: ${response.status}`); - let errorMessage = `Error unlinking memo from space: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - const data = await response.json(); - return data.success; - } catch (error) { - console.error(`Failed to unlink memo ${memoId} from space ${spaceId}:`, error); - throw error; - } - } - - /** - * Get all pending invites for a space - */ - async getSpaceInvites(spaceId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.debug(`Fetching invites for space ${spaceId}`); - - const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/invites`, { - method: 'GET', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - let errorMessage = `Failed to get space invites: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.message || errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - const data = await response.json(); - return data.invites || []; - } catch (error) { - console.error(`Failed to get invites for space ${spaceId}:`, error); - throw error; - } - } - - /** - * Invite a user to a space by email - */ - async inviteUserToSpace( - spaceId: string, - email: string, - role: string, - appToken: string - ): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.debug(`Inviting user ${email} to space ${spaceId} with role ${role}`); - - const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/invite`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, role }), - }); - - if (!response.ok) { - let errorMessage = `Failed to invite user to space: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.message || errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - const data = await response.json(); - return data.inviteId; - } catch (error) { - console.error(`Failed to invite user to space ${spaceId}:`, error); - throw error; - } - } - - /** - * Resend a space invitation - */ - async resendInvite(inviteId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.debug(`Resending invite ${inviteId}`); - - const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/${inviteId}/resend`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - let errorMessage = `Failed to resend invitation: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.message || errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - return true; - } catch (error) { - console.error(`Failed to resend invite ${inviteId}:`, error); - throw error; - } - } - - /** - * Accept a space invitation - */ - async acceptInvite(inviteId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.debug(`Accepting invite ${inviteId}`); - - const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/accept`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ inviteId }), - }); - - if (!response.ok) { - let errorMessage = `Failed to accept invitation: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.message || errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - return true; - } catch (error) { - console.error(`Failed to accept invite ${inviteId}:`, error); - throw error; - } - } - - /** - * Decline a space invitation - */ - async declineInvite(inviteId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.debug(`Declining invite ${inviteId}`); - - const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/decline`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ inviteId }), - }); - - if (!response.ok) { - let errorMessage = `Failed to decline invitation: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.message || errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - return true; - } catch (error) { - console.error(`Failed to decline invite ${inviteId}:`, error); - throw error; - } - } - - /** - * Cancel a space invitation (for space owners/admins) - */ - async cancelInvite(inviteId: string, appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.debug(`Canceling invite ${inviteId}`); - - const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/cancel`, { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ inviteId }), - }); - - if (!response.ok) { - let errorMessage = `Failed to cancel invitation: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.message || errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - return true; - } catch (error) { - console.error(`Failed to cancel invite ${inviteId}:`, error); - throw error; - } - } - - /** - * Get all invitations for the current user - */ - async getUserInvites(appToken: string): Promise { - try { - if (!appToken) { - throw new Error('Not authenticated'); - } - - console.debug('Getting user invites'); - - const response = await fetch(`${this.apiUrl}/memoro/invites/pending`, { - method: 'GET', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - let errorMessage = `Failed to get user invitations: ${response.statusText}`; - try { - const errorData = await response.json(); - errorMessage = errorData.message || errorData.error || errorMessage; - } catch (jsonError) { - // If we can't parse JSON, just use the status text - } - throw new Error(errorMessage); - } - - const data = await response.json(); - console.log('User invites:', data); - return data.invites || []; - } catch (error) { - console.error('Failed to get user invites:', error); - throw error; - } + const d = await response.json(); + msg = d.error || d.message || msg; + } catch {} + throw new Error(msg); + } +} + +function mapSpace(s: any): Space { + return { + id: s.id, + name: s.name, + description: s.description || '', + memoCount: s.memo_count || 0, + isDefault: s.is_default || false, + color: s.color || '#4CAF50', + created_at: s.created_at, + updated_at: s.updated_at, + owner_id: s.owner_id, + app_id: s.app_id, + credits: s.credits || 0, + roles: s.roles, + apps: s.apps, + isOwner: s.isOwner || false, + }; +} + +// ── Service ──────────────────────────────────────────────────────────────────── + +class SpaceService { + async getSpaces(token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces`, { headers: headers(token) }); + await throwOnError(r, 'Error fetching spaces'); + const d = await r.json(); + return (d.spaces ?? []).map(mapSpace); + } + + async getSpace(spaceId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/${spaceId}`, { headers: headers(token) }); + await throwOnError(r, 'Error fetching space'); + const d = await r.json(); + return mapSpace(d.space); + } + + async createSpace(spaceData: CreateSpaceRequest, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces`, { + method: 'POST', + headers: headers(token), + body: JSON.stringify({ name: spaceData.name, description: spaceData.description }), + }); + await throwOnError(r, 'Error creating space'); + const d = await r.json(); + return mapSpace(d.space); + } + + async updateSpace(spaceId: string, spaceData: UpdateSpaceRequest, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/${spaceId}`, { + method: 'PUT', + headers: headers(token), + body: JSON.stringify(spaceData), + }); + await throwOnError(r, 'Error updating space'); + return this.getSpace(spaceId, token); + } + + async deleteSpace(spaceId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/${spaceId}`, { + method: 'DELETE', + headers: headers(token), + }); + await throwOnError(r, 'Error deleting space'); + return true; + } + + async leaveSpace(spaceId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/leave`, { + method: 'POST', + headers: headers(token), + }); + await throwOnError(r, 'Error leaving space'); + return true; + } + + async getSpaceMemos(spaceId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/memos`, { + headers: headers(token), + }); + await throwOnError(r, 'Error fetching space memos'); + const d = await r.json(); + return d.memos ?? []; + } + + async linkMemoToSpace(memoId: string, spaceId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/memos/link`, { + method: 'POST', + headers: headers(token), + body: JSON.stringify({ memoId }), + }); + await throwOnError(r, 'Error linking memo to space'); + return true; + } + + async unlinkMemoFromSpace(memoId: string, spaceId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/memos/unlink`, { + method: 'POST', + headers: headers(token), + body: JSON.stringify({ memoId }), + }); + await throwOnError(r, 'Error unlinking memo from space'); + return true; + } + + async getSpaceInvites(spaceId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/invites`, { + headers: headers(token), + }); + await throwOnError(r, 'Error fetching space invites'); + const d = await r.json(); + return d.invites ?? []; + } + + async inviteUserToSpace( + spaceId: string, + email: string, + _role: string, + token: string + ): Promise { + const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/invite`, { + method: 'POST', + headers: headers(token), + body: JSON.stringify({ email }), + }); + await throwOnError(r, 'Error inviting user to space'); + const d = await r.json(); + return d.invite?.id ?? d.inviteId; + } + + async resendInvite(inviteId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/invites/${inviteId}/resend`, { + method: 'POST', + headers: headers(token), + }); + await throwOnError(r, 'Error resending invite'); + return true; + } + + async cancelInvite(inviteId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/spaces/invites/${inviteId}`, { + method: 'DELETE', + headers: headers(token), + }); + await throwOnError(r, 'Error canceling invite'); + return true; + } + + async acceptInvite(inviteId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/invites/accept`, { + method: 'POST', + headers: headers(token), + body: JSON.stringify({ inviteId }), + }); + await throwOnError(r, 'Error accepting invite'); + return true; + } + + async declineInvite(inviteId: string, token: string): Promise { + const r = await fetch(`${API()}/api/v1/invites/decline`, { + method: 'POST', + headers: headers(token), + body: JSON.stringify({ inviteId }), + }); + await throwOnError(r, 'Error declining invite'); + return true; + } + + async getUserInvites(token: string): Promise { + const r = await fetch(`${API()}/api/v1/invites/pending`, { headers: headers(token) }); + await throwOnError(r, 'Error fetching user invites'); + const d = await r.json(); + return d.invites ?? []; } } -// Export singleton instance export const spaceService = new SpaceService(); diff --git a/apps/memoro/apps/web/src/lib/services/transcriptionService.ts b/apps/memoro/apps/web/src/lib/services/transcriptionService.ts index 1e7bfc481..9004cbd2d 100644 --- a/apps/memoro/apps/web/src/lib/services/transcriptionService.ts +++ b/apps/memoro/apps/web/src/lib/services/transcriptionService.ts @@ -1,17 +1,12 @@ /** * Transcription Service for Memoro Web - * Handles audio transcription via memoro-service middleware - * - * Pattern adapted from memoro_app/features/storage/transcriptionUtils.ts + * Triggers transcription via the new Hono/Bun memoro-server. */ import { env } from '$lib/config/env'; -const MEMORO_SERVICE_URL = env.middleware.memoroUrl.replace(/\/$/, ''); +const SERVER_URL = env.server.memoroUrl.replace(/\/$/, ''); -/** - * Enhanced transcription result with network error information - */ export interface TranscriptionRequestResult { success: boolean; error?: string; @@ -32,44 +27,6 @@ export interface TranscriptionParams { mediaType?: 'audio' | 'video'; } -/** - * Transcribes via Memoro Service (which handles intelligent routing) - */ -async function transcribeViaMemoryService( - audioPath: string, - appToken: string, - duration: number, - memoId?: string, - spaceId?: string, - recordingLanguages?: string[], - title?: string, - blueprintId?: string, - mediaType?: 'audio' | 'video' -): Promise { - console.debug('🎯 Using Memoro Service for intelligent transcription routing'); - - return fetch(`${MEMORO_SERVICE_URL}/memoro/process-uploaded-audio`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${appToken}`, - }, - body: JSON.stringify({ - filePath: audioPath, - duration, - memoId, - spaceId, - recordingLanguages, - title, - blueprintId, - mediaType, - }), - }); -} - -/** - * Triggers transcription via memoro-service (which handles intelligent routing) - */ export async function triggerTranscription({ userId, fileName, @@ -79,63 +36,47 @@ export async function triggerTranscription({ recordingLanguages, title, blueprintId, - appToken, + accessToken, mediaType, -}: TranscriptionParams & { appToken: string }): Promise { +}: TranscriptionParams & { accessToken: string }): Promise { try { - if (!appToken) { - const errorMsg = 'No authenticated token found'; - return { success: false, error: errorMsg }; + if (!accessToken) { + return { success: false, error: 'No authenticated token found' }; } - const audioPath = `${userId}/${fileName}`; + const filePath = `${userId}/${fileName}`; - console.debug('🎯 Triggering transcription with:', { - audioPath, - duration, - memoId, - spaceId, - title, - blueprintId, - recordingLanguages, + const response = await fetch(`${SERVER_URL}/api/v1/memos`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + filePath, + duration, + memoId, + spaceId, + recordingLanguages, + title, + blueprintId, + mediaType, + }), }); - // Let memoro-service handle the intelligent routing - const transcribeResponse = await transcribeViaMemoryService( - audioPath, - appToken, - duration, - memoId, - spaceId, - recordingLanguages, - title, - blueprintId, - mediaType - ); - - // Handle response - if (!transcribeResponse.ok) { - const errorText = await transcribeResponse.text(); - console.debug('Error calling transcription via Memoro Service:', { - status: transcribeResponse.status, - statusText: transcribeResponse.statusText, - errorText, - }); - + if (!response.ok) { + const errorText = await response.text(); return { success: false, error: errorText, - isNetworkError: transcribeResponse.status >= 500, + isNetworkError: response.status >= 500, userMessage: 'Transcription could not be started', technicalMessage: errorText, }; } - console.debug('✅ Transcription started successfully via Memoro Service'); return { success: true }; } catch (error) { - console.debug('Error triggering transcription:', error); - return { success: false, error: String(error), diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index 3b1e886c9..17664b2d8 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -1455,6 +1455,76 @@ services: retries: 3 start_period: 45s + memoro-server: + build: + context: . + dockerfile: docker/Dockerfile.hono-server + args: + APP: memoro + image: memoro-server:local + container_name: mana-app-memoro-server + restart: always + mem_limit: 256m + depends_on: + mana-auth: + condition: service_healthy + environment: + NODE_ENV: production + PORT: 3015 + MANA_CORE_AUTH_URL: http://mana-auth:3001 + CORS_ORIGINS: http://memoro-web:5038,https://memoro.mana.how + MEMORO_SUPABASE_URL: ${MEMORO_SUPABASE_URL} + MEMORO_SUPABASE_SERVICE_KEY: ${MEMORO_SUPABASE_SERVICE_KEY} + SERVICE_KEY: ${MEMORO_SERVICE_KEY} + AUDIO_SERVER_URL: http://memoro-audio-server:3016 + GEMINI_API_KEY: ${GEMINI_API_KEY} + AZURE_OPENAI_KEY: ${AZURE_OPENAI_KEY} + AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT} + AZURE_OPENAI_DEPLOYMENT: ${AZURE_OPENAI_DEPLOYMENT} + MANA_CREDITS_URL: http://mana-credits:3006 + MANA_CREDITS_SERVICE_KEY: ${MANA_CREDITS_SERVICE_KEY} + ports: + - "3015:3015" + healthcheck: + test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3015/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + + memoro-audio-server: + build: + context: apps/memoro/apps/audio-server + dockerfile: Dockerfile + image: memoro-audio-server:local + container_name: mana-app-memoro-audio-server + restart: always + mem_limit: 512m + environment: + NODE_ENV: production + PORT: 3016 + SERVICE_KEY: ${MEMORO_SERVICE_KEY} + MEMORO_SERVER_URL: http://memoro-server:3015 + MEMORO_SUPABASE_URL: ${MEMORO_SUPABASE_URL} + MEMORO_SUPABASE_SERVICE_KEY: ${MEMORO_SUPABASE_SERVICE_KEY} + AZURE_SPEECH_KEY_1: ${AZURE_SPEECH_KEY_1} + AZURE_SPEECH_KEY_2: ${AZURE_SPEECH_KEY_2} + AZURE_SPEECH_KEY_3: ${AZURE_SPEECH_KEY_3} + AZURE_SPEECH_KEY_4: ${AZURE_SPEECH_KEY_4} + AZURE_SPEECH_REGION: ${AZURE_SPEECH_REGION:-germanywestcentral} + AZURE_SPEECH_ENDPOINT: ${AZURE_SPEECH_ENDPOINT} + AZURE_STORAGE_ACCOUNT_NAME: ${AZURE_STORAGE_ACCOUNT_NAME} + AZURE_STORAGE_ACCOUNT_KEY: ${AZURE_STORAGE_ACCOUNT_KEY} + AZURE_STORAGE_CONTAINER: ${AZURE_STORAGE_CONTAINER:-memoro-batch-audio} + ports: + - "3016:3016" + healthcheck: + test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3016/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + mana-llm: build: context: ./services/mana-llm diff --git a/package.json b/package.json index fe56a0a13..49e4ea0bb 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,11 @@ "dev:todo:app": "concurrently -n server,web -c yellow,cyan \"pnpm dev:todo:server\" \"pnpm dev:todo:web\"", "dev:todo:full": "./scripts/setup-databases.sh todo && ./scripts/setup-databases.sh auth && concurrently -n auth,sync,server,web -c blue,magenta,yellow,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:todo:server\" \"pnpm dev:todo:web\"", "dev:todo:local": "concurrently -n sync,server,web -c magenta,yellow,cyan \"pnpm dev:sync\" \"pnpm dev:todo:server\" \"pnpm dev:todo:web\"", + "dev:memoro:web": "pnpm --filter @memoro/web dev", + "dev:memoro:server": "cd apps/memoro/apps/server && bun run --watch src/index.ts", + "dev:memoro:audio-server": "cd apps/memoro/apps/audio-server && bun run --watch src/index.ts", + "dev:memoro:app": "concurrently -n server,audio,web -c yellow,green,cyan \"pnpm dev:memoro:server\" \"pnpm dev:memoro:audio-server\" \"pnpm dev:memoro:web\"", + "dev:memoro:full": "concurrently -n auth,server,audio,web -c blue,yellow,green,cyan \"pnpm dev:auth\" \"pnpm dev:memoro:server\" \"pnpm dev:memoro:audio-server\" \"pnpm dev:memoro:web\"", "dev:uload:web": "pnpm --filter @uload/web dev", "dev:uload:server": "cd apps/uload/apps/server && bun run --watch src/index.ts", "dev:uload:landing": "pnpm --filter @uload/landing dev",