Azure OpenAI Serviceの利用申請

Microsoft AzureのAzure OpenAI On Your Dataを検証することとなった。

Azure OpenAI Serviceの利用には、事前申請が必要ということで、2024/2/6時点での申請方法をメモ。

事前準備

2024/2/6時点の申請フォームはこちら

利用申請はサブスクリプション単位。申請フォームに有効化するサブスクリプションのSubscriptions IDを入力する必要があるため、すべてのサービス > サブスクリプションから、IDを確認しておく。

また、メールアドレスの入力が必要だが、 gmail.com, hotmail.com などは使用できず、会社のものを使用する必要がある模様。

Your Company Email Address - Applications submitted with a personal email address (e.g. gmail.com, hotmail.com, outlook.com, etc.) will be DENIED.

申請内容

引っ掛かった項目をメモしていく。なお、名前や会社名、会社の住所などは、日本語で入力して申請が通った。

14, 15: If you have a contact at Microsoft, ...

14「If you have a contact at Microsoft, please provide their full name.」および15「If you have a contact at Microsoft, please provide their email address.」。よくわからないため、空白にしておく。

17: Which Azure OpenAI service feature(s) are you requesting access for

17「Which Azure OpenAI service feature(s) are you requesting access for?」に対して以下の選択肢、複数選択可能。検証に使用するのはGPT-3.5系だけでいいため、一番上のみチェック。

  • GPT-3.5, GPT-3.5 Turbo, GPT-4, GPT-4 Turbo, and/or Embeddings Models (Conversational AI, Search, Summarization, Writing Assistance or content generation, Code-based scenarios, Reason over Structured and Unstructured data)
  • DALL-E 2 and/or DALL-E 3 models (text to image)
  • OpenAI Whisper model (Speech-to-Text)
  • GPT-4 Turbo with Vision

18: ユースケース

17でWhisper以外を選択すると、18としてユースケースを聞かれる。

GPT-3.5...を選択した場合以下。1, 2, 6, 8を選択。

  1. Chat and conversation interaction: Users can interact with a conversational agent that responds with responses drawn from trusted documents such as internal company documentation or tech support documentation; conversations must be limited to answering scoped questions. Available to internal, authenticated external users, and unauthenticated external users.
  2. Chat and conversation creation: Users can create a conversational agent that responds with responses drawn from trusted documents such as internal company documentation or tech support documentation; conversations must be limited to answering scoped questions. Limited to internal users only.
  3. Code generation or transformation scenarios: For example, converting one programming language to another, generating docstrings for functions, converting natural language to SQL. Limited to internal and authenticated external users.
  4. Journalistic content: For use to create new journalistic content or to rewrite journalistic content submitted by the user as a writing aid for pre-defined topics. Users cannot use the application as a general content creation tool for all topics. May not be used to generate content for political campaigns. Limited to internal users.
  5. Most Valuable Professional (MVP) or Regional Director (RD) Demo Use: Any applicant who is not in the Microsoft Most Valuable Professional (MVP) Award Program and in the MVP database, or in theRegional Director (RD) Program, will be denied if this use case is selected. For use by a current participant in the MVP or RD Program (the name entered in Questions 1-2 must be the name of the MVP or RD participant) solely to develop, test, and demonstrate one or more sample applications showcasing the Azure OpenAI Service GPT-3.5, GPT-3.5 Turbo, GPT-4, GPT-4 Turbo, and/or Embeddings Models capability (in accordance with a use case listed in this Question [X]). No production use, sale, or other disposition of an application is permitted under this use case; if an MVP, RD, or their employer wants to use an Azure OpenAI Service application in production, a separate form must be submitted, the appropriate use case must be selected, and a separate eligibility determination will be made.
  6. Question-answering: Users can ask questions and receive answers from trusted source documents such as internal company documentation. The application does not generate answers ungrounded in trusted source documentation. Available to internal, authenticated external users, and unauthenticated external users.
  7. Reason over structured and unstructured data: Users can analyze inputs using classification, sentiment analysis of text, or entity extraction. Examples include analyzing product feedback sentiment, analyzing support calls and transcripts, and refining text-based search with embeddings. Limited to internal and authenticated external users.
  8. Search: Users can search trusted source documents such as internal company documentation. The application does not generate results ungrounded in trusted source documentation. Available to internal, authenticated external users, and unauthenticated external users.
  9. Summarization: Users can submit content to be summarized for pre-defined topics built into the application and cannot use the application as an open-ended summarizer. Examples include summarization of internal company documentation, call center transcripts, technical reports, and product reviews. Limited to internal, authenticated external users, and unauthenticated external users.
  10. Writing assistance on specific topics: Users can create new content or rewrite content submitted by the user as a writing aid for business content or pre-defined topics. Users can only rewrite or create content for specific business purposes or pre-defined topics and cannot use the application as a general content creation tool for all topics. Examples of business content include proposals and reports. May not be selected to generate journalistic content (for journalistic use, select the above Journalistic content use case). Limited to internal users and authenticated external users.
  11. Data generation for fine-tuning: Users can use a model in Azure OpenAI to generate data which is used solely to fine-tune (i) another Azure OpenAI model, using the fine-tuning capabilities of Azure OpenAI, and/or (ii) another Azure AI custom model, using the fine-tuning capabilities of the Azure AI service. Generating data and fine-tuning models is limited to internal users only; the fine-tuned model may only be used for inferencing in the applicable Azure AI service and, for Azure OpenAI service, only for customer’s permitted use case(s) under this form.

ユースケース以降の項目

ユースケースの次(今回の場合は19)は利用規約の確認チェック。

20はコンテンツフィルターによってフラグが立てられた場合、デバッグや不正使用の調査のため、Microsoftの従業員がコンテンツを確認する場合があることへの同意チェック。

21はアンケートなので記入不要。

申請結果

左下の「送信」ボタンをクリックすると以下のメッセージ。だいたい1日で終わるとのこと。

Most applications are processed within 24 hours. Some applications may require additional processing time and take up to 10 business days

メッセージ通り、翌日には「Welcome to the Azure OpenAI Service, <名前>! [ApplicationID <7桁の数字>]」という件名で、申請が通ったとのメールが来た。

振り返り

設問がすべて英語なので、ユースケースの内容を理解するのが大変だった。

他は一般的な利用申請という感じだが、会社メールアドレスでないと2024/2時点では申請できそうにないので、個人で試すのは難しそう。

Yarn v1を使用しているとStorybook v7の実行がERR_REQUIRE_ESMで失敗する

昨日まで動いていたStorybookが、 yarn install したら突然動かなくなったのでメモ。

環境

Node.js v20.10.0, Yarn 1.22.19, Storybook v7.5.3。

問題

Next.js v13プロジェクトで、Storybookを使ってコンポーネント等の確認をしている。

今回、新しいパッケージの検証のため、 yarn addyarn removeyarn install を繰り返した。

その後、 yarn storybook でStorybookを起動しようとすると、以下のエラーが発生し、起動できなくなった。

:red_circle: Error: It looks like you are having a known issue with package hoisting.
Please check the following issue for details and solutions: https://github.com/storybookjs/storybook/issues/22431#issuecomment-1630086092


<workspace>/node_modules/cli-table3/src/utils.js:1
const stringWidth = require('string-width');
          ^

Error [ERR_REQUIRE_ESM]: require() of ES Module <workspace>/node_modules/string-width/index.js from <workspace>/node_modules/cli-table3/src/utils.js not supported.
Instead change the require of index.js in <workspace>/node_modules/cli-table3/src/utils.js to a dynamic import() which is available in all CommonJS modules.
  at Object.<anonymous> (<workspace>/node_modules/cli-table3/src/utils.js:one:21) {
 code: 'ERR_REQUIRE_ESM'
}

npm run storybook でも同様のエラーが発生する。

対応

エラーメッセージ内にGitHubのissueへのリンクがあるので、そちらを確認。

github.com

このメッセージに、経緯や原因、対応方法の記載がある。

以下のような経緯らしい。(軽く眺めただけなので間違っているかも)

  1. cliuiの変更に起因して、jackspeak v2.1.2以降ではYarn v1非対応のフォークである@isaacs/cliui v8.0.2を使うよう更新された
  2. Storybookが使用しているglobからjackspeakへの依存性が jackspeak@^2.0.3 となっており、jackspeak v2.1.2以降を使用するため、Yarn v1を使用しているとエラーが発生する

根本的な対応として推奨されているのは、Yarnをv3に更新することだが、Yarn v1のままで対応したい場合、 package.jsonresolutions を追加し、 jackspeak のバージョンを 2.1.1 で指定する。

{
  ...
  "resolutions": {
    "jackspeak": "2.1.1"
  }
}

上記の記述を package.json に追加し、 yarn install を再実行すると、Storybookの起動時のエラーが発生しなくなった。

振り返り

突然動かなくなったので焦ったが、エラーメッセージにURLを含めてくれていたので非常に助かった。

もう2年くらい、「Yarn v1から移行しないとな~」とか言ってきたが、こういう問題が起こってくるといいかげん対応しないとだなあ。

AWS AmplifyでNode.js v18を使うと「GLIBC not found」が発生する

最近新たに開始したプロジェクトで、開発環境をNode.js v18 + Next.js v13に更新した。検証のためにAWS Amplifyにデプロイしたがエラーが発生。

ちょっと調べるとあるあるらしいので、何番煎じかわからないが対応方法をメモ。

環境

Node.js v18.16.1, Next.js v13.4.19。

問題

AWS Amplifyに前述のバージョンのNext.jsアプリケーションをホストした。ビルドの設定は以下。

  • 構築イメージ: Amazon Linux:2 (デフォルト)
  • ライブパッケージの更新
    1. Next.js version: 13.4.19
    2. Node.js version: 18.16.1

この状態でデプロイすると、以下のエラーが「構築」>「フロントエンド」で発生し、アプリケーションのビルドに失敗する。

[WARNING]: node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by node)
           node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by node)  
[ERROR]: !!! Build failed

なお、AmplifyはNext.js v13に対応済み。

aws.amazon.com

原因

「node GLIBC not found」で検索すると、同様の事例が出てくる。OSにインストールされているGLIBC(GNU C Library)が古いのが原因らしい。

コンテナイメージとしてはデフォルトのAmazon Linux 2を指定していた。Amazon Linux 2のベースと思われるCentOS7でも同様の問題が発生する模様。

it.ama2pro.net

対応

コンテナイメージを変更してやればいい。ECR Public GalleryでNode.jsの公式イメージが使用できる。

今回はv18.16.1を使うので、構築イメージのURLとして public.ecr.aws/docker/library/node:18.16.1 を指定。

これでビルドが通るだろうと思ったが、今度は [ERROR]: !!! Node version not available: 18.16.1 というエラーが発生。

「ライブパッケージの更新」でNode.jsのバージョンを指定しているのが悪いのかと思い、 Node.js version を削除したところ、エラーが発生しなくなった。

振り返り

今回の問題が起こるまで、Amplifyのアプリケーション実行環境を気にしていなかった。適切なコンテナイメージがあれば、それを指定したほうがいいかもしれない。

Amazon Linux 2、サポート期限が2023年6月末じゃなかったっけと思ったら、2年延長してたのね。正式リリースが2018年6月、それだけ古ければ、そろそろ動かないものが出てくることもあるよなぁ。

Whisper APIで解析した結果から、Google Colabで話者ダイアリゼーション(話者の識別)を行う

Whisperで文字起こしした電話内容を、OpenAI APIgpt-3.5-turbo モデルで要約させている。

会議議事録の要約などは、プロンプトの指定次第で高精度で行えるが、電話の場合、文章の体裁を取らず、会話内容もまとまりがなかったりで、精度が低い。

特に、話者がどちらかの情報が、Whisperでの文字起こし結果には含まれないため、双方の話している内容がまとめられたりと、改善要望が多い。

ちょうど gpt-4 がOpenAI APIで一般解禁されたので試してみたところ、入力テキストやプロンプトをいじらなくても精度向上したが、コストが gpt-3.5-turbo の20倍ということで、稟議が通らなかった。

精度向上のため、Speaker Diarisation(話者ダイアリゼーション、話者識別、話者分離、話者分割)できないか、場合によってはWhisperからの乗り換えも検討しつつ調べたところ、Whisperの解析結果を元に、Google Colab上でそれっぽいことができたのでメモ。

方法の調査

調べると、Pythonを使った例が出てきた。 spkrec-ecapa-voxcelebPyannoteWhisperX あたりが名前として出てくる。

spkrec-ecapa-voxceleb

spkrec-ecapa-voxceleb は、SpeechBrainが公開しているツール。トレーニング済みのECAPA-TDNNモデルで話者の検証を行うのに必要なツールが含まれているとのこと。

huggingface.co

VoxCelebYoutubeを元にしたデータセットらしい。ECAPA-TDNNはモデルの模様。

Pyannote

今回のユースケースのように、音声に対して使用するのは pyannote.audio 。他にも動画用の pyannote.video などあった。

github.com

PyTorchベースの、speaker diarization用ツールとのこと。

READMEに記載があるが、使用時に認証トークンが必要。その発行のためには、HuggingFaceのアカウントが必要となる。

WhisperX

Whisper + Pyannoteによる文字起こし&話者分離が広く使われているようで、それらをあらかじめ合わせてあるのがWhisperX。

github.com

Whisperの高速化モデルである、Faster-Whisperを使用しており、オリジナルのWhisperを使っているのではない模様。

Whisper APIを使用できたらよかったのだが、現時点では未対応の模様。また、Pyannoteの認証トークンはやはり必要となる。

話者ダイアリゼーション可能なサービス

Whisperから、話者ダイアリゼーションをしてくれるサービスへの乗り換えも検討。

GCPSpeech-to-Textや、LINEのCLOVA NoteNootaというサービスもあった。

方法の選定

PyannoteのためのHuggingFaceアカウント作成が、「(決済者が)聞いたことのないWebサイトだから」ということで稟議が通らず、外部サービスについてもそれぞれの理由でお見送りとなったため、消去法でspkrec-ecapa-voxcelebに決定。

サービスの中でも、特にNootaは要約やSalesforce連携(要約結果をカスタムオブジェクトに保存している)など、自前で実装したことに対応しており、かつ話者識別もできるということで乗り換えたかったが、解析対象となる電話の総時間がビジネスプランの最大月12,000分でも足りず、月額30,000円以上のエンタープライズプランになるため見送りとなった。

CLOVA Noteはオープンベータらしく、利用料は無料でスマートフォンアプリ経由であれば無制限に使用可能だが、ファイルアップロード形式だと月300分まで、データ利用を許可しても+300分のトータル月600分までなので見送り。

Speech-To-Textは軽く試してみたが、60秒以上の音声ファイルはGoogle Cloud Storageに保存する必要があるのと、単純にエラーが多かった(時折変換失敗するファイルが存在し、リトライしても失敗し続けてしまう)。また、精度もWhisperより低かったように感じる。

spkrec-ecapa-voxcelebによる話者ダイアリゼーションのGoogle Colaboratoryでの実装

以下で、話者のサンプル音声をもとにした話者ダイアリゼーションを行っている。

qiita.com

ざっくりでいいので、サンプル音声なしの例はないかと調べたところ、以下に例があった。

huggingface.co

これらを参考に実装してみる。

音声ファイルとWhisper APIによる解析結果の用意

事前準備として、音声ファイルとそれをWhisper APIのtranscribeにかけた結果のJSONを保存しておく。

注意点として、transcribeする際のオプションとして response_format="verbose_json" を指定しておく。発話ごとに音声ファイルを分割する必要があるが、 verbose_json でないと、発話の開始・終了時間が結果に含まれない。

今回は、それぞれ sample.mp3sample.json という名前でColab ノートブックにアップロード。音声ファイルは約25分、128Kbpsで容量が25MB弱のものを使用。

Pythonパッケージのインストール

speechbrain , pydub , scikit-learn をインストールする。

!pip install speechbrain pydub scikit-learn

Embeddingの計算

speechbrainのサンプルコードだと以下のようにしている。

import torchaudio
from speechbrain.pretrained import EncoderClassifier

classifier = EncoderClassifier.from_hparams(
    source="speechbrain/spkrec-ecapa-voxceleb",
    # GPUを使用するよう設定、デフォルトではCPUが使われる
    run_opts={"device": "cuda"}
)

signal, fs = torchaudio.load("sample.mp3")

ただ、今回使用している音声ファイルを読み込ませると、 OutOfMemoryError: CUDA out of memory. が発生。

以下のように、Whisperの解析結果をもとに、ファイルを分割して読み込ませる必要があった。

!mkdir audio-segments

import json
import torchaudio

from pydub import AudioSegment
from speechbrain.pretrained import EncoderClassifier

def audio_segmentation(audio, segments, format):
    """
    音声ファイルをセグメントに分割して保存し、保存先のファイルパスを配列で返します。
    """
    segment_file_names = []

    for segment in segments:
        # ミリ秒で指定する必要があるため1000倍
        segment_start = segment["start"] * 1000
        segment_end = segment["end"] * 1000

        audio_segment = audio[segment_start:segment_end]
        segment_file_name = f"audio-segments/sample_{segment['id']:03}.mp3"
        audio_segment.export(segment_file_name, format=format)
        segment_file_names.append(segment_file_name)

    return segment_file_names


def segments_to_embeddings(classifier, segment_file_names, format):
    """
    音声ファイルをEmbeddingに変換します。
    """
    embeddings = []

    for segment_file_name in segment_file_names:
        signal, _ = torchaudio.load(segment_file_name, format=format)
        embeddings.append(classifier.encode_batch(signal))

    return embeddings


def main():
    audio = AudioSegment.from_mp3("sample.mp3")
    format = "mp3"
    classifier = EncoderClassifier.from_hparams(
        source="speechbrain/spkrec-ecapa-voxceleb",
        run_opts={"device": "cuda"},
    )

    # Whisper解析結果の読み込み
    with open("sample.json") as f:
        whisper_result = json.load(f)
        segments = whisper_result["segments"]

        segment_file_names = audio_segmentation(audio, segments, format)
        embeddings = segments_to_embeddings(classifier, segment_file_names, format)

main()

初回実行時は、モデルのダウンロードが実行される。

2回目以降では、25分程度のファイルに対して、実行時間は30秒程度。

ただ、処理時間のほとんどはファイル分割および保存によるもので、Embeddingでは3.5秒程度しかかからなかった。ファイルに保存せず、オンメモリで処理できないか調べたが、 torchaudio.load はファイルパスしか受け付けない模様。

クラスタリングの実施

続いて、クラスタリングを行う。

階層的クラスタリングによる教師なし次元削減として、 AgglomerativeClusteringFeatureAgglomeration が使える。今回は AgglomerativeClustering を用いて実装。

import json
import torchaudio

from sklearn.cluster import AgglomerativeClustering
from speechbrain.pretrained import EncoderClassifier


def get_segment_file_names(segments):
    segment_file_names = []

    for segment in segments:
        segment_file_name = f"audio-segments/sample_{segment['id']:03}.mp3"
        segment_file_names.append(segment_file_name)

    return segment_file_names


def segments_to_embeddings(classifier, segment_file_names, format):
    """
    音声ファイルをEmbeddingに変換します。
    """
    embeddings = []

    for segment_file_name in segment_file_names:
        signal, _ = torchaudio.load(segment_file_name, format=format)
        embedding = classifier.encode_batch(signal).detach().cpu().numpy()
        # embedding は(1, 1, 192) の3次元配列
        embeddings.append(embedding.reshape(192,))

    return embeddings


def main():
    format = "mp3"
    classifier = EncoderClassifier.from_hparams(
        source="speechbrain/spkrec-ecapa-voxceleb",
        run_opts={"device": "cuda"},
    )

    # Whisper解析結果の読み込み
    with open("sample.json") as f:
        whisper_result = json.load(f)
        segments = whisper_result["segments"]

        segment_file_names = get_segment_file_names(segments)
        embeddings = segments_to_embeddings(classifier, segment_file_names, format)

        # コンストラクタにクラスタ数を渡すこともできる。デフォルトは2
        clustering = AgglomerativeClustering().fit(embeddings)

        with open("sample-speaker.txt", mode="w", encoding="utf-8") as f:
            current_speaker = clustering.labels_[0]
            spoken_words = []

            for label, segment in zip(clustering.labels_, segments):
                if label != current_speaker:
                    f.write(f"[話者{current_speaker + 1}] {''.join(spoken_words)}\n")
                    spoken_words = []
                    current_speaker = label

                spoken_words.append(segment["text"])

main()

クラスタリングは一瞬で終わる。

コードをまとめる

後ほど使うことを想定し、作業ディレクトリ名の変数化、作業ディレクトリの作成/削除を追加などを行い、まとめてみる。

import json
import logging
import math
import os
import re
import shutil
import torchaudio

from pydub import AudioSegment
from sklearn.cluster import AgglomerativeClustering
from speechbrain.pretrained import EncoderClassifier
from time import perf_counter


work_dir = "audio-segments"

classifier = EncoderClassifier.from_hparams(
    source="speechbrain/spkrec-ecapa-voxceleb",
    run_opts={"device": "cuda"},
)


def audio_segmentation(audio, segments, format):
    """
    音声ファイルをセグメントに分割して保存し、保存先のファイルパスを配列で返します。
    """
    start_time = perf_counter()

    duration = audio.duration_seconds * 1000
    print(f"duration: {duration}")
    segment_file_names = []

    for segment in segments:
        segment_start = segment["start"] * 1000
        segment_end = segment["end"] * 1000

        audio_segment = audio[segment_start:segment_end]
        segment_file_name = f"{work_dir}/segment-{segment['id']:03}.{format}"
        audio_segment.export(segment_file_name, format=format)
        segment_file_names.append(segment_file_name)

    print(segment_file_names[-1])
    return segment_file_names


def segments_to_embeddings(segment_file_names, format):
    """
    音声ファイルをEmbeddingに変換します。
    """
    start_time = perf_counter()

    embeddings = []

    for segment_file_name in segment_file_names:
        signal, _ = torchaudio.load(segment_file_name, format=format)
        embedding = classifier.encode_batch(signal).detach().cpu().numpy()
        embeddings.append(embedding.reshape(192,))

    return embeddings


def speaker_diarization(embeddings, segments):
    start_time = perf_counter()

    clustering = AgglomerativeClustering().fit(embeddings)

    speakers_file_path = "speakers.txt"

    with open("debug.txt", mode="w", encoding="utf-8") as f:
        current_speaker = clustering.labels_[0]
        statements_by_speaker = [] # 話者が連続して発言した内容
        speaker_dialyzed_conversations = [] # 話者ダイアリゼーションされた会話

        def join_statements():
            # 「。」で結合。ファイルによっては「。、?」で text が終わるので、それぞれ調整
            joined_statement = '。'.join(
                statements_by_speaker
            ).replace('。。', '。').replace('、。', '、').replace('?。', '? ')
            return f"話者{current_speaker + 1}:{joined_statement}"

        for label, segment in zip(clustering.labels_, segments):
            f.write(f"{label}: {segment['text']}\n")

            # 話者が交代した場合
            if label != current_speaker:
                statement = "。".join(statements_by_speaker).replace("。。", "。")
                speaker_dialyzed_conversations.append(join_statements())
                statements_by_speaker = []
                current_speaker = label

            statements_by_speaker.append(segment["text"])

        if statements_by_speaker:
            speaker_dialyzed_conversations.append(join_statements())

    with open(speakers_file_path, mode="w", encoding="utf-8") as f:
        f.write("\n".join(speaker_dialyzed_conversations) + "\n")

    return speakers_file_path


def speaker_diarization_by_whisper_json(whisper_json_file_path, audio_file_path):
    with open(whisper_json_file_path) as f:
        whisper_json = json.load(f)
        segments = whisper_json["segments"]

        audio_format = audio_file_path.split(".")[-1]
        audio = AudioSegment.from_file(audio_file_path, audio_format)

        segment_file_names = audio_segmentation(audio, segments, audio_format)
        embeddings = segments_to_embeddings(segment_file_names, audio_format)
        speakers_file_path = speaker_diarization(embeddings, segments)
        print(f"speakers_file_path: {speakers_file_path}")


def main(whisper_json_file_path, audio_file_path):
    speaker_diarization_by_whisper_json(whisper_json_file_path, audio_file_path)


if os.path.isdir(work_dir):
    shutil.rmtree(work_dir)

os.mkdir(work_dir)
main("sample.json", "sample.mp3")

振り返り

わからないなりに何とかなったが、さて実際に運用に乗せるとなると、どんな構成がいいか。

AWS上で動かす前提だが、EC2のGPUインスタンスを動かすと結構高いのと、CUDAなどの環境構築をしたことがない。

SageMakerが使えれば安くなりそうだが、あれは自分で学習させたモデルを使うものだしなぁ。

JavaScriptで配列の末尾の値を取得したいときはat(-1)が使える

Node.jsで、ファイル名から拡張子を取るときに、 fileName.split('.').slice(-1)[0] という書き方を同僚がしていた。

Node.jsのバージョンを18にしていたので、コードレビューで、「配列の末尾を取りたいなら .at(-1) でいいよ」と指摘したら、 at を知らなかったのでメモ。

Array.at について

Node.jsの場合、v16.6.0以降であれば使用可能。比較的新しいメソッドだが、v16.6.0のリリースが2021/7/29なので、使えるようになってから2年近く経っている。主要なWebブラウザでも、同じ時期に利用可能となっているため、最新のブラウザであれば問題なく使用可能。

developer.mozilla.org

引数が 0 ~ length の間であれば、ブラケット演算子によるアクセスと同じ値を返す。また、 length よりも大きい引数であれば、ブラケット演算子と同様、 undefined を返す。

結果が異なるのは引数が負数の場合。ブラケット演算子であれば常に undefined を返すが、 at の場合は 引数 + length の値を返す。

配列の末尾の値の取得

at の引数が負数の場合の性質により、空配列でなければ、配列長にかかわらず at(-1) は配列の末尾の値を返す。

console.log([1, 2, 3].at(-1)) // 3
console.log([1, 2].at(-1)) // 2
console.log([1].at(-1)) // 1
console.log([].at(-1)) // undefined

MDNには、わざわざ「配列の末尾の値を返す」例が載っている。「メソッドの比較」として、ブラケット演算子length を使ったパターンや、 slice を使ったパターンとの比較があるが、 at を使うのが最も分かりやすいだろう。

振り返り

at は末尾を簡単に取得するために生まれたメソッドな気がする。

JavaIndexOutOfBoundsException のように、配列の範囲外の添え字を指定すると例外が発生するならともかく、JavaScriptの場合、配列長を超える値をブラケット演算子に渡してもエラーにならず undefined が返っているので、負数を指定できること以外、ブラケット演算子と差異がないんだよなあ。

ちなみに、 slice(-1)[0] の書き方をした同僚は、「配列の末尾 javascript」で検索して出てきたQiitaの記事を読んだみたい。

qiita.com

ここも、コメントを最後まで読むと at(-1)コメントされている

Stable Diffusion web UIで「No Python at」で起動できなくなった場合の対処法

友人から、「PC買い換えてStable Diffusion web UIを移動したら起動できなくなった」という話を聞いた。

状況聞いて解決できたが、エンジニアじゃない人にはちょっと面倒かなあと思いメモ。

問題

Stable Diffusion web UIをフォルダごと、古いPC(Windows 10)から新しいPC(Windows 11)にコピー。この時、保存先は同じパスにした模様。

新しいPCでGit for WindowsおよびPython3.10をインストールし、Stable Diffusion web UIの webui-user.bat を実行すると、 No Python at 'C:\Python310\python.exe' で起動に失敗するとのこと。

Pythonは、古いPCでは C:\Python310 にインストールした模様。新しいPCではMicrosoft Storeからインストールしたため、 どこにインストールされたかはわかっていなかった。

調査

webui-user.bat の内容を教えてもらうと、 set PYTHON= があったので、そこに where python した結果( C:\Users\<ユーザー名>\AppData\Local\Microsoft\WindowsApps\python.exe )を設定してもらったが変わらず。

上記のパスの python.exe は0KBのため、シンボリックリンクっぽい。実ファイルパスを入れてみるかとも思ったが、それよりも set VENV_DIR=webui-user.bat にあるのが気になる。

エラーメッセージと venvGitHubを検索すると、以下のissueが見つかった。

github.com

こちらのコメントにあるように、 venv フォルダを削除すればよい。

対応

webui-user.batVENV_DIR を設定していればそのディレクトリ、未設定であれば webui-user.bat と同じフォルダ内の venv フォルダを削除(Windowsのゴミ箱に入れればいい)し、 webui-user.bat を実行することで解消した。

このフォルダには、PythonがStable Diffusion web UIを実行するために必要なモジュール等の保存をしているが、そこに webui-user.bat を最初に実行したときのPythonのパスも保存されており、それが原因で動かなくなっていた模様。

削除後の実行では、モジュールのダウンロード等が再度実行されるため多少時間はかかるが、 venv フォルダが一番手っ取り早い。

同じPC上でも、Pythonを更新してパスが変わった場合には、同様の問題が発生しそう。また、フォルダを移動したら動かなくなった、という話もちらほら見かけたが、それも同様の原因だと思う。

なお、設定ファイルなどは venv に保存されていないため、安心して削除していい。

ユーザー設定は、Settingsタブでの設定が config.json 、txt2imgやimg2imgタブの設定が ui-config.json、保存したStyleは styles.csv に保存されている模様。

git pull で更新できない問題

ついでに、「git pull で更新しようとしたらできない」ということで相談を受けた。

git status してもらうと、一部の py ファイル等に変更があるが、心当たりはないとのこと。

適当なファイルをメモ帳で開いてもらうと、改行コードがCRLFになっていた。Git Bashgit config --get core.autocrlf を実行してもらうと true となっていたため、それが原因でpullした際に改行コードが変換され、それが変更差分として扱われたものと思われる。

git config --global core.autocrlf false を実行してもらい、リポジトリの変更は git reset --hard で切り戻した。

config.json など、ユーザー設定ファイルや、 models , outputs といったフォルダ配下の変更は差し戻されないか聞かれたが、いずれも .gitignore に記載されているため、対象外となっている。

注意点として、 webui-user.bat および webui-user.sh.gitignore に記載されているが、すでにGit管理下に含まれているため、おそらくユーザーが変更する可能性のあるファイルのうち、これらのファイルに加えた変更のみ git reset で差し戻される可能性がある。

友人は COMMANDLINE_ARGS を変更していたため、そこだけバックアップしてもらい、 git reset --hard および git pull を実行してもらうと、最新の状態に更新できた。

振り返り

インストールにGitを使ってPythonで動かすとか、なかなか非エンジニアにはインストールが面倒そうだなぁと思った。

最近はDockerでも動かせるようだが、Docker使うよりはGit+Pythonのほうが楽か。

しかし、ChatGPTといいStable Diffusionといい、すごい時代になったなぁ。

ところで、Stable Diffusion web UIのメインのブランチ名、 master なのね。すっかり main に慣れたので、逆に珍しいと感じた。

LangChainのドキュメントのURLが変わっていた

前回書いたLangChain.jsのサンプルプロジェクトの設定と実行を、別PCで試してみたらどうもうまく動かない。

なんでだろうと思って調べると、本家のPython版LangChain、およびLangChain.jsの公式ドキュメントのURLが変わっていたのでメモ。

変更前後のURL

Python版LangChain、JavaScript版LangChain、両方ともドキュメントのドメインが変更されていた。

langchain.com独自ドメインとして、プログラミング言語名をサブドメインにする形になった模様。

JavaScript版のドメインは、2023/3/28の午後の時点で切り替わっていることを確認。

Python版にはリダイレクトが設定されていたが、JavaScript版はリダイレクトも設定されておらず、以下のissueの添付画像のように、崩れた画面が表示されていた。

github.com

この記事を書いている2023/3/31時点では、JavaScript版の旧ページは404になる。また、どうやらページ階層が変わったらしく、単純にドメインを更新後のものに変えただけでは開けないページが多い。

サンプルプロジェクトの修正

前回のサンプルプロジェクト設定で、Python版ドキュメントをダウンロードしているが、ドメインが変更されているため、対応する必要がある。

変更箇所は2つ。

  1. download.shwget 対象URLを、 https://langchain.readthedocs.io/en/latest/ から https://python.langchain.com/en/latest/ に変更
  2. ingest.tsconst directoryPath に設定する文字列を、 langchain.readthedocs.io から python.langchain.com に変更

また、Gitで管理する場合、 .gitignorelangchain.readthedocs.io/ が記載されているので、 python.langchain.com/ も追加しておくといい。

これらの変更で、ドメイン変更前と同様に動くようになった。

振り返り

仕事でまとめたドキュメントのリンクが全滅したので、不貞寝します。