bericht/api/transcribe.php
Eduard Wisch 344f884a0f
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
feat: Bericht-Vorlagen + Whisper-Transkription + Cron-Fix
Bericht-Vorlagen (Phase 5.5):
- DB: is_template, template_label in llx_bericht
- Bericht::fetchAllTemplates() und createFromTemplate()
- fetchAllForElement() blendet Vorlagen aus
- ajax/save_as_template.php erzeugt Vorlage aus aktuellem Bericht
- Desktop-Editor: '📋 Als Vorlage' Button im Action-Bereich
- Bericht-Übersicht: Vorlagen-Dropdown beim + Neu Button
- api/templates.php: GET list + POST create_from_template

Schnell-Bericht (Phase 4.a/4.i):
- api/reports.php?action=create POST-Endpoint: Titel, Format,
  Orientation, ODT-Template, optional template_id
- api/odt_templates.php: Liste der Deckblatt-Vorlagen

Whisper-Transkription (Phase 5.7):
- api/transcribe.php: POST mit relpath, nutzt externen Whisper-
  HTTP-Endpoint (whisper.cpp server ODER OpenAI-kompatibel)
- Konfiguration im Admin: BERICHT_WHISPER_URL/MODE/API_KEY/LANG
- Sprache default 'de'

Cron-Fix:
- BerichtUploadToken::cleanupExpired() ist jetzt Instanz-Methode
  (Dolibarr ruft new Klasse($db) bei jobtype=method auf)
- Returnwert für Cron-Success/Failure
- Statische Variante als cleanupExpiredStatic() für direkte Aufrufe
- Damit läuft der tägliche Cron 'Expired Upload-Tokens bereinigen'
  nicht mehr hängend

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
2026-04-09 08:27:45 +02:00

92 lines
3 KiB
PHP

<?php
/* POST /api/transcribe.php
* Body: { relpath: "commande/SO.../voice_xxx.webm" }
* → Lädt die Audio-Datei zu einem Whisper-HTTP-Endpoint und gibt den Text zurück.
*
* Der Whisper-Endpoint wird über die Konstante BERICHT_WHISPER_URL konfiguriert.
* Default: leer → Feature deaktiviert, liefert Hinweis.
*
* Unterstützt gängige Whisper-HTTP-APIs:
* - whisper.cpp server (POST /inference mit file= field)
* - OpenAI-kompatibel (POST /v1/audio/transcriptions mit file + model=whisper-1)
* - faster-whisper-server (POST /v1/audio/transcriptions)
*
* Auswahl per BERICHT_WHISPER_MODE: 'whispercpp' (default) | 'openai'
*/
require_once __DIR__.'/_inc.php';
api_authenticate();
global $db, $user;
$in = api_input();
$relpath = (string) ($in['relpath'] ?? $_POST['relpath'] ?? '');
if (empty($relpath)) api_fail('relpath fehlt');
if (!preg_match('#^(commande|facture|propal|bericht)/#', $relpath)) {
api_fail('Pfad nicht erlaubt', 403);
}
$full = bericht_resolve_data_path($relpath);
if (!$full || !file_exists($full)) api_fail('Datei nicht gefunden', 404);
$whisper_url = getDolGlobalString('BERICHT_WHISPER_URL', '');
$mode = getDolGlobalString('BERICHT_WHISPER_MODE', 'whispercpp');
$api_key = getDolGlobalString('BERICHT_WHISPER_API_KEY', '');
$language = getDolGlobalString('BERICHT_WHISPER_LANG', 'de');
if (empty($whisper_url)) {
api_fail('Whisper ist nicht konfiguriert. Admin muss BERICHT_WHISPER_URL setzen.', 501);
}
// Datei-MIME ermitteln
$mime = function_exists('mime_content_type') ? mime_content_type($full) : 'audio/webm';
$ch = curl_init();
$cfile = new CURLFile($full, $mime, basename($full));
if ($mode === 'openai') {
curl_setopt_array($ch, array(
CURLOPT_URL => rtrim($whisper_url, '/').'/v1/audio/transcriptions',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => array(
'file' => $cfile,
'model' => 'whisper-1',
'language' => $language,
),
CURLOPT_HTTPHEADER => $api_key ? array('Authorization: Bearer '.$api_key) : array(),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
));
} else {
// whisper.cpp server /inference
curl_setopt_array($ch, array(
CURLOPT_URL => rtrim($whisper_url, '/').'/inference',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => array(
'file' => $cfile,
'language' => $language,
'response_format' => 'json',
),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
));
}
$resp = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err) api_fail('Whisper-Fehler: '.$err, 502);
if ($status !== 200) api_fail('Whisper HTTP '.$status.': '.substr($resp, 0, 200), 502);
$data = json_decode($resp, true);
$text = '';
if (is_array($data)) {
$text = $data['text'] ?? ($data['transcription'] ?? '');
} else {
$text = (string) $resp;
}
$text = trim($text);
api_ok(array('text' => $text, 'language' => $language));