html Javascript Mac プログラミング

動画や音声の文字起こし用に作ったメディアプレーヤー付きエディタ(Mac/Chrome用)

キーボードから手を離さずに動画・音声の再生コントロールがしたい

録音の文字起こしをすることが結構あります。

最近は音声入力を使うことが多くなりましたが、音声を聞きながらキーボード入力することもまだまだあります。

人間の話す速度は結構速いので、なかなかリアルタイムにキーボード入力するのは難しいです。

ひらがなレベルでは音声に追いついても、漢字の変換候補を選択したり、打ち間違いの修正をしている間に置いて行かれてしまいます。

このため音声の一時停止と再生、ちょっと巻き戻してまた再生ということを繰り返すのですが、メディアプレーヤーとエディタが分離しているとこの操作がとにかく面倒くさい。

キーボードから手を離さずに音声の再生をコントロールしたいと思って作ったのが、この文字おこし用のWebアプリです。

これはプレーヤーとテキストエディタを合体させたものです。

テキスト入力中にもショートカットキーで音声の再生・停止・ちょっと巻き戻して再生が瞬時に行えます。

最初は確か同様の機能を持ったフリーソフトを使っていたと思いますが、OSのバージョンアップで動かなくなったのでWebアプリを自作しました。

Webアプリでもブラウザの仕様変更などで動かなくなる時はありますが、Javascriptは情報も多いので、コード修正を重ねて長く使っているアプリとなりました。

 

再生速度変更などの便利機能も搭載

というわけでアプリとコードは以下にあります。

Transcription Helper(github pages)

ソースコード(github)

画面、地味ですねw。まぁ完全に実用ツールなので。

サンプルデータもありますのでアプリの概要はリンク先のアプリを見ていただくのが早いと思います。ただし、Mac/Chromeのみの動作確認です。日本語入力ソフトとの兼ね合いで機種依存度が高いかもしれません。

サンプル以外の動画か音声を使いたい時は読み込みボタンをクリックしてローカルにあるファイルを読み込みます。

ローカルWebアプリですからファイルは参照だけでサーバーへアップロードはされません。

あとは下のテキストエリアで文字を入力しながらshift+ returnで一時停止と再生、shift+spaceでちょい戻し、shift+option+spaceでちょい送りできます。デフォルトのちょい送り(戻し)は4秒です。

ボタンで再生速度の変更もできます。

エディタには以前作った日本語括弧の自動補完機能を付けてあります。

入力したテキストはlocal storageを使った一時保存、ダウンロード、コピーで利用することができます。

とにかく自分にとって必要十分な機能を搭載してあります。控えめに言って滅茶苦茶役に立っています。

最近はキーボード入力だけでなく音声入力用のプレーヤーとしても活躍しています。音声入力を楽に行うためのシステムについては別途記事を書く予定です。

 

【備忘録】コードの各パーツメモ

結構、他の用途にも使える基本的なコードが多いので、以下、各部のコードを備忘録的に残しておきます。

 

input type="file"を使ったvideo要素への動画・音声ファイルの読み込み

 html
<video id="media" width="800" controls > // controlsでコントローラー表示
<source src="sample.mp4"> //sourceタグを使わずにvideoタグ内に記述してもよい
</video>
 
<input id="fileInput"  type="file" onchange="setFilePath()" value="sample.mp4" />
//onchangeに読み込み操作を行ったときの処理をする関数を書く
 
function setFilePath() {
var fileInput = document.getElementById("fileInput");
file = fileInput.files[0]; //配列の先頭だけ取り出す
var video = document.getElementById("media");
video.src = URL.createObjectURL(file);
}

 

 

inputから受け取ったfileオブジェクトをURL.createObjectURLに通すとsrcとして使えます。

video要素のsrcに音声ファイルを割り当てても問題なく動作します。ただ、本来映像が表示されるエリアが空白になってしまうので、ちょっと画面が間抜けになります。

欲を出して選択したファイルの拡張子を見てvideoかaudioを動的に生成する方法もやってみましたが、上手くいかない部分があってやめました。

 

video要素の再生と停止

play()、pause()メソッド

 javascript
const video = document.getElementById("media");
if (video.paused) {
video.play();
} else {
video.pause();
}
//videoの状態を見てplay(),pause()メソッドを呼ぶ

 

video要素の再生速度変更

video.playbackRateプロパティを変更。1が通常の速度

 javascript
video.playbackRate = video.playbackRate - 0.1;

 

video要素のちょい送りとちょい戻し

video.currentTimeプロパティを変更

 javascript
video.currentTime = video.currentTime + SKIP_TIME;

 

キーボードショートカット

こちらにまとめました。

KeyboardEvent.keyCode が非推奨になっていたので KeyboardEventを整理してみた

KeyboardEventで取得できる値について整理しました

続きを見る

 

local storageへの保存と読み込み

window.localStorage.setItem()と.getItem()メソッドを使う

 javascript
window.localStorage.setItem("bkup", genko.value);
genko.value = window.localStorage.getItem("bkup") || "";

 

ウインドウを閉じるときの警告

window.onbeforeunloadに処理をセット

 javascript
window.onbeforeunload = function (event) { event = event || window.event; return event.returnValue = 'メッセージ'; }

 

テキストのダウンロード

ダウンロードボタンにイベントをセットします。

ダウンロードリンクを付けたaタグを生成して、それをプログラム中からクリックするところがミソらしいです。

 javascript
document.getElementById("download").addEventListener("click", function () {
const fileName = "download.txt";
const text = document.getElementById("genko").value;
const blob = new Blob([text], { type: 'text/plain' });
const aTag = document.createElement('a');
aTag.href = URL.createObjectURL(blob);
aTag.target = '_blank';
aTag.download = fileName;
aTag.click();
URL.revokeObjectURL(aTag.href);
}, false);

 

日付付きファイル名の生成

new Date(Date.now() + 9 * 3600000) でJSTを生成。正規表現で数字以外を削除して、前から14文字も抽出(説明するとややこしいw)

 javascript
const fileName = "transcription_" + new Date(Date.now() + 9 * 3600000).toISOString().replace(/[^\d]/g, "").slice(0, 14) + ".txt";

 

textareaの内容をコピー

textarea.select();で全選択、document.execCommand("copy"); でコピー

 javascript
document.getElementById("copy").addEventListener("click", function () {
const textarea = document.getElementById("genko");
textarea.select();
document.execCommand("copy");
}, false);

 

 

ボタンやビデオ要素からフォーカスを外す

noFocusクラスが設定されている要素をクリックした時に原稿エリアにカーソルを戻すようにしました。

クラス指定でまとめてイベントハンドラーを設定するときはdocument.querySelectorAllを使います。

 javascript
const noFocus = document.querySelectorAll(".noFocus");
noFocus.forEach((e) => {
e.addEventListener("focus", function () {
genko.focus();
}, true);
});

 

コード全体も載せておきます。githubに載っている物と違うかもしれません。

index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>

<head>
  <meta http-equiv="content-type" content="text/html;charset=utf-8">
  <title>Transcription Helper</title>
  <link rel="stylesheet" type="text/css" href="main.css" media="all">

</head>

<body>
  <button id="manual">使い方など</button><br><br>
  <div id="man_txt" style="display:none">
    <h3>●文字起こしの補助ソフト</h3>
    <p>会議やインタビューなどの文字起こしをアシストするアプリケーションです。動画や音声を聞きながらテキスト入力ができます。</p>

    <h3>●ショートカット:Shift+Return=再生/停止、Shift+Space=ちょい戻し、Shift+Option+Space=ちょい送り</h3>
    <p>テキスト入力中でも、キーボードショートカットで再生、一時停止や、ちょい戻しが出来ます。</p>

    <h3>●Mac/Chrome専用</h3>
    <p>
      ちなみに<b>MacのChrome専用です。</b>SafariやWindowsではうまく動かなかったと思います</p>

    <h3>●再生速度が変えられる</h3>

    <p>その他の再生機能はボタンで操作します。再生速度を遅くすると、一時停止や巻き戻しの回数が減って効率が良くなります。</p>
    <p>不要な所は再生速度を速めたり、ちょい送りで飛ばしましょう。</p>

    <h3>●一時保存機能</h3>

    <p>
      一時保存をクリックするとテキストエリアのデータをブラウザのlocal storage機能を使ってPCのどこかに保存します。この機能はバックアップ用です。うっかりテキストを消してしまった時などに、一時保存を読み込むをクリックすると、保存した所までのデータが復活できるかもしれません。
    </p>

    <h3>●テキストの保存</h3>

    <p>クリップボードへのコピーとテキストファイルのダウンロードができます。</p>

    <h3>●動画・音声フォーマット</h3>

    <p>MP4やMP3などの動画・音声フォーマットが使えます。</p>

    <h3>●利用条件など</h3>

    <p>
      MITライセンス。利用、再配布に伴うトラブルに作者は一切責任を負いません。<br>
    </p>

    <p>ryjkmr.com</p>
  </div>

  <button id="store" class="noFocus">テキストを一時保存</button>
  <button id="restore" class="noFocus">一時保存したテキストを読み込む</button>
  <button id="copy" class="noFocus">テキストをコピー</button>
  <button id="download" class="noFocus">テキストをダウンロード</button>
  <br>


  <br />
  <div>
    <!-- ビデオ -->
    <video id="media" width="800" controls class="noFocus">
      <source src="sample.mp4">
    </video>
  </div>
  <!-- コントローラー部 -->
  <p id="controller" class="noFocus">
    <input id="fileInput" class="noFocus" type="file" onchange="setFilePath()" value="sample_sound.mp3" />
    <button id="playOrPauseButton" class="noFocus">再生</button>
    <button id="stopButton" class="noFocus">先頭に</button>
    <button id="backButton" class="noFocus">ちょい戻し</button>
    <button id="forwardButton" class="noFocus">ちょい送り</button>
    <button id="quickButton" class="noFocus">高速再生</button>
    <button id="slowButton" class="noFocus">低速再生</button>
    <button id="normalButton" class="noFocus">通常再生</button>
    <br>
    再生レート:<span id="rate"></span>
  </p>

  <h6>ショートカット:再生/停止="Shift+Return"、ちょい戻し="Shift+Space"、ちょい送り="Shift+Option+Space"<br>
  </h6>
  <textarea id="genko"></textarea>
  <br>

  <script type="text/javascript" src="main.js" charset="UTF-8"></script>

</body>

</html>

 

main.js

window.onload = function () {

    //----------------------------ページロード後に変数を取得---------------------
    // ビデオ要素
    const video = document.getElementById("media");
    // 再生/中断ボタン
    const playOrPauseButton = document.getElementById("playOrPauseButton");
    // 先頭にボタン
    const stopButton = document.getElementById("stopButton");
    // 少し戻るボタン
    const backButton = document.getElementById("backButton");
    // 少し進むボタン
    const forwardButton = document.getElementById("forwardButton");
    // 高速再生ボタン
    const quickButton = document.getElementById("quickButton");
    // 低速再生ボタン
    const slowButton = document.getElementById("slowButton");
    // 通常再生ボタン
    const normalButton = document.getElementById("normalButton");

    // 原稿エリア
    const genko = document.getElementById("genko");
    //スキップ時間(秒)の設定
    const SKIP_TIME = 4;


    //-------------------------各種ボタンが押された時の処理-----------------------------------
    // 再生が開始されたら、ボタンのラベルを変更
    video.addEventListener("play", function () {
        if (video.src) {
            playOrPauseButton.textContent = "停止";
            document.getElementById("rate").innerHTML = video.playbackRate.toFixed(2);
        }
        // this.blur();
    }, false);

    // 一時中断されたら、ボタンのラベルを変更
    video.addEventListener("pause", function () {
        playOrPauseButton.textContent = "再生";
    }, false);

    // 再生/中断ボタンを押された
    playOrPauseButton.addEventListener("click", function () {
        if (video.paused) {
            video.play();
        } else {
            video.pause();
        }
    }, false);

    // 少し戻るボタンが押されたら、動画の再生位置を変更
    backButton.addEventListener("click", function () {
        video.currentTime = video.currentTime - SKIP_TIME;
    }, false);

    // 少し進むボタンが押されたら、動画の再生位置を変更
    forwardButton.addEventListener("click", function () {
        video.currentTime = video.currentTime + SKIP_TIME;
    }, false);


    // 終了ボタンをクリックされたら、ビデオを一時停止し、再生位置を初期に戻す
    stopButton.addEventListener("click", function () {
        video.pause();
        video.currentTime = video.initialTime || 0;
    }, false);

    // 高速再生ボタンをクリックされたら、再生速度を上げる
    quickButton.addEventListener("click", function () {
        video.playbackRate = video.playbackRate + 0.1;
        document.getElementById("rate").innerHTML = video.playbackRate.toFixed(2);
    }, false);

    // 低速再生ボタンをクリックされたら、再生速度を下げる
    slowButton.addEventListener("click", function () {
        video.playbackRate = video.playbackRate - 0.1;
        document.getElementById("rate").innerHTML = video.playbackRate.toFixed(2);
    }, false);

    // 通常再生ボタンをクリックされたら、再生速度をリセット
    normalButton.addEventListener("click", function () {
        video.playbackRate = 1;
        document.getElementById("rate").innerHTML = video.playbackRate.toFixed(2);
    }, false);


    // 原稿をローカルストレージに保存する.
    document.getElementById("store").addEventListener("click", function () {
        window.localStorage.setItem("bkup", genko.value);
    }, true);

    // ローカルストレージに保存しておいた原稿を戻す
    document.getElementById("restore").addEventListener("click", saveData, true);

    // マニュアル表示
    document.getElementById("manual").addEventListener("click", showManual, true);

    //テキストをクリップボードにコピー
    document.getElementById("copy").addEventListener("click", function () {
        const textarea = document.getElementById("genko");
        textarea.select();
        document.execCommand("copy");
    }, false);


    //ダウンロード処理
    document.getElementById("download").addEventListener("click", function () {
        const fileName = "transcription_" + new Date(Date.now() + 9 * 3600000).toISOString().replace(/[^\d]/g, "").slice(0, 14) + ".txt";
        const text = document.getElementById("genko").value;
        const blob = new Blob([text], { type: 'text/plain' });
        const aTag = document.createElement('a');
        aTag.href = URL.createObjectURL(blob);
        aTag.target = '_blank';
        aTag.download = fileName;
        aTag.click();
        URL.revokeObjectURL(aTag.href);
    }, false);



    //---------------------------------------ショートカットキーの処理-----------------------

    window.document.onkeydown = function (evt) {

        const keyCode = evt.code;

        if (evt.shiftKey && !evt.altKey && !evt.ctrlKey && keyCode == "Space") { //Shift+Spaceで少し戻る
            backButton.click();
            evt.preventDefault();//テキストに改行やスペースが入らないようにイベントを無効化する
        }

        if (evt.shiftKey && !evt.altKey && !evt.ctrlKey && keyCode == "Enter") { //Shift+Returnで再生・停止
            playOrPauseButton.click();
            evt.preventDefault();//テキストに改行やスペースが入らないようにイベントを無効化する
        }
        if (evt.shiftKey && evt.altKey && !evt.ctrlKey && keyCode == "Space") { //Shift+option(alt)+Spaceで少し進める
            forwardButton.click();
            evt.preventDefault();//テキストに改行やスペースが入らないようにイベントを無効化する
        }


    }


    //-------------------- no focus 処理 --------------------

    //作業中にnoFocusクラスが設定されている要素をクリックした時に原稿エリアにカーソルを戻す
    //要素が選択されている時にEnterやSpaceを押して予期しない動作が起こることも防ぐ

    const noFocus = document.querySelectorAll(".noFocus");
    noFocus.forEach((e) => {
        e.addEventListener("focus", function () {
            genko.focus();
        }, true);
    });



    //---------------------日本語括弧の自動補完機能

    const input_element = document.getElementById('genko');
    // 入力終了時に内容をチェックして置き換える
    input_element.addEventListener('compositionend', function (e) {
        const data = e.data;
        switch (data) {
            case "「":
                complete_quote(input_element, '」');
                break;
            case "\”":
                replace_quote(input_element, '“”');
                break;
            case "{":
                complete_quote(input_element, '}');
                break;
            case "『":
                complete_quote(input_element, '』');
                break;
            case "’":
                replace_quote(input_element, '‘’');
                break;
            case "【":
                replace_quote(input_element, '】');
                break;
            case "[":
                replace_quote(input_element, ']');
                break;
            case "(":
                replace_quote(input_element, ')');
                break;
            default:
                break;
        }
    });

    //後ろに補完する関数
    function complete_quote(element, charactor) {
        const content = element.value;
        const len = content.length;
        const pos = element.selectionStart;
        element.value = content.substr(0, pos) + charactor + content.substr(pos, len);
        element.setSelectionRange(pos, pos);
    }
    //そっくり入れ替える関数
    function replace_quote(element, charactor) {
        const content = element.value;
        const len = content.length;
        const pos = element.selectionStart;
        element.value = content.substr(0, pos - 1) + charactor + content.substr(pos, len);
        element.setSelectionRange(pos, pos);
    }



    genko.focus();

}  // onload処理の終わり


//新しいビデオのパスを取得してセット
function setFilePath() {
    var fileInput = document.getElementById("fileInput");
    file = fileInput.files[0];
    var video = document.getElementById("media");
    video.src = URL.createObjectURL(file);
}



//---------------------------ウィンドウを閉じる時の警告--------------------------------

window.onbeforeunload = function (event) { event = event || window.event; return event.returnValue = '保存しないとやばくね?'; }

function saveData() {
    // 「OK」時の処理開始 + 確認ダイアログの表示
    if (window.confirm('本当にいいんですね?')) {
        genko.value = window.localStorage.getItem("bkup") || ""; //読み込み
    }
    // 「OK」時の処理終了// 「キャンセル」時の処理開始
    else {
        window.alert('キャンセルされました'); // 警告ダイアログを表示
    }
    // 「キャンセル」時の処理終了
}

//マニュアルの表示
function showManual() {
    let man_txt = document.getElementById("man_txt");
    man_txt.style.display = (man_txt.style.display == "none") ? "block" : "none";
}


 

main.css

#genko {
    font-size: 200%;
    width: 800px;
    height: 400px;
    background-color:#6666;
}

#debug{
    background-color:#AAAA;
}

#man_txt {
    width: 800px;
}

p {
    margin-left: 20px;
}

body {
    background-color:#5555;
}

 

-html, Javascript, Mac, プログラミング