AIで曲探しができるボカロポータルサイトを作ろうと思い立ち、OpenAIの研究を始めたわけだが、探した曲からインスピレーションを得たスチルが作成できると素敵だよねー、と画像生成AIに手を出してみた話。
これがクッソ高い。
やった~ API叩けた~ とテストで10枚程作成して、Usage みてびっくり
gpt-image-1 image, output
■ $2.45 total
え!10枚くらい作っただけなんだが!
$2.45!?(日本円で360円くらい)
クオリティを指定できるが、high のレベルが段違い過ぎて実質 high 一択なのも痛い。
これはさすがにエンドユーザに好きに使わせるわけにはいかないな~。
ChatCompletion や Embeddings はいくら叩いても 0.01$ とか財布にやっさしーと思っていたのにこの仕打ち。
1枚当たり30円~50円である。
入力に前の画像を使ったり、修正指示を繰り返すと1枚当たり100円を超える可能性もあるというので驚きである。
遊び半分で画像を作りまくるとびっくり請求が飛んで来るので気をつけましょう。
ここからが本題
個人で使うとめっちゃ高い画像生成だが、ChatGPT Plus なら定額&使い放題 なのである。月22$(税込み)で、さっき1枚で100円超えることもあると言った画像生成が使い放題。今月すでに100枚くらい作って遊んでいるので、十分元が取れた計算になる。
ChatGPT Plusで使える画像生成モデルが image-1 かは不明だが、OpenAI の Responses API で使われる画像生成モデルが image-1 なのでたぶん同じ。
これに加えて GPT-5 でのチャットも使い放題、さらにテーマごとにプロジェクトで管理すればチャットが散らばらなくていいということに気づいた。
「月 22$ か~。とりあえず1月だけ試して継続利用はないかな」と思っていたが「半年くらいは継続しよっかな」くらいには依存し始めている。
OpenAI の回し者みたいになってしまったが、皆も是非一度使ってみて欲しい。
せっかくなので OpenAI Responses API の使い方
このままだと ChatGPT Plus の宣伝で終わってしまうので、技術的な話。
まず筆者は生粋の PHP スクリプターである。
最近 Python をようやく理解できるようになってきたが、インデントでブロックを書く書き方にいまだに慣れない。
「途中で空行入れてもいいの?」 とか 「見た目揃ってるけど、タブとスペースでぐちゃぐちゃになってたらどうなのるの?」 とか 「forで1から回したいだけなのにいちいち配列作るとか、100万回まわしたいときメモリ足りなくならない?」 とかいろいろ気になって仕方がない。
素直に { } でくくれるならもう少し好きになれたかもしれない。
何が言いたいかというと、ここに OpenAI の Responses API を PHP から叩くコードを掲載する。
Function Calling まで使った例は探しても無かったので結構希少かもしれない。
/**
* チャット回答生成
*/
public function chatCompletion(
$conv_id,
string $add_msg,
string $model = "gpt-4o-mini"
): array {
// ベースとなるリクエスト作成
$request = $this->getBaseRequest($conv_id, $model);
// ユーザ入力を追加
$user = [
"role" => "user",
"content" => [
["type" => "input_text", "text" => $add_msg]
]
];
$request["input"] = [$user];
// 以下はGPT-5 モデルでは使えない
//$request["top_p"] = 0.5; // 上位何% を結果として使うか 0 に近い程固定的 temperature とどちらか一方を指定
//$request["temperature"] = 1.0; // ランダム性 0~2。高いほどランダム
try{
// function_call に対応するためループさせる
$loop_cnt = 0;
while(true){
$res = $this->request("responses", $request);
$input = [];
foreach($res["output"] as $row){
if($row["type"] == "reasoning"){
// 試行プロセスは無視
continue;
}
if($row["type"] == "function_call"){
// 関数呼び出しのリクエスト発生
$loop_cnt++;
if($loop_cnt > 6){
$result = [
"error" => [
"code" => "RATE_LIMIT",
"message" => "Function Calling の上限に達しました。しばらく待ってから再試行してください。"
]
];
}else{
if($row["name"] == "my_func"){
// 自作の関数をコール
$param = json_decode($row["arguments"], true);
$result = myFunc($param);
}
}
$input[] = [
"type" => "function_call_output",
"call_id" => $row["call_id"],
"output" => json_encode($result, JSON_UNESCAPED_UNICODE)
];
continue;
}
}
if(count($input) <= 0){
// 再入力がないのでループ終了
break;
}
// 再入力を組み立ててループさせる
$request = $this->getBaseRequest($conv_id, $model);
$request["input"] = $input;
}
}catch(Exception $e){
$res = [
"output" => [
[
"type" => "message",
"content" => [
[
"text" => "エラー: " . $e->getMessage()
]
]
]
]
];
}
return $res["output"];
}
/**
* ベースとなるリクエスト配列を構築
*/
public function getBaseRequest($conv_id, $model){
$request = [];
$request["model"] = $model;
$request["tool_choice"] = "auto";
$request["tools"] = [
[
// AI に Web 検索を許可する場合はこれを定義
"type" => "web_search",
"user_location" => [
"type" => "approximate",
"country" => "JP",
"timezone" => "Asia/Tokyo"
]
],
[
// AI に画像生成を許可する場合はこれを定義
"type" => "image_generation",
"output_format" => "jpeg"
],
[
"type" => "function",
"name" => "my_func",
"description" => "Function Calling を使う場合はここに定義。properties は例",
"parameters" => [
"type" => "object",
"properties" => [
"keyword" => [
"type" => "string",
"description" => "キーワード"
],
"publishedAfter" => [
"type" => "string",
"description" => "この時間以降に公開された曲が検索対象"
],
"publishedBefore" => [
"type" => "string",
"description" => "この時間以前に公開された曲が検索対象"
],
"order" => [
"type" => "string",
"enum" => ["title", "publish_time ASC", "publish_time DESC"],
"description" => "並び順"
],
],
"required" => ["keyword", "publishedAfter", "publishedBefore", "order"],
"additionalProperties" => false
],
"strict" => true,
],
];
$request["conversation"] = $conv_id;
return $request;
}
$conv_id はこのように作る。
/**
* 保存用のキー生成
*/
public function createConversations(string $sys_prompt, string $dev_prompt): string
{
$res = $this->request("conversations", [
"items" => [
[
"type" => "message",
"role" => "system",
"content" => $sys_prompt
],
[
"type" => "message",
"role" => "developer",
"content" => $dev_prompt
]
]
]);
return $res["id"];
}
こうすると、いちいちチャットレスポンスを積み上げて毎回渡さなくていいので INPUT を消費しない。(ただしキャッシュを消費するようである)
conversations で取得したIDは30日間利用できるらしい。(リファレンスに書いてあった)
request メソッドは次の通り。
/**
* 汎用リクエスト
*/
private function request(string $path, array $payload): array
{
$ch = curl_init("{$this->baseUrl}/{$path}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$this->apiKey}",
"Content-Type: application/json",
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$resp = curl_exec($ch);
if ($resp === false) {
throw new Exception("cURL error: " . curl_error($ch));
}
curl_close($ch);
$data = json_decode($resp, true);
if (isset($data["error"])) {
throw new Exception("OpenAI API error: " . $data["error"]["message"]);
}
return $data;
}
これらを OpenAIAPIHelper としてクラス化して利用する。
レスポンスには応答メッセージや、画像が生成された場合は Base64 で画像のバイナリが返ってくるので取り出して使う。
チャットを続ける場合は、conversations で取得した ID を使って再度 chatCompletion をコールする。
告知
ここで得た得た知識を活かして 「ボカロAIポータル」 を開発中です。
クラファンも走らせていますが、なかなか支援が集まらなく、半ば諦めモードに入っております。
もう自分一人の趣味で使うだけいっかなとか考えています(つまり最後まで作るつもりはある)。
それでも、ここまで読んでくれた読者様、少しでも面白いと思ったら、応援してくれると本当に嬉しいです。
ボカロAIポータル作成:AIと一緒に“最高の一曲”を見つけよう
ぜひ応援お願いします。
デモサイトを公開しています。
感想いただけるとうれしいです。
https://www.pyurucos-lab.com/