import { Body, Controller, Get, Inject, Param, Post, Query } from "@nestjs/common"; import { VoiceSheetService } from "./voice-sheet.service.js"; import { ProsodyService } from "./prosody.service.js"; import { NonverbalService } from "./nonverbal.service.js"; import { ParalinguisticService, type ParalinguisticFeatures } from "./paralinguistic.service.js"; import { TTS_PROVIDER, type TTSProvider } from "./tts-provider.js"; import { EmotionService } from "../emotion/emotion.service.js"; interface SeedVoiceSheetBody { verbalTics?: string[]; forbiddenSounds?: string[]; } interface SynthesizeBody { text: string; now?: string; seed?: number; } interface AnalyzeInputBody { userId: string; text: string; paralinguistic: ParalinguisticFeatures; now?: string; } @Controller("voice") export class VoiceController { constructor( private readonly voiceSheet: VoiceSheetService, private readonly prosody: ProsodyService, private readonly nonverbal: NonverbalService, private readonly paralinguistic: ParalinguisticService, private readonly emotion: EmotionService, @Inject(TTS_PROVIDER) private readonly ttsProvider: TTSProvider, ) {} @Post(":characterId/voice-sheet/seed-default") async seedVoiceSheet(@Param("characterId") characterId: string, @Body() body: SeedVoiceSheetBody) { return this.voiceSheet.seedDefault(characterId, body); } @Get(":characterId/voice-sheet") async getVoiceSheet(@Param("characterId") characterId: string) { return this.voiceSheet.get(characterId); } @Get(":characterId/:userId/prosody") async getProsody( @Param("characterId") characterId: string, @Param("userId") userId: string, @Query("now") now?: string, ) { return this.prosody.resolveProsody(characterId, userId, now ? new Date(now) : undefined); } @Get(":characterId/:userId/nonverbal-cue") async getNonverbalCue( @Param("characterId") characterId: string, @Param("userId") userId: string, @Query("now") now?: string, ) { return this.nonverbal.resolveCue(characterId, userId, now ? new Date(now) : undefined); } @Post(":characterId/:userId/synthesize") async synthesize( @Param("characterId") characterId: string, @Param("userId") userId: string, @Body() body: SynthesizeBody, ) { const now = body.now ? new Date(body.now) : undefined; const [voiceSheet, resolvedProsody, nonverbalCue] = await Promise.all([ this.voiceSheet.getOrSeedDefault(characterId), this.prosody.resolveProsody(characterId, userId, now), this.nonverbal.resolveCue(characterId, userId, now), ]); return this.ttsProvider.synthesize({ text: body.text, voiceSheet, prosody: resolvedProsody, nonverbalCue, seed: body.seed, }); } // P-5 驗收用端點:同一段文字配不同副語言特徵,應該得到不同的情緒標記結果。 @Post(":characterId/analyze-input") async analyzeInput(@Param("characterId") characterId: string, @Body() body: AnalyzeInputBody) { const now = body.now ? new Date(body.now) : new Date(); const paralinguisticSignal = this.paralinguistic.deriveSignal(body.paralinguistic) ?? undefined; const result = await this.emotion.processInput(characterId, body.text, { now, paralinguisticSignal }); return { ...result, paralinguisticSignal: paralinguisticSignal ?? null }; } }