Herokuでpython+mecab+ffmpegを使う
はじめに
最近、ちまちまとdiscord上で動くchatbotを作っているのですが、やっぱり公開サーバーで動かさないと日常的に使えないのでHerokuにデプロイすることにしました。 ただ、無料枠のHerokuで動かそうとするといろいろと解決しないといけない課題が多くてひたすら逃げていたのですが、ようやく重い腰を上げてデプロイしたので、手順をメモがてら残してみたいと思います。
必要な要件
を使っているのでこれらが動かせる環境が必要。
Herokuでpython+mecabの環境づくりは下記の記事を参考にしました。 herokuでpython+django+scikit-learn+mecab(1) ffmpegの環境づくりはこちらを参考にしました。 DiscordBotでyoutubeの音声をボイスチャットに流す
いざ、構築
condaとherokuの複数のビルドパックを使うのでheroku-buildpack-multiでアプリを作成する必要があります。気になるのはこのリポジトリがメンテナンスを終了していることでしょうか。
This buildpack is no longer actively maintained. The associated functionality exists natively on the Heroku platform. Please refer to https://devcenter.heroku.com/articles/buildpacks and https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app for documentation.
まず、ローカルでリポジトリを作ります。
$ git init $ heroku create --buildpack https://github.com/heroku/heroku-buildpack-multi
使用するビルドパックは以下の通りです。
https://github.com/Sashimimochi/conda-buildpack.git https://github.com/sunny4381/heroku-buildpack-linuxbrew.git https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git https://github.com/Crazycatz00/heroku-buildpack-libopus.git
scikit-learn
などのCコンパイラを必要とするライブラリも使いたいのでcondaのビルドパックがいるのですが、pythonのバージョンを固定したいので以下のリポジトリをforkして自前で用意することにしました。
https://elements.heroku.com/buildpacks/teamupstart/conda-buildpack
minicondaのバージョン一覧はこちらから見れます。
https://repo.continuum.io/miniconda/
ちなみに、ものによっては、以下のような最近のpipでは廃止されたオプションが付いていてビルドエラーを起こすので注意。
pip install -r requirements.txt --exists-action=w --allow-all-external | indent
- https://github.com/heroku-python/conda-buildpack/issues/36
- https://stackoverflow.com/questions/57546079/no-such-option-allow-all-external-when-deploying-a-django-app-to-heroku-wit
上記の.buildpacks
でheroku-buildpack-linuxbrew
を入れたのでbrew
が使えるようになっています。brew
で入れたいパッケージを.cellar
で指定します。
mecab mecab-ipadic open-jtalk
conda
およびpip
でインストールするライブラリはconda-requirements.txt
やrequirements.txt
に書いておきます。
Herokuにmecab-pythonをインストールしようとすると案の定、エラーが出ました。 herokuでpython+django+scikit-learn+mecab(1)では手動でインストールしていましたが、今回は、python3系を使うわけですし、mecab-python3ならはいったので、代わりにこちらを入れます。 Heroku mecabインストール時にエラー
あとはパスを指定してデプロイします。
$ heroku config:add LD_LIBRARY_PATH=/app/.linuxbrew/lib $ heroku config:set MECAB_PATH=/app/.linuxbrew/lib/libmecab.so $ git add . $ git commit -m 'initial' $ git push heroku master
滞りなく、一連のデプロイが進んで無事うまくいったように思えたのですが、最後のメッセージであえなく撃沈。
-----> Compressing... ! Compiled slug size: 3009.8M is too large (max is 500M). ! See: http://devcenter.heroku.com/articles/slug-size ! Push failed
https://devcenter.heroku.com/articles/slug-compiler#slug-size 上記のメッセージにもある通り、Herokuの無料枠のストレージサイズが500MBなのでデプロイ時にこの範囲内に収める必要があります。
容量を食っているのが、機械学習モデルだったので、Herokuでこれを動かすにはモデルのサイズを圧縮しておく必要がありました。 中でも一番のファイルサイズが大きいのがWord2Vecだったので、こちらのライブラリを使ってモデルサイズを圧縮しました。 50,000語程度にまで絞れば約60MBまで圧縮できます。 https://github.com/yagays/minify_w2v 重要語は以下から選択しました。 日本語を読むための語彙データベース Ver. 1.1 その他にもcacheを削除するなどいろいろ工夫の余地はありそうです。
これでリポジトリの容量をかなり圧縮できました。
大量のライブラリを使ってしまったせいで、これでもデプロイ時にpip install
までしてしまうと上限の500MBを超えてしまったので、起動時にpip install
が走るようにしておきました。
bot: bash run.sh
pip install -r requirements.txt python app.py
以上で、なんとか無事に動きました。
トラブルシューティング
Herokuリポジトリにログイン
Herokuにログインしてマニュアル操作するときは以下のようにすればログインしてbash
コマンドが使えます。
heroku run bash
ログの確認
デプロイ後にアプリケーションのログが見たければ以下のコマンドで確認できます。
$ heroku logs
やっぱりはてなブログを自分のホームページに埋め込みたい
※2021/1/18に複数のサイトからRSSを取得したい場合のやり方を末尾に追記しました。
経緯
長年お世話になっていたYQLが残念ながら2019年1月に無期限のサービス停止を発表されました。Yahooさん、今までありがとうございました。
On Jan. 3, 2019, YQL service at https://t.co/g4W9RhdMLk will be retired. YQL based services that use https://t.co/g4W9RhdMLk, including users of https://t.co/5IkUaEykdl, will no longer operate. Yahoo Weather API users see the tweet below for info about continuing your service.
— Y! Developer Network (@ydn) 2018年12月31日
したがって、再び新しい方法を考える必要が出てきました。
私情ではありますが、最近ようやくReactを勉強し始めましたので、今回はReactを使ったRSSフィードの取得・表示方法を考えてみました。
着手当初はどうしようと戦々恐々だったのですが、比較的簡単に行けそうだったのでご紹介してみます。
やり方さえ分かれば、30分もかからずにできてしまうと思います。(私は調べる過程を含めると半日くらいかかりました。)
※Reactのチュートリアルすらまともに理解しているか怪しい初心者がトライ&エラーで考えた方法なので、ベテランの方からすれば滅茶苦茶な実装になっているかもしれません。ご容赦ください。
とりあえずRSS情報を取得して表示する
何はともあれ、まずは完成形をお見せします。無事、ブログのタイトルとリンクを取得して飛べるようになりました。
見てくれ部分は私のUIセンスの問題なので、これなんかよりいくらでも格好よくできると思うので、ぜひ各々アレンジしてみてください。
(というか、格好いいデザインができたら教えてください)
環境は
$ create-react-app -V 2.1.1
を使っています。
で、肝心の実装ですが、あれこれ調べてみたところ、手っ取り早く実現できそうなものにRSSParserというライブラリがありました。
ということで今回は、このRSSParserを使います。
www.npmjs.com
ライブラリのインストールは
npm i rss-parser
とかで出来ます。
ひとまず、RSS情報を取ってきてコンソールに表示させてみます。
取り急ぎ動作確認したいならcreate-react-app
でプロジェクト?を作ってApp.jsに書き込めば動作確認は出来ます。
import React from 'react'; //中略 const RssParser = require('rss-parser'); //importではなくrequireで呼びます const url = 'http://t-n-clark.hatenadiary.jp/feed' const rssParser = new RssParser(); rssParser.parseURL(url) .then((feed) => { console.log(feed); }); .catch((error) => { console.error('Get Failed', error); }); class App extends Component { render(props) { const classes = this.props.classes; //...後略
Chromeのデベロッパーツールで確認すると、ちゃんとコンソールに取得したRSS情報が出ています。
ざっと見たところ、記事のタイトルとかの情報はこのitemsに入っているみたいです。
各ブログのトップURL/rss
もしくは
各ブログのトップ/feed
で取得出来ます。
例えば、このブログのRSSは
https://t-n-clark.hatenadiary.jp/feed
で取れます。
※HTTPでも取れそうな気がするのですが、HTTPだとスマホではちゃんと取れなかった表示されなかったので、HTTPSをおすすめします。
(端末ではなく、ブラウザの問題かもしれませんが... これが関係するのでしょうか?)
bookmark.hatenastaff.com
Qiitaであれば、
https://qiita.com/アカウント名/feed.atom
で取得出来ます。
例えば、こんな感じ
https://qiita.com/Sashimimochi/feed.atom
ただ、HTTPSの場合はクロスドメイン制約があるので、URLの前にCORS(cross-origin resource sharing)PROXYをつけてあげる必要があります。
上記のRSSParserのページを読んだところ、こんな感じで行けそうです。
const CORS_PROXY = "https://cors-anywhere.herokuapp.com/" //はてなブログならこっち const url = CORS_PROXY + 'https://t-n-clark.hatenadiary.jp/feed' //Qiitaならこっち const url = CORS_PROXY + 'https://qiita.com/Sashimimochi/feed.atom'
あとは取得した情報をstateに格納してJSX内で呼び出してみます。 先ほどデバッグ用に出していたConsoleへの出力はもういらないので消しても大丈夫です。
import React from 'react'; import { Component } from 'react'; import PropTypes from 'prop-types'; import './App.css'; /*RSSParserのインスタンス生成*/ const RssParser = require('rss-parser'); const url = "http://t-n-clark.hatenadiary.jp/feed" const rssParser = new RssParser(); class App extends Component { /*コンストラクターの定義*/ constructor(props) { super(props); this.state = { contents: [], }; this.componentDidMount = this.componentDidMount.bind(this) } /*stateにrssParserの結果をバインド*/ componentDidMount() { rssParser.parseURL(url) .then((feed) => { const data = feed.items; console.log(data); this.setState({ contents: [...data] }); }) .catch((error) => { console.error('Get Failed', error); }) } render(props) { const classes = this.props.classes; /*表示するコンテンツの形に合わせてstateの中身を書きだす(mapで拡張forループみたいに使う)*/ const contents = this.state.contents.map(content => { return <div> {content.title} /*{content.pubDate} 公開日(Qiitaなら{content.published})*/ /*{content.link} ページリンク*/ </div> }); return ( <div className="App"> <header className="App-header"> {contents} /*ここに出力*/ </header> </div> ); } } App.propTypes = { classes: PropTypes.object.isRequired, }; export default App;
これで無事RSS情報を取得して表示できるようになりました。
あとは、これを自分なりに装飾していくことになります。
見てくれを整える
参考になるかはわかりませんが、冒頭にあった私なりの装飾を紹介してみます。 主にMaterial-Ul(のCard)とreact-moment(※momentではありません)を使っています。 material-ui.com www.npmjs.com
×こっちではありません。 www.npmjs.com
それぞれ
# Material-UI (@4.0.1) $ npm install @materail-ui/core # react-moment (@0.9.2) $ npm i react-moment
でインストールできます。 まずは、ほぼほぼMaterial-Ulの公式ドキュメントにあるCardのサンプルに先程のRSSデータを流し込みます。
import React from 'react'; import { Component } from 'react'; import PropTypes from 'prop-types'; import Card from '@material-ui/core/Card'; import Button from '@material-ui/core/Button'; import Avatar from '@material-ui/core/Avatar'; import red from '@material-ui/core/colors/red'; import { withStyles } from '@material-ui/core/styles'; import Grid from '@material-ui/core/Grid'; import './App.css'; import { CardHeader, CardContent } from '@material-ui/core'; const RssParser = require('rss-parser'); const url = "http://t-n-clark.hatenadiary.jp/feed" const rssParser = new RssParser(); const styles = theme => ({ blog: { margin: 20, padding: 20, marginTop: 10, }, blogcontent: { overflowY: 'scroll', height: '200px', }, avatar: { backgroundColor: red[500], }, }); class App extends Component { constructor(props) { super(props); this.state = { contents: [], }; this.componentDidMount = this.componentDidMount.bind(this) } componentDidMount() { rssParser.parseURL(url) .then((feed) => { const data = feed.items; console.log(data); this.setState({ contents: [...data] }); }) .catch((error) => { console.error('Get Failed', error); }) } render(props) { const classes = this.props.classes; const contents = this.state.contents.map(content => { return <Card> <CardHeader avatar={<Avatar arial-label="Blog" className={classes.avatar}>も</Avatar>} subheader={content.pubDate} title={content.title} /> <CardContent key={content.id}> <Button size="small" color="primary" href={content.link}>Read More</Button> </CardContent> </Card> }); return ( <div className="App"> <header className="App-header"> <Grid container> <Grid item xs={12} sm={7}> <div className={classes.blog}> <h3>はてなブログ更新情報</h3> <div className={classes.blogcontent}>{contents}</div> </div> </Grid> </Grid> </header> </div> ); } } App.propTypes = { classes: PropTypes.object.isRequired, }; export default withStyles(styles)(App);
今回はタイトル(title
)と日付(pubDate
)とリンク(link
)のみを使っていますが、
スペースとの相談次第では、サマリー(contentSnippet
)とか入れてもいいかもしれません。
(RSSParserってサムネイル画像も取れているのでしょうか?もし取れているならサムネイル画像を使うのもありだと思います。)
はてブのfeed側の仕様なのか、RSSParser側の仕様なのか確かめてませんが、デフォルトだと最新30件?分のRSS情報が取れているみたいです。
なので、それをそのまま表示すると中々悲惨なことになります。
Material-UIにもbootstrapのようなグリッドシステム的なものがありますので、Gridで幅調整をすることが出来ます。
Gridを使えば多少のレスポンシブデザインに対応出来ます。
あとはoverflowY: 'scroll'
で表示枠のサイズを指定したりしてみました。
これで個人的には、おおよその見た目は及第点くらいにはなったのですが、時刻表示が世界標準のままで見にくいです。
そこで、react-moment
というライブラリを使ってこれを整形します。
RSSParserで取ってきた日付をフォーマットを指定してMomentタグで挟んであげればいいみたいです。
例えば、YYYY/MM/DD
にしたければ、
import Moment from 'react-moment' <Moment format="YYYY/MM/DD">{content.pubDate}</Moment>
とやれば、
# 変換前 2019-03-01T02:30:00.000Z # 変換後 2019/03/01
に出来ました。
他にもいろいろあるみたいなので、公式のサンプルを参考にお好みのフォーマットにしてみてください。
ちなみにですが、Twitterの埋め込みなら、専用のライブラリ(私はreact-twitter-widgetsを使っています)を使えばもっとお手軽にできてしまいます。 www.npmjs.com
あとがき
以上で、React限定&初心者によるメチャメチャ実装ではありますが、ひとまず再びブログの更新情報を埋め込めるようになりました。
この方法はあと何年使えるんだろう…?
先日、Googleから発表されたiframeに代わるPortalsタグを使えば、現在稼働中のクラシカルなページでもSPA並みにサクサク遷移できるようになるみたいです。
まあ、まだearly stageみたいなので私は試していませんが…
docs.google.com
blog.uskay.io
私事ですが、今回RSSフィードの実現方法を変える必要があったので、この機にいっそのことホームページの方も一新しようかなと思っています。
今見返すと一昔前のような見た目ですし。無事、一新したら紹介するかもしれません。
暫定版→http://clarkphys.html.xdomain.jp/index.htmlclarkphys.html.xdomain.jp
それではまた。
2021/1/18追記
効率が良いかは自信がありませんが、複数のサイトからRSS情報を取得する場合は以下のようにurl
とcomponentDidMount()
を書き換えればできます。
ポイントは次の2つです。
setState
で情報を更新するときに、既存のstateと新規取得情報を結合しておく- サイト内では順番通りになっているが結合したときに日付がバラバラなので、sortしておく
const RssParser = require('rss-parser'); const CORS_PROXY = "https://cors-anywhere.herokuapp.com/" // 取得したいURLをListで定義しておく const urls = [ CORS_PROXY + 'https://t-n-clark.hatenadiary.jp/feed', CORS_PROXY + 'https://qiita.com/Sashimimochi/feed.atom' ] const rssParser = new RssParser(); class Profile extends Component { constructor(props) { super(props); this.state = { contents: [], }; this.componentDidMount = this.componentDidMount.bind(this) } componentDidMount() { urls.map((url) => { rssParser.parseURL(url) .then((feed) => { // 新たに取得したfeedを取り出す const data = feed.items; // 既存のstateを取り出しておく var feeds = this.state.contents // 既存のstateに新規で取得したfeedを1つずつ追加する data.map((d) => { feeds.push(d) }) // 日付でsortする feeds.sort(function (a, b) { if (a.pubDate < b.pubDate) { return 1; } if (a.pubDate > b.pubDate) { return -1; } return 0 }); // stateをupdateする this.setState({ contents: feeds }); }) .catch((error) => { console.error('Get Failed', error); }) }) }
次回
PythonでDiscordのTRPG用のダイスボットを自作してみた
始まり
始めに断っておきますが、私はTRPGもDiscordもにわか勢です。
最近、身内でTRPG(主にCoC)をぼちぼちやるのですが、毎回ダイス振って判定するのってメンドくさいよねって話になりました。 セッション環境はdiscordを使い始めたので、discord上で動くbotにしようということになりました。正直なところ、既に先人のお方が作られた素晴らしいものがあるので「discord-bcdicebot使えばよいのでは?」というのが賢い選択だと思います。 ですが、ポンコツの私には使い方がわからなかったので、勉強も兼ねて友人に手伝ってもらいながら作ってみました。
Python + Googleスプレッドシート (+ Heroku)で実現しています。 ソースコード全体はGitHubに晒しておきます。
PythonからGoogleスプレッドシートにアクセスする
ダイスを振るだけなら不要なのですが、技能の成否判定も自動でしたいのでキャラクターシートをGoogleスプレッドシートに作って判定することにしました。 Googleスプレッドシートの操作方法はこちらを参考にしました。
こちらを参考にOAuth用のクライアントIDを作成してjsonファイルをダウンロードするところまで進めます。 セキュリティを度外視するならGoogle Apps ScriptでHTTPリクエストを受け付けるという方法もあるかとは思います。
スプレッドシートの仕様は適当でこんな感じです。
技能 | 合計値 | 純減 | 初期値 | 職業P | 興味P |
---|---|---|---|---|---|
HP | 9 | 0 | 9 | 0 | 0 |
MP | 7 | 0 | 7 | 0 | 0 |
SAN | 60 | 0 | 60 | 0 | 0 |
db | 1d4 | 0 | 1d4 | 0 | 0 |
こぶし | 50 | 0 | 50 | 0 | 0 |
図書館 | 50 | 0 | 25 | 25 | 0 |
初期値の決め方は下記を参照しました。
この中で使うのは技能と合計値のカラムです。これを以下のような感じでpythonから取得します。
def get_gs(): scopes = ['https://www.googleapis.com/auth/spreadsheets'] json_file = './hoge.json'#OAuth用クライアントIDの作成でダウンロードしたjsonファイル credentials = ServiceAccountCredentials.from_json_keyfile_name(json_file, scopes=scopes) http_auth = credentials.authorize(Http()) # スプレッドシート用クライアントの準備 doc_id = 'doc_id'#これはスプレッドシートのURLのうちhttps://docs.google.com/spreadsheets/d/以下の部分です gs = gspread.authorize(credentials) gfile = gs.open_by_key(doc_id) #読み書きするgoogle spreadsheet return gfile
ユーザーごとにでシートを切り替えられるようにします。シート名はなんでも良いのですが、私はdiscordから自動でシートを切り替えられるようにdiscordのIDをシート名にしています。
def get_charactor(sheet_name): gfile = get_gs() worksheet = gfile.worksheet(sheet_name) charactor = {} #技能名カラム cell_keys = worksheet.col_values(1) #合計値カラム cell_values = worksheet.col_values(2) for k,v in zip(cell_keys, cell_values): charactor[k] = v return charactor
ここまででスプレッドシートの設定は終了です。
Discord側のBOTを作る
次にDiscordの設定をしていきます。
Bot作成
DiscordのBOTの作り方はこちらを参考にさせていただきました
- DiscordでBOT作成メモ w/ discord.py
- PythonでDiscord botを作る 【discord.py解説】
- DiscordのBotをPythonで作ってみた
- Pythonで実用Discord bot(discord.py解説)
メッセージを取る
discordのメッセージ欄に特定の入力があったらダイスを振るようにします。入力形式は以下の仕様にします。ダイスを振るトリガーはdice
にします。
dice 1d100 技能名
これで受け取ってメッセージを返す待機botの処理を実装します。
client = discord.Client() client_id = conf['client_id'] @client.event async def on_ready(): print('Logged in') print('-----') @client.event async def on_message(message): # 開始ワード if message.content.startswith('dice'): # 送り主がBotではないか if client.user != message.author: info = parse('dice {}d{} {}', message.content) if info: if info[1].isdecimal() and info[0].isdecimal(): dice_num = int(info[0]) dice_size = int(info[1]) key = info[2] # メッセージを書きます m = message.author.name + ' ' if key == '一時的狂気': m = temp_madness() elif key == '不定の狂気': m = ind_madness() elif key == 'dice': m = simple_dice(dice_size, dice_num) else: chara = get_charactor(str(message.author)) msg, result = judge(chara, key, dice_size, dice_num) m += msg if result: d = damage(chara, key) else: d = None if d is not None: m += '\nダメージ: ' + str(np.sum(d)) + ' = ' + str(d) # メッセージが送られてきたチャンネルへメッセージを送ります await client.send_message(message.channel, m) client.run(client_id)
ダイス
ダイス振る部分を実装します。単純に入力したダイスを振るものと成否判定をするものの2種類用意します。
まず、1〜dice_size
までの一様整数乱数を1つ生成します。
def dice(dice_size): num = np.random.randint(1, int(dice_size)) return num
単純にダイスを振る場合は次のようにしています。上記のダイスをdice_num
回分振ります。2d6
なら1d6
のダイスを2回振っています。あとでメッセージ表示の際に個別のダイス結果も見たいのでダイス結果はnumpy.array
にしています。合計値はnp.sum
で計算します。msg
はdiscordに返すメッセージです。
def simple_dice(dice_size, dice_num): dice_val = np.array([], dtype=np.int64) for i in range(dice_num): dice_val = np.append(dice_val, dice(dice_size)) msg = 'dice: ' + str(np.sum(dice_val)) + ' = ' + str(dice_val) return msg
discord上ではこんな感じになります。
成否判定をするときもsimple_dice
をベースに実装します。
成否判定
スプレッドシートから引っ張ってきた情報を参照して成否判定をさせます。 取得したスプレッドシートの情報は辞書型にして持たせておきます。
charactor = { 'HP': 9, 'MP': 7, 'SAN': 60, 'こぶし': 50, '図書館': 50 }
と言っても、インスタンス生成とかしているわけではないので、判定のたびにスプレッドシートからデータを参照しているので、たぶん非効率的です。
処理の流れは
- 入力メッセージからダイスのサイズと数を取得する
- ダイスを振ってダイス値を取得する
- 技能値をダイス値を比較する
といった感じです。ちなみに卓ルールで5以下でクリティカル、96以上でファンブルにしています。return
のbool値はダメージ判定時に使用します。
def judge(charactor, key, dice_size, dice_num): dice_val = np.array([], dtype=np.int64) for i in range(dice_num): dice_val = np.append(dice_val, dice(dice_size)) if int(charactor[key]) >= np.sum(dice_val): msg = key + ' ' + str(charactor[key]) + ' >= ' + str(np.sum(dice_val)) + ' = ' + str(dice_val) if np.sum(dice_val) <= 5: msg += ' 【クリティカル】' msg += ' Success' return msg, True else: msg = key + ' ' + str(charactor[key]) + ' < ' + str(np.sum(dice_val)) + ' = ' + str(dice_val) if np.sum(dice_val) >= 96: msg += ' 【ファンブル】' msg += ' Fail' return msg, False
discord上ではこのように見えます。
ダメージ判定
特定の技能名
で技能判定が成功した際に自動でダメージロールを振るようにしました。トリガーとなる技能名
はあらかじめダメージがわかっているこぶし,頭突き,キックだけに絞って実装します。マーシャルアーツは考慮していません。
ダメージボーナスがあればマイナスも含めて追加しています。
def damage(charactor, key): d = np.array([], dtype=np.int64) if key == 'こぶし': d = np.append(d, dice(3)) elif key == '頭突き': d = np.append(d, dice(4)) elif key == 'キック': d = np.append(d, dice(6)) else: return None if 'd' in charactor['db']: result = parse('{}d{}', charactor['db']) dice_size = int(result[1]) dice_num = int(result[0]) for i in range(np.abs(dice_num)): if dice_num < 0: d = np.append(d, -dice(dice_size)) else: d = np.append(d, dice(dice_size)) return d
- 成功
- 失敗
- ダメージボーナスあり
- ダメージボーナスあり(マイナス)
狂気表
discord上で次のように入力した場合に狂気票を振るようにします。
dice 1d10 狂気の種類
狂気の種類
は一時的発狂
と不定の狂気
の2種類が振れます。1d10
はフォーマットの統一の為につけていますが、実質使っていません。
一瞬、狂気表もスプレッドシートに書こうかと思いましたが、処理速度をあげる為にハードコーディングしてます(正直、面倒臭かったので)。
def temp_madness(): roll = {} roll[1] = '鸚鵡返し(誰かの動作・発言を真似することしか出来なくなる)' #(中略) roll[20] = '過信(自分を全能と信じて、どんなことでもしてしまう)' msg = roll[dice(20)] msg += '\n一時的狂気(' + str(dice(10)+4) + 'ラウンドまたは' + str(dice(6)*10+30) + '分)' return msg def ind_madness(): roll = {} roll[1] = '失語症(言葉を使う技能が使えなくなる)' #(中略) roll[10] = '殺人癖(誰彼構わず殺そうとする) ' msg = roll[dice(10)] msg += '\n不定の狂気(' + str(dice(10)*10) + '時間)' return msg
- 一時的狂気
- 不定の狂気
Herokuにデプロイする
そのままではローカルでbotを起動している時しか使えないので不便です。そこでサーバーを立てて常時使えるようにします。今回はHerokuを使います。
Procfile
まずは、Heroku上で動かす実行スクリプトを作成します。今回はtrpg_bot.py
を実行するだけなので
woker: python trpg_bot.py
とします。
requirement.txt
requirement.txt
にpythonファイル内で使っているライブラリを記入します。今回は以下のライブラリを使用しています。
oauth2client httplib2 gspread discord parse numpy
デプロイ
基本的にはHerokuのアカウントを作ってCreate New AppしてHeroku Gitにしたがって進めれば良いです。
# Herokuにログインする $ heroku login # リポジトリをクローンする $ heroku git:clone -a trpg_dice_bot $ cd trpg_dice_bot
cloneしたリポジトリに作成したアプリケーションなどを格納します。
- trpg_bot.py #メインアプリケーション
- config.json #設定ファイル
- requirement.txt # さっき作ったやつ
- Procfile #さっき作ったやつ
- oauth.json #Googleスプレッドシートを作った時にダウンロードしたoauthのjsonファイルです
$ git add . $ git commit -am "make it better" $ git push heroku master
無事デプロイに成功すると
remote: Verifying deploy... done.
のようなメッセージが表示されます。 デプロイした時点ではまだdiscord上ではbotはオフラインのはずです。
有効にするには、Resourcesからアプリを起動すればdiscord上でbotがオンラインになるはずです。 ちなみにHerokuの無料枠は550時間です。
無事、オンラインになりました。
このあたりを参考にさせていただいています。
これで快適なTRPGライフが送れるはず。 少しでも誰かの参考になれば幸いです。
その他のリファレンス
UnityEditorのバージョン管理をしたい
本記事は関東ゲーム制作部 Advent Calendar 2018 16日目の記事です。 adventar.org
経緯
昨日12/14にUnity 2018.3がリリースされましたね。prtimes.jp個人的には長らく切望していたNested Prefab機能が実装されたと聞いて早速使ってみたいと思いました。ですが、Unity使いの皆様ならよくあると思うのですが、「Unityのバージョンアップすると過去のが動かなくなるから上げたくないんだよなぁ...」ということがあるかと思います。デフォルトではUnityは1環境に1バージョンしか存在できないっぽい?ので、特に共同開発なんかしていると周りと足並みそろえて変更する必要があると思います。Pythonであれば、pyenvとかで複数バージョンを共存させられるけど、Unityでもそういったことできないのかな~と思っていたら2通りのやり方を見つけたので、誰かの役に立てばと思い、メモしておきます。
UnityHubを使う
私はこの記事を書くにあたって知りましたが、こちらを使っている人は割といるのではないでしょうか?UnityHubの特徴はこちらの記事にわかりやすくまとめられています。
qiita.com
要点だけ抜粋すると
- 複数のバージョンを管理できる
- Componentを追加インストールできる
- ver.2017以降であればUnityHub経由でインストールできる。それ以前もローカルにあれば、追加できる
- 同一バージョンのUnityEditorを同時に起動できる
あたりでしょうか。これを使えば、プロジェクトでは古いバージョンを使っているけど、自分の環境では新しいバージョンを試したいなんてときに便利だと思います。手っ取り早く使いたいならこっちを使うのがおすすめです。
Unity on Docker
ここからが本命かつマイナーなお話になります。最近、バージョン管理や環境を汚さないようにDocker上でいろいろやろうという話を聞きます。かくいう私もPythonの環境とかはDocker上で作ったりしています。そこで、最近流行りのDockerを使ってUnityを動かせないかと思いました。結論から言うとおすすめしません。それでも、供養の意味も込めて検討記録ということで長くはなりますが、それでもいいよという心優しい方はお付き合いください。
Docker自体のインストールも書いているといよいよ長くなりすぎるので、ここでは割愛します。私はWindows10なので、Docker for Windowsを使うためにこの辺りを参考にしました。 qiita.com ついでにGUIでコンテナ管理したいのでKitematicも入れています。
Dockerのインストールには成功しました。いよいよ、Unityのコンテナを用意します。といっても私はdocker-compose up -d
程度しか使えないので、誰かいい感じのDockerfile
なりdocker-compose.yml
など提供してくれてないかなぁと検索したところ、ありました。
github.com
探せばいるもんですね。先人は偉大です。しかもReadMeを読んだところなにも考えずにdocker-compose up -d
をと唱えれば良いと書かれているもんだからありがたい限りです。ということで、早速コマンドプロンプトを起動して、リポジトリをcloneしてきて、Dokerfile
があるディレクトリまで移動して、実行します。
※ここからは回線状況にもよりますが、作業に2~3時間程度要します。また、ディスク容量に4GB程度の空きを作っておく必要があります。
$docker-compose up -d
と叩くと、Dockerfile
に従ってインストールが始まります。時間がかかりそうだったので、放置していたら、タイムアウトの関係でいくつかのインストールに失敗していたのと、share directoryにのアクセスに失敗しました的なメッセージが出て止まっていました。同時にポップアップでshare directoryのアクセスを許可しますか?のようなものが出ていたので、OKを押して再インストール。うまくいかない場合は、一度インストールしたイメージ類をすべて消してから再度実行するとうまくいくと思います。
$docker image list #ダウンロードしたイメージ一覧を表示 $docker rmi *** #***で指定したイメージを削除
当該のイメージが見つかりませんでしたのようなメッセージが出た場合はタグをつけて削除するとうまくいくと思います。 www.memotansu.jp
これでコンテナのダウンロードまでは成功したようなのですが、コンテナの起動がうまくいかない。メッセージにリビルドしてねと出ていたので
$docker-compose up --build
を実行。概ね上手くいったぽいけど、すると今度は、
standard_init_linux.go:190: exec user process caused "no such file or directory"
Windows10を使っているのですが、どうやらリポジトリからcloneしてきたときに、改行コードがCRLF
に変換されて落としてきています。ですが、コンテナ自体はUbuntu
だからかLF
でないと動かないようです。
qiita.com
なので、テキストエディタで改行コードをLF
に変更して再度
$docker-compose up --build
を実行するとようやくUnityのコンテナが起動!!ゴールは近いです。提供ページに書かれている通り、VNC ViewerでVM内に入ってくださいとのとこなので、お好きなVNC Viewerを使ってコンテナ内に入ります。私は今回はUltra VNCを使いました。forest.watch.impress.co.jp www.atmarkit.co.jp辺りを参考にしながら、インストールの設定は基本デフォルトで、環境変数に登録とデスクトップにショートカットアイコンを作る的なものだけチェックを入れてインストール。さーて、IPアドレスを打っていざConnectと思ったのですが、またコケました。今度は何だと思ってKitematicのログを見ると、
Creating unity-service ... done Attaching to unity-service unity-service | stored passwd in file: /home/adminuser/.vnc/passwd unity-service | 16/12/2018 07:00:26 -usepw: found /home/adminuser/.vnc/passwd unity-service | 16/12/2018 07:00:26 x11vnc version: 0.9.13 lastmod: 2011-08-10 pid: 8 unity-service | 16/12/2018 07:00:26 unity-service | 16/12/2018 07:00:26 wait_for_client: WAIT:cmd=FINDCREATEDISPLAY-Xvfb unity-service | 16/12/2018 07:00:26 unity-service | 16/12/2018 07:00:26 initialize_screen: fb_depth/fb_bpp/fb_Bpl 24/32/2560 unity-service | 16/12/2018 07:00:26 unity-service | 16/12/2018 07:00:26 Autoprobing TCP port unity-service | 16/12/2018 07:00:26 Autoprobing selected TCP port 5900 unity-service | 16/12/2018 07:00:26 Autoprobing TCP6 port unity-service | 16/12/2018 07:00:26 Autoprobing selected TCP6 port 5900 unity-service | 16/12/2018 07:00:26 listen6: bind: Address already in use unity-service | 16/12/2018 07:00:26 Not listening on IPv6 interface. unity-service | 16/12/2018 07:00:26 unity-service | unity-service | The VNC desktop is: 767f1c3f2d95:0 unity-service | PORT=5900 unity-service | 16/12/2018 07:05:11 Got connection from client 172.16.250.1 unity-service | 16/12/2018 07:05:11 other clients: unity-service | 16/12/2018 07:05:11 webSocketsHandshake: unknown connection error unity-service | 16/12/2018 07:05:11 Client 172.16.250.1 gone unity-service | 16/12/2018 07:05:11 Statistics events Transmit/ RawEquiv ( saved) unity-service | 16/12/2018 07:05:11 TOTALS : 0 | 0/ 0 ( 0.0%) unity-service | 16/12/2018 07:05:11 Statistics events Received/ RawEquiv ( saved) unity-service | 16/12/2018 07:05:11 TOTALS : 0 | 0/ 0 ( 0.0%) unity-service exited with code 137
的なメッセージが出ていました。どうやらVNCアクセスに失敗していたのかよくわかりませんが、VNC Viewerを立ち上げる前にUnityのコンテナを立ち上げたのがいけなかったぽいので、一度コンテナをdown
させてからVNC Viewerを立ち上げた後に、再度
$docker-compose up --build
を実行すると、
Creating unity-service ... done Attaching to unity-service unity-service | stored passwd in file: /home/adminuser/.vnc/passwd unity-service | 16/12/2018 07:24:51 -usepw: found /home/adminuser/.vnc/passwd unity-service | 16/12/2018 07:24:51 x11vnc version: 0.9.13 lastmod: 2011-08-10 pid: 8 unity-service | 16/12/2018 07:24:51 unity-service | 16/12/2018 07:24:51 wait_for_client: WAIT:cmd=FINDCREATEDISPLAY-Xvfb unity-service | 16/12/2018 07:24:51 unity-service | 16/12/2018 07:24:51 initialize_screen: fb_depth/fb_bpp/fb_Bpl 24/32/2560 unity-service | 16/12/2018 07:24:51 unity-service | 16/12/2018 07:24:51 Autoprobing TCP port unity-service | 16/12/2018 07:24:51 Autoprobing selected TCP port 5900 unity-service | 16/12/2018 07:24:51 Autoprobing TCP6 port unity-service | 16/12/2018 07:24:51 Autoprobing selected TCP6 port 5900 unity-service | 16/12/2018 07:24:51 listen6: bind: Address already in use unity-service | 16/12/2018 07:24:51 Not listening on IPv6 interface. unity-service | 16/12/2018 07:24:51 unity-service | unity-service | The VNC desktop is: 6afa1c440a08:0 unity-service | PORT=5900
となり、Connection Errorは解消されたっぽい。ということで、ローカルホストにVNCServer: 127.0.0.1
でアクセスすると、パスワードを聞かれました。提供元のReadMeに書かれていた通り、パスワードpass123
を入力すると、無事、コンテナ内に入れました。こんな感じのコンソール画面だけが表示された画面が見えるはずです。で、肝心のUnity
はどこにあるのかというと、/opt/Unity/Editor/Unity
にあるとのことですが、デフォルトのディレクトリが/opt/Unity/Editor
なので、その場でUnity
を実行すれば起動できました。
adminuser@6afa1c440a08:~$cd Unity
あとは画面に従って通常のUnityのインストールと同様に進めていきます。
ログインもできて、さーて開発を始める準備が整ったことでしょう。新規に作るもよし、既存のプロジェクトを使うもよし。自分で作っていたプロジェクトを使いたい場合はCloudに上げておくか、ローカルであれば、cloneしてきたリポジトリ直下にあるcontextフォルダにおくように書かれていたのでお好みの箇所に置きましょう。コンテナ内からアクセスするときは、File System->context->任意のフォルダ
に入っています。
ところが、これで動くはず...あれ?一瞬エディター画面が表示されてAborted
とか言われるぞ...???なんかいろいろ調べるとバージョンによってはバグで動かないっぽいことを見かけたので、Unityのバージョンをアップすることにしました。
Installing Unity 2017.1.0f2 In Docker Container
使用したバージョンはDockerfile
に書かれていた当時のlatestであるver.2017.2.0f3にしました。バージョンを変更したい場合は、お好みのバージョンのURLをコピーしてDockerfile
の71行目あたりにある.deb
ファイルを指定しているURLを変更すれば行けました。それ以外のバージョンを使いたい場合は、たぶんこちらのスレッドから対応したUnityのイメージを落としてくればいいんだと思います。スレッドのページを下に繰っていくと、古いのだと5.1から最新で2017.2.0b11まであるみたいです。どれが行けてどれが行けないまでは検証していないのでわかりません(^-^;
バージョンアップして再度dockder-compose up -d
からやり直すと無事NewProject作成で画面が表示された...?
メモリが足りないのかこれ以上まともに描画されませんでした。こんな調子なので当然、プロジェクトが重すぎたのか、2dだからダメなのか自前のプロジェクトはうまく描画できませんでした。本当はこんな感じの画面を出したかったんですが...
画像は私の所属するサークルNOELで制作したゲームです。現在は第2作目のリリースに向けて鋭意制作中です。最新情報は随時Twitterで流していきますので、よろしければフォローしてやってください。@CircleNoelGame(←単なる宣伝です。)
これでめでたくDocker上でUnityが表示できるようになりました(まともに動くとは言っていない)。ものすっごく動作がもっさりしますが、とりあえず動いたということで、今回はここらへんで力尽きてしまいました。あとは割り当てメモリとか増やせばいいんですかね?でも、私の持つ低スペックPCでは3GB割り当てるのが精いっぱいなんですよね。年内に進展があれば、時間を見つけて続きを書きたいと思います。
ちなみにUnrealEngineのdockerイメージも存在はするようです。頑張ればこっちも行けるんでしょうかね(;^_^A github.com
少しでも誰かの参考になれば幸いです。よければサークルNOELもよろしくお願いします。 Twitterはこちら@CircleNoelGame
その他のリファレンス
2018年私的読みたいアドベントカレンダー(随時更新)
ゲーム開発
その3まである。やっぱり最近ホットなのでしょうか?その2でAgent-MLのことを書いてくださる方がいるようなので楽しみです。 qiita.com qiita.com qiita.com adventar.org ↑私も投稿しました t-n-clark.hatenadiary.jp
お勉強
最近、まじめにお仕事でも使う必要が出てきたのでこの機に勉強でもし直そうかと思ってます。GAN関連の論文紹介もちらほら見かけるのでそのあたりを中心に見ておこうと思ってます。 qiita.com qiita.com qiita.com