277 lines
11 KiB
PHP
277 lines
11 KiB
PHP
|
|
<?php
|
|||
|
|
// aiassist/fileHandler.php
|
|||
|
|
require_once 'logger.php';
|
|||
|
|
|
|||
|
|
function normalizeFilename($filename) {
|
|||
|
|
$filename = iconv('UTF-8', 'UTF-8//IGNORE', $filename);
|
|||
|
|
return preg_replace('/[^\w\.]+/u', '_', $filename);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function cleanQueryText($text) {
|
|||
|
|
$text = preg_replace('/[\x00-\x1F\x7F]/u', ' ', $text);
|
|||
|
|
$text = preg_replace('/\s+/', ' ', $text);
|
|||
|
|
return trim($text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function extractText($filePath) {
|
|||
|
|
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
|||
|
|
if ($extension !== 'pdf') {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
$outputFile = tempnam(sys_get_temp_dir(), 'txt_') . '.txt';
|
|||
|
|
$command = "pdftotext " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile);
|
|||
|
|
logMessage("Выполняем команду для извлечения текста: " . $command);
|
|||
|
|
exec($command, $output, $returnVar);
|
|||
|
|
if ($returnVar !== 0) {
|
|||
|
|
logMessage("Ошибка pdftotext: " . implode("\n", $output));
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
if (!file_exists($outputFile)) {
|
|||
|
|
logMessage("Файл pdftotext не создан: $filePath");
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
$text = file_get_contents($outputFile);
|
|||
|
|
unlink($outputFile);
|
|||
|
|
$cleanText = trim(preg_replace('/\pC+/u', '', $text));
|
|||
|
|
if (mb_strlen($cleanText, 'UTF-8') < 200) {
|
|||
|
|
logMessage("Извлечённый текст для $filePath слишком короткий или не содержит полезной информации. Запуск OCR.");
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
logMessage("Извлечённый текст для $filePath (первые 500 символов): " . mb_substr($cleanText, 0, 500, 'UTF-8'));
|
|||
|
|
return $cleanText;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function doOCR($filePath) {
|
|||
|
|
logMessage("🔍 Запуск OCR. Сначала конвертируем PDF в изображения: $filePath");
|
|||
|
|
|
|||
|
|
// Создаем временную папку для изображений
|
|||
|
|
$imageDir = sys_get_temp_dir() . "/ocr_images_" . uniqid();
|
|||
|
|
$images = convertPdfToImages($filePath, $imageDir);
|
|||
|
|
|
|||
|
|
if (empty($images)) {
|
|||
|
|
logMessage("❌ Ошибка: Не удалось преобразовать PDF в изображения");
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logMessage("✅ PDF преобразован в " . count($images) . " изображений, запускаем OCR");
|
|||
|
|
|
|||
|
|
$outputText = '';
|
|||
|
|
foreach ($images as $image) {
|
|||
|
|
$text = runOCRonImage($image);
|
|||
|
|
$outputText .= "\n" . $text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logMessage("✅ OCR завершен. Извлеченный текст:\n" . mb_substr($outputText, 0, 500, 'UTF-8'));
|
|||
|
|
return trim($outputText);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Функция OCR для обработки каждого изображения
|
|||
|
|
function runOCRonImage($imagePath) {
|
|||
|
|
$outputFile = tempnam(sys_get_temp_dir(), 'ocr_') . '.txt';
|
|||
|
|
$command = "tesseract " . escapeshellarg($imagePath) . " " . escapeshellarg($outputFile) . " -l rus --psm 6 --oem 1";
|
|||
|
|
logMessage("🔍 OCR на изображении: $command");
|
|||
|
|
|
|||
|
|
exec($command, $output, $returnVar);
|
|||
|
|
if ($returnVar !== 0) {
|
|||
|
|
logMessage("❌ Ошибка Tesseract: " . implode("\n", $output));
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!file_exists($outputFile . ".txt")) {
|
|||
|
|
logMessage("❌ Ошибка: OCR-файл не создан");
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$text = file_get_contents($outputFile . ".txt");
|
|||
|
|
unlink($outputFile . ".txt");
|
|||
|
|
return $text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
function convertPdfToImages($pdfPath, $outputDir) {
|
|||
|
|
if (!file_exists($pdfPath)) {
|
|||
|
|
logMessage("Файл не существует: $pdfPath");
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
if (!file_exists($outputDir)) {
|
|||
|
|
mkdir($outputDir, 0777, true);
|
|||
|
|
logMessage("Создана директория для изображений: $outputDir");
|
|||
|
|
}
|
|||
|
|
$imagePattern = $outputDir . '/page-%03d.jpg';
|
|||
|
|
$command = "LC_ALL=en_US.UTF-8 convert -density 300 " . escapeshellarg($pdfPath) . " -quality 90 " . escapeshellarg($imagePattern);
|
|||
|
|
logMessage("Выполняем команду: " . $command);
|
|||
|
|
exec($command . " 2>&1", $output, $returnVar);
|
|||
|
|
if ($returnVar !== 0) {
|
|||
|
|
logMessage("Ошибка при конвертации PDF в изображения.");
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
return glob($outputDir . '/*.jpg');
|
|||
|
|
}
|
|||
|
|
/*
|
|||
|
|
function checkNSFWWithVision($filePath) {
|
|||
|
|
logMessage("NSFW-проверка через URL " . NSFW_MODERATION_API . " для файла: $filePath");
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
CURLOPT_URL => NSFW_MODERATION_API,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_POSTFIELDS => [
|
|||
|
|
'file' => new CURLFile($filePath)
|
|||
|
|
],
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
"Content-Type: application/json",
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($curl);
|
|||
|
|
curl_close($curl);
|
|||
|
|
logMessage("Ответ NSFW: HTTP $httpCode - " . $response);
|
|||
|
|
if ($curlError) {
|
|||
|
|
logMessage("Ошибка cURL при проверке NSFW: " . $curlError);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
if ($httpCode !== 200 || isset($decoded['detail'])) {
|
|||
|
|
logMessage("Ошибка анализа NSFW: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return $decoded['nsfw'] ?? null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
*/
|
|||
|
|
/*function checkNSFWWithVision($filePath) {
|
|||
|
|
logMessage("NSFW-проверка через URL https://api.proxyapi.ru/v1/moderation для файла: $filePath");
|
|||
|
|
|
|||
|
|
$curl = curl_init();
|
|||
|
|
curl_setopt_array($curl, [
|
|||
|
|
// CURLOPT_URL => "https://api.proxyapi.ru/v1/moderation",
|
|||
|
|
CURLOPT_URL => NSFW_MODERATION_API,
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_POST => true,
|
|||
|
|
CURLOPT_POSTFIELDS => [
|
|||
|
|
'file' => new CURLFile($filePath)
|
|||
|
|
],
|
|||
|
|
CURLOPT_HTTPHEADER => [
|
|||
|
|
'Authorization: Bearer ' . OPENAI_API_KEY
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
$response = curl_exec($curl);
|
|||
|
|
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|||
|
|
$curlError = curl_error($curl);
|
|||
|
|
curl_close($curl);
|
|||
|
|
|
|||
|
|
logMessage("Ответ NSFW: HTTP $httpCode - " . $response);
|
|||
|
|
|
|||
|
|
if ($curlError) {
|
|||
|
|
logMessage("Ошибка cURL при проверке NSFW: " . $curlError);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$decoded = json_decode($response, true);
|
|||
|
|
|
|||
|
|
// Проверяем статус ответа
|
|||
|
|
if ($httpCode !== 200 || isset($decoded['detail'])) {
|
|||
|
|
logMessage("Ошибка анализа NSFW: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $decoded['data'][0]['nsfw'] ?? null;
|
|||
|
|
}
|
|||
|
|
*/
|
|||
|
|
function checkNSFWWithVision($filePath) {
|
|||
|
|
logMessage("⚠️ NSFW-проверка отключена для файла: $filePath");
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Формирует объединённое сообщение по документам.
|
|||
|
|
* Если текст можно извлечь напрямую, то передаётся только ссылка (ID файла).
|
|||
|
|
* Если не удалось извлечь текст, используется OCR, и включается полученный текст.
|
|||
|
|
* Также производится проверка на цензуру.
|
|||
|
|
*
|
|||
|
|
* @param array $documents Массив документов (с полями 'filepath' и 'title').
|
|||
|
|
* @param array $uploadedFileIds Ассоциативный массив с путями к файлам => их идентификаторами.
|
|||
|
|
* @return string Объединённое сообщение для передачи в GPT.
|
|||
|
|
*/
|
|||
|
|
function analyzeDocuments($documents, $uploadedFileIds) {
|
|||
|
|
$combinedMessages = [];
|
|||
|
|
foreach ($documents as $doc) {
|
|||
|
|
if (empty($doc['filepath'])) {
|
|||
|
|
logMessage("Неверный путь: " . json_encode($doc, JSON_UNESCAPED_UNICODE));
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
// Получаем fileID, полученный при загрузке
|
|||
|
|
$fileId = $uploadedFileIds[$doc['filepath']] ?? 'неизвестный_ID';
|
|||
|
|
|
|||
|
|
// Выполняем проверку на NSFW
|
|||
|
|
$nsfw = checkNSFWWithVision($doc['filepath']);
|
|||
|
|
if ($nsfw !== null && $nsfw > 0.8) {
|
|||
|
|
$messageBlock = "Документ: " . $doc['title'] . "\n";
|
|||
|
|
$messageBlock .= "Файл (ID: " . $fileId . ") не прошёл проверку цензуры.\n";
|
|||
|
|
} else {
|
|||
|
|
// Пытаемся извлечь текст напрямую
|
|||
|
|
$text = extractText($doc['filepath']);
|
|||
|
|
if (!empty($text)) {
|
|||
|
|
// Если файл читаемый, передаём только ссылку (ID файла)
|
|||
|
|
$messageBlock = "Документ: " . $doc['title'] . "\n";
|
|||
|
|
$messageBlock .= "Файл передан с ID: " . $fileId . ". Текст не загружен, так как файл читаемый.\n";
|
|||
|
|
} else {
|
|||
|
|
// Если не удалось извлечь текст, пробуем OCR
|
|||
|
|
$ocrText = doOCR($doc['filepath']);
|
|||
|
|
if (!empty($ocrText)) {
|
|||
|
|
$messageBlock = "Документ: " . $doc['title'] . "\n";
|
|||
|
|
$messageBlock .= "Файл передан с ID: " . $fileId . ". OCR текст:\n" . $ocrText . "\n";
|
|||
|
|
} else {
|
|||
|
|
$messageBlock = "Документ: " . $doc['title'] . "\n";
|
|||
|
|
$messageBlock .= "Файл передан с ID: " . $fileId . ". Не удалось извлечь текст.\n";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$combinedMessages[] = $messageBlock;
|
|||
|
|
}
|
|||
|
|
return implode("\n-----------------\n", $combinedMessages);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Создает Vector Store, загружает файлы и возвращает массив с идентификаторами.
|
|||
|
|
*
|
|||
|
|
* @param array $filePaths Массив путей к файлам.
|
|||
|
|
* @return array|null Массив с ключами 'vectorStoreId' и 'fileIds' (ассоциативный массив [путь => fileID]), или null при ошибке.
|
|||
|
|
*/
|
|||
|
|
function createVectorStoreAndUploadFiles($filePaths) {
|
|||
|
|
logMessage("Создание Vector Store и загрузка файлов...");
|
|||
|
|
$vectorStoreId = createVectorStore();
|
|||
|
|
if (!$vectorStoreId) {
|
|||
|
|
logMessage("Ошибка: не удалось создать Vector Store.");
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
$uploadedFiles = [];
|
|||
|
|
foreach ($filePaths as $filePath) {
|
|||
|
|
logMessage("Загрузка файла: $filePath");
|
|||
|
|
if (!file_exists($filePath)) {
|
|||
|
|
logMessage("Ошибка: Файл не существует: $filePath");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$fileId = uploadFileToOpenAI($filePath);
|
|||
|
|
if (!$fileId) {
|
|||
|
|
logMessage("Ошибка загрузки файла: $filePath");
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if (!addFileToVectorStore($vectorStoreId, $fileId)) {
|
|||
|
|
logMessage("Ошибка добавления файла в Vector Store: $filePath");
|
|||
|
|
} else {
|
|||
|
|
logMessage("Файл успешно добавлен в Vector Store: $filePath");
|
|||
|
|
$uploadedFiles[$filePath] = $fileId;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ['vectorStoreId' => $vectorStoreId, 'fileIds' => $uploadedFiles];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
?>
|