🌊

読み上げ算のhtmlをchatGPTで生成

2023/04/17に公開

1. はじめに

そろばん練習のための読み上げ算の読み上げをするhtmlファイルをchatGPTに出力させてみました。出力されたコードはうまく動作しました。中身を見てみると、コードの作りに特徴を感じます。
なお、今回のコード等は次の所にリポジトリとして置いておきます。
https://github.com/dzonesasaki/abcus_dictation_of_sums

2. 指示文章と生成結果

chatGPTに次の指示文章を入力しました。

次の要件を満たしwebspeechを使ったhtmlファイルを生成してください。
要件
・開始ボタンをクリックすると読み上げ算とその回答を乱数で生成し読み上げる
・読み上げ算のプロパティは、最小桁数(最小1,初期値2,最大6)、最大桁数(最小1,初期値2,最大7)、引き算の有無、口数(項数)を設定でき、プルダウンで指定する
・読み上げスピードを変えられ、とてもゆっくり、ゆっくり、普通 の3段階
・回答は読み上げ算を読み上げ終わった3秒後に喋る
・数値の内容を一列で表示し、その下に答えという行の後に答えを表示する
・開始ボタンをクリックしたら、前回の内容は消去してから、読み上げを開始する
・chromeブラウザでローカルでも実行できるようにする

読み上げ算について。
・決められた口数分ある数値を次々と加減算していく計算
・口数とは、加減算の項の数
・開始時は「ねがいましては」と言う
・開始直後は加算
・口数最後以外は、数値の後ろに「円 なり」と言う
・減算に変化する場合「引いては」と言い、その後加算があるまで減算し続ける
・加算に変更する場合「加えて」と言い、その後減算があるまで加算し続ける
・最後の口数で、数値の後ろに「円 では」と言う

以上です。わかりましたか?

今回は課金したchatGPTにログインしGPT4を選択して出力させました。
その結果が次です。これをhtmlファイルにして保存しダブルクリックすると実行できます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>読み上げ算 </title>
</head>
<body>
    <h1>読み上げ算</h1>
    <p>
        最小桁数:
        <select id="minDigits">
            <option>1</option>
            <option selected>2</option>
            <option>3</option>
            <option>4</option>
            <option>5</option>
            <option>6</option>
        </select>
    </p>
    <p>
        最大桁数:
        <select id="maxDigits">
            <option>1</option>
            <option selected>2</option>
            <option>3</option>
            <option>4</option>
            <option>5</option>
            <option>6</option>
            <option>7</option>
        </select>
    </p>
    <p>
        引き算の有無: <input type="checkbox" id="allowSubtraction">
    </p>
    <p>
        口数:
        <select id="iterations">
            <option>1</option>
            <option>2</option>
            <option>3</option>
            <option>4</option>
            <option selected>5</option>
            <option>6</option>
            <option>7</option>
            <option>8</option>
        </select>
    </p>
    <p>
        読み上げスピード:
        <select id="speed">
            <option value="0.5">とてもゆっくり</option>
            <option value="0.75">ゆっくり</option>
            <option value="1" selected>普通</option>
        </select>
    </p>
    <button id="startBtn">開始</button>
    <hr>
    <div id="content"></div>
    <script>
        const startBtn = document.getElementById('startBtn');
        const minDigitsSelect = document.getElementById('minDigits');
        const maxDigitsSelect = document.getElementById('maxDigits');
        const allowSubtractionCheckbox = document.getElementById('allowSubtraction');
        const iterationsSelect = document.getElementById('iterations');
        const speedSelect = document.getElementById('speed');
        const content = document.getElementById('content');

        startBtn.addEventListener('click', () => {
            content.innerHTML = '';
            const minDigits = parseInt(minDigitsSelect.value);
            const maxDigits = parseInt(maxDigitsSelect.value);
            const allowSubtraction = allowSubtractionCheckbox.checked;
            const iterations = parseInt(iterationsSelect.value);
            const speed = parseFloat(speedSelect.value);

            let text = 'ねがいましては ';
            let currentOperation = 'add';
            let total = 0;

            for (let i = 0; i < iterations; i++) {
                const num = Math.floor(Math.random() * (Math.pow(10, maxDigits) - Math.pow(10, minDigits - 1))) + Math.pow(10, minDigits - 1);
                content.innerHTML += `${num}円 `;
                if (i === iterations -1) {
                  text += `${num} 円 では`;
                  total += num;
                } else {
                  if (allowSubtraction && Math.random() < 0.5) {
                    if (currentOperation === 'add') {
                    currentOperation = 'subtract';
                    text += `引いては ${num} 円 なり` ;
                    } else {
                      text += `${num} 円 なり` ;
                    }
                    total -= num;
                  } else {
                    if (currentOperation === 'subtract') {
                    currentOperation = 'add';
                    text += `加えて ${num} 円 なり` ;
                    } else {
                    text += `${num} 円 なり` ;
                    }
                    total += num;
                  }
                }
            }

         content.innerHTML += '<br>答え: ';
      setTimeout(() => {
         content.innerHTML += `${total}円`;
         const utterance = new SpeechSynthesisUtterance(text);
         utterance.rate = speed;
         speechSynthesis.speak(utterance);
     }, 3000);
 });
 </script>
</body>
</html>

3. 考察

chatGPTが出力した内容を見てみましょう。

まずはUIの部分を抜粋してみます。次のようにプルダウンメニューで選ぶように記述されており、違和感が無いです。

        最小桁数:
        <select id="minDigits">
            <option>1</option>
            <option selected>2</option>
            <option>3</option>
            <option>4</option>
            <option>5</option>
            <option>6</option>
        </select>

続いて、javascriptの部分。一部抜粋します。

  let text = 'ねがいましては ';
  let currentOperation = 'add';
  let total = 0;

  for (let i = 0; i < iterations; i++) {
      const num = Math.floor(Math.random() * (Math.pow(10, maxDigits) - Math.pow(10, minDigits - 1))) + Math.pow(10, minDigits - 1);
      content.innerHTML += `${num}`;
      if (i === iterations -1) {
        text += `${num} 円 では`;
        total += num;
      } else {
        if (allowSubtraction && Math.random() < 0.5) {
							

読み上げ文章をtextとして定義し、そこに追記していく形でプログラムが進められています。
乱数で10の累乗を使い、最後に切り捨てを行っています。ここは、なるほど、そういう作りもあるなと感心できます。引き算が有効の場合は更に乱数で1/2確率にしています。

プログラムする方には、「これもありだが。。。うーーん」という感想を持つ方もいらっしゃるのではないでしょうか。

指示文章の内容から処理手順を考えると次のようになると思います。

  • UIで指定された各種パラメータを読み取る
  • 最大最小桁および口数に従って、乱数を発生する
  • 答えを計算する
  • 読み上げ文を生成する
  • 表示部分を生成する
  • 読み上げる
  • 表示する

chatGPTが生成したコードを見ると次のように表現できそうです。

  • UIで指定された各種パラメータを読み取る
  • 最大最小桁および口数に従って乱数を発生させ、答えも生成し、読み上げ文章化し表示部分も生成する
  • 読み上げる
  • 表示する

処理としては同じではあるものの、集約された形になっています。

今回の処理は規模が小さく、このような記述でも問題ないかもしれません。
しかし、仕様変更する場合、全てを確認して、書き直す必要があります。

そこで、chatGPTが出力したコードに手を加えてみました。ボタン押下の呼び出しはmainfunc()に変更する前提です。

function mainfunc(){
  const speed = parseInt(document.getElementById('speed').value);
  const waitTime = 3000; //[ms]
  const iterations = parseInt(document.getElementById('iterations').value);
  var aryVal = new Array(iterations);

  clearDisp();
  aryVal = genArray(aryVal);
  const ansVal = calcAns(aryVal);
  const strScript = makeScript(aryVal);
  const strAns = makeAnser(ansVal);

  doSpeech(strScript, speed);
  setTimeout(() => { doSpeech(strAns,1); showArray(aryVal,ansVal);}, waitTime);
}

function doSpeech( mytext, speed){
  const speech = new SpeechSynthesisUtterance(mytext);
  speech.lang = "ja-JP";
  speech.rate = speed;
  window.speechSynthesis.speak(speech);
}

function genArray(aryVal){
  const minDigits = parseInt(document.getElementById('minDigits').value);
  const maxDigits = parseInt(document.getElementById('maxDigits').value);
  const allowSubtraction = document.getElementById('allowSubtraction').checked;

  flagSumMinus = 1;

  while(flagSumMinus){
    for (let i = 0; i < aryVal.length; i++) {
      const num = Math.floor(Math.random() * (Math.pow(10, maxDigits) - Math.pow(10, minDigits - 1))) + Math.pow(10, minDigits - 1);
      const mySign = 1+(allowSubtraction && (Math.random() < 0.5)&&(i>0))*(-2);
      aryVal[i] = mySign*num;
    }
    tmpsum=0;
    aryVal.forEach(x =>{
      tmpsum += x;
    });
    flagSumMinus = tmpsum<0;
  }

  return aryVal;
}

function calcAns(aryval){
  ansval=0;
  aryval.forEach(element => {
    ansval += element;
  });
  return ansval;
}


function makeScript(aryvalues){
  let outStr = 'ねがいましては ';
  var flagMinus = 0;
  
  for (let i = 0; i < aryvalues.length; i++) {
    if((aryvalues[i]<0) && (flagMinus==0)){
      flagMinus = 1;
      outStr += '引いては '
    }else{
      if((aryvalues[i]>0) && (flagMinus==1)){
        flagMinus = 0;
        outStr += '加えて '
      }
    }
    outStr += Math.abs(aryvalues[i]);
    if(i<(aryvalues.length-1)){
      outStr += '円 なり'
    }
    else{
      outStr += '円 では'
    }
  }

  return outStr;
}

function makeAnser(ansval){
  let outStr = '答えは ';
  outStr += ansval.toString();
  outStr += '円 です'
  return outStr;
}

function showArray(aryval,ansval){
  let showstr = '<BR>';
  aryval.forEach(e =>{
    showstr += e.toString();
    showstr += '<BR>';
  });
  showstr += '<BR>';
  showstr += '答え';
  showstr += '<BR>';
  showstr += ansval.toString();
  showstr += '<BR>';
  showstr += ' です';
  showstr += '<BR>';

  document.getElementById('content').innerHTML = showstr;
}

function clearDisp(){
  document.getElementById('content').innerHTML = '';
}

メインの関数mainfunc()では、関数呼び出しのみにしています。
この関数を見ると何を行うのかが一目で分かります。仕様変更がある場合は、どの関数を変更すればよいかなど、あたりをつけることができます。私が行った方法は、機能分類をして階層構造の土台を構築してから詳細の作成にとりかかる流れです。

一方、chatGPTの作り方は、大きな流れを理解したら、あとはその流れに従って順序に作っていくやりかたです。chatGPTは、自然言語処理をベースにしたチャットボットですので、コード出力も自然言語つまり話しことばを出力する形に近くなったと予想されます。更には、chatGPTは出力中に止まることがあります。「続けてください」と投げかけると続きを出力しはじめますが、コードを出力している最中に止まってしまうと、連結させる作業が必要になり不便です。ですので、できるだけ短いコードで出力される方が有り難い訳です。

このように、出力されるコードを見ても、人間のやりかたとはちょっと違う点を感じさせられます。

4. おわりに

今回はchatGPTで、読み上げ算のhtmlを出力させ、その特徴について考察してみました。
自分が知らないタイプのプログラムを出力してくれるので有り難い存在ではあるのですが、その後のメンテナンスを想定したコードは出してくれない可能性が高いです。
膨大なトランジスタを抱えたマシンで可動するchatGPTは、人間の作業時間を短縮してくれる代わりに、多くの資源を使っています。
だからこそ、うまく利用していきたいと感じます。

Discussion