はじめに
この記事は、個人的に 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 音声埋め込みの例です。
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 は長いので省略)。複数チャンクなので taskType は SINGLE_EMBEDDING ではなく SEGMENTED_EMBEDDING、embeddingMode は見本と同じく AUDIO_VIDEO_COMBINED、segmentationConfig ブロックの中に durationSeconds を入れて、と。
一つだけ見本と異なるのは source ブロックで、見本では s3Location 指定となっています。S3...
仕方ないので、重い腰を上げて S3 上にバケットを作りに行きます。
動画格納用の videos と、結果格納用の embeddings フォルダを作成。
IAM ユーザーに Bedrock と S3 のフルアクセスを付与(本番環境では絞って下さい)。なお、.env には以下を記述しています。
AWS_ACCESS_KEY_ID="my-id"
AWS_SECRET_ACCESS_KEY="my-key"
AWS_REGION="us-east-1"
そして見本実装をほぼコピペしただけの以下のプログラムを実行してみます。
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
より細切れにされました。
出力フォルダを覗いてみると、
確かに jsonl に埋め込みベクトルが書き出されていました。
まとめ
- 動画を base64 で投げる場合は長さ 30 秒未満で
-
durationSeconds併用不可(多分。違ってたら教えて欲しいです)
-
- それより長い動画を埋め込みたい場合は S3 バケット上への配置が必須
-
durationSeconds併用可。指定しなくてもよしなに細切れにしてくれる
-
おわりに
また nova2 関連で何かハマったらここに追記していきます。現場からは以上です。



