コンユウメモ @kon_yu

作ったガラクタとか、旅行とかの話

お前らを ”スピードの向こう側” に連れて行ってやるぜ

はじめに

この記事は、クソアプリ - Qiita Advent Calendar 2024 - Qiitaの7日目の記事です このアドベントカレンダーも今年で10周年で、おめでたい。オレ以外の毎年書いているいかれたメンバーは何人いるのだろうか?

今までの記事のリンク これを数えたらこの記事を入れてクソアプリが13個ある。アドベントカレンダーが10年皆勤賞で参加しているのに、3個多い。どうやら日常でもいらん物を作ってはブログを書いているらしい。1円にもならないというのもかかわらずだ。

さて、遡ること10年以上前、2000年代後、当時SEだった僕は美容室に努めていた地元の友人にこんな約束をした。 「いつか自分の美容室をオープンしたらHPをつくってやるよ」

経て2020年頃、ついに彼は独立し美容室がオープンした。約束を守る時が来たのだ。

制作費や運用費をもらわない代わりに好き勝手やらせてもらうことした。 飲食店や美容室のHPは必要な情報にアクセスしやすくするべきだと思っていた。

わかりやすいシンプルなサイトにしてやろうじゃないか。GoogleSEO対策もしてやろう。

Googleに好かれつつ、シンプルなHPで可能な遊べる余白はなにか?

!?

"スピードの向こう側"爆速の表示速度だ。

オレが手に入れてやる・・・! "その領域"・・ "スピードの向こう側"を・・!!

今年のクソアプリは、お前らを連れて行ってやるぜ "スピードの向こう側" によォ!!

※注意 この記事は、2000年代のインターネットのノリと週刊マガジン連載されていた"疾風伝説 特攻の拓"(かぜでんせつ ぶっこみのたく) の薄い知識の影響を受けています

作ったもの

道楽で運用している地元の友だちの美容室のHP(隠しページあり)はこちら。

今回は、このHPを"スピードの向こう側へ" 走り抜けた軌跡を紹介していくぜ・・・!

kalon1923.com

HPのベース

Astroを使って静的サイトをSSGしている。 Astroはコンパイルすることでいらない情報を諸々削除たり圧縮したり表示を早くすることに特化したFW。 またページ内の一部のJavaScriptを遅延ロードして動かせ、Reactに対応している。

astro.build

UIコンポーネントにTailwindベースのdaisyUIを使用

daisyui.com

Prefetchを使うことで、ページ内のリンクをキャッシュして画面遷移時に表示早くするようにしている。

developer.mozilla.org

第一世代のデプロイ先Netlify

HPのホスティング先は慣れている Netlify いや

"電脳璃覇威"Netlify にデプロイした

パフォーマンスの計測したLighthouseの結果を見てくれ

こいつを見てどう思う?

!?

すごく大きいです100点満点です

Netlifyにホスティングしたキャッシュを使わない場合の表示速度は、トップページで500ms程度

ここまで十分速度も早い、必要な情報なコンテンツは全部実装しているし、画像もSVGにして容量を下げている。おまけにSEO対策もした。地方都市の美容室のHPにしては十分だ。

だが待ってほしい、今回のテーマは"スピードの向こう側"爆速の表示速度だ。

スピードの向こう側への旅はまだ始まったばかりだ・・・!!

第二世代のデプロイ先 Cloudflare

"電脳璃覇威"Netlifyにも弱点がある。日本にCDNサーバのリージョンがないのだ

一番近くてシンガポール

そして通信プロトコルがHTTP/2に対応しているが "HTTP/3" には対応していない

CDNサーバが日本にあって、HTTP/3に対応している、

それでいてNetlify並に開発体験がいい、そして無料枠が大きいところ

そんな "都合" の良いサービスが有るか?あるわけがねェ・・・!

!?

!?

"雲羅宇怒陽炎"Cloudflare

"こいつ"だ・・・!!! ドエレーー ”COOOL”じゃん・・・?

CloudflareのWorkers & PagesにデプロイしてCDNをキャッシュヒットさせた状態で、トップページにアクセスすると。 30ms !!

もちろんレスポンスヘッダにCache-Controlも追加している、サーバにアクセスせずにディスクキャッシュ利用した場合は 3msを叩き出す。

だが、まだまだ足りない・・!もっとだ・・!もっとスピードの向こう側へ・・・!!

計測!計測!計測

トップページには、お知らせ情報にアクセスしてJsonを取得してカルーセルを表示するReactのパーツがある。

このAPIを高速化していくぜ!!!

お知らせ情報を取得するWebAPIの機能

美容室のブログであるアメブロにあるお知らせ情報だけをスクレイピングしてJSONに変換して返す。

CloudflareのWorkersにTypeScriptでコードを作成してデプロイしている。 このTypeScriptのコードで利用しているFWはHonoを使っている。Cloudflare上で動きルーティングその他速いらしい。 今回の機能のサイズでは速さを実感するほどものではないがFW自体のサイズが小さいので起動時に時間がかかることがないだろう。

hono.dev

WorkersのTypeScriptのコードでデータをキャッシュする方法は下記の3つのサービスでできそうだ。

Cacheは地域のデータセンターで保存されるらしいが、このデータセンターとCDNのエッジサーバで保存されるのだろうか?

KVの方が早いかもしれない。

D1はSQLiteCDNエッジサーバで動くらしい、とても早そうだ。

一体どれが一番早いのだろうか?わからない。

それでは計測だ!!!

ローカル環境でベンチマークテスト

Workersはローカルで開発できるので、ベンチマーク用のコードをそれぞれ用意した

Cache版の計測用コード(クリックすると展開) 

import { Context } from "hono";

export const benchmarkCache = async (c: Context) => {
    const cache = caches.default;

    const writeStart = performance.now();
    for (let i = 0; i < 100; i++) {
        const key = `https://example.com/benchmark_key_${i}`;
        const value = new Response('benchmark_value');
        await cache.put(new Request(key), value);
    }
    const writeEnd = performance.now();

    const readStart = performance.now();
    for (let i = 0; i < 100; i++) {
        const key = `https://example.com/benchmark_key_${i}`;
        const response = await cache.match(new Request(key));
        const value = response ? await response.text() : null;
    }
    const readEnd = performance.now();

    const totalWriteDuration = writeEnd - writeStart;
    const totalReadDuration = readEnd - readStart;

    const avgWriteDuration = totalWriteDuration / 100;
    const avgReadDuration = totalReadDuration / 100;

    return c.json({ totalWriteDuration, totalReadDuration, avgWriteDuration, avgReadDuration });
};

KV版の計測用コード(クリックすると展開)  import { Context } from "hono";

export const benchmarkKV = async (c: Context) => { const writeStart = performance.now(); for (let i = 0; i < 100; i++) { await c.env.kv.put(benchmark_key_${i}, 'benchmark_value'); } const writeEnd = performance.now();

const readStart = performance.now();
for (let i = 0; i < 100; i++) {
    const value = await c.env.kv.get(`benchmark_key_${i}`);
}
const readEnd = performance.now();

const totalWriteDuration = writeEnd - writeStart;
const totalReadDuration = readEnd - readStart;

const avgWriteDuration = totalWriteDuration / 100;
const avgReadDuration = totalReadDuration / 100;

return c.json({ totalWriteDuration, totalReadDuration, avgWriteDuration, avgReadDuration });

};

D1版の計測用コード(クリックすると展開)  import { Context } from "hono";

export const benchmarkD1 = async (c: Context) => { // テーブルが存在しない場合は作成 await c.env.d1.prepare( CREATE TABLE IF NOT EXISTS benchmark_table ( key TEXT PRIMARY KEY, value TEXT ) ).run();

// テーブルをクリア
await c.env.d1.prepare(`DELETE FROM benchmark_table`).run();

const writeStart = performance.now();
for (let i = 0; i < 100; i++) {
    await c.env.d1.prepare(`INSERT INTO benchmark_table (key, value) VALUES (?, ?)`)
        .bind(`benchmark_key_${i}`, 'benchmark_value')
        .run();
}
const writeEnd = performance.now();

const readStart = performance.now();
for (let i = 0; i < 100; i++) {
    const result = await c.env.d1.prepare(`SELECT value FROM benchmark_table WHERE key = ?`)
        .bind(`benchmark_key_${i}`)
        .first();
}
const readEnd = performance.now();

const totalWriteDuration = writeEnd - writeStart;
const totalReadDuration = readEnd - readStart;

const avgWriteDuration = totalWriteDuration / 100;
const avgReadDuration = totalReadDuration / 100;

return c.json({ totalWriteDuration, totalReadDuration, avgWriteDuration, avgReadDuration });

};

上記のベンチマーク用のWorkersのコードをローカルで動かすと書き込みはD1が一番早く、読み込みはKVが一番早い。

キャッシュ方式 合計書き込み時間 (ms) 合計読み込み時間 (ms) 平均書き込み時間 (ms) 平均読み込み時間 (ms)
Cache 95 35 0.95 0.35
KV 102 17 1.02 0.17
D1 71 43 0.71 0.43

今回の使い方だとキャッシュいかに早く取り出して返すかが重要なので、読み込み速度を重視してKVを利用するのが良さそうだ。

Cloudflare Workers上で速度を計測

今度は、実際にCloudflareの環境でそれぞれの方式を使ってAPIを実装した。

そのアメブロスクレイピングしたデータをキャッシュしたものを取り出して返すAPIの速度を比較すると下記のような結果となり、ベンチマークで計測しただけではわからなかった事が判明した。

キャッシュ方式 平均速度 備考
キャッシュなし 200ms
Cache 25ms~50ms 80-100msと上振れすることも多い
KV 30ms程度 少し間をおいて動かすときにアクセスする際にLambda的なのを起動するのか最初だけ500msぐらいかかる
D1 480ms程度 キャッシュなしより遅い、KVと同様に起動時は1秒以上かかる

この結果から、API上のキャッシュは最初の起動が遅いが安定的なKVを利用することとした。 D1が早いことを期待したが少なくとも2024年現在では、今回の使い方ではKVに軍配が上がった。

どこにでもキャッシュを仕掛けろ!

キャッシュはできるだけ近いところに仕掛けるのが効果が高い

  • サーバよりはCDN
  • CDNよりはクライアント

でキャッシュするほうが効果てきめんだ。

さぁここまでにCloudflareの利用によって、HTMLや画像など静的なファイルはCDNでキャッシュし、Cache-Controlをレスポンスヘッダーにつけることによってブラウザにキャッシュを利用している。

WebAPIではWorkersにおいてアメブロスクレイピングしたデータをKVに保存することでキャッシュするようにした。

まだだ、まだまだ行ける。

!?

WebAPIのアクセスをクライアントでキャッシュしたらサーバにアクセスせずに済むんじゃぁないか?

こんな感じでuseEffectに仕込めばWebAPIアクセスもキャッシュ利用してキャンセルできる

        const cachedArticles = localStorage.getItem('latestArticles');
        const cachedTime = localStorage.getItem('cacheTime');
        const now = new Date().getTime();

        if (cachedArticles && cachedTime && now - cachedTime < 600000) {
          setLatestArticles(JSON.parse(cachedArticles));
          setLoading(false);
          return;
        }

最終的結果

最終的なパフォーマンスを見て見よう

HA☆YA☆SU☆GI☆RU

さぁ各過程で工夫した箇所をもう一度まとめてみよう

項目 内容
SSG FW Astro
他ページの先読み Prefetch
プロトコル HTTP/3
CDNサーバ 東京リージョン
WebAPIの実行 Cloudflareのエッジサーバ
WebAPIのFW Hono
WebAPIのキャッシュ KVの利用
ブラウザ静的ファイルのキャッシュ Cache-Controlの利用
ブラウザWebAPIアクセスのキャッシュ LocalStorageの利用

まとめ

いかがだっただろうか?見た目的には取り立てて派手さのないHPでも視線を変えると遊べる余白があることを証明できたのではないだろうか?

高速化の過程を見せることをお前らを"スピードの向こう側"爆速の表示速度に導くことができたのではないだろうか。

ちなみにオレはマガジンよりはジャンプ派なので特攻の拓が連載していた頃の漫画だと幽遊白書るろうに剣心、幕張が好きである

動画内のJYPのパンチラインで学ぶ英語学習サービスを作ってやったぜ

はじめに

今年も懲りずに クソアプリアドベントカレンダー2023 8日目の記事でございます。 9年連続でやっているらしい。継続は力なりとはよく言ったもので、毎年やっていると今年もやらねばという気持ちになるのは不思議なものである。

さてさて、YouTubeで注目を集めたK-Popオーディション番組「A2K」は、JYP Entertainmentが主催し、アメリカでのK-Popアイドルの発掘を目的としている。

番組内でのJYP Entertainmentの代表JY Park(以下JYP)による印象的なパンチラインが英語学習の素晴らしい素材になるだろうと考えて、どうにかしてWebサービスにしてみた。

具体的には、パンチラインが表示される瞬間に現れる大きなテロップを検出し、その出現時刻を自動で収集するシステムを開発した。ある程度の精度でデータを収集することができたので、このデータをもとにWebサービスを作成した。

今回のクソアプリではChatGPTなど生成AIを使ったものが多くなりそうなので、あえて使わない制約と誓約をかけた。 そうそう今年になってHUNTER×HUNTERをまとめて読んだ。特にキメラアント編をまとめて読んだらとても面白かった。面白すぎて思わず大阪まで冨樫義博展を見に行ってTシャツを買ってしまった。

閑話休題

A2Kプロジェクトとは

A2K、つまり「America to Korea」は、アメリカにおけるK-Popアイドルの発掘を目的としたJYP Entertainmentの野心的なプロジェクトである。K-Pop業界全体がグローバル化を目指す中、JYPは「globalization by localization」という独自の戦略を採用している。この戦略の一環として、JYP自らがオーディションの審査を行い、その過程で放たれる数々の魅力的なパンチラインが、オレの心を熱くする。

一番のお気に入りはA2K ep.16 "Korean Boot Camp Begins"のJYPがJYP Entertainmentのアーティストに求める3つTipsを話しているところ youtu.be

A2Kプロジェクト選定の理由

  • アメリカのオーディション参加者は自然な英語を話すため、リアルな英語学習の素材として最適である。
  • JYP自身も非常に流暢な英語を話すため、彼の発言はリアルな英語表現の学習に有効である(時折韓国語のアクセントも聞こえるが、オレの現在の英語力では十分にきれいな英語である)
  • 番組ではJYPのパンチラインが大きなテロップとして画面に表示されるため、視覚的にも捉えやすく、学習しやすい。
  • JYPのポジティブでエモーショナルな言葉は、学習を楽しくさせると同時に、英語の表現力を高めるのに役立つのだ。

開発したサービスの特徴

基本の使用方法

  • セレクトボックスから視聴したいエピソードを選択する
  • 選択後、そのエピソードに含まれるパンチラインの一覧が表で表示される
  • 表には以下の情報が含まれる:
  • 表の左端にある画像をクリックすることで、対応するパンチラインが含まれる動画の該当部分が再生される。これにより、何度も繰り返し聞いたり、実際にJYPのように発話することで、英語表現を身につけることができる

下記のデモのようにJYPのパンチラインを何度も繰り返し聞いて練習することができるのだ。 youtu.be

詳細な使用方法

  • Seekセレクトボックスを使用して、動画の進行や戻りの時間を1秒から20秒の間で設定できる。デフォルト設定は3秒
  • 進むボタンや戻るボタンで設定した時間だけ動画を進めたり戻したりすることができる
  • ショートカットキーも利用可能で、スペースキーで動画を一時停止・再生、右矢印キーで設定時間だけ進行、左矢印キーで設定時間だけ戻る

システム構成

  • 本サービスはTypeScriptとNext.jsを用いて開発された。デプロイはCloudflareのworkers and pagesを利用した

データ収集のための前処理

  • YouTubeからの動画ダウンロード
  • 動画から音声データとしてmp3ファイルの作成

パンチラインデータ抽出の挑戦

動画の中でパンチラインがいつ表示されているかのデータを探すのには、試行錯誤を必要とした。2秒毎に動画のキャプチャを取り色々処理してみた戦いの結果がこれである。

ChatGPTvやその他OCRのAIライブラリを使えば簡単じゃないかと思うじゃないですか?ところがどっこいそんなに簡単じゃないんだなこれが。AIライブラリだと画像にある文字は取り出すことは精度良く出せるが、動画の中で大きい文字を抽出するはできないのだ(少なくとも2023年12月現在では)

画像の解像度をどんどん下げて、小さいテロップは文字認識できないが、大きいテロップのみ文字認識する境目を探すアプローチを取ってみたが全然上手くいかなかった。

しょうがないので自前でPythonを書いてOCR処理をすることにした。自前でOCR処理をすると認識した文字のサイズを計算することができる。この文字の大きさに基づくしきい値設定した。具体的には認識した文字列のそれぞれの文字サイズを計算してその中央値をしきい値とした。

  • ただしOCRの精度は色々工夫したがあまり良くなくて例えばこんな変な文字列を取得してしまうのだ。(hieinw That won Dawe fo perfor ree | ben oe 4 ; ji
  • なので、OCRで文字列を認識した + 認識した文字列が一定以上大きい」パンチラインが表示されているキャプチャであるという作りにした。

具体的な例を見ていこう、下記のような小さい文字のテロップを避けたい。そしてずっと出てるA2Kという文字列も無視するように細かい調整をしている。

そして、このようなデカいテロップの文字列があるのを探して、動画内内の何分何秒目であるかを記録して次の工程に進めているのだ。

データ収集後の後処理

  • Speach to Text APIを用いて動画の音声のmp3から文字起こししSRT形式で保存
  • Flaskを使用して開発された手動チェックするUIを作成
    • これでデータの最終選別と整理を行った。OCR処理で行ったデータは緩めのしきい値にして最後は人間の目と耳でこれはパンチラインである・ないをより分けた。
    • キャプチャの画像と、対象の時間のyoutubeの動画のリンクを作り、リンクをクリックして音声とテロップをみて、行っていることとテロップの文字が同じかもチェクしている
  • 文字起こしをしたSRTファイルとキャプチャ画像の照合しCSVファイルの作成
  • 英語の文字起をした内容を、GCPの翻訳APIにかけて日本語に翻訳をCSVファイルに追加
    • このA2KのYoutubeの動画には、自動翻訳の英語と、翻訳された日本語字幕があるのだが、このデータを検証した結果、タイムスタンプが一対一で対応していないので却下した

まとめ

本サービスは、JYPのパンチラインを活用し、英語学習を支援することを目的としている。A2Kオーディション番組から得られたデータを基に、英語の表現力を効果的に高めることが可能である。本サービスを通じて、英語学習に新たな一歩を踏み出す機会を提供する。

インターネットだけじゃ時差は克服できないんだ。それをわからせてやる The Art of Time Differences

みんなー!役に立たないもの作ってるー!?オレは今年もクソも役に立たないものを作ったよ!!

毎度おなじみクソアプリ Advent Calendar 2022 23日目の記事でございます

qiita.com

過去のクソアプリの作品はこちら クソアプリ カテゴリーの記事一覧 - コンユウメモ @kon_yu

人類は距離を克服してもまだ時差は克服できてない

ブロードバンドの発達やリモートワークツールの発展によって国をまたいで同期的に仕事ができるようになったよね。でもインターネットだけじゃ時差はどうにもならないんだ。時差を解決するには時間を乗り越える必要があって、そんなテクノロジーはもうデロリアンで時速88マイル以上で走るしか無い。使い方を間違えるとうっかりマーティ・マクフライの右手が消えかけてしまう。

さて日本とアメリカ西海岸のシリコンバレーがあるサンフランシスコでは時差が17時間あるので、平日でも仕事がしやすい時間帯は日本の朝と向こうの夕方でせいぜい2, 3時間ぐらいしか無い。

実際去年サンフランシスコの人と仕事をした際に気づいたのは、週末と週頭がどちらかの土日とかぶってしまうのだ。これが非常に難儀した。

また、グローバル企業で日本、ヨーロッパ、アメリカで会議をする際にはどうしても日本が変な時間で参加させられる羽目になると聞く。

もしあなたが本社や顧客の立場で自分のタイムゾーンでいつも会議をしているなら、今回作ったサービスを使ってみてどれだけ別のタイムゾーンにいる人たちに面倒を強いているのか確認して悔い改めよ!そして神と和解せよ。さすればキリストはあなたを罪から開放する。

今回作ったもの

サービスのURL: https://time-difference.konyu.net/

www.youtube.com

簡単な使い方

  • 基準とする月日と時間、タイムゾーンを設定する
  • [Add Timzone]ボタンを押してタイムゾーンを追加、比較したいタイムゾーンに変更する
  • 月日や時間を変更して同一時間帯に他のタイムゾーンが何曜日のなのかを見せてくれる
    • その時間が一般的な休みの日や仕事の時間外であればステータスの絵文字が怒り出すよ!

サンプル

まず協定世界時 (UTC) では

  • 日本 UTC+9
  • イギリス UTC+0
  • サンフランシスコ UTC-8

3拠点問題

日本、イギリス、サンフランシスコの3拠点で都合の良いミーティングの時間はない。 イギリスとサンフランシスコに合わせるとどこの時間で会議しようとしても日本はきつい

時差が大きいと、金曜日、月曜日は相手側の週末と被る問題

日本と、サンフランシスコ時差が17時間あるので土日に負担がかかりがち

サンフランシスコの金曜日は、日本の土曜日

サンフランシスコで今週中に片付けるタスクで必要な問い合わせは日本の土曜日に返答を求められる。

日本の月曜は、サンフランシスコの日曜日

日本で週明けに質問をサンフランシスコに投げかけたら、そこは日曜日なのだ。

参考: サンフランシスコと日本で両方平日の場合

どうやって作ったか

ベースはNext.jsとTypeScriptで作成

別にシンプルなReactで良かったんだけど、なれているのNext.jsで実装した。

Linterとかの設定をやるのが面倒くさかったのでボイラープレートを使った。下記を使うとNext.jsの最新版やTailwind CSSとかHuskyとかだいたい全部入ってて楽だった。

GitHub - ixartz/Next-js-Boilerplate: 🚀🎉📚 Boilerplate and Starter for Next.js 13+, Tailwind CSS 3.2 and TypeScript ⚡️ Made with developer experience first: Next.js + TypeScript + ESLint + Prettier + Husky + Lint-Staged + Jest + Testing Library + Cypress + Commitlint + VSCode + Netlify + PostCSS + Tailwind CSS

Tailwind CSSでスタイルを当てるのはChatGPTに手伝ってもらった

Tailwind CSSを使っているが、ゼロからクラスを設定するのが面倒で適当なデザイン当てていないTSXを作ったら

ChatGPTでそのTSXを渡して「Tailwind CSSで装飾して」とお願いするとなんかいい感じにTailwind CSSのクラスを当ててくれるのでそれをちょっと手直している

まとめ

異なるタイムゾーンで同期的に仕事をするのは大変、特に日付変更線をまたぐような日本-アメリカ間は、金曜日や月曜日が相手の土日に被るので実質的に同期的に働ける曜日は限られる。その貴重な限られた時間なのをサービスを作ることで明らかにすることができた。

異なるタイムゾーンで仕事をする場合はオンラインミーティングのような同期的なコミュニケーションは貴重な時間なので、その時間を大事にするためにコミュニケーションをするときには最大限生産性をあげる下準備をしよう。

今後の展開

忌まわしいサマータイムを考慮した機能を実装したい。

タイムゾーンを設定しないようをシェア機能シェアできるようにしてたくさんの人を悔い改めさせたい

終わりでーす