feat:음성입력 STT 사용량 AI 토큰 추적 연동
logSttUsage 엔드포인트 추가, 녹음 종료 시 duration 전송 AI 토큰 사용량에 '공사현장사진대지-음성입력' 카테고리로 기록 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Juil;
|
||||
|
||||
use App\Helpers\AiTokenHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Juil\ConstructionSitePhoto;
|
||||
use App\Services\ConstructionSitePhotoService;
|
||||
@@ -233,4 +234,15 @@ public function downloadPhoto(Request $request, int $id, string $type): Response
|
||||
->header('Content-Disposition', "{$disposition}; filename=\"{$encodedFilename}\"; filename*=UTF-8''{$encodedFilename}")
|
||||
->header('Cache-Control', 'private, max-age=3600');
|
||||
}
|
||||
|
||||
public function logSttUsage(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'duration_seconds' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
AiTokenHelper::saveSttUsage('공사현장사진대지-음성입력', $validated['duration_seconds']);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
deletePhoto: (id, type) => `/juil/construction-photos/${id}/photo/${type}`,
|
||||
downloadPhoto: (id, type) => `/juil/construction-photos/${id}/download/${type}`,
|
||||
photoUrl: (id, type) => `/juil/construction-photos/${id}/download/${type}?inline=1`,
|
||||
logSttUsage: '/juil/construction-photos/log-stt-usage',
|
||||
};
|
||||
|
||||
const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
@@ -58,16 +59,30 @@ function VoiceInputButton({ onResult, disabled }) {
|
||||
const [recording, setRecording] = useState(false);
|
||||
const [interim, setInterim] = useState('');
|
||||
const recognitionRef = useRef(null);
|
||||
const startTimeRef = useRef(null);
|
||||
|
||||
const isSupported = typeof window !== 'undefined' &&
|
||||
(window.SpeechRecognition || window.webkitSpeechRecognition);
|
||||
|
||||
const logUsage = useCallback((startTime) => {
|
||||
const duration = Math.max(1, Math.round((Date.now() - startTime) / 1000));
|
||||
apiFetch(API.logSttUsage, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ duration_seconds: duration }),
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const stopRecording = useCallback(() => {
|
||||
recognitionRef.current?.stop();
|
||||
recognitionRef.current = null;
|
||||
if (startTimeRef.current) {
|
||||
logUsage(startTimeRef.current);
|
||||
startTimeRef.current = null;
|
||||
}
|
||||
setRecording(false);
|
||||
setInterim('');
|
||||
}, []);
|
||||
}, [logUsage]);
|
||||
|
||||
const startRecording = useCallback(() => {
|
||||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
@@ -96,12 +111,17 @@ function VoiceInputButton({ onResult, disabled }) {
|
||||
};
|
||||
recognition.onerror = () => stopRecording();
|
||||
recognition.onend = () => {
|
||||
if (startTimeRef.current) {
|
||||
logUsage(startTimeRef.current);
|
||||
startTimeRef.current = null;
|
||||
}
|
||||
setRecording(false);
|
||||
setInterim('');
|
||||
recognitionRef.current = null;
|
||||
};
|
||||
|
||||
recognitionRef.current = recognition;
|
||||
startTimeRef.current = Date.now();
|
||||
recognition.start();
|
||||
setRecording(true);
|
||||
setInterim('');
|
||||
|
||||
@@ -1325,5 +1325,6 @@
|
||||
Route::delete('/{id}', [ConstructionSitePhotoController::class, 'destroy'])->name('destroy');
|
||||
Route::delete('/{id}/photo/{type}', [ConstructionSitePhotoController::class, 'deletePhoto'])->name('delete-photo');
|
||||
Route::get('/{id}/download/{type}', [ConstructionSitePhotoController::class, 'downloadPhoto'])->name('download');
|
||||
Route::post('/log-stt-usage', [ConstructionSitePhotoController::class, 'logSttUsage'])->name('log-stt-usage');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user