今回以下のようなものを作成しました。
動画の自動文字起こしです。
文字起こしの素材は、「youtube summary with chatgpt & claude」のChromeのプラグインを活用しました。
デモはこちらから
完全なソースコードはこちら
使い方
チャプター機能: 目次の項目をクリックすると、動画の該当箇所にジャンプします
文字起こし表示: 「📝 文字起こしを表示」をクリックすると、トランスクリプトが表示されます
自動スクロール: 動画の再生に合わせて、現在話されている部分が自動的にハイライトされ、スクロールします
同期機能: チャプターをクリックすると、文字起こしも該当箇所に自動的にスクロールします
インタラクティブ: 文字起こしの各セグメントをクリックすると、その箇所から再生できます
最後に:AIによるプロンプトで出力させた流れ。(長文注意)
ナオ:あなたは、優秀なプログラマーです。
今回作成するのは、Youtube Frame APIを用いて、以下の機能のコードを実装します。目的は、この技術を共有し、聴覚障害者のためのアプローチやユーザービリティに向上するために使用する開発となります。
実装済み
- Youtube Frame APIとチャプター機能でブログで目次をクリックすると、シーケンスが移動する「0:00 こんにちは」→開始地点に動画が移動
- Youtubeの動画IDをデータ属性に埋め込んで自動でYoutubeのFlamePlayerを生成させる
- チャプターは、現在箇条書きでまとめられている。(ネタバレ防止)
要件
- Youtubeの完全文字起こし(素材)をアコーディオンをが開いたときに、動画のシーケンスに応じて移動して、ボックス内で文字腰をスムーズスクロールを実装してください。DOM監視必要あり。(htmlタグのsummaryタグなどでopenになった時に自動で文字起こしをする。)
素材
#0 ナオのポッドキャストのルール - YouTube
https://www.youtube.com/watch?v=_yyBJeZgACA
Transcript:
(00:01) 大抵の初回のポッドって自己紹介から 始まることが多いですよねそれで思ったん ですけれども初回のポッドキャストは自己 紹介を必ずしも入れなくてはいけないのか についてをふと疑問に思いまして自己紹介 って概要欄にかけるから別にいいのかなっ てふと考えたんですよでラジオの上収率の 9りっていうのは実は開始10秒で聞くか 聞かないかを判断されるっていうことが あってすごいシビアな世界なんですねなの でこれはポッドキャストも音声配信なので 当然ながら適用されるルールだと思ってい ますそのため先ほどあげた自己紹介って いうのを初回でもちろん挨拶も含めて入れ るっていう風になると魅力的な自己紹介で あれば確かに今後も聞いてみようっていう 風にリスナーさんには届くかもしれないん ですけれどもそこまで魅力的ではないなっ て思ったら残念ながらその初回だけは一時
(01:07) 的に聞いてくださる方が多くなって次回 以降も聞こうかっていう風になると微妙に なっくるのではないのだろうかっていうふ と考えたんですねそこで今回タイトルにも あるように初回だからこそなおこの私の ポッドキャストについてルールを設ける ことにしましたやはり個人の ポッドキャストだからこそオリジナルの ルールを設けることで他のポッドキャスト と一戦を隠すというやり方ですこれが こうすかどうかは分からないんですけれど もちょっと実験的にやってみようかなと 思いまして今回設けたルールは3つあり まして1つ目は冒頭の挨拶すらせずに本題 に入るということですこれ実はこの エピソードからすでに導入されてるんです よ普通だったらこんにちはなおです今回の えー初回のエピソードになるから今回は私 の自己紹介については話したいと思います
(02:12) みたいな感じになると思うんですよねそれ かポドキャストの説明してえ私の ポッドキャストではこういうことをして こういうことをしますみたいなことを冒頭 に入れると思うんですけどこういった冒頭 の挨拶させずに本題に入ることをしてい ますで自己紹介は興味がある方は概要欄を 見るはずなんですよなのでえ本題に入って このポッドキャストの配信者はどういう人 なんだろうっていう風に思ったら必ず概要 欄を見るはずなのでこの冒頭の挨拶を不要 にしてね効率化を測ってみるというそう いうやり方をしてみようかなと思います やはり本題にスピーに入るっていうことは リスナーにとってもストレスがなくなるの ではないのかっていうちょっと実験的な ことをやってみたいなと思ったからです その次のルールは背景BGMは一切使用し ないこれは音声に集中しやすくするためと リスナーがその聞きたいBGMとは限らな いっていうのもあって別にBGMは実際
(03:18) 使用しなくてもいいのではないのだろう かっていう風に思ったんですねそれにこの ポッドキャストを聞いてる間になんか音楽 がないとつまらないなって思えば裏でね また音声というか音楽を流しても聞ける ようにすることも可能にできると思うので まそういうこともできると思う思う多分 できるはずなのでえ私はパソコンからやっ てるのでできるんですけどスマホだと多分 アプリを利用すれば多分できると思うん ですよねバックグラウンドでなのでこれも そういう配慮となっておりますでできるよ ねちょっと自信ないけどえでルール3は 概要欄に時間を入れた目次と説明を追加 するを実行していますこのポッドキャスト でもすでに目次が追加されてると思うん ですけどこのようにねポッドキャストを 聞くかどうかの判断材料になることや聞き たい部分だけを聞けるため長くなっても 時間短縮ができるための配慮として実行さ
(04:23) せていただきたいと思いますということで え完璧な配信っていうのは私は最初から できるとは思っていなくて今さっきもね ちょっとグダグダになってしまった部分も あるかもしれないしちょっと話し方もねえ 不自然に見えたかもしれないしっていうの も多分あると思うんですけれども初回の エピソードで完璧をモぜすってことは 私自身はしてないのでこれもあくまで スタイルとして少しずつアップデートして いっでより一層エピソードであのお聞きに なられる方リスナーの方々に楽しんで もらえるようにえ役に立っていただける ようなそんなポドキャストを目指したいと 思っていますのでえ今後ともよろしくお 願いいたしますということで今回はこちら のポッドキャストのルールについてご説明 させていただきましたえ以上なんです けれどもまた次回もね是非とも聞いて いただけることを切に心から願っており
(05:31) ます次回もなるべくいい配信にあ今いい 配信だったのかわからないですけどでも いい配信になれるように頑張りますので 是非とも期待して聞いていただければ幸い でございます以上です
実装コード
// ============================================
// グローバル変数
// ============================================
let player; // YouTubeプレーヤーオブジェクト
// ============================================
// YOUTUBEAPI
// ============================================
let tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
let firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
// ============================================
// API準備完了時の処理
// ============================================
function onYouTubeIframeAPIReady() {
// YouTubeの動画IDを取得
const videoId = document.getElementById('player').getAttribute('data-id');
if (!videoId) {
return;
}
// frontmatterElが存在するか確認
const ytVideo = document.getElementById('player');
if (!ytVideo) {
return;
}
console.log(`Initializing player for video ID: ${videoId}`); // ログ追加
try {
player = new YT.Player('player', {
height: '360',
width: '640',
videoId: videoId,
events: {
// 'onReady'イベントハンドラを追加
onReady: onPlayerReady,
// 必要であれば他のイベントも追加
// 'onStateChange': onPlayerStateChange
},
});
console.log('YT.Player object created.'); // ログ追加
} catch (error) {
console.error('Error creating YT.Player:', error); // エラーハンドリング
}
}
// ============================================
// プレーヤー準備完了時の処理
// ============================================
// YT.Playerの準備ができたら呼び出される関数
function onPlayerReady(event) {
console.log('Player is ready.'); // ログ追加: プレーヤー準備完了を確認
// プレーヤーが準備できてからクリックイベントを設定
setupChapterClickEvents();
}
// ============================================
// チャプターリスト関連
// ============================================
// 時間文字列 (HH:MM:SS or MM:SS) を秒に変換する関数
function timeToSeconds(timeString) {
const parts = timeString.split(':').map(Number);
let seconds = 0;
if (parts.length === 3) {
// HH:MM:SS
seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
} else if (parts.length === 2) {
// MM:SS
seconds = parts[0] * 60 + parts[1];
}
return seconds;
}
// チャプターリストのクリックイベントを設定する関数
function setupChapterClickEvents() {
console.log('Setting up chapter click events.'); // ログ追加
const chapterListElement = document.getElementById('chapter-list');
let text = chapterListElement.innerHTML;
if (!chapterListElement) {
console.warn('Element with id "chapter-list" not found.');
return;
}
const lines = text.split('\n').filter((line) => line.trim() !== ''); // テキストを行ごとに分割
const chapters = [];
// 正規表現でタイムスタンプとタイトルを抽出
const regex = /^(\d{1,2}:\d{2}(?::\d{2})?)\s+(.+)$/;
lines.forEach((line) => {
const match = line.trim().match(regex);
if (match) {
const time = match[1]; // 時間文字列 (例: '1:15')
const title = match[2]; // タイトル (例: 'トピックA')
const seconds = timeToSeconds(time); // 秒に変換 (例: 75)
chapters.push({ time, title, seconds });
}
});
if (chapters.length === 0) {
chapterListElement.innerHTML =
'<p>チャプター情報が見つかりませんでした。</p>';
return;
}
// 目次リストのHTMLを生成
const ul = document.createElement('ul');
chapters.forEach((chapter) => {
const li = document.createElement('li');
li.textContent = `${chapter.time} ${chapter.title}`;
li.setAttribute('data-seconds', chapter.seconds); // 秒数をdata属性として保持
// 既存のイベントリスナーを削除(複数回呼ばれた場合の重複防止)
// ※ より堅牢にするなら、関数自体を渡して削除する
// chapter.removeEventListener('click', handleChapterClick);
// 新しいイベントリスナーを追加
li.addEventListener('click', handleChapterClick);
ul.appendChild(li);
});
chapterListElement.innerHTML = ''; // 古い内容をクリア
chapterListElement.appendChild(ul); // 生成したリストを追加
}
// クリックイベントのハンドラ関数
function handleChapterClick(event) {
const chapter = event.currentTarget; // クリックされたli要素
const seconds = chapter.getAttribute('data-seconds');
if (seconds === null) {
console.warn(
'Clicked item is missing data-seconds attribute:',
chapter.textContent
);
return;
}
console.log(
`Chapter clicked: ${chapter.textContent.trim()}, seeking to ${seconds}s`
); // ログ追加
// playerオブジェクトとseekToメソッドの存在を確認
if (player && typeof player.seekTo === 'function') {
player.seekTo(seconds, true); // 指定秒数に移動し、再生を開始
} else {
console.error(
'Player is not ready or seekTo function is unavailable when clicking chapter.'
);
}
}
## 音声配信
こんにちは、ナオです。今回は、初回ですが、私自身がポッドキャストにルールを設けることによってリスナーさんにとって、聴きやすいポッドキャストを目指すという趣旨を説明した音声配信となっております。
<div id="player" class="youtube-ratio" data-id="_yyBJeZgACA"></div>
## 目次
<div id="chapter-list">
00:00 初回のポッドキャストの自己紹介って必要性ある?
00:26 ラジオの聴取率の9割は開始10秒で決まる
01:28 ポッドキャストのオリジナルルールの必要性
01:47 ルール1:冒頭のあいさつすらせずに、本題に入る
02:59 ルール2:背景BGMは一切使用しない
04:00 ルール3:概要欄に、時間を入れた目次と説明を追加する
04:26 配信は、日々のアップデートしながらクオリティーの向上を目指す
05:14 最後の挨拶
</div>
ここまで、JSで文字起こしを記載しようとしているコードを発見したため、指示を変える。
ナオ: ありがとうございます、汎用性を高くするために文字起こしはhtmlから取得して、活用できるようにするとありがたいです。たとえばWordpressやGhostなどのテーマ(カスタムテーマだと金額が高くなってしまう)の拡張性で毎回スクリプトに文字起こしを入れるのは不便なため、文字起こしは、HTMLの要素をタイムスタンプから取得して(素材の時間をデータ属性に入れるでもありです)JSで制御するような作りだとさらに制作側も、手間をかけずに実装ができるため、多くの人に活用できるコードが可能となると考えられます。実装お願いしてもよろしいでしょうか?
CSSの調整と、指示の仕方で、汎用性の高いコードが実現しました。
Wordpressなどのテーマにも活用できるため、ぜひ、動画を入れてサイト構築される方h活用してみるといいかもしれません!