Setzt den in #84 skizzierten Workflow um: record.html nimmt im Handy-Browser eine Sprachnotiz per MediaRecorder auf und laedt sie unauthentifiziert zu POST /api/v1/agents/{id}/recordings hoch. Ein Klick auf die Personen-Figur jedes registrierten Agenten in der Buero-Visualisierung verlinkt dorthin, ein Zaehler am Schreibtisch zeigt offene Aufnahmen. Der Agent selbst holt seine Warteschlange ueber die bestehenden Api-Key-authentifizierten Endpunkte ab (GET .../recordings, GET /recordings/{id}/audio) und markiert eine Aufnahme nach der Transkription per PATCH als erledigt - die eigentliche Transkription (whisper.cpp/faster-whisper) bleibt agent-seitig, siehe fds2610/Agent#17. Schliesst #85
135 lines
4.9 KiB
PHP
135 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Monitor\Api\Controller;
|
|
|
|
use InvalidArgumentException;
|
|
use Monitor\Api\Http\Request;
|
|
use Monitor\Api\Http\Response;
|
|
use Monitor\Api\Repository\AgentRepository;
|
|
use Monitor\Api\Repository\VoiceRecordingRepository;
|
|
|
|
/**
|
|
* Issue #85: Sprachaufnahme-Warteschlange. Zwei Zugriffsarten mit unterschiedlicher
|
|
* Berechtigung, analog zu HostConfigController:
|
|
*
|
|
* - Upload aus record.html (Handy-Browser eines Menschen, kein registrierter Agent) -
|
|
* unauthentifiziert wie die uebrigen von der UI ausgeloesten Schreibvorgaenge auf oeffentlich
|
|
* verlinkten Seiten; die Agenten-Id kommt als URL-Segment mit.
|
|
* - Abholen/Markieren durch den Agenten selbst - hinter dem bestehenden Agenten-API-Key,
|
|
* mit Ownership-Check wie bei AgentController::activity() (ein Agent sieht/holt nur seine
|
|
* eigene Warteschlange).
|
|
*/
|
|
final class VoiceRecordingController
|
|
{
|
|
public function __construct(
|
|
private readonly VoiceRecordingRepository $recordings,
|
|
private readonly AgentRepository $agents,
|
|
) {
|
|
}
|
|
|
|
/** @param array<string, string> $params */
|
|
public function upload(Request $request, array $params): Response
|
|
{
|
|
$agentId = (int) $params['id'];
|
|
if ($this->agents->findById($agentId) === null) {
|
|
return Response::error('Unbekannter Agent: ' . $agentId, 404);
|
|
}
|
|
|
|
$file = $request->files['audio'] ?? null;
|
|
if (!is_array($file) || !isset($file['tmp_name'], $file['error'])) {
|
|
return Response::error('Feld "audio" (Datei) ist erforderlich.', 422);
|
|
}
|
|
|
|
$error = (int) $file['error'];
|
|
if ($error === UPLOAD_ERR_INI_SIZE || $error === UPLOAD_ERR_FORM_SIZE) {
|
|
return Response::error('Datei zu gross.', 413);
|
|
}
|
|
if ($error !== UPLOAD_ERR_OK) {
|
|
return Response::error('Datei-Upload fehlgeschlagen.', 422);
|
|
}
|
|
|
|
$contents = @file_get_contents((string) $file['tmp_name']);
|
|
if ($contents === false) {
|
|
return Response::error('Datei-Upload fehlgeschlagen.', 422);
|
|
}
|
|
|
|
try {
|
|
$recording = $this->recordings->store($agentId, $contents, (string) ($file['type'] ?? ''));
|
|
} catch (InvalidArgumentException $e) {
|
|
return Response::error($e->getMessage(), 422);
|
|
}
|
|
|
|
return Response::json(['recording' => $recording], 201);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $params
|
|
* @param array<string, mixed> $authenticatedAgent
|
|
*/
|
|
public function index(Request $request, array $params, array $authenticatedAgent): Response
|
|
{
|
|
if ((int) $params['id'] !== (int) $authenticatedAgent['id']) {
|
|
return Response::error('Ein Agent darf nur seine eigene Warteschlange abfragen.', 403);
|
|
}
|
|
|
|
$status = isset($request->query['status']) ? (string) $request->query['status'] : null;
|
|
|
|
try {
|
|
$recordings = $this->recordings->listForAgent((int) $params['id'], $status);
|
|
} catch (InvalidArgumentException $e) {
|
|
return Response::error($e->getMessage(), 400);
|
|
}
|
|
|
|
return Response::json(['recordings' => $recordings]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $params
|
|
* @param array<string, mixed> $authenticatedAgent
|
|
*/
|
|
public function download(Request $request, array $params, array $authenticatedAgent): Response
|
|
{
|
|
$recording = $this->recordings->findById((int) $params['id']);
|
|
if ($recording === null) {
|
|
return Response::error('Aufnahme nicht gefunden.', 404);
|
|
}
|
|
if ((int) $recording['agent_id'] !== (int) $authenticatedAgent['id']) {
|
|
return Response::error('Diese Aufnahme gehoert einem anderen Agenten.', 403);
|
|
}
|
|
|
|
$bytes = $this->recordings->readAudio((int) $recording['id']);
|
|
if ($bytes === null) {
|
|
return Response::error('Audiodatei nicht gefunden.', 404);
|
|
}
|
|
|
|
return Response::binary($bytes, $recording['mime_type'], 200, [
|
|
'Content-Disposition' => 'attachment; filename="recording-' . $recording['id'] . '"',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $params
|
|
* @param array<string, mixed> $authenticatedAgent
|
|
*/
|
|
public function markProcessed(Request $request, array $params, array $authenticatedAgent): Response
|
|
{
|
|
$recording = $this->recordings->findById((int) $params['id']);
|
|
if ($recording === null) {
|
|
return Response::error('Aufnahme nicht gefunden.', 404);
|
|
}
|
|
if ((int) $recording['agent_id'] !== (int) $authenticatedAgent['id']) {
|
|
return Response::error('Diese Aufnahme gehoert einem anderen Agenten.', 403);
|
|
}
|
|
|
|
$status = (string) ($request->body['status'] ?? '');
|
|
if ($status !== 'done') {
|
|
return Response::error('Feld "status" muss "done" sein.', 422);
|
|
}
|
|
|
|
$updated = $this->recordings->markDone((int) $recording['id']);
|
|
|
|
return Response::json(['recording' => $updated]);
|
|
}
|
|
}
|