ファイルのアップロードはこちら(Laravel 11でAmazon S3に画像を保存し、表示する手順)から。
Attachment.php
<?php
namespace App\Models;
class Attachment extends Model
{
// s3 署名付きURLを取得するメソッド
public function getFileUrl($filePath)
{
if (!$filePath) {
return null;
}
$cacheKey = "s3_url_{$filePath}";
// キャッシュに保存(有効期限はURLの期限に合わせる)
return Cache::remember($cacheKey, now()->addMinutes(15), function () use ($filePath) {
return Storage::disk('s3')->temporaryUrl($filePath, now()->addMinutes(15));
});
}
}
Web.php
// test
use App\Http\Controllers\Test\FileController;
Route::prefix('test')->group(function () {
Route::get('/analyze/{id}', [FileController::class, 'show'])->name('test.analyze');
});
FileController
<?php
namespace App\Http\Controllers\Test;
use App\Http\Controllers\Controller;
use OpenAI\Laravel\Facades\OpenAI;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Smalot\PdfParser\Parser;
// attachmentsのfileに保存されているデータ
use App\Models\Attachment;
class FileController extends Controller
{
public function show($id)
{
try {
// ファイルの取得
$attachment = Attachment::findOrFail($id);
$fileUrl = $attachment->getFileUrl($attachment->file);
$fileName = basename($attachment->file);
// 一時ファイルのパスを設定
$tempPath = storage_path('app/temp/' . uniqid() . '_' . $fileName);
// ディレクトリが存在することを確認
if (!file_exists(dirname($tempPath))) {
mkdir(dirname($tempPath), 0755, true);
}
// ファイルをコピー
if (!copy($fileUrl, $tempPath)) {
throw new \Exception('ファイルのコピーに失敗しました。');
}
try {
// MIMEタイプの取得を修正
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($tempPath);
Log::info('File MIME type: ' . $mimeType); // デバッグ用ログ
// ファイル拡張子からもチェック
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
// MIMEタイプとファイル拡張子の両方をチェック
if (strpos($mimeType, 'image/') === 0 || in_array($extension, ['jpg', 'jpeg', 'png'])) {
$analysis = $this->analyzeImage($tempPath);
} elseif ($mimeType === 'application/pdf' || $extension === 'pdf') {
$analysis = $this->analyzePdf($tempPath);
} else {
throw new \Exception("未対応のファイル形式です。(MIME: {$mimeType}, 拡張子: {$extension})");
}
// 分析結果を返す
return view('test.analysis', [
'fileName' => $fileName,
'analysis' => $analysis,
'error' => null
]);
} finally {
// 一時ファイルの削除を確実に実行
if (file_exists($tempPath)) {
unlink($tempPath);
}
}
} catch (\Exception $e) {
Log::error('分析エラー: ' . $e->getMessage());
// エラー時も同じビューを使用し、エラーメッセージを表示
return view('test.analysis', [
'fileName' => $fileName ?? null,
'analysis' => null,
'error' => '分析中にエラーが発生しました: ' . $e->getMessage()
]);
}
}
private function analyzeImage($path)
{
$imageData = base64_encode(file_get_contents($path));
$result = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
[
'role' => 'system',
'content' => '画像の内容を詳細に分析し、日本語で説明してください。'
],
[
'role' => 'user',
'content' => [
[
'type' => 'text',
'text' => '画像について以下の点を説明してください:
1. 画像の主な内容は?
2. 画像の特徴は?
3. 画像から読み取れる情報や意図'
],
[
'type' => 'image_url',
'image_url' => [
'url' => "data:image/jpeg;base64,{$imageData}"
]
]
]
]
],
'max_tokens' => 1000
]);
return $result->choices[0]->message->content;
}
private function analyzePdf($path)
{
$parser = new Parser();
$pdf = $parser->parseFile($path);
// テキストの分析
$text = $this->sanitizeText($pdf->getText());
$textAnalysis = '';
if (!empty(trim($text))) {
$result = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
[
'role' => 'system',
'content' => 'PDFのテキスト内容を分析して日本語で要約してください。'
],
[
'role' => 'user',
'content' => $text
]
],
]);
$textAnalysis = $result->choices[0]->message->content;
}
// 画像の分析(必要な場合)
$imageAnalyses = $this->analyzePdfImages($path);
// 結果の統合
return $this->combinedAnalysis($textAnalysis, $imageAnalyses);
}
private function analyzePdfImages($path)
{
try {
if (!extension_loaded('imagick')) {
return ["Imagick拡張モジュールが利用できないため、画像分析はスキップされました。"];
}
$imagick = new \Imagick();
$imagick->readImage($path);
$imageAnalyses = [];
for ($i = 0; $i < $imagick->getNumberImages(); $i++) {
$imagick->setIteratorIndex($i);
$imagick->setImageFormat('jpeg');
$imageData = base64_encode($imagick->getImageBlob());
$result = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
[
'role' => 'system',
'content' => 'PDFに含まれる画像の内容を分析して日本語で説明してください。'
],
[
'role' => 'user',
'content' => [
[
'type' => 'text',
'text' => 'この画像の内容を解説してください。'
],
[
'type' => 'image_url',
'image_url' => [
'url' => "data:image/jpeg;base64,{$imageData}"
]
]
]
]
],
'max_tokens' => 500
]);
$imageAnalyses[] = "画像 " . ($i + 1) . ":\n" . $result->choices[0]->message->content;
}
$imagick->clear();
$imagick->destroy();
return $imageAnalyses;
} catch (\Exception $e) {
Log::error('PDF画像分析エラー: ' . $e->getMessage());
return ["画像の抽出または分析中にエラーが発生しました: " . $e->getMessage()];
}
}
private function combinedAnalysis($textAnalysis, $imageAnalyses)
{
if (empty($textAnalysis) && empty($imageAnalyses)) {
return "分析可能なコンテンツが見つかりませんでした。";
}
$imageAnalysisText = empty($imageAnalyses) ?
"PDFから画像は抽出されませんでした。" :
"PDF内の画像分析:\n" . implode("\n\n", $imageAnalyses);
if (empty(trim($textAnalysis))) {
return $imageAnalysisText;
}
$result = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
[
'role' => 'system',
'content' => 'PDFのテキストと画像の分析結果を統合して、総合的な要約を作成してください。'
],
[
'role' => 'user',
'content' => "テキスト分析結果:\n{$textAnalysis}\n\n画像分析結果:\n{$imageAnalysisText}"
]
],
]);
return $result->choices[0]->message->content;
}
private function sanitizeText($text)
{
$encoding = mb_detect_encoding($text, ['ASCII', 'UTF-8', 'EUC-JP', 'SJIS'], true);
if ($encoding) {
$text = mb_convert_encoding($text, 'UTF-8', $encoding);
}
$text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text);
$text = iconv('UTF-8', 'UTF-8//IGNORE', $text);
$text = preg_replace('/\s+/', ' ', $text);
$text = preg_replace('/\n\s*\n/', "\n\n", $text);
return trim($text);
}
}
analysis.blade.php
<div class="container">
@if ($error)
<div class="alert alert-danger">
{{ $error }}
</div>
@endif
@if ($fileName && $analysis)
<div class="card">
<div class="card-body">
<h2 class="card-title">分析結果</h2>
<div class="mb-4">
<h3>ファイル名:</h3>
<p>{{ $fileName }}</p>
</div>
<div class="mb-4">
<h3>総合分析:</h3>
<div class="analysis-content">
{!! nl2br(e($analysis)) !!}
</div>
</div>
</div>
</div>
@endif
</div>
サーバーにもよりますが、重いファイルは処理ができないかもしれません。一定以上の大きいファイルを処理する場合は、バックグラウンドで処理させると良いと思います。