PR
AIで記事作成を効率化したいなら → 高品質SEO記事生成AIツール【Value AI Writer byGMO】
※本記事は2026年9月に実際に行った作業の記録です。APIの仕様・料金は変更される可能性があります。
こんにちは!Conomacです。
当ブログでは、AIエージェント基盤のOpenClawとFal.aiを組み合わせて、画像生成からWordPressへの下書き作成までを自動化しています。今回はその中でも「実在の場所をAIでイラスト化する」ときに効く、パブリックドメイン(PD)写真を参照画像に使う方法をまとめます。
お題は日光東照宮の陽明門。極彩色の彫刻で埋め尽くされ、「日暮御門」とも呼ばれる国宝の門です。プロンプトだけで描くと、どうしても“それらしい別物”になりがちな建築も、実写を参照させることでディテールが一気に締まります。Wikimedia CommonsでPD素材を探すところから、Fal.aiで参照生成するまでの全手順と、実際につまずいたポイントを公開します。
この記事でわかること
- Wikimedia Commons APIで「パブリックドメインの写真だけ」を機械的に探す方法
- Fal.aiで参照画像を使うときの落とし穴(
file_download_error)と回避策 - FLUX+LoRA構成のまま image-to-image で実在の建物に寄せる手順
- ジョブのポーリングで失敗しないための正しい順番
- 生成画像に文字が混入していないかをローカルで検査する方法
なぜ「プロンプトだけ」では実在の建物に寄らないのか
FLUX系のモデルは、プロンプトの言葉から「それらしい景色」を組み立てるのが得意です。ただし、特定の建物の固有の意匠(陽明門なら唐破風の屋根、びっしりと並ぶ彫刻、朱と金の配色)は、テキストで説明しきれません。参照画像を渡して「同じ構図で描き直す」ほうが、はるかに正確になります。
そこで使うのが、Wikimedia CommonsのPD(パブリックドメイン)/CC0の写真です。著作権が切れている、または権利者が権利を放棄している素材なので、参照に使うのも、生成物を公開するのも自由です(出典表示は習慣として書いておくのがおすすめです)。
世の中の「フリー素材」には、クレジット必須・改変禁止・商用不可など、条件付きのものも多くあります。その点Commonsは、ライセンス情報がAPIで機械的に取れるので、自動化と相性がいいんですね。
使用した構成(チェックポイントとLoRA)
- 実行環境: Fal.ai の
fal-ai/flux-lora(FLUX.1 dev ベース) - LoRA 3種: Sinozick Style(scale 0.8)/ Blue Future(0.7)/ Gothica(0.7)
- 出力: 1024×592 / 25 steps
- 参照生成:
fal-ai/flux-lora/image-to-image(strength 0.5)
ひとつ補足です。SeaArtなどで使われている統合済みのフルチェックポイント(十数GB以上)は、そのままFalに持ち込めません。同じFLUX.1 dev系のベースに、同じLoRAを同じweightで載せることで画風を寄せる、というのが現実的な落としどころになります。
手順1: Wikimedia Commons APIでPD/CC0素材を探す
CommonsはMediaWiki APIが使えるので、ライセンスまで機械的に絞り込めます。画像情報の extmetadata.LicenseShortName を見て、Public domain や CC0 のものだけを採用します。
const API = 'https://commons.wikimedia.org/w/api.php';
const PD = /(public domain|pd-|cc0)/i;
const UA = 'my-blog-research/1.0 (contact@example.com)';
async function searchPd(query) {
const url = new URL(API);
const params = new URLSearchParams({
action: 'query',
format: 'json',
generator: 'search',
gsrsearch: query,
gsrnamespace: '6', // ファイル名前空間だけ
prop: 'imageinfo',
iiprop: 'url|size|extmetadata',
});
url.search = params; // params はそのまま代入できる
const res = await fetch(url, { headers: { 'User-Agent': UA } });
const json = await res.json();
const pages = Object.values(json.query.pages || {});
return pages.map((p) => {
const info = p.imageinfo[0];
return {
title: p.title,
license: info.extmetadata.LicenseShortName.value,
width: info.width, height: info.height, url: info.url,
};
}).filter((r) => {
return PD.test(r.license) && r.width >= r.height;
});
}
ポイントは3つあります。①User-Agentにサイト名と連絡先を入れる ②gsrnamespace=6でファイル名前空間だけを検索する ③短時間に連打すると「You are making too many requests」で弾かれるので、リクエストの間隔を空ける。このあたりは、実際に動かしながら調整しました。
手順2: 参照画像をFalのストレージに上げる(ここが最大の落とし穴)
Wikimediaで見つけた写真のURLを、そのままimage_urlに渡すと失敗します。返ってくるのはこのエラーです。
{
"detail": [{
"loc": ["body", "image_url"],
"msg": "Failed to download the file.",
"type": "file_download_error",
"input": "https://upload.wikimedia.org/.../Yomeimon_M3249.jpg"
}]
}
原因は、Fal側のダウンローダーがWikimediaのファイルを取得できないこと(file_download_error)。回避策はシンプルで、いったんローカルにダウンロードして、Falのストレージにアップロードしてから渡すという流れにします。アップロードは「アップロード先URLをもらう → PUTで本体を送る」の2段階です。
// Wikimedia から落として、Fal のストレージに上げてから使う
const UPLOAD_API =
'https://rest.alpha.fal.ai/storage/upload/initiate';
const KEY_HEADER = 'Key ' + process.env.FAL_KEY;
async function hostOnFal(localPath, fileName) {
const buf = await readFile(localPath);
// 1) アップロード先をもらう
const init = await fetch(UPLOAD_API + '?storage_type=fal-cdn-v3', {
method: 'POST',
headers: {
'Authorization': KEY_HEADER,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content_type: 'image/jpeg',
file_name: fileName,
}),
});
const { upload_url, file_url } = await init.json();
// 2) 本体を PUT する
await fetch(upload_url, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: buf,
});
return file_url; // これを image_url に渡す
}
手順3: image-to-imageで参照生成する
あとは fal-ai/flux-lora/image-to-image に投げるだけです。LoRAの構成・画像サイズ・stepsは、通常のテキスト生成のときとまったく同じにして、画風がずれないようにします。違うのはimage_urlとstrengthが増える点だけです。
// 参照画像つきで生成(LoRA構成はプロンプトのみのときと同じ)
const I2I = 'https://queue.fal.run/fal-ai/flux-lora/image-to-image';
const CIVITAI = 'https://civitai.com/api/download/models/';
const BLUE_FUTURE = 'https://v3b.fal.media/files/b/0aa97b0b/' +
'QaklsR1mN2gvVvqFeZJ-k_Blue_Future.safetensors';
const BEARER = 'Bea' + 'rer '; // 2つに分けて組み立てる
const body = {
prompt: 'anime illustration of the same scene, ...',
image_url: falFileUrl, // 手順2で得たURL
strength: 0.5, // 元写真をどこまで残すか
loras: [
{ path: CIVITAI + '884629', scale: 0.8 }, // Sinozick
{ path: BLUE_FUTURE, scale: 0.7 }, // Blue Future
{ path: CIVITAI + '785935', scale: 0.7 }, // Gothica
],
num_images: 1,
image_size: { width: 1024, height: 592 },
num_inference_steps: 25,
};
const res = await fetch(I2I, {
method: 'POST',
headers: {
'Authorization': BEARER + process.env.FAL_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const job = await res.json(); // { status_url, response_url }
strengthは「元写真をどこまで残すか」のつまみです。小さすぎると元の写真のままでイラスト化が進まず、大きすぎると元の構図が消えてプロンプトのみの生成と変わらなくなります。今回は0.5で、建物の意匠を残しつつ人物を描き込めるバランスに落ち着きました。
手順4: ポーリングは「status_url → COMPLETED → response_url」の順で
ここでも一度ハマりました。response_urlを先に叩くと、まだ処理中の場合にこんなレスポンスが返ってきます。
{
"detail": "Request is still in progress",
"request_id": "01a0acc8-...",
"status": "IN_QUEUE"
}
一見すると生成失敗に見えますが、これは「まだ出来ていない」だけです。正しい順番は、status_urlをポーリングしてstatusがCOMPLETEDになってから、response_urlをGETする、という流れになります。
// NG: いきなり response_url を叩くと "Request is still in progress"
// OK : status_url が COMPLETED になってから response_url を GET
const auth = { 'Authorization': 'Bea' + 'rer ' + process.env.FAL_KEY };
let image = null;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 4000));
const s = await fetch(job.status_url, { headers: auth });
const status = await s.json();
if (status.status === 'FAILED') throw new Error('failed');
if (status.status === 'COMPLETED') {
const r = await fetch(job.response_url, { headers: auth });
const result = await r.json();
image = result.images[0].url;
break;
}
}
手順5: 生成画像に文字が混ざっていないか検査する
日本の風景をプロンプトで描くと、看板や標識の「読めない文字」が写り込むことがあります。これを弾くために、macOSのVisionフレームワークでOCRをかけて、テキストが検出されないことを確認しています。
# 生成画像に文字(看板・ロゴ)が混入していないかローカルで検査する
# macOS の Vision フレームワークを osascript から呼ぶ
osascript -l JavaScript ocr.jxa /absolute/path/to/image.jpg
# → 何も出力されなければ、文字は写り込んでいない
何も出力されなければ、看板やロゴの文字は写り込んでいません。逆に文字が検出されたら、その画像は使わずにプロンプトを見直します。
結果: 元のPD写真と、イラスト化した「日暮御門」を並べてみる
左が参照元のPD写真(Wikimedia Commons / Fg2 / パブリックドメイン)、右がそれを参照して生成したイラストです。参照画像を渡したことで、門の彫刻の密度や屋根のシルエット、石畳の広がりが、写真と同じ構図でイラストに置き換わっているのがわかります。


参考までに、同じプロンプト・同じLoRA構成で、参照画像を渡さずに生成した版も載せておきます。参照あり/なしでどこが変わるのか、見比べてみてください。

つまずきポイントまとめ
| 症状 | 原因と対処 |
WikimediaのURLでfile_download_error | FalのダウンローダーがWikimediaを取得できない。ローカルに落としてFalストレージへ上げてから渡す |
Request is still in progress | 完了前にresponse_urlを叩いている。status_urlがCOMPLETEDになってから取得する |
Commons APIがtoo many requests | 連打している。User-Agentに連絡先を書き、リクエスト間隔を空ける |
| 参照写真が縦長で構図が崩れる | 出力が1024×592の横長なので、縦長素材は引き伸ばされる。横長の素材を選ぶのが無難 |
| 看板の文字が崩れて写り込む | プロンプトにno text, no letters, no signage, no logosを足したうえで、OCRで検査する |
閲覧できません(403)でPOSTが弾かれる | WAF(SiteGuard Lite)が本文中の「文字列化メソッド」の記述をXSSの可能性として誤検知する。管理画面のWAFログに理由が出るので、まず理由を確認する。回避は「その書き方を使わない」のが基本(代入で済む場面は代入にする) |
コストの目安
今回の3枚(プロンプトのみ3枚+参照版3枚の計6枚)で、Fal側の実費は$0.1前後でした。テキスト生成も含めた記事1本のコストはおよそ30円程度で、これは過去の実測と同じ水準です。参照生成を足しても、1枚あたり数円の世界なので、試行錯誤のコストはほとんど気になりません。
まとめ
実在の建物をイラスト化するときは、「PD写真を参照画像に渡す」のがもっとも効きます。Wikimedia Commons APIなら、ライセンスがPD/CC0の素材だけを機械的に拾えるので、権利的にも安心して自動化に組み込めます。
今回ハマった2点(file_download_errorとRequest is still in progress)は、どちらも「APIの返し方を知らないと生成失敗に見える」タイプの罠でした。同じところで止まる人の助けになればうれしいです。
最後までお読みいただきありがとうございました!
🎬 あわせて読みたい: AIショート動画に喋らせてみた|VOICEVOX導入でkokoro版と聴き比べ+実測コスト全公開
PR・広告
画像素材を扱うなら
参照写真や生成データの保存に、高速な外付けSSDがあると作業が快適になります。