- 投稿日:2020-11-30T23:01:53+09:00
【TypeScript】ObjectのKeyに変数でアクセスする
1. はじめに
こんにちは!みなさま楽しいプログラミンライフをお過ごしでしょうか?
最近TypeScriptでのReact開発を勉強しています。ObjectのKeyに変数でアクセスしようとしたときに、
型付けのところでハマったポイントがあったので、備忘録として残しておきたいと思います。
Version node.js 14.12.0 yarn 1.22.7 TypeScript 3.8.3 2. やりたいこと
以下のような、
object2のcategoryのvalueと、object1のkeyが一致するvalueの配列に、object2をまるっとpushしたい。object3のようになるイメージ。const object1 = { category1: [], category2: [] } const object2 = { id: 1, title: "nazeudon", category: "category1", } const object3 = { category1: [ { id: 1, title: "nazeudon", category: "category1", }, ], category2: [] }3. JavaScriptだとこう書ける
とてもシンプル。まさに。Simple is the Best.
const cat = object2.category; object1[cat].push(object2);4. ハマったポイント
同じことをTypeScriptでやろうとすると。。。
const cat = object2.category; object1[cat].push(object2);Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ category1: never[]; category2: never[]; }'.
No index signature with a parameter of type 'string' was found on type '{ category1: never[]; category2: never[]; }'.(7053)要素は暗黙のうちに 'any' 型を持っています。なぜなら、型 'string' の式は型 '{ category1: never[]; category2: never[]; }' をインデックス化するために使用できないからです。
type '{ category1: never[]; category2: never[]; }' には、型 'string' のパラメータを持つインデックスシグネチャが見つかりませんでした。そりゃ、TypeScriptだからね。型付けしろと言うわけですよね!
interface
で型付けしてやって〜、でも。。。interface OBJECT1 { category1: OBJECT2[] category2: OBJECT2[] } interface OBJECT2 { id: number title: string category: string } const object1: OBJECT1 = { category1: [], category2: [] } const object2: OBJECT2 = { id: 1, title: "nazeudon", category: "category1", } const cat = object2.category; object1[cat].push(object2);Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'OBJECT1'.
No index signature with a parameter of type 'string' was found on type 'OBJECT1'.(7053)要素は暗黙のうちに 'any' 型を持っていますが、それは 'string' 型の表現が型 'OBJECT1' のインデックスに使用できないからです。
型 'OBJECT1' の型 'string' のパラメータを持つインデックス・シグネチャは見つかりませんでした。おっと、これでもダメなのね。OBJECT1のインデックスにString型を指定できないと言われています。
ってことで、この解決に小一時間費やしたので、解決策を残しておきます。
5. TypeScriptでの書き方(例)
const cat: keyof OBJECT1 = object2.category as keyof OBJECT1; object1[cat].push(object2);こんな感じで、
cat
変数とobject2.category
を共に、OBJECT1のkeyですよ、と明示してあげれば無事動きました!!6. 最後に思ったこと
- 自分のわかっている知識の範囲だと、型付けはバグを防げて便利だなと言う印象。
- 自分のわからない範囲の知識が必要で、型に起因するエラーが出ると、型付けめんどくさ!ってなる。
- 結論、もっと勉強しましょう。今回も良い勉強になりました。
- 投稿日:2020-11-30T22:11:20+09:00
俺のNode.jsでTop-Level awaitが動かない!!なぜだ!?
事象
俺の環境でTop-Level awaitが動かない!!なぜだ!?
原因
Top-Level await
はES Module
の機能でCommon JS
は未対応なので動かない。The await keyword may be used in the top level (outside of async functions) within modules as per the ECMAScript Top-Level await proposal.
訳:awaitキーワードは、ECMAScriptトップレベルawaitプロポーザルに従って、モジュール内のトップレベル(非同期関数の外部)で使用できます。
出典:Node.js v15.3.0 Documentation
Node.jsのバージョン
v14.15.1
サンプルコード
function wait(time) { return new Promise(function(resolve, reject) { setTimeout(function() { resolve(); }, time); }) } async function waitSync(time){ console.log('start timer', time); await wait(time); console.log('time out',time); } await waitSync(2000); console.log('here');エラーメッセージ
/Users/username/async/index.js:15 await waitSync(2000); ^^^^^ SyntaxError: await is only valid in async function at wrapSafe (internal/modules/cjs/loader.js:979:16) at Module._compile (internal/modules/cjs/loader.js:1027:27) at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10) at Module.load (internal/modules/cjs/loader.js:928:32) at Function.Module._load (internal/modules/cjs/loader.js:769:14) at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)対応策
パッと思いつく対応は以下の2択
- ファイルの拡張子を
.mjs
に変更するpackage.json
にtype
属性を追加して、値をmodule
にする現状
そもそも
Top-level await
のステータスはStability: 1 - Experimental
なので現状ガリガリ使うものでもない。
あくまでv14.8
でExperimental
のフラグが不要になっただけ。エラーメッセージについて
module: Improves Top-Level await error in cjs
上記PRにて改善案が実装済み
SyntaxError: Top-Level await is only supported in ESM.
とちゃんと教えてくれるようになるよ!
やったね!
- 投稿日:2020-11-30T21:50:28+09:00
Next.jsとStripe Connectでプラットフォームアプリを作る
現在参画中の案件でStripe Connectを使用して決済機能を実装する機会がありました。
今回、初めてStripeとStripe Connectを触ってみて、実際に動くものを見ながら概要がわかるようなドキュメントがあったらいいなと思ったので簡単なプロトタイプを作成し、記事にしてみました。
他の誰かの理解の助けにあれば幸いです。
これから解説する以下のアプリケーションはStripeのテストモードを使用しています。クレジットカード番号やそのほかの入力情報はテスト用のデータを使用してください。
https://stripe.com/docs/testing
https://stripe.com/docs/connect/testingやったこと
Stripe ConnectのExpressモードでプラットフォームアプリのプロトタイプを作りました
https://stripe.com/ja-us/connect/use-casesコードはこちら
作成したものはこちら
顧客ができること
- クレジットカードをプラットフォームに登録する
- 登録したクレジットカードで複数店舗の支払いを行う
店舗ができること
- 顧客からクレジットカードでの支払いを受け付けられる
- 売上代金を受け取るための銀行口座を登録できる
プラットフォームができること
- 店舗ごとの売上をみれる
実装の手順
以下の順番で実装しました。
- Stripeの登録・設定
- 店舗側の実装
- 顧客側の実装
環境
インフラ:vercel
フロント:Next.js(10.0.0)
APIサーバー:Node.js(Next.jsのAPI Routes)実装
1. Stripeの登録・設定
まずはStripeに登録し、Stripe Connectの設定をする。
上記の設定画面でプラットフォームのサービス名やアイコンなどを指定することで、
店舗のアカウント情報を登録できるようになる。2. 店舗側の実装
Stripe Connectの設定が完了したら店舗用の銀行口座を登録できるようにする。
Stripe ConnectのアカウントタイプはStandard, Express, Customの3種類が存在するが、今回はExpressモードを使用します。やりたいこと
店舗側の実装でやりたいことは以下の4つです。
① Stripeが用意した口座登録用のページに遷移できるようにする
② 登録完了ページを表示できる
③ プラットフォーム実装者のStripeダッシュボードで店舗のアカウント情報を確認できる
④ 店舗管理者用のダッシュボードを表示できるExpress用のStripe Connectのドキュメントを参考に実装しました。
https://stripe.com/docs/connect/express-accounts見た目と実装
① ~ ④の見た目と実装を順に説明していきます。
① Stripeが用意した口座登録用のページに遷移できるようにする
見た目
実装
APIサーバー側
フロント側に口座登録用のURLを返すAPIを作成する。/pages/api/create-connect-account.jsimport stripe from '../../lib/stripe' export default async (req, res) => { try { // Stripe用の connected accountを作成する // このタイミングでアカウントのタイプを選択する(今回は'express') const account = await stripe.accounts.create({ type: 'express', country: 'JP', }) // 作成したconnected accountのidから口座登録用のURLを発行する。 const origin = process.env.NODE_ENV === 'development' ? `http://${req.headers.host}` : `https://${req.headers.host}` const accountLinkURL = await generateAccountLink(account.id, origin) res.statusCode = 200 res.json({ url: accountLinkURL }) } catch (err) { res.status(500).send({ error: err.message }); } } function generateAccountLink(accountID, origin) { return stripe.accountLinks.create({ type: "account_onboarding", account: accountID, refresh_url: `${origin}/onboard-user/refresh`, return_url: `${origin}/success`, }).then((link) => link.url); }フロント側
上記のAPIサーバーから口座登録用のURL取得し、遷移させる。pages/owner/register.jsimport { useRouter } from 'next/router' import Layout from '../../component/Layout' import styles from '../../styles/Home.module.css' import { POST } from '../../lib/axios' const RegisterPage = () => { const router = useRouter() const getSetLink = async () => { const result = await POST('/api/create-connect-account', { name: 'test', email: 'test@mail.com'}) await router.push(result.url) } return ( <Layout> <main className={styles.main}> <h2>店舗オーナー用のメニュー</h2> <div className={styles.grid}> <div className={styles.card} onClick={() => getSetLink()}> <p>店舗の銀行口座を登録する</p> </div> </div> </main> </Layout> ) } export const getServerSideProps = async () => { return { props: {} } } export default RegisterPage② 登録完了ページを表示できる
見た目
実装
①のAPIサーバーで指定したreturn_urlに一致するようにページを作成します。
フロント側pages/success.jsimport Head from 'next/head' import styles from '../styles/Home.module.css' export default function Home() { return ( <div className={styles.container}> <Head> <title>Create Next App</title> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <h1 className={styles.title}> Success!! </h1> <div className={styles.grid}> <a href="/" className={styles.card}> <h3>stripeの登録が完了しました。</h3> <p>Topへ戻る</p> </a> </div> </main> </div> ) }③ プラットフォーム実装者のStripeダッシュボードで店舗のアカウント情報を確認できる
②まで完了すると、プラットフォーム管理者のダッシュボードで店舗のアカウントを確認できるようになります。
見た目
④ 店舗用のダッシュボードを表示できる
②まで完了した店舗担当者は、プラットフォーム管理者とは別のダッシュボードで自身の店舗の口座情報を確認できるようになります。
見た目
実装
店舗担当者用のダッシュボードも口座登録時と同様に、Stripeから発行されたURLでアクセスが可能となります。
なので、Stripeのライブラリを使用してURLを取得します。フロント側
pages/owner/shop/[id].jsimport styles from '../../../styles/Home.module.css' import stripe from '../../../lib/stripe' import Layout from '../../../component/Layout' const RegisterPage = (props) => { return ( <Layout> <main className={styles.main}> <h2>店舗画面</h2> <div className={styles.grid}> <a href={props.loginLinkUrl} className={styles.card}> <h3>店舗の口座情報を確認する</h3> </a> </div> </main> </Layout> ) } export const getServerSideProps = async (ctx) => { const accountId = ctx.query.id const loginLink = await stripe.accounts.createLoginLink(accountId) return { props: { loginLinkUrl: loginLink.url, shopId: ctx.query.id } } } export default RegisterPage3. 顧客側の実装
次は顧客用にクレジットカードを登録できるようにして、決済できるようにします。
やりたいこと
店舗側の実装でやりたいことは以下の2つです。
① クレジットカードを登録できる
② 店舗毎に登録したクレジットカードで決済できる参考にしたstripeのドキュメントは
① => https://stripe.com/docs/payments/save-and-reuse
② => https://stripe.com/docs/payments/payment-methods/connect#cloning-payment-methods
です見た目と実装
①クレジットカードを登録できる
見た目
Stripeが提供しているinput formを使用してStripe側にクレジットカード情報を登録します。
実装
APIサーバー側
APIサーバー側で行うことは以下の3つです。
- Stripeに顧客のアカウント情報を登録する
- クレジットカード登録用のセットアップを行う
- フロント側にclient_secretを渡す
client_secretとはフロント側でクレジットカードの情報をstripeに送る際に必要となるキーです。
pages/api/register-customer.jsimport stripe from '../../lib/stripe' export default async (req, res) => { try { const customerName = req.body.customerName // Stripeに顧客のアカウント情報を登録する const customer = await stripe.customers.create({ name: customerName }) // クレジットカード登録用のセットアップを行う const setupIntent = await stripe.setupIntents.create({ payment_method_types: ['card'], customer: customer.id }); // フロント側にclient_secretを渡す res.statusCode = 201 res.json({ id: customer.id, name: customer.name, client_secret: setupIntent.client_secret }) } catch (err) { console.error(err) res.status(500).send({ error: err.message }); } }フロント側
フロント側で行うことは以下の3つです
- Stripe用の入力フォーム('@stripe/react-stripe-js')のセットアップ
- APIサーバー側に問い合わせてclient_secretを取得する
- client_secretを使用して入力フォームで受け取ったクレジットカード情報をStripeへ送付する
pages/customer/register.js クレジットカード登録画面import * as React from 'react' import { Elements } from '@stripe/react-stripe-js' import stripePromise from '../../lib/loadStripe' import { CustomerContext } from '../../context/CustomerContext' import { POST } from '../../lib/axios' import Layout from '../../component/Layout' import styles from '../../styles/Home.module.css' import CardInputForm from '../../component/CardInputForm' const RegisterPage = () => { const { customerState, customerSetter } = React.useContext(CustomerContext) const [name, setName] = React.useState('名無しさん') const [loading, setLoading] = React.useState(false) const registerCustomer = async (e) => { e.preventDefault() setLoading(true) const result = await POST('/api/register-customer', { customerName: name }) customerSetter({ name: result.name, id: result.id, client_secret: result.client_secret }) setLoading(false) } return ( <Layout> <main className={styles.main}> {customerState.client_secret ? ( <div> <h4>こちら↓からクレジットカードを登録してください</h4> <p>**テスト用の番号 "4242424242424242" を使用してください**</p> {loading ? ( '登録中...' ):( <Elements stripe={stripePromise}> <CardInputForm clientSecret={customerState.client_secret} customerName={customerState.name}/> </Elements > )} </div> ) : ( <div> <h4>お客様のお名前を登録してください</h4> <form onSubmit={(e) => registerCustomer(e)}> <input type="text" defaultValue={name} onChange={(e) => setName(e.target.value)}></input> <button>名前を登録する</button> </form> </div> )} </main> </Layout> ) } export const getServerSideProps = async () => { return { props: { } } } export default RegisterPagelib/loadStripe.jsimport {loadStripe} from '@stripe/stripe-js'; // Stripeにクレカ情報をPOSTするためのライプラリの設定 const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ); export default stripePromisecomponent/CardInputForm.js カード番号入力フォームimport * as React from 'react' import {useStripe, useElements, CardElement} from '@stripe/react-stripe-js' const CardInputForm = (props) => { const stripe = useStripe() const elements = useElements() const [loading, setLoading] = React.useState(false) const [message, setMessage] = React.useState('登録する') // カードの登録処理 const handleSubmit = async (event) => { event.preventDefault() setLoading(true) setMessage('登録中。。。') if (!stripe || !elements) { return; } // APIサーバー側から受け取ったclient_secretを使用してStripeへカード情報を送付する const result = await stripe.confirmCardSetup(props.clientSecret, { payment_method: { card: elements.getElement(CardElement), billing_details: { name: props.customerName, }, } }); if (result.error) { setMessage('失敗しました') } else { setMessage('完了しました') } setLoading(true) } return ( <form onSubmit={handleSubmit}> <CardElement /> <button disabled={!stripe || loading}>{message}</button> </form> ) } export default CardInputForm上記のフォームで顧客の登録とクレジットカードの登録が完了すると
プラットフォームのStripeアカウントのダッシュボードから顧客のデータが確認できるようになります。② 店舗毎に登録したクレジットカードで決済できる
①で登録したクレジットカードを使用して決済できるように実装します。
見た目
実装
APIサーバー側
上記の①クレジットカードを登録できる
が完了した状態では、クレジットカードの登録はできた状態ですが、
顧客が店舗に対して支払いを行うためには、このドキュメントでいうと
- connected account(店舗)
- customer(顧客)
- payment method(カード情報)
の3つを紐づける必要があります。
なので、商品の注文に対して
店舗 - 顧客 - カード情報
の紐付けと、
支払いのセットアップを
APIサーバー側で行います。pages/api/shop/[id]/buy.jsimport stripe from '../../../../lib/stripe' export default async (req, res) => { try { // フロントからPOSTされた商品データ const item = req.body.item const stripeConnectedAccountId = req.query.id const customerId = req.body.customer_id // 顧客のカードの登録情報を取得(複数のカードが登録されている場合は、複数件のカード情報をする) const paymentMethodData = await stripe.paymentMethods.list({ customer: customerId, type: 'card', }); // 店舗毎(stripeConnectedAccountId)にクレジットカード情報(payment_method)を複製 const clonedPaymentMethod = await stripe.paymentMethods.create({ customer: customerId, payment_method: paymentMethodData.data[0].id, }, { stripeAccount: stripeConnectedAccountId, }); // 店舗毎(stripeConnectedAccountId)に顧客情報を複製(customer))を複製 const clonedCustomer = await stripe.customers.create({ payment_method: clonedPaymentMethod.id, }, { stripeAccount: stripeConnectedAccountId, }) // 上記の複製したpayment_methodとaccountを使用し、支払いのためのセットアップを行う const paymentIntent = await stripe.paymentIntents.create({ amount: item.price, currency: 'jpy', payment_method_types: ['card'], payment_method: clonedPaymentMethod.id, customer: clonedCustomer.id, description: `${item.name}の購入代金`, metadata: {'name': item.name, 'price': item.price} }, { stripeAccount: stripeConnectedAccountId, }); // 支払い処理自体はブラウザから行う必要があるため、決済に必要なキー(client_secret)をフロントに渡す res.statusCode = 201 res.json({ client_secret: paymentIntent.client_secret }) } catch (err) { console.error(err) res.status(500).send({ error: err.message }); } }フロント側
- 決済用のstripe.jsを店舗(connected account)用にセットアップする(
loadStripe
)- 商品のデータをAPIサーバーに渡して, client_secretを受け取る
- client_secretを使用して、Stripeへ決済情報をPOSTする
pages/customer/shop/[id].js 商品一覧ページimport * as React from 'react' import {Elements} from '@stripe/react-stripe-js'; import {loadStripe} from '@stripe/stripe-js'; import { CustomerContext } from '../../../context/CustomerContext' import Layout from '../../../component/Layout' import styles from '../../../styles/Home.module.css' import CheckoutForm from '../../../component/CheckoutForm' const RegisterPage = (props) => { const { customerState } = React.useContext(CustomerContext) const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, { stripeAccount: props.shopId} ) return ( <Layout> <main className={styles.main}> <h2>商品一覧</h2> <Elements stripe={stripePromise}> <div className={styles.grid}> {props.itemList.map((item, index) => <div className={styles.card} key={index}> <CheckoutForm item={item} customerId={customerState.id} shopId={props.shopId} /> </div> )} </div> </Elements> </main> </Layout> ) } export const getServerSideProps = async (ctx) => { const itemList = [ { name: 'キノコのかさ', price: 100, }, { name: 'キノコのスツール', price: 200 }, { name: 'キノコのかべがみ', price: 300 }, ] return { props: { itemList: itemList, shopId: ctx.query.id } } } export default RegisterPagepagescomponent/CheckoutForm.jsimport {useStripe} from '@stripe/react-stripe-js'; import { POST } from '../lib/axios' import * as React from 'react' import styles from '../styles/Home.module.css' const CheckoutForm = (props) => { const [message, setMessage] = React.useState() const stripe = useStripe() const handleSubmit = async () => { setMessage('処理中。。。') const result = await POST(`/api/shop/${props.shopId}/buy`, { customer_id: props.customerId, item: props.item }) const confirm_result = window.confirm('選択した商品を購入します。よろしいですか?'); if (confirm_result) { const paymentResult = await stripe.confirmCardPayment(result.client_secret) if (paymentResult.error) { setMessage('失敗しました') } else { setMessage('購入しました') } } else { setMessage('') } } return ( <div onClick={() => handleSubmit()}> <h3>{props.item.name}</h3> <div>¥{props.item.price}</div> {message && ( <div className={styles.title}>{message}</div> )} </div> ) } export default CheckoutFormブラウザで
stripe.confirmCardPayment()
の実行が成功すると、
プラットフォーム実装者のダッシュボードで店舗の売り上げが確認できるようになります。
- 投稿日:2020-11-30T20:53:22+09:00
【NeosVR】LogiX+Node.jsでWebSocketを使ってみる
この記事は「NeosVR reso Advent Calendar 2020」および「NeosVR Advent Calendar 2020」の21日目です。
自己紹介と記事について
数か月前からNeosVRを始めた初心者です。テトリスばかりやっていますが、最近LogiXの勉強も始めたので、まずは簡単なWebSocketの使い方を調べてまとめてみることにしました。
準備(NeosVR側)
NeosVRではWebSocketClientが用意されており、VR空間からWebSocketを使ったプログラミングも可能です。
チュートリアルワールドに行く
ワールド一覧から「チュートリアル」を選択し「Websocket Example」のセッションをスタート。
基本的な組み方は網羅されていたため、今回はこのLogiXをほぼそのまま使います。
処理の説明
真ん中あたりのWebSocketClientを中心として、各LogiXノード群がつながっています。
1) WebSocketの接続を開始する(Pulse
が流れると指定されたURLに接続する)
2) WebSocketの接続が成功した時の処理
3) サーバにメッセージを送信する
4) サーバから受信したメッセージをString型で書き出す自分でLogiXを組む場合
任意のオブジェクトのインスペクタより[Attach Component]->[Network]->[WebsocketClient]を追加。
関連するLogiXノードはノードブラウザの[Network]->[Websocket]にあります。準備(Node.js側)
WebSocket通信を行うサーバを用意します。
環境
Windows10
Package Version Explanation Node.js 12.19.0 JavaScriptでいろいろできるやつ ws 7.4.0 WebSocketを使うやつ
Socket.ioもありますが今回はこちらでやりますパッケージを追加
npm install ws
WebSocketサーバを立てる
今回はメッセージを受信すると接続しているクライアント全てにメッセージをそのままブロードキャストするようにします。
app.jsconst server = require("ws").Server; const ws = new server({ port: 443 }); ws.on("connection", socket => { console.log("connected!"); socket.on("message", ms => { console.log(ms); ws.clients.forEach(client => { client.send(ms); }); }); socket.on("close", () => { console.log("good bye."); }); });node app.jsWebSocketクライアントを追加する
NeosVRの外にクライアントを用意します。今回はChromeの開発者ツールのConsoleで実行します。
xxxxxx
の部分は同じPCで実行しているならlocalhost
となります。const conn = new WebSocket('ws://xxxxxx:443'); conn.onmessage = (m) => { console.log(m.data) };NeosVRの中と外でやり取りしてみる
まずはHello World
1) のURLを
ws://echo.websocket.org
から、自分で立てたサーバに変更してPulse
を押します。
サーバ側のコンソールにconnected!
と表示されれば接続は成功です。NeosVRが遊べるPCとNode.jsを入れているPCが別だったので、LAN内のIPアドレスにしています。
(アクセスできるんですね。。。)
マルチプレイの場合のスコープは未確認ですが、また調べてみようと思います。
3) に何かメッセージを入れて送信してみます。
サーバ側のコンソールと、追加したWebSocketクライアントに入力したメッセージが表示されます。
Hello NeosVR!今度はNeosVRの外のWebSocketクライアント(Chrome)からメッセージを送信してみます。
conn.send('Hello NeosVR! (Chrome)');4) に送信されたメッセージが表示されます。
簡単なテキストメッセージですが、NeosVR内と外でWebSocket通信をすることができました。NeosVR内の座標を取得する/NeosVR外から座標を操作する
チュートリアルワールドの奥に行きます。
5) 近くのユーザの座標を取得してWebSocketのメッセージとして送信する
6) 受信したメッセージが座標としてParseできればオブジェクトの座標を変更する奥にあった球のオブジェクトが自分のいた場所にワープしてきました。
サーバ側のコンソールを見るとメッセージの内容がわかります。
この形式でXYZの座標を指定してやれば、NeosVRの外から球の位置を操作できそうです。[1.409628; 1.852089; -5.665314]0.0.0の位置を指定すると、
conn.send('[0; 0; 0]');ワールドの中央に飛んでいきます。
Y座標に10を指定すると、conn.send('[0; 10; 0]');おわり
WebSocketを使えるようになると、VR空間と現実を連携して色々おもしろいことができそうです。
今回の記事の内容はデスクトップモード(VRHMDなし)でもできますので、NeosVRが気になっている方は試してみてはいかがでしょうか。
よければアドベントカレンダーの他の記事も覗いてみてください。
「NeosVR reso Advent Calendar 2020」
「NeosVR Advent Calendar 2020」
- 投稿日:2020-11-30T20:52:11+09:00
Node.jsでOpenCVを使おう! 〜インストール編〜
Node.js それはJavascriptであんな事やこんな事ができる素敵なプラットフォームですよね!
最近お仕事で「Node.jsでカメラ画像をとって、WebRTCに載せて発信する」という案件が出てきましたしかし、意外にNode.jsからカメラ画像を取得する用途に需要がないらしく、しっかり資料がまとまっていて扱いやすそうだったのがOpenCVくらいでした
それでも導入に詰まってしまったので備忘録がてらまとめていこうと思いますNode.jsでカメラ画像を取得する時の選択肢
あまり選択肢的には多くありません。代表格として扱われている印象の強かった2つをあげておきます。
タイトルにあるとおり、今回はOpenCVラッパーであるOpenCV4Nodejsを使用します。
準備/必要な物
インストール前に何が必要かを確認しましょう!
この記事で掲載するのは2020年11月末の物なので、もしかしたらみなさんが使うときには変わってる可能性もありますが、
そこまで大きくは違わないはず!
- node-gyp (Node.js標準のものでよいみたい)
- Python 2.7
- CMake
なお、Windowsの場合は
- windows-build-tools
も必要になります。
インストールの選択肢
Opencv4Nodejsにはインストールに対して2つの選択肢があります。
この記事では2番目の一緒にインストールコースで説明します
OpenCV本体をインストール後、Opencv4Nodejsを別にインストールする
- 他のPJとかですでにOpenCVを使ってるから別にインストールしたくない場合はこっち
- ただし、とってもめんどくさい上に問題が発生しまくりで成功しませんでした
Opencv4Nodejsにバンドルされているのを一緒にインストール
- 全自動でOpenCVのビルド等々をやってくれます。素敵
- 一部
インストール(ビルド)時に干渉する
パッケージがあります。以下のものは事前にアンインストールしておくことをお勧めします
- ffmpeg
- tesseract
- 上記2つのパッケージはOpencv4Nodejsインストール後であれば再インストール可能と思われる
Nodeプロジェクトの作成
作りましょう
mkdir test cd test npm initpackage.jsonができてれば一丁あがり♪
opencv4nodejsのインストール実行
インストールの前に
package.jsonに細工をする必要があると書かれているStackoverflowの記事とかもあるのですが
それは一緒にインストールコースでは不要です。
デフォルト設定でOpenCVの自動ビルド & それを使う様に設定されるので無視しましょう〜npm install
npm install opencv4nodejs@5.2.0コツはバージョンを指定してあげることです。5.4以上あたりからビルドに問題が出るらしくより安心できる5.2を使用しました
たくさんwarningやnoteが出て意味不明ですが、最後までいけてエラーと言われければおそらくOKです
成功しましたか?
きっと失敗します
それくらい失敗率が高いです。
このへんを参考にしてみてください。https://www.366service.com/jp/qa/cfbda42f824f4ed3c16004c872820b88
https://github.com/justadudewhohacks/opencv4nodejs/issues/645
https://github.com/justadudewhohacks/opencv4nodejs/issues/546
動作確認
インストールに成功してるのかどうかは、使ってみればわかります。
例えば以下のような簡単なプログラムをつくって数字がたくさん出てきていればOpencv4Nodejsはインストール成功していて
カメラ画像の取得ができていますapp.jsconst cv = require('opencv4nodejs'); const wCap = new cv.VideoCapture(0); setTimeout(function() { let frame = wCap.read(); console.log(frame.getDataAsArray()); }, 2000)もっと実感が欲しい方は以下のサイトを参考にしてみてください!
https://kokensha.xyz/javascript/yes-you-can-do-opencv-with-node-js-and-tensorflow/余談:OpenCVと別でインストールについて
上の方で「問題が発生しまくりで成功しませんでした」としている方のお話。
失敗した要因的には2つあります1つが「OpenCVへの参照」になぜか成功できなかったこと
package.jsonに以下の様なセクションを追加する必要があるのですが、そもそも何を設定すればいいのか正解を探し切れず、下記の様にbrewインストールディレクトリへの指定ではcore.h
が見つからない! みたいなエラーばかりで解消できませんでした
どなたか正解知ってる方いらっしゃいましたらコメントいただけると嬉しいです!package.json{ "name": "sample", "version": "1.0.0", "main": "app.js", ... "opencv4nodejs": { "disableAutoBuild": 1, <= 別にインストールコースでは1に、一緒にコースの場合は0に設定します "opencvIncludeDir": "/usr/local/opt/opencv@3/include/opencv", "opencvLibDir": "/usr/local/opt/opencv@3/lib", "opencvBinDir": "/usr/local/opt/opencv@3/bin" } }2つ目が「opencv_contrib」の存在
これも厄介です。
contribというのは特許その他ライセンス上の問題でOpenCV本体から分離された機能たちを指します。
opencv3nodejsでは、SHIFTと言うものをどこかで使っているらしくcontribのインストールが必要だった様です。ただ、私はMacのbrewコマンドからインストールしていたのでcontribも一緒に入ってるはずらしかったのですが
解決できず諦めました。。。おわり
インストール編はこれで終わりです
私はこの後さらにnode-webrtc(wrtc)のフレームワークにOpenCVから取得した画像を載せて発信すると言うところまでやっていきます
「node-webrtcから画像を発信する」と言う分野もなかなかまとまった資料がなかったので備忘録しとして投稿する予定です
その時はぜひ読んでくださいね〜
- 投稿日:2020-11-30T17:54:16+09:00
そろそろNode14へ移行せな
はじめに
- 2020/4/22にNode.js14がリリースされました。
- また、2020/10/27にNode.js14のActive LTSが開始され、2020/11/30にNode.js12のMaintenance LTSに移った為、そろそろ切り替え時かなということで、この記事を残します。
LTSってなんだ??
LTSとは、[Long Term Support]の略を指し、長期の保守運用が約束されているバージョンになります。
Current LTS
- 最新版だが、安定性を約束しないことで機能追加を盛り込んだバージョン
Active LTS
- リリースラインに適切で安定していると判断された新機能、バグ修正、および更新。
安定しているため、本番環境をアップグレードする最適な時期ともいえます
Maintenance LTS
- 重大なバグ修正とセキュリティアップデート
Node14の新機能
診断レポートの安定
- 診断レポートは、Node14の安定した機能としてリリースされています (Node12では、実験的な機能として追加されていました)
→ 診断レポート機能を使用すると、オンデマンドまたは特定のイベントが発生したときにレポートを生成できます。
- このレポートには、クラッシュ、パフォーマンスの低下、メモリリーク、高いCPU使用率、予期しないエラーなど、本番環境での問題の診断に役立つ情報が含まれています。
実行方法については、次のように[--report-on-fatalerror]を指定します。
また、例外がcatchされなかったときにレポートを出力する[--report-uncaught-exception]などがあります。node --report-on-fatalerror server.jsV8がV8 8.1にアップグレード
V8のバージョンが上がることで使用できるJavaScriptの構文や機能が増えます。
Optional Chaining
Optional chainingは、参照したオブジェクトや関数の値がundefinedやnullの可能性があっても、その値が持つプロパティに安全にアクセスすることができます。
example.js// 使用前 let nameLength; if (user && user.info && user.info.name) userName = user.info.name; // Optional Chainingを使用 const userName = user?.info?.name;Nullish Coalescing
Nullish coalescingは、参照する値がundefined または null の時、デフォルト値を取得することができます。
example.jsconst resultString = null ?? 'default'; console.log(resultString); // => default const resultNumber = 0 ?? 42; console.log(resultNumber); // => 0Intl.DisplayNames
Intl.DisplayNamesは、指定したロケールとオプションに基づいた表示名称の翻訳を取得することができます。
example.js// 国/地域コードから国名/言語名を出力する例 const languageNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'language' }); const languageNamesInFrance = new Intl.DisplayNames(['fr'], { type: 'language' }); console.log(languageNamesInEnglish.of('ja')); // => "Japanese" console.log(languageNamesInFrance.of('en-US')); // => "anglais américain"Intl.DateTimeFormatのcalendar optionとnumberingSystemオプションの有効化
Intl.DateTimeFormatのoptions引数を用いて、calendarとnumberingSystemが使えるようになりました。
example.jsconst date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); console.log(new Intl.DateTimeFormat('en-US').format(date)); // => "12/20/2012" console.log(new Intl.DateTimeFormat('en-GB').format(date)); // => "20/12/2012" console.log(new Intl.DateTimeFormat('ja', { calendar: 'japanese', numberingSystem: 'jpan', era: 'long' }).format(date)); // => "平成24年十二月二十日"実験的にAsync Local Storage APIの追加
AsyncLocalStorageは、コールバックとプロミスチェーン内に非同期状態を作成するために使用されます。
→ これにより、Webリクエストの存続期間またはその他の非同期期間を通じてデータを保存できます。これは、他の言語のスレッドローカルストレージに似ています。
Streams API全体の一貫性を向上
変更点としては、
[http.OutgoingMessage] → [stream.Writable]に一貫しました。
[net.Socket] → [stream.Duplex]に一貫しました。
変更としては以上ですが、アプリケーションに影響はないと思っています。
ES Moduleの警告を削除
ES Modulesとは
JavaScriptにおけるモジュール機能としては、下記のものがあります。
- CommonJS
- ECMAScript Modules(ES Modules)
- etc..
CommonJS
CommonJSとは、言語仕様のModules解決するために主にNodeに実装されています。
example.jsconst { test } = require("./test");ES Modules
ES Modulesとは、再利用のためにJavaScriptコードをパッケージ化するための公式の標準形式です。
example.jsimport { test } from "./test.js"じゃあどうなったの??
今までは、ES Modulesを使用する場合、以下の警告が表示されていました。
ExperimentalWarning: The ESM module loader is experimental.これが、Node.js v14 からは上記の警告は表示されなくなります。
注意点としては、あくまでまだ実験的なものであることです
- 投稿日:2020-11-30T17:54:16+09:00
そろそろNode.js 14へ移行せな (新機能まとめ)
はじめに
- 2020/4/22にNode.js14がリリースされました。
- また、2020/10/27にNode.js14のActive LTSが開始され、2020/11/30にNode.js12のMaintenance LTSに移った為、そろそろ切り替え時かなということで、この記事を残します。
LTSってなんだ??
LTSとは、[Long Term Support]の略を指し、長期の保守運用が約束されているバージョンになります。
Current LTS
- 最新版だが、安定性を約束しないことで機能追加を盛り込んだバージョン
Active LTS
- リリースラインに適切で安定していると判断された新機能、バグ修正、および更新。
安定しているため、本番環境をアップグレードする最適な時期ともいえます
Maintenance LTS
- 重大なバグ修正とセキュリティアップデート
Node14の新機能
診断レポートの安定
- 診断レポートは、Node14の安定した機能としてリリースされています (Node12では、実験的な機能として追加されていました)
→ 診断レポート機能を使用すると、オンデマンドまたは特定のイベントが発生したときにレポートを生成できます。
- このレポートには、クラッシュ、パフォーマンスの低下、メモリリーク、高いCPU使用率、予期しないエラーなど、本番環境での問題の診断に役立つ情報が含まれています。
実行方法については、次のように[--report-on-fatalerror]を指定します。
また、例外がcatchされなかったときにレポートを出力する[--report-uncaught-exception]などがあります。node --report-on-fatalerror server.jsV8がV8 8.1にアップグレード
V8のバージョンが上がることで使用できるJavaScriptの構文や機能が増えます。
Optional Chaining
Optional chainingは、参照したオブジェクトや関数の値がundefinedやnullの可能性があっても、その値が持つプロパティに安全にアクセスすることができます。
example.js// 使用前 let nameLength; if (user && user.info && user.info.name) userName = user.info.name; // Optional Chainingを使用 const userName = user?.info?.name;Nullish Coalescing
Nullish coalescingは、参照する値がundefined または null の時、デフォルト値を取得することができます。
example.jsconst resultString = null ?? 'default'; console.log(resultString); // => default const resultNumber = 0 ?? 42; console.log(resultNumber); // => 0Intl.DisplayNames
Intl.DisplayNamesは、指定したロケールとオプションに基づいた表示名称の翻訳を取得することができます。
example.js// 国/地域コードから国名/言語名を出力する例 const languageNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'language' }); const languageNamesInFrance = new Intl.DisplayNames(['fr'], { type: 'language' }); console.log(languageNamesInEnglish.of('ja')); // => "Japanese" console.log(languageNamesInFrance.of('en-US')); // => "anglais américain"Intl.DateTimeFormatのcalendar optionとnumberingSystemオプションの有効化
Intl.DateTimeFormatのoptions引数を用いて、calendarとnumberingSystemが使えるようになりました。
example.jsconst date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0)); console.log(new Intl.DateTimeFormat('en-US').format(date)); // => "12/20/2012" console.log(new Intl.DateTimeFormat('en-GB').format(date)); // => "20/12/2012" console.log(new Intl.DateTimeFormat('ja', { calendar: 'japanese', numberingSystem: 'jpan', era: 'long' }).format(date)); // => "平成24年十二月二十日"実験的にAsync Local Storage APIの追加
AsyncLocalStorageは、コールバックとプロミスチェーン内に非同期状態を作成するために使用されます。
→ これにより、Webリクエストの存続期間またはその他の非同期期間を通じてデータを保存できます。これは、他の言語のスレッドローカルストレージに似ています。
Streams API全体の一貫性を向上
変更点としては、
[http.OutgoingMessage] → [stream.Writable]に一貫しました。
[net.Socket] → [stream.Duplex]に一貫しました。
変更としては以上ですが、アプリケーションに影響はないと思っています。
ES Moduleの警告を削除
ES Modulesとは
JavaScriptにおけるモジュール機能としては、下記のものがあります。
- CommonJS
- ECMAScript Modules(ES Modules)
- etc..
CommonJS
CommonJSとは、言語仕様のModules解決するために主にNodeに実装されています。
example.jsconst { test } = require("./test");ES Modules
ES Modulesとは、再利用のためにJavaScriptコードをパッケージ化するための公式の標準形式です。
example.jsimport { test } from "./test.js"じゃあどうなったの??
今までは、ES Modulesを使用する場合、以下の警告が表示されていました。
ExperimentalWarning: The ESM module loader is experimental.これが、Node.js v14 からは上記の警告は表示されなくなります。
注意点としては、あくまでまだ実験的なものであることです
- 投稿日:2020-11-30T16:53:52+09:00
SteinでSpreadSheetからデータを複数条件(AND)でとって動的にメッセージを作成
「今日飲みに行きたい人」を募るLINE Botを作っています
タイトルを汎用的なものに言い換えると、
「DBからデータを抽出して、配列を作るよ。」
です。飲みに行く人を誘いにくい状態が続きますが、
もう少し落ち着いたら、対策はしたうえで飲みに行くこともありそうですね。
社内など、知らない人もある程度混じっているコミュニティで、
「今日飲みたい」と思ったときに誘えるLINE Botを作っています。こんなサービスを実際に誰かに動くところを見ていただきたくて、
使う人が「飲みに行きたい」とやったら、その人の名前(あらかじめ登録した)が出る。
という基本部分だけとりあえず動的に動くようにしました。今までは、固定の人に対して送るようにしていましたが、
実際に自分の名前が出たほうが楽しくなるかと思い、今回の対応をしました。ただ、飲みに行く人数は4人一組にしたかったのですよ。そこで、
このような形で、日にち・何番のグループ・何人目というデータを作りました。
// 最後に登録した人が所属するグループに入っている名前を取得して格納 let userNameList = await store.read(tranSheet, { search: { date: today, group_no: groupNo.toString() } }).then(data => { return data }).catch(e => console.log(e))今日、かつ、グループNoを指定して、そのグループにいる人たちを取得します。
抽出した人たちを宛先にセットして、let pushText; pushText += " グループの飲みに行くメンバーは、" // 同じグループの人数分ループ for(var i = 0 ; i <= userNameList.length - 1 ; i++ ){ pushText += userNameList[i].user_name + "さん、"; } pushText += "です!張り切っていきましょう!"このような形でメッセージを作ります。そのメッセージを最終的にチャンネルに登録されている方たちに絵文字を文末につけて配信。
let textLength = pushText.length; pushText += "$"; // チャンネルに登録されている方たちに連絡 await client.broadcast([{ "type": "text", "text": pushText, "emojis": [ { "index": textLength, "productId": "5ac1bfd5040ab15980c9b435", "emojiId": "002" } ] },{ type: "sticker", packageId: "11537", stickerId: "52002745" }]).then(data => console.log(data)) .catch(e => console.log(e))構成
LINEでメッセージを送ると、Node.jsで受け取って処理する。
その際、Steinを使って、SpreadSheetをDB代わりに、
append(INSERT) 飲みに行きたい人追加や名前登録
search(SELECT) 今日飲みに行きたい人のグループを抽出
delete キャンセル
update 名前の更新今回つまったポイント 複数条件の設定
Steinというサービスを使って、Google Spread Sheetをテーブルとして使ってデータを入れたり出したりしています。
複数条件指定したときに、なぜかデータが0件だったり、2つ目の条件が効いてないという現象が起きた。。
そもそものSteinのメソッドを呼び出すときの書き方の問題なのか、
複数条件を設定できない仕様なのか。(DBとしてそんなことはまずないだろうと思ってはいた。)気を付けよう。以下のポイント
・SpreadSheet とってくるときは文字列型。数値型で入れたのにな!!SpreadSheet上も数値のよう。右寄せなので。
・Node.js 数値型。次のグループNoなど足し算をしたかったので数値で取り扱っています。最初、何も気にしなかったら文字列型になっていて、2+1→21になっていた。笑反省としてJavaScript特有?の型のあいまいさをどうするか考えていく
今回の考察として、そもそものSteinの構文で、
複数条件を指定した例がなかなか見つからなかった+公式ドキュメントを見ると書いてあるが、推測通りなのかわからなかった。
(たぶんJSONみたいな指定だから、2つ目の条件はカンマで区切って書くよな・・・と理解はしていた。)そのため、別の理由で間違ってても構文が間違ってる!?と思い込んでしまっていた。
実際は、型が誤っていてデータをとれなかった。型をうまく変換できてなかったなどが理由だった。今後の方針として、文字列型なのか数値型なのかは、ちゃんと都度値が取れているかで判断するか、明示的に型指定してから扱おう。
と学んだ。
この考え方はif文の条件とかでも使えそう。「==」で判断するのか、「===」で判断するのかの違い。
みなさんどうやってるんだろう。今回全体のソースは長すぎるので、ひかえておきます。。。
- 投稿日:2020-11-30T14:53:30+09:00
node.jsとserverlessを使ってlambdaでアプリを動かす -複数の関数をlambdaにアップする-
前回までのあらすじ
node.jsとserverlessでローカル環境のコードをlambdaにアップできましたとさ
目的
ローカル環境で作成した複数の関数をLambdaにアップして動かしたい
実践
環境
node.js v12.18.2
ディレクトリ構成
ls -a . .gitignore node_modules package.json .. handler.js package-lock.json serverless.ymlpackage.jsonの確認
{ "name": "application-no-namae-desu", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "serverless": "^2.13.0" } }handler.js
'use strict'; module.exports.hello = async event => { return { statusCode: 200, body: JSON.stringify( { message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }, null, 2 ), }; // Use this code if you don't use the http event with the LAMBDA-PROXY integration // return { message: 'Go Serverless v1.0! Your function executed successfully!', event }; };前回までの状態を確認できたので、複数の関数をLambdaで使えるようにしていく。
handler.jsで複数エクスポートできるようにする
handler.js
を以下のように修正'use strict'; async function hello() { return { statusCode: 200, body: JSON.stringify( { message: "Go Serverless v1.0! Your function executed successfully!", }, null, 2 ), }; }; // bonjour関数を追加、メッセージをフランス語にする async function bonjour() { return { statusCode: 200, body: JSON.stringify( { message: "Passez à la v1.0 sans serveur! Votre fonction s'est exécutée avec succès!", }, null, 2 ), }; }; module.exports = { hello, bonjour };Lambdaの関数として使用するためには、Lambdaファンクションの設定を行わなければならない。
設定はserverless.ymlで行う。serverless.ymlの設定
service: application-no-namae-desu frameworkVersion: '2' provider: name: aws runtime: nodejs12.x functions: hello: handler: handler.hello # ここを追加 bonjour: handler: handler.bonjourbonjour関数をローカルで実行
sls invoke local --function bonjour Serverless: Running "serverless" installed locally (in service node_modules) { "statusCode": 200, "body": "{\n \"message\": \"Passez à la v1.0 sans serveur! Votre fonction s'est exécutée avec succès!\"\n}" }いけそうなので、AWSにデプロイしよう。
AWSにデプロイ
serverless deploy --region ap-northeast-1デプロイした関数を実行
sls invoke --function hello --region ap-northeast-1 Serverless: Running "serverless" installed locally (in service node_modules) { "statusCode": 200, "body": "{\n \"message\": \"Go Serverless v1.0! Your function executed successfully!\"\n}" }sls invoke --function bonjour --region ap-northeast-1 Serverless: Running "serverless" installed locally (in service node_modules) { "statusCode": 200, "body": "{\n \"message\": \"Passez à la v1.0 sans serveur! Votre fonction s'est exécutée avec succès!\"\n}" }おしまい
- 投稿日:2020-11-30T14:25:26+09:00
電車好きの子供からの猛攻に耐える妻を救うLINE Botを作る
こちらの記事に発想を得て、自分でもLINE Botを作った。
誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた
https://qiita.com/Teru_3/items/80cecd138860fbd0c924Qiitaで公開していただいたことにとても感謝いたします。
仕様
「LINEから電車の画像(写真でも絵でも)を送ると、その系統・形式を教えてくれる」という、至ってシンプルなもの。
うちの子供が電車をとてもだーーい好きなのだが、普段から
「(駅のホームに入ってきたのを指しながら)あの電車なーんだ?」
とか
「(自分が描いた絵を見せながら)何系描いたかわかる?」
といった質問を、(子供にとっては残念なことだが)電車に全く興味のないママに対して飽きもせず浴びせまくっており、ママがほとほと疲れている・・・というのを科学の力で助けてあげよう!ということで手がけてみた。
なお、今回は、今現在子供が一番興味を持っている「東急」を対象にした。ちょっと前は「京急」だったし、その前は乗ったこともない「京阪」だったりしたので、興味の対象が移ってしまう前に仕上げなければならない。
準備
開発環境
先日、React学習のために導入した環境をそのまま利用。
- VSCode(Visual Studio Code) 1.51.1
- npm 6.14.8
- Node.js 14.15.1
heroku
Node.jsを動かすための環境としてherokuを利用する。
(1) 下記サイトにてアカウントを作成する
heroku
https://jp.heroku.com/(2) 「herokuのCLI」をインストールする
heroku CLI(英語)
https://devcenter.heroku.com/articles/heroku-cliUbuntu 16+はsnapを使う方法が掲示されているが、自分の開発環境にsnap入れていなかったので、「Other installation methods」の「Ubuntu / Debian apt-get」の方法を取った。
$ curl https://cli-assets.heroku.com/install-ubuntu.sh | shLINE Messaging API
今時LINEアカウントを持っていない人はなかなかいないと思うので、アカウントの用意については問題ないと思う。
LINE Developers
https://developers.line.biz/ja/プロバイダー作成→チャネル作成→アクセストークン生成と進めることで、チャネルID、チャネルシークレット、チャネルアクセストークンの3つのデータを得る。Webhookはherokuへのデプロイが終わった後に設定した。
ただ、必要な情報を設定・取得するのに、LINE DevelopersとOffcial Account Managerの2つのサイトを行き来するのがやや面倒に感じた。
AIメーカー
こちらはTwitterアカウントが必要ということで、全くと言っていいほど使っていなかったTwitterアカウント(パスワード忘れてた)を掘り起こした。
AIメーカー
https://aimaker.io/無料の範囲で使わせていただくのだが、こういったサイトを個人で開発・運営される才能というか能力は本当にすごいと思う。
電車の画像
一番面倒なのは「AIメーカーに学習させる画像の収集」と考えていたのだが、AIメーカーのサイトでは検索文字列を入れると自動的に画像を集めてくれる機能(50個ほど)があり、非常に助かった。
集められた画像が「学習に使えるのか」の判断は必要だが、それでも50枚x判別させたい車両型番の数を集めてアップすることを考えたら、かかる時間と手間は雲泥の差。本当にすごい。
なお、東急が予想以上に形式が多い&ラッピング車のバリエーションがあったので、
全部やるのは面倒比較的新しい電車に絞ることにした。
- 5000系(田園都市線、緑と赤)
- 5000系5050番台(東横線、ピンクと赤)
- 5000系5080番台(目黒線、青と赤)
- 2020系(田園都市線、緑)
- 3020系(目黒線、水色)
- 6020系(大井町線、オレンジ)
実装
参考記事にjsのソースが掲載されていたので、まずは動かしてみることを最優先として
- LINEのチャネルID、トークン、シークレットを変更
- AIメーカーのモデルID、トークンを変更
- AIメーカーからの応答のlabelを見て判断するswitch文の変更
を施してデプロイした。
しかし、これでは全く勉強にならないので、ソースを読んで自分で説明ができるように理解しよう!と思い、ソースを印刷しようとしたら、いくら探してもVSCodeに印刷機能が無くて驚いた(拡張機能にはあるようですが)。
今時、印刷なんかしないんですかね、やっぱ・・・。
トラブル
git commitでエラー
ソースを変更&保存後、VSCodeのターミナルから以下の順番にコマンドを実行したが、commitでエラーとなった。
$ git init $ heroku create $ git add . $ git commit -m 'init' (こんな感じのエラーでした) Run git config --global user.email "yourmail@domain" : errorこのエラーメッセージに何やら指示(RUN)があったので、以下のコマンドを実行。
$ git config --global user.email "herokuに登録したアカウントのメアド" $ git config --global user.name "herokuに登録した姓名"そうしたところ無事、git commit出来るようになったが、メアドも名前もherokuのものにしなくてもよかったのでは?と後で思った。
LINE「正常以外の応答が返された」
gitのcommitが出来たら、herokuにデプロイする。
$ git push heroku master : (結構時間かかった) : remote: remote: Verifying deploy... done. To https://git.heroku.com/xxxxxx-xxxxxx-99999.git 194da99..029fa94 master -> masterこんな感じに「master -> master」と出ればOK。
早速、LINE Developerの管理画面の「Webhookの検証」を実行したところ、正常以外の応答が返されたとのダイアログが出てきた(まぁ予想通り)。
しかし、果たして何が起きたのか?を調べる術がわからなかったので、ひとまずブラウザからherokuにデプロイした先のURLを入力して実行したところ、アプリケーションエラーとなったので、jsに問題があることはわかった。
そのエラー画面に「heroku logs --tailしてね」とあったので、VSCodeのターミナルで実行すると、herokuのログがずらずらっと表示された。そこで再度、実行するとログがばーっと動き、エラーコード503を返していることが判明。
さらにログを遡っていくと、jsの最初の頭の方でクラッシュしており、
require('request');が悪さをしているようであった。
ということで対処として
$ npm install request <--- まさか入っていないとは・・・ $ git add . $ git commit -m 'init' $ git push heroku master再デプロイ後、LINE Developerから検証すると「成功」となり、ログでもステータスコード200を返しているのが見えたので、この問題は解決した。
動作確認
早速、LINEから色々な画像を送ってみた。
といった感じで、送った画像の実体が実車でもおもちゃでも
ちゃんと判定できていた。が、実際には的中率は5割程度といったところで、外れた場合はなぜか5080番台、または3020番台(どちらも青系の色が入った目黒線)の判定に偏っている感じがした。
外れる原因
ひとえに「学習させた画像が偏っている」ことが原因だろう。
試しにGoogleで「東急」を画像検索すると
といった感じで、ほとんどが「前面をメインに、車体は後ろに細く伸びている」といった体の画像である。今回対象にした50xx系およびxx20系は、前面はほぼ同じ形状かつ色の差異はほとんどない一方、側面には特徴(色)が強く出ている。想像だが、電車に乗る人は電車の前面より側面を見てる割合が多く、鉄道会社も側面で判断できるように配色や行先表示を配しているのだろう。
この学習させた画像を見るに
- 差異が少ない前面を複数のラベルに亘って学習している
- 差異が多いはずの側面の学習量が少ない
というのが今の学習状況になっているのではないかと想像する。
実際にいくつかの画像で試した限りではあるが
- 前面の情報量が多い画像
- 50xx系なのか、xx20系なのかは当たっている(=形の違いは判断出来ている)
- 緑と青の判定が難しいのか、田園都市線と目黒線の取り違えが多い(スコアが近い)
- 側面の情報量が多い画像
- 緑を含む2020系を青を含む5080番台と判定する場合があるなど、形・色ともにハズレ度合いが大きい
- ただし、ピンクが特徴的だからか、東横線(5050番台)は間違えないことが多い
といった傾向が見られたので、自分の想像もあながち間違ってはいないと思う。
改善(?)
しかし、このままでは
「ママはやっぱわかってないねー(やれやれ)」
とか
「パパが作ったのはハズレてばっかりじゃん(やれやれ)」
ということで親の沽券に関わる事態に瀕するので、どうにか対処しなければならない。
本来ならばAIの調整をするべきなのだろうが、良い方向に改善できるかわからないし、子供の興味が次の鉄道会社に移ってしまえば出番がなくなってしまうので、今出来ることをやろう。
AIメーカーからの応答にはラベルとともにスコア(<1)が含まれており、1に近いほど似ているという判断が可能になる。これを利用して
- 1番高いスコアが0.8を超えるときは信頼できるとして「これだね!」と断定する
- 0.8未満の場合はスコアの合計が0.8を超えるまでが「あれか、それか、どれか・・・だよね?」と列挙する。ただし、いっぱい出すと却って怪しくなるので3こまで。
と判定結果の応答のバリエーションを増やして、答えを数打って当たりを拾いに行く方式に改善?した。
tokyu_ai.js: if (labels[0].label && labels[0].score) { /* ここから変更 */ const threshold = 0.8 /* しきい値 */ if(labels[0].score >= threshold) { /* しきい値以上の場合は断定する */ message = 'この電車はきっと「' + labels[0].label + '」ですよね。' + trainMessage[labels[0].label]; } else { /* しきい値未満の場合は列挙する */ message = 'この電車は'; let totalScore = 0.0; let i; for(i = 0; (totalScore < threshold) && (i < 3); i++){ message = message + '「' + labels[i].label + '」'; totalScore = totalScore + labels[i].score; } if(i > 2){ message = message + 'のどれかじゃないかな?'; } else { message = message + 'のどっちかじゃないかな?'; } } /* ここまで変更 */ :とりあえずベタに書いてみたが、もっときれいに書けるとは思う。
改善?後の動作確認
いろいろな画像を判定させてみた。
- 側面:5050番台 →セーフ
これなら何とか親の面目は保てそう。
おわりに
勝手ながらやり方はほぼ丸パクリさせていただいたが、Node.jsを使ってLINEと他サービスをつなぐやり方の1つを知れたのはとても有意義だった。今回の経験を踏まえてあれやこれやと出来るのでは想像が膨らむし、チャレンジしたくもなる。
また、AIの判断精度を高めるためには、学習コスト(時間、手間)をおろそかにしてはならない(というか肝だろ)こともよくわかった。
少なくとも
と断定してしまうような学習状況というのはよろしくない(正解は5000系の田園都市線だが、AIメーカーのスコアは0.81と試した中では相当スコアが高い)ので、機を見て側面も含めた再学習をさせてみたいと思う。
ちなみに東急電鉄の現有形式はこれだけあるそうで、さらにはラッピング車などが数種類(リバイバルの緑、黄色のHikarie号など)あるので、全部やろうとすると・・・
- 投稿日:2020-11-30T13:35:42+09:00
node.jsとserverlessでローカル環境のコードをlambdaにアップする
はじめに
node.jsで作成したアプリをサーバーレスで動かしたい。
環境
node.js v12.18.2
Lambda関数の作成
ディレクトリ作成
mkdir serverless-node-sample cd serverless-node-samplenpmの初期化
npm initServerlessのインストール
npm install serverless --saveServerlessの動作確認
バージョン確認
sls -v以下のように表示されればOK
Framework Core: 2.13.0 (local) Plugin: 4.1.2 SDK: 2.3.2 Components: 3.4.2Serverlessのテンプレート生成
今回はnode.jsを使うので
aws-nodejs
を使用、他にもpythonやruby用のテンプレートが用意されている
コマンド実行後、handler.js
serverless.yml
.gitignore
が生成されるserverless create --template aws-nodejsディレクトリ構成の確認
確認
ls -aきっとこうなっているはず
. .gitignore node_modules package.json .. handler.js package-lock.json serverless.ymlローカルでの動作確認
今回のメインファイルとなる
handler.js
の中身を確認。
うまくいけばGo Serverless v1.0! Your function executed successfully!
が表示するはず。'use strict'; module.exports.hello = async event => { return { statusCode: 200, body: JSON.stringify( { message: 'Go Serverless v1.0! Your function executed successfully!', input: event, }, null, 2 ), }; // Use this code if you don't use the http event with the LAMBDA-PROXY integration // return { message: 'Go Serverless v1.0! Your function executed successfully!', event }; };早速コマンドから実行する。
sls invoke はデプロイされた関数を呼び出すコマンド、sls invoke localにしてあげると、ローカル環境で関数を呼び出す。sls invoke local --function helloおお、表示された
Serverless: Running "serverless" installed locally (in service node_modules) { "statusCode": 200, "body": "{\n \"message\": \"Go Serverless v1.0! Your function executed successfully!\",\n \"input\": \"\"\n}" }AWSにアクセスするための設定
AWSにアクセスするコマンドを叩く。
aws_access_key_id
aws_secret_access_key
はお手持ちのAWSアカウントから取得してもらえれば。serverless config credentials --provider aws --key aws_access_key_id --secret aws_secret_access_key補足:IAM ユーザーのアクセスキーとクレデンシャル情報の取得
AWSマネジメントコンソールの「サービスを検索する」という入力欄で「IAM」と入力すればIAMダッシュボードのページに飛びます。ページアクセス後はサイドバーのアクセス管理のタブからユーザーをクリック。あとはユーザーを追加して新しくアクセスキーを作成するもよし、既存のユーザーから取得するもよし。(詳しくはこちら)
AWSに関数をデプロイ
以下のコマンドでデプロイは完了
region
オプションはAWSの東京リージョンにアップするよっていう指定をしてくれる。serverless deploy --region ap-northeast-1デプロイした関数を実行
sls invoke --function hello --data Hello --region ap-northeast-1先ほどと同じ結果が帰ってきていればOK
{ "statusCode": 200, "body": "{\n \"message\": \"Go Serverless v1.0! Your function executed successfully!\",\n \"input\": \"Hello\"\n}" }以上です、やったぜ。
- 投稿日:2020-11-30T12:32:45+09:00
簡易HTTPサーバーを起動する魔法のコマンドを軽くまとめてみた
はじめに
とにかくHTTPサーバーを起動したい要望は時々発生します。
例えば、開発中のツールから生成したHTMLファイルをさくっとレンダリングしたいときや、CDNでjsをインクルードしたHTMLファイルをレンダリングしたりするときなどです。しかし、この世は広い広い情報の海、適切な情報にぶち当たらないときがあります(先日の私みたいに)。そこで、このページでは、そんな悩みに即座に答えられそうなページを作ろうとおもいたったわけでございます。
Node.jsの場合
vue.jsをCDNで取り込んだときの動作確認でお世話になりました
インストール
npm install -g http_server # インストール時のみ起動
http_server参考:node.js http-serverコマンドでwebサーバーを起動する
Python2の場合
10年ぐらい前、業務で生成したHTMLをどうやってプレビューしようと悩んでいたときに教えていただいたのがこれ。いやー、これは助かりました。
python -m SimpleHTTPServer <port>Python3の場合
python -m http.server <port>最後に
…やっぱり世の中は広い。Rubyの場合のコマンドを調べてみたら先人がいらっしゃいました。素晴らしい世界!
上記の他に、Rubyとかでも例が載っておりますのでぜひご参照ください。
- 投稿日:2020-11-30T11:35:04+09:00
wikipediaのワードを使った連想ゲームアプリを作ってみた【Node.js+express】
このアプリはいたってシンプルで、wikipediaからランダムに取得したワードに関連した言葉を3つ入力するアプリです。
(めちゃくちゃ簡単なアプリです。ご承知おきください。)実装環境
ubuntu:18.04.5 LTS
node.js :v10.14.2
express : 4.16.0アプリの使い方
こちらがトップ画面です。「岐阜中警察署」と出ています。思いついたものなんでもいいので、3つ入力します。
中学校なのか、警察署なのか、どちらなのでしょうか…?
あとは岐阜から連想しました。送信を押すと、また新しいワードが出てきます。「光原逸裕」って誰でしょうか。何回もやっていると「この人誰だよ」っていう人物名がめちゃくちゃ出てきます。笑
本当に知らないものが多いので、高度な連想力が試されます。技術紹介
今回は、random-word-wikipediaというAPIを使いました。Wikipediaのあらゆるページからランダムに単語を引っ張ってきてくれるものです。
使い方
まず
random-word-wikipedia
をインストールします。$ npm install random-word-wikipediaそして、requireでモジュールを読み込み、thenによって取得できます。下のコードは公式ドキュメントのusageです。
const randomWordWikipedia = require('random-word-wikipedia'); randomWordWikipedia().then(console.log); //=> [ 'Saxifraga spathularis' ] randomWordWikipedia('ja', 2).then(console.log); //=> [ 'ジョン・イサーク・ブリケ', '月は闇夜に隠るが如く' ]ソースコード
実際にこのAPIを使用したコードの抜粋です。
index.jsconst randomWordWikipedia = require('random-word-wikipedia'); function wrap_randomWordWikipedia(callback){ randomWordWikipedia('ja').then((value) => { callback(value); }) } router.get('/', function(req, res, next){ function callback(param){ var data = { theme: param, } res.render('index', data); } wrap_randomWordWikipedia(callback); }); router.post('/add', (req, res, next) => { function callback(param){ console.log('callback: ', param); var ans1 = req.body['ans1']; var ans2 = req.body['ans2']; var ans3 = req.body['ans3']; var data = { theme: param, ans1: ans1, ans2: ans2, ans3: ans3 } res.render('index', data); } wrap_randomWordWikipedia(callback); })index.pugextends layout block content h1= "Output three objects" p 「#{theme} 」に関連したものを入力してください form(method="post" action="/add") input(type="text" name="ans1") input(type="text" name="ans2") input(type="text" name="ans3") input(type="submit" name="送信") p #{ans1} #{ans2} #{ans3}苦戦したポイント
random-word-wikipedia
が非同期で動くモジュールという点に苦戦しました。
後から動くモジュールをどうレンダリングするか色々試行錯誤しましたが、結局コールバック関数を使うことにより同期処理を非同期処理の後に実行させました。
もっといい書き方があればご教授いただけると幸いです。解決方法を書いていたら長くなったので、別記事にします。(アップ次第、こちらにリンク貼ります。)
これを作った意図
人と会話するときに、何にも話題とか返答が思いつかないことってありませんか?
私はよくあるので、頭の体操になるんじゃないかという軽い思いつきで作りました。最後に
まだデータベース接続もしておらずとても簡単なつくりですが、とりあえずアウトプットしました。
ちょっとだけ作るつもりでしたが、作り出すと色々発想が生まれてきて、タイム計測やオリジナル問題など色々機能追加しようかと考え中です。やっぱ自作アプリは楽しいですね。最後までご覧いただきありがとうございました!
もし参考になったという方がいれば、LGTMお願いします!
- 投稿日:2020-11-30T06:00:39+09:00
node-sass で ENOENT: no such file or directory, scandir が出るとき
node-sass
ではたまに以下のようなエラーが出ます。ERROR in ./content_script/App.vue?vue&type=style&index=0&lang=sass& (../node_modules/css-loader/dist/cjs.js??ref--4-1!../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../node_modules/sass-loader/dist/cjs.js??ref--4-2!../node_modules/vue-loader/lib??vue-loader-options!./content_script/App.vue?vue&type=style&index=0&lang=sass&) Module Error (from ../node_modules/sass-loader/dist/cjs.js): ENOENT: no such file or directory, scandir '*****/node_modules/node-sass/vendor' @ ./content_script/App.vue?vue&type=style&index=0&lang=sass& (../node_modules/vue-style-loader!../node_modules/css-loader/dist/cjs.js??ref--4-1!../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../node_modules/sass-loader/dist/cjs.js??ref--4-2!../node_modules/vue-loader/lib??vue-loader-options!./content_script/App.vue?vue&type=style&index=0&lang=sass&) 4:14-299 @ ./content_script/App.vue?vue&type=style&index=0&lang=sass& @ ./content_script/App.vue @ ./content_script/content_script.tsこれは
node
やnode-sass
のバージョンを上げたときなどに起こるもので、以下のコマンドでnode-sass
をビルドしなおしてあげると解消します。$ npm rebuild node-sass