meridian / api-gateway /src /app.controller.ts
GitHub Actions
Deploy to HuggingFace Spaces
2b0c376
Raw
History Blame Contribute Delete
16.5 kB
import {
Controller, All, Req, Res, Next, Get, Post, Put, Patch, Delete, Param, Body, Query,
UseGuards, Request, UseInterceptors, UploadedFile, Headers, HttpCode
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import { ProxyService } from './proxy.service';
import { Request as ExpressRequest, Response } from 'express';
@Controller('api')
export class AppController {
constructor(private proxy: ProxyService) {}
private getAuthHeader(req: any) {
return req.headers?.authorization || '';
}
// AUTH ROUTES
@Post('auth/login')
login(@Body() body: any) {
return this.proxy.forward('auth', '/auth/login', 'POST', body);
}
@Get('auth/verify')
@UseGuards(AuthGuard('jwt'))
verify(@Request() req: any) {
return req.user;
}
// USER ROUTES
@Get('users')
@UseGuards(AuthGuard('jwt'))
getUsers(@Request() req: any) {
return this.proxy.forward('auth', '/users', 'GET', null, { Authorization: this.getAuthHeader(req) });
}
@Post('users')
@UseGuards(AuthGuard('jwt'))
createUser(@Body() body: any, @Request() req: any) {
return this.proxy.forward('auth', '/users', 'POST', body, { Authorization: this.getAuthHeader(req) });
}
@Put('users/:id')
@UseGuards(AuthGuard('jwt'))
updateUser(@Param('id') id: string, @Body() body: any, @Request() req: any) {
return this.proxy.forward('auth', `/users/${id}`, 'PUT', body, { Authorization: this.getAuthHeader(req) });
}
@Delete('users/:id')
@UseGuards(AuthGuard('jwt'))
deleteUser(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('auth', `/users/${id}`, 'DELETE', null, { Authorization: this.getAuthHeader(req) });
}
// BLOG ROUTES - Public
@Get('posts')
getPosts(@Query() query: any) {
const params = new URLSearchParams(query).toString();
return this.proxy.forward('blog', `/posts?${params}`, 'GET');
}
@Get('posts/featured')
getFeatured() { return this.proxy.forward('blog', '/posts/featured', 'GET'); }
@Get('posts/recent')
getRecent() { return this.proxy.forward('blog', '/posts/recent', 'GET'); }
@Get('posts/stats')
@UseGuards(AuthGuard('jwt'))
getStats(@Request() req: any) {
return this.proxy.forward('blog', '/posts/stats', 'GET', null, { Authorization: this.getAuthHeader(req) });
}
@Get('posts/admin')
@UseGuards(AuthGuard('jwt'))
getAdminPosts(@Query() query: any, @Request() req: any) {
const params = new URLSearchParams({ ...query, status: query.status || '' }).toString();
return this.proxy.forward('blog', `/posts/admin?${params}`, 'GET', null, { Authorization: this.getAuthHeader(req) });
}
@Get('posts/:slug')
getPost(@Param('slug') slug: string) {
return this.proxy.forward('blog', `/posts/${slug}`, 'GET');
}
@Post('posts')
@UseGuards(AuthGuard('jwt'))
createPost(@Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', '/posts', 'POST', body, { Authorization: this.getAuthHeader(req) });
}
@Patch('posts/:id/approve')
@UseGuards(AuthGuard('jwt'))
approvePost(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('blog', `/posts/${id}/approve`, 'PATCH', null, { Authorization: this.getAuthHeader(req) });
}
@Patch('posts/:id/reject')
@UseGuards(AuthGuard('jwt'))
rejectPost(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('blog', `/posts/${id}/reject`, 'PATCH', null, { Authorization: this.getAuthHeader(req) });
}
@Put('posts/:id')
@UseGuards(AuthGuard('jwt'))
updatePost(@Param('id') id: string, @Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', `/posts/${id}`, 'PUT', body, { Authorization: this.getAuthHeader(req) });
}
@Delete('posts/:id')
@UseGuards(AuthGuard('jwt'))
deletePost(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('blog', `/posts/${id}`, 'DELETE', null, { Authorization: this.getAuthHeader(req) });
}
// CATEGORIES
@Get('categories')
getCategories() { return this.proxy.forward('blog', '/categories', 'GET'); }
@Post('categories')
@UseGuards(AuthGuard('jwt'))
createCategory(@Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', '/categories', 'POST', body, { Authorization: this.getAuthHeader(req) });
}
@Put('categories/:id')
@UseGuards(AuthGuard('jwt'))
updateCategory(@Param('id') id: string, @Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', `/categories/${id}`, 'PUT', body, { Authorization: this.getAuthHeader(req) });
}
@Delete('categories/:id')
@UseGuards(AuthGuard('jwt'))
deleteCategory(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('blog', `/categories/${id}`, 'DELETE', null, { Authorization: this.getAuthHeader(req) });
}
// TAGS
@Get('tags')
getTags() { return this.proxy.forward('blog', '/tags', 'GET'); }
@Post('tags')
@UseGuards(AuthGuard('jwt'))
createTag(@Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', '/tags', 'POST', body, { Authorization: this.getAuthHeader(req) });
}
@Post('tags/many')
@UseGuards(AuthGuard('jwt'))
createManyTags(@Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', '/tags/many', 'POST', body, { Authorization: this.getAuthHeader(req) });
}
// COMMENTS
@Get('comments')
@UseGuards(AuthGuard('jwt'))
getComments(@Request() req: any) {
return this.proxy.forward('blog', '/comments', 'GET', null, { Authorization: this.getAuthHeader(req) });
}
@Get('comments/post/:postId')
getPostComments(@Param('postId') postId: string) {
return this.proxy.forward('blog', `/comments/post/${postId}`, 'GET');
}
@Post('comments/post/:postId')
createComment(@Param('postId') postId: string, @Body() body: any) {
return this.proxy.forward('blog', `/comments/post/${postId}`, 'POST', body);
}
@Put('comments/:id/approve')
@UseGuards(AuthGuard('jwt'))
approveComment(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('blog', `/comments/${id}/approve`, 'PUT', null, { Authorization: this.getAuthHeader(req) });
}
@Delete('comments/:id')
@UseGuards(AuthGuard('jwt'))
deleteComment(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('blog', `/comments/${id}`, 'DELETE', null, { Authorization: this.getAuthHeader(req) });
}
// MEDIA
@Get('media')
@UseGuards(AuthGuard('jwt'))
getMedia(@Request() req: any) {
return this.proxy.forward('media', '/media', 'GET', null, { Authorization: this.getAuthHeader(req) });
}
@Post('media/upload')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(FileInterceptor('file', { storage: memoryStorage() }))
uploadMedia(@UploadedFile() file: Express.Multer.File, @Body() body: any, @Request() req: any) {
return this.proxy.forwardWithFile('/media/upload', file, body, this.getAuthHeader(req));
}
@Delete('media/:id')
@UseGuards(AuthGuard('jwt'))
deleteMedia(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('media', `/media/${id}`, 'DELETE', null, { Authorization: this.getAuthHeader(req) });
}
// STORY ROUTES - Public
@Get('stories')
getStories(@Query() query: any) {
const params = new URLSearchParams(query).toString();
return this.proxy.forward('blog', `/stories?${params}`, 'GET');
}
@Get('stories/recent')
getRecentStories() { return this.proxy.forward('blog', '/stories/recent', 'GET'); }
@Get('stories/stats')
@UseGuards(AuthGuard('jwt'))
getStoryStats(@Request() req: any) {
return this.proxy.forward('blog', '/stories/stats', 'GET', null, { Authorization: this.getAuthHeader(req) });
}
@Get('stories/admin')
@UseGuards(AuthGuard('jwt'))
getAdminStories(@Query() query: any, @Request() req: any) {
const params = new URLSearchParams({ ...query, status: query.status || '' }).toString();
return this.proxy.forward('blog', `/stories/admin?${params}`, 'GET', null, { Authorization: this.getAuthHeader(req) });
}
@Get('stories/:slug')
getStory(@Param('slug') slug: string) {
return this.proxy.forward('blog', `/stories/${slug}`, 'GET');
}
@Post('stories')
@UseGuards(AuthGuard('jwt'))
createStory(@Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', '/stories', 'POST', body, { Authorization: this.getAuthHeader(req) });
}
@Patch('stories/:id/approve')
@UseGuards(AuthGuard('jwt'))
approveStory(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('blog', `/stories/${id}/approve`, 'PATCH', null, { Authorization: this.getAuthHeader(req) });
}
@Patch('stories/:id/reject')
@UseGuards(AuthGuard('jwt'))
rejectStory(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('blog', `/stories/${id}/reject`, 'PATCH', null, { Authorization: this.getAuthHeader(req) });
}
@Put('stories/:id')
@UseGuards(AuthGuard('jwt'))
updateStory(@Param('id') id: string, @Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', `/stories/${id}`, 'PUT', body, { Authorization: this.getAuthHeader(req) });
}
@Delete('stories/:id')
@UseGuards(AuthGuard('jwt'))
deleteStory(@Param('id') id: string, @Request() req: any) {
return this.proxy.forward('blog', `/stories/${id}`, 'DELETE', null, { Authorization: this.getAuthHeader(req) });
}
// NEWS ROUTES
@Get('news')
getNews(@Query() query: any) {
const params = new URLSearchParams(query).toString();
return this.proxy.forward('blog', `/news?${params}`, 'GET');
}
@Post('news/refresh')
@UseGuards(AuthGuard('jwt'))
refreshNews(@Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', '/news/refresh', 'POST', body, { Authorization: this.getAuthHeader(req) });
}
// PUSH NOTIFICATION ROUTES
@Get('push/vapid-key')
getPushVapidKey() {
return this.proxy.forward('blog', '/push/vapid-key', 'GET');
}
@Post('push/subscribe')
subscribePush(@Body() body: any) {
return this.proxy.forward('blog', '/push/subscribe', 'POST', body);
}
@Delete('push/unsubscribe')
unsubscribePush(@Body() body: any) {
return this.proxy.forward('blog', '/push/unsubscribe', 'DELETE', body);
}
// AGENT RUNS
@Post('agent-runs/dispatch')
@UseGuards(AuthGuard('jwt'))
dispatchAgent(@Body() body: any, @Request() req: any) {
return this.proxy.forward('blog', '/agent-runs/dispatch', 'POST', body, { Authorization: this.getAuthHeader(req) });
}
@Post('agent-runs')
createAgentRun(@Body() body: any) {
return this.proxy.forward('blog', '/agent-runs', 'POST', body);
}
@Put('agent-runs/:runId')
updateAgentRun(@Param('runId') runId: string, @Body() body: any) {
return this.proxy.forward('blog', `/agent-runs/${runId}`, 'PUT', body);
}
@Get('agent-runs')
@UseGuards(AuthGuard('jwt'))
getAgentRuns(@Request() req: any) {
return this.proxy.forward('blog', '/agent-runs', 'GET', null, { Authorization: this.getAuthHeader(req) });
}
@Get('agent-runs/:runId')
@UseGuards(AuthGuard('jwt'))
getAgentRun(@Param('runId') runId: string, @Request() req: any) {
return this.proxy.forward('blog', `/agent-runs/${runId}`, 'GET', null, { Authorization: this.getAuthHeader(req) });
}
// TEXT-TO-SPEECH β€” one chunk per request (live playback); style selects the voice/pace.
// format: 'wav' (default, live per-chunk playback) or 'mp3' (whole-post pre-rendered
// narration for the media library β€” mp3 encoding only exists on the local Kokoro path,
// so an mp3 request skips the HF fallbacks rather than silently returning the wrong format.
// News: Primary: maya-research/maya1 (HF) β†’ Secondary: Kokoro-82M β†’ Tertiary: facebook/mms-tts-eng (HF)
// Other: Primary: Kokoro-82M local β†’ Fallback: facebook/mms-tts-eng (HF)
@Post('tts')
async textToSpeech(
@Body() body: { text: string; style?: string; format?: string },
@Res() res: Response,
) {
const text = (body.text || '').trim();
const style = (body.style || '').trim();
const format = (body.format || 'wav').trim().toLowerCase() === 'mp3' ? 'mp3' : 'wav';
if (!text) { res.status(400).json({ message: 'text is required' }); return; }
const localTts = process.env.TTS_SERVICE_URL;
const hfToken = process.env.HF_TOKEN;
if (!localTts && !hfToken) {
res.status(503).json({ message: 'TTS not configured: set TTS_SERVICE_URL or HF_TOKEN' });
return;
}
const errors: string[] = [];
// Retries once on HF 503 "model loading" response.
const hfInfer = async (model: string): Promise<{ buf: Buffer; ct: string }> => {
const doFetch = async () => {
const r = await fetch(
`https://api-inference.huggingface.co/models/${model}`,
{
method: 'POST',
headers: { Authorization: `Bearer ${hfToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ inputs: text }),
signal: AbortSignal.timeout(90_000),
},
);
if (r.status === 503) {
const b = await r.json().catch(() => ({})) as { estimated_time?: number };
const wait = Math.min((b.estimated_time ?? 20) * 1000, 30_000);
await new Promise(resolve => setTimeout(resolve, wait));
const r2 = await fetch(
`https://api-inference.huggingface.co/models/${model}`,
{
method: 'POST',
headers: { Authorization: `Bearer ${hfToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ inputs: text }),
signal: AbortSignal.timeout(90_000),
},
);
if (!r2.ok) throw new Error(`HF ${model}: ${r2.status}`);
return r2;
}
if (!r.ok) throw new Error(`HF ${model}: ${r.status}`);
return r;
};
const r = await doFetch();
return { buf: Buffer.from(await r.arrayBuffer()), ct: r.headers.get('content-type') || 'audio/flac' };
};
// ── News primary: maya-research/maya1 (HF) ──────────────────────────────
// (mp3 output only exists on the local Kokoro path β€” skip HF branches entirely.)
if (format === 'mp3') {
if (!localTts) {
res.status(503).json({ message: 'mp3 TTS requires TTS_SERVICE_URL (local Kokoro)' });
return;
}
} else if (style === 'news' && hfToken) {
try {
const { buf, ct } = await hfInfer('maya-research/maya1');
res.set({ 'Content-Type': ct, 'Content-Length': String(buf.length) });
return res.send(buf);
} catch (e) {
errors.push((e as Error).message);
}
}
// ── Primary (all styles): Kokoro-82M local service ───────────────────────
// mp3 requests (whole-post pre-rendered narration) take much longer than a short live
// chunk, so they get a longer timeout matching tts-service's own gunicorn timeout.
if (localTts) {
try {
const r = await fetch(`${localTts}/tts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, style, format }),
signal: AbortSignal.timeout(format === 'mp3' ? 280_000 : 120_000),
});
if (!r.ok) throw new Error(`Kokoro: ${r.status}`);
const buf = Buffer.from(await r.arrayBuffer());
res.set({
'Content-Type': format === 'mp3' ? 'audio/mpeg' : 'audio/wav',
'Content-Length': String(buf.length),
});
return res.send(buf);
} catch (e) {
errors.push((e as Error).message);
if (format === 'mp3') {
res.status(503).json({ message: `mp3 TTS unavailable: ${errors.join(' | ')}` });
return;
}
}
}
// ── Tertiary / final fallback: HF facebook/mms-tts-eng ──────────────────
if (hfToken) {
try {
const { buf, ct } = await hfInfer('facebook/mms-tts-eng');
res.set({ 'Content-Type': ct, 'Content-Length': String(buf.length) });
return res.send(buf);
} catch (e) {
errors.push((e as Error).message);
}
}
res.status(503).json({ message: `TTS unavailable: ${errors.join(' | ')}` });
}
}