403Webshell
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 :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/clients/client2/web20/web/wp-content/plugins/dgt-verkehrsmonitor/includes/analyzer.php
<?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' );

Youez - 2016 - github.com/yon3zu
LinuXploit