mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 15:06:43 +02:00
chore: archive 25 standalone web apps, move wisekeep to apps-archived
All standalone SvelteKit web apps have been superseded by the unified ManaCore app (apps/manacore/apps/web). Moved to web-archived/ within each project to preserve history while removing from active workspace. Archived: calc, cards, chat, citycorners, contacts, context, guides, inventar, moodlit, mukke, news, nutriphi, photos, picture, planta, presi, questions, skilltree, storage, times, zitare, todo, calendar, uload, memoro Moved to apps-archived/: wisekeep (not integrated, inactive) Kept active: manacore (unified), matrix, manavoxel, arcade (separate containers) Server, landing, and package directories remain active for each project. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
373976a11b
commit
2eb1a0cd76
1994 changed files with 278 additions and 1310 deletions
30
apps-archived/wisekeep/apps/server/src/config.ts
Normal file
30
apps-archived/wisekeep/apps/server/src/config.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export interface Config {
|
||||
port: number;
|
||||
databaseUrl: string;
|
||||
manaAuthUrl: string;
|
||||
cors: { origins: string[] };
|
||||
groqApiKey: string;
|
||||
whisperModel: string;
|
||||
}
|
||||
|
||||
export function loadConfig(): Config {
|
||||
const requiredEnv = (key: string, fallback?: string): string => {
|
||||
const value = process.env[key] || fallback;
|
||||
if (!value) throw new Error(`Missing required env var: ${key}`);
|
||||
return value;
|
||||
};
|
||||
|
||||
return {
|
||||
port: parseInt(process.env.PORT || '3072', 10),
|
||||
databaseUrl: requiredEnv(
|
||||
'DATABASE_URL',
|
||||
'postgresql://manacore:devpassword@localhost:5432/mana_sync'
|
||||
),
|
||||
manaAuthUrl: requiredEnv('MANA_CORE_AUTH_URL', 'http://localhost:3001'),
|
||||
cors: {
|
||||
origins: (process.env.CORS_ORIGINS || 'http://localhost:5173').split(','),
|
||||
},
|
||||
groqApiKey: process.env.GROQ_API_KEY || '',
|
||||
whisperModel: process.env.WHISPER_MODEL || 'whisper-large-v3-turbo',
|
||||
};
|
||||
}
|
||||
28
apps-archived/wisekeep/apps/server/src/index.ts
Normal file
28
apps-archived/wisekeep/apps/server/src/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { loadConfig } from './config';
|
||||
import { errorHandler } from './middleware/error-handler';
|
||||
import { jwtAuth } from './middleware/jwt-auth';
|
||||
import { TranscribeService } from './services/transcribe';
|
||||
import { healthRoutes } from './routes/health';
|
||||
import { createTranscribeRoutes } from './routes/transcribe';
|
||||
|
||||
const config = loadConfig();
|
||||
const transcribeService = new TranscribeService(config);
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.onError(errorHandler);
|
||||
app.use('*', cors({ origin: config.cors.origins, credentials: true }));
|
||||
|
||||
// Public
|
||||
app.route('/health', healthRoutes);
|
||||
|
||||
// Protected
|
||||
app.use('/api/v1/*', jwtAuth(config.manaAuthUrl));
|
||||
app.route('/api/v1/transcribe', createTranscribeRoutes(transcribeService));
|
||||
|
||||
export default {
|
||||
port: config.port,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
19
apps-archived/wisekeep/apps/server/src/lib/errors.ts
Normal file
19
apps-archived/wisekeep/apps/server/src/lib/errors.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
export class NotFoundError extends HTTPException {
|
||||
constructor(message = 'Not found') {
|
||||
super(404, { message });
|
||||
}
|
||||
}
|
||||
|
||||
export class BadRequestError extends HTTPException {
|
||||
constructor(message = 'Bad request') {
|
||||
super(400, { message });
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends HTTPException {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(401, { message });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import type { ErrorHandler } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
export const errorHandler: ErrorHandler = (err, c) => {
|
||||
if (err instanceof HTTPException) {
|
||||
return c.json({ statusCode: err.status, message: err.message }, err.status);
|
||||
}
|
||||
|
||||
console.error('Unhandled error:', err);
|
||||
return c.json({ statusCode: 500, message: 'Internal server error' }, 500);
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import type { MiddlewareHandler } from 'hono';
|
||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
import { UnauthorizedError } from '../lib/errors';
|
||||
|
||||
export interface AuthUser {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
|
||||
|
||||
function getJwks(authUrl: string) {
|
||||
if (!jwks) {
|
||||
jwks = createRemoteJWKSet(new URL('/api/auth/jwks', authUrl));
|
||||
}
|
||||
return jwks;
|
||||
}
|
||||
|
||||
export function jwtAuth(authUrl: string): MiddlewareHandler {
|
||||
return async (c, next) => {
|
||||
const authHeader = c.req.header('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedError('Missing or invalid Authorization header');
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7);
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, getJwks(authUrl), {
|
||||
issuer: authUrl,
|
||||
audience: 'manacore',
|
||||
});
|
||||
|
||||
const user: AuthUser = {
|
||||
userId: payload.sub || '',
|
||||
email: (payload.email as string) || '',
|
||||
role: (payload.role as string) || 'user',
|
||||
};
|
||||
|
||||
c.set('user', user);
|
||||
await next();
|
||||
} catch {
|
||||
throw new UnauthorizedError('Invalid or expired token');
|
||||
}
|
||||
};
|
||||
}
|
||||
10
apps-archived/wisekeep/apps/server/src/routes/health.ts
Normal file
10
apps-archived/wisekeep/apps/server/src/routes/health.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Hono } from 'hono';
|
||||
|
||||
export const healthRoutes = new Hono().get('/', (c) =>
|
||||
c.json({
|
||||
status: 'ok',
|
||||
service: 'wisekeep-server',
|
||||
runtime: 'bun',
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
);
|
||||
25
apps-archived/wisekeep/apps/server/src/routes/transcribe.ts
Normal file
25
apps-archived/wisekeep/apps/server/src/routes/transcribe.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { Hono } from 'hono';
|
||||
import type { TranscribeService } from '../services/transcribe';
|
||||
import type { AuthUser } from '../middleware/jwt-auth';
|
||||
|
||||
export function createTranscribeRoutes(transcribeService: TranscribeService) {
|
||||
return new Hono<{ Variables: { user: AuthUser } }>().post('/', async (c) => {
|
||||
const { url, language } = await c.req.json<{ url: string; language?: string }>();
|
||||
if (!url) return c.json({ error: 'URL is required' }, 400);
|
||||
|
||||
const result = await transcribeService.transcribe(url, language || 'de');
|
||||
|
||||
// Return result — client saves to local-first store
|
||||
return c.json({
|
||||
id: crypto.randomUUID(),
|
||||
url,
|
||||
title: result.title,
|
||||
channel: result.channel,
|
||||
duration: result.duration,
|
||||
transcript: result.transcript,
|
||||
language: result.language,
|
||||
model: result.model,
|
||||
status: 'completed',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import OpenAI from 'openai';
|
||||
import { $ } from 'bun';
|
||||
import { unlink } from 'node:fs/promises';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import type { Config } from '../config';
|
||||
|
||||
export interface TranscriptionResult {
|
||||
title: string;
|
||||
channel: string;
|
||||
duration: number;
|
||||
transcript: string;
|
||||
language: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export class TranscribeService {
|
||||
private groq: OpenAI | null;
|
||||
|
||||
constructor(private config: Config) {
|
||||
this.groq = config.groqApiKey
|
||||
? new OpenAI({ apiKey: config.groqApiKey, baseURL: 'https://api.groq.com/openai/v1' })
|
||||
: null;
|
||||
}
|
||||
|
||||
async transcribe(url: string, language = 'de'): Promise<TranscriptionResult> {
|
||||
if (!this.groq) throw new Error('Groq API key not configured');
|
||||
|
||||
// 1. Get video info
|
||||
const infoResult =
|
||||
await $`yt-dlp --print "%(title)s|||%(channel)s|||%(duration)s" --no-download ${url}`.text();
|
||||
const [title, channel, durationStr] = infoResult.trim().split('|||');
|
||||
const duration = parseInt(durationStr) || 0;
|
||||
|
||||
// 2. Download audio to temp file
|
||||
const tempFile = `/tmp/wisekeep-${Date.now()}.mp3`;
|
||||
await $`yt-dlp -x --audio-format mp3 --audio-quality 5 -o ${tempFile} ${url}`;
|
||||
|
||||
try {
|
||||
// 3. Transcribe with Groq (use ReadStream for OpenAI SDK compat)
|
||||
const transcription = await this.groq.audio.transcriptions.create({
|
||||
file: createReadStream(tempFile) as unknown as Parameters<
|
||||
typeof this.groq.audio.transcriptions.create
|
||||
>[0]['file'],
|
||||
model: this.config.whisperModel,
|
||||
language,
|
||||
});
|
||||
|
||||
return {
|
||||
title: title || 'Unknown',
|
||||
channel: channel || 'Unknown',
|
||||
duration,
|
||||
transcript: transcription.text,
|
||||
language,
|
||||
model: this.config.whisperModel,
|
||||
};
|
||||
} finally {
|
||||
await unlink(tempFile).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue