every Tech Blog

株式会社エブリーのTech Blogです。

IVSを用いたライブ配信

はじめに

エブリーでソフトウェアエンジニアをしている本丸です。この記事は every Tech Blog Advent Calendar 2023 の 23 日目の記事となります。
DELISH KITCHENでは、2023年12月12日(火)にアプリ内での初めてのライブ配信を行いました。アプリ内のライブ配信では、AWSのIVS(Interactive Video Service)の低レイテンシーストリーミングを利用しています。
今回はライブ配信にIVSを使う上で、どのようなことをおこなったのかなどについてお話しできればと思います。

対象読者

  • IVSを用いたライブ配信に興味があるエンジニア

本記事の目的

  • IVSの概要を知ってもらうこと
  • IVSでライブ配信を行う上で何をしたかを知ってもらうこと

本記事の対象外

  • IVSでのライブ配信のフロントエンドの実装について
  • IVSでのリアルタイムストリーミングについて
  • IVSの具体的な設定

IVSとは

Amazon Interactive Video Service (Amazon IVS) は、低レイテンシーやリアルタイムでライブ配信を行うことができるAWSのマネージドなライブストリーミングサービスです。

また、IVSの低レイテンシーストリーミングの特徴として、AWSのドキュメントからの引用ですが、次のようなことが挙げられています。

  • チャネルを作成して数分以内にストリーミングを開始する。
  • 魅力的でインタラクティブな体験を、超低レイテンシーのライブビデオと併せて構築できる。
  • さまざまなデバイスやプラットフォーム向けに大規模に動画を配信する。
  • ウェブサイトやアプリに簡単に統合できる。

IVSがやってくれること

ライブ配信の映像に関わるところはIVSが全てやってくれると言っても過言ではないです。
従来のAWS Elemental Media Serviceでは変換・保存・配信などそれぞれのサービスが用意されていて、それを組み合わせる必要があったようですが、IVSは変換から配信まで全て行ってくれます。

今回はIVSが条件に合っていたので選択しましたが、従来のAWS Elemental Media Serviceを使うとIVSを使うよりもより細かい制御ができるなどのメリットがあるため、ユースケースによって使い分ける必要がありそうです。

また、DELISH KITCHENのライブ配信にはチャット機能が必要だったのですが、IVSにはIVS ChatというIVSに付随するマネージド型のチャット機能もありました。

自分で用意が必要なこと

どのような配信を行いたいかの仕様によって異なる部分があると思いますが、弊社の場合には下記のようなことを準備する必要がありました。

  • チャットトークンを発行するためのサーバの用意
  • リアクション機能
  • 同時接続数の表示

同時接続数やリアクション機能はIVSの機能である時間指定メタデータを使用しています。
IVSでは専用のIVS Player SDKが用意されていて、メタデータをイベントとして受け取ることができます。 フロントエンドでこのイベントと連携するアクションを実装することで、任意のタイミングで同時視聴者数の更新などを可能にしています。   https://docs.aws.amazon.com/ja_jp/ivs/latest/LowLatencyUserGuide/player.html

ブログに載せるために簡略化したものになりますが、同時接続数を取得して時間指定メタデータを送信する場合、下記のようなコードになります。
これを一定間隔で実行し、フロントエンドでそのデータを使用することで同時接続数を表示できるようになります。

func sendViewerCount() error{
  // ivsのリソースを操作するクラアント
  client := ivs.NewFromConfig(config)

    streamInput := ivs.GetStreamInput{
        ChannelArn: aws.String(in.ChannelARN),
    }
  // 配信しているチャンネルのARNからストリム情報を取得
    stream, err := client.GetStream(ctx, &streamInput)
  if err != nil {
    return err
  }

  // 送信するメタデータを作成する
  metadata := map[string]interface{}{}
  metadata["viewer_count"] = stream.ViewerCount
  jsonData, err := json.Marshal(metadata)
    if err != nil {
        return err
    }

  metadataInput := ivs.PutMetadataInput{
        ChannelArn: aws.String(channelARN),
        Metadata:   aws.String(jsonData),
    }
  // metadataを送信する
    err = client.PutMetadata(ctx, &metadataInput)
    if err != nil {
    return err
  }

  return nil
}

IVSを使って良かったこと

他のライブ配信サービスに関わったことがないため比較はできないのですが、実装のコストは低いように感じました。動画の配信はもちろんですが、同時接続数のようなライブ配信に使用する他の機能に関しても時間指定メタデータなどを用いることで比較的簡単に実装することができたと思います。
フロントエンドでIVS Player SDKを使わなければならないという制約はあるようなのですが、3秒程度の遅延で配信ができているようでした。ライブ配信中のコメントへの反応も少ない遅延で行えているようでした。

まとめ

ライブ配信にIVSを使用してみて、実装のコストも低く、実際の配信の面でも遅延が少なく便利だと感じました。
IVSを使ったライブ配信を考えている人の参考になれば幸いです。
ここまで読んでいただきありがとうございました。

参考資料

wear OS について

目次

はじめに

every Tech Blog Advent Calendar 23日目の記事になります!
こんにちは トモニテ でAndroidアプリの開発を行っている岡田です。
今回は、挑戦WEEK中にAndroidスマホで表示している動画をスマートウォッチで操作するアプリを作成したので、その内容についてご紹介させていただきます。

弊社の挑戦WEEKの取り組みについてはこちらをご覧ください!
https://tech.every.tv/entry/2023/10/13/172151

Wear OSとは

正式名称は Wear OS by Google。
Googleが開発したウェアラブルデバイス向けのOSです。
2023年12月現在、一般的に普及しているウェアラブルデバイスといえばスマートウォッチくらいだと思いますので、スマートウォッチに搭載されるのが主な用途だと思います。

環境

IDE: Android Studio Iguana | 2023.2.1 Canary 11
言語: Kotlin

今回実装するアプリについて

Android Appで再生している動画をWear Appで操作するアプリを作成します。
ここではWear Appで再生ボタンが押された時に、それをAndroid Appに通知する処理を実装します。
したがってAndroid Appが受信側、Wear Appが送信側になります。

実装の流れ

以下の流れで実装しました。
1. プロジェクトの作成
2. 受信側が機能をアドバタイズする
3. 送信側でノードを取得する
4. 送信側でメッセージを送信する
5. 受信側でメッセージを受信する

これらは公式のガイドを参考に作成しました。

1. プロジェクトの作成

AndroidStudioのNew Projectに空のWear APPとAndroid APPを作成するテンプレートがあるので、これを利用しました。

2. 受信側が機能をアドバタイズする

受信側のres/values/にXML形式で機能を文字列で記述し、機能をアドバタイズします。

アドバタイズとは、Bluetooth Low Energy(BLE)などを使用して、デバイスの情報を周囲の他のデバイスに知らせる行為のことを指します。

Androidスマホは複数のスマートウォッチを接続できるため、Wear Appは使用する接続ノードが特定の機能を備えているのか判断する必要があります。
したがって、Android Appでは実行されているノードで特定の機能を提供していることをアドバタイズする必要があります。

例えば今回作成するアプリでは、ノードがAVPlayerを操作する機能を備えているかを判別する必要があります。
機能を識別するためにplayer_operationとし、以下のように記述しました。

<resources xmlns:tools="http://schemas.android.com/tools"
        tools:keep="@array/android_wear_capabilities">
    <string-array name="android_wear_capabilities">
        <item>player_operation</item>
    </string-array>
</resources>

3. 送信側でノードを取得する

送信側で受信先と通信するためのノードを取得します。
CapabilityClientクラスのgetCapability()を呼び出すことで、必要な機能を備えたノードを検出できます。
取得したノードのidはidupdateOperationCapability()にてoperationNodeIdで保持します。

companion object {
    // 受信側でアドバタイズしたものと同じ必要がある
    private const val PLAYER_OPERATION_NAME = "player_operation"
}

private var operationNodeId: String? = null

fun setupOperation() {
    viewModelScope.launch(Dispatchers.IO) {
        val capabilityInfo: CapabilityInfo = Tasks.await(
            Wearable.getCapabilityClient(context)
                .getCapability(
                    PLAYER_OPERATION_NAME,
                    CapabilityClient.FILTER_REACHABLE
                )
        )
        updateOperationCapability(capabilityInfo)
    }
}

private fun updateOperationCapability(capabilityInfo: CapabilityInfo) {
    operationNodeId = pickBestNodeId(capabilityInfo.nodes)
}

private fun pickBestNodeId(nodes: Set<Node>): String? {
    // デバイスに直接接続されているノードがあるかit.isNearbyで識別する
    return nodes.firstOrNull { it.isNearby }?.id ?: nodes.firstOrNull()?.id   
}

4. 送信側でメッセージを送信する

いよいよメッセージを送信します。
CapabilityClientクラスのsendMessage()を呼び出すことで、指定したノードにメッセージを送信できます。
3 で取得したノードidと、任意のPath、そして送信するテキストをByteArray型で指定します。

const val PLAYER_OPERATION_MESSAGE_PATH = "/player_operation"

private fun requestOperationn(textData: ByteArray) {
    val context = getApplication<Application>()
    operationNodeId?.also { nodeId ->
        Wearable.getMessageClient(context).sendMessage(
            nodeId,
            ClientPath.MESSAGE.path,
            textData
        ).apply {
            addOnSuccessListener {
                // 成功時の処理
             }
            addOnFailureListener {
                // 失敗時の処理
             }
        }
    }
}

// ClickEventで呼び出す
fun onPlayButtonClick() {
    val dataText = "play"
    val data = dataText.toByteArray(Charsets.UTF_8)
    requestOperation(data)
}

...

今回は送信側の再生ボタンにonPlayButtonClick()のようなメソッドを用意して呼び出しました。
他にもボタンを用意し、対応するメソッドを作成・呼び出してメッセージを送信します。

5. 受信側でメッセージを受信する

MessageClient.OnMessageReceivedListenerインターフェースを実装します。
addListener()を使用してリスナーを登録することで、メッセージを受け取ることができます。

onMessageReceived()では、受信したMessageEventを用いて処理を記述していきます。
例えば「送信されたMessageが"play"だったら、動画の再生・一時停止処理を行う」といった処理を記述しています。

class MainActivity : AppCompatActivity(), MessageClient.OnMessageReceivedListener {
    ...

    const val PLAYER_OPERATION_MESSAGE_PATH = "/player_operation"

    override fun onMessageReceived(messageEvent: MessageEvent) {
        when (messageEvent.path) {
            PLAYER_OPERATION_MESSAGE_PATH -> {
                val message = messageEvent.data.toString(Charsets.UTF_8) //文字列に変換
                when (message){
                    "play" -> {
                        if (binding.videoView.player?.isPlaying == true){
                            binding.videoView.player?.pause()
                        } else {
                            binding.videoView.player?.play()
                        }
                    }
                    ...
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        Wearable.getMessageClient(this).addListener(this)
    }

    ...
}

まとめと感想

まとめ

  • Androidスマホで表示している動画をスマートウォッチで操作するアプリを作成
  • 受信側では機能のアドバタイズと受信処理を記述
    • 機能のアドバタイズはres/values/にXMLで記述
    • 受信はMessageClient.OnMessageReceivedListenerインターフェースを実装
  • 送信側ではノードの取得とメッセージ送信処理を記述
    • ノードを検出はCapabilityClientクラスのgetCapability()で処理
    • メッセージ送信はCapabilityClientクラスのsendMessage()で処理

感想

  • アドバタイズする機能やPathなど共通で扱うものは、共通で使用するModuleで管理するのが良さそう
  • 送信するデータはByteArray型であることに注意しないといけない

終わりに

今回はWear OS端末を触ってみました!
最近はスマートウォッチと連携しているアプリも多く、新世代ウェアラブルデバイスの普及次第ではもっと盛り上がる技術だと思いますので、今後も注目して追っていきたいと思います!

参考

microCMS × Next.js でのキャンペーン LP 制作効率化

はじめに

こんにちは。DELISH KITCHEN 開発部の村上です。 この記事は every Tech Blog Advent Calendar 2023 の 22 日目です。いよいよ長く続いたアドカレも終盤になりました。これまで投稿された他の記事もリスト化されているのでぜひ見てみてください!!

さて今回はエブリーで運営しているキャンペーンの LP 開発においてヘッドレス CMS の microCMS と Next.js を用いた効率化に取り組んでいるのでその取り組みや知見を紹介させていただきます。

ヘッドレス CMS 導入の背景

エブリーではクライアントが協賛する形でいくつかのプレゼントキャンペーンが実施されており、それぞれのキャンペーンは独自の LP ページを持ちます。元々は Google フォームで行っていたものを LP ページ作成での CV 向上を計るという方針にあたり、取り組み当初はうまくいくか不透明な中で仕組みを作りすぎず、最速で PoC を行うためにエンジニアが自らマークアップを行い、Next.js で SSG したコンテンツを S3 + CloudFront で配信する最小構成を選択しました。実際にこの構成である程度のスピード感を持って施策の検証をすることができたのですが、PoC を終えて拡大フェーズに事業が入っていく中で管理するキャンペーンも増え、以下のような課題も浮き彫りになってきました。

  • 同時並行で複数キャンペーンが開始したい時にエンジニアの工数がボトルネックになる
  • 開催中のキャンペーン情報の細かい変更や改善でもエンジニアへの依頼が必要
  • キャンペーン担当者との実装確認依頼もありコミュニケーションコストが高く、ケアレスミスも発生しやすい

このようにその当時は最適と思われた意思決定も事業の成長スピードに対して開発体制やシステムが次第に追いつかなくなり、開発がボトルネックになることが目立ってきていたような状況でした。そこでこうした変化にシステム側も対応するためにエンジニアが作業をすることなく、キャンペーンの作成や更新をできるような状態を目指し、ヘッドレス CMS の導入を検討しました。既存の配信構成をほぼ変える事なく、コンテンツ管理も内製せずに改善ができるのはヘッドレス CMS の大きなメリットでした。

ヘッドレス CMS で有名なところだとContentfulなどもありますが、実際に使う運用者を考えるとわかりやすい UI や日本語サポートが充実していて、日本での採用事例の多くあるmicroCMSという日本製の CMS を最終的には採用しました。エンジニアとしては開発ロードマップが公開されて日々アップデートがされていたり、Next.js での新しい機能でのmicroCMS の利用をいち早く公式として発信したりとその開発体制にも好感を持てました。

microCMS を用いたシステムの全体像

microCMS の導入後のシステム全体像は以下のようになりました。

キャンペーンページの配信までは大きく以下のような流れをたどります。

  1. microCMS 内でコンテンツの入稿または更新
  2. 変更を検知して、Github Actions が発火
  3. Next.js で SSG してビルド、 S3 にデプロイ
  4. Cloudfront 経由でキャンペーンページを配信

ここからはこの仕組みを導入していくまでの工程の詳細を説明していきます。

microCMS の API 作成

まずはコンテンツ入稿やシステム連携の土台となる API の作成から行っていきます。API スキーマはフィールドという単位で要素を追加しながら定義していきます。選択できるフィールドの種類はたくさんあるのでこちらの公式ドキュメントを参考にしていただきたいのですが、その中でも私たちが LP のコンテンツ作成において一番活用しているカスタムフィールドと繰り返しフィールドを紹介します。

カスタムフィールド

カスタムフィールドは microCMS 内において複数のフィールドを組み合わせて固有のフィールドを作ることができる機能です。例えば、LP においてボタンを表示したいとしても自由にカスタマイズできるようにしようと思うと、ボタンのリンクだけではなく、表示するテキストや独自スタイルごとのボタン種別、分析用のイベント識別子などを追加したくなります。そういった場合はカスタムフィールドを用いて以下のようなフィールドを作成することで実現可能です。

実際の API はレスポンス内でネストされたオブジェクトの形をとります。

{
  "id": "test-id",
  "createdAt": "2023-12-20T08:30:38.460Z",
  "updatedAt": "2023-12-20T08:30:38.460Z",
  "publishedAt": "2023-12-20T08:30:38.460Z",
  "revisedAt": "2023-12-20T08:30:38.460Z",
  "button": {
    "fieldId": "itemButton",
    "text": "ボタン",
    "type": ["form"],
    "link": "https://example.com",
    "eventLabel": "test"
  }
}

繰り返しフィールド

カスタムフィールドと活用することで API スキーマの柔軟性が格段に向上するのが繰り返しフィールドです。繰り返しフィールドはカスタムフィールドを複数選択し、選択したカスタムフィールドを好きな順序で繰り返し入れることができるものです。例えば、先ほどのボタンに加えて、シンプルにタイトルを表すカスタムフィールドを追加して繰り返しフィールドを追加すると以下のような形で入稿することができます。

実際の API レスポンスは配列の中に各カスタムフィールドのオブジェクトが入っているので識別子でそのオブジェクトの型を判別して実装します。

{
  "id": "test-id",
  "createdAt": "2023-12-20T08:30:38.460Z",
  "updatedAt": "2023-12-20T08:33:39.574Z",
  "publishedAt": "2023-12-20T08:30:38.460Z",
  "revisedAt": "2023-12-20T08:33:39.574Z",
  "items": [
    {
      "fieldId": "itemTitle",
      "title": "タイトルA"
    },
    {
      "fieldId": "itemButton",
      "text": "ボタンA",
      "type": ["form"],
      "link": "https://example.com",
      "eventLabel": "test_a"
    },
    {
      "fieldId": "itemTitle",
      "title": "タイトルB"
    }
  ]
}

今回 CMS 導入に至った LP のコンテンツ制作においてはテキストや画像、ボタンなど各要素の配置をそれぞれのキャンペーンに最適化して作るため、自由にコンテンツを入れ替えて構築することができることは必須要件でそれを実現できるこういった機能はとても便利でした。

Next.js の build 時に microCMS の API を利用してページを生成

API が作成できたら次はその API を利用して実際にページを生成する部分を作っていきます。既存のシステムでは Next.js の SSG 機能を使って build し S3 から CloudFront 経由で配信をしているので build 時に microCMS の API を呼び出す形で連携していきます。microCMS では Javascript SDK を npm で配布しており、 microcms-js-sdkを使うことで簡単に連携をすることができます。

実際にこれらを利用することで以下のような実装をすることができます。

// libs/client.js
import { createClient } from "microcms-js-sdk";

export const client = createClient({
  serviceDomain: "domain",
  apiKey: process.env.API_KEY,
});

// pages/campaign/[id].jsx
import { client } from "libs/client";

export default function Campaign({ campaign }) {
  return (
    <main>
      <h1>{campaign.title}</h1>
      <p>{campaign.content}</p>
    </main>
  );
}

export const getStaticPaths = async () => {
  const data = await client.get({
    endpoint: "campaigns",
  });

  const paths = data.contents.map((c) => `/campaign/${c.id}`);
  return { paths, fallback: false };
};

export const getStaticProps = async ({ params }) => {
  const data = await client.get({
    endpoint: "campaigns",
    contentId: params.id,
  });

  return {
    props: {
      campaign: data,
    },
  };
};

getStaticPaths と getStaticProps で microCMS から返却されたデータを使ってページ生成することができました。これでデプロイ時に自動で CMS のコンテンツを取得してサイトに反映することが可能になりました。

変更を検知して Github Actions を起動

デプロイ時に自動でコンテンツ反映ができるようになりましたが、CMS での運用を考えると管理画面内の変更が即時に反映できる形が望ましいです。これを実現するために microCMS 内での変更を検知して Github Actions を起動するようにします。microCMS では Github Actions への webhook 通知ができるようになっており、変更を検知すると dispatch イベントが発火し、起動するようになります。

API 設定 > Webhook からトリガーイベントや通知タイミングの設定を行えます。

Github Actions は既存の CI/CD ですでに組み込んでいたので一部のビルド、デプロイ処理を切り出すことで特別このためにやることなく設定することが可能です。

本番運用する中での工夫

以上で最低限、microCMS と連携して自動でコンテンツ反映ができるようになると思います。ただ、実際に本番運用するとなるといくつか注意したいポイントがあったのでそちらについても触れていきたいと思います。

1. CMS で入稿された画像を S3 に同期して別で配信を行う

microCMS では画像管理、配信も行える基盤が整備されており、裏側は imgix と連携しています。したがって、imgixAPI で行えることが microCMS でも行うことができ、動的なサイズやフォーマット変更など画像を配信する上で強力な機能をたくさん備えています。ただ、この画像配信は無料で制限なく使えるものではなく、実際には各料金プランで確保された月のデータ転送量外で増えた転送量はその分従量課金されていく形になります。今回の場合では LP として画像配信が多く、トラフィックを試算したところ自前で画像配信基盤を構築した方が低コストに運用ができそうだったので移行に踏み出しました。

先ほどのコンテンツ管理での Github Actions 連携同様に microCMS でのメディア管理では Webhook の機能があります。今回はこの機能を利用して、API Gateway + lambda で S3 に画像を同期するような設定を行っています。

body には以下の内容が入ってきます

{
  "service": "test",
  "type": "edit", // new または update または delete
  "old": {
    "url": "https://image.microcms-assets.io/xxxxxx",
    "width": 100,
    "height": 100
  },
  "new": {
    "url": "https://image.microcms-assets.io/xxxxxx",
    "width": 100,
    "height": 100
  }
}

今回はドメインの置換のみで参照先を切り替えられるようにしたいので、lambda 側では microCMS でのパスを引き継いだ状態で S3 に格納します。弊社で動いている python のコードは以下のような実装になっています。

from http import HTTPStatus
from urllib.request import urlopen
import os
import boto3

s3 = boto3.resource('s3')
bucket = s3.Bucket(os.environ.get('S3_BUCKET', 'test.bucket'))
replace_url = os.environ.get('REPLACE_URL', 'https://images.microcms-assets.io/')

def sync_media_cms_to_s3(event, _):
    try:
        if event['type'] == 'new':
            uploadS3(event['new']['url'])
        elif event['type'] == 'edit':
            deleteS3(event['old']['url'])
            uploadS3(event['new']['url'])
        elif event['type'] == 'delete':
            deleteS3(event['old']['url'])
    except Exception as e:
        body = f"failed to sync media. err: {e}"
        print(f"ERROR: {body}")
        return {
            "statusCode": HTTPStatus.INTERNAL_SERVER_ERROR.value,
            "body": body,
        }

    return {
        "statusCode": 200,
    }

def uploadS3(url):
    upload_s3_path = url.replace(replace_url, '')
    bucket.upload_fileobj(urlopen(url), upload_s3_path)

def deleteS3(url):
    delete_s3_path = url.replace(replace_url, '')
    bucket.Object(delete_s3_path).delete()

次に独自の画像ドメインに参照を変える必要もあります。こちらはシンプルな対応になりますが、API のレスポンス内にある microCMS の画像ドメイン(images.microcms-assets.io)を独自ドメインに置換して build することで参照を変えることができます。

あくまで今回上げさせてもらったこの取り組みは弊社の基盤での最適解であり、確保された月のデータ転送量の範囲内で運用できる場合や超えたとしても自前で基盤を作るコストと天秤にかけて問題がない場合には積極的に画像配信の機能も使った方がいいと思うので、参考程度に見ていただけると嬉しいです。

2. microCMS の API 制限の回避

先ほど Next.js を用いた microCMS との連携とページ生成について、その手法を紹介させていただきましたが、こちらには一つ問題があります。それはある程度コンテンツ数が増えてくるとその数だけ getStaticProps 内での API 呼び出しが行われ、GET API のレートリミットである 60 回/秒を超えてしまう可能性が考えられることです。

これは microCMS に限らず、Next.js 内の SSG での課題として議論されていたり、レスポンスを再利用するような手法も紹介されていたりします。実際に私たちも紹介された手法を用いて、getStaticPaths で fetch した API のレスポンスをファイルとしてキャッシュして getStaticProps で再利用するようなやり方で API の呼び出し回数を抑えています。

詳細な実装は紹介されているサイトを見ていただきたいですが、ページ生成部分の実装は以下のように変わります。

import { client } from "libs/client";

export default function Campaign({ campaign }) {
  return (
    <main>
      <h1>{campaign.title}</h1>
      <p>{campaign.content}</p>
    </main>
  );
}

export const getStaticPaths = async () => {
  const data = await client.get({
    endpoint: "campaigns",
  });

  // データをキャッシュする
  await client.cache.set(data.contents);

  const paths = data.contents.map((c) => `/campaign/${c.id}`);
  return { paths, fallback: false };
};

export const getStaticProps = async ({ params }) => {
  // キャッシュからデータを取得
  const data = await client.cache.get(params.id);

  return {
    props: {
      campaign: data,
    },
  };
};

3. プレビュー機能を活用する

実際に運用していくとおそらく変更前にその見た目の確認をできれば本番と同じような環境で行いたくなってくると思います。microCMS ではプレビュー画面を表示する機能が備わっており、設定する URL の中に {CONTENT_ID}{DRAFT_KEY} という文字列を埋めることによって、プレビュー画面利用時に自動で置換され、URL が構築されるようになっています。

この機能を使って、特定のパスに preview ページを作って、受け取った CONTENT_ID と DRAFT_KEY から動的に画面を生成することができ、自前で実装せずとも簡単にプレビュー画面を作れるようになっています。

おわりに

現在では徐々に microCMS でのコンテンツ連携に各キャンペーン LP が移行している状態で、すでにエンジニア側、担当者双方で工数が減り、改善スピードが上がったことを実感しています。元々こういった改善案もエンジニア側で事業の成長スピードに合わせて、議論されて挑戦してみた結果生まれており、エンジニアがその時々の事業状況からその都度最適なシステムを考えるオーナシップがあるからこそ実現できたと思います。エブリーではこれからも技術的挑戦を行いながら、エンジニア側から技術で事業の成長を牽引していきたいと思っているので、ぜひ興味を持った方はカジュアルにお話させていただきたいです!

https://corp.every.tv/recruits/engineer

新卒1年目Web系エンジニアがChatGPTを利用した社内ChatAppのテンプレート機能の実装に挑戦した話

新卒1年目Web系エンジニアが社内ChatAppのテンプレート機能の実装に挑戦した話
新卒1年目Web系エンジニアが社内ChatAppのテンプレート機能の実装に挑戦した話

新卒1年目Web系エンジニアが社内ChatAppのテンプレート機能の実装に挑戦した話

目次

はじめに

こんにちは。 トモニテ開発部でバックエンドやフロントエンドの設計・開発に携わっている 新卒1年目エンジニアのktanonymousです。
every Tech Blog Advent Calendar 2023 の22日目の記事執筆担当者として参加させていただきました! いよいよ最終日が近づいてきていますが、是非最後までチェックしていってください!

先日、エブリーでは開発部全体のイベントである挑戦week1が開催されました。
挑戦weekの運営についての記事も出していますので是非ご覧ください。

こちらの記事では、 挑戦weekで実装した社内ChatApp2のテンプレート機能についてご紹介していきたいと思います。

現在の社内ChatAppについて

ChatGPTが使えるようになって以降、作業の効率化やクリエイティブな活動など非常に様々な場面で利活用されるようになっています。

弊社でも例に漏れず、ChatGPTを利用した社内ChatAppが利用されています。 ChatAppの仕様はシンプルで、プロンプトを入力して送信することで回答を得られます。

現在のChatAppの画面
現在のChatAppの画面
また、上記画像には写っていませんが、会話の履歴をcsvファイルとしてダウンロードすることもできます。

今回自分が挑戦するテーマを考えるにあたり、ChatGPTに対して常々思っていたことがありました。 それは、「プロンプト考えるの面倒だし、何が『良い』プロンプトか分からない!」ということです。
これは共感していただける方が多いと思っているのですが、プロンプトを考えるのは面倒だし、 かといって適当に書いたプロンプトで中途半端に間違えられると修正の方がゼロから作るより大変になることもあるのが辛みポイントだと感じています。 そこで、プロンプト自体をChatGPTに生成してもらい、それをテンプレートとして利用することで 手軽に「良い」プロンプトを使えるようにしたいと考え、テンプレート機能の実装に挑戦することにしました。

実装したテンプレート機能について

プロンプトのテンプレート機能を実装するにあたり、どのように使えると便利になるかを考え以下の要件を設定しました。

  • 簡単な用件を伝えるだけで「良い」プロンプトのテンプレートを生成することができる
  • 生成したプロンプトのテンプレートを保存しておくことで再利用・共有できる
  • 保存されているプロンプトのテンプレートを書き換えるだけで類似の用件のプロンプトをすぐに作成できる

以下のようなイメージです。

テンプレート機能のイメージ
テンプレート機能のイメージ

最終的に実装したものは以下のような手順で利用できるようになりました。

  1. テンプレートを生成する(Template Generator)
    1. テンプレート生成ページにアクセスして、用件のジャンルを選択する
      ページトップに表示されるジャンルから希望のものを選択します。
      用件のジャンルを選択する
      用件のジャンルを選択する
    2. 用件を送信する
      先ほど選択したジャンルに応じた用件を入力して送信します。
      用件を送信する
      用件を送信する
    3. 生成されたプロンプトのテンプレートを確認・保存する
      プロンプトが生成されて画面に表示されます。 左下の「Save templates」ボタンを押すと、テンプレートを保存することができます。
      生成されたプロンプトのテンプレートを確認・保存する
      生成されたプロンプトのテンプレートを確認・保存する
  2. テンプレートを利用する(Use Template)
    1. テンプレート利用ページにアクセスして、セレクトボックスから使用したいテンプレートを選択する
      テンプレートはGoogleスプレッドシートに保存されているため、社内全体で共有することもできますし、Template Generator以外で作成したテンプレートを各自で保存することもできます。
      使用したいテンプレートを選択する
      使用したいテンプレートを選択する
    2. 必要に応じてテキストを編集して送信する
      選択したテンプレートがテキストエリアに書き出されるので、必要に応じて編集します。
      必要に応じてテキストを編集して送信する
      必要に応じてテキストを編集して送信する

おまけ: 用件「テックブログの書き方を教えて」から生成されたプロンプトへの回答

テックブログの書き方についてのガイドラインは以下の通りです。

  1. テックブログを書く目的とその重要性: テックブログを書く目的は、技術的な知識や経験を共有し、読者に価値を提供することです。これにより、著者は自身の専門知識を示し、業界内での信頼性を築くことができます。また、コミュニティとの関わりを深め、新しい技術トレンドやアイデアを広めるプラットフォームとしても機能します。重要性は、教育的なリソースを提供し、同時にSEOを通じてオンラインでの可視性を高めることにあります。
  2. ターゲットオーディエンスの特定方法:
    • オーディエンス分析: 読者がどのような技術的背景を持っているか、どのような情報を求めているかを理解します。
    • ニーズの特定: 読者が直面している問題や興味を持っているトピックを特定します。
    • フィードバック: コメントやソーシャルメディアを通じて読者からのフィードバックを収集し、それに基づいてコンテンツを調整します。
  3. 効果的なタイトルの作り方:
    • 明確さ: タイトルは具体的で、読者が記事の内容をすぐに理解できるようにします。
    • キーワードの使用: 検索エンジンで見つけやすいように、関連するキーワードを含めます。
    • 興味を引く: 読者の好奇心を刺激するような言葉遣いを使い、クリックを促します。
  4. テックブログの構成:
    • 導入: 読者に記事のトピックとその重要性を紹介し、興味を引きます。
    • 本文: 論点を明確にし、段落ごとに一つのアイデアに焦点を当てます。サブヘッダー、リスト、画像などを使用して情報を整理し、読みやすくします。
    • 結論: 主要なポイントをまとめ、読者に次のステップを促すような結びを提供します。
  5. SEOの基本とテクニック:
    • キーワードリサーチ: 記事に関連するキーワードを見つけ、それらをタイトル、見出し、本文に適切に組み込みます。
    • メタデータ: メタタイトルとメタディスクリプションにもキーワードを含め、検索結果でのクリック率を高めます。
    • 内部リンク: 関連する他のブログ記事へのリンクを含めることで、サイト内での滞在時間を延ばし、SEOを強化します。
  6. 関心を持ち続けるコンテンツの書き方:
    • 教育的価値: 読者が新しい知識を得られるようにします。
    • 事例の使用: 実際の例やケーススタディを取り入れ、理論を実践に結びつけます。
    • ビジュアル: 画像、図表、動画を使用して、複雑な情報を視覚的に表現します。
  7. 定期的な更新の重要性とスケジュール管理:
    • 一貫性: 定期的な更新は読者に信頼感を与え、検索エンジンによる評価も高まります。
    • スケジュール: コンテンツカレンダーを作成し、投稿の頻度とトピックを計画的に管理します。
  8. プロモーション戦略とソーシャルメディアの活用:
    • ソーシャルメディア: Twitter、LinkedIn、Facebookなどで記事を共有し、関連するコミュニティやフォーラムでディスカッションを促進します。
    • メーリングリスト: ニュースレターを通じて定期的にコンテンツを配信し、読者との関係を築きます。
    • ゲストブログ: 他のブログやメディアにゲスト投稿をすることで、新しいオーディエンスにリーチします。

これらのガイドラインに従うことで、テックブログの品質を高め、より多くの読者に価値を提供することができます。

以上の手順で、簡単な要件から「良い」プロンプトを利用して回答を得ることができます。
ここからは、機能の核を担う Template Generator について詳しく説明していきます。

Template Generator

Template Generatorは、与えられた用件をChatAppに尋ねるための「良い」プロンプトを生成する機能です。 ChatGPT自身にプロンプトを考えてもらうことで、利用者の負担の軽減を実現することができます。
「良い」プロンプトを生成してもらうために、アプリ内部でユーザーの入力を利用して、 先ほどの用件を入力して送信している画像のように肉付けしたプロンプトをメッセージとして送信します。 プロンプトの工夫はプロンプトエンジニアリングと呼ばれますが、筆者はプロンプトの肉付けをする上で以下のようなことを意識していました。

  1. プロンプト生成の過程でユーザーにアクションを求めさせない
  2. 指示が具体的になるようにする
  3. テンプレートして使いまわしやすいようにシステムメッセージが極力入り込まず、かつ、整形されている
  4. プロンプトを受け取るChatGPTに役割を自覚させるような文言を盛り込ませる
  5. プロンプトを受け取るChatGPTに発破をかけるような文言を盛り込ませる

上記の点はテンプレートを受け取るChatGPT向けの視点で書かれていますが、 テンプレートを生成するためのプロンプト自身にも反映されるように気をつけました。 また、用件・条件・それ以外のメッセージが明確に区別されるようにもしました。
これらの中で特に工夫した点として、4, 5が挙げられます。 4. に挙げているのは、モデルに役割設定を与える手法です3。 また、5. に挙げているのは、Emotion Promptsと呼ばれる手法4で、モデルに 感情的な言葉を投げかけることでパフォーマンスが向上するそうです。
プロンプトエンジニアリングの世界も奥深く、筆者自身知らないことが多いです。 興味のある方は是非調べてみてください。

また、生成したテンプレートの保存先はGoogleスプレッドシートとしました。 テンプレートの保存先をGoogleスプレッドシートにすることで社内全体で共有することができ、 さらに、直接編集することでテンプレートの微調整や自分で作成したプロンプトをテンプレートとして保存することもできます。

今後について&まとめ

今回の挑戦では社内ChatAppの本番環境へのリリースまでは間に合わなかったので、なるべく早くリリースして社内のみんなに使っていただきたいと思っています。 また、やりきれなかったことも多いと思っているので、隙を見て改善を進めていけたら良いなと思っていたりもします。 以下の点は特に改善したいと思っている点です。

  • 実はジャンルがハードコーディングされているので、ジャンルもスプレッドシートで管理したい
  • 「テンプレート」とはいうものの、いわゆる穴埋めして利用できるテンプレートにはなっていない
    • AIの力を借りて穴埋め形式にしてみたり、テンプレートの中身を自動で臨機応変に書き換えてもらうのはアリかもしれない

本記事では、先日開催された挑戦weekで取り組んだ社内ChatAppのテンプレート機能の実装についてご紹介させていただきました。 OpenAIのChatGPT-3の発表以降、急速に勢いを増しているLLMを利用した開発に携わることができたのは非常に貴重な経験でした。 AIに限らず技術の変化は日々進んでいるので、これからも様々な技術に触れてキャッチアップしていきたいと思っています。


  1. エブリーでは今年、1週間普段の業務から離れて開発事業の推進のために技術的な挑戦に集中する期間の設定を試みていました。
  2. エブリーではChatGPTを利用した社員向けのチャットアプリが利用されています。
  3. SkillUp AI | ChatGPTのプロンプトエンジニアリングとは|7つのプロンプト例や記述のコツを紹介
  4. GigaziNE | AIに「それがファイナルアンサーなの?」「全力を尽くして」といった感情的な命令文を伝えるとパフォーマンスが向上する

monorepo環境でeslint flat configを導入してみた

はじめに

この記事は、every Tech Blog Advent Calendar 2023 の21日目の記事です!

男梅シート、あのクセになるしょっぱさと噛めば噛むほど溢れ出てくる旨さは悪魔的ですよね。僕の推しです。

初めまして!エブリーで内定者インターンをしている @きょー です! インターンでは業務でサーバーやフロントをタスクベースで開発しています。

現在フロントのコードをリプレイスしていて、Eslintの設定ファイルを見直す機会をもらったのでその際に得た知見を共有していきたいと思います!

導入に至った背景

We expect the first alpha release of ESLint v9.0.0 to be released in December or January, depending on the progress we make on our tasks.

ESLint v9.0.0の最初のアルファリリースは、タスクの進捗にもよりますが、12月か1月にリリースされる予定です。

Eslint v9.0.0 からFlat Configという新しい設定ファイルがデフォルトになります。それに伴いこれまで使っていたeslintrc形式は非推奨になり、v10.0.0(公式:予定では2024年末〜2025年初頭) では完全に削除されてしまいます。現在のプロジェクトはeslintrc形式で記述してあったので対応する必要がありました。

いざ導入!

ゴール

「monorepoの各projectでflat configを導入すること」をゴールとします!現プロジェクトではmonorepoで開発しているためです。monorepoの説明は省きますが、詳しく知りたい方は circleci blog を見てみてください。

書いていく!

root配下に一つだけeslint.config.jsを置くところから移行は始めました。これからその過程を書いていこうと思います。また、turborepoを導入しているのでcacheをうまく使えるようにするのを意識しながら書いていきました。 最初は公式とuhyoさんの記事 を参考にさせていただきました。uhyoさん、丁寧でわかりやすい記事をありがとうございます!!! - https://eslint.org/docs/latest/use/configure/migration-guide - https://eslint.org/docs/latest/use/configure/configuration-files-new

Step 1:

一番最初は↓のような構成でした。root配下の設定ファイルの中で各projectのパスと設定したいrulesやその他tsconfig.jsonなどの設定ファイルをprojectごとに書いていきました。

|---/projectA
|---/projectB
|---/projectC
|---eslint.config.js
eslint.config.js
module.exports = [
  {
    files: ['/projectA/**/*.ts'],
    // その他設定
  },

  {
    files: ['/projectB/**/*.ts'],
    // その他設定
  },

  {
    files: ['/projectC/**/*.ts'],
    // その他設定
  },
];

しかしこの構成だとあるプロジェクト固有のlint設定を追加した時や、一部のlintを修正しただけで全てのlintのcacheが効かなくなってしまいます。

Step 2:

そこでその課題を解決するために各project配下に設定ファイルを置き、それぞれの設定ファイルから共通でlintさせたいrulesを読み込み適用させる必要がありました。最終的に↓のような構造になります。この状態だとそれぞれのprojectごとでcacheが残るようになります。

|---/projectA
  |---eslint.config.js
|---/projectB
  |---eslint.config.js
|---/projectC
  |---eslint.config.js
|---/eslint-config-custom
  |---index.js
eslint-config-custom/index.js

全projectで共通にしたいlintのruleやparserの設定が書かれています。

const globals = require('globals');
const tsEsLintParser = require('@typescript-eslint/parser');
const { FlatCompat } = require('@eslint/eslintrc');

const compat = new FlatCompat();

const jsRules = {
  // jsに適用するルール
};

const tsRules = {
  // js, tsに適用するルール
};

module.exports = [
  ...compat.extends('eslint-config-airbnb-base'),

  {
    rules: {
      ...jsRules,
    },
  },

  {
    files: ['/**/*.ts', '/**/*.tsx'],
    languageOptions: {
      parser: tsEsLintParser,
      parserOptions: {
        globals: {
          ...globals.browser,
        },
        sourceType: 'module',
        project: './tsconfig.json',
      },
    },
    rules: {
      ...jsRules,
      ...tsRules,
    },
  },
];
project~/eslint.config.js

index.jsの設定を引き継いだproject~/の設定ファイルです。ここではindex.jsでexportsされた設定を展開させ、projectごとで適応させたい設定(例えばtsconfig.jsonなど)を設定しています。

const custom = require('../eslint-config-custom/index.js');

module.exports = [
  ...custom,
  {
    // projectで設定したいsetting
  },
];

導入で躓いたところ

エディター上でlintが効かなくなる

設定ファイルを編集している時にエディター上でlintが効かなくなることがありました。原因としては適切にeslintのrulesの設定がされていなかったり、exportsされたeslintの設定ファイルが配列ではなくオブジェクトになっていたためでした。 import / exportsしているlintの設定ファイルをconsole.logで出力して確認したり、cliでlintをチェックして確認しましょう。

exportsされたeslintの設定は↓のようになっていれば適用されるはずです。

[
  {
    files: [ '/**/*.ts', '/**/*.tsx' ],
    rules: { 'no-unused-vars': 'off' }
  },
  {
    files: [ '/**/*.js' ],
    rules: { 'no-undef': 'off' }
  }
]

ワークスペースが認識されない

今回のような複数のprojectで作業するときにエディター上でeslintが効いていないように見えることがあります。cliでeslintをチェックしてみて期待しているエラーが出ている場合はvscode/settings.jsonに明示的にワークスペースを登録する必要があるかも知れません。自分はこれで解決しました!

例)

"eslint.workingDirectories": [
    "./projectA",
    "./projectB"
]

monorepo環境でflat configを導入してみての感想

monorepo環境下でも無事に導入できました!基本的にproject配下の設定ファイルではindex.jsをimportしてくるだけで共通の設定を適用できるので、projectが増えても簡単に拡張しやすい構成になっていると思っています!また依存性の管理も/eslint-config-customの中を見れば大体書いてあるので把握しやすくなっています。

eslint自体の知識が浅かったのですが、どのようなルールでlintingされていてコードの可読性が保たれているのかを知れる機会になったので勉強になりました。綺麗なコードはlintから、、、!!

終わりに

monorepo環境下でのeslint flat config導入で得た知見を紹介しました!書いた記事が皆さんに役に立てたら嬉しいです。

もし何かありましたらtwitterなどで聞きにきてください!

明日も記事が出ると思います!お楽しみに!!

ps. ホロパレード楽しい、、!!