mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(memoro): add deployment infrastructure and migrate web services to new Hono server
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
6e0dd0c065
commit
6d2509c258
9 changed files with 359 additions and 789 deletions
|
|
@ -420,6 +420,41 @@ CITYCORNERS_BACKEND_PORT=3025
|
||||||
CITYCORNERS_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/citycorners
|
CITYCORNERS_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/citycorners
|
||||||
CITYCORNERS_WEB_PORT=5196
|
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 Server (Windows PC with RTX 3090)
|
||||||
GPU_API_KEY=sk-gpu-cf483ede1e05e28fba5e56c94cd3c24e7c245e57816d3e86
|
GPU_API_KEY=sk-gpu-cf483ede1e05e28fba5e56c94cd3c24e7c245e57816d3e86
|
||||||
GPU_SERVER_URL=https://gpu.mana.how
|
GPU_SERVER_URL=https://gpu.mana.how
|
||||||
|
|
|
||||||
19
apps/memoro/apps/audio-server/Dockerfile
Normal file
19
apps/memoro/apps/audio-server/Dockerfile
Normal file
|
|
@ -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"]
|
||||||
|
|
@ -119,7 +119,7 @@ memoRoutes.post('/:id/append', async (c) => {
|
||||||
callAudioServer({
|
callAudioServer({
|
||||||
memoId,
|
memoId,
|
||||||
userId,
|
userId,
|
||||||
filePath: body.filePath,
|
audioPath: body.filePath,
|
||||||
duration: body.duration,
|
duration: body.duration,
|
||||||
recordingIndex,
|
recordingIndex,
|
||||||
recordingLanguages: body.recordingLanguages,
|
recordingLanguages: body.recordingLanguages,
|
||||||
|
|
@ -158,7 +158,7 @@ memoRoutes.post('/:id/retry-transcription', async (c) => {
|
||||||
await updateMemoProcessingStatus(memoId, 'transcription', 'pending');
|
await updateMemoProcessingStatus(memoId, 'transcription', 'pending');
|
||||||
|
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
callAudioServer({ memoId, userId, filePath, duration }).catch((err) =>
|
callAudioServer({ memoId, userId, audioPath: filePath, duration }).catch((err) =>
|
||||||
console.error(`[memos] Retry transcription failed: ${err}`)
|
console.error(`[memos] Retry transcription failed: ${err}`)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@
|
||||||
PUBLIC_SUPABASE_URL=your-supabase-url
|
PUBLIC_SUPABASE_URL=your-supabase-url
|
||||||
PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
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_MEMORO_MIDDLEWARE_URL=https://your-memoro-middleware-url
|
||||||
PUBLIC_MANA_MIDDLEWARE_URL=https://your-mana-middleware-url
|
PUBLIC_MANA_MIDDLEWARE_URL=https://your-mana-middleware-url
|
||||||
PUBLIC_MIDDLEWARE_APP_ID=your-middleware-app-id
|
PUBLIC_MIDDLEWARE_APP_ID=your-middleware-app-id
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
PUBLIC_SUPABASE_URL,
|
PUBLIC_SUPABASE_URL,
|
||||||
PUBLIC_SUPABASE_ANON_KEY,
|
PUBLIC_SUPABASE_ANON_KEY,
|
||||||
PUBLIC_MEMORO_MIDDLEWARE_URL,
|
PUBLIC_MEMORO_MIDDLEWARE_URL,
|
||||||
|
PUBLIC_MEMORO_SERVER_URL,
|
||||||
PUBLIC_MANA_MIDDLEWARE_URL,
|
PUBLIC_MANA_MIDDLEWARE_URL,
|
||||||
PUBLIC_MIDDLEWARE_APP_ID,
|
PUBLIC_MIDDLEWARE_APP_ID,
|
||||||
PUBLIC_STORAGE_BUCKET,
|
PUBLIC_STORAGE_BUCKET,
|
||||||
|
|
@ -25,7 +26,12 @@ export const env = {
|
||||||
anonKey: PUBLIC_SUPABASE_ANON_KEY,
|
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: {
|
middleware: {
|
||||||
memoroUrl: PUBLIC_MEMORO_MIDDLEWARE_URL,
|
memoroUrl: PUBLIC_MEMORO_MIDDLEWARE_URL,
|
||||||
manaUrl: PUBLIC_MANA_MIDDLEWARE_URL,
|
manaUrl: PUBLIC_MANA_MIDDLEWARE_URL,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
/**
|
/**
|
||||||
* Space Service for Memoro Web
|
* Space Service for Memoro Web
|
||||||
* Handles space management and collaboration features
|
* Handles space management and collaboration via memoro-server (Hono/Bun).
|
||||||
*
|
|
||||||
* Pattern adapted from memoro_app/features/spaces/services/spaceService.ts
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { env } from '$lib/config/env';
|
import { env } from '$lib/config/env';
|
||||||
|
|
||||||
|
const API = () => env.server.memoroUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
export interface Memo {
|
export interface Memo {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -60,15 +61,11 @@ export interface Space {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
apps?: {
|
apps?: { name: string; slug: string };
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateSpaceRequest {
|
export interface CreateSpaceRequest {
|
||||||
name: string;
|
name: string;
|
||||||
appId?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -79,702 +76,196 @@ export interface UpdateSpaceRequest {
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpaceService {
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||||
private apiUrl: string;
|
|
||||||
private appId: string;
|
|
||||||
|
|
||||||
constructor() {
|
function headers(token: string) {
|
||||||
this.apiUrl = env.middleware.memoroUrl.replace(/\/$/, '');
|
return { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
||||||
this.appId = env.middleware.appId;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
async function throwOnError(response: Response, fallback: string) {
|
||||||
* Get all spaces for current user from the unified API
|
if (!response.ok) {
|
||||||
*/
|
let msg = fallback;
|
||||||
async getSpaces(appToken: string): Promise<Space[]> {
|
|
||||||
try {
|
try {
|
||||||
if (!appToken) {
|
const d = await response.json();
|
||||||
throw new Error('Not authenticated');
|
msg = d.error || d.message || msg;
|
||||||
}
|
} catch {}
|
||||||
|
throw new Error(msg);
|
||||||
const response = await fetch(`${this.apiUrl}/memoro/spaces`, {
|
}
|
||||||
method: 'GET',
|
}
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${appToken}`,
|
function mapSpace(s: any): Space {
|
||||||
'Content-Type': 'application/json',
|
return {
|
||||||
},
|
id: s.id,
|
||||||
});
|
name: s.name,
|
||||||
|
description: s.description || '',
|
||||||
if (!response.ok) {
|
memoCount: s.memo_count || 0,
|
||||||
const errorData = await response.json();
|
isDefault: s.is_default || false,
|
||||||
throw new Error(errorData.error || `Error fetching spaces: ${response.statusText}`);
|
color: s.color || '#4CAF50',
|
||||||
}
|
created_at: s.created_at,
|
||||||
|
updated_at: s.updated_at,
|
||||||
const data = await response.json();
|
owner_id: s.owner_id,
|
||||||
console.log(data);
|
app_id: s.app_id,
|
||||||
|
credits: s.credits || 0,
|
||||||
// Transform the response to match our Space interface
|
roles: s.roles,
|
||||||
return data.spaces.map((space: any) => ({
|
apps: s.apps,
|
||||||
id: space.id,
|
isOwner: s.isOwner || false,
|
||||||
name: space.name,
|
};
|
||||||
description: space.description || '',
|
}
|
||||||
memoCount: space.memo_count || 0,
|
|
||||||
isDefault: space.is_default || false,
|
// ── Service ────────────────────────────────────────────────────────────────────
|
||||||
color: space.color || '#4CAF50',
|
|
||||||
created_at: space.created_at,
|
class SpaceService {
|
||||||
updated_at: space.updated_at,
|
async getSpaces(token: string): Promise<Space[]> {
|
||||||
owner_id: space.owner_id,
|
const r = await fetch(`${API()}/api/v1/spaces`, { headers: headers(token) });
|
||||||
app_id: space.app_id,
|
await throwOnError(r, 'Error fetching spaces');
|
||||||
credits: space.credits || 0,
|
const d = await r.json();
|
||||||
roles: space.roles,
|
return (d.spaces ?? []).map(mapSpace);
|
||||||
isOwner: space.isOwner || false,
|
}
|
||||||
}));
|
|
||||||
} catch (error) {
|
async getSpace(spaceId: string, token: string): Promise<Space> {
|
||||||
console.error('Failed to fetch spaces:', error);
|
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}`, { headers: headers(token) });
|
||||||
throw error;
|
await throwOnError(r, 'Error fetching space');
|
||||||
}
|
const d = await r.json();
|
||||||
}
|
return mapSpace(d.space);
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Get a specific space by ID
|
async createSpace(spaceData: CreateSpaceRequest, token: string): Promise<Space> {
|
||||||
*/
|
const r = await fetch(`${API()}/api/v1/spaces`, {
|
||||||
async getSpace(spaceId: string, appToken: string): Promise<Space> {
|
method: 'POST',
|
||||||
try {
|
headers: headers(token),
|
||||||
if (!appToken) {
|
body: JSON.stringify({ name: spaceData.name, description: spaceData.description }),
|
||||||
throw new Error('Not authenticated');
|
});
|
||||||
}
|
await throwOnError(r, 'Error creating space');
|
||||||
|
const d = await r.json();
|
||||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, {
|
return mapSpace(d.space);
|
||||||
method: 'GET',
|
}
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${appToken}`,
|
async updateSpace(spaceId: string, spaceData: UpdateSpaceRequest, token: string): Promise<Space> {
|
||||||
'Content-Type': 'application/json',
|
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}`, {
|
||||||
},
|
method: 'PUT',
|
||||||
});
|
headers: headers(token),
|
||||||
|
body: JSON.stringify(spaceData),
|
||||||
if (!response.ok) {
|
});
|
||||||
const errorData = await response.json();
|
await throwOnError(r, 'Error updating space');
|
||||||
throw new Error(errorData.error || `Error fetching space: ${response.statusText}`);
|
return this.getSpace(spaceId, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
async deleteSpace(spaceId: string, token: string): Promise<boolean> {
|
||||||
|
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}`, {
|
||||||
// Transform the response to match our Space interface
|
method: 'DELETE',
|
||||||
return {
|
headers: headers(token),
|
||||||
id: data.space.id,
|
});
|
||||||
name: data.space.name,
|
await throwOnError(r, 'Error deleting space');
|
||||||
description: data.space.description || '',
|
return true;
|
||||||
memoCount: data.space.memo_count || 0,
|
}
|
||||||
isDefault: data.space.is_default || false,
|
|
||||||
color: data.space.color || '#4CAF50',
|
async leaveSpace(spaceId: string, token: string): Promise<boolean> {
|
||||||
created_at: data.space.created_at,
|
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/leave`, {
|
||||||
updated_at: data.space.updated_at,
|
method: 'POST',
|
||||||
owner_id: data.space.owner_id,
|
headers: headers(token),
|
||||||
app_id: data.space.app_id,
|
});
|
||||||
credits: data.space.credits || 0,
|
await throwOnError(r, 'Error leaving space');
|
||||||
roles: data.space.roles,
|
return true;
|
||||||
apps: data.space.apps,
|
}
|
||||||
isOwner: data.space.isOwner || false,
|
|
||||||
};
|
async getSpaceMemos(spaceId: string, token: string): Promise<Memo[]> {
|
||||||
} catch (error) {
|
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/memos`, {
|
||||||
console.error(`Failed to fetch space ${spaceId}:`, error);
|
headers: headers(token),
|
||||||
throw error;
|
});
|
||||||
}
|
await throwOnError(r, 'Error fetching space memos');
|
||||||
}
|
const d = await r.json();
|
||||||
|
return d.memos ?? [];
|
||||||
/**
|
}
|
||||||
* Create a new space
|
|
||||||
*/
|
async linkMemoToSpace(memoId: string, spaceId: string, token: string): Promise<boolean> {
|
||||||
async createSpace(spaceData: CreateSpaceRequest, appToken: string): Promise<Space> {
|
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/memos/link`, {
|
||||||
try {
|
method: 'POST',
|
||||||
if (!appToken) {
|
headers: headers(token),
|
||||||
throw new Error('Not authenticated');
|
body: JSON.stringify({ memoId }),
|
||||||
}
|
});
|
||||||
|
await throwOnError(r, 'Error linking memo to space');
|
||||||
// Ensure appId is set
|
return true;
|
||||||
if (!spaceData.appId) {
|
}
|
||||||
spaceData.appId = this.appId;
|
|
||||||
}
|
async unlinkMemoFromSpace(memoId: string, spaceId: string, token: string): Promise<boolean> {
|
||||||
|
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/memos/unlink`, {
|
||||||
// Prepare request data according to API docs
|
method: 'POST',
|
||||||
const requestData = {
|
headers: headers(token),
|
||||||
name: spaceData.name,
|
body: JSON.stringify({ memoId }),
|
||||||
};
|
});
|
||||||
|
await throwOnError(r, 'Error unlinking memo from space');
|
||||||
const response = await fetch(`${this.apiUrl}/memoro/spaces`, {
|
return true;
|
||||||
method: 'POST',
|
}
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${appToken}`,
|
async getSpaceInvites(spaceId: string, token: string): Promise<SpaceInvite[]> {
|
||||||
'Content-Type': 'application/json',
|
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/invites`, {
|
||||||
},
|
headers: headers(token),
|
||||||
body: JSON.stringify(requestData),
|
});
|
||||||
});
|
await throwOnError(r, 'Error fetching space invites');
|
||||||
|
const d = await r.json();
|
||||||
if (!response.ok) {
|
return d.invites ?? [];
|
||||||
const errorData = await response.json();
|
}
|
||||||
throw new Error(errorData.error || `Error creating space: ${response.statusText}`);
|
|
||||||
}
|
async inviteUserToSpace(
|
||||||
|
spaceId: string,
|
||||||
const data = await response.json();
|
email: string,
|
||||||
console.log('Create space response:', JSON.stringify(data, null, 2));
|
_role: string,
|
||||||
|
token: string
|
||||||
// Handle both formats: {success, spaceId} or {space: {success, spaceId}}
|
): Promise<string> {
|
||||||
const success = data.success || (data.space && data.space.success);
|
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/invite`, {
|
||||||
const spaceId = data.spaceId || (data.space && data.space.spaceId);
|
method: 'POST',
|
||||||
|
headers: headers(token),
|
||||||
if (!success) {
|
body: JSON.stringify({ email }),
|
||||||
throw new Error(data.error || (data.space && data.space.error) || 'Failed to create space');
|
});
|
||||||
}
|
await throwOnError(r, 'Error inviting user to space');
|
||||||
|
const d = await r.json();
|
||||||
if (!spaceId) {
|
return d.invite?.id ?? d.inviteId;
|
||||||
throw new Error('No space ID returned from server');
|
}
|
||||||
}
|
|
||||||
|
async resendInvite(inviteId: string, token: string): Promise<boolean> {
|
||||||
try {
|
const r = await fetch(`${API()}/api/v1/spaces/invites/${inviteId}/resend`, {
|
||||||
// Try to get the new space details
|
method: 'POST',
|
||||||
return await this.getSpace(spaceId, appToken);
|
headers: headers(token),
|
||||||
} catch (fetchError) {
|
});
|
||||||
console.log('Could not fetch newly created space details. Creating fallback space object.');
|
await throwOnError(r, 'Error resending invite');
|
||||||
|
return true;
|
||||||
// Create a fallback space object with minimal information
|
}
|
||||||
return {
|
|
||||||
id: spaceId,
|
async cancelInvite(inviteId: string, token: string): Promise<boolean> {
|
||||||
name: spaceData.name,
|
const r = await fetch(`${API()}/api/v1/spaces/invites/${inviteId}`, {
|
||||||
description: '',
|
method: 'DELETE',
|
||||||
memoCount: 0,
|
headers: headers(token),
|
||||||
isDefault: false,
|
});
|
||||||
color: spaceData.color || '#4CAF50',
|
await throwOnError(r, 'Error canceling invite');
|
||||||
created_at: new Date().toISOString(),
|
return true;
|
||||||
updated_at: new Date().toISOString(),
|
}
|
||||||
};
|
|
||||||
}
|
async acceptInvite(inviteId: string, token: string): Promise<boolean> {
|
||||||
} catch (error) {
|
const r = await fetch(`${API()}/api/v1/invites/accept`, {
|
||||||
console.error('Failed to create space:', error);
|
method: 'POST',
|
||||||
throw error;
|
headers: headers(token),
|
||||||
}
|
body: JSON.stringify({ inviteId }),
|
||||||
}
|
});
|
||||||
|
await throwOnError(r, 'Error accepting invite');
|
||||||
/**
|
return true;
|
||||||
* Update an existing space
|
}
|
||||||
*/
|
|
||||||
async updateSpace(
|
async declineInvite(inviteId: string, token: string): Promise<boolean> {
|
||||||
spaceId: string,
|
const r = await fetch(`${API()}/api/v1/invites/decline`, {
|
||||||
spaceData: UpdateSpaceRequest,
|
method: 'POST',
|
||||||
appToken: string
|
headers: headers(token),
|
||||||
): Promise<Space> {
|
body: JSON.stringify({ inviteId }),
|
||||||
try {
|
});
|
||||||
if (!appToken) {
|
await throwOnError(r, 'Error declining invite');
|
||||||
throw new Error('Not authenticated');
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, {
|
async getUserInvites(token: string): Promise<SpaceInvite[]> {
|
||||||
method: 'PUT',
|
const r = await fetch(`${API()}/api/v1/invites/pending`, { headers: headers(token) });
|
||||||
headers: {
|
await throwOnError(r, 'Error fetching user invites');
|
||||||
Authorization: `Bearer ${appToken}`,
|
const d = await r.json();
|
||||||
'Content-Type': 'application/json',
|
return d.invites ?? [];
|
||||||
},
|
|
||||||
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<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
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<Memo[]> {
|
|
||||||
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<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
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<SpaceInvite[]> {
|
|
||||||
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<string> {
|
|
||||||
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<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
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<SpaceInvite[]> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
export const spaceService = new SpaceService();
|
export const spaceService = new SpaceService();
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* Transcription Service for Memoro Web
|
* Transcription Service for Memoro Web
|
||||||
* Handles audio transcription via memoro-service middleware
|
* Triggers transcription via the new Hono/Bun memoro-server.
|
||||||
*
|
|
||||||
* Pattern adapted from memoro_app/features/storage/transcriptionUtils.ts
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { env } from '$lib/config/env';
|
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 {
|
export interface TranscriptionRequestResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|
@ -32,44 +27,6 @@ export interface TranscriptionParams {
|
||||||
mediaType?: 'audio' | 'video';
|
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<Response> {
|
|
||||||
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({
|
export async function triggerTranscription({
|
||||||
userId,
|
userId,
|
||||||
fileName,
|
fileName,
|
||||||
|
|
@ -79,63 +36,47 @@ export async function triggerTranscription({
|
||||||
recordingLanguages,
|
recordingLanguages,
|
||||||
title,
|
title,
|
||||||
blueprintId,
|
blueprintId,
|
||||||
appToken,
|
accessToken,
|
||||||
mediaType,
|
mediaType,
|
||||||
}: TranscriptionParams & { appToken: string }): Promise<TranscriptionRequestResult> {
|
}: TranscriptionParams & { accessToken: string }): Promise<TranscriptionRequestResult> {
|
||||||
try {
|
try {
|
||||||
if (!appToken) {
|
if (!accessToken) {
|
||||||
const errorMsg = 'No authenticated token found';
|
return { success: false, error: 'No authenticated token found' };
|
||||||
return { success: false, error: errorMsg };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioPath = `${userId}/${fileName}`;
|
const filePath = `${userId}/${fileName}`;
|
||||||
|
|
||||||
console.debug('🎯 Triggering transcription with:', {
|
const response = await fetch(`${SERVER_URL}/api/v1/memos`, {
|
||||||
audioPath,
|
method: 'POST',
|
||||||
duration,
|
headers: {
|
||||||
memoId,
|
'Content-Type': 'application/json',
|
||||||
spaceId,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
title,
|
},
|
||||||
blueprintId,
|
body: JSON.stringify({
|
||||||
recordingLanguages,
|
filePath,
|
||||||
|
duration,
|
||||||
|
memoId,
|
||||||
|
spaceId,
|
||||||
|
recordingLanguages,
|
||||||
|
title,
|
||||||
|
blueprintId,
|
||||||
|
mediaType,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Let memoro-service handle the intelligent routing
|
if (!response.ok) {
|
||||||
const transcribeResponse = await transcribeViaMemoryService(
|
const errorText = await response.text();
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorText,
|
error: errorText,
|
||||||
isNetworkError: transcribeResponse.status >= 500,
|
isNetworkError: response.status >= 500,
|
||||||
userMessage: 'Transcription could not be started',
|
userMessage: 'Transcription could not be started',
|
||||||
technicalMessage: errorText,
|
technicalMessage: errorText,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('✅ Transcription started successfully via Memoro Service');
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.debug('Error triggering transcription:', error);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: String(error),
|
error: String(error),
|
||||||
|
|
|
||||||
|
|
@ -1455,6 +1455,76 @@ services:
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 45s
|
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:
|
mana-llm:
|
||||||
build:
|
build:
|
||||||
context: ./services/mana-llm
|
context: ./services/mana-llm
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,11 @@
|
||||||
"dev:todo:app": "concurrently -n server,web -c yellow,cyan \"pnpm dev:todo:server\" \"pnpm dev:todo:web\"",
|
"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: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: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:web": "pnpm --filter @uload/web dev",
|
||||||
"dev:uload:server": "cd apps/uload/apps/server && bun run --watch src/index.ts",
|
"dev:uload:server": "cd apps/uload/apps/server && bun run --watch src/index.ts",
|
||||||
"dev:uload:landing": "pnpm --filter @uload/landing dev",
|
"dev:uload:landing": "pnpm --filter @uload/landing dev",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue