20250527_活動報告

活動報告

はじめに

みなさんこんにちは。№です。

先週はデュエマのGPに行っていました。予選完走できたのでよかったです。

ボードゲーム

「It’s a wondeful world」や「DORASURE」などが遊ばれていました。

カードゲーム

「デュエルマスターズ」や「遊☆戯☆王」、「WIXOSS」などが遊ばれていました。

私は4時間くらい天門vsファイアー・バードをしました。

ハンプティ・ルピアとかいう謎の鳥、本当に何なんですかね。

おわりに

今日は人が少なかったです。テスト期間だからですかね。

マイコン研では、毎週火曜日と金曜日にサークル会館の256にて活動を行っています。

来週くらいから自己紹介ゼミがまた始まるので、ぜひ来てほしいです。

それでは。

皆さん、「自動化」してますか?

勤勉なマイクロコンピューター研究会の会員の皆様なら、マイコンなどを駆使して日々色々作ったり作らなかったりしているのでは無いでしょうか。

マイコンを用いることで、色々なことが出来ると思います。テレビと電気を同時につけたり、今日の天気を教えてくれたり(適当)

でも、こうは思いませんか?

「ボタンを押すのって面倒だな」と。

ボタン一つで色々出来るようにしても、そのボタンを押すのが面倒なんですよ。

前置きが長くなりましたが、今回は「スマートスピーカー」について語っていこうと思います。

マイコン研らしく、色々なものを遠隔で、手を触れずに操作可能な次世代のデバイスです。

会社としては採算が取れないので部署を縮小しているという噂もありますが・・・

これを用いることで、声によってクラウド上で処理を行い、喋らせることが出来ます!

googleのスマートスピーカーのスキル開発機能は約2年前にサ終しました。許せません。

Alexaのスキル開発は今も現役で、AWS上で動作するpython又はNode.jsによって記述出来ます。

もしあなたが今からAlexaのスキル開発に手を出すなら、Node.jsの方で書くことをおすすめします。

なぜなら、pythonの方はほぼ情報がインターネット上に無いからです。

ちなみに、Node.jsの方もそんなに情報があるわけでは無いです。

数年前に後方互換の無い謎アプデが入ったせいで、ある時期よりも昔のコードはコピペしても動かないことがあります。なぜそうなったのか
これのせいでChatGPTが昔の、動かないコードを出すことがあります。許せない

クラウド上で動くので、特にAlexa本体以外に買い足すことなく開発を行うことが出来ます。

開発もクラウド上で行えるので、ipadからでも、パソコンからでもいつでもどこでも開発が出来るのも便利です。頑張ればスマホからでも出来ます。

せっかくなのでAlexaからChatGPTを呼び出して会話するコードを貼っておきます。

/* *
 * This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
 * Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
 * session persistence, api calls, and more.
 * */
const Alexa = require('ask-sdk-core');
const { Configuration, OpenAIApi } = require("openai");
const keys = require('keys');
const persistenceAdapter = require('ask-sdk-s3-persistence-adapter');

//パブリックな変数を宣言。
let messages_log; //メッセージのログを格納する。画面表示用
let conversation_log;//メッセージのログ。GPTとのAPIで使う用。
let pre_answer;//回答の保存。繰り返しに使う。

const configuration = new Configuration({
    //apiKey直書き。面倒だし
    apiKey: "api_key"
});
const openai = new OpenAIApi(configuration);

function insertBr(text, interval) {
  // 文字列を配列に変換
  const chars = text.split('');

  // 改行カウンタを初期化
  let brCount = 0;

  // 結果となる空文字列を初期化
  let result = '';

  // 各文字を処理
  for (let i = 0; i < chars.length; i++) {
    // 文字を追加
    result += chars[i];

    // 文字間隔に達したら改行タグを追加
    if ((i + 1) % interval === 0) {
      result += '<br>';
      brCount++;
    }
  }

  // 改行タグの数が偶数になるように調整(する必要を感じないのでコメントアウト)
//   if (brCount % 2 !== 0) {
//     result += '<br>';
//   }

  return result;
}

// apiトークン辺りの処理
async function getAnswer(messages) {
  const response = await openai.createChatCompletion({
    model: "gpt-4o-mini",//ここにバージョンを書く
    messages: messages,
    max_tokens:250
});

  return response.data;
}
function formatString(text) {
  try {
    return text.replace(/\n+/g, " ");
  } catch (error) {
    return text;  // エラーが発生した場合、元のテキストを返す
  }
}


const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = 'ようこそ。';
        
        messages_log = "";
        
        conversation_log = [{role:"assistant",content:speakOutput}]
        
        if (Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)['Alexa.Presentation.APL']){
            console.log("ユーザーのデバイスはAPLに対応しています");
            const documentName = "HelloWorldDocument"; // オーサリングツールに保存されたドキュメントの名前
            // RenderDocumentディレクティブを応答に追加します
            handlerInput.responseBuilder.addDirective({
                type: 'Alexa.Presentation.APL.RenderDocument',
                token: "documentToken",
                document: {
                    src: "doc://alexa/apl/documents/HelloWorldDocument",
                    type: 'Link'
                },
                datasources: {
                    "TextDataSource": {
                        "text": "知りたいことを質問してください",
                    }
                }
            });
            
        } else {
            // デバイスがAPLに対応していないことをログに記録するだけです。
            // 実際のスキルでは、ユーザーに別の内容を読み上げることもできます。
            console.log("ユーザーのデバイスはAPLに対応していません。画面付きのデバイスで再テストしてください")
        }
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};



//回答用handler
const ChatGPTIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'ChatGPTIntent';
    },
    async handle(handlerInput) {
        // 質問内容を回収
        const question = Alexa.getSlotValue(handlerInput.requestEnvelope, 'question');
        
        // // if(question.included("に変更")){
        // //     return handlerInput.responseBuilder
        // //     .speak("GPTのバージョンを変更します。")
        // //     .reprompt('他に質問はありますか?')
        // //     .getResponse();
        // // }else{
            
        // // }
        
        
        // 配列に追加
        conversation_log.push({role:"user",content:question});
        
        //  GPTに投げる
        let response = await getAnswer(conversation_log);
        // const response = conversation_log;
        let speakOutput = formatString(response.choices[0].message.content);
        
        response = response.choices[0].message.content
        
        conversation_log.push({role:"assistant",content:speakOutput});
        
        pre_answer = speakOutput
        
        messages_log += "<br>" + insertBr(question,25) + "<br>" + insertBr(speakOutput,25);
        
        
        if (Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)['Alexa.Presentation.APL']){
            console.log("ユーザーのデバイスはAPLに対応しています");
        
            const documentName = "HelloWorldDocument"; // オーサリングツールに保存されたドキュメントの名前
            // const token = documentName + "Token";
        
            // RenderDocumentディレクティブを応答に追加します
            handlerInput.responseBuilder.addDirective({
                type: 'Alexa.Presentation.APL.RenderDocument',
                token: "documentToken",
                document: {
                    src: "doc://alexa/apl/documents/HelloWorldDocument",
                    type: 'Link'
                },
                datasources: {
                    "TextDataSource": {
                        "text": messages_log,
                        
                    }
                }
            });
            
        //下にスクロール
        handlerInput.responseBuilder.addDirective({
            type:'Alexa.Presentation.APL.ExecuteCommands',
            token:"documentToken",
            commands:[{
                type:"ScrollToComponent",
                "align": "last",
                // "index":0,
            }]
        });
        
            
        } else {
            // デバイスがAPLに対応していないことをログに記録するだけです。
            // 実際のスキルでは、ユーザーに別の内容を読み上げることもできます。
            console.log("ユーザーのデバイスはAPLに対応していません。画面付きのデバイスで再テストしてください")
        }
        
        return handlerInput.responseBuilder
            .speak(response)
            .reprompt('他に質問はありますか?')
            .getResponse();
    }
};



const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'You can say hello to me! How can I help?';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = 'Goodbye!';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
/* *
 * FallbackIntent triggers when a customer says something that doesn’t map to any intents in your skill
 * It must also be defined in the language model (if the locale supports it)
 * This handler can be safely added but will be ingnored in locales that do not support it yet 
 * */
const FallbackIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'Sorry, I don\'t know about that. Please try again.';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
/* *
 * SessionEndedRequest notifies that a session was ended. This handler will be triggered when a currently open 
 * session is closed for one of the following reasons: 1) The user says "exit" or "quit". 2) The user does not 
 * respond or says something that does not match an intent defined in your voice model. 3) An error occurs 
 * */
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`);
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse(); // notice we send an empty response
    }
};
/* *
 * The intent reflector is used for interaction model testing and debugging.
 * It will simply repeat the intent the user said. You can create custom handlers for your intents 
 * by defining them above, then also adding them to the request handler chain below 
 * */
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
        const speakOutput = `You just triggered ${intentName}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
/**
 * Generic error handling to capture any syntax or routing errors. If you receive an error
 * stating the request handler chain is not found, you have not implemented a handler for
 * the intent being invoked or included it in the skill builder below 
 * */
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        const speakOutput = 'Sorry, I had trouble doing what you asked. Please try again.';
        console.log(`~~~~ Error handled: ${JSON.stringify(error)}`);

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

/**
 * This handler acts as the entry point for your skill, routing all request and response
 * payloads to the handlers above. Make sure any new handlers or interceptors you've
 * defined are included below. The order matters - they're processed top to bottom 
 * */
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        // ReturnIntentHandler,
        ChatGPTIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        FallbackIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler)
    .addErrorHandlers(
        ErrorHandler)
    .withCustomUserAgent('sample/hello-world/v1.2')
    .lambda();

このコードは、また別の概念であるAlexa Presentation Language(APL)というものを使用しているため、このままでは動きません。ごめんなさい。画面付き機体を使うときはこれが関わってくるので、とても面倒です。これもネット上に情報が少ないです。悲しい。

これはスマートスピーカーあるあるなんですけど、ここまで頑張って作ってもスマートスピーカーの判定の問題で自作スキルは起動しにくいんですよね。

みんなもスマートスピーカー、使おう!

PS.少し技術的な話をしたかったので書きました。ここまで読んでくれてありがとうございます。

AlexaとFireTVを同時に使用している条件下で、AlexaがTVを認識していると、TVでスキルを起動する傾向があるので、多彩なTVのスキルと自作スキルが混在して判定がずれることがある。