見出し画像

AItuber開発(困ってたら読んで下さい)


AItuber開発

AItuberとは何かとかも書こうと思ったんですが、
辞めました。
AItuber開発者向けの記事なので省かせ頂きます。

AItuber開発は色々な分野の技術を使うので何処かしらに触った事の無い技術が出てきますので、困ってる方の解決に繋がればと思い記事にしました。

ソースコード

先にソースコード見せてから解説します。

HTML

<!DOCTYPE html>

<html>

  <head>

    <meta charset="utf-8">

    <title>AITuber demo</title>

    <link href="https://fonts.googleapis.com/css?family=M+PLUS+1p" rel="stylesheet">

    <link rel="stylesheet" href="style.css">

    <script src="script.js"></script>

  </head>



  <body>

    <div id="user-comment-box">

    </div>

    <div id="response-box">

      <h1 id="aituber-response"></h1>

    </div>

    <div id="question-box">

      <h2 id="question-from-user">

      </h2>

    </div>

  </body>

</html>

CSS

#user-comment-box {
	position: absolute;
	top: 13%;
	left: 3%;
	width: 30%;
	height: 55%;
	padding: 1%;
	z-index: 1;
	/* box外は非表示 */
	overflow: hidden;
	display: flex;
	flex-direction: column-reverse;
}

#question-box {
	position: absolute;
	top: 20%;
	left: 40%;
	width: 40%;
	padding: 1%;
	z-index: 1;
	font-size: 18px;
}

#response-box {
	position: absolute;
	top: 73%;
	left: 5%;
	width: 85%;
	padding: 1%;
	z-index: 1;
}

#aituber-response {
	font-size: 20px;
	font-family: "M PLUS 1p";
}

/* user-container */
.user-container {
	display: flex;
	font-size: 14px;
	margin: 6px 2px;
}

.image-cropper {
  width: 20px;
  height: 20px;
  position: relative;
  overflow: hidden;
  border-radius: 50%;
	flex-shrink: 0;
}

.user-icon {
  display: inline;
  margin: 0 auto;
  height: 100%;
  width: auto;
}

.user-name, .user-comment {
	margin: 0 0 0 4px;
}

.user-name {
	flex-shrink: 0;
}


JavaScript

const YOUTUBE_DATA_API_KEY = '自分のAPIキー';
const OPENAI_API_KEY = '自分のAPIキー';
const OPENAI_URL = "https://api.openai.com/v1/chat/completions"
// 配信の都度修正
const YOUTUBE_VIDEO_ID = "自分のID"
const VOICE_VOX_API_URL = "http://localhost:50021";
const VOICEVOX_SPEAKER_ID = '1';

var audio = new Audio();
// コメントの取得インターバル (ms)
const INTERVAL_MILL_SECONDS_RETRIEVING_COMMENTS = 18000

// 処理するコメントのキュー
let liveCommentQueues = [];
// YouTube LIVEのコメント取得のページング
let nextPageToken = "";

// Youtube Data API系の処理
// 10000 quota/dayが上限. chat ID=1q, chat=5q

// VIDEO IDからchat IDを取得
const getLiveChatId = async (YOUTUBE_VIDEO_ID) => {
  const params = {
    part: 'liveStreamingDetails',
    id: YOUTUBE_VIDEO_ID,
    key: YOUTUBE_DATA_API_KEY,
  }
  const query = new URLSearchParams(params)
  const response = await fetch(`https://youtube.googleapis.com/youtube/v3/videos?${query}`, {
    method: 'get',
    headers: {
      'Content-Type': 'application/json'
    },
  })
  const json = await response.json();
  if (json.items.length == 0) {
    return "";
  }
  const liveChatId = json.items[0].liveStreamingDetails.activeLiveChatId
  // return chat ID
  console.log(liveChatId)
  return liveChatId
}

// Chat GPT系の処理
const getAITuberResponse = async (userComment) => {
  const openAiHeaders = {
    'Authorization':`Bearer ${OPENAI_API_KEY}`,
    'Content-type': 'application/json',
    'X-Slack-No-Retry': 1
  }

  const openAiParams = { 
    headers: openAiHeaders, 
    method: 'POST',
    body: JSON.stringify({
      'model': 'gpt-3.5-turbo',
      'max_tokens': 200,
      'messages': [
{'role': 'system', 'content': `自分のキャラのプロンプト`},
  {"role": "assistant", "content": "自分のキャラのコメント"},
  ]})
  }

  const response = await fetch(OPENAI_URL, openAiParams)
  console.log(response)
  const json = await response.json();
  console.log(json)
  const AITuberResponse = json.choices[0].message.content;

   // 以下の行を追加してレスポンスを音声として再生
   playVoice(AITuberResponse);


  // 表示を書き換える
  const target = document.getElementById("aituber-response")
  target.innerHTML = AITuberResponse

  return AITuberResponse
}

const playVoice = async (inputText) => {
  audio.pause();
  audio.currentTime = 0;
  const ttsQuery = await fetch(VOICE_VOX_API_URL + '/audio_query?speaker=' + VOICEVOX_SPEAKER_ID + '&text=' + encodeURI(inputText), {
      method: 'post',
      headers: {
          'Content-Type': 'application/json'
      }
  })
  if (!ttsQuery) return;
  const queryJson = await ttsQuery.json();
  const response = await fetch(VOICE_VOX_API_URL + '/synthesis?speaker=' + VOICEVOX_SPEAKER_ID + '&speedScale=2', {
      method: 'post',
      headers: {
          'Content-Type': 'application/json'
      },
      body: JSON.stringify(queryJson)
  })
  if (!response) return;
  const blob = await response.blob();
  const audioSourceURL = window.URL || window.webkitURL
  audio = new Audio(audioSourceURL.createObjectURL(blob));
  audio.onended = function () {
      setTimeout(handleNewLiveCommentIfNeeded, 1000);
  }
  audio.play();
}

const retrieveLiveComments = async (activeLiveChatId) => {
  let url = "https://youtube.googleapis.com/youtube/v3/liveChat/messages?liveChatId=" + activeLiveChatId + '&part=authorDetails%2Csnippet&key=' + YOUTUBE_DATA_API_KEY
  if (nextPageToken !== "") {
    url = url + "&pageToken=" + nextPageToken
  }
  const response = await fetch(url, {
    method: 'get',
    headers: {
      'Content-Type': 'application/json'
    }
  })
  const json = await response.json()
  const items = json.items;
  console.log("items:", items)
  let index = 0
  let currentComments = []
  nextPageToken = json.nextPageToken;
  items?.forEach(
    (item) => {
      try {
        const userName = item.authorDetails.displayName
        const userIconUrl = item.authorDetails.profileImageUrl
        let userComment = ""
        if (item.snippet.textMessageDetails != undefined) {
          // 一般コメント
          userComment = item.snippet.textMessageDetails.messageText;
        }
        if (item.snippet.superChatDetails != undefined) {
          // スパチャコメント
          userComment = item.snippet.superChatDetails.userComment;
        }
        const additionalComment = { userName, userIconUrl, userComment }
        if (!liveCommentQueues.includes(additionalComment) && userComment != "") {
          // キューイング
          liveCommentQueues.push(additionalComment)

          // #つきコメントの除外
          additionalComment.userComment.includes("#") || currentComments.push(additionalComment)

          // ユーザーコメントの表示
          let target = document.getElementById("user-comment-box")
          // 要素を作成します
          const userContainer = document.createElement('div');
          userContainer.classList.add('user-container');
      
          const imageCropper = document.createElement('div');
          imageCropper.classList.add('image-cropper');
      
          const userIcon = document.createElement('img');
          userIcon.classList.add('user-icon');
          userIcon.setAttribute('src', additionalComment.userIconUrl);
      
          const userName = document.createElement('p');
          userName.classList.add('user-name');
          userName.textContent = additionalComment.userName + ':';
      
          const userComment = document.createElement('p');
          userComment.classList.add('user-comment');
          userComment.textContent = additionalComment.userComment;
      
          // 要素を追加します
          imageCropper.appendChild(userIcon);
          userContainer.appendChild(imageCropper);
          userContainer.appendChild(userName);
          userContainer.appendChild(userComment);
          target.prepend(userContainer)
        }
      } catch {
        // Do Nothing
      }
      index = index + 1
    })

    // 読まれてないコメントからランダムに選択
    if (currentComments.length != 0) {
      let { userName, userIconUrl, userComment } = currentComments[Math.floor(Math.random() * currentComments.length)]
      const aituberResponse = await getAITuberResponse(userComment)
      playVoice(aituberResponse)  // Change from speakAITuber to playVoice
  

      let target = document.getElementById("question-box")
      target.innerHTML = `${userName} : ${userComment}`
    }

    console.log("liveCommentQueues", liveCommentQueues)

    // 繰り返し処理
    setTimeout(retrieveLiveComments, INTERVAL_MILL_SECONDS_RETRIEVING_COMMENTS, activeLiveChatId);
}

const startLive = async () => {
  const liveChatId = await getLiveChatId(YOUTUBE_VIDEO_ID)
  console.log(liveChatId)
  retrieveLiveComments(liveChatId)
}

startLive()

ソースコードをコピーしてAPIキー記入して頂ければ動きます。

配信画面が映らない

配信画面が映らない方で良くあるのは

OBSの配信画面URLが間違っている。

YouTube IDが間違っている。

の2つが原因だと思うので確認してみて下さい。

YouTubeコメントが表示されない

YouTubeコメントが表示されない原因は

YouTubeの配信アカウントと同一アカウントでコメントしているのが原因です。

別のアカウントからコメントしてみて下さい。

音声が聞こえない

音声が聞こえないのは

voiceboxのエンジンを起動させてない。

voicevoxのCORSが原因なので確認してみて下さい。

AItuberのコミュニティーです。


うちの子宣伝

分からない所や詳しく書いた方がいい所あったらコメント下さい。

この記事が気に入ったらサポートをしてみませんか?