Twitch配信開始の通知を自動でツイートする
準備
Twitter Developer 登録
登録について
英語でアプリの目的とかを書いて申請する必要あり。
自分の場合は、英語で申請した後に日本語のメールが来て、英語と同じような内容を日本語でちゃんと書いて返信したら登録できた。
単純な通知アプリを作りたい、というのの他に、個人で使う用です、とか、勉強がてらです、とか書いたけど必要だったかは分からない。
プロジェクトを登録出来たら Settings の App permissions で Write を含むように変更する。
控えておくもの
Twitchから情報取得
アクセストークン取得
Authentication | Twitch Developers
App access tokens を取得したいので、
Client credentials flow を使う。
curl -X POST -H "Content-Type: application/json" "https://id.twitch.tv/oauth2/token?client_id=クライアントID&client_secret=クライアントシークレット&grant_type=client_credentials"
返ってきた access_token を控える。
ユーザID取得
curl "https://api.twitch.tv/helix/users?login=ユーザー名" -H "Content-Type: application/json" -H "Authorization: Bearer Twitchアクセストークン" -H "client-id: クライアントID"
控えておくもの
- Twitchアクセストークン
- ユーザーID
Firebase Functions 登録
Functions を使うために料金プランをBlazeにする。
料金参考:Firebaseで無料枠の範囲内のはずが課金されたのならこれが理由かも? - Qiita
控えておくもの
- コールバックURL
Firebase Functions プロジェクト作成
npm インストール
npm のはじめかた - Qiita
npm でインストールするもの
- Firebase CLI
npm install -g firebase-tools
npm install twitter
フォルダ準備
適当な場所にデプロイ用のフォルダを作成。
控えておくもの
- Firebaseプロジェクトパス
ログイン
Firebase にログイン。
firebase login
プロジェクト作成
用意したフォルダに移動。
cd Firebaseプロジェクトパス
プロジェクト作成開始。
firebase init
するといろいろ質問される。
Firebase Functions 環境変数設定
キー構成は自分で分かりやすければ何でも大丈夫だと思う。
firebase functions:config:set credential.twitter.api_key="値"
設定するもの Twitch関連
- クライアントID
- クライアントシークレット
- ユーザーID
- Twitchアクセストークン
- HMAC用シークレット
署名の確認に使用する。
10~100の文字列をパスワードのように決めておく。
Twitch で取得したアクセスシークレットや Twitter で取得したクライアントシークレットとは異なるので注意。
設定するもの Twitter関連
Twitch API サブスクリプション登録
- 登録の curl コマンドを流す
- webhook_callback_verification_pending のレスポンスが返ってくる
- コールバックURLの方にリクエストが送られてくる
- 署名の確認をする
- challenge の値を返す
- 登録成功
という流れ。
なのでまず、コールバックURLにくるリクエスト処理(上の4, 5)を作成する。
Firebase Functions 受け取り側作成
署名の確認
Verify a signature | Twitch Developers
// 署名確認 const hmac_message = headers['twitch-eventsub-message-id'] + headers['twitch-eventsub-message-timestamp'] + request.rawBody; const signature = crypto .createHmac('sha256', twitchConfig.hmac_secret) .update(hmac_message) .digest('hex'); const expected_signature_header = 'sha256=' + signature; const message_signature = headers["twitch-eventsub-message-signature"]; if (message_signature != expected_signature_header) { functions.logger.error("Error", "Verify Signature Failed"); response.sendStatus(403); }
前に設定したHMAC用シークレットを元に HMAC-SHA256 でなんやかんやしたものと、送られてきた twitch-eventsub-message-signature を比較する。
合っていなかったら403が返るようにする。
リクエスト受け取り
if (messageType == "webhook_callback_verification") { // Webhhok登録認証 response.send(request.body.challenge); }
ヘッダーの twitch-eventsub-message-type で判断。
webhook_callback_verification なら challenge を返す。
デプロイ
firebase deploy
ESLint 修正
サブスクリプション登録
curl -X POST -d "{\"type\":\"サブスクリプションタイプ\", \"version\":\"1\", \"condition\":{\"broadcaster_user_id\":\"ユーザID\"}, \"transport\":{\"method\":\"webhook\", \"callback\":\"コールバックURL\", \"secret\":\"HMAC用シークレット\"}}" -sS -H "Content-Type: application/json" -H "Authorization: Bearer Twitchアクセストークン" -H "client-id: クライアントID" https://api.twitch.tv/helix/eventsub/subscriptions
サブスクリプションタイプ
EventSub Subscription Types | Twitch Developers
ここでは配信開始の通知を受け取りたいので stream.online を使用する。
配信終了も受け取りたかったので stream.offline も後で同じように登録する。
Firebase 側を作成出来たら、上の curl コマンドを流す。
webhook_callback_verification_pending のレスポンスが返ってくるのを確認して、リクエスト受け取りの方も上手くいったら登録成功。
サブスクリプション確認
curl "https://api.twitch.tv/helix/eventsub/subscriptions" -H "Content-Type: application/json" -H "Authorization: Bearer Twitchアクセストークン" -H "client-id: クライアントID"
サブスクリプション一覧が取得できる。
status が enabled になっていれば大丈夫。
検証に失敗して status が webhook_callback_verification_failed になっていたり、余計なサブスクリプションを削除したい場合は以下のコマンド。
curl -X DELETE https://api.twitch.tv/helix/eventsub/subscriptions?id=サブスクリプションID -H "Content-Type: application/json" -H "Authorization: Bearer Twitchアクセストークン" -H "client-id: クライアントID"
通知受け取り処理
ヘッダーの twitch-eventsub-message-type で判断。
通知は notification。
Twitter処理
// Twitter準備 const client = new Twitter({ consumer_key: twitterConfig.api_key, consumer_secret: twitterConfig.api_secret, access_token_key: twitterConfig.access_token_key, access_token_secret: twitterConfig.access_token_secret, }); var tweet = ""; if (subscriptionType == "stream.online") { // 配信開始 // 配信タイトル取得 const title = await getStreamTitle(); tweet = title + " https://www.twitch.tv/" + request.body.event.broadcaster_user_login; } else if (subscriptionType == "stream.offline") { // 配信終了 tweet = "配信おわり"; } if (tweet != "") { await client.post("statuses/update", { status: tweet }) }
twitch-eventsub-subscription-type にサブスクリプションタイプが入っているので、これで配信開始か終了かを判断する。
配信タイトル取得
以前のサブスクリプションでは配信タイトルが通知データ内に入っていたのだが、更新されてからは入っていないので都度取得する必要がある。
// 配信タイトル取得 async function getStreamTitle() { functions.logger.log("getStreamTitle Start"); var title = ""; try { const accessToken = await checkAccessToken(); if (accessToken == "") { title = "配信はじめ"; return title; } const headers = { "Content-Type": "application/json", "Authorization": "Bearer " + accessToken, "client-id": twitchConfig.client_id, } const url = "https://api.twitch.tv/helix/channels?broadcaster_id=" + twitchConfig.user_id; const options = { url: url, method: "GET", headers: headers, json: true, } const result = await requestAsync(options); if (result && result.data[0].title) { title = result.data[0].title; } else { title = "配信はじめ"; } } catch (e) { functions.logger.error("Error", e); } return title; }
アクセストークン確認、更新
上の配信タイトル取得にはアクセストークンが必要だが、アクセストークンには期限が設定されているので切れている可能性がある。
その為、まず疎通確認をする。
// Twitchアクセストークン疎通確認 async function checkAccessToken() { functions.logger.log("checkAccessToken Start"); var accessToken = ""; try { const headers = { "Content-Type": "application/json", "Authorization": "Bearer " + twitchConfig.access_token, } const url = "https://id.twitch.tv/oauth2/validate"; const options = { url: url, method: "GET", headers: headers, json: true, } const result = await requestAsync(options); if (result && result.status != 200) { accessToken = await getAccessToken(); } else { accessToken = twitchConfig.access_token; } } catch (e) { functions.logger.error("Error", e); } return accessToken; }
期限が切れていたらアクセストークンを取得しなおす。
// Twitchアクセストークン更新 async function getAccessToken() { functions.logger.log("getAccessToken Start"); var accessToken = ""; try { const headers = { "Content-Type": "application/json", } const url = "https://id.twitch.tv/oauth2/token" + "?client_id=" + twitchConfig.client_id + "&client_secret=" + twitchConfig.client_secret + "&grant_type=client_credentials"; const options = { url: url, method: "POST", headers: headers, json: true, } const result = await requestAsync(options); if (result && result.access_token) { functions.logger.log("getAccessToken Success"); accessToken = result.access_token; } else { functions.logger.log("getAccessToken Failed"); } } catch (e) { functions.logger.error("Error", e); } return accessToken; }
※本当はここで取得しなおしたアクセストークンで Firebase Functions 環境変数を変更したいのだが……コマンドライン以外の変更方法が無さそうだったので断念。
バッチ作成
上のアクセストークン確認、更新だと取得したトークンを保存できないため、一度切れたらずっと都度取得し直しになってしまう。
それもどうかと思ったので、週一くらいの頻度でアクセストークンの確認をして、切れていたらコマンドを叩いて Firebase Functions の環境変数を更新するバッチを作成する。
(ドキュメントでは60日で切れるらしいので頻度はもっと低くてもいいかもしれない)
と思ったが、いろいろした結果バッチでなく exe になった。
exe の中でバッチを叩く。
コンフィグに入れるもの
- Twitchアクセストークン
- クライアントID
- クライアントシークレット
確認処理
/// <summary> /// OAuth疎通チェック /// </summary> /// <returns></returns> private static async Task<bool> CheckOAuth() { Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); string twitch_access_token = config.AppSettings.Settings["twitch_access_token"].Value; string url = "https://id.twitch.tv/oauth2/validate"; HttpClient client = new HttpClient(); HttpRequestMessage request = new HttpRequestMessage(System.Net.Http.HttpMethod.Get, url); request.Headers.Add("ContentType", "application/json"); request.Headers.Add("Authorization", "Bearer " + twitch_access_token); HttpResponseMessage response = await client.SendAsync(request); return response.IsSuccessStatusCode; }
取得処理
/// <summary> /// アクセストークン取得 /// </summary> private static async Task<string> GetOAuth() { Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); string twitch_client_id = config.AppSettings.Settings["twitch_client_id"].Value; string twitch_client_secret = config.AppSettings.Settings["twitch_client_secret"].Value; string url = "https://id.twitch.tv/oauth2/token" + "?client_id=" + twitch_client_id + "&client_secret=" + twitch_client_secret + "&grant_type=client_credentials"; HttpClient client = new HttpClient(); HttpRequestMessage request = new HttpRequestMessage(System.Net.Http.HttpMethod.Post, url); HttpResponseMessage response = await client.SendAsync(request); var responseContent = await response.Content.ReadAsStringAsync(); dynamic data = JsonConvert.DeserializeObject(responseContent); string new_access_token = data.access_token; config.AppSettings.Settings["twitch_access_token"].Value = new_access_token; config.Save(); UpdateFireBaseConfig(new_access_token); return new_access_token; }
更新処理
/// <summary> /// FirebaseのConfigを更新 /// </summary> /// <param name="new_access_token"></param> private static void UpdateFireBaseConfig(string new_access_token) { string deployPath = @"Firebaseプロジェクトパス"; string batPath = @"バッチを置きたい場所のパス\バッチ名.bat"; StringBuilder sb = new StringBuilder(); sb.AppendLine("cd " + deployPath); sb.AppendLine("firebase functions:config:set credential.twitch.access_token=" + new_access_token); sb.AppendLine("firebase deploy"); File.WriteAllText(batPath, sb.ToString()); ProcessStartInfo processStartInfo = new ProcessStartInfo(); processStartInfo.FileName = batPath; processStartInfo.CreateNoWindow = true; processStartInfo.UseShellExecute = false; Process.Start(processStartInfo); }
バッチファイルを作成して、起動する。
その中で Firebase Functions の環境変数を更新し、デプロイし直す。
index.js 全体
const functions = require("firebase-functions"); const Twitter = require("twitter"); const crypto = require('crypto'); const request = require('request'); const twitchConfig = functions.config().credential.twitch; const twitterConfig = functions.config().credential.twitter; exports.tw2tw = functions.https.onRequest(async (request, response) => { try { functions.logger.log("tw2tw Start"); const headers = request.headers; const messageType = headers["twitch-eventsub-message-type"]; const subscriptionType = headers["twitch-eventsub-subscription-type"]; // 署名確認 const hmac_message = headers['twitch-eventsub-message-id'] + headers['twitch-eventsub-message-timestamp'] + request.rawBody; const signature = crypto .createHmac('sha256', twitchConfig.hmac_secret) .update(hmac_message) .digest('hex'); const expected_signature_header = 'sha256=' + signature; const message_signature = headers["twitch-eventsub-message-signature"]; if (message_signature != expected_signature_header) { functions.logger.error("Error", "Verify Signature Failed"); response.sendStatus(403); } functions.logger.log("MessageType", messageType); functions.logger.log("subscriptionType", subscriptionType); if (messageType == "webhook_callback_verification") { // Webhhok登録認証 response.send(request.body.challenge); } else if (messageType == "notification") { // 通知 // Twitter準備 const client = new Twitter({ consumer_key: twitterConfig.api_key, consumer_secret: twitterConfig.api_secret, access_token_key: twitterConfig.access_token_key, access_token_secret: twitterConfig.access_token_secret, }); var tweet = ""; if (subscriptionType == "stream.online") { // 配信開始 // 配信タイトル取得 const title = await getStreamTitle(); tweet = title + " https://www.twitch.tv/" + request.body.event.broadcaster_user_login; } else if (subscriptionType == "stream.offline") { // 配信終了 tweet = "配信おわり"; } if (tweet != "") { await client.post("statuses/update", { status: tweet }) } functions.logger.log("Tweet Posted", tweet); response.send("Tweet Posted"); } else { functions.logger.error("Error", "Unknown MessageType"); response.sendStatus(500); } } catch (e) { functions.logger.error("Error", e); response.sendStatus(500); } }); // 配信タイトル取得 async function getStreamTitle() { functions.logger.log("getStreamTitle Start"); var title = ""; try { const accessToken = await checkAccessToken(); if (accessToken == "") { title = "配信はじめ"; return title; } const headers = { "Content-Type": "application/json", "Authorization": "Bearer " + accessToken, "client-id": twitchConfig.client_id, } const url = "https://api.twitch.tv/helix/channels?broadcaster_id=" + twitchConfig.user_id; const options = { url: url, method: "GET", headers: headers, json: true, } const result = await requestAsync(options); if (result && result.data[0].title) { title = result.data[0].title; } else { title = "配信はじめ"; } } catch (e) { functions.logger.error("Error", e); } return title; } // Twitchアクセストークン疎通確認 async function checkAccessToken() { functions.logger.log("checkAccessToken Start"); var accessToken = ""; try { const headers = { "Content-Type": "application/json", "Authorization": "Bearer " + twitchConfig.access_token, } const url = "https://id.twitch.tv/oauth2/validate"; const options = { url: url, method: "GET", headers: headers, json: true, } const result = await requestAsync(options); if (result && result.status != 200) { accessToken = await getAccessToken(); } else { accessToken = twitchConfig.access_token; } } catch (e) { functions.logger.error("Error", e); } return accessToken; } // Twitchアクセストークン更新 async function getAccessToken() { functions.logger.log("getAccessToken Start"); var accessToken = ""; try { const headers = { "Content-Type": "application/json", } const url = "https://id.twitch.tv/oauth2/token" + "?client_id=" + twitchConfig.client_id + "&client_secret=" + twitchConfig.client_secret + "&grant_type=client_credentials"; const options = { url: url, method: "POST", headers: headers, json: true, } const result = await requestAsync(options); if (result && result.access_token) { functions.logger.log("getAccessToken Success"); accessToken = result.access_token; } else { functions.logger.log("getAccessToken Failed"); } } catch (e) { functions.logger.error("Error", e); } return accessToken; } // 非同期リクエスト function requestAsync(options) { try { return new Promise(function(resolve, reject) { request(options, function(error, res, body) { if (res.statusCode != 200) { functions.logger.error("requestAsync Status", res.statusCode); } if (error) { functions.logger.error("requestAsync Error", error); } resolve(body); }); }); } catch (e) { functions.logger.error("Error", e); return null; } }
Twitch配信開始の通知を自動でツイートする ※旧
※2021/10/08 追記
Twitch API の Webhook サブスクリプション登録方法が更新され、WebSub 非推奨になり EventSub が推奨になったようです。
blog.twitch.tv
ということで色々試してみた結果、この記事よりかなり構成が変わりました。
なので、それについて新しく書き直しました。
wfoo13101.hatenablog.com
はじめに
この記事は、こちらの記事qiita.comを参考にしながら、分からなかったところのメモです。
こちらの記事冒頭にもあるように IFTTT を使えば手軽に配信開始の通知自体はできるんですが、遅延より何より、配信タイトルをツイートに入れたくて手作りに手を出しました。
目次
Twitch API への Webhook 登録
記事通り Twitch Developers に登録して、
クライアントIDとトークンを控えられたらOK。
はもっと後で実行するやつなので
の curl のところはとりあえず飛ばす。
配信通知を受け取る Webhook の登録
curl はまだ入れる情報が足りないのでここは飛ばす。
Webhook 用 API の開発
Twitter Developer への登録
ちょっと面倒。
自分の場合は、英語で申請した後に日本語のメールが来て、英語と同じような内容を日本語でちゃんと書いて返信したら登録できた。
単純な通知アプリを作りたい、というのの他に、個人で使う用です、とか、勉強がてらです、とか書いたけど必要だったかは分からない。
設定変更
プロジェクトを登録出来たら、
Settings で App permissions を Write 含むように変更する。
キー、トークン取得
記事内で取得したい情報は
consumer_key = Consumer Keys の API Key
consumer_secret = Consumer Keys の secret
access_token_key = Authentication Tokens の Access token
access_token_secret = Authentication の Tokens secret
という感じ。
consumer_key と consumer_secret はプロジェクトを登録した最初に表示されたと思う。そこで控えておかないと再表示できないので、再作成しないといけなくなる。
あとの二つは Keys and tokens で作成する。こちらも作成後に表示された時に控えておかないと、再作成しないといけなくなる。
Firebase functions の設定
何も知らなかったのでここで詰まった。
npm インストール
Firebase 登録
Functions を使うために料金プランをBlazeにする。
firebase login
Firebase にログイン。
firebase login
firebase init
Firebase最初の処理。
firebase init
するといろいろ質問される。
Are you ready to proceed?
(これからの処理をして大丈夫?)
→ y
Which Firebase CLI features do you want to set up for this folder?
(Firebase 機能の内、どれをセットアップする?)
→ Functions: Configure and deploy Cloud Functions のみ選択して決定
Please select an option
(オプションを選択、作成済みのプロジェクトを使うか、新たに作るかなど)
→ Use an existing project(先にプロジェクトを作成できている場合)
→ 作成済みのプロジェクト一覧が出てくるので選択して決定
What language would you like to use to write Cloud Functions?
(使用する言語は?)
→ 好きな方を選択(よく分からなければJavaScriptの方がいい?)
Do you want to use ESLint to catch probable bugs and enforce style?
(ESLint を使う?)
→ 使った方がいいと思うけど、後で設定とか調整が必要
Do you want to install dependencies with npm now?
(ついでに必要ないろいろを npm でインストールする?)
→ y
これでようやく、ある程度は下準備が出来た。
参考:
Firebaseの始め方 - Qiita
Firebase で Cloud Functions を簡単にはじめよう - Qiita
npm install twitter
先に作ったフォルダの中にいろいろファイルが出来ているはず。
コマンドプロンプトでその中の functions フォルダに移動(cd)。
Twitter でいろいろ出来るようになるやつを npm でインストール。
npm install twitter
index.js
細かい手順については省きますが,以下のようにコードを書き……
のコードを functions 内の index.js にコピー。
コード内の配信URLが固定で書かれているので編集。
'https://www.twitch.tv/' + streamInfo.user_login
にしても良さそう。
もしくは好きな文言に変える。
firebase deploy
そしたらデプロイをしてみる。
firebase deploy
ESLint 修正
そのままだと大体の場合 ESLint に怒られるはずなので調整。
Parsing error: Unexpected token =>
というエラーが発生していたら、.eslintrc.js に
parserOptions: { ecmaVersion: 8, },
を追加。
位置は多分どこでもいいけど、env の下がいい?
またデプロイをすると今度は大量に ESLint に怒られると思う。
1:27 error Strings must use doublequote
最初のは 1行目 27文字目という意味。
この位置を頼りに修正。
Strings must use doublequote
文字列でシングルクォート(')を使ってるところをダブルクォート(")にする。
Expected linebreaks to be 'LF' but found 'CRLF'
改行コードをLFにする。
Block must not be padded by blank lines
余計な空白行を削除する。
'data' is not defined
data の前に const を追加。
Expected space(s) after "if"
if の後に空白を入れる。
Missing space before opening brace
{ の前に空白を入れる。
Missing trailing comma
行の最後にカンマを入れる。
'+' should be placed at the end of the line
文字列を繋げる + は行の最後に入れる。
There should be no space after '{'
余計な空白を削除する。
Missing semicolon
行の最後にセミコロンを入れる。
Trailing spaces not allowed
行の最後にある余計な空白を削除する。
エラーがなくなるまで修正。
Webhook 登録
これまで飛ばしていた curl の出番。
配信通知を受け取る Webhook の登録
にある以下のコマンドを編集する。
curl -X POST -d "{\"hub.callback\":\"WebhookとするURL\", \"hub.mode\":\"subscribe\", \"hub.topic\":\"https://api.twitch.tv/helix/streams?user_id=130871908\", \"hub.lease_seconds\":\"864000\"}" -sS -H 'Content-Type: application/json' -H 'Authorization: Bearer さっきのアクセストークン' -H 'client-id: さっきのクライアントID' https://api.twitch.tv/helix/webhooks/hub
分かりやすくすると、こう。
curl -X POST -d "{ \"hub.callback\":\"WebhookとするURL\", \"hub.mode\":\"subscribe\", \"hub.topic\":\"https://api.twitch.tv/helix/streams?user_id=130871908\", \"hub.lease_seconds\":\"864000\" }" -sS -H 'Content-Type: application/json' -H 'Authorization: Bearer さっきのアクセストークン' -H 'client-id: さっきのクライアントID' https://api.twitch.tv/helix/webhooks/hub
編集部分
- WebhookとするURL
Firebase のページを確認
デプロイが成功していたら、Function のダッシュボードに関数が表示されているはず
(コピーしたままだったら、関数名が tw2tw)
これのトリガー列で、リクエストの下に小さく表示されているURL
- ~?user_id=130871908
Twitch のユーザーID(数字列)を取得する必要がある。
curl "https://api.twitch.tv/helix/users?login=ユーザー名" -H "Content-Type: application/json" -H "Authorization: Bearer トークン" -H "client-id: クライアントID"
ユーザー名部分には普通のアルファベットとかのユーザー名を入れる。
トークンとクライアントIDは Twitch で取得したものを入れる。
これを実行して取得できた id を user_id= の先に入れる。
- さっきのアクセストークン
Twitch で取得したトークン。
- さっきのクライアントID
Twitch で取得したクライアントID。
以上をすべて入れて、コマンドプロンプトで実行。
返事は特に帰ってこないが、それで多分大丈夫……。
2021/4/2追記
※このWebhook(サブスクリプション)は有効期限があり、10日で切れてしまう。
https://dev.twitch.tv/docs/api/webhooks-guide
上のcurlコマンドのbatファイルを作って、タスクスケジューラーに一週間毎実行で登録してみた。
2021/5/28追記
タスクスケジューラー自体は上手くいってるっぽい。
これで通知が上手くいってない場合、OAuthの期限切れの可能性。
適当なユーザID取得を流して401が返ってきた場合は多分、そう。
適当なユーザ情報取得コマンド
curl "https://api.twitch.tv/helix/users?login=ユーザー名" -H "Content-Type: application/json" -H "Authorization: Bearer アクセストークン" -H "client-id: クライアントID"
OAuthアクセストークン取得をやり直して、
batに書いたコマンドのアクセストークンを新しいものに変える。
確認
実際の動作は試しにTwitchで配信を開始して終了させ、
Twitterに投稿されているか見る必要がある。
あとは、Firebase のページでログを見ることが出来る。
Function execution started
から
Function execution took 243 ms, finished with status code: 200
となっていたら何事もなかった感じ。
Function execution took 411 ms, finished with status: 'crash'
とかになっていたら何かあったので直前のログを確認。
Unhandled rejection
とかあったら、大抵 Twitter 関連の問題。
[ { code: 187, message: 'Status is a duplicate.' } ]
とかあるので、code や message + Twitter errorで検索。
上の code: 187 は短い間隔で連続して同じツイートをしようとしたときに怒られるやつ。
code: 89 はトークンが期限切れしたとからしいので、
Authentication Tokens の Access token & secret を再作成する。
環境変数も入れ直してデプロイしなおす。
という感じでできました、というメモでした。
【FaceRig】資料
FaceRig公式サイト
カスタムアバター作成について
Another example can be found here:
以下のリンクからサンプルデータがダウンロードできる
You can find all the necessary documentation using this direct link:
以下のリンクからは下記のドキュメントがダウンロードできる
「FaceRig Model and Textures Documentation v1.0」
このドキュメントに各命名規則やアニメーション一覧などが載っている
How to use the importer wizard:
以下のリンクからは下記のドキュメントがダウンロードできる
「How To Use ImportWizard」
このドキュメントにImportWizardの使い方が載っている
【FaceRig】困ったら
『ImportWizardのフォルダ取り込みでつまる』
- パスの中に空白がある場合は取り除く
- FaceRigを一度も起動してない場合は起動する
- 「管理者で実行」でImportWizardを起動してみる
『ImportWizardのモデル取り込みでつまる』
- モデルのボーン名を確認する
- モデルのボーンを減らしてみる
- UVマップが2つ以上あるオブジェクトが無いか確認する
『ImportWizardのインポートでつまる』
- animフォルダの中身を削除してみる
『FaceRigでモデルが表示されない』
- Cameraの位置と向きを確認
- モデルの大きさを確認
『FaceRigでモデルの一部が表示されない』
『FaceRigでモデルの大きさがおかしい』
- Blenderでオブジェクトごとに「ctrl + A」で位置、回転、拡大縮小を適用する
『FaceRigでモデルの向きがおかしい』
- Cameraの位置と向きを確認
- 軸を確認
『FaceRigでモデルの一部の向きがおかしい』
- ボーンの回転を確認
【FaceRig・Blender】モデリング - ⑤エクスポート
ジオメトリ
エクスポートする際はデフォルトポーズであること
つまりポーズモードなどでポーズの変更をしていない状態であること
ポーズモードで「ポーズ」>「トランスフォームをクリア」>「すべて」
アニメーションをつけていない状態にする
アーマチュアを選択し、ドープシートでアクションとのリンクを切る
この「×」をクリック
COLLADAファイルでエクスポート
- メッシュ
- アーマチュア
- カメラ(ボーンでカメラを作成していない場合)
をすべて選択し
「ファイル」>「エクスポート」>「Collada(デフォルト)(.dae)」を選ぶ
左側の設定のエクスポートデータオプションで
- 選択物のみ
- 子を含む
- アーマチュアを含む
のチェックを入れ
- シェイプキーを含む
のチェックを外す
ジオメトリの命名規則は
モデル名 + "Geometry"
モデル名:Test
→「TestGeometry.dae」
この命名規則は無視可能
インポート時に自動で読み込まれなくなるだけ
アニメーション
COLLADAファイルでエクスポート
命名規則は自由?
公式ドキュメントのアニメーション一覧にある名前と同じものをつける?
ドープシートで出力したいアクションを選び、あとはモデル本体と同じようにエクスポート
【FaceRig・Blender】モデリング - ④アニメーション
必須のアニメーションはidle1のみ
モデルのインポートのみ確認したい場合はモデル本体の.daeファイルで代替できるので特に作成しなくていい
アニメーションは30FPSであること
レンダータブ>寸法>フレームレートで設定(画像右下)
ボーンでのアニメーションであること
頂点モーフなどは不可
すべてのアニメーションでActionSelectorの横のFを押すようにする
そうしないと、アーマチュアに現在リンクされているもの以外のアニメーションが保存されない
ジオメトリを編集しても、大きさや構造が大きく変わらなければアニメーションはそのまま使えるっぽい
手と腕のアニメーションはLeap Motionが無いとFaceRigで反映されないので、機器がない場合は作成しなくていい
アニメーション一覧
アニメーションがImportWizardでインポートされるとアニメーションは全てanimという名前のフォルダに入れられる
さらにanimの中で以下のサブフォルダに分けられる
In generalMovement anim sub-folder:
<全体の動きサブフォルダ>
Avatar_FB
Avatar_LR
Avatar_Twist
Head_LR
Head_Twist
Head_UD
idle1 ※必須
In EyesAndEyebrows anim subfolder:
<目と眉サブフォルダ>
LeftEye_LR
LeftEye_UD
LeftEyebrow_D
LeftEyebrow_D_ext
LeftEyebrow_U
LeftEyebrow_U_ext
LeftEyeClosed
LeftEyeSquint
LeftEyeWideOpen
RightEye_LR
RightEye_UD
RightEyebrow_D
RightEyebrow_D_ext
RightEyebrow_U
RightEyebrow_U_ext
RightEyeClosed
RightEyeSquint
RightEyeWideOpen
In MouthAndNose anim subfolder:
<口と鼻サブフォルダ>
CheekPuff_L
CheekPuff_R
Mouth_pursedLips_LR
Mouth_unveilledTeeth_D
Mouth_unveilledTeeth_U
MouthClosedLeft_D
MouthClosedLeft_U
MouthClosedLeft_U_visime
MouthClosedRight_D
MouthClosedRight_U
MouthClosedRight_U_visime
MouthOpen
MouthOpen_base
MouthOpen_pursedLips_LR
MouthOpenLeft_D
MouthOpenLeft_U
MouthOpenRight_D
MouthOpenRight_U
MouthTongueBase
NoseWrinker_D
NoseWrinker_U
TongueIdle
TongueOut_LR
TongueOut_UD
In ShouldersAndHands anim subfolder:
<肩と手サブフォルダ>
FingerL0_extFlex
FingerL1_extFlex
FingerL2_extFlex
FingerL3_extFlex
FingerL4_extFlex
FingerR0_extFlex
FingerR1_extFlex
FingerR2_extFlex
FingerR3_extFlex
FingerR4_extFlex
HandL_closeDown_LR
HandL_closeMiddle_LR
HandL_closeUp_LR
HandL_farDown_LR
HandL_farMiddle_LR
HandL_farUp_LR
HandL_solo_LR
HandL_solo_Twist
HandL_solo_UD
HandR_closeDown_LR
HandR_closeMiddle_LR
HandR_closeUp_LR
HandR_farDown_LR
HandR_farMiddle_LR
HandR_farUp_LR
HandR_solo_LR
HandR_solo_Twist
HandR_solo_UD
In visime anim subfolder:
<音素サブフォルダ>
visime_new_AA
visime_new_AH
visime_new_AO
visime_new_AW-OW
visime_new_CH-J-SH
visime_new_EH-AE
visime_new_EY
visime_new_FV
visime_new_IH-AY
visime_new_L
visime_new_M-P-B
visime_new_N-NG-DH
visime_new_OY-UH-UW
visime_new_R-ER
visime_new_W
visime_new_X
visime_new_Y-IY
Directly in anim folder:
<animフォルダ直下>
_frown
_laugh
_unveilTeeth
_wonder
cheekL
cheekR
special01
special02
special03
special04
special05
special06
これだけ作っておけばそれっぽくなるアニメーション(独断と偏見)
Avatar_FB (体全体の動き)
Avatar_LR
Avatar_Twist
Head_LR (頭全体の動き)
Head_Twist
Head_UD
idle1 (静止)※必須
LeftEye_LR (目の動き)
LeftEye_UD
LeftEyeClosed
LeftEyeSquint
LeftEyeWideOpen
RightEye_LR
RightEye_UD
RightEyeClosed
RightEyeSquint
RightEyeWideOpen
MouthOpen (口の動き)
MouthOpen_base
MouthClosedLeft_U
MouthClosedRight_U
次点
LeftEyebrow_D (眉の動き)
LeftEyebrow_D_ext
LeftEyebrow_U
LeftEyebrow_U_ext
RightEyebrow_D
RightEyebrow_D_ext
RightEyebrow_U
RightEyebrow_U_ext
参考
アニメーションの作り方
アニメーション一覧
【FaceRig・Blender】モデリング - ③ボーンとウェイト
ボーンの命名規則は必須
ただし全種類作る必要はない
(もしかしてBipを先頭につければあとの名前は自由?)
必須ボーンは
- Camera(カメラ)
- BipHead(頭)
- BipLEye(左目)
- BipREye(右目)
の4つ
CameraはボーンでなくBlenderで新規ファイル作成時に既にあるCmaeraでOK
位置と向きは調整すること
顔の正面に向かい合うようにして配置する
全ての頂点にウェイトが設定されていること
スケルトンヒエラルキー
以下、公式ドキュメントに載っているもの
下半身のボーンも含まれているが下半身はFaceRig Studioでのみ使用できる?
重要そうなボーン名に日本語名を追記
あとわかりにくかったので少し順番を並び替え
BipRoot
| BipSpine (腰)
| | BipSpine1
| | | BipSpine2
| | | | BipNeck (首)
| | | | | BipHead (頭)※必須
| | | | | | BipJaw (あご)
| | | | | | | BipBeard
| | | | | | | Bip_LipDown (中央下唇)※下唇は BipJaw(あご)の子供になっていること
| | | | | | | | Bip_LipDownInner
| | | | | | | BipLLipDown_1 (左下唇)
| | | | | | | | BipLLipDownInner_1
| | | | | | | BipLLipDown_2
| | | | | | | | BipLLipDownInner_2
| | | | | | | BipLLipDown_3
| | | | | | | | BipLLipDownInner_3
| | | | | | | BipRLipDown_1 (右下唇)
| | | | | | | | BipRLipDownInner_1
| | | | | | | BipRLipDown_2
| | | | | | | | BipRLipDownInner_2
| | | | | | | BipRLipDown_3
| | | | | | | | BipRLipDownInner_3
| | | | | | | BipTeethDown (下歯)
| | | | | | | BipTongue1 (舌)
| | | | | | | | BipTongue2
| | | | | | | | | BipTongue3
| | | | | | | | | | BipTongue4
| | | | | | | BipLCheek_4 (頬)
| | | | | | | BipRCheek_4
| | | | | | Bip_JawRef
| | | | | | BipTeethUp (上歯)
| | | | | | BipLLipCorner (左口端)
| | | | | | BipRLipCorner (右口端)
| | | | | | Bip_LipUpper (中央上唇)
| | | | | | BipLLipUpper_1 (左上唇)
| | | | | | BipLLipUpper_2
| | | | | | BipLLipUpper_3
| | | | | | BipLLipUpper_4
| | | | | | BipRLipUpper_1 (右上唇)
| | | | | | BipRLipUpper_2
| | | | | | BipRLipUpper_3
| | | | | | BipRLipUpper_4
| | | | | | BipLEye (左目)※必須
| | | | | | BipREye (右目)※必須
| | | | | | BipLEyeCornerIn (左目頭)
| | | | | | BipREyeCornerIn (右目頭)
| | | | | | BipLEyeCornerOut (左目尻)
| | | | | | BipREyeCornerOut (右目尻)
| | | | | | BipLEyelid_down1 (左下まぶた)
| | | | | | BipREyelid_down1 (右下まぶた)
| | | | | | BipLEyelid_up1 (左上まぶた)
| | | | | | BipREyelid_up1 (右上まぶた)
| | | | | | BipLEyebrow_0 (左眉)
| | | | | | BipLEyebrow_1
| | | | | | BipLEyebrow_2
| | | | | | BipLEyebrow_3
| | | | | | BipLEyebrow_4
| | | | | | BipREyebrow_0 (右眉)
| | | | | | BipREyebrow_1
| | | | | | BipREyebrow_2
| | | | | | BipREyebrow_3
| | | | | | BipREyebrow_4
| | | | | | BipLNose (鼻)
| | | | | | BipRNose
| | | | | | BipNoseRing
| | | | | | BipNoseTip
| | | | | | BipSnout
| | | | | | BipLEar (耳)
| | | | | | BipREar
| | | | | | BipLCheek_1 (頬)
| | | | | | BipLCheek_2
| | | | | | BipLCheek_3
| | | | | | BipLCheekMiddle
| | | | | | BipLCheekUpper
| | | | | | BipRCheek_1
| | | | | | BipRCheek_2
| | | | | | BipRCheek_3
| | | | | | BipRCheekMiddle
| | | | | | BipRCheekUpper
| | | | | | BipLHead_D
| | | | | | BipRHead_D
| | | | | | BipLHead_U
| | | | | | BipRHead_U
| | | | BipLClavicle (左鎖骨)
| | | | | BipLUpperArm (左上腕)
| | | | | | BipLForearm (左二の腕)
| | | | | | | BipLHand (左手)
| | | | | | | | BipLFinger0 (左親指)
| | | | | | | | | BipLFinger01
| | | | | | | | | | BipLFinger02
| | | | | | | | BipLFinger1 (左人差し指)
| | | | | | | | | BipLFinger11
| | | | | | | | | | BipLFinger12
| | | | | | | | BipLFinger2 (左中指)
| | | | | | | | | BipLFinger21
| | | | | | | | | | BipLFinger22
| | | | | | | | BipLFinger3 (左薬指)
| | | | | | | | | BipLFinger31
| | | | | | | | | | BipLFinger32
| | | | | | | | BipLFinger4 (左小指)
| | | | | | | | | BipLFinger41
| | | | | | | | | | BipLFinger42
| | | | | | | BipLWristCorr
| | | | | | | BipLWrist
| | | | | | BipLShoulder1
| | | | BipRClavicle (右鎖骨)
| | | | | BipRUpperArm (右上腕)
| | | | | | BipRForearm (右二の腕)
| | | | | | | BipRHand (右手)
| | | | | | | | BipRFinger0 (右親指)
| | | | | | | | | BipRFinger01
| | | | | | | | | | BipRFinger02
| | | | | | | | BipRFinger1 (右人差し指)
| | | | | | | | | BipRFinger11
| | | | | | | | | | BipRFinger12
| | | | | | | | BipRFinger2 (右中指)
| | | | | | | | | BipRFinger21
| | | | | | | | | | BipRFinger22
| | | | | | | | BipRFinger3 (右薬指)
| | | | | | | | | BipRFinger31
| | | | | | | | | | BipRFinger32
| | | | | | | | BipRFinger4 (右小指)
| | | | | | | | | BipRFinger41
| | | | | | | | | | BipRFinger42
| | | | | | | BipRWristCorr
| | | | | | | BipRWrist
| | | | | | BipRShoulder1
| | | | BipLShoulder2
| | | | BipLShoulder3
| | | | BipRShoulder2
| | | BipLShoulder4
| | | BipRShoulder3
| | | BipRShoulder4
| BipLThigh
| | BipLCalf
| | | BipLFoot
| | | | BipLToe
| BipRThigh
| | BipRCalf
| | | BipRFoot
| | | | BipRToe
Camera (カメラ)※必須