fal genmedia CLI는 모델 탐색, 스키마 확인, 비동기 요청 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에서는 키를 스크립트나 명령 로그에 넣지 말고 러너의 시크릿 저장소를 통해 주입하는 편이 안전하다.
배치를 시작하기 전에는 엔드포인트를 고정하고, 현재 적용되는 계약을 직접 확인한다.
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다. 한 영상 엔드포인트에서 통하던 플래그가 다른 엔드포인트에서도 작동하리라 가정하면 안 된다. workflow skill도 검증 오류가 발생했을 때 스키마를 다시 확인하라고 명시한다.
셸 루프 대신 큐로 배치 설계하기
공식 genmedia 자료에는 비동기 실행 방법이 나와 있지만, CSV 500행을 읽어 실행하는 네이티브 명령이나 자동 재시도 정책까지 보장하지는 않는다. CLI는 공급자 특성을 아는 실행기로 두고, 큐 제어는 별도 래퍼가 담당하도록 설계하는 것이 좋다.
재개 가능한 작업 레코드에는 최소한 다음 필드가 필요하다.
| 필드 | 필요한 이유 |
|---|---|
id | 재개와 중복 제거에 쓰는 안정적인 입력 식별자 |
endpoint | 사용한 정확한 모델 경로 |
prompt | 재현성과 감사 추적 |
status | pending, submitted, complete, failed, 또는 skipped |
request_id | genmedia status에 필요한 값 |
attempts | 통제되지 않는 재시도 방지 |
output | 결정론적인 로컬 저장 경로 |
error | 실패한 행을 후속 조치 가능한 상태로 만듦 |
시간이 오래 걸리는 image-to-video 또는 영상 작업에는 --async를 사용한다. 반환된 request_id는 즉시 저장하고, 엔드포인트 ID와 요청 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 스키마 검증 오류, 안전성 거부, 잘못된 JSON은 재시도하지 않는다.
- 제출 요청이 공급자에게 도달했을 가능성이 있는 상태에서 타임아웃됐다면, 두 번째 유료 요청을 만들기 전에 저장된 요청 레코드를 먼저 확인한다.
이 정책은 래퍼가 담당해야 한다. 공개 genmedia README는 명령과 라이프사이클 작업을 설명하지만, 자동 재시도를 보장한다고 말하지는 않는다. 422가 발생하면 validation_errors를 확인하고 genmedia schema를 다시 실행한 뒤, 무작정 재제출하지 말고 지목된 필드를 수정한다.
복사해서 시작할 수 있는 Python 배치 래퍼
아래 래퍼는 동기식 이미지 배치를 위한 출발점이다. 인수 목록과 함께 subprocess.run을 사용하고, 완료된 출력 경로는 건너뛰며, 동시 작업 수를 제한하고, 일시적 프로세스 실패를 재시도한 뒤, 원자적으로 매니페스트를 쓴다. 오래 걸리는 영상 작업은 폴링 전에 비동기 제출 정보를 저장해야 한다. 순서는 submit -> write endpoint/request_id -> restart -> poll saved request_id -> download이며, 요청 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를 바꾸고 해당 엔드포인트 스키마에 맞는 플래그를 추가한다. image-to-video 체인이라면 로컬 이미지를 genmedia upload ./frame.png --json로 한 번 업로드하고, 반환된 URL을 영상 작업에 전달한 뒤 두 레코드를 모두 매니페스트에 남긴다. 이 래퍼가 genmedia 자체의 지출 추정이나 최대 지출 제한 기능을 제공한다고 주장하는 것은 아니다. 중복된 로컬 작업을 막고 재시도 횟수를 제한할 뿐이다.
유료 생성을 시작하기 전 비용 게이트 두기
genmedia pricing <endpoint_id> --json은 가격 조회 명령일 뿐, 예약이나 예산 상한 기능은 아니다. 배치 실행 전에 가격을 확인하고, 행 수, 행당 출력 수, 해상도·길이 설정, 최대 재시도 횟수를 기준으로 보수적인 상한을 계산해야 한다.
현실적으로 적용하기 좋은 게이트는 다음과 같다.
| 통제 항목 | 구현 방법 |
|---|---|
| 행 수 하드 캡 | 매니페스트가 승인된 건수를 넘으면 시작을 거부한다 |
| 모델 티어 | 저렴하거나 빠른 엔드포인트로 초안을 만들고, QA를 마친 결과만 최종 렌더링한다 |
| 출력 수 제한 | 기본값에 맡기지 말고 num_images를 명시한다 |
| 재시도 예산 | 최초 시도와 재시도를 분리해 집계한다 |
| 재개 | 검증된 로컬 출력이 있는 행은 건너뛴다 |
| 취소 | 적절한 경우 대기 중인 작업에 genmedia status ... --cancel을 사용한다 |
모델 가격과 엔드포인트 제공 여부는 바뀔 수 있으므로 가격 조회 응답도 매니페스트와 함께 기록한다. 공급자가 비교 가능한 단위를 제공하지 않는다면, 이를 청구 금액이 아니라 요청 수 상한으로 표기해야 한다.
fal genmedia CLI, Replicate CLI, 커스텀 스크립트의 선택 기준
세 도구는 해결하는 계층이 다르다. Replicate 공식 CLI는 예측 실행과 스트리밍, 모델 스키마 확인, 업로드, 학습, 모델 관리를 위한 명령을 제공한다. 반면 작업의 시작점이 fal 엔드포인트 탐색이고, fal의 큐 및 다운로드 라이프사이클을 다뤄야 한다면 genmedia가 더 적합하다.
| 선택지 | 잘 맞는 용도 | 주요 트레이드오프 |
|---|---|---|
| fal genmedia CLI | fal 중심의 모델 검색, 스키마 조회, 비동기 작업, 다운로드, 에이전트 셸 사용 | 공급자 종속적이며, 배치 정책은 여전히 CLI 밖에서 관리해야 함 |
| Replicate CLI | Replicate 예측, 스트리밍, 모델·스키마 작업, 학습 명령 | 카탈로그와 라이프사이클이 다른 공급자임. fal 엔드포인트 ID나 플래그가 그대로 통할 것으로 기대하면 안 됨 |
| 커스텀 Python/HTTP 스크립트 | 멀티 공급자 라우팅, 승인 게이트, 데이터베이스 상태, 큐, 과금 정책 | 인증, 스키마 변경, 폴링, 다운로드, 오류 처리를 직접 책임져야 함 |
추천은 단순하다. 탐색에는 genmedia를 직접 쓰고, fal만 사용하는 프로덕션 배치에는 작은 Python 래퍼를 더하면 된다. 공급자 전환이 실제 요구사항일 때만 커스텀 공급자 추상화로 옮기자. 래퍼가 더 “엔터프라이즈”처럼 보인다는 이유만으로 갈 필요는 없다.
출력물을 기록으로 남기고 QA까지 마무리하기
공개 workflow skill은 목표, 노드 ID, 엔드포인트 ID, 요청 ID, 입력 URL, 출력 URL, 내려받은 파일, 결함 메모를 담은 간결한 매니페스트를 권한다. 생성된 이름의 파일만 가득한 폴더보다 훨씬 유용하다.
배치를 승인하기 전에 다음을 확인한다.
- 모든
complete행에 로컬 파일과 JSON 영수증이 있다. failed행이 아무 흔적 없이 누락되지 않았다.- 이미지의 크기가 예상과 일치하며 파일 크기가 0이 아니다.
- 영상이 정상적으로 열리고 예상한 길이, 해상도, 프레임 레이트를 갖는다. 이 확인에는
ffprobe가 적합하다. - 보관할 에셋에는 프롬프트와 엔드포인트 ID가 유지돼 있다.
- 같은 매니페스트를 다시 실행했을 때 중복 파일이 생성되는 대신 작업이 건너뛰어진다.
fal 가이드도 생성 미디어를 JSON 메타데이터 가까이에 보관할 것을 강조한다. 이 습관은 나중에 공급자를 옮길 때도 도움이 된다. 파일명만 보고 실행 이력을 복원하려 애쓰는 대신 출력, 요청, 비용 기록을 비교할 수 있기 때문이다.
FAQ
fal genmedia CLI만으로 수백 개 프롬프트를 네이티브 배치 처리할 수 있나?
문서화된 명령은 모델 실행, 비동기 상태 처리, JSON 출력, 업로드, 다운로드를 제공한다. 하지만 수백 개 프롬프트를 재개 가능한 배치로 처리하려면 매니페스트 리더와 동시성 제어기가 여전히 필요하다.
genmedia가 Replicate CLI보다 저렴한가?
CLI가 공급자의 모델 가격을 결정하지는 않는다. 실제 워크로드를 기준으로 특정 엔드포인트 가격, 출력 설정, 재시도 횟수, 전송 방식을 비교해야 한다. 모델 카탈로그가 다른 상황에서 도구 수준으로 어느 쪽이 더 저렴하다고 단정하는 것은 의미가 없다.
가장 짧은 경로를 택한다면, 탐색과 실행에는 genmedia를 사용하고 배치에는 매니페스트 기반 래퍼를 추가하자. 멀티 공급자 라우팅이나 중앙화된 작업 상태가 필요할 때만 커스텀 스크립트를 선택하면 된다.