Azure OpenAI（ChatGPT3.5）を活用して、Teams会議の議事録を自動で作成するアプリを1時間で作る。

# Azure OpenAIを活用して、Teams会議の議事録を自動で作成するアプリを1時間で作る。

## はじめに
掲題の通りです。
ここ最近、少人数プロジェクトを発足したんですが
それぞれの作業が多く、議事録が負担になりそうだったので
Open AIを活用して議事録を自動で作成したいと思います。

サンデープログラムなので
Open AIでコードの生成しつつ、自分の手で直しつつ、臨機応変にOpen AIの手を借りながら1時間で作ります。


　
　
### 完成イメージ
　
#### Ver1.0の完成イメージ

**インプットに使うデータのイメージ**
Teamsで会議したときの会議のトランスクリプトです。
マスクかけているので、わけがわからん内容となっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/8283962d-9433-0dd7-3f57-959c6d5a1c21.png)

　
　
**議事録としてアウトプットした際のイメージ**
わけがわからないトランスクリプトでもきちんと議事録として内容が整理されていることがわかります。
MarkDown形式(以下MD)の指定のフォーマットで議事録を出力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/1e95c3a7-3222-bb0e-339c-80e4ac7d8c4a.png)



　
　

#### Ver1.1（魔改造後）の完成イメージ
　
**【魔改造後】インプットに使うデータのイメージ**
国会の会議録情報やどんな情報からでも議事を作成できるように魔改造してみました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/bd816450-5279-c8af-89fb-e3c8d19299a7.png)
　


**【魔改造後】アウトプットのイメージ**
どんな形式だろうが長文だろうが議事録を作成するように修正しました。
議事録を作成できるトークン数（3000トークン以下）まで繰り返し要約するので従量課金が怖いですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/38ce71eb-8542-74eb-c014-9729de30735d.png)



　
### Open AIを活用した議事録作成アプリについて
議事録を作成するだけなら
『わざわざアプリなんか作らないで、直接テキストをChatGPTに投げればいいじゃーん』
って突っ込みがありそうですが、ChatGPTないし、Open AIチャットをそのまま使う場合、いくつかの課題点があります。

　
　
　
### ChatGPTないし、Open AIチャットで議事録を作成するにあたる課題点


#### セキュリティの課題

現在、ChatGPTではHistoryやTrainingのON/OFFができるので
以前よりは、機密情報保護能力が高くはなっていますが
会社やプロジェクトのセキュリティポリシーに応じて使用可能かどうか判断は必要です。
今後開始されるであろう法人向けの『GhatGpt Business』であればセキュリティポリシーの課題をクリアできることが多いかもしれません。

一方、Azure OpenAIでは標準で再学習を封じているだけでなくMicrosoft Azureのセキュリティとコンプライアンス基準に準拠していたり、データの暗号化やアクセス制御・監査ログにも対応しているため
会社やプロジェクトのセキュリティポリシーをクリアできる可能性が高いです。

もしこういった機密情報を取り扱うアプリを作る場合、必ずセキュリティポリシーの確認をしたほうがいいと思います。
　
　


#### トークン制限の壁を越えていかなければならない

Open AIにはトークン制限の壁というのがあり
（ざっくりいうと形態素解析してばらした単語単位をトークンとして数える）

GPT4で約8,000トークン、GPT3.5ターボで約4,000トークンの壁があります。
1時間の会議でだいたい30,000トークン近いトランスクリプトになるので、トランスクリプトをそのままOpen AIに投げるとエラーが返ってきます。

そこでチャンク分割を行い、全体が8,000トークンに収まるように
トランスクリプトを縮小していく必要があります。


　
　
　

### トークン数の壁を越えて、議事録を作成するまでにざっくりとした処理の流れ

例として10,000トークンと書いていますが
1時間の会議だとだいたい30,000トークンくらいになります。
前処理で、テキストクリーニングすれば若干はトークンの節約になるとは思いますが・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/818c1879-ff00-e7b4-34a2-5c7ce3760610.png)


　
## チャンク分割機能の作成開始


### とりあえず叩きを作る

筆者はnode信者なので、アプリはnodeで作成します。
スペックの低いPCでもさくさく動いてくれるのが嬉しい。
Open AIを使ってチャンク分割機能をまずは作っていきます。

よく具体的なお願いをしなさいと言われていますが、これに関しては具体的な案がないので
ばっくりとしたお願いをしていきます。
あとで軌道修正すれば何んとなかります。

あとで分割するトークン数は変更しますが仮で7000トークン分割するようにプロンプトを書きます。

#### 叩きの作成
```text:INPUT
日本語で書かれたvtt形式のドキュメントを7000トークン程度でチャンク分割するアプリをnode.jsで作成してください
```


<details>
<summary>output（長いので折りたたんでます）</summary>

```text:output
私はAIアシスタントであり、コードを直接作成することはできませんが、アプリの作成に役立つ情報を提供できます。以下は、日本語のVTTファイルを7000トークンごとに分割する基本的なnode.jsアプリの概要です。
必要なパッケージをインストールする
まず、プロジェクトに必要なパッケージをインストールしましょう。fs（ファイルシステム）はNode.jsの標準モジュールであり、ファイル操作を行うために使用されます。また、kuromojiは、日本語テキストのトークン化に役立つパッケージです。
npm install kuromoji  
 
2. アプリのコードを作成する
app.jsというファイルを作成し、以下のコードを追加します。
const fs = require('fs');  
const kuromoji = require('kuromoji');  
  
const INPUT_FILE = 'input.vtt';  
const OUTPUT_FILE_PREFIX = 'output_';  
const TOKEN_LIMIT = 7000;  
  
// 日本語テキストをトークン化する関数  
function tokenizeText(text, callback) {  
  kuromoji.builder({ dicPath: 'node_modules/kuromoji/dict' }).build((err, tokenizer) => {  
    if (err) throw err;  
    const tokens = tokenizer.tokenize(text);  
    callback(tokens);  
  });  
}  
  
// VTTファイルを読み込む関数  
function readVttFile(file) {  
  return new Promise((resolve, reject) => {  
    fs.readFile(file, 'utf8', (err, data) => {  
      if (err) reject(err);  
      resolve(data);  
    });  
  });  
}  
  
// VTTファイルを分割する関数  
async function splitVttFile() {  
  try {  
    const vttContent = await readVttFile(INPUT_FILE);  
    const vttLines = vttContent.split('\n');  
  
    let currentTokenCount = 0;  
    let currentChunk = '';  
    let chunkIndex = 0;  
  
    for (const line of vttLines) {  
      if (!line.startsWith('00:')) {  
        currentChunk += line + '\n';  
        continue;  
      }  
  
      const text = line.replace(/<[^>]*>/g, '').replace(/(\d\d:){2}\d\d\.\d\d\d --> (\d\d:){2}\d\d\.\d\d\d/g, '');  
      await tokenizeText(text, (tokens) => {  
        currentTokenCount += tokens.length;  
  
        if (currentTokenCount >= TOKEN_LIMIT) {  
          fs.writeFileSync(`${OUTPUT_FILE_PREFIX}${chunkIndex}.vtt`, currentChunk.trim());  
          currentChunk = '';  
          currentTokenCount = 0;  
          chunkIndex++;  
        }  
        currentChunk += line + '\n';  
      });  
    }  
  
    if (currentChunk) {  
      fs.writeFileSync(`${OUTPUT_FILE_PREFIX}${chunkIndex}.vtt`, currentChunk.trim());  
    }  
  } catch (err) {  
    console.error('Error:', err.message);  
  }  
}  
  
splitVttFile();  
 
3. VTTファイルを分割する
input.vttという名前のVTTファイルをプロジェクトのルートフォルダに配置し、
```
</details>
　

　
　　
　
途中で切れてしまってますね。
途中で切れてしまった場合は以下のようにリクエストします。

```text:INPUT
続けてください
```


<details>
<summary>output（長いので折りたたんでます）</summary>

```text:output
アプリを実行する準備が整いました。以下の手順に従って、VTTファイルを分割しましょう。
input.vttという名前のVTTファイルをプロジェクトのルートフォルダに配置します。このファイルは、アプリが読み込み、分割する対象のファイルです。
コマンドプロンプトまたはターミナルを開き、プロジェクトのルートフォルダに移動します。
次のコマンドを実行して、アプリを起動します。
node app.js  
 
このコマンドを実行すると、アプリはinput.vttファイルを読み込み、7000トークンごとに分割します。分割された各チャンクは、output_0.vtt、output_1.vtt、output_2.vttなどの名前でプロジェクトのルートフォルダに保存されます。

この基本的なアプリは、VTTファイルをトークン単位で分割する機能を提供しますが、さらに高度な機能やエラー処理を追加することができます。たとえば、入力ファイル名や出力ファイル名のプレフィックスをコマンドライン引数として受け入れるように変更したり、分割されたファイルの品質を維持するために字幕のタイミングを調整することができます。


```
</details>
 
　
　
　 
実際に動かしてみないとわからないのでとりあえず
言われるがままアプリを作って動かしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/29a7f5c2-9048-ae41-3927-be8ecf43f257.png)


　
とりあえず実行してみると
kuromojiが動いているので、くっそ重たいです。
ただ分割ファイルが１つしか生成されません。　7000トークン程度で切れてはいますが2つ目のファイルができません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/cbeec56d-9c90-f2a2-d586-45e9c0ebe2a9.png)




　
　 
修正を依頼します
```text:INPUT
動かしてみましたが  
チャンク分割は行われず
output_0.vttしか作成されません。
```
　
　
　　　
8回ほど修正を依頼して動くものが出てきました
（自分で直せばいいじゃーん　と思いつつ何度も修正依頼。）



<details>
<summary>output（長いので折りたたんでます）</summary>

```text:output
すみません、前回のコードが期待通りに動作しなかったことについて謝罪いたします。こちらが改良されたコードです。今回は、各行に対して行番号やタイムスタンプを処理し、字幕テキストをトークン化する際に適切な処理を行います。
const fs = require('fs');  
const kuromoji = require('kuromoji');  
  
const INPUT_FILE = 'input.vtt';  
const OUTPUT_FILE_PREFIX = 'output_';  
const TOKEN_LIMIT = 7000;  
  
// 日本語テキストをトークン化する関数  
function tokenizeText(text, callback) {  
  kuromoji.builder({ dicPath: 'node_modules/kuromoji/dict' }).build((err, tokenizer) => {  
    if (err) throw err;  
    const tokens = tokenizer.tokenize(text);  
    callback(tokens);  
  });  
}  
  
// VTTファイルを読み込む関数  
function readVttFile(file) {  
  return new Promise((resolve, reject) => {  
    fs.readFile(file, 'utf8', (err, data) => {  
      if (err) reject(err);  
      resolve(data);  
    });  
  });  
}  
  
// VTTファイルを分割する関数  
async function splitVttFile() {  
  try {  
    const vttContent = await readVttFile(INPUT_FILE);  
    const vttLines = vttContent.split('\n');  
  
    let currentTokenCount = 0;  
    let currentChunk = '';  
    let chunkIndex = 0;  
    let isTextLine = false;  
  
    for (const line of vttLines) {  
      if (line.startsWith('WEBVTT')) {  
        currentChunk = 'WEBVTT\n\n';  
        continue;  
      }  
  
      if (line.match(/^\d+$/)) {  
        isTextLine = false;  
        currentChunk += line + '\n';  
        continue;  
      }  
  
      if (line.match(/(\d\d:){2}\d\d\.\d\d\d --> (\d\d:){2}\d\d\.\d\d\d/)) {  
        isTextLine = true;  
        currentChunk += line + '\n';  
        continue;  
      }  
  
      if (isTextLine) {  
        const tokens = await new Promise((resolve) => tokenizeText(line, resolve));  
        currentTokenCount += tokens.length;  
        if (currentTokenCount >= TOKEN_LIMIT) {  
          fs.writeFileSync(`${OUTPUT_FILE_PREFIX}${chunkIndex}.vtt`, currentChunk.trim());  
          currentChunk = 'WEBVTT\n\n';  
          currentTokenCount = tokens.length;  
          chunkIndex++;  
        }  
      }  
      currentChunk += line + '\n';  
    }  
  
    // 最後のチャンクを保存する  
    if (currentChunk) {  
      fs.writeFileSync(`${OUTPUT_FILE_PREFIX}${chunkIndex}.vtt`, currentChunk.trim());  
    }  
  } catch (err) {  
    console.error('Error:', err.message);  
  }  
}  
  
splitVttFile();  
 
このコードでは、isTextLine変数を使用して、現在の行が字幕テキスト行であるかどうかをチェックしています。これにより、正しくトークン化されたテキストのみがカウントされ、ファイルが適切に分割されます。再度アプリを実行し、正しく動作することを確認してください。



```
</details>
 
 
　
　
　
　
分割できた！！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/4d3c0b6f-2be1-4521-13a9-7331cce495c1.png)

　
　


### 次は分割単位でOpen AIで要約して、議事録を圧縮します。
　
　
#### Open AIではOpen AIと接続して、要約を作成する処理を書くことができない。
　
5～6回くらいがんばったんですが
Open AIでは、Open AIではOpen AIに接続して、文章を要約する処理は書けませんでした。

おそらく現行のAPIは2023年仕様ですが、OepnAIの情報は2021年時点のものになるので
API仕様が変化していることなどが理由かと思われますが、Open AIを使用して、Open AIを使うためのコードの自動生成はできませんでした。

なので自力で書いていきます。

　




### 手作業で、Open AIとの接続処理を書く

#### 手作業で追加した処理は以下の通り　
```text:追加した処理

// OpenAI APIで要約を行う関数  
async function summarize(chunk) {  
  chunk = cleanText(chunk);  
  
  const input_prompt = `以下を意味や重要な点を損なうことなく、日本語で要約してください。   
  ${chunk}`;  
  
  const uri = OPENAI_API_ENDPOINT;  
  const header = {  
    'Content-Type': 'application/json',  
    'api-key': OPENAI_API_KEY  
  };  
  const postBody = {  
    max_tokens: 1900,  
    temperature: 0.7,  
    top_p: 0.95,  
    frequency_penalty: 0,  
    presence_penalty: 0,  
    stop: ['##'],  
    messages: [  
      {  
        role: 'user',  
        content: input_prompt  
      }  
    ]  
  };  
  try {  
    const response = await axios.post(uri, postBody, {  
      headers: header  
    });    
    const answer = response.data.choices[0].message.content;  
    return answer;  
  } catch (error) {  
    console.error(error);  
    return '要約に失敗しました。';  
  }  
}  

```
　


#### 実行結果　


**インプット**
マスクしてるので、わけのわからないトランスクリプトになってます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/8283962d-9433-0dd7-3f57-959c6d5a1c21.png)



**アウトプット**
あれだけわけのわからないトランスクリプトが整理されて要約されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/8419440a-eaaf-dd31-863d-85b0229b1260.png)




 
 
 
　
　
### コンソールで動かし続けるのも微妙なので、画面で動くようにします。

めんどうなのでここからまたOpen AIを使用してコードを作成していきます。
長くなるので、どんな命令をしたか略して書いていきます。


　
**自分の手でコードを変更しているので、一度コードの全文を覚えさせます**
```text:INPUT
最新のapp.jsです。　理解できたらyesとだけ回答してください
｛最新のコード全文｝
```
　
```text:output
yes
```
　
　
　
　
#### 最新のコードに対して改良の命令を出していきます
**画面で動くようにします**
```text:INPUT
Webで要約結果を出力するようにしたいです。
画面を開いた時点では何もせず、ボタンを押したら要約を実行して、要約が完了したら要約を表示してください。
```
　

```text:output
{略（コードが大量に出てきますが省略します。）}
```



　
　
　
　
**画面で動くようになったのですがトランスクリプトが長いと処理に時間がかかりすぎて
タイムアウトするので、更に改良します**
```text:INPUT
タイムアウトするため、ループ数から進捗を算出し、10秒起きに画面上の進捗を更新し
進捗が100%になったら要約結果を画面表示するようにしたいです。
```

```text:output
{略（コードが大量に出てきますが省略します。）}
```


　
多少手直しをしてしまっていますが、主に以下のような処理が追加されました。

```text:このプロンプトで追加されたコードの一部
/**********************************************
 *  画面上から最新の情報を取得するためのio
 **********************************************/
  io.on('connection', (socket) => {  
    console.log('Client connected');  
    
  // クライアントからのイベントをリッスン  
  socket.on('start_summarization', async (vttContent) => { // vttContentを引数に追加  
    console.log('サーバー側で受信したファイル内容:', vttContent); // デバッグ用  
    await splitAndSummarizeVttFile(vttContent);  
  });  
    
    // クライアントとの接続が切れたときのイベントをリッスン  
    socket.on('disconnect', () => {  
      console.log('Client disconnected');  
    });  
  });  
    
  // 10秒ごとに進捗をクライアントに送信し、100%になったら議事録と要約を表示  
  setInterval(() => {  
    // 現在の進捗を画面に送信
    io.emit('progress', progress);  
    if (isCompleted) {  
      // 完成した議事録を画面に送信
      io.emit('completed', giji);   
    }  
     // 10秒おきに更新する
  }, 10000);  

```

　
　
　
　
**テストを繰り返し、問題が解決するまで改良します**
エラーを指摘し、直るまでひたすら修正します。
ChatGPTで直すのがめんどくさくなったら、自分の手で直したほうが早い場合もあります。
```text:INPUT
●●でエラーが出ます。　修正してください。
```
```text:output
{略（コードが大量に出てきますが省略します。）}
```


　
　
　
　

### 要約の集合体から議事録を作成する処理の追加

#### 手作業で要約から議事録を作成する処理を追加します

プロンプトは仮なので、テストして
気に入らなければ後で修正します。

```text:手作業で追加
// OpenAI APIで議事録作成を行う関数  
async function gijiroku(chunk) {  
    chunk = cleanText(chunk);  
    
    const input_prompt = `以下の文章から議事録を作成してください。タイトル・出席者・議題・議論の概要・決定事項・課題を整理してください。句点の後ろに改行をいれてください。
    ${chunk}`;  
    
    const uri = OPENAI_API_ENDPOINT;  
    const header = {  
      'Content-Type': 'application/json',  
      'api-key': OPENAI_API_KEY  
    };  
    const postBody = {  
      max_tokens: 1900,  
      temperature: 0.7,  
      top_p: 0.95,  
      frequency_penalty: 0,  
      presence_penalty: 0,  
      stop: ['##'],  
      messages: [  
        {  
          role: 'user',  
          content: input_prompt  
        }  
      ]  
    };  
    try {  
      const response = await axios.post(uri, postBody, {  
        headers: header  
      });      
      const answer = response.data.choices[0].message.content;  
      return answer;  
    } catch (error) {  
      console.error(error);  
      return '議事録の作成に失敗しました。';  
    }  
  }  
```

　　
　
　

#### デザインはずたぼろですがトランスクリプトから作成した議事録が表示されるようになりました。

とりあえず、やりたかったことはできたので
あとはプロジェクト内のファイルを参照するのではなく
アップロードしたファイルから議事録を作成するように改良したい。
今回はマスクデータを使用してテストしていたので、実データを使用しながらプロンプトチューニングもしなければいけない。
ここから先が長い。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/2bbb8d97-5f74-607d-4256-af0732181760.png)


　
　
　

### 画面上から議事録元のファイルをアップロードして、議事録を作成するように修正

現状はプロジェクト内にあるinput.vttを参照するようにしていますが、画面からアップロードしたvttを参照するようにしたいです

```text:INPUT
現状はプロジェクト内にあるinput.vttを参照するようにしていますが、画面からアップロードしたvttを参照するようにしたいです
```
```text:output
{略（コードが大量に出てきますが省略します。）}
```


　
　
　 
 

当然のごとく一発で動くものはできなかったので
何回ややりとりをして、不具合を直した結果です。

```text:app.js
/**********************************************
 *  VTTファイルを分割し、要約し、コンソールに出力する関数  
 **********************************************/
async function splitAndSummarizeVttFile(vttContent) {  

    try {
      if (!vttContent) {  
          console.log('ローカルファイル');
          vttContent = await readVttFile(INPUT_FILE);  
      } else {
        console.log('アップロードファイル');
      }
      const vttLines = vttContent.split('\n');  
```


```text:index.html
<!DOCTYPE html>  
<html lang="en">  
<head>  
  <meta charset="UTF-8">  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">  
  <title>GIJIROKU</title>  
</head>  
<body>  
  <h1>GIJIROKU</h1>  
  <input type="file" id="vttFile" accept=".vtt">  
  <button id="summarizeButton">議事録を作成</button>  
  <h2>進捗: <span id="progress">0%</span></h2>  
  <h2>作成した議事録:</h2>  
  <textarea id="result" rows="25" cols="120" wrap="soft" readonly style="white-space: pre-wrap; overflow-wrap: break-word;"></textarea>  
  
  <script src="/socket.io/socket.io.js"></script>  
  <script>  
    const socket = io();  
    const vttFileInput = document.getElementById('vttFile');  
    const summarizeButton = document.getElementById('summarizeButton');  
    const progressElement = document.getElementById('progress');  
    const resultElement = document.getElementById('result');  
    
    // サマライズボタンのクリックイベントをリッスン  
    summarizeButton.addEventListener('click', () => {  
      const vttFile = vttFileInput.files[0];  
      if (!vttFile) {  
        alert('VTTファイルを選択してください。');  
        return;  
      }  
    
      const reader = new FileReader();  
      reader.onload = (e) => {  
        const vttContent = e.target.result;  
        console.log('クライアント側のファイル内容:', vttContent); // デバッグ用  
        socket.emit('start_summarization', vttContent);  
      };  
      reader.onerror = (e) => {  
        console.error('ファイルの読み込みに失敗しました:', e);  
      };  
      reader.readAsText(vttFile, 'UTF-8');  
    
      summarizeButton.disabled = true;  
    });  
    
    // サーバーからの進捗イベントをリッスン  
    socket.on('progress', (data) => {  
      progressElement.textContent = data;  
    });  
  
    // サーバーからの完了イベントをリッスン  
    socket.on('completed', (data) => {  
      resultElement.textContent = data;  
      summarizeButton.disabled = false;  
    });  
  </script>  
</body>  
</html>  

```
　
　
　
　
　


アップロードしたファイルから議事録を作成できるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/11a06d7f-1813-a0b4-4013-cbf79ba258a0.png)
　




## 細かい部分の修正

### あとでプロンプトチューニングしやすいように修正しておきます。


#### 修正前
```text:修正前

/**********************************************
 *  OpenAI APIで要約を行う関数   
 **********************************************/
async function summarize(chunk) {  
  chunk = cleanText(chunk);  
  
  const input_prompt = `以下を意味や重要な点を損なうことなく、日本語で要約してください。   
  ${chunk}`;  
  
  const uri = OPENAI_API_ENDPOINT;  
  const header = {  
    'Content-Type': 'application/json',  
    'api-key': OPENAI_API_KEY  
  };  
  const postBody = {  
    max_tokens: 1900,  
    temperature: 0.7,  
    top_p: 0.95,  
    frequency_penalty: 0,  
    presence_penalty: 0,  
    stop: ['##'],  
    messages: [  
      {  
        role: 'user',  
        content: input_prompt  
      }  
    ]  
  };  
  try {  
    const response = await axios.post(uri, postBody, {  
      headers: header  
    });  
  
    const answer = response.data.choices[0].message.content;  
    return answer;  
  } catch (error) {  
    console.error(error);  
    return '要約に失敗しました。';  
  }  
}  

/**********************************************
 *  OpenAI APIで議事録作成を行う関数  
 **********************************************/
async function gijiroku(chunk) {  
    chunk = cleanText(chunk);  
    
    const input_prompt = `以下の文章から議事録を作成してください。タイトル・出席者・議題・議論の概要・決定事項・課題を整理してください。句点の後ろに改行をいれてください。
    ${chunk}`;  
    
    const uri = OPENAI_API_ENDPOINT;  
    const header = {  
      'Content-Type': 'application/json',  
      'api-key': OPENAI_API_KEY  
    };  
    const postBody = {  
      max_tokens: 1900,  
      temperature: 0.7,  
      top_p: 0.95,  
      frequency_penalty: 0,  
      presence_penalty: 0,  
      stop: ['##'],  
      messages: [  
        {  
          role: 'user',  
          content: input_prompt  
        }  
      ]  
    };  
    try {  
      const response = await axios.post(uri, postBody, {  
        headers: header  
      });      
      const answer = response.data.choices[0].message.content;  
      return answer;  
    } catch (error) {  
      console.error(error);  
      return '議事録の作成に失敗しました。';  
    }  
  }  
```



　
　
　

#### 修正後

```text:修正後 頻繁にメンテナンスしそうなものを定数化
// チャンク分割時のトークン数
const CHUNK_TOKEN_LIMIT = 1900;

// Open AIのトークンリミット数
const OPENAI_TOKEN_LIMIT = 1900;    
  
// 要約作成用のプロンプト
const SUMMARIZE_PROMPT = '以下を意味や重要な点を損なうことなく、日本語で要約してください。';    

// 議事録作成用のプロンプト
const GIJIROKU_PROMPT = '以下の文章から議事録を作成してください。タイトル・出席者・議題・議論の概要・決定事項・課題を整理してください。句点の後ろに改行をいれてください。';    
```




```text:関数をまとめておく
/**********************************************
 *  OpenAI APIで要約や議事録作成を行う関数   
 **********************************************/
async function useOpenAi(prompt,chunk) {  
  chunk = cleanText(chunk);  
  
  const input_prompt = `${prompt}  
  ${chunk}`;  
  
  const uri = OPENAI_API_ENDPOINT;  
  const header = {  
    'Content-Type': 'application/json',  
    'api-key': OPENAI_API_KEY  
  };  
  const postBody = {  
    max_tokens: OPENAI_TOKEN_LIMIT,  
    temperature: 0.7,  
    top_p: 0.95,  
    frequency_penalty: 0,  
    presence_penalty: 0,  
    stop: ['##'],  
    messages: [  
      {  
        role: 'user',  
        content: input_prompt  
      }  
    ]  
  };  
  try {  
    const response = await axios.post(uri, postBody, {  
      headers: header  
    });  
  
    const answer = response.data.choices[0].message.content;  
    return answer;  
  } catch (error) {  
    console.error(error);  
    return 'OpenAIでの処理に失敗しました。';  
  }  
}  
```




```text:呼び出し元も修正しておく
/**********************************************
 *  VTTファイルを分割し、要約し、コンソールに出力する関数  
 **********************************************/
async function splitAndSummarizeVttFile(vttContent) {  
    try {
      if (!vttContent) {  
          // 何もアップロードされなかったらサンプルファイルから議事録を作成する
          console.log('ローカルファイル');
          vttContent = await readVttFile(INPUT_FILE);  
      } else {
        console.log('アップロードファイル');
      }
      const vttLines = vttContent.split('\n');  
    
      // 変数・フラグの初期化
      let currentTokenCount = 0;  
      let currentChunk = '';  
      let chunkIndex = 0;  
      let isTextLine = false;  
      let summaries = '';  
      let count = 1;
    
      // チャンク分割し、要約を作成
      for (const line of vttLines) {  
        // 進捗を更新
        progress = ((count / vttLines.length) * 100).toFixed(2) + '%';  
        console.log(progress);  
        count++;  

        if (line.startsWith('WEBVTT')) {  
          currentChunk = '\n\n';  
          continue;  
        }  
    
        if (line.match(/^\d+$/)) {  
          isTextLine = false;  
          currentChunk += line + '\n';  
          continue;  
        }  
    
        if (line.match(/(\d\d:){2}\d\d\.\d\d\d --> (\d\d:){2}\d\d\.\d\d\d/)) {  
          isTextLine = true;  
          currentChunk += line + '\n';  
          continue;  
        }  
    
        if (isTextLine) {  
          const tokens = await new Promise((resolve) => tokenizeText(line, resolve));  
          currentTokenCount += tokens.length;  
          if (currentTokenCount >= CHUNK_TOKEN_LIMIT) {  
            // Open AIで要約する
            const summary = await useOpenAi(SUMMARIZE_PROMPT,currentChunk);  
            summaries += summary.trim() + '\n\n';  
    
            currentChunk = '\n\n';  
            currentTokenCount = tokens.length;  
            chunkIndex++;  
          }  
        }  
        currentChunk += line + '\n';  
      }  
    
      // 最後のチャンクを要約し、結果を追加する  
      if (currentChunk) {  
        // Open AIで要約する
        const summary = await useOpenAi(SUMMARIZE_PROMPT,currentChunk);  
        summaries += summary.trim() + '\n\n';  
      }  
    
      // まとめた要約をコンソールに出力する  
      console.log(summaries);  

      // まとめた要約をファイルに出力する  
      fs.writeFile(OUTPUT_FILE_NAME, summaries, 'utf8', (err) => {  
        if (err) {  
            console.error('Error writing summaries to file:', err.message);  
        } else {  
            console.log(`Summaries written to ${OUTPUT_FILE_NAME}`);  
        }  
      });  

      // まとめた要約から議事録を作成してファイルに出力する  
      giji = await await useOpenAi(GIJIROKU_PROMPT,summaries);  
      fs.writeFile(OUTPUT_FILE_SUM, giji, 'utf8', (err) => {  
        if (err) {  
            console.error('Error writing summaries to file:', err.message);  
        } else {  
            console.log(`Summaries written to ${OUTPUT_FILE_SUM}`);  
        }  
      });  
      isCompleted = true;  
    
    } catch (err) {  
      console.error('Error:', err.message);  
    }  
  }  

```
　
　
　
　


#### これでチューニングしたいときは以下を修正すればいいので後でわかりやすいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/1bcaab9a-207a-9fcc-9d46-1180185ecd9c.png)

　
　
　

念のため、動作確認しましたが異常はありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/b89b1041-110b-e079-558f-83c4e2b049c3.png)





### 議事録のフォーマットを指定するプロンプトを作成しておく


#### 何も命令しないとフリーフォーマットで議事録を書かれてしまうので、フォーマットを指定します。



```text:MDでフォーマットを指定して、議事録を書いてもらうプロンプト例

// 議事録作成用のプロンプト
const GIJIROKU_PROMPT = `＜トランスクリプト＞を解釈して＜フォーマット＞に合わせてマークダウンで議事録を出力してください。  
{A}に参加者（複数ある場合はloopしてください）
{B}に目的
{E}に決定事項（複数ある場合はloopしてください）
{F}に課題事項（複数ある場合はloopしてください）
{G}に議事（複数ある場合はloopしてください）
{D}に議事の要約（句点で改行してください。）
  
  
＜フォーマット＞  
# 参加者  
- {A}  
  
# 目的  
{B}  
  
# 議事 
- {G}  
  
# 決定事項  
- {E}  
    
# 課題事項  
- {F} 

# 全体の要約
{D}  


   
＜トランスクリプト＞  `;   


```

　
　
　


#### フォーマットを指定して議事録を出力した結果

フォーマットやプロンプトはまだまだ改良の余地がありそうですが
議事録をトランスクリプトから作成できるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/1e95c3a7-3222-bb0e-339c-80e4ac7d8c4a.png)




　　
　
　
　
　
## 魔改造してみた

Teamsのトランスクリプトに関わらず
あらゆる文章から議事録が作成できるように魔改造しました。



　
　
　
以下は、国会の会議のトランスクリプトから議事録を作成してみた例。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/a8c135b6-a480-428e-d80d-0a79e368eda9.png)


### 再帰的に要約する。
3000トークン以下になったら議事録を作成するようにし
トランスクリプトが3000トークン以下になるまで、チャンク分割 → 要約 → 結合を繰り返します。


```text:参考
/**********************************************
 *  kuromojiでトークン数をカウントして、指定のトークンずつ分割して、配列にする関数  
 **********************************************/
async function splitTextIntoChunks(text, chunkSize) {  
  const dicPath = path.join(__dirname, 'node_modules/kuromoji/dict');  
  return new Promise((resolve, reject) => {  
    kuromoji.builder({ dicPath }).build((err, tokenizer) => {  
      if (err) {  
        reject(err);  
        return;  
      }  
  
      const tokens = tokenizer.tokenize(text);  
      const chunks = [];  
  
      for (let i = 0; i < tokens.length; i += chunkSize) {  
        const chunk = tokens.slice(i, i + chunkSize);  
        chunks.push(chunk);  
      }  
 
      resolve(chunks);  
    });  
  });  
}  

/**********************************************
 *  指定のトークン数以下になるまで要約を繰り返して、トークン数を減らす関数
 **********************************************/
async function processText(text,chunkSize) {  
  let chunks = await splitTextIntoChunks(text, chunkSize);  

  let result = '';  
  progress = 0;

  while (true) {  
    const summarizedChunks = [];  
    for (const chunk of chunks) {  
      const chunkText = chunk.map(token => token.surface_form).join('');  
      console.log(`要約中: ${chunkText}`); // 要約中の文章を表示  
      const summary = await useOpenAi(SUMMARIZE_PROMPT, chunkText);  
      console.log(`要約結果: ${summary}`); // 要約結果を表示  
      summarizedChunks.push(summary);  
    }  
  
    const summarizedText = summarizedChunks.join('');  
    const summarizedChunksAgain = await splitTextIntoChunks(summarizedText, chunkSize);  
  
    if (summarizedChunksAgain.length === 1) {  
      progress = 100; // 進捗を100%に設定  
    } else {  
      progress = (1 - (1 / summarizedChunksAgain.length)) * 100; // 進捗を更新  
    }  
    progress = progress.toFixed(2); // 進捗を少数第二位まで丸める  
    console.log(`進捗予測: ${progress}%`); // 進捗を表示  
  
    if (summarizedChunksAgain.length === 1) {  
      result = summarizedChunksAgain[0].map(token => token.surface_form).join('');  
      break;  
    } else {  
      chunks = summarizedChunksAgain;  
    }  
  }  
  
  return result;  
}  


```




　
　
# おまけ：Open AIとの面白かったやりとり集


## バグコードを当たり前のように書いてくる

### 処理の進捗を出したい
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/f70d8c21-6702-733c-6966-3e2966777493.png)

　
### 再起関数の中で進捗を計算するので、進捗が戻ったり進んだりする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/55a51990-f813-8b2f-76c8-421c9ff34739.png)
　



### 惜しいが進捗が100%になった瞬間に画面上の進捗が0%になるバグをぶち込んでくる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/32d10919-99ff-b707-73dd-357340191bee.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/7b1775d6-e007-2d18-0add-c935dfb6aa6e.png)






## 別にほめてない

　
### 進捗計算の式がおかしかったので指摘したのですが

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/c68c080c-1857-0f19-0459-1d3ae8c8e562.png)

　
### 予想を上回る力業コードを書いてきたのでツッコミを入れたら予想外の返答がきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/abc9063a-6382-30c5-767f-f3a1cf4f97b4.png)


　
　

## ただ考えるのが面倒なちょっと複雑な関数をすぐに考えてくれる

### 自分が嫌いな再起関数もすぐに書てくれる

ただOpen AIを再帰的に使うので、事前に無限ループしないかのテストはしないと大変。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/a7289e69-cde4-986e-2c6a-efe2bbcb7ce8.png)

　
　
　
### ループ系は初歩的なバグをぶち込んでくるので注意が必要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/94887e0c-528e-cddd-9d70-0a83773186e7.png)