fal genmedia CLI 已經處理了模型探索、schema 查詢、非同步 request ID、下載與 JSON 收據;真正需要自行補上的,是一層能限制併發數、重試暫時性錯誤、續跑已完成工作,並保存非同步任務進行中 ID 的編排器。
先完成安裝,讓每次執行都能重現
macOS 與 Linux 可依照專案 README 的方式安裝:
curl https://genmedia.sh/install -fsS | bash
genmedia setup --non-interactive --api-key "$FAL_KEY" --no-auto-update
Windows 則使用文件提供的 PowerShell 安裝指令:
irm https://genmedia.sh/install.ps1 | iex
genmedia setup --non-interactive --api-key "$env:FAL_KEY"
genmedia CLI README 指出,genmedia 支援 FAL_KEY、JSON 輸出、非互動式設定,以及可選用的背景更新檢查。若在 CI 環境執行,應透過 runner 的 secret store 注入金鑰,避免把它寫進腳本或命令紀錄。
啟動批次前,先固定 endpoint,並確認它目前的實際介面規格:
genmedia models "image to video" --json
genmedia schema bytedance/seedance-2.0/image-to-video --json
genmedia pricing bytedance/seedance-2.0/image-to-video --json
fal 指南建議的流程很實用:models → schema → run → status → download。不要假設某個影片 endpoint 的旗標能直接套用到另一個 endpoint;workflow skill 特別建議,在遇到驗證錯誤後應重新查閱 schema。
把批次工作當成佇列管理,不是 shell 迴圈
官方 genmedia 文件說明了非同步執行方式,但沒有承諾提供「讀取這份 CSV 並執行 500 列」的原生指令,也未提供自動重試策略。較合理的分工是:CLI 負責理解供應商的任務執行,外層 wrapper 則負責控制佇列。
一筆可長期保存的任務紀錄,至少應包含:
| 欄位 | 用途 |
|---|---|
id | 可用於續跑與去重的穩定輸入識別碼 |
endpoint | 實際使用的完整模型路徑 |
prompt | 用於重現與稽核 |
status | pending、submitted、complete、failed 或 skipped |
request_id | genmedia status 所需的識別碼 |
attempts | 避免重試失控 |
output | 可預測的本機輸出路徑 |
error | 讓失敗列可以後續處理 |
圖片轉影片或其他耗時影片任務應使用 --async。取得回傳的 request_id 後要立刻保存,接著以 endpoint ID 與 request ID 一起輪詢:
genmedia run bytedance/seedance-2.0/image-to-video \
--image_url "$IMAGE_URL" \
--prompt "Slow product turn on a studio table; no text or logo" \
--duration 4 --resolution 720p --aspect_ratio 16:9 \
--async --json > logs/shot-001-submit.json
REQUEST_ID=$(jq -r '.request_id' logs/shot-001-submit.json)
genmedia status bytedance/seedance-2.0/image-to-video "$REQUEST_ID" \
--download "outputs/{request_id}_{index}.{ext}" --json > logs/shot-001-result.json
{request_id}_{index}.{ext} 這種命名模式可降低兩個任務在無聲無息間覆寫彼此輸出的風險。JSON 紀錄應直接放在媒體檔旁邊,而不是丟進另一個暫存目錄。
只重試有機會恢復的失敗
可先採用以下實務策略:
- 重試網路逾時、連線重設、HTTP 429 與暫時性的 5xx 回應。
- 採用指數退避,例如 2、4、8、16、32 秒,並加入少量隨機抖動。
- 限制每列的嘗試次數,例如最多提交四次,或在一段時間內最多輪詢五次狀態。
- 401/403 驗證錯誤、422 schema 驗證錯誤、安全拒絕與格式錯誤的 JSON 都不應重試。
- 如果提交逾時,但請求可能已送達供應商,建立第二筆付費請求前應先檢查已保存的請求紀錄。
這套策略應由 wrapper 實作;公開的 genmedia README 說明的是命令與任務生命週期操作,並未保證提供自動重試行為。遇到 422 時,請讀取 validation_errors、再次執行 genmedia schema,並修正被點名的欄位,而不是盲目重新提交。
可直接複製使用的 Python 批次 wrapper
下方 wrapper 是同步圖片批次處理的起點:它使用帶有引數列表的 subprocess.run,略過已完成的輸出路徑、限制並行工作數、重試暫時性的程序失敗,並以原子方式寫入 manifest。若要處理長時間影片任務,必須先保存非同步提交結果再輪詢:submit -> write endpoint/request_id -> restart -> poll saved request_id -> download;只有在不存在 request ID 時才重新提交。
#!/usr/bin/env python3
import concurrent.futures as pool
import json, os, random, subprocess, tempfile, threading, time
from pathlib import Path
ENDPOINT = "fal-ai/flux/dev"
OUT = Path("outputs/images")
LOG = Path("outputs/logs")
MAX_WORKERS = 3
MAX_ATTEMPTS = 4
MANIFEST_LOCK = threading.Lock()
JOBS = [
{"id": "shoe-001", "prompt": "Black running shoe, clean studio product photo", "file": "shoe-001.png"},
{"id": "shoe-002", "prompt": "Black running shoe on wet pavement at dawn", "file": "shoe-002.png"},
]
OUT.mkdir(parents=True, exist_ok=True)
LOG.mkdir(parents=True, exist_ok=True)
MANIFEST = Path("outputs/manifest.json")
PREVIOUS = json.loads(MANIFEST.read_text()) if MANIFEST.exists() else {"results": []}
STATE = {r["id"]: r for r in PREVIOUS.get("results", [])}
DONE = {k: r for k, r in STATE.items() if r.get("status") == "complete"}
MAX_REQUESTS = len(JOBS) * MAX_ATTEMPTS
if MAX_REQUESTS > 100:
raise SystemExit(f"request ceiling exceeded: {MAX_REQUESTS}")
def save_result(result):
with MANIFEST_LOCK:
STATE[result["id"]] = result
payload = {"endpoint": ENDPOINT, "max_requests": MAX_REQUESTS,
"results": list(STATE.values())}
fd, tmp = tempfile.mkstemp(dir=MANIFEST.parent, prefix="manifest.", text=True)
with os.fdopen(fd, "w") as f:
json.dump(payload, f, indent=2)
os.replace(tmp, MANIFEST)
TRANSIENT_WORDS = ("429", "500", "502", "503", "504", "timeout", "temporarily", "connection")
def run_one(job):
target = OUT / job["file"]
receipt = LOG / f"{job['id']}.json"
if job["id"] in DONE and target.exists() and target.stat().st_size > 0 and receipt.exists():
return DONE[job["id"]]
cmd = ["genmedia", "run", ENDPOINT, "--prompt", job["prompt"],
"--num_images", "1", "--download", str(target), "--json"]
last_error = ""
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
p = subprocess.run(cmd, text=True, capture_output=True, timeout=900)
raw = p.stdout.strip()
if p.returncode != 0:
last_error = p.stderr[-1000:] or raw[-1000:]
if not any(w in last_error.lower() for w in TRANSIENT_WORDS):
break
if attempt < MAX_ATTEMPTS:
time.sleep((2 ** attempt) + random.random())
continue
try:
data = json.loads(raw) if raw else {}
except json.JSONDecodeError as exc:
return {**job, "status": "failed", "attempts": attempt, "error": f"invalid JSON: {exc}"}
if target.exists():
(LOG / f"{job['id']}.json").write_text(json.dumps(data, indent=2))
return {**job, "status": "complete", "attempts": attempt, "output": str(target)}
last_error = p.stderr[-1000:] or raw[-1000:]
if not any(w in last_error.lower() for w in TRANSIENT_WORDS):
break
except (subprocess.TimeoutExpired, OSError) as exc:
last_error = str(exc)
if attempt < MAX_ATTEMPTS:
time.sleep((2 ** attempt) + random.random())
return {**job, "status": "failed", "attempts": MAX_ATTEMPTS, "error": last_error}
results = []
with pool.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = [executor.submit(run_one, job) for job in JOBS]
for future in pool.as_completed(futures):
result = future.result()
results.append(result)
save_result(result)
print(json.dumps(results, indent=2))
若改用影片,只要替換 ENDPOINT,再加入該 endpoint schema 所要求的旗標即可。建立圖片轉影片鏈路時,先以 genmedia upload ./frame.png --json 上傳本機圖片一次,將回傳 URL 傳給影片任務,並在 manifest 中保留兩筆紀錄。這個 wrapper 並不表示 genmedia 本身會估算或強制執行最高支出;它只會避免重複的本機工作,並限制重試次數。
付費生成前先設成本閘門
genmedia pricing <endpoint_id> --json 是查價工具,不是費用保留或預算上限。請在批次前使用它,再依列數、每列輸出數、解析度/時長設定與最大重試次數,計算保守的成本上限。
可採取的實務閘門如下:
| 控制項 | 實作方式 |
|---|---|
| 列數硬上限 | manifest 超過核准數量時拒絕啟動 |
| 模型層級 | 先以較便宜或快速的 endpoint 製作草稿,通過 QA 後才渲染最終版本 |
| 輸出上限 | 明確指定 num_images,不要依賴預設值 |
| 重試預算 | 將重試與首次嘗試分開計數 |
| 續跑 | 略過已有驗證本機輸出的列 |
| 取消 | 適當時以 genmedia status ... --cancel 取消佇列中的工作 |
應將查價回應和 manifest 一併保存,因為模型價格與 endpoint 可用性可能改變。若供應商沒有公開可比較的計價單位,應把結果標示為請求數量上限,而不是帳單金額。
fal genmedia CLI、Replicate CLI 與自訂腳本怎麼選
這些工具解決的是不同層次的問題。Replicate 官方 CLI 提供執行與串流 prediction、查看模型 schema、上傳、訓練與模型管理等命令。若工作從探索 fal endpoint 開始,並需走完 fal 的佇列與下載生命週期,genmedia 會更順手。
| 選擇 | 最適合的情境 | 主要取捨 |
|---|---|---|
| fal genmedia CLI | fal 原生模型搜尋、schema 查詢、非同步任務、下載與 agent shell 操作 | 供應商專用;批次策略仍需放在 CLI 外部 |
| Replicate CLI | Replicate prediction、串流、模型/schema 操作與訓練命令 | 模型目錄與生命週期都不同;不要期待 fal 的 endpoint ID 或旗標可以直接沿用 |
| 自訂 Python/HTTP 腳本 | 多供應商路由、核准閘門、資料庫狀態、佇列與帳務策略 | 驗證、schema 變動、輪詢、下載與錯誤處理都由你維護 |
我的建議很直接:探索階段直接使用 genmedia;要處理只使用 fal 的正式批次工作,則加上一個小型 Python wrapper。只有在切換供應商已是明確需求時,才需要升級成自訂的供應商抽象層,而不是因為 wrapper 看起來不夠「enterprise」。
把輸出當成可追溯紀錄,再進行 QA
公開 workflow skill 建議建立精簡 manifest,內容包括目標、node ID、endpoint ID、request ID、輸入 URL、輸出 URL、下載檔案與瑕疵備註。這比留下一整個存放自動命名檔案的資料夾實用得多。
接受整批輸出前,請確認:
- 每一列
complete都有本機檔案與 JSON 收據。 - 沒有任何
failed列被悄悄遺漏。 - 圖片的尺寸符合預期,且檔案大小不為零。
- 影片可以開啟,並具有預期的時長、解析度與影格率;
ffprobe適合用於這項檢查。 - 保留下來的資產仍保有對應的 prompt 與 endpoint ID。
- 重新執行同一份 manifest 時,應略過既有工作,而非產生重複檔案。
fal 指南強調,生成媒體應和它的 JSON 中繼資料放在一起。這個習慣也讓日後遷移供應商成為可能:你可以比較輸出、請求與成本紀錄,不必再從檔名中嘗試還原當初的執行過程。
常見問題
fal genmedia CLI 原生支援數百條 prompt 的批次處理嗎?
文件中的命令涵蓋模型執行、非同步狀態處理、JSON 輸出、上傳與下載。若要建立可續跑的數百條 prompt 批次作業,仍需要 manifest 讀取器與併發控制器。
genmedia 比 Replicate CLI 便宜嗎?
CLI 不會決定供應商的模型價格。請依你的工作負載比較具體 endpoint 的價格、輸出設定、重試次數與傳輸行為;面對不同模型目錄,工具層級的「較便宜」並沒有實際意義。
若想走最短路徑,使用 genmedia 進行探索與執行,加入由 manifest 驅動的 wrapper 處理批次;只有在需要多供應商路由或集中化任務狀態時,再選擇自訂腳本。