This commit is contained in:
Dmitry 2026-01-10 03:23:50 +03:00
parent 4e2ec9d8c7
commit b50a00a65f
7 changed files with 128 additions and 6 deletions

View file

@ -6,7 +6,6 @@ import {
type CreateThemeDto,
} from '@/api/themes'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import {

View file

@ -36,7 +36,6 @@ import { Checkbox } from '@/components/ui/checkbox'
import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight, Upload } from 'lucide-react'
import { ThemeEditorDialog } from '@/components/ThemeEditorDialog'
import { ThemeImportDialog } from '@/components/ThemeImportDialog'
import type { CreateThemeDto } from '@/api/themes'
export default function ThemesPage() {
const queryClient = useQueryClient()

View file

@ -102,8 +102,7 @@ export class RoomsService {
}
// Не возвращаем пароль в ответе
const roomWithoutPassword = { ...room };
delete roomWithoutPassword.password;
const { password, ...roomWithoutPassword } = room;
return roomWithoutPassword;
}

View file

@ -74,6 +74,29 @@ export class VoiceController {
}
}
@Post('test')
async testVoice(@Res() res: Response) {
this.logger.log('POST /voice/test - Test voice request received');
try {
const testText = 'Привет! Это тестовое сообщение голосового режима.';
const audioBuffer = await this.voiceService.generateTTS(testText);
this.logger.log(`POST /voice/test - Success, sending ${audioBuffer.length} bytes`);
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 test speech';
this.logger.error(`POST /voice/test - Error: ${message} (status: ${status})`);
return res.status(status).json({
error: message,
});
}
}
@Get('effects/:effectType')
async getEffect(
@Param('effectType') effectType: string,

View file

@ -1,7 +1,12 @@
import { useEffect, useRef } from 'react'
import VoicePlayer from './VoicePlayer'
import { useVoice } from '../hooks/useVoice'
import './Answer.css'
const Answer = ({ answer, onClick, isRevealed, roomId, questionId }) => {
const { autoPlayAnswers, speak } = useVoice()
const wasRevealedRef = useRef(false)
const getAnswerClass = () => {
if (!isRevealed) return 'answer-hidden'
return 'answer-revealed'
@ -15,6 +20,18 @@ const Answer = ({ answer, onClick, isRevealed, roomId, questionId }) => {
return '#ff6b6b'
}
// Auto-play answer when it becomes revealed
useEffect(() => {
if (isRevealed && !wasRevealedRef.current && autoPlayAnswers && roomId && questionId && answer.id) {
console.log('[Answer] Auto-playing answer:', { roomId, questionId, answerId: answer.id })
speak({ roomId, questionId, contentType: 'answer', answerId: answer.id })
wasRevealedRef.current = true
}
if (!isRevealed) {
wasRevealedRef.current = false
}
}, [isRevealed, autoPlayAnswers, roomId, questionId, answer.id, speak])
return (
<button
className={`answer-button ${getAnswerClass()}`}

View file

@ -7,17 +7,19 @@ const VoiceSettings = () => {
isEnabled,
volume,
effectsVolume,
autoPlayAnswers,
setIsEnabled,
setVolume,
setEffectsVolume,
speak,
setAutoPlayAnswers,
testVoice,
playEffect,
} = useVoice();
const [isOpen, setIsOpen] = useState(false);
const handleTestVoice = () => {
speak('Привет! Это тестовое сообщение голосового режима.');
testVoice();
};
const handleTestEffect = (effectType) => {
@ -78,6 +80,20 @@ const VoiceSettings = () => {
</button>
</div>
{/* Auto-play Answers */}
<div className="voice-settings-section">
<label className="voice-settings-toggle">
<input
type="checkbox"
checked={autoPlayAnswers}
onChange={(e) => setAutoPlayAnswers(e.target.checked)}
/>
<span className="voice-settings-toggle-label">
Озвучивать ответы при открытии
</span>
</label>
</div>
{/* Effects Volume */}
<div className="voice-settings-section">
<label className="voice-settings-label">

View file

@ -21,6 +21,11 @@ export function useVoice() {
return saved ? parseFloat(saved) : 0.6;
});
const [autoPlayAnswers, setAutoPlayAnswers] = useState(() => {
const saved = localStorage.getItem('voice-auto-play-answers');
return saved ? JSON.parse(saved) : false;
});
const [isPlaying, setIsPlaying] = useState(false);
const [currentText, setCurrentText] = useState(null);
@ -40,6 +45,10 @@ export function useVoice() {
localStorage.setItem('effects-volume', effectsVolume.toString());
}, [effectsVolume]);
useEffect(() => {
localStorage.setItem('voice-auto-play-answers', JSON.stringify(autoPlayAnswers));
}, [autoPlayAnswers]);
/**
* Generate speech from question/answer IDs
* @param {Object} params - Parameters
@ -222,6 +231,63 @@ export function useVoice() {
}
}, []);
/**
* Test voice with a constant phrase
*/
const testVoice = useCallback(async () => {
if (!isEnabled) {
console.warn('[useVoice] Voice is disabled, cannot test');
return;
}
try {
console.log('[useVoice] Testing voice with test endpoint');
const response = await fetch(`${API_URL}/voice/test`, {
method: 'POST',
credentials: 'include',
});
console.log('[useVoice] Test voice response status:', response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unable to read error response');
console.error('[useVoice] Test voice request failed:', {
status: response.status,
statusText: response.statusText,
errorText,
});
throw new Error(`Voice test error: ${response.status} - ${errorText}`);
}
const blob = await response.blob();
console.log('[useVoice] Received test audio blob, size:', blob.size, 'bytes, type:', blob.type);
const audioUrl = URL.createObjectURL(blob);
const audio = new Audio(audioUrl);
audio.volume = volume;
audio.onended = () => {
URL.revokeObjectURL(audioUrl);
console.log('[useVoice] Test voice playback ended');
};
audio.onerror = () => {
console.error('[useVoice] Test voice audio playback error');
URL.revokeObjectURL(audioUrl);
};
console.log('[useVoice] Playing test voice');
await audio.play();
} catch (error) {
console.error('[useVoice] Failed to test voice:', error);
console.error('[useVoice] Error details:', {
message: error?.message,
stack: error?.stack,
API_URL,
});
}
}, [isEnabled, volume]);
/**
* Play sound effect
* @param {string} effectType - Type of effect (correct/error/victory)
@ -296,13 +362,16 @@ export function useVoice() {
currentText,
volume,
effectsVolume,
autoPlayAnswers,
// Actions
setIsEnabled,
setVolume,
setEffectsVolume,
setAutoPlayAnswers,
speak,
stop,
testVoice,
playEffect,
preload,
preloadBatch,