要約機能実装
Browse files
gemini.js
CHANGED
@@ -1,5 +1,7 @@
|
|
1 |
let lastSaveTimestamp = 0;
|
2 |
let controller;
|
|
|
|
|
3 |
|
4 |
function formatText() {
|
5 |
const textOrg = document.getElementById('novelContent1').value;
|
@@ -32,6 +34,27 @@ function unmalform(text) {
|
|
32 |
return result || '';
|
33 |
}
|
34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
function partialEncodeURI(text) {
|
36 |
if (!document.getElementById("partialEncodeToggle").checked) {
|
37 |
return text;
|
@@ -106,7 +129,7 @@ function loadFromJson() {
|
|
106 |
if (jsonData.savedTitle) {
|
107 |
document.getElementById('savedTitle').value = jsonData.savedTitle;
|
108 |
}
|
109 |
-
alert('JSON
|
110 |
} catch (error) {
|
111 |
alert('無効なJSONファイルです。');
|
112 |
}
|
@@ -167,13 +190,54 @@ function loadFromUserStorage() {
|
|
167 |
}
|
168 |
}
|
169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
170 |
function createPayload() {
|
171 |
const novelContent1 = document.getElementById('novelContent1');
|
172 |
-
|
|
|
|
|
|
|
173 |
const lines = text.split('\n').filter(x => x);
|
174 |
|
175 |
-
|
176 |
let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`;
|
|
|
177 |
let messages = [
|
178 |
{
|
179 |
"role": "user",
|
@@ -185,7 +249,7 @@ function createPayload() {
|
|
185 |
},
|
186 |
{
|
187 |
"role": "user",
|
188 |
-
"parts": [{ "text":
|
189 |
}
|
190 |
];
|
191 |
|
@@ -498,7 +562,7 @@ function fetchOpenAINonStream(ENDPOINT, payload) {
|
|
498 |
});
|
499 |
}
|
500 |
|
501 |
-
function tokenCount() {
|
502 |
const selectedEndpoint = document.getElementById('endpointSelect').value;
|
503 |
let payload = createPayload();
|
504 |
payload.body = {
|
@@ -506,14 +570,14 @@ function tokenCount() {
|
|
506 |
};
|
507 |
payload.body = JSON.stringify(payload.body);
|
508 |
const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:countTokens?key=` + document.getElementById('geminiApiKey').value;
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
.
|
515 |
-
|
516 |
-
|
517 |
}
|
518 |
|
519 |
async function createDraft() {
|
@@ -528,8 +592,8 @@ async function createDraft() {
|
|
528 |
|
529 |
async function Request() {
|
530 |
let selectedEndpoint = document.getElementById('endpointSelect').value;
|
531 |
-
|
532 |
-
|
533 |
document.getElementById('novelContent2').value = '';
|
534 |
const outputAccordion = document.querySelector('#content2Collapse');
|
535 |
if (outputAccordion) {
|
@@ -546,8 +610,19 @@ async function Request() {
|
|
546 |
} else if (selectedEndpoint === 'restart') {
|
547 |
ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-002:generateContent?key=` + document.getElementById('geminiApiKey').value;
|
548 |
document.getElementById('novelContent2').value = '(下書き中)';
|
549 |
-
|
550 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
551 |
const prompt = `以下の文章を、要件を満たす形に直したものを返信してください。\n返信内容は直した後のテキストのみになるようにし他の内容が混ざらないようにしてください。\n\n# 要件\n- 元の文章や会話の言い回しは不自然な日本語になっている部分もかなり多いので、根本的に書き替えること。ただし、登場人物の一人称や他者への呼称は変更しないこと。\n- 物語の大筋やキャラクターの性格は保ったまま、文章は全く別物に書き換える必要がある。元のテキストそのものはなるべく流用しないで新規に書くよう心掛ける。\n- 随所にレトリックを駆使しつつも凝り過ぎて読み辛くなることのないシンプルな美文になることを意識する。\n- 登場人物の会話を重視し、文脈を読み取り元のキャラクター設定に合った台詞回しを保ちつつ、より生き生きとした魅力的な人物像に仕上がるようにする。\n- 細かい動作や心理描写のディテールを重視し、よりリアルな描写になるようにする。\n- 文章の終わりに「。」をつける、字下げをするなど、一般的な小説のフォーマットに従う書き方にする。\n\n# 文章\n${draft}`;
|
552 |
payload = {
|
553 |
method: 'POST',
|
@@ -747,6 +822,20 @@ function updateNavbarBrand() {
|
|
747 |
}
|
748 |
}
|
749 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
750 |
function generateIndexMenu() {
|
751 |
const content = document.getElementById('novelContent1').value;
|
752 |
const tokens = marked.lexer(content);
|
@@ -816,6 +905,9 @@ function generateIndexMenu() {
|
|
816 |
} else {
|
817 |
indexOffcanvasBody.textContent = '目次がありません';
|
818 |
}
|
|
|
|
|
|
|
819 |
}
|
820 |
|
821 |
function toggleSubMenu(li) {
|
@@ -828,13 +920,42 @@ function toggleSubMenu(li) {
|
|
828 |
|
829 |
function addTextarea(ul, content) {
|
830 |
const li = document.createElement('li');
|
|
|
|
|
831 |
const textarea = document.createElement('textarea');
|
832 |
textarea.readOnly = true;
|
833 |
-
textarea.className = 'form-control mt-2';
|
834 |
textarea.value = content;
|
835 |
textarea.rows = 3;
|
836 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
837 |
li.appendChild(textarea);
|
|
|
|
|
838 |
ul.appendChild(li);
|
839 |
}
|
840 |
|
@@ -880,15 +1001,35 @@ function openAccordionContainingPosition(position) {
|
|
880 |
}
|
881 |
}
|
882 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
883 |
document.addEventListener('DOMContentLoaded', function () {
|
884 |
// ページ読み込み時にデータを復元
|
885 |
loadFromUserStorage();
|
886 |
|
887 |
-
// メイン画面の要素のイベントリスナー。inputイベントが発生する頻度が非常に高いのでこちらの発動は
|
888 |
['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'savedTitle'].forEach(id => {
|
889 |
document.getElementById(id).addEventListener('input', () => {
|
890 |
saveToUserStorage(false);
|
891 |
generateIndexMenu();
|
|
|
892 |
});
|
893 |
});
|
894 |
|
@@ -897,6 +1038,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
897 |
document.getElementById(id).addEventListener('input', () => {
|
898 |
saveToUserStorage(true);
|
899 |
generateIndexMenu();
|
|
|
900 |
});
|
901 |
});
|
902 |
|
@@ -904,6 +1046,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
904 |
document.getElementById(id).addEventListener('change', () => {
|
905 |
saveToUserStorage(true);
|
906 |
generateIndexMenu();
|
|
|
907 |
});
|
908 |
});
|
909 |
|
@@ -926,6 +1069,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
926 |
setInterval(() => {
|
927 |
saveToUserStorage();
|
928 |
generateIndexMenu();
|
|
|
929 |
}, 60000);
|
930 |
|
931 |
// 基本設定のアコーディオンを開く
|
@@ -944,4 +1088,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
944 |
// 初期表示時にも実行
|
945 |
updateNavbarBrand();
|
946 |
generateIndexMenu();
|
|
|
|
|
947 |
});
|
|
|
1 |
let lastSaveTimestamp = 0;
|
2 |
let controller;
|
3 |
+
let lastTokenUpdateTimestamp = 0;
|
4 |
+
let summeries = {};
|
5 |
|
6 |
function formatText() {
|
7 |
const textOrg = document.getElementById('novelContent1').value;
|
|
|
34 |
return result || '';
|
35 |
}
|
36 |
|
37 |
+
async function summerize(text) {
|
38 |
+
const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-002:generateContent?key=${document.getElementById('geminiApiKey').value}`;
|
39 |
+
const prompt = `以下の文章を240文字程度に要約してください:\n\n${text}`;
|
40 |
+
const payload = {
|
41 |
+
method: 'POST',
|
42 |
+
headers: {},
|
43 |
+
body: JSON.stringify({
|
44 |
+
contents: [{ parts: [{ text: prompt }] }],
|
45 |
+
generationConfig: { temperature: 0.7, max_output_tokens: 256 }
|
46 |
+
})
|
47 |
+
};
|
48 |
+
try {
|
49 |
+
const response = await fetch(ENDPOINT, payload);
|
50 |
+
const data = await response.json();
|
51 |
+
return data.candidates[0].content.parts[0].text;
|
52 |
+
} catch (error) {
|
53 |
+
console.error('要約エラー:', error);
|
54 |
+
return '';
|
55 |
+
}
|
56 |
+
}
|
57 |
+
|
58 |
function partialEncodeURI(text) {
|
59 |
if (!document.getElementById("partialEncodeToggle").checked) {
|
60 |
return text;
|
|
|
129 |
if (jsonData.savedTitle) {
|
130 |
document.getElementById('savedTitle').value = jsonData.savedTitle;
|
131 |
}
|
132 |
+
alert('JSONファイルを正常読み込みました');
|
133 |
} catch (error) {
|
134 |
alert('無効なJSONファイルです。');
|
135 |
}
|
|
|
190 |
}
|
191 |
}
|
192 |
|
193 |
+
function createSummarizedText() {
|
194 |
+
const indexOffcanvasBody = document.querySelector('#indexOffcanvas .offcanvas-body');
|
195 |
+
const rootUl = indexOffcanvasBody.querySelector('ul.list-unstyled');
|
196 |
+
let summarizedText = '';
|
197 |
+
|
198 |
+
function processUl(ul, level = 0) {
|
199 |
+
const items = ul.children;
|
200 |
+
for (let item of items) {
|
201 |
+
const a = item.querySelector(':scope > a');
|
202 |
+
if (a) {
|
203 |
+
summarizedText += '#'.repeat(level + 1) + ' ' + a.textContent + '\n';
|
204 |
+
}
|
205 |
+
|
206 |
+
const contentItem = item.querySelector(':scope > ul > li');
|
207 |
+
if (contentItem) {
|
208 |
+
const fullText = contentItem.querySelector('.full-text');
|
209 |
+
const summaryText = contentItem.querySelector('.summery-text');
|
210 |
+
if (summaryText && summaryText.value.trim()) {
|
211 |
+
summarizedText += summaryText.value + '\n\n';
|
212 |
+
} else if (fullText) {
|
213 |
+
summarizedText += fullText.value + '\n\n';
|
214 |
+
}
|
215 |
+
}
|
216 |
+
|
217 |
+
const subUl = item.querySelector(':scope > ul');
|
218 |
+
if (subUl) {
|
219 |
+
processUl(subUl, level + 1);
|
220 |
+
}
|
221 |
+
}
|
222 |
+
}
|
223 |
+
|
224 |
+
if (rootUl) {
|
225 |
+
processUl(rootUl);
|
226 |
+
}
|
227 |
+
|
228 |
+
return summarizedText.trim();
|
229 |
+
}
|
230 |
+
|
231 |
function createPayload() {
|
232 |
const novelContent1 = document.getElementById('novelContent1');
|
233 |
+
let text = novelContent1.value;
|
234 |
+
if (document.getElementById('summerizedPromptToggle').checked) {
|
235 |
+
text = createSummarizedText();
|
236 |
+
}
|
237 |
const lines = text.split('\n').filter(x => x);
|
238 |
|
|
|
239 |
let systemPrompt = `${partialEncodeURI(document.getElementById('generatePrompt').value)}`;
|
240 |
+
let prompt = `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。${systemPrompt}`;
|
241 |
let messages = [
|
242 |
{
|
243 |
"role": "user",
|
|
|
249 |
},
|
250 |
{
|
251 |
"role": "user",
|
252 |
+
"parts": [{ "text": prompt }]
|
253 |
}
|
254 |
];
|
255 |
|
|
|
562 |
});
|
563 |
}
|
564 |
|
565 |
+
async function tokenCount() {
|
566 |
const selectedEndpoint = document.getElementById('endpointSelect').value;
|
567 |
let payload = createPayload();
|
568 |
payload.body = {
|
|
|
570 |
};
|
571 |
payload.body = JSON.stringify(payload.body);
|
572 |
const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/${selectedEndpoint}:countTokens?key=` + document.getElementById('geminiApiKey').value;
|
573 |
+
try {
|
574 |
+
const response = await fetch(ENDPOINT, payload);
|
575 |
+
const data = await response.json();
|
576 |
+
return data.totalTokens;
|
577 |
+
} catch (error) {
|
578 |
+
console.error('エラー:', error);
|
579 |
+
return null;
|
580 |
+
}
|
581 |
}
|
582 |
|
583 |
async function createDraft() {
|
|
|
592 |
|
593 |
async function Request() {
|
594 |
let selectedEndpoint = document.getElementById('endpointSelect').value;
|
595 |
+
const requestButton = document.getElementById('requestButton');
|
596 |
+
requestButton.disabled = true;
|
597 |
document.getElementById('novelContent2').value = '';
|
598 |
const outputAccordion = document.querySelector('#content2Collapse');
|
599 |
if (outputAccordion) {
|
|
|
610 |
} else if (selectedEndpoint === 'restart') {
|
611 |
ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-002:generateContent?key=` + document.getElementById('geminiApiKey').value;
|
612 |
document.getElementById('novelContent2').value = '(下書き中)';
|
613 |
+
try {
|
614 |
+
draft = await createDraft();
|
615 |
+
} catch (error) {
|
616 |
+
console.error('エラー:', error);
|
617 |
+
requestButton.disabled = false;
|
618 |
+
requestButton.classList.add('green-flash-bg');
|
619 |
+
setTimeout(() => {
|
620 |
+
requestButton.classList.remove('green-flash-bg');
|
621 |
+
}, 2000);
|
622 |
+
return;
|
623 |
+
}
|
624 |
+
//document.getElementById('novelContent2').value = `# 下書き\n${draft}\n\n# リライト\n`;
|
625 |
+
document.getElementById('novelContent2').value = "";
|
626 |
const prompt = `以下の文章を、要件を満たす形に直したものを返信してください。\n返信内容は直した後のテキストのみになるようにし他の内容が混ざらないようにしてください。\n\n# 要件\n- 元の文章や会話の言い回しは不自然な日本語になっている部分もかなり多いので、根本的に書き替えること。ただし、登場人物の一人称や他者への呼称は変更しないこと。\n- 物語の大筋やキャラクターの性格は保ったまま、文章は全く別物に書き換える必要がある。元のテキストそのものはなるべく流用しないで新規に書くよう心掛ける。\n- 随所にレトリックを駆使しつつも凝り過ぎて読み辛くなることのないシンプルな美文になることを意識する。\n- 登場人物の会話を重視し、文脈を読み取り元のキャラクター設定に合った台詞回しを保ちつつ、より生き生きとした魅力的な人物像に仕上がるようにする。\n- 細かい動作や心理描写のディテールを重視し、よりリアルな描写になるようにする。\n- 文章の終わりに「。」をつける、字下げをするなど、一般的な小説のフォーマットに従う書き方にする。\n\n# 文章\n${draft}`;
|
627 |
payload = {
|
628 |
method: 'POST',
|
|
|
822 |
}
|
823 |
}
|
824 |
|
825 |
+
async function updateTokenCount(force = false) {
|
826 |
+
const currentTime = Date.now();
|
827 |
+
if (currentTime - lastTokenUpdateTimestamp < 60000 && !force) {
|
828 |
+
console.debug('トークン数更新をスキップします');
|
829 |
+
return;
|
830 |
+
}
|
831 |
+
console.debug('トークン数更新を実行します');
|
832 |
+
|
833 |
+
const count = await tokenCount();
|
834 |
+
const indexOffcanvasLabel = document.getElementById('indexOffcanvasLabel');
|
835 |
+
indexOffcanvasLabel.textContent = `目次 (${count}トークン)`;
|
836 |
+
lastTokenUpdateTimestamp = currentTime;
|
837 |
+
}
|
838 |
+
|
839 |
function generateIndexMenu() {
|
840 |
const content = document.getElementById('novelContent1').value;
|
841 |
const tokens = marked.lexer(content);
|
|
|
905 |
} else {
|
906 |
indexOffcanvasBody.textContent = '目次がありません';
|
907 |
}
|
908 |
+
|
909 |
+
updateAllAccordionHeaderCounts();
|
910 |
+
updateTokenCount(true); // トークン数を強制更新
|
911 |
}
|
912 |
|
913 |
function toggleSubMenu(li) {
|
|
|
920 |
|
921 |
function addTextarea(ul, content) {
|
922 |
const li = document.createElement('li');
|
923 |
+
|
924 |
+
// テキストエリアの作成
|
925 |
const textarea = document.createElement('textarea');
|
926 |
textarea.readOnly = true;
|
927 |
+
textarea.className = 'form-control mt-2 full-text';
|
928 |
textarea.value = content;
|
929 |
textarea.rows = 3;
|
930 |
+
|
931 |
+
// 要約用のテキストエリアの作成
|
932 |
+
const summaryInput = document.createElement('textarea');
|
933 |
+
summaryInput.className = 'form-control mt-2 summery-text';
|
934 |
+
summaryInput.placeholder = '要約';
|
935 |
+
summaryInput.rows = 3;
|
936 |
+
|
937 |
+
// 要約取得ボタンの作成
|
938 |
+
const summaryButton = document.createElement('button');
|
939 |
+
summaryButton.textContent = '要約を取得';
|
940 |
+
summaryButton.className = 'btn btn-secondary mt-2';
|
941 |
+
summaryButton.onclick = async () => {
|
942 |
+
summaryButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Loading...';
|
943 |
+
summaryButton.disabled = true;
|
944 |
+
try {
|
945 |
+
const summary = await summerize(textarea.value);
|
946 |
+
summaryInput.value = summary;
|
947 |
+
summeries[textarea.value] = summary;
|
948 |
+
updateTokenCount(true);
|
949 |
+
} finally {
|
950 |
+
summaryButton.innerHTML = '要約を取得';
|
951 |
+
summaryButton.disabled = false;
|
952 |
+
}
|
953 |
+
};
|
954 |
+
|
955 |
+
// 要素の追加
|
956 |
li.appendChild(textarea);
|
957 |
+
li.appendChild(summaryInput);
|
958 |
+
li.appendChild(summaryButton);
|
959 |
ul.appendChild(li);
|
960 |
}
|
961 |
|
|
|
1001 |
}
|
1002 |
}
|
1003 |
|
1004 |
+
function updateAccordionHeaderCount(accordionId) {
|
1005 |
+
const accordionItem = document.getElementById(accordionId).closest('.accordion-item');
|
1006 |
+
if (!accordionItem) return;
|
1007 |
+
|
1008 |
+
const textarea = accordionItem.querySelector('.accordion-body textarea');
|
1009 |
+
const header = accordionItem.querySelector('.accordion-header button');
|
1010 |
+
|
1011 |
+
if (textarea && header) {
|
1012 |
+
const charCount = textarea.value.length;
|
1013 |
+
const originalText = header.textContent.split('(')[0].trim();
|
1014 |
+
header.textContent = `${originalText} (${charCount}文字)`;
|
1015 |
+
}
|
1016 |
+
}
|
1017 |
+
|
1018 |
+
function updateAllAccordionHeaderCounts() {
|
1019 |
+
const accordionIds = ['promptsCollapse', 'content1Collapse', 'nextPromptCollapse', 'content2Collapse'];
|
1020 |
+
accordionIds.forEach(updateAccordionHeaderCount);
|
1021 |
+
}
|
1022 |
+
|
1023 |
document.addEventListener('DOMContentLoaded', function () {
|
1024 |
// ページ読み込み時にデータを復元
|
1025 |
loadFromUserStorage();
|
1026 |
|
1027 |
+
// メイン画面の要素のイベントリスナー。inputイベントが発生する頻度が非常に高いのでこちらの発動は60秒に1回に制限する
|
1028 |
['novelContent1', 'novelContent2', 'generatePrompt', 'nextPrompt', 'savedTitle'].forEach(id => {
|
1029 |
document.getElementById(id).addEventListener('input', () => {
|
1030 |
saveToUserStorage(false);
|
1031 |
generateIndexMenu();
|
1032 |
+
updateTokenCount(); // 60秒に1回の制限が適用される
|
1033 |
});
|
1034 |
});
|
1035 |
|
|
|
1038 |
document.getElementById(id).addEventListener('input', () => {
|
1039 |
saveToUserStorage(true);
|
1040 |
generateIndexMenu();
|
1041 |
+
updateTokenCount(); // 60秒に1回の制限が適用される
|
1042 |
});
|
1043 |
});
|
1044 |
|
|
|
1046 |
document.getElementById(id).addEventListener('change', () => {
|
1047 |
saveToUserStorage(true);
|
1048 |
generateIndexMenu();
|
1049 |
+
updateTokenCount(); // 60秒に1回の制限が適用される
|
1050 |
});
|
1051 |
});
|
1052 |
|
|
|
1069 |
setInterval(() => {
|
1070 |
saveToUserStorage();
|
1071 |
generateIndexMenu();
|
1072 |
+
updateTokenCount(true); // 強制的に更新
|
1073 |
}, 60000);
|
1074 |
|
1075 |
// 基本設定のアコーディオンを開く
|
|
|
1088 |
// 初期表示時にも実行
|
1089 |
updateNavbarBrand();
|
1090 |
generateIndexMenu();
|
1091 |
+
updateAllAccordionHeaderCounts();
|
1092 |
+
updateTokenCount(true); // 強制的に更新
|
1093 |
});
|