1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

amazon.nova-2-multimodal-embeddings-v1:0 を使おうとして色々ハマったのでメモ

1
Last updated at Posted at 2025-11-20

はじめに

 この記事は、個人的に Raggify という RAG システム構築用ライブラリを自作する途上、マルチモーダル埋め込みモデルの amazon.nova-2-multimodal-embeddings-v1:0 (以降、nova2)を使おうとして色々とつまずきがあったので、技術メモとしてスピンオフさせたものです。

 そもそも multimodal embeddings とは、ざっくり言うと、

  • RAG (Retrieval-Augmented Generation) 検索の中核技術で、
  • セマンティック検索(意味的に近いドキュメントを検索)をするのに必要な、テキスト → 意味空間ベクトル 変換の操作を一般に embedding(埋め込み)と言い、
  • 特に、入力として単一のモダリティ(例:テキスト)だけでなく画像、音声、動画等の複数のモダリティを扱うものは multimodal embeddings と呼ばれています。
  • nova2 は、その multimodal embeddings モデルの一つです。
(イメージ)
Newral Network Models
├─ LLM(Large Language Models)
│   ├─ OpenAI/gpt-5.1
│   ├─ Google/gemini-3-pro-preview
│   └─ ...
└─ Embedding Models
    ├─ singlemodal
    │   ├─ OpenAI/text-embedding-3-small
    │   ├─ Google/gemini-embedding-001
    │   └─ ...
    └─ multimodal
        ├─ Cohere/embed-v4.0
        ├─ AWS/amazon.nova-2-multimodal-embeddings-v1:0
        └─ ...

 埋め込みモデルも LLM と同様、各社 API 利用が可能です。ローカルモデルもあります。

nova2 について

リリース発表

 2025/10/28 に AWS からリリース発表がありました。動画もネイティブに扱えるのすごい!

サポート対象のファイルフォーマット

 以下のフォーマットをサポートしているらしい。

Media File Type File Formats supported Input Method Parsing Strategy
Image PNG, JPG, JPEG, GIF, WebP Base64 / Amazon S3 URI Image Vision Understanding
Text Document (Converse API Only) CSV, XLS, XLSX, HTML, TXT, MD, DOC Bytes / Amazon S3 URI Textual Understanding from the document only
Media Document (Converse API Only) PDF, DOCX Bytes / Amazon S3 URI Text with interleaved Image Understanding
Video MP4, MOV, MKV, WebM, FLV, MPEG, MPG, WMV, 3GP Base64 / Amazon S3 URI Video Vision Understanding

リクエスト json 形式

 受理する json は以下の形式。

{
    "schemaVersion": "nova-multimodal-embed-v1",
    "taskType": "SINGLE_EMBEDDING",
    "singleEmbeddingParams": {
        "embeddingPurpose": "GENERIC_INDEX" | "GENERIC_RETRIEVAL" | "TEXT_RETRIEVAL" | "IMAGE_RETRIEVAL" | "VIDEO_RETRIEVAL" | "DOCUMENT_RETRIEVAL" | "AUDIO_RETRIEVAL" | "CLASSIFICATION" | "CLUSTERING",
        "embeddingDimension": 256 | 384 | 1024 | 3072,
        "text": {
            "truncationMode": "START" | "END" | "NONE",
            "value": string,
            "source": SourceObject,
        },
        "image": {
            "detailLevel": "STANDARD_IMAGE" | "DOCUMENT_IMAGE",
            "format": "png" | "jpeg" | "gif" | "webp",
            "source": SourceObject
        },
        "audio": {
            "format": "mp3" | "wav" | "ogg",
            "source": SourceObject
        },
        "video": {
            "format": "mp4" | "mov" | "mkv" | "webm" | "flv" | "mpeg" | "mpg" | "wmv" | "3gp",
            "source": SourceObject,
            "embeddingMode": "AUDIO_VIDEO_COMBINED" | "AUDIO_VIDEO_SEPARATE"
        }
    }
}   

 記事中、至る所に SourceObject (see "Common Objects" section) って書いてあるのにその "Common Objects" section) が見当たらない。とりあえず、以下の 2 パターンは使えそう。

"source": {
  "bytes": "base64 エンコードしたメディア"
}
"source": {
  "s3Location": {
    "uri": "S3 上に置いたメディアの URI"
  }
}

Python による利用例

 色々載っていますが、S3 を使わず、認証情報は .env で渡すようにアレンジしてみました。以下は wav 音声埋め込みの例です。

test.py
import base64
import json

import boto3
from dotenv import load_dotenv

load_dotenv()

MODEL_ID = "amazon.nova-2-multimodal-embeddings-v1:0"
AUDIO_FILE = "test.wav"

with open(AUDIO_FILE, "rb") as f:
    audio_b64 = base64.b64encode(f.read()).decode("utf-8")

request_body = {
    "taskType": "SINGLE_EMBEDDING",
    "singleEmbeddingParams": {
        "embeddingPurpose": "GENERIC_INDEX",
        "embeddingDimension": 1024,
        "audio": {
            "format": "wav",
            "source": {"bytes": audio_b64},
        },
    },
}

client = boto3.client("bedrock-runtime", region_name="us-east-1")
res = client.invoke_model(
    modelId=MODEL_ID,
    body=json.dumps(request_body),
    accept="application/json",
    contentType="application/json",
)

print(res["ResponseMetadata"]["RequestId"])
body = res["body"].read().decode("utf-8")
print(body)

ハマりメモ

🎤 音声埋め込み:mp3 は通るけど wav と ogg が通らない

try & error

 先程の test.py で mp3 ファイルを投げてみたところ、無事埋め込みベクトルが返ってきました。

f4c1d8eb-d6b7-405b-a6d5-2b856cc648aa
{"embeddings":[{"embeddingType":"AUDIO","embedding":[0.0122651355,-0.036321066, ...

 一方、wav と ogg は、投げると以下のエラーが。

Traceback (most recent call last):
  File "/workspaces/raggify/temp/test4.py", line 31, in <module>
    res = client.invoke_model(
          ^^^^^^^^^^^^^^^^^^^^
  File "/workspaces/raggify/.venv/lib/python3.12/site-packages/botocore/client.py", line 602, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/workspaces/raggify/.venv/lib/python3.12/site-packages/botocore/context.py", line 123, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/workspaces/raggify/.venv/lib/python3.12/site-packages/botocore/client.py", line 1078, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.ValidationException: An error occurred (ValidationException) when calling the InvokeModel operation: Media bytes are malformed. Please provide a valid media file and try again.

 Media bytes are malformed だそうで。mp3 は通るので、リクエストの json 形式が悪いわけではなく、音声ファイルの中身の問題だろう、とあたりを付ける。

 ChatGPT くんに相談して、以下の通り整形してみるも効果なし。

ffmpeg -y -i original.wav -ac 1 -ar 16000 -c:a pcm_s16le text.wav

 ここまで拾い物の音声ファイルを使っていましたが、単純な 440Hz、3 秒間のトーンを生成して使用したらどうか。

ffmpeg -f lavfi -i "sine=frequency=440:duration=3" -ac 1 -ar 16000 -sample_fmt s16 test.wav
df993e87-9d2b-412b-8ae8-e39b8da1c259
{"embeddings":[{"embeddingType":"AUDIO","embedding":[-0.020448454,-0.04607718, ...

 いけるんかい!
 じゃあ ogg も…?

ffmpeg -f lavfi -i "sine=frequency=440:duration=3" -ac 1 -ar 16000 -c:a libopus test.ogg
The detected file MIME type application/ogg does not match the expected type audio/ogg. Reformat your input and try again.

 いけないんかい!
 でもエラーの内容が変わりました。MIME type が変、と言われています。

 コーデックに関し、「Nova は Vorbis OGG だけ audio/ogg と見なしていて、Opus OGG は application/ogg と見なしている」という仮説で、libopus ではなく libvorbis を試してみます。

ffmpeg -f lavfi -i "sine=frequency=440:duration=3" -ac 1 -ar 16000 -c:a libvorbis -q:a 4 test.ogg
fc29767b-1cde-448d-9369-47dc5f8c85a3
{"embeddings":[{"embeddingType":"AUDIO","embedding":[-0.020448454,-0.04607718, ...

 いけるんかい!

まとめ

  • mp3: なんでも OK?
    • ただし、(nova2 音声ファイル全般に)長さ 30 秒未満、リクエスト json と合わせて 100MB 未満等の制約あり
  • wav: バリデーションガチガチ?
    • 拾い物のファイルだと NG
    • ffmpeg で生成したトーンファイルだと OK
  • ogg: 一番ガチガチ?
    • 拾い物のファイルだと NG
    • トーンファイルも、コーデックに opus を指定すると NG、vorbis だと OK

 結論、wav と ogg は nova2 側の入力バリデーションが厳しすぎますね。まだ発表直後なので今後挙動が変わるかもしれませんが、しばらくは入力ファイルを mp3 に変換してから投げる等の対応が無難そうです。

🎬 動画埋め込み:durationSeconds 指定が受理されない

try & error

 nova2 の動画埋め込みでは、入力の動画長がデフォルトで 30 秒未満である必要があるのですが、ここの実装例を見ると、durationSeconds なるオプションを指定することで複数チャンクにセグメントできることが示唆されています。

# 音声付き動画の非同期埋め込みジョブを作成します
model_input = {
    "taskType": "SEGMENTED_EMBEDDING",
    "segmentedEmbeddingParams": {
        "embeddingPurpose": "GENERIC_INDEX",
        "embeddingDimension": EMBEDDING_DIMENSION,
        "video": {
            "format": "mp4",
            "embeddingMode": "AUDIO_VIDEO_COMBINED",
            "source": {
                "s3Location": {"uri": S3_VIDEO_URI}
            },
            "segmentationConfig": {
                "durationSeconds": 15  # 15 秒単位のチャンクにセグメント化します
            },
        },
    },
}

この例では、動画ファイルのビジュアルと音声の両方のコンポーネントから埋め込み情報を抽出する方法を示します。セグメンテーション特徴量により、長い動画が扱いやすいチャンクに分割されるため、何時間にも及ぶコンテンツを効率的に検索できます。

ほう

動画と音声の入力は最大 30 秒のセグメントをサポートし、モデルはより長いファイルをセグメント化できます。このセグメンテーション機能は、特に大容量のメディアファイルを扱う際に役立ちます。モデルはファイルを扱いやすいサイズに分割し、各セグメントの埋め込みを作成します。

ほうほう

 今、手元にそのまま投げると 30 秒制限に引っかかる mp4 ファイルがあります。

Invalid input configuration. Source video length exceeds the 30 second limit

 これをクリアすべく、早速自分の実装に durationSeconds を取り入れてみました。
 実行!

Malformed input request: #: required key [messages] not found, please reformat your input and try again.

 いけないんかい!

{
  "taskType": "SEGMENTED_EMBEDDING",
  "segmentedEmbeddingParams": {
    "embeddingPurpose": "GENERIC_INDEX",
    "video": {
      "format": "mp4",
      "embeddingMode": "AUDIO_VIDEO_COMBINED",
      "source": {
        "bytes": "..."
      },
      "segmentationConfig": {
        "durationSeconds": 15
      }
    },
    "embeddingDimension": 1024
  }
}

 失敗時のリクエストボディを表示してみるとこんな感じでした(base64 の bytes は長いので省略)。複数チャンクなので taskTypeSINGLE_EMBEDDING ではなく SEGMENTED_EMBEDDINGembeddingMode は見本と同じく AUDIO_VIDEO_COMBINEDsegmentationConfig ブロックの中に durationSeconds を入れて、と。

 一つだけ見本と異なるのは source ブロックで、見本では s3Location 指定となっています。S3...

 仕方ないので、重い腰を上げて S3 上にバケットを作りに行きます。

image.png

 動画格納用の videos と、結果格納用の embeddings フォルダを作成。

image.png

 IAM ユーザーに Bedrock と S3 のフルアクセスを付与(本番環境では絞って下さい)。なお、.env には以下を記述しています。

.env
AWS_ACCESS_KEY_ID="my-id"
AWS_SECRET_ACCESS_KEY="my-key"
AWS_REGION="us-east-1"

 そして見本実装をほぼコピペしただけの以下のプログラムを実行してみます。

test2.py
test2.py
import json
import time

import boto3
from dotenv import load_dotenv

load_dotenv()

MODEL_ID = "amazon.nova-2-multimodal-embeddings-v1:0"
VIDEO_URI = "s3://my-video-bucket/videos/test.mp4"
DST_URI = "s3://my-video-bucket/embeddings/"

bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-east-1")
s3 = boto3.client("s3", region_name="us-east-1")

model_input = {
    "taskType": "SEGMENTED_EMBEDDING",
    "segmentedEmbeddingParams": {
        "embeddingPurpose": "GENERIC_INDEX",
        "video": {
            "format": "mp4",
            "embeddingMode": "AUDIO_VIDEO_COMBINED",
            "source": {"s3Location": {"uri": VIDEO_URI}},
            "segmentationConfig": {"durationSeconds": 15},
        },
        "embeddingDimension": 1024,
    },
}

response = bedrock_runtime.start_async_invoke(
    modelId=MODEL_ID,
    modelInput=model_input,
    outputDataConfig={"s3OutputDataConfig": {"s3Uri": DST_URI}},
)

invocation_arn = response["invocationArn"]
print(f"Async job started: {invocation_arn}")

# ジョブが完了するまでポーリングします
print("\nPolling for job completion...")
while True:
    job = bedrock_runtime.get_async_invoke(invocationArn=invocation_arn)
    status = job["status"]
    print(f"Status: {status}")

    if status != "InProgress":
        break
    time.sleep(5)

# ジョブが正常に完了したかどうかをチェックします
if status == "Completed":
    output_s3_uri = job["outputDataConfig"]["s3OutputDataConfig"]["s3Uri"]
    print(f"\nSuccess! Embeddings at: {output_s3_uri}")

    # S3 URI を解析してバケットとプレフィックスを取得します
    s3_uri_parts = output_s3_uri[5:].split(
        "/", 1
    )  # プレフィックス「s3://」を削除します
    bucket = s3_uri_parts[0]
    prefix = s3_uri_parts[1] if len(s3_uri_parts) > 1 else ""

    # AUDIO_VIDEO_COMBINED モードは、embedding-audio-video.jsonl に出力します
    # output_s3_uri には既にジョブ ID が含まれているため、ファイル名を付加するだけです
    embeddings_key = f"{prefix}/embedding-audio-video.jsonl".lstrip("/")

    print(f"Reading embeddings from: s3://{bucket}/{embeddings_key}")

    # JSONL ファイルを読み取って解析します
    response = s3.get_object(Bucket=bucket, Key=embeddings_key)
    content = response["Body"].read().decode("utf-8")

    embeddings = []
    for line in content.strip().split("\n"):
        if line:
            embeddings.append(json.loads(line))

    print(f"\nFound {len(embeddings)} video segments:")
    for i, segment in enumerate(embeddings):
        print(
            f"  Segment {i}: {segment.get('startTime', 0):.1f}s - {segment.get('endTime', 0):.1f}s"
        )
        print(f"    Embedding dimension: {len(segment.get('embedding', []))}")
else:
    print(f"\nJob failed: {job.get('failureMessage', 'Unknown error')}")

↓実行結果

Async job started: arn:aws:bedrock:us-east-1:035597190979:async-invoke/wu3nj1kjx9ka

Polling for job completion...
Status: InProgress
Status: InProgress
Status: InProgress
Status: InProgress
Status: InProgress
Status: InProgress
Status: InProgress
Status: InProgress
Status: InProgress
Status: InProgress
Status: InProgress
Status: InProgress
Status: Completed

Success! Embeddings at: s3://my-video-bucket/embeddings/wu3nj1kjx9ka
Reading embeddings from: s3://my-video-bucket/embeddings/wu3nj1kjx9ka/embedding-audio-video.jsonl

Found 3 video segments:
  Segment 0: 0.0s - 0.0s
    Embedding dimension: 1024
  Segment 1: 0.0s - 0.0s
    Embedding dimension: 1024
  Segment 2: 0.0s - 0.0s
    Embedding dimension: 1024

Success !!!

 ちなみに durationSeconds を指定しなかった場合、

Found 8 video segments:
  Segment 0: 0.0s - 0.0s
    Embedding dimension: 1024
  Segment 1: 0.0s - 0.0s
    Embedding dimension: 1024
  Segment 2: 0.0s - 0.0s
    Embedding dimension: 1024
  Segment 3: 0.0s - 0.0s
    Embedding dimension: 1024
  Segment 4: 0.0s - 0.0s
    Embedding dimension: 1024
  Segment 5: 0.0s - 0.0s
    Embedding dimension: 1024
  Segment 6: 0.0s - 0.0s
    Embedding dimension: 1024
  Segment 7: 0.0s - 0.0s
    Embedding dimension: 1024

 より細切れにされました。

 出力フォルダを覗いてみると、

image.png

image.png

 確かに jsonl に埋め込みベクトルが書き出されていました。

まとめ

  • 動画を base64 で投げる場合は長さ 30 秒未満
    • durationSeconds 併用不可(多分。違ってたら教えて欲しいです)
  • それより長い動画を埋め込みたい場合は S3 バケット上への配置が必須
    • durationSeconds 併用可。指定しなくてもよしなに細切れにしてくれる

おわりに

 また nova2 関連で何かハマったらここに追記していきます。現場からは以上です。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?