Markdown AIと動体検知を組み合わせたかった！呼び込みAI制作のログ

# はじめに
ご覧いただきありがとうございます！

Qiita Advent Calendar 2024「[Markdown AIのサーバーAI機能を使ってWebサイトを作ってみよう](https://qiita.com/advent-calendar/2024/markdownai)」22日目です！

この記事を書いている時点で既にクリスマスは通り過ぎてしまっていますが、枠が空いていたので1本記事を書いてみたいと思います

今回は、Markdown AIのサーバーAI機能を使い、動体検知で通行人を認識、そしてナレッジに記載しているお店の紹介をしてくれるWebアプリを~~作ってみた~~ので紹介します

**記事投稿前に追記**
Webアプリを作っている途中まで、きちんと動作をしていたのですが、Markdown AIの仕様が変わった？のか、数日前からJavaScriptが上手く動かなくなってしまいました…

CDNで読み込んでいるOpenCV.jsはダウンロード出来ているのでどこに原因があるのかサッパリです…

以下Webアプリ作成途中に書いた記事のため、若干内容の辻褄が合わない箇所があるかもしれません

こんなことやろうとしてたんだな、ということが読者の皆さんに伝わりましたら嬉しいです

# どんなWebアプリを作ろうとした？
JavaScriptでOpenCVを動かし、動体検知をさせ、動くものを検出したらMarkdown AIのサーバーAIが喋ってくれるWebアプリです

Markdown AIはMarkdownだけでなく、HTML要素やJavaScriptも組み合わせることができるので、自由な発想を形にすることができます

# 開発までの構想
Shikuno (@shikuno_dev) さんが先日投稿されていた記事を見て、
Markdown AIにカメラを組み合わせるというアイデアを頂きました

https://qiita.com/shikuno_dev/items/58f726fa5fceecb6ba67

この記事を読んだ後に、Markdown AIがどんなところでより活躍できそうかお風呂で考えていたら、今回のWebアプリの着想を得た次第です

せっかくAIが使えるなら、通行人の性別や年齢に合わせて話題を変えられたら良いなぁ、と思いましたが、JavaScriptで性別年齢検出をする際によく用いられているface-api.jsとMarkdown AIの相性が悪そう(検出用のモデルファイルを配置しないといけない)だったので今回は見送りました

# つくりかた
動かなくなってしまったので割愛します

# 作成したコード
一応ギリギリまで動作していたコードを貼り付けておきます

```html:index.html
<video autoplay playsinline muted></video>
<div id="kanyu">MOVE!</div>
<script src="https://docs.opencv.org/3.4.0/opencv.js"></script>
<script>
    class MotionDetection {
        constructor({ video, onMove, onStop, fps = 8 }) {
            this.video = video;
            this.onMove = onMove;
            this.onStop = onStop;
            this.fps = fps;
            this.running = false;
            this.lastFrame = null;
            this.timer = null; this.cap = null;
            this.currentFrame = null;
            this.previousFrame = null;
            this.diffFrame = null;
            this.grayDiff = null;
        } start() {
            if (this.running) return;
            this.running = true; const width = this.video.videoWidth;
            const height = this.video.videoHeight; this.currentFrame = new cv.Mat(height, width, cv.CV_8UC4);
            this.previousFrame = new cv.Mat(height, width, cv.CV_8UC4);
            this.diffFrame = new cv.Mat(height, width, cv.CV_8UC4);
            this.grayDiff = new cv.Mat(height, width, cv.CV_8UC1); this.cap = new cv.VideoCapture(this.video);
            this.loop();
        } stop() {
            this.running = false;
            clearTimeout(this.timer);
            this.cleanUp();
        } loop() {
            if (!this.running) return; const delay = 1000 / this.fps;
            this.cap.read(this.currentFrame); if (!this.previousFrame.empty()) {
                cv.absdiff(this.previousFrame, this.currentFrame, this.diffFrame);
                cv.cvtColor(this.diffFrame, this.grayDiff, cv.COLOR_RGBA2GRAY);
                const thresh = new cv.Mat();
                cv.threshold(this.grayDiff, thresh, 25, 255, cv.THRESH_BINARY); const motionDetected = cv.countNonZero(thresh) > 1000; if (motionDetected) {
                    this.onMove();
                } else {
                    this.onStop();
                }
                thresh.delete();
            } this.currentFrame.copyTo(this.previousFrame);
            this.timer = setTimeout(() => this.loop(), delay);
        } cleanUp() {
            this.previousFrame?.delete();
            this.currentFrame?.delete();
            this.diffFrame?.delete();
            this.grayDiff?.delete();
        }
    } function loadOpenCv() {
        cv['onRuntimeInitialized'] = () => {
            const medias = {
                audio: false,
                video: {
                    facingMode: 'user'
                }
            }; navigator.mediaDevices.getUserMedia(medias)
                .then(successCallback)
                .catch(errorCallback);
        };
    } async function successCallback(stream) {
        const video = document.querySelector('video');
        const kanyu = document.getElementById('kanyu');
        const fps = 8; video.onloadedmetadata = () => {
            video.width = video.videoWidth;
            video.height = video.videoHeight; const motionDetection = new MotionDetection({
                video,
                onMove: async () => {
                    kanyu.style.display = 'block';
                    const serverAi = new ServerAI();
                    const message = "人が通ります";
                    try {
                        const answer = await serverAi.getAnswerText('qV3jL7aqpBLucJDvmLchKU', '', message);
                        if (answer) {
                            const speech = new SpeechSynthesisUtterance(answer);
                            window.speechSynthesis.speak(speech);
                        }
                    } catch (error) {
                        console.error('Error fetching answer:', error);
                    }
                },
                onStop: () => {
                    kanyu.style.display = 'none';
                },
                fps
            }); motionDetection.start();
        }; video.srcObject = stream;
    } function errorCallback(err) {
        alert('Error accessing media devices: ${ err.message }');
    } loadOpenCv();
</script>
```

# 作成したプロンプト
```
あなたはKnowledgeに記載されている店の従業員です。
今、店の前を通りかかっている人に対して宣伝を行います。

1文で魅力が伝わるよう、声に出す言葉を出力してください。
全ての情報を入れ込まなくてもかまいません。

Knowledge
# 美味しいポップコーン店
このお店では、美味しいポップコーンを販売しています
芳醇な香りを楽しめる高級な材料を使用しているので絶品です

# メニュー
## 塩キャラメル
1カップ 200円
1袋 400円

## 北海道バター
1カップ 200円
1袋 300円

## 4種のチーズ
1カップ 300円
1袋 500円

# 営業時間
年中無休
午前8時から午後8時
```

本当はMarkdown AIのKnowledge機能を使用したかったのですが、どうしてもエラーが出てしまい利用できなかったので、プロンプトに記載しました

# 注意点と対策
## Markdown AIとCDN
このWebアプリではCDNでOpenCV.jsを読み込ませていますが、Markdown AIでは、**ページ公開前のプレビュー画面**と、**実際に公開されたページ**で、CDNの扱いが変化します

実際に公開されたページではCDNを利用した外部ライブラリの読み込みができますが、プレビュー画面では読み込むことができません

プレビュー画面で動きを確認しながらページを作成したい場合は、CDNの中身を直接貼り付けてあげる必要があります

**記事投稿前追記**
仕様が変わった可能性があるため、上記記述については正確性が担保できません

CDNが使えないときに試してみると良いかもしれません

## Markdown AIでよく見るエラー

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2921137/812a7b87-60d0-8e70-d5c8-c8faa98f51ae.png)

Markdown AIでJavaScriptを動かそうとするとよく見るエラーです
ブラウザのデベロッパーツールを使うと確認できます

JavaScriptの記述をコードブロックとして処理してしまうことに起因するようです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2921137/18065ec7-043e-3690-0b98-f187c0098fda.png)
↑動かない例

非常にコードが読みにくくなりますが、インデントと1行以上の空行を**全て**削除してあげると動くようになります

# 今回作ろうとしたWebアプリを改良するなら…
Markdown AIのサーバーAI機能は、実行してから返答が返ってくるまで数秒程度の時間がかかります

つまり通行人が通り過ぎた後に喋り出してしまうんですね、はい…
実際に使用するためには、第一声は定型文にする等の対策が必要だと思います

音声認識を導入し、簡易的な応対機能をつけても面白いでしょう

# おわりに
Qiitaのアドベントカレンダーって大晦日まで投稿できるって知ってました？

私、恥ずかしながら知らなかったので、25日の23時58分に慌てて中途半端な記事を投稿してしまいました…
期待してリンクを踏んでもらった方々、大変申し訳ないです
心よりお詫び申し上げます

本当は、今回制作しようとしたWebアプリを紹介して、記事を締めたいと思っていたのですが、成果物が無くなってしまったので、記事中でも少し触れたMarkdown AIの仕様変更について考察したいと思います

曖昧な記憶とかすかな履歴が頼りなのですが、公開したページのレンダリングが、記事作成画面で表示されるプレビュー画面のレンダリングと同様になるように変更されている気がしています

以前はMarkdown AI内で記述した内容はbodyタグ直下あたりで読み込まれていたはずなのですが、今はプレビュー画面での表示と同じ、class名が"text-preview"のdiv要素内で読み込まれています

このあたりの変更がJavaScriptの動作に影響を与えているんじゃないかなぁ…？と思っていますが、お手上げ状態です

いつかまた、今回のWebアプリをMarkdown AIで動かしたいです

最後までお読みいただきありがとうございました


# 参考文献
OpenCV.jsを使った動体検知の実装

https://blog.kimizuka.org/entry/2023/02/07/234930