voice
This commit is contained in:
parent
249bc2e8cb
commit
19dc953d6e
6 changed files with 175 additions and 7 deletions
|
|
@ -8,6 +8,7 @@ import { RoomsModule } from './rooms/rooms.module';
|
|||
import { QuestionsModule } from './questions/questions.module';
|
||||
import { GameModule } from './game/game.module';
|
||||
import { StatsModule } from './stats/stats.module';
|
||||
import { VoiceModule } from './voice/voice.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -18,6 +19,7 @@ import { StatsModule } from './stats/stats.module';
|
|||
QuestionsModule,
|
||||
GameModule,
|
||||
StatsModule,
|
||||
VoiceModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
|
|
|||
70
backend/src/voice/voice.controller.ts
Normal file
70
backend/src/voice/voice.controller.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
Param,
|
||||
Res,
|
||||
HttpStatus,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { VoiceService } from './voice.service';
|
||||
|
||||
@Controller('voice')
|
||||
export class VoiceController {
|
||||
constructor(private voiceService: VoiceService) {}
|
||||
|
||||
@Post('tts')
|
||||
async generateTTS(
|
||||
@Body() body: { text: string; voice?: string },
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const { text, voice = 'sarah' } = body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(HttpStatus.BAD_REQUEST).json({
|
||||
error: 'Text is required',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const audioBuffer = await this.voiceService.generateTTS(text, voice);
|
||||
|
||||
res.setHeader('Content-Type', 'audio/mpeg');
|
||||
res.setHeader('Content-Length', audioBuffer.length.toString());
|
||||
res.send(audioBuffer);
|
||||
} catch (error) {
|
||||
const status = error instanceof HttpException
|
||||
? error.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
const message = error.message || 'Failed to generate speech';
|
||||
return res.status(status).json({
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Get('effects/:effectType')
|
||||
async getEffect(
|
||||
@Param('effectType') effectType: string,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
try {
|
||||
const audioBuffer = await this.voiceService.getEffect(effectType);
|
||||
|
||||
res.setHeader('Content-Type', 'audio/mpeg');
|
||||
res.setHeader('Content-Length', audioBuffer.length.toString());
|
||||
res.send(audioBuffer);
|
||||
} catch (error) {
|
||||
const status = error instanceof HttpException
|
||||
? error.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
const message = error.message || 'Failed to get sound effect';
|
||||
return res.status(status).json({
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
backend/src/voice/voice.module.ts
Normal file
11
backend/src/voice/voice.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { VoiceService } from './voice.service';
|
||||
import { VoiceController } from './voice.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [VoiceController],
|
||||
providers: [VoiceService],
|
||||
exports: [VoiceService],
|
||||
})
|
||||
export class VoiceModule {}
|
||||
|
||||
74
backend/src/voice/voice.service.ts
Normal file
74
backend/src/voice/voice.service.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class VoiceService {
|
||||
private readonly voiceServiceUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.voiceServiceUrl = this.configService.get<string>('VOICE_SERVICE_HOST');
|
||||
|
||||
if (!this.voiceServiceUrl) {
|
||||
throw new Error('VOICE_SERVICE_HOST environment variable is not set');
|
||||
}
|
||||
}
|
||||
|
||||
async generateTTS(text: string, voice: string = 'sarah'): Promise<Buffer> {
|
||||
try {
|
||||
const url = `${this.voiceServiceUrl}/api/voice/tts`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ text, voice }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new HttpException(
|
||||
`Voice service error: ${response.statusText}`,
|
||||
response.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
throw new HttpException(
|
||||
`Failed to generate speech: ${error.message}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getEffect(effectType: string): Promise<Buffer> {
|
||||
try {
|
||||
const url = `${this.voiceServiceUrl}/api/voice/effects/${effectType}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new HttpException(
|
||||
`Voice service error: ${response.statusText}`,
|
||||
response.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
throw new HttpException(
|
||||
`Failed to get sound effect: ${error.message}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
const VOICE_SERVICE_URL = import.meta.env.VITE_VOICE_SERVICE_URL || 'http://localhost:3001';
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
||||
|
||||
/**
|
||||
* Hook for voice generation and playback
|
||||
|
|
@ -56,11 +56,12 @@ export function useVoice() {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${VOICE_SERVICE_URL}/api/voice/tts`, {
|
||||
const response = await fetch(`${API_URL}/voice/tts`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ text, voice }),
|
||||
});
|
||||
|
||||
|
|
@ -147,7 +148,7 @@ export function useVoice() {
|
|||
if (!isEnabled) return;
|
||||
|
||||
try {
|
||||
const audio = new Audio(`${VOICE_SERVICE_URL}/api/voice/effects/${effectType}`);
|
||||
const audio = new Audio(`${API_URL}/voice/effects/${effectType}`);
|
||||
audio.volume = effectsVolume;
|
||||
await audio.play();
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -29,25 +29,35 @@ body {
|
|||
/* Новогодние снежинки */
|
||||
@keyframes snow {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
transform: translateY(-20px) rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
2% {
|
||||
opacity: 1;
|
||||
}
|
||||
98% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) rotate(360deg);
|
||||
transform: translateY(calc(100vh + 20px)) rotate(360deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: fixed;
|
||||
top: -10px;
|
||||
top: 0;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
font-family: Arial;
|
||||
text-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
|
||||
animation: snow 10s linear infinite;
|
||||
animation-name: snow;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* Page styles */
|
||||
|
|
|
|||
Loading…
Reference in a new issue