Google AI会話機能ビルドイン製作セット@トランジスタ技術寄稿

トラ技2017 11月号掲載 Google AI会話機能ビルドイン製作セット

 

プログラム一式は下記。

だんご@GitHub

 

記事で書ききれなかった部分の補足を下記に。

 

Cloud Speech APIのAccess Tokenを取得する

今回、音声のテキスト化や翻訳機能の全てをGoogleに任せています。

GoogleはGoogle Cloud Platform(通称GCP)というCloudサービスを提供しています。

これを使用するにはGoogle Platform Consoleを使用します。

今回使用するGCPのサービス(Cloud Speech API & Cloud Translate API)はAccess Tokenを使用するので、GCPで取得する手順を紹介します。

 

◆Cloud Platform Consoleでプロジェクトを作成する

GCP

①GCPにアクセスします。アカウントを持っていない方は作ってください。→ コンソールを開くをクリック

 

②下記のようなGCPのページに移動します。

・左上のメニューから課金設定を有効化します。

・クレジットとして、3万円分Googleからもらえるので有効化しても最初は問題ありません。(3万円クレジットは個人で実験として使う分には使いきれない量の金額になります)

 

③ダッシュボードから使用するAPIを有効化します。

・今回はCloud Speech API、Cloud Translation APIを使用するのでこの二つを有効化します。

 

④コードからCloud Speech APIの承認を受けるために、サービスアカウントの設定を行います。

詳細が下記にあるので、手順通りに手続きを進めてください。

サービスの有効化

簡単に流れを説明すると下記のようになります。

左上のメニューからAPIマネージャーに移動 → 左の項目から認証情報のタブを選択 → 認証情報の作成をクリック → サービスアカウントキーをクリック → (サービスアカウントキーの作成パネルが出てきます) → サービスアカウント:[新しいサービスアカウント]、サービス名:[好きな名前を入力]、サービスID:[変更しない]、キーのタイプ:[JSON] → 作成ボタンを押す

下記のようなJSONファイルがダウンロードできます

{
  "type": "service_account",
  "project_id": "project-id",
  "private_key_id": "some_number",
  "private_key": "-----BEGIN PRIVATE KEY-----\n....
  =\n-----END PRIVATE KEY-----\n",
  "client_email": "<api-name>api@project-id.iam.gserviceaccount.com",
  "client_id": "...",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/...<api-name>api%40project-id.iam.gserviceaccount.com"
}

 

プロジェクトに戻って、認証情報を作成をクリック → APIキーをクリック

 

④Google Cloud PlatformからCloud Speech APIのAccess Tokenをとってくる

・Cloud Speech APIを使用するにはAccess Tokenが必要です。

・Access Tokenを入手するにはふた通りの方法があります。

***Google Cloud SDKを自分のPCにインストールして使用する

下記のチュートリアル通りに進めていけばインストールできます。

Cloud SDK

***Cloud Shellを使用する

GoogleのCloud環境でShellを動かすことができるので、そこでAccess Tokenを入手することもできます。

こちらの方法の方が非常に簡単なのでおすすめです。

書き方Cloud Shellにアクセスしてください。

Cloud Shell

 

下記コマンドで、先ほどダウンロードしたサービスアカウントキーの認証を行います

 

gcloud auth activate-service-account --key-file=service-account-key-file

 

下記コマンドでアクセストークンを取得できます

gcloud auth print-access-token

 

アクセストークンをプログラムに組み込みます。

AccessTokenの宣言があるので、内容を変更してください。下記のような例になります。

/*
 * ### Google API Access token
 * How to Get Access token : https://cloud.google.com/docs/authentication?hl=ja
 */
#define AccessToken "ya29.El_tBAN_1ptLuxTSUpd0EqAmxIBS7M4wKzHb0lKDYC4DbXavLxaIUukk879u9GaFm2KuRQRMNFHhaJR3KbWR5PVaPwMcdNP0m3zcQeucz85dkFd8QkyNBekvKXDmN56RbA"

アクセストークンは一定時間が過ぎると認証が切れてしまいます。
切れたら随時更新しましょう。

 

簡単なプログラムの紹介

今回プログラムが長かったので記事には一切プログラムを載せていません。軽く機能実装方法などに触れた程度でした。

補足説明を下記に。

 

◆BASE64 ENCODE / DECODE

Cloud Speech APIにデータを投げる時にBASE64でエンコードしますが、このとき使っているAPIはオープンソースのものになります。

確かそのままじゃ使用できなかったのでちょい変した記憶がかすかにありますが、どこを変更したか忘れてしまいました。

元のコードは下記にあります。

BASE64code

 

◆高速ADC

・音声録音のために16kHz 16bitの高速サンプリングをADCで行なっています。

・ESP-IDFで用意されているvTaskDelayという遅延関数がありますが、この関数ではus単位のディレイを発生させるのが不可能なので、今回は空のfor文を回してディレイを作っています。

・for文の回数はsystemClockによって変える必要があります。私のプログラムの例では240MHzでCPUを回している時の例になります。

・ADCは12bitで値を取得するように設定しています。録音の精度を高めるため、ESP32で取得可能な最大bit数で取得しています。

・16bitのビット深度で録音する必要があります。その後1Byte(8bit)ずつ処理する部分があるので、Char型(Size 1Byte)の変数に6bitずつデータを詰めています。

・16bit(char型*2 = 16bit)の箱に12bitを詰めるのでセンターをずらしています。

・録音を開始する前に、配列の先頭にヘッダーデータを詰めておきます。今回は決め打ちの値(1秒録音)を入れていますが、本当は動的に変わるようにすべきです。

 

以上の部分までのプログラムが下記になります。

/*------main関数-----***/
    /* initialize ADC */
    adc1_config_width(ADC_WIDTH_12Bit);
    adc1_config_channel_atten(ADC1_TEST_CHANNEL,ADC_ATTEN_11db);
    printf("The adc1 value:%d\n",adc1_get_voltage(ADC1_TEST_CHANNEL));
    
    
    for(int i = 0; i<sizeof(header); i++ ){
        
        audio_test[i] = header[i];
        
    }
    
    /* start adc task (RECORDING) */
    xTaskCreate(adc1task, "adc1task", 2048, NULL, 1, adcEnd);
/*-----main関数-----*/


char flag = 1;

void adc1task(void* arg)
{
    
    while(1){
        
        for (unsigned int loop_index=0; loop_index<1300; loop_index++){};
        
        if(bufferCount <= bufferLength){ gpio_set_level(GPIO_OUTPUT_IO_0, 1); analogVal = adc1_get_voltage(ADC1_TEST_CHANNEL); audio_test[51 + bufferCount] = (unsigned char)(analogVal>>2);
            bufferCount++;
            audio_test[51 + bufferCount] = (unsigned char)(analogVal<<2); bufferCount++; }else if(flag && bufferCount>=bufferLength+1){
            gpio_set_level(GPIO_OUTPUT_IO_0, 0);
            
            mbedtls_base64_encode(base64_buffer, sizeof(base64_buffer)+1, &len, audio_test, sizeof(audio_test));
            
            flag = 0;
            
        }else{
            
            ESP_ERROR_CHECK( nvs_flash_init() );
            initialise_wifi();
            
            /* Cloud Speech API task start */
            xTaskCreate(&https_get_task, "https_get_task", 8192, NULL, 2, wifiEnd);
            
            /* Audio REC task end */
            vTaskDelete(adcEnd);
            
        }
        
    }
    
}

 

◆HTTPリクエストの分割

ESP-IDFでは12000Byte以上のデータを一気にリクエストで送れませんでした。

(ちゃんと関数みていないのですが、さらっとみた感じたどそんな制約なかったような…)

そのため、データ量が12000Byteを超える場合はリクエストを分割して送信しています。

 

以上の部分が下記になります。

            /* JSON half first */
            while((ret = mbedtls_ssl_write(&ssl, (const unsigned char *)SPEECH_API_REQ_1, strlen(SPEECH_API_REQ_1))) <= 0){
                if(ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE){
                    goto exit;
                }
            }
            
            len += ret;
            
            /*
             * ### Request content
             * mbedtls_ssl_writeで送信可能なデータ上限を超えないように分割して送る
             */
            static char req_buffer_size = sizeof(base64_buffer) / 12000;
            static unsigned int req_buffer_size_surpl = sizeof(base64_buffer) % 12000;
            static char req_count = 0;
            
            for(req_count=0; req_count<req_buffer_size; req_count++){
                while((ret = mbedtls_ssl_write(&ssl, &(base64_buffer[12000*req_count]), 12000)) <= 0){
                    if(ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE){
                        goto exit;
                    }
                }
            }
            
            while((ret = mbedtls_ssl_write(&ssl, &(base64_buffer[12000*req_count]), req_buffer_size_surpl-2)) <= 0){
                if(ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE){
                    goto exit;
                }
            }
            
            req_count = 0;
            len += ret;
            
            /* JSON half last */
            while((ret = mbedtls_ssl_write(&ssl, (const unsigned char *)SPEECH_API_REQ_2, strlen(SPEECH_API_REQ_2))) <= 0){
                if(ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE){
                    goto exit;
                }
            }

 

 

◆RESPONSの処理

HTTPリクエストを送信すると、GoogleからJSONのデータが帰ってきます。

これを分解するのですが、今回用があるのは音声がテキスト化されたデータと、翻訳データなので、その部分のみ取り出すような乱暴なプログラムにしています。性格が出ています。ちゃんと処理したい方は書き直してください。

 

以上の部分が下記になります。

       do{
            len = sizeof(buf) - 1;
            bzero(buf, sizeof(buf));
            ret = mbedtls_ssl_read(&ssl, (unsigned char *)buf, len);
            
            if(ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE)
                continue;
            
            if(ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY){
                ret = 0;
                break;
            }
            
            if(ret < 0){
                break;
            }
            
            if(ret == 0){
                break;
            }
            
            len = ret;
            /* Print response directly to stdout as it is read */
            for(int i = 0; i < len; i++){
                putchar(buf[i]);
            }
            
            char *jsonBuffer;
            char translations[64];
            char div[] = "\"";
            
            jsonBuffer = strtok(buf,div);
            
            while(jsonBuffer != NULL){
                
                printf("%s\n",jsonBuffer);
                jsonBuffer = strtok(NULL,div);
                
                if(jsonBuffer != NULL){
                    /* Cloud Speech APIの結果を取り出す */
                    if(strcmp(jsonBuffer,"transcript") == 0){
                        
                        jsonBuffer = strtok(NULL,div);
                        jsonBuffer = strtok(NULL,div);
                        
                        strcpy(transcript,jsonBuffer);
                        
                        if(SpeeechTrancrateSW){
                            SpeeechTrancrateSW = 0;
                            mbedtls_ssl_close_notify(&ssl);
                            
                            /* Cloud Transrate API task start */
                            xTaskCreate(&https_get_task, "https_get_task_tranlate", 8192, NULL, 5, NULL);
                            
                            /* Cloud Speech API task end */
                            vTaskDelete(wifiEnd);
                        }
                    }
                    /* Cloud Transrate APIの結果を取り出す */
                    else if(strcmp(jsonBuffer,"translatedText") == 0){
                        
                        jsonBuffer = strtok(NULL,div);
                        jsonBuffer = strtok(NULL,div);
                        
                        strcpy(translations,jsonBuffer);
                        
                        static int dicCount=0;
                        char sample_buffer[8];
                        
                        sample_buffer[3] = '\0';
                        //printf("TEST 00 : %s\n", sample_buffer);
                        for(int i=0;i<(strlen(translations)/3);i++){
                            
                            strncpy(sample_buffer,translations+(i*3),3);
                            sample_buffer[3] = '\0';
                            
                            printf("R");
                            for(dicCount=0;dicCount<dicIndex;dicCount++){
                                if(strcmp(sample_buffer,dic[dicCount].kana) == 0){
                                    strcat(romaji,dic[dicCount].roma);
                                }
                            }
                            dicCount = 0;
                        }
                        
                        /* ATP3011 play Audio */
                        echo_task(5);
                        
                    }
                }
            }
        } while(1);

 

 

◆UARTのchannel 1or2 を使用する

・UARTのchannel0はデバッグ用で使用するので、UART1か2を使用して音声合成ICと通信を行います。

・音声合成ICはアルファベットデータしか受け付けないので、ひらがな→アルファベット変換の辞書を用意します。

 

以上の部分が下記になります。

/*
 * ひらがな⇆ローマ字変換dictionary
 * 文字code : UTF-8
 * 頑張れば漢字なんかもおしゃべりさせることができるかもしれない...(手記はここで途絶えている)
 */
#define dicIndex 71
struct convertDic{
    char kana[5];
    char roma[3];
};
struct convertDic dic[dicIndex] = {
    {"あ","a"},{"い","i"},{"う","u"},{"え","e"},{"お","o"},
    {"か","ka"},{"き","ki"},{"く","ku"},{"け","ke"},{"こ","ko"},
    {"が","ga"},{"ぎ","gi"},{"ぐ","gu"},{"げ","ge"},{"ご","go"},
    {"さ","sa"},{"し","si"},{"す","su"},{"せ","se"},{"そ","so"},
    {"ざ","za"},{"じ","zi"},{"ず","zu"},{"ぜ","ze"},{"ぞ","zo"},
    {"た","ta"},{"ち","ti"},{"つ","tu"},{"て","te"},{"と","to"},
    {"だ","da"},{"ぢ","di"},{"づ","du"},{"で","de"},{"ど","do"},
    {"な","na"},{"に","ni"},{"ぬ","nu"},{"ね","ne"},{"の","no"},
    {"は","ha"},{"ひ","hi"},{"ふ","hu"},{"へ","he"},{"ほ","ho"},
    {"ば","ba"},{"び","bi"},{"ぶ","bu"},{"べ","be"},{"ぼ","bo"},
    {"ぱ","pa"},{"ぴ","pi"},{"ぷ","pu"},{"ぺ","pe"},{"ぽ","po"},
    {"ま","ma"},{"み","mi"},{"む","mu"},{"め","me"},{"も","mo"},
    {"や","ya"},{"ゆ","yu"},{"よ","yo"},
    {"ら","ra"},{"り","ri"},{"る","ru"},{"れ","re"},{"ろ","ro"},
    {"わ","wa"},{"を","wo"},{"ん","nn"}
};


/*
 * use UART1 port
 * baudRate : 9600bps
 * parity   : none
 * stop bit : 1
 * flow ctrl: none
 */
static void echo_task(int voice_num){
    
    const int uart_num = UART_NUM_2;
    
    uart_config_t uart_config = {
        .baud_rate = 9600,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .rx_flow_ctrl_thresh = 5500,
    };
    
    uart_param_config(uart_num, &uart_config);
    uart_set_pin(uart_num, ECHO_TEST_TXD, ECHO_TEST_RXD, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(uart_num, BUF_SIZE * 2, 0, 0, NULL, 0);
    
    const char data1[19] = {'k','i','k','i','t','o','r','e','m','a','s','e','n','d','e','s','i','t','a'};
    const char data2[9] = {'k','o','n','n','i','t','i','w','a'};
    const char data3[8] = {'k','o','n','b','a','n','w','a'};
    const char data4[14] = {'d','o','u','i','t','a','s','i','m','a','s','i','t','e'};
    const char data5[8] = {'g','u','n','n','m','o','n','i'};
    const char ends[2] = {'\r','\n'};
    switch(voice_num){
        case 0:
            uart_write_bytes(uart_num, (const char*) data1, sizeof(data1));
            break;
        case 1:
            uart_write_bytes(uart_num, (const char*) data2, sizeof(data2));
            break;
        case 2:
            uart_write_bytes(uart_num, (const char*) data3, sizeof(data3));
            break;
        case 3:
            uart_write_bytes(uart_num, (const char*) data4, sizeof(data4));
            break;
        case 4:
            uart_write_bytes(uart_num, (const char*) data5, sizeof(data5));
            break;
        case 5:
            uart_write_bytes(uart_num, (const char*) romaji, sizeof(romaji)-1);
            break;
        default:
            break;
    }
    
    uart_write_bytes(uart_num, (const char*) ends, sizeof(ends));
    vTaskDelay(500 / portTICK_PERIOD_MS);
    
}

 

これ以外はほとんどESP-IDFのwifi HTTPSリクエストのプログラムのままなのでじっくりAPI仕様書と一緒に読むと理解できると思います。Good Luck!

コメントを残す

メールアドレスが公開されることはありません。