AICU 音声 API
OpenAI 互換の音声合成(TTS)と音声認識(STT)。1本のキーで LLM・画像と同じエンドポイントから使えます。
音声合成 TTS
POST /v1/audio/speech(OpenAI 互換)
curl -X POST https://api.aicu.ai/v1/audio/speech \
-H "Authorization: Bearer aicu_live_xxx" \
-H "Content-Type: application/json" \
-d '{"input": "こんにちは、AICU です", "voice": "nao"}' \
--output speech.mp3
| パラメータ | 説明 |
|---|---|
input | 読み上げるテキスト |
voice | AICU 登録ボイスID(下記一覧)、または ElevenLabs の生 voice_id(20桁英数字)。既定 nao |
model | eleven_multilingual_v2(既定)または eleven_v3。下記「モデル」参照 |
seed | 生成の乱数種。省略時はキャラクターごとの既定値が使われます(キャラクターボイスと seed 参照) |
多言語対応の ElevenLabs ボイスで、日本語・英語などをそのまま合成できます。災害時の多言語音声案内などに。
:::info API キーのスコープ
読み上げ・文字起こしには tts スコープ付きのキーが必要です。403 で required_scope: tts が返る場合はスコープ不足です。https://api.aicu.ai/dashboard/keys の「権限変更」で付けられます(発行し直す必要はありません)。
:::
POST /v1/el/tts(ElevenLabs 互換パス)
body は /v1/audio/speech と同一。voice には AICU 登録ボイス(elena 等)だけでなく、ElevenLabs の生 voice_id をそのまま渡せます(自分の ElevenLabs アカウントで作成・クローンしたボイスなど)。TTS はゲートウェイ(鍵・課金・キャッシュ)に徹し、ボイスの善し悪しは ElevenLabs 側に任せる設計です — キャラクター固有の権利表記や royalty はまた別のレイヤー(/v1/char)で扱います。
# AICU 登録ボイス
curl -X POST https://api.aicu.ai/v1/el/tts \
-H "Authorization: Bearer aicu_live_xxx" \
-H "Content-Type: application/json" \
-d '{"input": "こんにちは、AICU です", "voice": "nao", "model": "eleven_v3"}' \
--output speech.mp3
# 生 voice_id(自分の ElevenLabs アカウントのボイス)
curl -X POST https://api.aicu.ai/v1/el/tts \
-H "Authorization: Bearer aicu_live_xxx" \
-H "Content-Type: application/json" \
-d '{"input": "Hello from AICU", "voice": "pqHfZKP75CvOlQylNhV4", "model": "eleven_v3"}' \
--output speech.mp3
モデル: Multilingual v2 と v3
| モデル | 特徴 | 1リクエストの文字数上限 |
|---|---|---|
eleven_multilingual_v2(既定) | 安定・実績重視。従来どおりの合成 | 10,000文字 |
eleven_v3 | text 中に [excited] [whispers] [laughs] のような角括弧の audio tag を書くと、その感情・話し方が音声に反映される | 5,000文字 |
長文を v3 で読み上げたい場合は、上限に収まるようチャンク分割してください(上限超過は 400 で明示的にエラーになります)。model に未知の値を渡した場合も 400(黙って v2 にフォールバックしません)。
:::info 長文の安定化(2026-08-16) 以前は長い入力(日本語で約3,000文字以上)が途中で 502/524 になることがありましたが、内部をストリーミング化して解消しました。表の上限(v2: 10,000文字 / v3: 5,000文字)まで安定して生成でき、応答が始まるまでの時間も短くなっています。音声はチャンクとして届き始めるので、受信側はレスポンスボディをそのままファイルに書けば完全な MP3 になります。同じ入力の再生成はキャッシュから返り、何度でも無料です。 :::
curl -X POST https://api.aicu.ai/v1/el/tts \
-H "Authorization: Bearer aicu_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"input": "[SHOUTING][excited] GOOOAL! What a strike!!",
"voice": "nao",
"model": "eleven_v3"
}' \
--output goal.mp3
上記は実際にサッカー実況サービス RoboFoot が使っているパターンです(tier に応じて [SHOUTING] [laughing] を重ねて熱量を上げる)。
ElevenLabs 公式の参考資料:
- モデル一覧・使い分け: https://elevenlabs.io/docs/eleven-creative/playground/text-to-speech#models-1
- v3 の紹介(audio tag の書き方): https://elevenlabs.io/ja/v3
- 声を探す(Voice Library): https://elevenlabs.io/app/voice-library
GET /v1/tts/voices・GET /v1/el/voices(要 API キー)
curl https://api.aicu.ai/v1/tts/voices \
-H "Authorization: Bearer aicu_live_xxx"
2026-08-17 から API キーが必要になりました(scope は問いません)。未認証は 401 が返ります。
| ボイス | 表記 | 声質 | 試聴 |
|---|---|---|---|
elena | エレナ・ブルーム | 落ち着いて聞き取りやすい、標準語の女性ナレーション | |
mei | メイ・ソレイユ | 元気で子供でも聴きやすい、明るい少女の声 | |
mina | ミナ・アズール | 落ち着いた知的なナレーション。長時間でも聴き疲れしない | |
nao | ナオ・ヴェルデ | 優しい理系男子のソフトボイス | |
saki | サキ・ノワール | 優しさとあやしい魅力がある囁きボイス | |
marsha | マーシャ・アランチャ | さわやかで聴きやすい元気な女性 | |
luc4 | ルカ(全力肯定彼氏くん) | 日本の関西を感じさせる明朗快活な青年 |
各サンプルは公式 seed で生成したメンバー自己紹介です(AiCuty-all-intro と同一テイク)。
いずれも ElevenLabs(多言語・v2/v3 両対応)。声質は tagline フィールドで返ります。
声はキャラクターごとに固定されています。 voice に slug を渡すだけで、
何度呼んでも同じ声・同じ抑揚が返ります。仕組みは
キャラクターボイスと seed を参照してください。
クレジット表記義務があるボイスは、レスポンスヘッダ x-aicu-credit に表記文言が返ります。使用したモデルは x-aicu-model に返ります。
長文の読み上げ(章まるごと・書籍全文)
1リクエストの上限(v2 は 10,000文字、v3 は 5,000文字)を超えるテキストは、 分割 → 個別に合成 → 連結します。数万〜十数万文字の原稿を1本のMP3にする場合の実装例です。
分割は句点で切る
文字数で機械的に切ると語の途中で切れて発音が乱れます。句点を優先し、 段落をまたがないようにすると繋ぎ目が自然になります。
def split_chunks(text: str, limit: int = 4800):
chunks, buf = [], ""
for para in text.split("\n"):
para = para.strip()
if not para:
continue
if len(buf) + len(para) + 1 <= limit:
buf = f"{buf}\n{para}" if buf else para
continue
if buf:
chunks.append(buf); buf = ""
while len(para) > limit: # 段落自体が長い場合
cut = para.rfind("。", 0, limit)
cut = cut + 1 if cut > limit // 2 else limit
chunks.append(para[:cut]); para = para[cut:]
buf = para
if buf:
chunks.append(buf)
return chunks
チャンク単位でキャッシュする
長文は必ずどこかで失敗します。テキストのハッシュをファイル名にして保存しておけば、 再実行時に未生成分だけを合成でき、原稿を一部だけ直したときも差分で済みます。
import hashlib, os, requests
from pathlib import Path
API = "https://api.aicu.ai/v1/tts/generate"
KEY = os.environ["AICU_API_KEY"] # scope: tts が必要
VOICE, CACHE = "mina", Path(".cache")
CACHE.mkdir(exist_ok=True)
def synth(text: str, i: int) -> Path:
tag = hashlib.sha1((VOICE + text).encode()).hexdigest()[:8]
dest = CACHE / f"{i:03d}-{tag}.mp3"
if dest.exists():
return dest
r = requests.post(API,
headers={"Authorization": f"Bearer {KEY}"},
json={"text": text, "slug": VOICE, "format": "mp3"}, timeout=600)
r.raise_for_status()
dest.write_bytes(r.content)
print(f" {i}: {r.headers.get('x-credits-used')} AP")
return dest
連結は ffmpeg の concat デミューサで
同じ設定で合成された MP3 同士なら、再エンコードなし(-c copy)で連結できます。速く、劣化もしません。
import subprocess
def concat(parts, dest="out.mp3"):
listing = Path("concat.txt")
listing.write_text("".join(f"file '{p.resolve()}'\n" for p in parts))
subprocess.run(["ffmpeg", "-y", "-loglevel", "error",
"-f", "concat", "-safe", "0", "-i", str(listing),
"-c", "copy", dest], check=True)
listing.unlink()
章や見出しへシークできるようにする
連結前に各チャンクの秒数を ffprobe で測って積算しておくと、
「この見出しから再生」を実装できます。プレイヤー側は audio.currentTime = 開始秒 を代入するだけです。
def duration(p) -> float:
out = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
"format=duration", "-of", "csv=p=0", str(p)],
capture_output=True, text=True).stdout.strip()
return float(out or 0)
offsets, cursor = {}, 0.0
for i, ch in enumerate(chunks):
part = synth(ch, i)
offsets[i] = round(cursor, 2) # このチャンクの開始秒
cursor += duration(part)
コストの目安
課金は送った文字数基準です(出来上がった音声の長さではありません)。
したがって、どの声で読んでも同じ原稿なら同じ金額になります。
実績はレスポンスヘッダ X-AICU-AP-Cost に返ります(x-credits-used も同じ値を返しますが、古いクライアント向けの別名です)。
現在の単価は料金を参照してください。
実測(同一テキスト261字、日本語):
| ボイス | 所要 | 12万字換算 |
|---|---|---|
| elena | 21.8秒 | 約2.8時間 |
| saki | 23.3秒 | 約3.0時間 |
| mei | 24.3秒 | 約3.1時間 |
| mina | 27.4秒 | 約3.5時間 |
| nao | 27.9秒 | 約3.6時間 |
12万字を読み上げて3時間強、約1,100 AP(¥11前後) です。 キャッシュヒットは無課金なので、原稿を直して再生成しても差分ぶんしかかかりません。
:::tip 読み上げ前にテキストを整える
URL・表・箇条書き記号をそのまま渡すと、記号を読み上げてしまい聴きづらくなります。
https?://\S+ は「(リンク省略)」に置換、表組みは読み飛ばし、# や - の行頭記号は落とす、
といった前処理を入れるだけで完成度が大きく変わります。
:::
音声認識 STT
POST /v1/audio/transcriptions(OpenAI 互換 / Whisper)
さくらの AI Engine の whisper-large-v3-turbo で文字起こしします。1リクエスト最大 30MB / 30分。エージェント向けの機械可読な仕様は https://api.aicu.ai/skills/stt にあります。
# テキストだけ
curl -X POST https://api.aicu.ai/v1/audio/transcriptions \
-H "Authorization: Bearer aicu_live_xxx" \
-F file=@meeting.mp3 -F language=ja \
-F prompt="AICU, ComfyUI, しらいはかせ" # 固有名詞のヒント(誤変換対策)
# → {"text": "文字起こしされたテキスト…"}
# セグメント単位のタイムスタンプ(字幕・議事録向け)
curl -X POST https://api.aicu.ai/v1/audio/transcriptions \
-H "Authorization: Bearer aicu_live_xxx" \
-F file=@meeting.mp3 -F language=ja \
-F response_format=verbose_json -F "timestamp_granularities[]=segment"
# → {"task":"transcribe","language":"ja","duration":60.0,"text":"…","segments":[{"id":0,"start":0.0,"end":4.32,"text":"…"}, …]}
# 字幕ファイルをそのまま
curl -X POST https://api.aicu.ai/v1/audio/transcriptions \
-H "Authorization: Bearer aicu_live_xxx" \
-F file=@meeting.mp3 -F language=ja -F response_format=srt -o meeting.srt
| パラメータ | 説明 |
|---|---|
file(必須) | 音声ファイル。30MB / 30分まで(超過は 413) |
language | ja / en など。指定すると精度と速度が上がる |
prompt | 固有名詞・専門用語のヒント |
response_format | json(既定)/ verbose_json / text / srt / vtt |
timestamp_granularities[] | segment / word(verbose_json のときのみ) |
response_format と timestamp_granularities[] はバックエンドにそのまま透過します。
:::tip 長い収録(会議・講義の録画) 動画をそのまま送らず、音声だけを mono 16kHz の mp3 に落として、30分ごとに分割してください(65分 → 3リクエスト)。60秒刻みのような細切れは、リクエスト数だけ増えてレート制限に当たりやすくなります。
ffmpeg -y -i recording.mp4 -vn -map 0:a:0 -ac 1 -ar 16000 -b:a 64k audio.mp3 # 3GB → 約8MB
ffmpeg -y -i audio.mp3 -f segment -segment_time 1800 -c copy part_%02d.mp3 # 30分ごと
:::
エラーとレート制限
HTTP ステータスをそのまま返します(429 を本文に包んで 200 で返すことはしません)。curl -f / raise_for_status() / OpenAI SDK の自動リトライがそのまま効きます。
| HTTP | 意味 | 対処 |
|---|---|---|
413 | ファイルが 30MB 超 | 音声のみ抽出・分割 |
429 | レート制限(ゲートウェイ 60 req/分/キー、またはバックエンドの枠) | Retry-After 秒待って再送 |
502 | バックエンド障害(error.upstream_status に上流のステータス) | 時間をおいて再送。続くなら X-AICU-Request-Id を添えて問い合わせ |
応答ヘッダ: X-AICU-AP-Cost(消費 AP)・X-AICU-Request-Id・X-AICU-Latency-Ms・X-RateLimit-Limit / -Remaining / -Reset。
料金
音声 10 秒 = 3 AP(10秒単位切り上げ・2026年9月9日から)。1時間 ≈ 1,080 AP ≈ $0.11。verbose_json のときは実測の duration で課金します。それ以外の形式には duration が無いので、受け取った音声の長さからの見積もりと、文字起こしの文字数からの見積もりのうち、大きいほうで課金します。
OpenAI SDK からそのまま
from openai import OpenAI
client = OpenAI(base_url="https://api.aicu.ai/v1", api_key="aicu_live_xxx")
# TTS
speech = client.audio.speech.create(model="tts-1", voice="nao", input="こんにちは")
speech.stream_to_file("out.mp3")
# STT(タイムスタンプ付き。429 は SDK が Retry-After を見て自動再送)
with open("meeting.mp3", "rb") as f:
tr = client.audio.transcriptions.create(
model="whisper-large-v3-turbo", file=f, language="ja",
response_format="verbose_json", timestamp_granularities=["segment"],
)
for seg in tr.segments:
print(f"{seg.start:8.2f} --> {seg.end:8.2f} {seg.text}")
ベース URL と API キーを差し替えるだけで、既存の OpenAI 互換コードから使えます。
© 2026 AICU Inc.