AI-powered speech-to-text transformation
This commit is contained in:
@@ -1,89 +0,0 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
ServiceUnavailableException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
const MAX_AUDIO_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
@Injectable()
|
||||
export class AiSttService {
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return (
|
||||
this.config.get<string>('STT_ENABLED') === 'true' &&
|
||||
Boolean(this.sttUrl())
|
||||
);
|
||||
}
|
||||
|
||||
async transcribe(file: Express.Multer.File | undefined) {
|
||||
if (!this.isEnabled()) {
|
||||
throw new ServiceUnavailableException('تبدیل صوت به متن فعال نیست');
|
||||
}
|
||||
if (!file?.buffer?.length) {
|
||||
throw new BadRequestException('فایل صوتی ارسال نشده است');
|
||||
}
|
||||
if (file.size > MAX_AUDIO_BYTES) {
|
||||
throw new BadRequestException('حجم فایل صوتی بیش از حد مجاز است');
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append(
|
||||
'audio',
|
||||
new Blob([new Uint8Array(file.buffer)], {
|
||||
type: file.mimetype || 'audio/wav',
|
||||
}),
|
||||
file.originalname || 'audio.wav',
|
||||
);
|
||||
|
||||
const timeoutMs = Number(this.config.get<string>('STT_TIMEOUT_MS') || 180000);
|
||||
const headers: Record<string, string> = {};
|
||||
const token = this.config.get<string>('STT_INTERNAL_TOKEN')?.trim();
|
||||
if (token) headers['X-STT-Token'] = token;
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${this.sttUrl()}/transcribe`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: form,
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
} catch (e) {
|
||||
const name = e instanceof Error ? e.name : '';
|
||||
if (name === 'TimeoutError' || name === 'AbortError') {
|
||||
throw new ServiceUnavailableException(
|
||||
'زمان تبدیل صوت تمام شد. اگر تازه مدل عوض شده، چند دقیقه صبر کنید تا دانلود شود و دوباره امتحان کنید.',
|
||||
);
|
||||
}
|
||||
throw new ServiceUnavailableException(
|
||||
'سرویس تبدیل صوت خاموش است. مدل medium معمولاً حافظهٔ سرور را پر میکند و پروسه را میکشد. در apps/stt/.env مقدار STT_MODEL=small بگذارید و pm2 restart mento-stt بزنید.',
|
||||
);
|
||||
}
|
||||
|
||||
const payload = (await res.json().catch(() => ({}))) as {
|
||||
text?: string;
|
||||
detail?: string | Array<{ msg?: string }>;
|
||||
};
|
||||
|
||||
if (!res.ok) {
|
||||
const detail = Array.isArray(payload.detail)
|
||||
? payload.detail.map((item) => item.msg).filter(Boolean).join('، ')
|
||||
: payload.detail;
|
||||
const raw = String(detail || '').trim();
|
||||
let message = raw || 'تبدیل صوت به متن ناموفق بود';
|
||||
if (raw.length > 220) {
|
||||
message = 'تبدیل صوت به متن ناموفق بود. سرویس STT محلی را بررسی کنید.';
|
||||
}
|
||||
throw new ServiceUnavailableException(message);
|
||||
}
|
||||
|
||||
return { text: (payload.text ?? '').trim() };
|
||||
}
|
||||
|
||||
private sttUrl(): string {
|
||||
return (this.config.get<string>('STT_URL') ?? '').trim().replace(/\/$/, '');
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,10 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { memoryStorage } from 'multer';
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import type { Actor } from '../common/record-scope';
|
||||
import { AiAskService } from './ai-ask.service';
|
||||
import { AskMentoDto } from './dto';
|
||||
import { AiService } from './ai.service';
|
||||
import { AiSttService } from './ai-stt.service';
|
||||
|
||||
@Controller('ai')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -24,23 +12,11 @@ export class AiController {
|
||||
constructor(
|
||||
private readonly ai: AiService,
|
||||
private readonly ask: AiAskService,
|
||||
private readonly stt: AiSttService,
|
||||
) {}
|
||||
|
||||
@Get('status')
|
||||
status() {
|
||||
return { enabled: this.ai.isEnabled(), stt: this.stt.isEnabled() };
|
||||
}
|
||||
|
||||
@Post('transcribe')
|
||||
@UseInterceptors(
|
||||
FileInterceptor('audio', {
|
||||
storage: memoryStorage(),
|
||||
limits: { fileSize: 8 * 1024 * 1024 },
|
||||
}),
|
||||
)
|
||||
transcribe(@UploadedFile() file: Express.Multer.File) {
|
||||
return this.stt.transcribe(file);
|
||||
return { enabled: this.ai.isEnabled() };
|
||||
}
|
||||
|
||||
@Post('ask')
|
||||
|
||||
@@ -2,12 +2,11 @@ import { Global, Module } from '@nestjs/common';
|
||||
import { AiAskService } from './ai-ask.service';
|
||||
import { AiController } from './ai.controller';
|
||||
import { AiService } from './ai.service';
|
||||
import { AiSttService } from './ai-stt.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
controllers: [AiController],
|
||||
providers: [AiService, AiAskService, AiSttService],
|
||||
exports: [AiService, AiSttService],
|
||||
providers: [AiService, AiAskService],
|
||||
exports: [AiService],
|
||||
})
|
||||
export class AiModule {}
|
||||
|
||||
Reference in New Issue
Block a user