| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/clients/client2/web20/web/wp-content/plugins/dgt-verkehrsmonitor/includes/ |
Upload File : |
<?php
/**
* KI-Analyse: Polizeimeldungen strukturiert auswerten (Claude API).
*
* Cron: alle 30 Minuten.
*
* @package dgt-verkehrsmonitor
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/* ─── API-Aufruf ───────────────────────────────────────── */
function dgt_vm_call_claude( $prompt, $system = '' ) {
$api_key = get_option( 'dgt_vm_api_key' );
if ( ! $api_key ) {
return new WP_Error( 'no_api_key', 'Kein Claude API-Key konfiguriert.' );
}
$model = get_option( 'dgt_vm_ai_model', 'claude-haiku-4-5-20251001' );
$messages = array(
array( 'role' => 'user', 'content' => $prompt ),
);
$body = array(
'model' => $model,
'max_tokens' => 1200,
'messages' => $messages,
);
if ( $system ) {
$body['system'] = $system;
}
$response = wp_remote_post( 'https://api.anthropic.com/v1/messages', array(
'timeout' => 30,
'headers' => array(
'Content-Type' => 'application/json',
'x-api-key' => $api_key,
'anthropic-version' => '2023-06-01',
),
'body' => wp_json_encode( $body ),
) );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( 200 !== $code ) {
$msg = isset( $data['error']['message'] ) ? $data['error']['message'] : 'HTTP ' . $code;
return new WP_Error( 'api_error', $msg );
}
if ( isset( $data['content'][0]['text'] ) ) {
return $data['content'][0]['text'];
}
return new WP_Error( 'empty_response', 'Leere Antwort von der API.' );
}
/* ─── Analyse-Prompt ───────────────────────────────────── */
function dgt_vm_analysis_system() {
return 'Du bist ein Analyst für Verkehrsdelikte in Deutschland. '
. 'Analysiere den folgenden Polizeibericht und extrahiere strukturierte Daten. '
. 'Antworte ausschließlich mit einem validen JSON-Objekt, ohne Markdown-Formatierung.';
}
function dgt_vm_analysis_prompt( $title, $text ) {
return 'Polizeibericht:
Titel: ' . $title . '
Text: ' . mb_substr( $text, 0, 4000 ) . '
Extrahiere folgende Felder als JSON:
{
"relevant": true/false (true = Alkohol/Drogen im Straßenverkehr, Trunkenheitsfahrt, Drogenfahrt, Verkehrskontrolle mit positivem Test),
"headline": "SEO-optimierte Überschrift, max 70 Zeichen, journalistischer Stil",
"summary": "Zusammenfassung in 2-3 Sätzen, journalistischer Stil",
"slug": "seo-url-slug-mit-keywords (z.B. alkoholfahrt-a9-muenchen)",
"delikt": "alkohol" | "drogen" | "beides" | "sonstiges",
"hat_alkohol": true/false,
"hat_drogen": true/false,
"substanzen": ["cannabis", "kokain", "amphetamin", "heroin", "mdma", "mischkonsum", "medikamente"] (nur zutreffende, leeres Array wenn nur Alkohol),
"bak_wert": 1.80 (Dezimalzahl in Promille, null wenn nicht erwähnt),
"thc_wert": 3.5 (ng/ml, null wenn nicht erwähnt),
"alter_fahrer": 34 (null wenn nicht erwähnt),
"geschlecht": "m" | "w" | "unbekannt",
"hat_unfall": true/false,
"unfall_schwere": "keine" | "sachschaden" | "personenschaden" | "toedlich",
"fs_sichergestellt": true/false,
"fahrzeugart": "pkw" | "lkw" | "motorrad" | "fahrrad" | "e-scooter" | "sonstiges",
"stadt": "Stadtname (aus dem Text extrahiert, leer wenn unklar)",
"rechtsinfo": "Kurze rechtliche Einordnung (1-2 Sätze, z.B. Ab 1,1 Promille gilt absolute Fahruntüchtigkeit...)"
}
Wichtig:
- relevant=false wenn der Bericht keinen Bezug zu Alkohol/Drogen im Straßenverkehr hat
- Alle Werte müssen plausibel sein (BAK 0.3-4.0, THC 1-100, Alter 14-99)
- slug: nur Kleinbuchstaben, Bindestriche, keine Umlaute (ue statt ü)
- Antworte NUR mit dem JSON-Objekt';
}
/* ─── JSON parsen (robust) ─────────────────────────────── */
function dgt_vm_parse_ai_json( $text ) {
$text = trim( $text );
// Markdown-Code-Blöcke entfernen.
if ( preg_match( '/```(?:json)?\s*(\{.*?\})\s*```/s', $text, $m ) ) {
$text = $m[1];
}
// Nur das JSON-Objekt extrahieren.
if ( preg_match( '/(\{.*\})/s', $text, $m ) ) {
$text = $m[1];
}
$data = json_decode( $text, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return null;
}
return $data;
}
/* ─── Einzelne Meldung analysieren ─────────────────────── */
function dgt_vm_analyze_incident( $incident ) {
global $wpdb;
$t = dgt_vm_table( 'incidents' );
$prompt = dgt_vm_analysis_prompt( $incident->original_titel, $incident->original_text );
$result = dgt_vm_call_claude( $prompt, dgt_vm_analysis_system() );
if ( is_wp_error( $result ) ) {
error_log( 'VM Analyzer Error (ID ' . $incident->id . '): ' . $result->get_error_message() );
return false;
}
$data = dgt_vm_parse_ai_json( $result );
if ( ! $data ) {
error_log( 'VM Analyzer: Ungültiges JSON für ID ' . $incident->id );
$wpdb->update( $t, array( 'status' => 'abgelehnt', 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $incident->id ) );
return false;
}
// Nicht relevant → ablehnen.
if ( empty( $data['relevant'] ) ) {
$wpdb->update( $t, array( 'status' => 'abgelehnt', 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $incident->id ) );
return false;
}
// Slug-Kollision prüfen.
$slug = trim( sanitize_title( isset( $data['slug'] ) ? $data['slug'] : '' ), '-' );
if ( ! $slug ) {
$slug = trim( sanitize_title( isset( $data['headline'] ) ? $data['headline'] : 'meldung-' . $incident->id ), '-' );
}
$orig_slug = $slug;
$suffix = 2;
while ( $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$t} WHERE ai_slug = %s AND bundesland = %s AND id != %d", $slug, $incident->bundesland, $incident->id ) ) ) {
$slug = $orig_slug . '-' . $suffix;
$suffix++;
}
// Substanzen als JSON-Array.
$substanzen = '';
if ( ! empty( $data['substanzen'] ) && is_array( $data['substanzen'] ) ) {
$substanzen = wp_json_encode( array_values( $data['substanzen'] ) );
}
// Stadt aus KI oder bestehend.
$stadt = ! empty( $data['stadt'] ) ? sanitize_text_field( $data['stadt'] ) : $incident->stadt;
$now = current_time( 'mysql' );
// Bulk-Checkpoint: Fallzahlen per Regex extrahieren.
$anzahl_alkohol = 1;
$anzahl_drogen = 1;
if ( ! empty( $data['hat_alkohol'] ) ) {
$bulk = dgt_vm_count_substance_cases( $incident->original_text, 'alkohol' );
if ( $bulk > 1 ) {
$anzahl_alkohol = $bulk;
}
}
if ( ! empty( $data['hat_drogen'] ) ) {
$bulk = dgt_vm_count_substance_cases( $incident->original_text, 'drogen' );
if ( $bulk > 1 ) {
$anzahl_drogen = $bulk;
}
}
$update = array(
'ai_headline' => mb_substr( sanitize_text_field( isset( $data['headline'] ) ? $data['headline'] : '' ), 0, 300 ),
'ai_summary' => sanitize_textarea_field( isset( $data['summary'] ) ? $data['summary'] : '' ),
'ai_slug' => $slug,
'ai_rechtsinfo' => sanitize_textarea_field( isset( $data['rechtsinfo'] ) ? $data['rechtsinfo'] : '' ),
'delikt' => in_array( $data['delikt'], array( 'alkohol', 'drogen', 'beides', 'sonstiges' ), true ) ? $data['delikt'] : 'sonstiges',
'hat_alkohol' => ! empty( $data['hat_alkohol'] ) ? 1 : 0,
'hat_drogen' => ! empty( $data['hat_drogen'] ) ? 1 : 0,
'substanzen' => $substanzen,
'bak_wert' => isset( $data['bak_wert'] ) && is_numeric( $data['bak_wert'] ) && $data['bak_wert'] > 0 ? round( (float) $data['bak_wert'], 2 ) : null,
'thc_wert' => isset( $data['thc_wert'] ) && is_numeric( $data['thc_wert'] ) && $data['thc_wert'] > 0 ? round( (float) $data['thc_wert'], 1 ) : null,
'alter_fahrer' => isset( $data['alter_fahrer'] ) && is_numeric( $data['alter_fahrer'] ) && $data['alter_fahrer'] >= 14 && $data['alter_fahrer'] <= 99 ? (int) $data['alter_fahrer'] : null,
'geschlecht' => in_array( $data['geschlecht'], array( 'm', 'w' ), true ) ? $data['geschlecht'] : 'unbekannt',
'hat_unfall' => ! empty( $data['hat_unfall'] ) ? 1 : 0,
'unfall_schwere' => in_array( $data['unfall_schwere'], array( 'keine', 'sachschaden', 'personenschaden', 'toedlich' ), true ) ? $data['unfall_schwere'] : 'keine',
'fs_sichergestellt' => ! empty( $data['fs_sichergestellt'] ) ? 1 : 0,
'fahrzeugart' => sanitize_text_field( isset( $data['fahrzeugart'] ) ? $data['fahrzeugart'] : 'pkw' ),
'anzahl_alkohol' => $anzahl_alkohol,
'anzahl_drogen' => $anzahl_drogen,
'stadt' => $stadt,
'status' => 'publiziert',
'published_at' => $now,
'updated_at' => $now,
);
$wpdb->update( $t, $update, array( 'id' => $incident->id ) );
return true;
}
/* ─── Bulk-Checkpoint: Fallzahlen extrahieren ─────────── */
function dgt_vm_count_substance_cases( $text, $type = 'alkohol' ) {
$number_words = array(
'einem' => 1, 'einer' => 1, 'einen' => 1, 'ein' => 1,
'zwei' => 2, 'drei' => 3, 'vier' => 4, 'fünf' => 5,
'sechs' => 6, 'sieben' => 7, 'acht' => 8, 'neun' => 9, 'zehn' => 10,
'elf' => 11, 'zwölf' => 12, 'dreizehn' => 13, 'vierzehn' => 14, 'fünfzehn' => 15,
'sechzehn' => 16, 'siebzehn' => 17, 'achtzehn' => 18, 'neunzehn' => 19, 'zwanzig' => 20,
);
$num_alts = implode( '|', array_keys( $number_words ) );
$num_pattern = '(\d+|' . $num_alts . ')';
if ( 'alkohol' === $type ) {
$substance = '(?:alkohol|promille|atemalkohol|trunkenheit|alkoholkonsum)';
} else {
$substance = '(?:drogen|btm|betäubungsmittel|berauschende\s+mittel|cannabis|drogenkonsum)';
}
$total = 0;
if ( preg_match_all( '/' . $num_pattern . '\s+(?:fällen|fälle|fall)\b.{0,150}?' . $substance . '/ui', $text, $matches, PREG_SET_ORDER ) ) {
foreach ( $matches as $m ) {
$n = is_numeric( $m[1] ) ? (int) $m[1] : ( $number_words[ mb_strtolower( $m[1] ) ] ?? 0 );
$total += $n;
}
}
if ( preg_match_all( '/' . $num_pattern . '\s+(?:blutproben|blutentnahmen).{0,150}?' . $substance . '/ui', $text, $matches, PREG_SET_ORDER ) ) {
foreach ( $matches as $m ) {
$n = is_numeric( $m[1] ) ? (int) $m[1] : ( $number_words[ mb_strtolower( $m[1] ) ] ?? 0 );
$total += $n;
}
}
if ( preg_match_all( '/' . $num_pattern . '\s+(?:alkoholfahrten|trunkenheitsfahrten)\b/ui', $text, $matches, PREG_SET_ORDER ) && 'alkohol' === $type ) {
foreach ( $matches as $m ) {
$n = is_numeric( $m[1] ) ? (int) $m[1] : ( $number_words[ mb_strtolower( $m[1] ) ] ?? 0 );
$total += $n;
}
}
if ( preg_match_all( '/' . $num_pattern . '\s+(?:drogenfahrten)\b/ui', $text, $matches, PREG_SET_ORDER ) && 'drogen' === $type ) {
foreach ( $matches as $m ) {
$n = is_numeric( $m[1] ) ? (int) $m[1] : ( $number_words[ mb_strtolower( $m[1] ) ] ?? 0 );
$total += $n;
}
}
return $total > 1 ? $total : 0;
}
/* ─── Regex-Fallback-Analyse (ohne API) ───────────────── */
function dgt_vm_quick_analyze( $incident ) {
global $wpdb;
$t = dgt_vm_table( 'incidents' );
$text = mb_strtolower( $incident->original_titel . ' ' . $incident->original_text );
// Verkehrskontext prüfen: muss Substanz + Verkehr enthalten.
if ( ! dgt_vm_is_relevant_text( $text ) ) {
$wpdb->update( $t, array( 'status' => 'abgelehnt', 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $incident->id ) );
return false;
}
$hat_alkohol = (bool) preg_match( '/alkohol|promille|trunkenheit|betrunken|atemalkohol|blutalkohol|trunkenheitsfahrt/u', $text );
$hat_drogen = (bool) preg_match( '/drogen|btm|cannabis|thc|kokain|amphetamin|speed|mdma|ecstasy|heroin|marihuana|betäubungsmittel|drogentest|drogenfahrt/u', $text );
if ( ! $hat_alkohol && ! $hat_drogen ) {
$wpdb->update( $t, array( 'status' => 'abgelehnt', 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $incident->id ) );
return false;
}
$delikt = 'sonstiges';
if ( $hat_alkohol && $hat_drogen ) {
$delikt = 'beides';
} elseif ( $hat_alkohol ) {
$delikt = 'alkohol';
} elseif ( $hat_drogen ) {
$delikt = 'drogen';
}
// BAK extrahieren — bei mehreren Angaben die präziseste nehmen (z.B. 0,52 statt 0,5 Vortest).
$bak = null;
$bak_precision = 0;
if ( preg_match_all( '/(\d)[,.](\d{1,2})\s*promille/u', $text, $all_m, PREG_SET_ORDER ) ) {
foreach ( $all_m as $m ) {
$val = (float) ( $m[1] . '.' . $m[2] );
if ( $val >= 0.3 && $val <= 4.0 && strlen( $m[2] ) > $bak_precision ) {
$bak = $val;
$bak_precision = strlen( $m[2] );
}
}
}
// THC.
$thc = null;
if ( preg_match( '/(\d+(?:[,.]\d+)?)\s*(?:ng|nanogramm)/u', $text, $m ) ) {
$thc = (float) str_replace( ',', '.', $m[1] );
if ( $thc < 1 || $thc > 100 ) $thc = null;
}
// Substanzen — alle nachgewiesenen Substanzen auflisten.
$substanzen = array();
if ( $hat_alkohol ) {
$substanzen[] = 'alkohol';
}
$sub_map = array(
'cannabis' => '/cannabis|marihuana|\bthc\b|joint|cannabiskonsum/u',
'kokain' => '/kokain|koks/u',
'amphetamin' => '/amphetamin|speed/u',
'heroin' => '/heroin/u',
'mdma' => '/mdma|ecstasy|xtc/u',
'medikamente' => '/medikament|rezeptpflichtig|benzodiazep/u',
);
foreach ( $sub_map as $name => $pattern ) {
if ( preg_match( $pattern, $text ) ) {
$substanzen[] = $name;
}
}
// Bei Drogen ohne spezifische Substanz: "sonstige" hinzufügen.
if ( $hat_drogen && count( array_diff( $substanzen, array( 'alkohol' ) ) ) === 0 ) {
$substanzen[] = 'sonstige';
}
// DUI-relevanten Textabschnitt extrahieren (für Multi-Case-Artikel).
$dui_text = $text;
$dui_orig = mb_strtolower( $incident->original_text );
if ( preg_match( '/(?:trunkenheitsfahrt|alkoholfahrt|drogenfahrt)\s*\n+(.{50,1500})/usi', $incident->original_text, $sec ) ) {
$dui_text = mb_strtolower( $sec[0] );
$dui_orig = $dui_text;
}
// Alter — bevorzugt nahe DUI-Kontext.
$alter = null;
if ( preg_match( '/(?:alkohol|promille|trunken|drogen|btm|berauscht).{0,200}?(\d{2})\s*-?\s*jährige[rn]?/ui', $incident->original_text, $m ) ) {
$alter = (int) $m[1];
} elseif ( preg_match( '/(\d{2})\s*-?\s*jährige[rn]?.{0,200}?(?:alkohol|promille|trunken|drogen|btm|berauscht)/ui', $incident->original_text, $m ) ) {
$alter = (int) $m[1];
} elseif ( preg_match( '/(\d{2})\s*-?\s*jährige[rn]?/u', $text, $m ) ) {
$alter = (int) $m[1];
} elseif ( preg_match( '/\((\d{2})\)/u', $text, $m ) ) {
$alter = (int) $m[1];
}
if ( $alter !== null && ( $alter < 14 || $alter > 99 ) ) $alter = null;
// Geschlecht — nahe DUI-Kontext prüfen.
$geschlecht = 'unbekannt';
if ( preg_match( '/\b(?:die|eine)\s+(?:\w+\s+)?(?:beschuldigte|tatverdächtige|alkoholisierte|betrunkene|fahrerin|\d{2}-jährige)\b/u', $dui_orig ) ) {
$geschlecht = 'w';
} elseif ( preg_match( '/\b(?:der|ein)\s+(?:\w+\s+)?(?:beschuldigte|tatverdächtige|alkoholisierte|betrunkene|fahrer|\d{2}-jährige[r]?)\b/u', $dui_orig ) ) {
$geschlecht = 'm';
} elseif ( preg_match( '/\b(?:die\s+)?\d{2}-jährige\s+(?:frau|fahrerin)/u', $dui_orig ) ) {
$geschlecht = 'w';
} elseif ( preg_match( '/\b(?:der\s+)?\d{2}-jährige[r]?\s+(?:mann|fahrer)/u', $dui_orig ) ) {
$geschlecht = 'm';
} elseif ( preg_match( '/(?:beschuldigte|tatverdächtige|alkoholisierte|betrunkene)\b/u', $dui_orig ) ) {
$geschlecht = 'm';
} elseif ( preg_match( '/\bfahrerin\b/u', $dui_orig ) && ! preg_match( '/\bfahrer\b/u', $dui_orig ) ) {
$geschlecht = 'w';
} elseif ( preg_match( '/\bmann\b/u', $dui_orig ) ) {
$geschlecht = 'm';
} elseif ( preg_match( '/\bfrau\b/u', $dui_orig ) ) {
$geschlecht = 'w';
}
// Unfall — nur konkrete Unfälle werten, nicht allgemeine Kontexte wie "Unfallursachen bei Verkehrsunfällen".
$hat_unfall = false;
if ( preg_match( '/kollidier|zusammenstoß|zusammengestoß|aufgefahren|von der fahrbahn|überschlagen|aufprall|verunfall/u', $text ) ) {
$hat_unfall = true;
} elseif ( preg_match( '/unfall/u', $text ) ) {
$hat_unfall = (bool) preg_match( '/(?:einem|einen|den|der|dem|zu\s+einem|ein|beim|nach\s+einem|nach\s+dem|bei\s+dem|verursacht)\s+(?:verkehrs)?unfall|unfallstelle|unfallort|unfallaufnahme|unfallbeteiligt|unfallfluch/u', $text );
}
$schwere = 'keine';
if ( $hat_unfall ) {
if ( preg_match( '/tödlich|verstorben|gestorben|tot|leben.*verloren|ums leben/u', $text ) ) {
$schwere = 'toedlich';
} elseif ( preg_match( '/verletzt|krankenhaus|rettungsdienst|personenschaden/u', $text ) ) {
$schwere = 'personenschaden';
} else {
$schwere = 'sachschaden';
}
}
// Führerschein.
$fs = (bool) preg_match( '/führerschein.*(?:sichergestellt|beschlagnahmt|entzogen|einbehalten)|fahrerlaubnis.*entzogen/u', $text );
// Fahrzeugart — Titel-basiert (zuverlässigster Indikator), Body als Fallback.
$fahrzeugart = 'pkw';
$orig_text = $incident->original_text;
// Mehrteilige Titel (++...++): nur DUI-relevanten Abschnitt prüfen.
$titel_check = $incident->original_titel;
if ( preg_match( '/\+\+/', $titel_check ) ) {
$parts = preg_split( '/\+\+/', $titel_check );
foreach ( $parts as $part ) {
if ( preg_match( '/alkohol|promille|trunken|drogen|btm|berauscht|cannabis/ui', $part ) ) {
$titel_check = $part;
break;
}
}
}
// 1. PKW-Fahrer als Beschuldigter im Titel → PKW, ungeachtet anderer Fahrzeuge.
if ( preg_match( '/(?:alkoholisiert|betrunken|berauscht)\w*\s+(?:pkw|auto)\w*/ui', $titel_check )
|| ( preg_match( '/(?:pkw-fahrer|autofahrer)\b/ui', $titel_check )
&& preg_match( '/alkohol|trunken|drogen|berauscht/ui', $titel_check ) ) ) {
$fahrzeugart = 'pkw';
}
// 2. E-Scooter im Titel.
elseif ( preg_match( '/e-scooter|e-roller|elektroroller/ui', $titel_check ) ) {
$fahrzeugart = 'e-scooter';
}
// 3. Fahrrad im Titel.
elseif ( preg_match( '/fahrradfahrer|\bradfahrer\b|\bradler\b|\bpedelec\b|\be-bike\b|\bfahrrad\b|(?:mit\s+dem\s+rad\b)/ui', $titel_check ) ) {
$fahrzeugart = 'fahrrad';
}
// 4. Motorrad/Roller im Titel.
elseif ( preg_match( '/\brollerfahrer|\brollerunfall|\bmotorrad|\bmotorroller|\bkradfahrer|\bmofa\b|\bmoped\b|\broller\b/ui', $titel_check )
&& ! preg_match( '/e-roller|e-scooter/ui', $titel_check ) ) {
$fahrzeugart = 'motorrad';
}
// 5. LKW im Titel.
elseif ( preg_match( '/\blkw\b|\bsattelzug|\blastwagen/ui', $titel_check ) ) {
$fahrzeugart = 'lkw';
}
// 6. Titel nicht eindeutig und kein PKW im Titel → Body mit DUI-Kontext prüfen.
elseif ( ! preg_match( '/pkw-fahrer|autofahrer|\bpkw\b/ui', $titel_check ) ) {
if ( preg_match( '/e-scooter-fahrer|(?:mit\s+(?:dem|seinem|ihrem|einem)\s+e-scooter)/ui', $orig_text )
|| preg_match( '/e-scooter[a-zäöüß\s,]{0,80}(?:alkohol|promille|trunken|drogen|cannabis)/ui', $orig_text )
|| preg_match( '/(?:alkohol|drogen|cannabis|trunkenheit)[a-zäöüß\s,]{0,80}e-scooter/ui', $orig_text ) ) {
$fahrzeugart = 'e-scooter';
} elseif ( preg_match( '/(?:mit\s+(?:dem|seinem|ihrem|einem)\s+(?:fahrrad|pedelec|e-bike|rad\b))/ui', $orig_text )
|| preg_match( '/(?:fahrradfahrer|radfahrer|pedelec\s*fahrer|radler)[a-zäöüß\s,]{0,80}(?:alkohol|promille|trunken|drogen)/ui', $orig_text )
|| preg_match( '/(?:alkohol|trunken|betrunken|berauscht)[a-zäöüß\s,]{0,80}(?:fahrradfahrer|radfahrer|radler|pedelec)/ui', $orig_text ) ) {
$fahrzeugart = 'fahrrad';
} elseif ( preg_match( '/(?:rollerfahrer|motorradfahrer|kradfahrer|(?:seines|seinem|ihrem)\s+motorrads?)[a-zäöüß\s,]{0,80}(?:alkohol|promille|trunken|drogen|btm|blutprobe|blutentnahme|cannabis)/ui', $orig_text )
|| preg_match( '/(?:alkohol|trunken|betrunken|berauscht|drogen|btm|blutprobe|blutentnahme)[a-zäöüß\s,]{0,80}(?:rollerfahrer|motorradfahrer|(?:seines|seinem)\s+motorrads?|auf\s+(?:dem|einem|seinem)\s+(?:roller|motorrad|kraftrad))/ui', $orig_text )
|| preg_match( '/(?:mit\s+(?:dem|seinem|ihrem|einem)\s+(?:motorrad|kraftrad|roller)\b)/ui', $orig_text ) ) {
$fahrzeugart = 'motorrad';
}
}
// Bulk-Checkpoint: Fallzahlen extrahieren.
$anzahl_alkohol = 1;
$anzahl_drogen = 1;
if ( $hat_alkohol ) {
$bulk = dgt_vm_count_substance_cases( $incident->original_text, 'alkohol' );
if ( $bulk > 1 ) {
$anzahl_alkohol = $bulk;
}
}
if ( $hat_drogen ) {
$bulk = dgt_vm_count_substance_cases( $incident->original_text, 'drogen' );
if ( $bulk > 1 ) {
$anzahl_drogen = $bulk;
}
}
// Stadt aus Text (erste Zeile hat oft "Stadtname (ots)").
$stadt = '';
if ( preg_match( '/^([A-ZÄÖÜ][a-zäöüß]+(?:\s[a-zäöüß.]+)*)\s*\(ots\)/um', $incident->original_text, $m ) ) {
$stadt = trim( $m[1] );
}
// Headline generieren.
$headline = $incident->original_titel;
if ( preg_match( '/^POL-[A-Z]+:\s*(.+)/u', $headline, $m ) ) {
$headline = trim( $m[1] );
}
if ( preg_match( '/^\d{6}-\d+-[A-Z\/]+\s*(.+)/u', $headline, $m ) ) {
$headline = trim( $m[1] );
}
$headline = mb_substr( $headline, 0, 200 );
// Slug.
$slug = trim( sanitize_title( $headline ), '-' );
$slug = mb_substr( $slug, 0, 80 );
$orig_slug = $slug;
$suffix = 2;
while ( $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$t} WHERE ai_slug = %s AND bundesland = %s AND id != %d", $slug, $incident->bundesland, $incident->id ) ) ) {
$slug = $orig_slug . '-' . $suffix++;
}
// Summary: Metadaten-Header entfernen, ersten Absatz vollständig übernehmen.
$clean_text = strip_tags( $incident->original_text );
$clean_text = preg_replace( '/^.*?\(ots\)\s*/su', '', $clean_text );
$clean_text = preg_replace( '/\s+/', ' ', trim( $clean_text ) );
$summary = $clean_text;
if ( mb_strlen( $summary ) > 500 ) {
$summary = mb_substr( $summary, 0, 500 );
$last_dot = mb_strrpos( $summary, '. ' );
if ( $last_dot !== false && $last_dot > 150 ) {
$summary = mb_substr( $summary, 0, $last_dot + 1 );
} else {
$summary = mb_substr( $summary, 0, mb_strrpos( $summary, ' ' ) ) . ' ...';
}
}
$now = current_time( 'mysql' );
$wpdb->update( $t, array(
'ai_headline' => $headline,
'ai_summary' => $summary,
'ai_slug' => $slug,
'ai_rechtsinfo' => '',
'delikt' => $delikt,
'hat_alkohol' => $hat_alkohol ? 1 : 0,
'hat_drogen' => $hat_drogen ? 1 : 0,
'substanzen' => $substanzen ? wp_json_encode( $substanzen ) : '',
'bak_wert' => $bak,
'thc_wert' => $thc,
'alter_fahrer' => $alter,
'geschlecht' => $geschlecht,
'hat_unfall' => $hat_unfall ? 1 : 0,
'unfall_schwere' => $schwere,
'fs_sichergestellt' => $fs ? 1 : 0,
'fahrzeugart' => $fahrzeugart,
'anzahl_alkohol' => $anzahl_alkohol,
'anzahl_drogen' => $anzahl_drogen,
'stadt' => $stadt,
'status' => 'publiziert',
'published_at' => $now,
'updated_at' => $now,
), array( 'id' => $incident->id ) );
return true;
}
function dgt_vm_do_quick_publish() {
global $wpdb;
$t = dgt_vm_table( 'incidents' );
$pending = $wpdb->get_results(
"SELECT * FROM {$t} WHERE status = 'entwurf' ORDER BY created_at ASC LIMIT 100"
);
$published = 0;
foreach ( $pending as $inc ) {
if ( dgt_vm_quick_analyze( $inc ) ) {
$published++;
}
}
return $published;
}
/* ─── Cron-Action: Pending analysieren ─────────────────── */
function dgt_vm_do_analyze_pending() {
$api_key = get_option( 'dgt_vm_api_key' );
if ( ! $api_key ) {
$count = dgt_vm_do_quick_publish();
if ( $count > 0 ) {
update_option( 'dgt_vm_last_analyze', current_time( 'mysql' ) );
update_option( 'dgt_vm_last_analyze_count', $count );
}
return;
}
global $wpdb;
$t = dgt_vm_table( 'incidents' );
$pending = $wpdb->get_results(
"SELECT * FROM {$t} WHERE status = 'entwurf' ORDER BY created_at ASC LIMIT 15"
);
if ( ! $pending ) {
return;
}
$analyzed = 0;
foreach ( $pending as $inc ) {
$result = dgt_vm_analyze_incident( $inc );
if ( $result ) {
$analyzed++;
}
usleep( 500000 ); // 0.5s Pause zwischen API-Calls.
}
update_option( 'dgt_vm_last_analyze', current_time( 'mysql' ) );
update_option( 'dgt_vm_last_analyze_count', $analyzed );
}
add_action( 'dgt_vm_analyze_pending', 'dgt_vm_do_analyze_pending' );