Azure Static Web AppsのVue.jsアプリに環境変数を設定する方法

Azure Static Web Appsは便利ですごく使いやすいんだけど環境変数を設定するのはまだ簡単ではなさそうだったので、設定方法を備忘録として残しておきます。

まず結論から

Azure Static Web Appsリソースを作成すると自動生成されるGitHub Actionsのワークフローファイルを使って環境変数を記載します。そのため、GitHubにアップロードするコード内に本番環境の環境変数が含まれることになってしまいますが、フロントエンドで動くもので基本的に機密情報ではないのでよしとします。類似サービスであるApp Serviceを使った感覚からするとAzure portalの「アプリケーション設定」で環境変数を設定できれば良さげなのですが、執筆時点はその機能はないそうです。

公式ドキュメントにも記載があります。

フロントエンド Web アプリケーションの構築に必要な環境変数を構成するには、「ビルドの構成」を参照してください。

Azure Static Web Apps のアプリケーション設定を構成する | Microsoft Learn

より良い方法については議論が続いているようです。

実際にやってみる

サンプルアプリについて

「VUE_APP_TEXT」という環境変数の文字列を表示するシンプルなVue.jsアプリです。vue create で生成されるコードをほぼそのまま使っています。ソースコードはGitHubに置いています。

手順の流れ

  1. GitHubにVueアプリを用意する
  2. Azure Static Web Appsのリソースを作成する
  3. .envファイルに記載している環境変数の値が表示されることを確認する
  4. 自動生成されたワークフローファイルに環境変数を記載する
  5. 変更分をGitHubにpushする
  6. ワークフローファイルに記載した環境変数の値が表示されることを確認する

1. GitHubにVueアプリを用意する

サンプルアプリをforkするか、ソースコードをダウンロードして自身のリポジトリを作成します。

2. Azure Static Web Appsのリソースを作成する

普通にリソースを作成します。「ビルドのプリセット」は [Vue.js] を選択してください。GitHubアカウント関連はご自身のリポジトリを指定してください。その他は特筆すべきことはありませんが、参考までにリソース作成時のスクリーンショットを載せておきます。

リソースを作成すると、下の画像のようにGitHub Actionsでデプロイ処理が実行している様子を確認できます。

3. .envファイルの環境変数の値が表示されることを確認する

作成されたリソースのWebサイトにアクセスします。下の画像のように「text from local environment variable」と表示されていることを確認します。これはソースコードの「.env」ファイルが反映されています。

4. 自動生成されたワークフローファイルに環境変数を記載する

この記事のメインです。リソース作成時に自動生成される「.github/workflows」フォルダ内のymlファイルに環境変数に関する記述を追加します。下のコードの最後の2行(env句)が追加する部分です。(参考

      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          ...
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: "/" # App source code path
          api_location: "" # Api source code path - optional
          output_location: "dist" # Built app content directory - optional
          ###### End of Repository/Build Configurations ######
        env: # Add environment variables here
          VUE_APP_TEXT: "text from GitHub workflow environment variable"

5. 変更分をGitHubにpushする

ymlファイルの変更をcommitしGitHubにpushします。再びGitHub Actionsでデプロイ処理が実行している様子を確認できます。

6. ワークフローファイルに記載した環境変数の値が表示されることを確認する

Webサイトにアクセスし、テキストが「text from GitHub workflow environment variable」となっていれば成功です。

外部API呼び出しを定期実行する処理をAzure Functionsで実装する(C#)

タイトルの通りです。サーバがあれば実行ファイルを置いてcronで実行させるのが手っ取り早いですが、クラウドのサービスを使って手軽にできないか試したら意外と簡単だったのでその時の備忘録です。1時間に1回の時報をslackに通知する、というケースで書いています。

必要なもの

  • Azureアカウント
  • Visual Studio
    • 今回は Visual Studio 2022を使いました。

1. Azureポータルで関数アプリを作成する

  • Azureポータルにアクセス
  • Azureサービスの一覧から [関数アプリ] を選択
  • [+作成] をクリック
  • 以下の画像の通りに必要な情報を入力
    • 今回のケースでは関数アプリ名は [jihou] とした
  • [作成] をクリック

2. Visual Studioでコードを書く

  • 新しいプロジェクトを作成
  • プロジェクト名を入力
  • [Timer trigger] を選択
    • 各時間の0分に通知してほしいので [Schedule] には「0 0 * * * *」と書く(記法について
  • コードを書く。
    • 17行目の変数 [url] にAPIのURLを代入
    • 18行目の変数 [payload] にPOSTするデータを代入
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;

namespace Jihou
{
    public class JihouHour
    {
        [FunctionName("JihouHour")]
        public void Run([TimerTrigger("0 0 * * * *")]TimerInfo myTimer, ILogger log)
        {
            log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");

            var url = "https://hooks.slack.com/services/*******************************";
            var payload = new Payload
            {
                text = DateTime.Now.ToString("yyyy/MM/dd HH") + "時です。"
            };

            var json = JsonSerializer.Serialize(payload);

            var client = new HttpClient();
            var content = new StringContent(json, Encoding.UTF8, "application/json");
            var res = client.PostAsync(url, content).Result;
            log.LogInformation(res.ToString());
        }
    }

    public class Payload
    {
        public string text { get; set; }
    }
}

尚、その場で動作を確認したい場合はVisual Studioのデバッグ([F5]キー)機能が使えます。その際は実行間隔を短く設定すればすぐに動作を確認できます。例えば [TimerTrigger("0 */1 * * * *")] とすれば1分ごとに実行してくれます。

3. コードをAzureにアップロードする

  • Visual Studio のプロジェクト名を右クリックして [発行] をクリック
  • ターゲットとして [Azure] を選択
  • [Azure Funciton App (Windows)] を選択
  • 冒頭で作成した関数アプリ [jihou] を選択
  • 公開の準備が完了
  • [発行] を押すとAzureにデプロイされる

確認

1時間に1回slackに通知が来ます。

参考

WPFアプリケーションの公開作業をAzure Pipelinesを使って自動化する

前回の記事で行った作業を自動化できたらいいなと思っていたらAzure Pipelinesでいい感じにできたので、その時の記録です。Azureの他のサービスとの連携が簡単で、Azure Blob Storageにアップロードする作業も簡単に書くことができました。

準備

前回の記事の続きとして書くので、記事の中で作成したWPFアプリケーションが既にあることを前提とします。今回の作業では、前回の「公開」作業を行った時点で生成される「ClickOnceProfile.pubxml」を使います。Azure Blob Storageも前回と同じコンテナーを使う想定で書きます。

必要なもの

前回の記事に加えて、本記事では以下のもの利用します。

記事の構成

この記事は以下の9つの手順でまとめます。

  1. プロジェクトにランタイムを指定する
  2. Azure DevOpsでプロジェクトを作成する
  3. Azure Reposにコードをプッシュする
  4. Azure Pipelinesでパイプラインを作成し、 Gitリポジトリと紐づける
  5. Pipelineを編集する
  6. Pipelineを実行する
  7. バージョンアップしてGitリポジトリにプッシュする
  8. Pipelineが自動実行することを確認する
  9. アプリが自動更新するか試す

1. プロジェクトにランタイムを指定する

SimpleWPFApplication.csprojRuntimeIdentifiers タグを追加します。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    ...
    <RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
    ...
  </PropertyGroup>
</Project>

ちょっと説明

RuntimeIdentifiers タグを追加しておかないと、手順6でパイプラインを実行したときに以下のようなエラーが出てしまいます。

##[error]C:\Program Files\dotnet\sdk\5.0.402\Sdks\Microsoft.NET.Sdk\targets\Microsoft.PackageDependencyResolution.targets(241,5): Error NETSDK1047: Assets file 'D:\a\1\s\SimpleWPFApplication\obj\project.assets.json' doesn't have a target for 'net5.0-windows/win-x64'. Ensure that restore has run and that you have included 'net5.0-windows' in the TargetFrameworks for your project. You may also need to include 'win-x64' in your project's RuntimeIdentifiers.

このエラーを避けるため、エラーメッセージで勧められている通りランタイムを指定しておきます。

2. Azure DevOpsでプロジェクトを作成する

Azure DevOpsの自分の組織(Organization)のページを開き、 [+ New project] から新しいプロジェクトを作成します。
今回は [SimpleWPFApplication] という名前で作成しました。

3. Azure Reposにコードをプッシュする

プロジェクトの中にいくつかアイコンがありますが、その中の [Repos] を開きます。

クローンしたりプッシュするためのURLやコマンド例が載せられているので、手元の Git クライアントを使ってコードをプッシュします。手順は省略します。ちなみに .gitignore はGitHub公式のこちらが役に立ちます。注意点として、この .gitignore は今回使いたい pubxml ファイルが含まれているので pubxml をGit管理できるように変更する必要があります。

プッシュするとReposの画面は以下のようになります。

4. Azure Pipelinesでパイプラインを作成し、 Gitリポジトリと紐づける

プロジェクトの中から [Pipelines] を開き、 [Create Pipeline] から新しいパイプラインを作成します。接続先を選択する画面では、 [Azure Repos Git] から先程作成したリポジトリを選択します。ちなみに、候補にあるGitHubやBitbucketなどのGitホスティングサービスと連携することもできます。試しにGitHubとの連携をやってみましたがとても簡単にできました。以降の手順はそれらのサービスと連携した場合も同じように行えます。

5. Pipelineを編集する

この記事のメインです。パイプラインのテンプレートを選べるので [.NET Desktop] を選択します。

そうすると画面上で直接yamlが編集できるようになるので、以下のように編集します。yamlの編集が完了したら [Save] します。

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pr: none

pool:
  vmImage: 'windows-latest'

variables:
  solution: 'SimpleWPFApplication.sln'
  project: 'SimpleWPFApplication\SimpleWPFApplication.csproj'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSBuild@1
  displayName: 'Publish'
  inputs:
    solution: '$(project)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    msbuildArgs: '-t:publish -p:PublishProfile=ClickOnceProfile.pubxml -p:PublishDir="bin\publish\\"'

- task: AzureFileCopy@3
  displayName: 'Upload setup.exe'
  inputs:
    SourcePath: 'SimpleWPFApplication\bin\publish\setup.exe'
    azureSubscription: '(サブスクリプション名)'
    Destination: 'AzureBlob'
    storage: '(ストレージアカウント名)'
    ContainerName: '$web'

- task: AzureFileCopy@3
  displayName: 'Upload SimpleWPFApplication.application'
  inputs:
    SourcePath: 'SimpleWPFApplication\bin\publish\SimpleWPFApplication.application'
    azureSubscription: '(サブスクリプション名)'
    Destination: 'AzureBlob'
    storage: '(ストレージアカウント名)'
    ContainerName: '$web'

- task: AzureFileCopy@3
  displayName: 'Upload Application Files'
  inputs:
    SourcePath: 'SimpleWPFApplication\bin\publish\Application Files'
    azureSubscription: '(サブスクリプション名)'
    Destination: 'AzureBlob'
    storage: '(ストレージアカウント名)'
    ContainerName: '$web'
    BlobPrefix: 'Application Files'

ちょっと説明

ブラウザの右側にある [Tasks] から使いたいタスクを選択し、フォーム入力すればタスクのyamlを自動生成してくれます。まずはこの機能を使ってyamlを自動生成し、必要であれば直接編集する、という流れが良いと思います。

pr: none はプルリクエストを作成した時にパイプラインを実行しないように指定します。

ClickOnce公開のためのコマンドは公式ページを参考にしました。yamlでは [VSBuild] となっていますが、おそらくMSBuildを表していて同じオプションで動きました。PublishProfileオプションを使えば、Visual Studioで操作したときに生成された [ClickOnceProfile.pubxml] を指定してオプションをすっきりさせることができます。しかし、公式ページにもある通りPublishDirオプションはここでも指定する必要があります。

[AzureFileCopy] というタスクでAzure Blob Storageにアップロードしています。[azureSubscription] には、自分のAzureサブスクリプションを指定してください。執筆時点では最新版であるバージョン4 [@4] では動作しませんでした。アップロードしているファイルやフォルダは基本的に前回の記事と同じです。ただしインストールするときに使えるWebページ Publish.html はMSBuildコマンドでは出力されないようで、今回は含めていません。インストール用のリンクが載ったページが必要であれば自分で作成する必要がありそうです。

6. Pipelineを実行する

画面右上の [Run] からパイプラインを実行します。うまくいけば下の図のように成功します。

7. バージョンアップしてGitリポジトリにプッシュする

最後に手順7から9で、コードを変更→mainブランチにプッシュ→パイプライン実行→アプリを更新の流れをやってみます。

ここはVisual Studioで作業します。前回の記事の「発行」機能を使えばリビジョンが自動で増加しましたが、今回はそういうわけにはいかないので ClickOnceProfile.pubxml を自分で編集する必要があります。バージョン番号はなんでもいいですが 1.0.1.0 としてみました。

<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <ApplicationRevision>0</ApplicationRevision>
    <ApplicationVersion>1.0.1.*</ApplicationVersion>
    ...
  </PropertyGroup>
</Project>

前回と同様に MainWindow.xaml を編集して画面にバージョン番号を表示するようにします。

<Window
    ...
    >
    <Grid>
        <TextBlock Text="Ver.1.0.1.0" />
    </Grid>
</Window>

これらの変更分をgit commitしてmainブランチにプッシュします。

8. Pipelineが自動実行することを確認する

Azure DevOpsのパイプラインのページを見にいくとパイプラインが実行しています。

9. アプリが自動更新するか試す

アプリを起動すると更新ダイアログが表示され、更新すると新しいバージョンになっていることが確認できます。

WPFアプリケーションをClickOnce発行してAzure Blob Storageに公開する

タイトル通りなのですが、開発したWindowsデスクトップアプリケーションをインターネットからインストールして使えるようにしたいということがあったので、その時の備忘録です。

必要なもの

この記事で紹介している方法を試すには以下のものが必要です。

記事の構成

この記事は以下の7つの手順でまとめます。

  1. Azure Blob Storageに静的Webサイト用コンテナーを作成する
  2. WPFアプリケーションを作成する
  3. WPFアプリケーションをClickOnce発行する
  4. アプリのファイル一式をAzure Blob Storageにアップロードする
  5. アプリをインストールする
  6. アプリをバージョンアップして再アップロードする
  7. アプリが自動更新するか試す

1. Azure Blob Storageに静的Webサイト用コンテナーを作成する

Azure Portalに移動して、ストレージアカウントを作成します。

作成し終わったら、左メニューから [静的なWebサイト] を選択します。

[有効] を選択して [保存] をクリックします。(裏で $web という名前のコンテナーが生成されます)

表示されるプライマリエンドポイントをコピーしておきます。(手順3で使います)

2. WPFアプリケーションを作成する

ここからはVisual Studioで作業します。新しいプロジェクトの作成で [WPFアプリケーション] を選択します。

プロジェクト名は [SimpleWPFApplication] とします。

ターゲットフレームワークは [.NET5.0] とします。

以下のようなファイルが自動生成されます。

MainWindow.xaml を少し書き換えてバージョン番号を表示するようにしておきます。

<Window
    ...
    >
    <Grid>
        <TextBlock Text="Ver.1.0.0.0" />
    </Grid>
</Window>

3. WPFアプリケーションをClickOnce発行する

Visual Studioのソリューションエクスプローラーでプロジェクトを右クリックし [発行] をクリックします。

[ClickOnce] を選択します。

発行場所はデフォルトの [bin\publish\] のままでいきます。

インストール場所は [Webサイトから] を選択し、手順1でコピーしたBlob Storageのプライマリエンドポイントを貼り付けます。

設定はデフォルトのままでいきます。ClickOnceには更新プログラムを確認して自動で更新する機能があるのでそれを使ってみます。

今回マニフェストの署名はなしでいきます。

構成はいろいろいじります。「配置モード」は [自己完結] 、「ターゲットランタイム」は [win-x64] を選択し、 [単一ファイルの作成] をチェックします。ここは全てのパターンを試してないですが、この構成でうまく動いたのでこれでいきます。

[完了] をクリックすると下の画像のような画面になるので、 [発行] をクリックします。

発行後、発行場所を見に行くといろいろなファイルが生成されています。

4. アプリのファイル一式をAzure Blob Storageにアップロードする

Azure Storage Explorerを使って作業します。以下のファイルとフォルダを$webコンテナーにアップロードします。Azure Storage Explorerを使えば簡単にドラッグアンドドロップでアップロードできます。

  • setup.exe
  • SimpleWPFApplication.application
  • Publish.html
  • Application Files フォルダ

アップロード後、Azure Portalでも確認できます。

5. アプリのインストール

ブラウザで「Publish.html」を見に行きます。(URLは https://ストレージアカウント名.z11.web.core.windows.net/Publish.html のようになります。)

[インストール] をクリックするとsetup.exeがダウンロードされます。setup.exeを起動すると確認ウィンドウが表示されるので発行元を確認して [実行] をクリックします。

[インストール] をクリックするとインストールが始まります。

インストールが完了するとアプリが起動します。

左下のスタートメニューから起動することができるようになりました。

6. アプリをバージョンアップして再アップロード

ここからは少し蛇足になりますが、新バージョン版をアップロードしてアプリが自動更新するところを手順6と7で記載します。まず、バージョンアップしたことが分かるように画面 MainWindow.xaml のバージョン番号を変えておきます。

<Window
    ...
    >
    <Grid>
        <TextBlock Text="Ver.1.0.0.1" />
    </Grid>
</Window>

手順3の再度で行った [発行] を再度クリックします。リビジョン(バージョン番号の一番右)が自動で増加するようになっているので、ここで発行したバージョンは「1.0.0.1」となります。

手順4で行ったアップロードをもう一度行います。

7. アプリの自動更新

手順5でインストールしたアプリを起動します。すると更新を促すウィンドウが出るので、そのまま [OK] をクリックすると更新されます。

更新後にアプリを開くと新しいバージョン(1.0.0.1)になっていることが確認できます。

Unityで作ったゲームをWebGLビルドしてAzure Static Web Appsで公開する

Unityで作ったゲームを気軽に遊べるようにしたいと考えていたところ、Azureに静的Webサイトを簡単に無料でデプロイできるサービス(Static Web Apps)があったので使ってみました。

ちなみにサンプルのゲームにはUnity勉強でお世話になっている本「Unity2021 3D/2Dゲーム開発実践入門」のCHAPTER-3で作成するゲーム「Illumiball」を利用しています。

必要なもの

この記事の内容を試すためには以下の準備が必要です。

  • Unity HubでWebGL Build Supportモジュールをインストールしておく
  • Azureアカウント
  • GitHubアカウント

記事の構成

本記事は以下の2部構成で書いています。

  1. UnityでWebGLビルドする
  2. Azure Static Web Appsで公開する

1. UnityでWebGLビルドする

まずはUnityでの操作です。

圧縮を無効化する

デフォルトの設定では生成されるJavaScriptファイルが圧縮されているのですが、恐らくStatic Web Appsが対応していないことによりゲームが動作しないので、圧縮を無効化します。

  • [Edit] -> [Project Settings…] とクリックし、プロジェクト設定ダイアログを開く
  • [Player]メニューを選択し、WebGLのアイコンのタブを開く
  • [Publishing Settings] の [Compression Format] を [Disabled] にする

ビルドする

ビルドしてWebコンテンツを生成します。

  • [File] -> [Build Settings…] とクリックし、ビルド設定ダイアログを開く
  • [Platform] で [WebGL] を選択し、 [Switch Platform] をクリックする
  • プラットフォームの切り替えが完了したら、 [Build] をクリックする
  • フォルダ選択のダイアログが開くので、Webコンテンツを生成してほしいフォルダを指定して [フォルダーの選択] をクリックする

2. Azure Static Web Appsで公開する

先ほど生成したWebコンテンツを公開します。

GitHubにWebコンテンツをアップロードする

Static Web Apps は基本的にGitHubとの連携を前提としているのでまずはWebコンテンツをGitHubにアップロードします。

Static Web Appsを作成する

いよいよ最後の作業です。

  • Azureポータルから [Static Web Apps] を選択し、 [Create] をクリックする
  • 以下の画像を参考に情報を入力する。特筆する点は以下の通り
    • 個人の開発なら [Plan type] はFreeを選べる
    • [Azure Functions and staging details] は地理的に近そうな [East Asia] を選ぶ
    • [Deployment details] は [GitHub] にして、 [Sign in with GitHub] からGitHubアカウントを認証する
    • その後、先ほど作成したGitHubリポジトリを指定する
    • [Build Presets] は [Custom] にする
  • [Review + create] をクリックし、リソースを作成する
  • 少し待つとGitHub Actionsのワークフローが動き出し、WebコンテンツがSite Web Appsにデプロイされる

確認

Static Web Appsのリソースページから [Browse] をクリックし、ゲーム画面が表示されれば成功です。

AzureでCentOS8の仮想マシンを構築する

AzureでCentOS8の仮想マシンを構築する方法

仮想マシンを構築

まずはAzureのポータルにアクセスする。

トップページ左上の三本線をクリック

「Virtual Machines」をクリック

「+追加」をクリック

以下の情報を入力または選択し、「確認および作成」をクリック

  • 仮想マシン名:testVM(なんでもいい)
  • 地域:(Asia Pacific) 東日本
  • イメージ:CentOS-based 8.1
  • サイズ:Standard_B1ls(なんでもいいが検証なので一番安いやつ)
  • 認証の種類:パスワード
  • パスワード、パスワードの確認:任意のパスワード

確認ページが表示されるので問題なければ「作成」をクリック

デプロイが完了するまで待つ

デプロイが完了したら「リソースに移動」をクリック

仮想マシンにSSHログイン

「パブリックIPアドレス」をコピー

何かしらのSSHクライアントでログイン(下の図はPowerShell)

ssh AzureUser@(Public IP address)

ログインできた。

SSHできるIPアドレスを制限

このままだとどこからでもSSHログインできてしまうので、IPアドレスで制限する方法を載せておく。

仮想マシンのページから「ネットワーク」をクリックし、名前が「SSH」の行をクリック

以下の情報を入力し「保存」をクリック

  • ソース:IP Addresses
  • ソースIPアドレス/CIDR範囲:自分が使っているグローバルIPアドレス

リソースを削除

検証用途の場合、リソースを削除することを忘れずに。

仮想マシンを作ると他にもリソースが勝手に作られるのでリソースグループごと消すのが安心。

トップページ左上の三本線をクリックし、「リソースグループ」をクリック

今回作成したリソースグループ(今回は「testVM_group」)をクリック

「リソースグループの削除」をクリック

削除確認があるのでテキストボックスにリソースグループ名を入力して「削除」をクリック

これで今回作成した仮想マシンに関連するリソースがすべて削除される。

Azureの音声テキスト変換サービス(Speech Services – Speech to Text)をPythonで使ってみた

Azureの文字起こしサービスを使ってみたので、Azureのポータル画面での操作とPythonコードをまとめておく。

今回使うAzureのサービスは文字起こしの中でも「バッチ文字起こし」というもので、Blob Storageにアップロードした音声ファイルを文字起こししてくれるものである。文字起こしの結果もBlob Storageに保存される。

処理の流れをまとめると以下のようになる。この記事は以下の順番で書いている。

  • 準備
  • STEP1: ローカルにある音声ファイルをBlob Storageにアップロード (Python SDK)
  • STEP2: AzureのSpeech Servicesに文字起こし処理を依頼 (REST API)
  • STEP3: 定期的に処理結果を確認 (REST API)
  • STEP4: 処理が完了したら処理結果のURLを取得 (REST API)
  • STEP5: 処理結果をBlob Storageからダウンロードし結果を表示 (Pyhon SDK)

準備

Azureポータル画面でリソースの作成と認証情報の取得を行う。

Blob Storage(コンテナー)を作成

まずはストレージアカウントというコンテナ―の上位概念を作成。作成ページへの行き方はいろいろあると思うけど、ポータルのホーム画面から左上の三本線アイコンから「ストレージアカウント」をクリックするのが簡単。

ストレージアカウント名はAzure内でユニークでなければならない。今回は「tetsisstorageaccount」にしてみた。リソースグループは事前に作っておいた「SpeechRecognition」、場所は「東日本」にした。その他はデフォルト。

ストレージアカウントが作成できたらコンテナーを作成する。図の赤枠で囲った「コンテナー」をクリックする。

右側から「新しいコンテナー」モーダルが出てくる。今回は「speechcontainer」というコンテナー名にしてみた。

接続文字列を取得

ストレージアカウント画面の左メニューから「アクセスキー」をクリックすると下図のようなページが表示されるので、「接続文字列」をメモしておく。(このページはいつでも見にこれる。key1とkey2はどちらでもよい)

Speech Servicesを作成

音声認識サービスのリソースを作成する。作成ページに行くにはポータル画面の検索欄で「speech」と入力して表示される「Cognitive Services」をクリックするのが簡単。Cognitive ServicesはSpeech Servicesの上位概念。

下図の「追加」をクリックする。

Marketplaceの検索欄で「speech」と入力して表示される「音声」をクリックする。

「作成」ボタンをクリックする。

情報入力ページが表示される。名前は任意でよい。今回は「firstSpeech」にしてみた。場所は「東日本」。価格レベルは「SO」を選択する。今回利用するバッチ文字起こしはSOでないと利用できない。

サブスクリプションキーを取得

Cognitive Services画面の左メニューから「Keys and Endpoint」をクリックすると下図のようなページが表示されるので、サブスクリプションキーである「キー1」をメモしておく。 (このページはいつでも見にこれる。キー1とキー2はどちらでもよい)

Pythonコーディング

ブラウザ操作はこれで終わり。ようやくコーディングのお時間。今回は以下の環境で実行している。
– OS: Windows 10
– Python 3.8.1
– pip 20.0.2

以下のライブラリをインストールしておく。

pip install requests azure-storage-blob

今回は一つのPythonファイルでコマンドラインから実行する簡単な方法でやってみる(ファイル名はazure_speech.py)。

まずファイルの先頭に以下のimport文を記載する。

# -*- coding: utf-8 -*-
import os
import uuid
import argparse
from datetime import datetime, timedelta
import requests
import json
import time

from azure.storage.blob import BlobServiceClient
from azure.storage.blob import ResourceTypes, AccountSasPermissions, ContainerSasPermissions
from azure.storage.blob import generate_account_sas, generate_container_sas

STEP1: ローカルにある音声ファイルをBlob Storageにアップロード (Python SDK)

関数コード

def upload_blob(connect_str, container_name, local_path, local_file_name):
    root_ext_pair = os.path.splitext(local_file_name)
    blob_name = root_ext_pair[0] + str(uuid.uuid4()) + root_ext_pair[1] # Blob Storage(コンテナー)内で唯一のBlob名になるようにランダム文字列(UUID)を挿入しておく
    upload_file_path = os.path.join(local_path, local_file_name)

    blob_service_client = BlobServiceClient.from_connection_string(connect_str)
    blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name)

    print("\nUploading to Azure Storage as blob:\n\t" + blob_name)

    with open(upload_file_path, "rb") as data:
        blob_client.upload_blob(data)

    return blob_name

main関数コード

実行時に引数として①音声ファイルと②言語を指定するようにした。これでいろんな言語の音声ファイルで試しやすくなるはず。

if __name__ == "__main__":
    connect_str = os.getenv("AZURE_STORAGE_CONNECTION_STRING")
    container_name = os.getenv("AZURE_STORAGE_CONTAINER_NAME")
    subscription_key = os.getenv("AZURE_SPEECH_SERVICE_SUBSCRIPTION_KEY")

    parser = argparse.ArgumentParser()
    parser.add_argument("-f", "--file", type=argparse.FileType("r", encoding="UTF-8"), required=True)
    parser.add_argument("-l", "--locale", help="e.g. \"en-US\" or \"ja-JP\"", required=True)
    args = parser.parse_args()
    file_name = args.file.name
    locale = args.locale

    # STEP1: ローカルにある音声ファイルをBlob Storageにアップロード (Python SDK)
    base_dir_pair = os.path.split(file_name)
    local_path = base_dir_pair[0]
    local_file_name = base_dir_pair[1]
    blob_name = upload_blob(connect_str, container_name, local_path, local_file_name) # ここでアップロード

実行

最初に環境変数を設定してからPythonファイルを実行する。今回は環境変数の設定方法がWindows仕様。Mac、Linuxの場合は export コマンドを使えば同じことができる。

音声ファイルは攻殻機動隊の名言でテストしてみた。攻殻機動隊SAC5話のセリフでNetflixから視聴可能。Pythonファイル(azure_speech.py)と同じフォルダに音声ファイル(荒巻名セリフ.wav)を置いている。

> $env:AZURE_STORAGE_CONNECTION_STRING = "メモしておいた接続文字列"
> $env:AZURE_STORAGE_CONTAINER_NAME = "作成したコンテナー名"
> $env:AZURE_SPEECH_SERVICE_SUBSCRIPTION_KEY = "メモしておいたサブスクリプションキー"
> python .\azure_speech.py -f .\荒巻名セリフ.wav -l ja-JP

Uploading to Azure Storage as blob:
        荒巻名セリフ43936629-807a-4d1a-b6d5-a5d0c67c50fe.wav

Azureのポータル画面にて、元のファイル名にUUIDが追加されたファイル(上の実行結果の場合は 荒巻名セリフ43936629-807a-4d1a-b6d5-a5d0c67c50fe.wav )がコンテナーにアップロードされているのが確認できる。

STEP2: AzureのSpeech Servicesに文字起こし処理を依頼 (REST API)

API仕様はSwaggerに載っていて実装の時は結構参考になった。「Custom Speech transcriptions:」セクションが今回使うAPI。

関数コード

最初の2つの関数はアカウントSASとサービスSASという、APIを使う際の認証情報を取得する関数である。SAS自体はAzureが提供している関数を利用して生成している。公式のドキュメントページでSAS生成関数が紹介されている。

TranscriptionResultsContainerUrl には処理結果のファイルを格納してもらうコンテナ―を認証情報つきで記載する。今回は最初に作ったコンテナ―を流用した。なので、音声ファイルと同じコンテナ―に処理結果のファイルも格納される。

def get_sas_token(connect_str):
    blob_service_client = BlobServiceClient.from_connection_string(connect_str)
    sas_token = generate_account_sas(
        blob_service_client.account_name,
        account_key=blob_service_client.credential.account_key,
        resource_types=ResourceTypes(object=True),
        permission=AccountSasPermissions(read=True),
        expiry=datetime.utcnow() + timedelta(hours=1)
    )
    return sas_token

def get_service_sas_token(connect_str, container_name):
    blob_service_client = BlobServiceClient.from_connection_string(connect_str)
    service_sas_token = generate_container_sas(
        blob_service_client.account_name,
        container_name,
        account_key=blob_service_client.credential.account_key,
        permission=ContainerSasPermissions(read=True, write=True),
        expiry=datetime.utcnow() + timedelta(hours=1),
    )
    return service_sas_token

def start_transcription(connect_str, container_name, blob_name, subscription_key, locale):
    blob_service_client = BlobServiceClient.from_connection_string(connect_str)

    account_name = blob_service_client.account_name
    container_url = "https://" + account_name + ".blob.core.windows.net/" + container_name

    sas_token = get_sas_token(connect_str)
    service_sas_token = get_service_sas_token(connect_str, container_name)

    url = "https://japaneast.cris.ai/api/speechtotext/v2.0/transcriptions"
    headers = {
        "content-type": "application/json",
        "accept": "application/json",
        "Ocp-Apim-Subscription-Key": subscription_key,
    }
    payload = {
        "recordingsUrl": container_url + "/" + blob_name + "?" + sas_token,
        "locale": locale,
        "name": blob_name,
        "properties": {
            "TranscriptionResultsContainerUrl" : container_url + "?" + service_sas_token
        }
    }
    r = requests.post(url, data=json.dumps(payload), headers=headers)

    print("\nTranscription start")

main関数コード

if __name__ == "__main__":
    # STEP1のコード(ここでは省略)

    # STEP2: AzureのSpeech serviceに文字起こし処理を依頼 (REST API)
    start_transcription(connect_str, container_name, blob_name, subscription_key, locale)

実行

> python .\azure_speech.py -f .\荒巻名セリフ.wav -l ja-JP

Uploading to Azure Storage as blob:
        荒巻名セリフcbbbe4e2-5507-4cae-8b54-a13003104be8.wav

Transcription start

最後に「Transcription start」と表示される。

STEP3: 定期的に処理結果を確認 (REST API)

STEP2で開始した処理を特定できる情報は名前 name なんだけど、実行状態を取得するためのGETのAPIはIDで指定する仕様になっている。そのため最初にget_transcription_id関数で名前からIDを取得してから、IDを使って定期的に状態を取得している。これ、STEP2でPOSTした時のレスポンスにIDを含めてくれたらいんだけどなぁ、、と思う。名前だけだと毎回リスト取得するしかなくて無駄に情報取ってくることになって効率悪いんだよなぁ。

関数コード

def get_transcription_id(subscription_key, transcription_name):
    url = "https://japaneast.cris.ai/api/speechtotext/v2.0/transcriptions"
    headers = {
        "content-type": "application/json",
        "accept": "application/json",
        "Ocp-Apim-Subscription-Key": subscription_key,
    }
    r = requests.get(url, headers=headers)
    response = json.loads(r.text)

    for res in response:
        if res["name"] == transcription_name:
            id = res["id"]
            print("\nTranscription ID:\n\t" + id)
            return id
    return None

def get_transcription_info_from_id(subscription_key, transcription_id):
    url = "https://japaneast.cris.ai/api/speechtotext/v2.0/transcriptions/" + transcription_id
    headers = {
        "content-type": "application/json",
        "accept": "application/json",
        "Ocp-Apim-Subscription-Key": subscription_key,
    }
    r = requests.get(url, headers=headers)
    response = json.loads(r.text)
    return response

def get_transcription_status(subscription_key, transcription_id):
    response = get_transcription_info_from_id(subscription_key, transcription_id)

    status = response["status"]
    print("Transcription status:\t" + status)
    return status

main関数コード

if __name__ == "__main__":
    # STEP1のコード(ここでは省略)
    # STEP2のコード(ここでは省略)

    # STEP3: 定期的に処理結果を確認 (REST API)
    transcription_id = get_transcription_id(subscription_key, blob_name) # Transcription nameからIDを取得
    status = ""
    while status != "Succeeded": # 処理が完了したら状態が"Succeeded"になるからそれまでループ
        status = get_transcription_status(subscription_key, transcription_id) # 処理状態を取得
        time.sleep(3)

実行

> python .\azure_speech.py -f .\荒巻名セリフ.wav -l ja-JP

Uploading to Azure Storage as blob:
        荒巻名セリフb822c506-843c-4f1d-af81-35799e9209dc.wav

Transcription start

Transcription ID:
        e5512338-1e66-44c0-bd5f-68b680d99ba6
Transcription status:   NotStarted
Transcription status:   NotStarted
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Succeeded

Transcription statusNotStartedからRunningになり、最終的にSucceededになっている。

STEP4: 処理が完了したら処理結果のBlob URLを取得 (REST API)

関数コード

def get_transcription_result_url(subscription_key, transcription_id):
    response = get_transcription_info_from_id(subscription_key, transcription_id)

    if "resultsUrls" in response:
        result_url = response["resultsUrls"]["channel_0"]
        print("\nResult URL:\n\t" + result_url)
        return result_url
    return None

main関数コード

if __name__ == "__main__":
    # STEP1のコード(ここでは省略)
    # STEP2のコード(ここでは省略)
    # STEP3のコード(ここでは省略)

    # STEP4: 処理が完了したら処理結果のBlob URLを取得 (REST API)
    result_url = get_transcription_result_url(subscription_key, transcription_id)

実行

> python .\azure_speech.py -f .\荒巻名セリフ.wav -l ja-JP

Uploading to Azure Storage as blob:
        荒巻名セリフ9b3f55c6-4c3f-4656-a48e-a4546907a31e.wav

Transcription start

Transcription ID:
        e83a0767-d4ab-421b-8476-7cba3f5dde30
Transcription status:   NotStarted
Transcription status:   NotStarted
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Succeeded

Result URL:
        https://tetsisstorageaccount.blob.core.windows.net/speechcontainer/e83a0767-d4ab-421b-8476-7cba3f5dde30_transcription_channel_0.json

処理結果のURLが表示される。

STEP5: 処理結果をBlob Storageからダウンロードし結果を表示 (Pyhon SDK)

関数コード

ダウンロードのコードもアップロードと同様に公式ページに載っている方法を参考にした。

結果のjsonファイルのデータフォーマットについては公式ページSwaggerページを参考にした。テキスト化した結果は Lexical, ITN, MaskedITN, Display の4種類があるみたいだけど、今回は句読点とかつけてくれる Display を表示することにした。

def download_blob(connect_str, container_name, blob_name):
    blob_service_client = BlobServiceClient.from_connection_string(connect_str)
    blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name)

    print("\nUploading to Azure Storage as blob:\n\t" + blob_name)
    with open(blob_name, "wb") as download_file:
        download_file.write(blob_client.download_blob().readall())

def get_transcript_from_file(file_name):
    with open(file_name, "r", encoding="utf-8") as f:
        df = json.load(f)

    transcript = ""
    for audio_file_result in df["AudioFileResults"]:
        for combined_result in audio_file_result["CombinedResults"]:
            transcript += combined_result["Display"] + "\n"
    
    print("\nTranscription result:\n" + transcript)
    return transcript

main関数コード

if __name__ == "__main__":
    # STEP1のコード(ここでは省略)
    # STEP2のコード(ここでは省略)
    # STEP3のコード(ここでは省略)
    # STEP4のコード(ここでは省略)

    # STEP5: 処理結果をBlob Storageからダウンロードし結果を表示 (Pyhon SDK)
    blob_name = result_url[result_url.rfind("/") + 1:]
    download_blob(connect_str, container_name, blob_name) # ここでダウンロード
    get_transcript_from_file(blob_name) # ダウンロードしたファイルから音声をテキスト化した文字列を取得

実行

> python .\azure_speech.py -f .\荒巻名セリフ.wav -l ja-JP

Uploading to Azure Storage as blob:
        荒巻名セリフc2d034b7-3803-431d-bcf0-368310cd678b.wav

Transcription start

Transcription ID:
        9388030b-cc98-428d-ae0c-3c5cff02ed3c
Transcription status:   NotStarted
Transcription status:   NotStarted
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Running
Transcription status:   Succeeded

Result URL:
        https://tetsisstorageaccount.blob.core.windows.net/speechcontainer/9388030b-cc98-428d-ae0c-3c5cff02ed3c_transcription_channel_0.json

Uploading to Azure Storage as blob:
        9388030b-cc98-428d-ae0c-3c5cff02ed3c_transcription_channel_0.json

Transcription result:
よかろう我々の間にはチームプレーなどという都合の良い言い訳は存在せん。あるとすれば、、スタンドプレーから生じるチームワークだけだ。

どのセリフをテキスト変換させてたのか完璧に分かる。

全コード

最後に今回のコードの全体を載せておく。

# -*- coding: utf-8 -*-
import os
import uuid
import argparse
from datetime import datetime, timedelta
import requests
import json
import time

from azure.storage.blob import BlobServiceClient
from azure.storage.blob import generate_account_sas, generate_container_sas, ResourceTypes, AccountSasPermissions, ContainerSasPermissions

def upload_blob(connect_str, container_name, local_path, local_file_name):
    root_ext_pair = os.path.splitext(local_file_name)
    blob_name = root_ext_pair[0] + str(uuid.uuid4()) + root_ext_pair[1] # Blob Storage(コンテナー)内で唯一のBlob名になるようにランダム文字列(UUID)を挿入しておく
    upload_file_path = os.path.join(local_path, local_file_name)

    blob_service_client = BlobServiceClient.from_connection_string(connect_str)
    blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name)

    print("\nUploading to Azure Storage as blob:\n\t" + blob_name)

    with open(upload_file_path, "rb") as data:
        blob_client.upload_blob(data)

    return blob_name

def download_blob(connect_str, container_name, blob_name):
    blob_service_client = BlobServiceClient.from_connection_string(connect_str)
    blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name)

    print("\nUploading to Azure Storage as blob:\n\t" + blob_name)
    with open(blob_name, "wb") as download_file:
        download_file.write(blob_client.download_blob().readall())

def get_sas_token(connect_str):
    blob_service_client = BlobServiceClient.from_connection_string(connect_str)
    sas_token = generate_account_sas(
        blob_service_client.account_name,
        account_key=blob_service_client.credential.account_key,
        resource_types=ResourceTypes(object=True),
        permission=AccountSasPermissions(read=True),
        expiry=datetime.utcnow() + timedelta(hours=1)
    )
    return sas_token

def get_service_sas_token(connect_str, container_name):
    blob_service_client = BlobServiceClient.from_connection_string(connect_str)
    service_sas_token = generate_container_sas(
        blob_service_client.account_name,
        container_name,
        account_key=blob_service_client.credential.account_key,
        permission=ContainerSasPermissions(read=True, write=True),
        expiry=datetime.utcnow() + timedelta(hours=1),
    )
    return service_sas_token

def start_transcription(connect_str, container_name, blob_name, subscription_key, locale):
    blob_service_client = BlobServiceClient.from_connection_string(connect_str)

    account_name = blob_service_client.account_name
    container_url = "https://" + account_name + ".blob.core.windows.net/" + container_name

    sas_token = get_sas_token(connect_str)
    service_sas_token = get_service_sas_token(connect_str, container_name)

    url = "https://japaneast.cris.ai/api/speechtotext/v2.0/transcriptions"
    headers = {
        "content-type": "application/json",
        "accept": "application/json",
        "Ocp-Apim-Subscription-Key": subscription_key,
    }
    payload = {
        "recordingsUrl": container_url + "/" + blob_name + "?" + sas_token,
        "locale": locale,
        "name": blob_name,
        "properties": {
            "TranscriptionResultsContainerUrl" : container_url + "?" + service_sas_token
        }
    }
    r = requests.post(url, data=json.dumps(payload), headers=headers)

    print("\nTranscription start")

def get_transcription_id(subscription_key, transcription_name):
    url = "https://japaneast.cris.ai/api/speechtotext/v2.0/transcriptions"
    headers = {
        "content-type": "application/json",
        "accept": "application/json",
        "Ocp-Apim-Subscription-Key": subscription_key,
    }
    r = requests.get(url, headers=headers)
    response = json.loads(r.text)

    for res in response:
        if res["name"] == transcription_name:
            id = res["id"]
            print("\nTranscription ID:\n\t" + id)
            return id
    return None

def get_transcription_info_from_id(subscription_key, transcription_id):
    url = "https://japaneast.cris.ai/api/speechtotext/v2.0/transcriptions/" + transcription_id
    headers = {
        "content-type": "application/json",
        "accept": "application/json",
        "Ocp-Apim-Subscription-Key": subscription_key,
    }
    r = requests.get(url, headers=headers)
    response = json.loads(r.text)
    return response

def get_transcription_status(subscription_key, transcription_id):
    response = get_transcription_info_from_id(subscription_key, transcription_id)

    status = response["status"]
    print("Transcription status:\t" + status)
    return status

def get_transcription_result_url(subscription_key, transcription_id):
    response = get_transcription_info_from_id(subscription_key, transcription_id)

    if "resultsUrls" in response:
        result_url = response["resultsUrls"]["channel_0"]
        print("\nResult URL:\n\t" + result_url)
        return result_url
    return None

def get_transcript_from_file(file_name):
    with open(file_name, "r", encoding="utf-8") as f:
        df = json.load(f)

    transcript = ""
    for audio_file_result in df["AudioFileResults"]:
        for combined_result in audio_file_result["CombinedResults"]:
            transcript += combined_result["Display"] + "\n"
    
    print("\nTranscription result:\n" + transcript)
    return transcript

if __name__ == "__main__":
    connect_str = os.getenv("AZURE_STORAGE_CONNECTION_STRING")
    container_name = os.getenv("AZURE_STORAGE_CONTAINER_NAME")
    subscription_key = os.getenv("AZURE_SPEECH_SERVICE_SUBSCRIPTION_KEY")

    parser = argparse.ArgumentParser()
    parser.add_argument("-f", "--file", type=argparse.FileType("r", encoding="UTF-8"), required=True)
    parser.add_argument("-l", "--locale", help="e.g. \"en-US\" or \"ja-JP\"", required=True)
    args = parser.parse_args()
    file_name = args.file.name
    locale = args.locale

    # STEP1: ローカルにある音声ファイルをBlob Storageにアップロード (Python SDK)
    base_dir_pair = os.path.split(file_name)
    local_path = base_dir_pair[0]
    local_file_name = base_dir_pair[1]
    blob_name = upload_blob(connect_str, container_name, local_path, local_file_name) # ここでアップロード

    # STEP2: AzureのSpeech serviceに文字起こし処理を依頼 (REST API)
    start_transcription(connect_str, container_name, blob_name, subscription_key, locale)

    # STEP3: 定期的に処理結果を確認 (REST API)
    transcription_id = get_transcription_id(subscription_key, blob_name) # Transcription nameからIDを取得
    status = ""
    while status != "Succeeded": # 処理が完了したら状態が"Succeeded"になるからそれまでループ
        status = get_transcription_status(subscription_key, transcription_id) # 処理状態を取得
        time.sleep(3)

    # STEP4: 処理が完了したら処理結果のBlob URLを取得 (REST API)
    result_url = get_transcription_result_url(subscription_key, transcription_id)

    # STEP5: 処理結果をBlob Storageからダウンロードし結果を表示 (Pyhon SDK)
    blob_name = result_url[result_url.rfind("/") + 1:]
    download_blob(connect_str, container_name, blob_name) # ここでダウンロード
    get_transcript_from_file(blob_name) # ダウンロードしたファイルから音声をテキスト化した文字列を取得