AIによるドキュメントの要約

このサンプルでは、ChatGPTを使用して、PDFドキュメントを要約し、その結果をドキュメントに追加する方法を紹介しています。 このサンプルでは、分析のためにChatGPTにテキストコンテンツを送信し、コンテンツの簡単な要約を生成します。ただし、送信できるコンテンツの総量には制限があります。

window.onload = async function() { const viewer = new DsPdfViewer("#viewer", { supportApi: getSupportApiSettings(), restoreViewStateOnLoad: false, language: "ja" }); viewer.addDefaultPanels(); // ツールバーのボタンレイアウトをモードごとに設定します viewer.toolbarLayout.viewer = { default: ["open", "save", "$navigation", "$split", "$zoom", "$split", 'doc-title', "about"], mobile: ["open", "save", "$navigation", "$split", "$zoom", "$split", 'doc-title', "about"], fullscreen: ["$fullscreen", "open", "save", "$navigation", "$split", "$zoom", "$split", 'doc-title', "about"] }; // ツールバーのレイアウトを適用します viewer.applyToolbarLayout(); // AIツールパネルをビューアに追加します addAiToolsPanel(viewer); // PDFドキュメントを開きます var pdfUrlToOpen = "/diodocs/pdfviewer/demos/product-bundles/assets/pdf/diodocs_a4_full.pdf"; await viewer.open(pdfUrlToOpen); // ビューワの表示倍率を"ページ幅に合わせる"に設定します viewer.zoomMode = 1; }
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>AIによるドキュメントの要約</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./src/styles.css"> <script src="/diodocs/pdfviewer/demos/product-bundles/build/dspdfviewer.js"></script> <script src="/diodocs/pdfviewer/demos/product-bundles/build/wasmSupportApi.js"></script> <script src="/diodocs/pdfviewer/demos/resource/js/init.js"></script> <script src="./src/aitools.js"></script> <script src="./src/ui.js"></script> <script src="./src/app.js"></script> </head> <body> <div id="viewer"></div> </body> </html>
#viewer { height: 100%; } .sample-ai-tools-panel label { margin-bottom: 10px; } .sample-ai-tools-panel select, .sample-ai-tools-panel button { width: 100%; height: 30px; line-height: 30px; text-align: center; } .sample-ai-tools-panel .gc-toggle input { width: 20px; height: 20px; margin-left: 10px; }
async function summarizePdfContent(viewer, apiModel, isConciseEnabled) { const searcher = viewer.searcher; try { // パフォーマンスを向上させるために、すべてのページから並列的にコンテンツを取得します const pageContentPromises = Array.from({ length: viewer.pageCount }, (_, i) => searcher.fetchPageContent(i)); const pageContents = await Promise.all(pageContentPromises); // 全ページのテキストを1つの文字列にまとめます const textContent = extractAnnotationsInfo(viewer) + pageContents.join(" ").trim(); // analyzeTextContentを呼び出す前に、テキストコンテンツがあるかどうかをチェックします if (textContent) { const summary = await analyzeTextContent(apiModel, textContent, isConciseEnabled); // 要約が文字列かどうかをチェックします if (typeof summary === "string") { return `Generated Summary:\n${summary}`; } // そうでない場合は、生成されたコンテンツを含むオブジェクトであると推測します const generatedText = summary?.choices?.[0]?.message?.content || "No content in the response."; // タイムスタンプを読み取り可能な日付に変換します const createdDate = summary?.created ? new Date(summary.created * 1000).toLocaleString() // Convert Unix timestamp to readable format : "Creation date not available"; // 使用したモデルを抽出します const modelUsed = summary?.model || "Model information not available"; // 技術的な詳細と追加情報を含む生成結果(要約)を返します return `Generated Summary:\n${generatedText}\n\nCreated: ${createdDate}\nModel: ${modelUsed}`; } else { return "[Error] The document does not contain any text content."; } } catch (error) { let errorMessage; if (typeof error === "string") { // エラーが文字列形式の場合、そのまま使用します errorMessage = error; } else if (error instanceof Error) { // エラーがインスタンスの場合、そのメッセージを使用します errorMessage = error.message; } else if (typeof error === "object" && error?.error?.message) { // エラーがネストされたエラーメッセージを持つオブジェクトの場合、そのネストされたメッセージを使用します errorMessage = error.error.message; } else { // その他の場合、固定メッセージを使用します errorMessage = "Server error. Unable to connect to ChatGPT server."; } return "[Error] " + errorMessage; } } function extractAnnotationsInfo(viewer) { try { // .pagescontent内のすべてのセクションを選択します const sections = viewer.scrollView.querySelectorAll(".pagescontent section"); if(!sections.length) return ""; // クラスリストから注釈タイプを抽出するヘルパー関数 function getAnnotationType(classList) { const annotationType = Array.from(classList).find(cls => cls.endsWith("Annotation")); return annotationType || "Undefined annotation"; } // 各セクションの情報を抽出します const annotations = Array.from(sections).map(section => { const classList = section.className.split(/\s+/); // Get all class names const annotationType = getAnnotationType(classList); // Extract annotation type const textContent = section.textContent.trim(); // Extract text content const pageElement = section.closest(".page"); // Find the closest parent with class "page" const pageIndex = pageElement ? pageElement.getAttribute("data-index") : "unknown"; // Extract page index return { annotationType, textContent, pageIndex }; }); // 要約テキストを生成します const totalAnnotations = annotations.length; let summary = `The document contains ${totalAnnotations} annotations. Here is the list:\n`; summary += annotations.map(anno => `- ${anno.annotationType} on page ${anno.pageIndex + 1} contains text content: "${anno.textContent}"` ).join("\n"); return summary + "\n"; } catch (error) { return ""; } } // HTTPステータスコードとエラー名を対応付ける関数 function getErrorNameByStatus(status) { const errorMap = { 100: "Continue", 101: "Switching Protocols", 200: "OK", 201: "Created", 202: "Accepted", 204: "No Content", 301: "Moved Permanently", 302: "Found", 304: "Not Modified", 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 408: "Request Timeout", 409: "Conflict", 413: "Payload Too Large", 415: "Unsupported Media Type", 429: "Too Many Requests", 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", }; const message = errorMap[status] || "Unknown Error"; return `${message} (Status: ${status})`; } /** * エラー・レスポンスを処理し、レスポンスのステータスとボディに基づいて詳細なエラーをスローする * * @param {Response} response - エラーをチェックするHTTPレスポンスオブジェクト * @throws {Error} ステータスコードとレスポンスの内容に基づいた、分かりやすいメッセージを含むエラー */ async function handleErrorResponse(response) { if (response.ok) return; let responseBody; try { // レスポンス本文をテキストとして読み込みます responseBody = await response.text(); } catch { throw new Error('Failed to read the response body.'); } let errorToThrow; try { // レスポンス・ボディをJSONとしてパースします const errorContent = JSON.parse(responseBody); const errorMessage = errorContent?.error?.message || JSON.stringify(errorContent); errorToThrow = new Error(`${getErrorNameByStatus(response.status)}. ${errorMessage}`); } catch (parseError) { // パースに失敗した場合、原文をエラーメッセージとして使用します errorToThrow = new Error(`${getErrorNameByStatus(response.status)}. ${responseBody || ''}`); } throw errorToThrow; } // OpenAI summarize APIを使ってテキストコンテンツを分析するメソッド async function analyzeTextContent(apiModel, content, isConciseEnabled) { console.log("analyzeTextContent:", content); const response = await fetch('api/openai/summarize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: apiModel, content: content, isConciseEnabled: isConciseEnabled }), }); await handleErrorResponse(response); return response.json(); }
let public_setIsSummaryAdded; function addAiToolsPanel(viewer) { const React = viewer.getType('React'); // カスタムアイコンと説明を含むAIツールパネルを作成します(非表示・無効状態) const aiToolsPanelHandle = viewer.createPanel(createPanelContentElement(React, viewer), null, 'AiToolsPanel', { icon: { type: 'svg', content: createSvgIconElement(React) }, label: 'AIツール', description: 'AIツールパネル', visible: false, enabled: false } ); // 'AIツールパネル'をビューワに追加します viewer.layoutPanels(['*', 'AiToolsPanel']); // ドキュメントを開いた後に、AIツールパネルを有効にして展開するイベントを登録します viewer.onAfterOpen.register(function() { if(public_setIsSummaryAdded) { public_setIsSummaryAdded(false); public_setIsSummaryAdded = null; } viewer.updatePanel(aiToolsPanelHandle, { visible: true, enabled: true }); viewer.leftSidebar.menu.panels.open(aiToolsPanelHandle.id); viewer.leftSidebar.menu.panels.pin(aiToolsPanelHandle.id); }); } function createPanelContentElement(React, viewer) { // "AIツールパネル"を生成します function PanelContentComponent(props) { // APIモデル選択、APIキー入力、ボタン有効化、ロード状態のローカル状態 const [apiModel, setApiModel] = React.useState(() => { return localStorage.getItem('selectedGptApiModel') || 'gpt-4'; }); const [isButtonEnabled, setIsButtonEnabled] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false); const [isSummaryAdded, setIsSummaryAdded] = React.useState(false); const [isConciseEnabled, setIsConciseEnabled] = React.useState(true); // APIモデルとキー入力の両方が入力され、ロードされていない場合、ボタンを有効にします React.useEffect(() => { setIsButtonEnabled(apiModel.trim().length > 0 && !isLoading); }, [apiModel, isLoading]); // 選択したモデルが変更されるたびにlocalStorageに保存します React.useEffect(() => { localStorage.setItem('selectedGptApiModel', apiModel); }, [apiModel]); // 選択したAIモデルの変更を処理します function handleModelChange(event) { setApiModel(event.target.value); } // "内容を要約"ボタンがクリックされた時の処理 async function handleSummarizeClick() { await handleResetSummaryClick(); setIsLoading(true); // 非同期処理中、ボタンを無効化します await props.summarizePdfContent(apiModel, isConciseEnabled); setIsLoading(false); // 完了後、ボタンを有効化します setIsSummaryAdded(true); // 要約が追加されたことをマークします public_setIsSummaryAdded = setIsSummaryAdded; } // "要約をリセット"ボタンがクリックされた時の処理 async function handleResetSummaryClick() { await props.resetSummary(); setIsSummaryAdded(false); // 要約をリセット後、状態をリセット public_setIsSummaryAdded = null; } return React.createElement('div', { className: 'sample-ai-tools-panel', style: { margin: '20px' } }, // AIモデルを選択するためのドロップダウンリスト React.createElement('label', { className: 'gc-label' }, 'リストからモデルを選択してください:'), React.createElement('select', { value: apiModel, onChange: handleModelChange, className: 'gc-input' }, React.createElement('option', { value: 'gpt-3.5-turbo', title: 'GPT-3.5-Turbo: 汎用タスクに適した、高速でコスト効率の高いモデル。' }, 'GPT-3.5-Turbo'), React.createElement('option', { value: 'gpt-3.5-turbo-16k', title: 'GPT-3.5-Turbo 16k: コンテキストウィンドウが大きくなったGPT-3.5-Turboの拡張版(16,000トークン)。' }, 'GPT-3.5-Turbo 16k'), React.createElement('option', { value: 'gpt-4', title: 'GPT-4: OpenAIの最先端モデルで、より高い精度と推論能力を提供' }, 'GPT-4') ), React.createElement( 'label', { className: 'gc-toggle gc-toggle--block', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', }, title: 'より短く、焦点を絞った要約とする', }, React.createElement( 'span', { style: { margin: '0 auto', }, }, '簡潔な要約' ), React.createElement('input', { type: 'checkbox', checked: isConciseEnabled, onChange: (e) => setIsConciseEnabled(e.target.checked), }) ), React.createElement('br'), // コンテンツを要約するボタン:APIモデルとキーの両方が提供され、ローディングがfalseの場合のみ有効 React.createElement('button', { onClick: handleSummarizeClick, disabled: !isButtonEnabled, className: 'gc-btn gc-btn--accent' }, isLoading ? '要約中...' : '内容を要約'), React.createElement('br'), React.createElement('br'), // 要約をリセットするボタン:要約が追加されている場合にのみ表示 React.createElement('button', { onClick: handleResetSummaryClick, disabled: !isSummaryAdded, className: 'gc-btn' }, '要約をリセット') ); } // 最後に作成された注釈のIDを保存し、削除できるようにしておきます let lastSummaryAnnotationId = null; // 要約機能を含む、パネル・コンテンツ用のReactコンポーネントを作成します return React.createElement(PanelContentComponent, { summarizePdfContent: async (model, isConciseEnabled) => { // PDFの内容を要約する機能を呼び出します let summaryResult = await summarizePdfContent(viewer, model, isConciseEnabled); const isError = summaryResult.startsWith("[Error]"); if (isError) { summaryResult = summaryResult.replace("[Error]", "").trim(); } const pageParams = { width: 612, height: 792, pageIndex: 0 }; // 既存の注釈がある場合は削除します if (lastSummaryAnnotationId !== null) { await viewer.removeAnnotation(0, lastSummaryAnnotationId); } else { // 要約を表示するために空のページを追加します viewer.newPage(pageParams); } // 要約結果を含む注釈を生成します const summaryAnnotation = (await viewer.addAnnotation(0, { annotationType: 3, // フリーテキスト注釈 borderStyle: { width: isError ? 5 : 0, style: 1 }, color: [255, 255, 255], borderColor: isError ? [255, 0, 0] : [0, 255, 0], textAlignment: isError ? 1 : 0, // テキストの水平位置:0,1,2 - 左, 中央, 右 rect: [10, 10, pageParams.width - 20, pageParams.height - 20], isRichContents: false, fontSize: 16, contents: summaryResult })).annotation; // 注釈IDを保存し、次回の要約時に削除します lastSummaryAnnotationId = summaryAnnotation.id; allowTextSelection(viewer.scrollView); }, resetSummary: async () => { if (lastSummaryAnnotationId !== null) { await viewer.removeAnnotation(0, lastSummaryAnnotationId); lastSummaryAnnotationId = null; } //await viewer.deletePage(0); // 要約が追加されたページを削除します } }); } function allowTextSelection(scrollArea) { const textDivs = scrollArea.querySelectorAll(".annotationLayer div.gc-text-content"); for (const div of textDivs) { // テキストの選択を許可し、干渉を除去する div.setAttribute("contenteditable", true); div.style.userSelect = "text"; div.style.outline = "none"; div.style.overflow = "auto"; div.style.pointerEvents = "auto"; } // 親要素がコンテキストメニューやテキスト選択をブロックしないようにします scrollArea.addEventListener('contextmenu', (e) => { e.stopPropagation(); }, true); } function createSvgIconElement(React) { return React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "#ffffff" }, React.createElement("path", { d: "M6.005 7.5h12.74c1.253 0 2.255 1.007 2.255 2.249v5.252c0 1.242-1.010 2.249-2.255 2.249h-12.74c-1.253 0-2.255-1.007-2.255-2.249v-5.252c0-1.242 1.010-2.249 2.255-2.249zM5.996 8.25c-0.826 0-1.496 0.675-1.496 1.494v5.262c0 0.825 0.677 1.494 1.496 1.494h12.758c0.826 0 1.496-0.675 1.496-1.494v-5.262c0-0.825-0.677-1.494-1.496-1.494h-12.758zM14.25 10.5v3.75h-0.75v0.75h2.25v-0.75h-0.75v-3.75h0.75v-0.75h-2.25v0.75h0.75zM11.25 12.75h-2.25v2.25h-0.75v-3.75c0-0.834 0.673-1.5 1.504-1.5h0.743c0.833 0 1.504 0.672 1.504 1.5v3.75h-0.75v-2.25zM9.749 10.5c-0.414 0-0.749 0.333-0.749 0.75v0.75h2.25v-0.75c0-0.414-0.332-0.75-0.749-0.75h-0.752z" })); }