kenken999 commited on
Commit
ee75ea1
1 Parent(s): 7757891

vtuber update

Browse files
staticfiles/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 makunugi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
staticfiles/README.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # これは何か
2
+ チャット欄に寄せられるコメントに自動で応答をしながら進行をする、AIによるYouTube LIVE配信を行うためのサンプルです。
3
+
4
+ # 準備
5
+
6
+ AIによる雑談配信を行うためのソースコードはすでに実装済みです。
7
+ 下記の準備が必要な項目のみ設定が必要です。
8
+
9
+ ## 1. 画像の用意
10
+ 下記の3つの名前の画像を用意し、ルートディレクトリ内に設置してください。
11
+
12
+ - chara.png
13
+ - chara_blinking.png
14
+ - background.png
15
+
16
+ ※`chara_blinking.png`は瞬きをしているキャラクターの静止画です。
17
+ ※`background.png`は背景の画像です。
18
+ 画像の名前を変更したい場合は、適宜index.html、aivtuber.js内を変更してください。
19
+
20
+ ## 2. meboのAPIキー・エージェントIDの設定
21
+ [mebo](https://mebo.work)を利用して、会話が可能なAIキャラクターを作成してください。
22
+ mebo内の[Chara.AI Generator](https://zenn.dev/makunugi/articles/ebecbb5de562d6)という機能を利用すると、スムーズにAIキャラクターが作成できます。
23
+
24
+ AIキャラクターを作成したら、meboの公開設定画面でAIキャラクターを限定公開し、「APIを有効化」してください。
25
+
26
+ APIを有効化するとAPIキーとエージェントIDを取得できます。
27
+
28
+ APIキーを取得したら、`aivtuber.js`を開き、下記の箇所にAPIキーとエージェントIDを入力しましょう。
29
+
30
+ ```js
31
+ const MEBO_API_KEY = "<meboのAPIキーを入力してください。>";
32
+ const MEBO_AGENT_ID = "<meboのAgent IDを入力してください。>";
33
+ ```
34
+
35
+ ## 3. VOICEVOXをインストール
36
+ 声の読み上げはVOICEVOXを利用します。
37
+ 下記からVOICEVOXをインストールし、起動してください。VOICEVOXが起動されることで、ローカル環境にAPIが立ち上がります。
38
+ [VOICEVOX公式サイト](https://voicevox.hiroshiba.jp/)
39
+
40
+
41
+ ```js
42
+ const VOICE_VOX_API_URL = "http://localhost:50021";
43
+ ```
44
+ デフォルトで上記が`aivtuber.js`に設定されています。ポート番号を変更する際は、上記のURLを適宜変更してください。
45
+
46
+ 尚、VOICEVOXを利用してYouTube配信をする場合は、ライセンス表記が必要です。概要欄などできちんと明記をして利用しましょう。
47
+ [VOICEVOX利用規約](https://voicevox.hiroshiba.jp/term/)
48
+
49
+ ## 4. YouTubeライブ配信のVIDEO IDを設定
50
+ YouTubeのライブ配信の準備が整ったら、ライブ配信の動画のURLに末尾にあるVideo IDを`aivtuber.js`の下記の箇所に入力してください。
51
+
52
+ ```js
53
+ const YOUTUBE_VIDEO_ID = '<YouTube Video IDを入力してください。>';
54
+ ```
55
+
56
+ Video IDは動画のURLの末尾にある「v=」より後の文字列です。
57
+ `https://www.youtube.com/watch?v=x12345667`
58
+ 上記であれば、Video IDは「x12345667」になります。
59
+
60
+
61
+ ## 5. YouTube Data APIのAPIキーの用意
62
+ YouTubeライブ配信のコメントを取得するため、YouTube Data APIのAPIキーを利用します。
63
+ APIキーの取得方法は、[こちら](https://qiita.com/shinkai_/items/10a400c25de270cb02e4#:~:text=%E8%AA%8D%E8%A8%BC%E6%83%85%E5%A0%B1%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%88API%E3%82%AD%E3%83%BC%E3%81%AE%E4%BD%9C%E6%88%90%EF%BC%89,-%E3%80%8C%E8%AA%8D%E8%A8%BC%E6%83%85%E5%A0%B1%E3%80%8D%E3%82%92&text=%E3%80%8C%E8%AA%8D%E8%A8%BC%E6%83%85%E5%A0%B1%E3%82%92%E4%BD%9C%E6%88%90%E3%80%8D%E3%82%92,%E4%BF%9D%E5%AD%98%E3%80%8D%E3%82%92%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%81%97%E3%81%BE%E3%81%99%E3%80%82)の記事が大変わかりやすくまとめられていました。
64
+
65
+ APIキーを取得したら、`aivtuber.js`の下記の箇所に入力しましょう。
66
+
67
+ ```js
68
+ const YOUTUBE_DATA_API_KEY = '<YouTube Data APIのAPIキーを入力してください。>';
69
+ ```
70
+
71
+ # 動作確認
72
+ `index.html`をブラウザで開きましょう。
73
+ ページ下部のテキスト入力欄にコメントを入力し「送信」ボタンを押して、無事応答が返ってくれば成功です。
74
+
75
+ # LINE開始
76
+ 動作確認が完了したら、「LIVE開始」を押しましょう。
77
+ YouTube LIVEのコメントに対して応答を返すようになります。
78
+ [OBS](https://obsproject.com/ja/download)などの画面配信が可能なツールを利用して、AI VTuberを表示しているブラウザのキャプチャをYouTubeに配信しましょう。
79
+
staticfiles/aivtuber.css ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html,
2
+ body {
3
+ background-image: url('background.png');
4
+ background-size: cover;
5
+ margin: 0;
6
+ overflow: hidden;
7
+ color: #FFFFFF;
8
+ }
9
+
10
+ div.aiResponseBox {
11
+ position: absolute;
12
+ top: 10%;
13
+ width: 70%;
14
+ right: 20%;
15
+ }
16
+
17
+ div.bottomBox {
18
+ position: absolute;
19
+ bottom: 0%;
20
+ width: 80%;
21
+ margin-left: 2%;
22
+ margin-right: 10%;
23
+ }
24
+
25
+ div {
26
+ text-align: center;
27
+ }
28
+
29
+ .ai-response {
30
+ font-size: 2.5rem;
31
+ font-weight: 600;
32
+ line-height: 1.4em;
33
+ padding: 5px;
34
+ background-color: rgba(0, 0, 0, 0.5);
35
+ }
36
+
37
+ #userComment {
38
+ text-align: center;
39
+ font-size: 24px;
40
+ margin: 2rem;
41
+ }
42
+
43
+ #typewriter::after {
44
+ content: "|";
45
+ animation-name: blink;
46
+ animation-duration: 1s;
47
+ animation-iteration-count: infinite;
48
+ }
49
+
50
+ #vtuber {
51
+ position: absolute;
52
+ bottom: -3%;
53
+ right: 15%;
54
+ width: 100%;
55
+ text-align: right;
56
+ }
57
+
58
+ @keyframes blink {
59
+ from {
60
+ opacity: 0;
61
+ }
62
+
63
+ to {
64
+ opacity: 1;
65
+ }
66
+ }
staticfiles/aivtuber.js ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //TODO: meboの定数
2
+ const MEBO_API_KEY = "<meboのAPIキーを入力してください。>";
3
+ const MEBO_AGENT_ID = "<meboのAgent IDを入力してください。>";
4
+
5
+ // TODO: VOICEVOXのURL (デフォルトの設定の場合は変える必要なし)
6
+ const VOICE_VOX_API_URL = "http://localhost:50021";
7
+
8
+ // TODO: ライブ配信するYouTubeのVideoID
9
+ const YOUTUBE_VIDEO_ID = '<YouTube Video IDを入力してください。>';
10
+ // TODO: YouTube Data APIを利用可能なAPIKEY
11
+ const YOUTUBE_DATA_API_KEY = '<YouTube Data APIのAPIキーを入力してください。>';
12
+
13
+ // コメントの取得インターバル (ms)
14
+ const INTERVAL_MILL_SECONDS_RETRIEVING_COMMENTS = 10000;
15
+ // QUEUEに積まれたコメントを捌くインターバル (ms)
16
+ const INTERVAL_MILL_SECONDS_HANDLING_COMMENTS = 3000;
17
+
18
+ // VOICEVOXのSpeakerID
19
+ const VOICEVOX_SPEAKER_ID = '10';
20
+
21
+ var audio = new Audio();
22
+ // 処理するコメントのキュー
23
+ var liveCommentQueues = [];
24
+ // 回答済みのコメントの配列
25
+ var responsedLiveComments = [];
26
+ // VTuberが応答を考え中であるかどうか
27
+ var isThinking = false;
28
+ // ライブごとに設定する識別子
29
+ var LIVE_OWNER_ID = createUuid();
30
+ // NGワードの配列
31
+ var ngwords = []
32
+ // YouTube LIVEのコメント取得のページング
33
+ var nextPageToken = "";
34
+ // コメントの取得が開始されているかどうかのフラグ
35
+ var isLiveCommentsRetrieveStarted = true;
36
+
37
+ const getLiveChatId = async () => {
38
+ const response = await fetch('https://youtube.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=' + YOUTUBE_VIDEO_ID + '&key=' + YOUTUBE_DATA_API_KEY, {
39
+ method: 'get',
40
+ headers: {
41
+ 'Content-Type': 'application/json'
42
+ }
43
+ })
44
+ const json = await response.json();
45
+ if (json.items.length == 0) {
46
+ return "";
47
+ }
48
+ return json.items[0].liveStreamingDetails.activeLiveChatId
49
+ }
50
+
51
+ const getLiveComments = async (activeLiveChatId) => {
52
+ const response = await fetch('https://youtube.googleapis.com/youtube/v3/liveChat/messages?liveChatId=' + activeLiveChatId + '&part=authorDetails%2Csnippet&key=' + YOUTUBE_DATA_API_KEY, {
53
+ method: 'get',
54
+ headers: {
55
+ 'Content-Type': 'application/json'
56
+ }
57
+ })
58
+ const json = await response.json();
59
+ const items = json.items;
60
+ return json.items[0].liveStreamingDetails.activeLiveChatId
61
+ }
62
+
63
+ const startTyping = (param) => {
64
+ let el = document.querySelector(param.el);
65
+ el.textContent = "";
66
+ let speed = param.speed;
67
+ let string = param.string.split("");
68
+ string.forEach((char, index) => {
69
+ setTimeout(() => {
70
+ el.textContent += char;
71
+ }, speed * index);
72
+ });
73
+ };
74
+
75
+ async function getMeboResponse(utterance, username, uid, apikey, agentId) {
76
+ var requestBody = {
77
+ 'api_key': apikey,
78
+ 'agent_id': agentId,
79
+ 'utterance': utterance,
80
+ 'username': username,
81
+ 'uid': uid,
82
+ }
83
+ const response = await fetch('https://api-mebo.dev/api', {
84
+ method: 'post',
85
+ headers: {
86
+ 'Content-Type': 'application/json'
87
+ },
88
+ body: JSON.stringify(requestBody)
89
+ })
90
+ const content = await response.json();
91
+ return content.bestResponse.utterance;
92
+ }
93
+
94
+ const playVoice = async (inputText) => {
95
+ audio.pause();
96
+ audio.currentTime = 0;
97
+ const ttsQuery = await fetch(VOICE_VOX_API_URL + '/audio_query?speaker=' + VOICEVOX_SPEAKER_ID + '&text=' + encodeURI(inputText), {
98
+ method: 'post',
99
+ headers: {
100
+ 'Content-Type': 'application/json'
101
+ }
102
+ })
103
+ if (!ttsQuery) return;
104
+ const queryJson = await ttsQuery.json();
105
+ const response = await fetch(VOICE_VOX_API_URL + '/synthesis?speaker=' + VOICEVOX_SPEAKER_ID + '&speedScale=2', {
106
+ method: 'post',
107
+ headers: {
108
+ 'Content-Type': 'application/json'
109
+ },
110
+ body: JSON.stringify(queryJson)
111
+ })
112
+ if (!response) return;
113
+ const blob = await response.blob();
114
+ const audioSourceURL = window.URL || window.webkitURL
115
+ audio = new Audio(audioSourceURL.createObjectURL(blob));
116
+ audio.onended = function () {
117
+ setTimeout(handleNewLiveCommentIfNeeded, 1000);
118
+ }
119
+ audio.play();
120
+ }
121
+
122
+ const visibleAIResponse = () => {
123
+ let target = document.getElementById('aiResponse');
124
+ target.style.display = ""
125
+ }
126
+
127
+ const invisibleAIResponse = () => {
128
+ let target = document.getElementById('aiResponse');
129
+ target.style.display = "none"
130
+ }
131
+
132
+ const handleLiveComment = async (comment, username) => {
133
+ isThinking = true;
134
+ visibleAIResponse();
135
+ startTyping({
136
+ el: "#aiResponseUtterance",
137
+ string: "Thinking................",
138
+ speed: 50
139
+ });
140
+ let userCommentElement = document.querySelector("#userComment");
141
+ userCommentElement.textContent = username + ": " + comment;
142
+ const response = await getMeboResponse(comment, username, LIVE_OWNER_ID, MEBO_API_KEY, MEBO_AGENT_ID);
143
+ isThinking = false;
144
+ if (username == "") {
145
+ await playVoice(response, true, response, false);
146
+ } else {
147
+ await playVoice(username + "さん、" + response, true, response, false);
148
+ }
149
+ startTyping({
150
+ el: "#aiResponseUtterance",
151
+ string: response,
152
+ speed: 50
153
+ });
154
+ }
155
+
156
+ const retrieveYouTubeLiveComments = (activeLiveChatId) => {
157
+ var url = "https://youtube.googleapis.com/youtube/v3/liveChat/messages?liveChatId=" + activeLiveChatId + '&part=authorDetails%2Csnippet&key=' + YOUTUBE_DATA_API_KEY
158
+ if (nextPageToken !== "") {
159
+ url = url + "&pageToken=" + nextPageToken
160
+ }
161
+ fetch(url, {
162
+ method: 'get',
163
+ headers: {
164
+ 'Content-Type': 'application/json'
165
+ }
166
+ }).then(
167
+ (response) => {
168
+ return response.json();
169
+ }
170
+ ).then(
171
+ (json) => {
172
+ const items = json.items;
173
+ let index = 0
174
+ nextPageToken = json.nextPageToken;
175
+ items?.forEach(
176
+ (item) => {
177
+ try {
178
+ const username = item.authorDetails.displayName;
179
+ let message = ""
180
+ if (item.snippet.textMessageDetails != undefined) {
181
+ // 一般コメント
182
+ message = item.snippet.textMessageDetails.messageText;
183
+ }
184
+ if (item.snippet.superChatDetails != undefined) {
185
+ // スパチャコメント
186
+ message = item.snippet.superChatDetails.userComment;
187
+ }
188
+ // :::で区切っているが、適宜オブジェクトで格納するように変更する。
189
+ const additionalComment = username + ":::" + message;
190
+ if (!liveCommentQueues.includes(additionalComment) && message != "") {
191
+ let isNg = false
192
+ ngwords.forEach(
193
+ (ngWord) => {
194
+ if (additionalComment.includes(ngWord)) {
195
+ isNg = true
196
+ }
197
+ }
198
+ )
199
+ if (!isNg) {
200
+ if (isLiveCommentsRetrieveStarted) {
201
+ liveCommentQueues.push(additionalComment)
202
+ } else {
203
+ responsedLiveComments.push(additionalComment);
204
+ }
205
+ }
206
+ }
207
+ } catch {
208
+ // Do Nothing
209
+ }
210
+ index = index + 1
211
+ }
212
+ )
213
+ }
214
+ ).finally(
215
+ () => {
216
+ setTimeout(retrieveYouTubeLiveComments, INTERVAL_MILL_SECONDS_RETRIEVING_COMMENTS, activeLiveChatId);
217
+ }
218
+ )
219
+ }
220
+
221
+ const getNextComment = () => {
222
+ let nextComment = ""
223
+ let nextRaw = ""
224
+ for (let index in liveCommentQueues) {
225
+ if (!responsedLiveComments.includes(liveCommentQueues[index])) {
226
+ const arr = liveCommentQueues[index].split(":::")
227
+ if (arr.length > 1) {
228
+ nextComment = arr[0] + "さんから、「" + arr[1] + "」というコメントが届いているよ。"
229
+ nextRaw = arr[1]
230
+ break;
231
+ }
232
+ }
233
+ }
234
+ return [nextComment, nextRaw];
235
+ }
236
+
237
+ const handleNewLiveCommentIfNeeded = async () => {
238
+ if (liveCommentQueues.length == 0) {
239
+ // QUEUEがなければ何もしない
240
+ setTimeout(handleNewLiveCommentIfNeeded, INTERVAL_MILL_SECONDS_HANDLING_COMMENTS);
241
+ return;
242
+ }
243
+
244
+ if (isThinking) {
245
+ // VTuberが応答を考えているときは新規コメントを捌かない
246
+ setTimeout(handleNewLiveCommentIfNeeded, INTERVAL_MILL_SECONDS_HANDLING_COMMENTS);
247
+ return;
248
+ }
249
+
250
+ if (!audio.ended) {
251
+ // VTuberが声を発しているときは新規コメントを捌かない
252
+ setTimeout(handleNewLiveCommentIfNeeded, INTERVAL_MILL_SECONDS_HANDLING_COMMENTS);
253
+ return;
254
+ }
255
+
256
+ for (let index in liveCommentQueues) {
257
+ if (!responsedLiveComments.includes(liveCommentQueues[index])) {
258
+ const arr = liveCommentQueues[index].split(":::")
259
+ if (arr.length > 1) {
260
+ responsedLiveComments.push(liveCommentQueues[index]);
261
+ isThinking = true;
262
+ await handleLiveComment(arr[1], arr[0]);
263
+ break;
264
+ }
265
+ }
266
+ }
267
+ setTimeout(handleNewLiveCommentIfNeeded, 5000);
268
+ }
269
+
270
+ const onClickSend = () => {
271
+ let utterance = document.querySelector("#utterance");
272
+ handleLiveComment(utterance.value, '匿名');
273
+ utterance.value = "";
274
+ }
275
+
276
+ // LIVEを開始する
277
+ const startLive = () => {
278
+ // 明示的にボタンをク��ックする等しなければ、音声が再生できない。そのためLIVE開始ボタンを下記のIDで設置する。
279
+ let startLiveButton = document.querySelector("#startLiveButton");
280
+ startLiveButton.style.display = "none";
281
+ let submitForm = document.querySelector("#submit_form");
282
+ submitForm.style.display = "none";
283
+ getLiveChatId().then(
284
+ (id) => {
285
+ retrieveYouTubeLiveComments(id);
286
+ }
287
+ )
288
+ //LIVE開始時は空文字を送信することで、meboで設定した初回メッセージが返される。
289
+ handleLiveComment('', '');
290
+ blink();
291
+ }
292
+
293
+ const img = ["chara.png", "chara_blinking.png"];
294
+ var isBlinking = false;
295
+
296
+ function blink() {
297
+ if (isBlinking) {
298
+ isBlinking = false;
299
+ document.getElementById("charaImg").src = img[1];
300
+ setTimeout(blink, 100);
301
+ } else {
302
+ isBlinking = true;
303
+ document.getElementById("charaImg").src = img[0];
304
+ setTimeout(blink, 3500);
305
+ }
306
+ }
307
+
308
+ function createUuid() {
309
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (a) {
310
+ let r = (new Date().getTime() + Math.random() * 16) % 16 | 0, v = a == 'x' ? r : (r & 0x3 | 0x8);
311
+ return v.toString(16);
312
+ });
313
+ }
staticfiles/index.html CHANGED
@@ -1 +1,29 @@
1
- file is staticsfiles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width">
7
+ <title>Sample Chat AI VTuber</title>
8
+ <link rel="stylesheet" href="aivtuber.css">
9
+ <script src="./aivtuber.js"></script>
10
+ </head>
11
+
12
+ <body>
13
+ <div id="vtuber">
14
+ <img id="charaImg" src="chara.png" width="auto" height="400" />
15
+ </div>
16
+ <div id="aiResponse" class="aiResponseBox">
17
+ <p class="ai-response" id="aiResponseUtterance"></p>
18
+ </div>
19
+ <div class="bottomBox">
20
+ <p id="userComment"></p>
21
+ <button id="startLiveButton" onclick="startLive();">LIVE開始</button>
22
+ <div id="submit_form">
23
+ <input type="text" id="utterance" />
24
+ <button id="sendButton" onclick="onClickSend();">送信</button>
25
+ </div>
26
+ </div>
27
+ </body>
28
+
29
+ </html>
staticfiles/publish_agent.png ADDED

Git LFS Details

  • SHA256: c01cf0625dbf5fe2fb3675f5d3c3b31f387c891ddf9901d7d8b1db1a1b3a622e
  • Pointer size: 130 Bytes
  • Size of remote file: 53.3 kB