feat:회의록 자동 화자 분리(Phase 2) 구현 및 세그먼트 저장 에러 수정
- GoogleCloudService에 speechToTextWithDiarization 메서드 추가
- Google STT V1 diarizationConfig 활성화로 자동 화자 구분
- MeetingMinuteService에 processDiarization 메서드 추가
- POST /{id}/diarize 엔드포인트 및 라우트 추가
- 프론트엔드에 '화자 분리' 버튼 추가 (RecordingControlBar)
- saveSegments 컨트롤러에 try-catch 에러 핸들링 추가
- 빈 텍스트 세그먼트 필터링 로직 추가 (서버/클라이언트 양쪽)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -135,21 +135,46 @@ public function saveSegments(Request $request, int $id): JsonResponse
|
||||
|
||||
$validated = $request->validate([
|
||||
'segments' => 'required|array',
|
||||
'segments.*.speaker_name' => 'required|string|max:100',
|
||||
'segments.*.speaker_name' => 'nullable|string|max:100',
|
||||
'segments.*.speaker_label' => 'nullable|string|max:20',
|
||||
'segments.*.text' => 'required|string',
|
||||
'segments.*.text' => 'nullable|string',
|
||||
'segments.*.start_time_ms' => 'nullable|integer|min:0',
|
||||
'segments.*.end_time_ms' => 'nullable|integer|min:0',
|
||||
'segments.*.is_manual_speaker' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$meeting = $this->service->saveSegments($meeting, $validated['segments']);
|
||||
// 빈 텍스트 세그먼트 필터링
|
||||
$segments = array_values(array_filter($validated['segments'], function ($seg) {
|
||||
return ! empty(trim($seg['text'] ?? ''));
|
||||
}));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '세그먼트가 저장되었습니다.',
|
||||
'data' => $meeting,
|
||||
]);
|
||||
if (empty($segments)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '저장할 세그먼트가 없습니다.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$meeting = $this->service->saveSegments($meeting, $segments);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '세그먼트가 저장되었습니다.',
|
||||
'data' => $meeting,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('MeetingMinute: 세그먼트 저장 실패', [
|
||||
'meeting_id' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '세그먼트 저장 중 오류가 발생했습니다: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadAudio(Request $request, int $id): JsonResponse
|
||||
@@ -218,6 +243,44 @@ public function summarize(int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
public function diarize(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$meeting = MeetingMinute::find($id);
|
||||
|
||||
if (! $meeting) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '회의록을 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (empty($meeting->audio_gcs_uri)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '오디오 파일이 없습니다. 먼저 녹음을 진행해주세요.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$minSpeakers = (int) ($request->input('min_speakers', 2));
|
||||
$maxSpeakers = (int) ($request->input('max_speakers', 6));
|
||||
|
||||
$result = $this->service->processDiarization($meeting, $minSpeakers, $maxSpeakers);
|
||||
|
||||
if (! $result) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '자동 화자 분리에 실패했습니다.',
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "자동 화자 분리가 완료되었습니다. ({$result['speaker_count']}명 감지)",
|
||||
'data' => $meeting->fresh()->load('segments'),
|
||||
'speaker_count' => $result['speaker_count'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadAudio(Request $request, int $id): Response|JsonResponse
|
||||
{
|
||||
$meeting = MeetingMinute::find($id);
|
||||
|
||||
Reference in New Issue
Block a user