hisaruki commited on
Commit
bcccbd1
·
1 Parent(s): b0e88e3

要約機能実装

Browse files
Files changed (1) hide show
  1. gemini.js +166 -20
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
- const text = novelContent1.value;
 
 
 
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": `続きを書いて。${partialEncodeURI(document.getElementById('nextPrompt').value)} ${document.getElementById('characterCountInput').value}文字程度。${systemPrompt}` }]
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
- fetch(ENDPOINT, payload)
510
- .then(response => response.json())
511
- .then(data => {
512
- console.log(data);
513
- })
514
- .catch(error => {
515
- console.error('エラー:', error);
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
- document.getElementById('requestButton').disabled = true;
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
- draft = await createDraft();
550
- document.getElementById('novelContent2').value = `# 下書き\n${draft}\n\n# リライト\n`;
 
 
 
 
 
 
 
 
 
 
 
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
- textarea.readOnly = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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イベントが発生する頻度が非常に高いのでこちらの発動は5秒に1回に制限する
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
  });