Tạo hàng loạt hình ảnh sản phẩm từ bảng tính CSV, có trần chi phí điểm
Một đoạn mã Python đọc bảng tính sản phẩm, tính trước tổng điểm, chạy thử năm dòng, đặt trần chi phí, chạy tiếp được khi đứt giữa chừng và ghi lại kết quả từng dòng.

Một gian hàng gốm trên Shopee có 240 mã sản phẩm, mỗi mã cần thêm hai hình ảnh bối cảnh: chén đĩa trên bàn ăn gia đình, bình hoa trên kệ gỗ. Ngồi gõ 480 lần trên trang web thì không ai làm nổi. Bảng tính sản phẩm thì có sẵn. Bài này biến bảng tính đó thành hình ảnh bằng một đoạn mã Python, với ba thứ mà người chủ gian hàng quan tâm hơn cả: biết trước tốn bao nhiêu điểm, không bao giờ vượt trần, và đứt giữa chừng thì chạy tiếp, không tốn lại.
Bước 1: chuẩn bị bảng tính
Xuất từ Google Sheets hay Excel ra CSV, mã hoá UTF-8. Các cột:
| Cột | Ý nghĩa | Ví dụ |
|---|---|---|
ma | Mã sản phẩm, dùng đặt tên tệp, chỉ chữ không dấu, số, gạch ngang | BT-CHEN-05 |
canh | Câu lệnh vẽ tiếng Anh tả cảnh | xem dưới |
hang | plus, pro hoặc max; để trống là plus | plus |
kho | 3:2, 1:1, 2:3, 16:9, 9:16, 4:5; trống là 1:1 | 1:1 |
so | Số hình ảnh cho dòng này, 1–4; trống là 1 | 2 |
Cột canh phải là tiếng Anh vì API không tự dịch. Cách viết nhanh cho cả trăm dòng: viết một khuôn cho mỗi nhóm hàng, chỉ thay phần đồ vật.
Set of handmade ceramic {đồ vật} on a rustic wooden dining table, Vietnamese home kitchen, morning window light, steam from a bowl of rice, no text, no logo, realistic product photoSet of handmade ceramic rice bowls with blue glaze on a rustic wooden dining table, ...Khuôn tốt thì hình ảnh cả bộ đồng đều về ánh sáng và màu, gian hàng trông chuyên nghiệp. Để tìm khuôn, thử vài mô tả tiếng Việt trên ô vẽ, xem dòng "Câu lệnh vẽ" mà AI dịch ra, giữ lại câu nào cho kết quả ưng nhất.
Bảng mẫu san_pham.csv:
```csv
ma,canh,hang,kho,so
BT-CHEN-05,"Set of handmade ceramic rice bowls with blue glaze on a rustic wooden dining table, Vietnamese home kitchen, morning window light, no text, no logo, realistic product photo",plus,1:1,2
BT-BINH-12,"Tall crackle-glaze ceramic vase with fresh lotus flowers on a dark wooden shelf, soft side light, calm living room, no text, no logo, realistic product photo",pro,4:5,1
```
Bước 2: đoạn mã
```python
#!/usr/bin/env python3
"""Tạo hình ảnh sản phẩm hàng loạt từ CSV qua API AI Hình Ảnh.
python3 ve_csv.py san_pham.csv --tran 150000 # chạy thật
python3 ve_csv.py san_pham.csv --tinh # chỉ tính điểm
python3 ve_csv.py san_pham.csv --thu 5 --tran 10000 # chạy thử 5 dòng
"""
import argparse, base64, csv, os, random, sys, time
from pathlib import Path
import requests
API = "https://aihinhanh.com/v1"
KEY = os.environ.get("AHN_KEY", "") # ahn_XXXX
KHO = {"3:2": "1536x1024", "1:1": "1280x1280", "2:3": "1024x1536",
"16:9": "1664x928", "9:16": "928x1664", "4:5": "1152x1440"}
RA = Path("hinh_anh_ra")
def bang_gia() -> dict:
r = requests.get(f"{API}/models", timeout=30)
r.raise_for_status()
return {m["id"]: m["price_vnd"] for m in r.json()["data"] if "price_vnd" in m}
def doc_csv(tep: str) -> list:
dong = []
with open(tep, encoding="utf-8-sig", newline="") as f:
for i, d in enumerate(csv.DictReader(f), start=2):
ma, canh = (d.get("ma") or "").strip(), (d.get("canh") or "").strip()
if not ma or not canh:
print(f"Dòng {i}: thiếu mã hoặc cảnh, bỏ qua")
continue
dong.append({"ma": ma, "canh": canh,
"hang": (d.get("hang") or "plus").strip().lower(),
"size": KHO.get((d.get("kho") or "1:1").strip(), "1280x1280"),
"so": max(1, min(4, int(d.get("so") or 1)))})
return dong
def con_thieu(d: dict) -> int:
"""Số hình ảnh của dòng chưa có tệp trên đĩa (để chạy tiếp)."""
co = sum(1 for k in range(1, d["so"] + 1) if (RA / f"{d['ma']}-{k}.webp").exists())
return d["so"] - co
def ve(d: dict, n: int) -> dict:
payload = {"model": d["hang"], "prompt": d["canh"], "size": d["size"],
"n": n, "output_format": "webp"}
for lan in range(5):
r = requests.post(f"{API}/images/generations", json=payload, timeout=120,
headers={"Authorization": f"Bearer {KEY}"})
if r.status_code == 200:
return r.json()
if r.status_code in (400, 401, 402):
raise SystemExit(f"{d['ma']}: lỗi {r.status_code} {r.text[:200]}")
cho = int(r.headers.get("Retry-After", "30")) + random.uniform(0, 5)
print(f" {d['ma']}: máy bận ({r.status_code}), chờ {cho:.0f}s")
time.sleep(cho)
return {"data": [], "usage": {"credits": 0}}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("csv")
ap.add_argument("--tran", type=int, default=0, help="trần điểm cho lần chạy này")
ap.add_argument("--tinh", action="store_true", help="chỉ tính điểm, không vẽ")
ap.add_argument("--thu", type=int, default=0, help="chỉ chạy N dòng đầu")
a = ap.parse_args()
gia = bang_gia()
dong = doc_csv(a.csv)
if a.thu:
dong = dong[: a.thu]
for d in dong:
if d["hang"] not in gia:
sys.exit(f"{d['ma']}: hạng '{d['hang']}' không có, chỉ dùng {list(gia)}")
viec = [(d, con_thieu(d)) for d in dong if con_thieu(d) > 0]
tong = sum(gia[d["hang"]] * n for d, n in viec)
so_hinh = sum(n for _, n in viec)
print(f"{len(viec)} dòng, {so_hinh} hình ảnh, ước tính {tong:,} điểm")
if a.tinh:
return
if not KEY or not a.tran:
sys.exit("Cần AHN_KEY và --tran (trần điểm).")
RA.mkdir(exist_ok=True)
da_tieu = 0
with open("ket_qua.csv", "a", encoding="utf-8", newline="") as log:
w = csv.writer(log)
for d, n in viec:
du_kien = gia[d["hang"]] * n
if da_tieu + du_kien > a.tran:
print(f"Dừng trước {d['ma']}: sẽ vượt trần {a.tran:,} điểm")
break
kq = ve(d, n)
co = sum(1 for k in range(1, d["so"] + 1) if (RA / f"{d['ma']}-{k}.webp").exists())
for i, x in enumerate(kq["data"], start=co + 1):
(RA / f"{d['ma']}-{i}.webp").write_bytes(base64.b64decode(x["b64_json"]))
da_tieu += kq["usage"]["credits"]
w.writerow([time.strftime("%Y-%m-%d %H:%M"), d["ma"], d["hang"],
len(kq["data"]), n, kq["usage"]["credits"]])
log.flush()
print(f"{d['ma']}: {len(kq['data'])}/{n} hình ảnh, cộng dồn {da_tieu:,} điểm")
print(f"Xong lần chạy, đã tiêu {da_tieu:,} điểm")
if __name__ == "__main__":
main()
```
Bước 3: chạy theo ba nhịp
Nhịp 1: tính điểm
```bash
python3 ve_csv.py san_pham.csv --tinh
```
Đoạn mã lấy bảng giá hiện hành từ GET /v1/models (không ghi cứng giá trong mã), đếm số hình ảnh còn thiếu và in tổng điểm. Với ví dụ gian hàng gốm:
| Nhóm | Số mã | Hạng | Hình ảnh mỗi mã | Điểm |
|---|---|---|---|---|
| Chén, bát, đĩa | 160 | Plus (500) | 2 | 160.000 |
| Bình hoa, chậu | 60 | Pro (1.000) | 1 | 60.000 |
| Bộ ấm trà có người rót | 20 | Max (1.500) | 1 | 30.000 |
| Cộng | 240 | 400 | 250.000 |
Nhóm "có người rót trà" lên Max vì bàn tay cầm ấm là chỗ hạng Plus hay sai (ngón thừa, ngón dính). Chén bát không có người thì Plus là đủ đẹp và nhanh nhất.
Nhịp 2: chạy thử năm dòng
```bash
export AHN_KEY="ahn_XXXX"
python3 ve_csv.py san_pham.csv --thu 5 --tran 10000
```
Mở thư mục hinh_anh_ra, xem từng hình ảnh thật kỹ: màu men có đúng không, có chữ lạ trên chén không, ánh sáng có đồng đều không. Năm dòng tốn vài nghìn điểm; sửa khuôn câu lệnh ở bước này rẻ hơn rất nhiều so với phát hiện sau khi đã vẽ 400 hình ảnh.
Nhịp 3: chạy thật có trần
```bash
python3 ve_csv.py san_pham.csv --tran 260000
```
Trần đặt hơi cao hơn ước tính một chút. Đoạn mã kiểm trước mỗi dòng: nếu vẽ dòng này mà vượt trần thì dừng. Trần là trần của lần chạy, không phải số dư ví; ví hết điểm thì API trả 402 và đoạn mã cũng dừng.
Chạy tiếp khi đứt giữa chừng
Tên tệp là ma-1.webp, ma-2.webp… Trước khi vẽ, đoạn mã đếm xem mỗi mã còn thiếu bao nhiêu tệp và chỉ gửi đúng số đó. Vì vậy:
- Mất điện ở dòng 137: chạy lại đúng lệnh cũ, 136 dòng đầu được bỏ qua, không tốn thêm điểm.
- Một dòng xin 2 hình ảnh mà máy chỉ kịp trả 1 (phần thiếu đã được hoàn điểm): lần chạy sau tự vẽ bù đúng 1.
- Muốn vẽ lại một mã vì không ưng: xoá tệp của mã đó, chạy lại.
Tệp ket_qua.csv ghi từng dòng: thời điểm, mã, hạng, số hình ảnh nhận, số xin, điểm thật. Cộng cột cuối là ra chi phí thật, đối chiếu được với lịch sử ví.
Những lỗi hay gặp với bảng tính thật
| Triệu chứng | Nguyên nhân | Sửa |
|---|---|---|
| Chữ có dấu thành ký tự lạ | CSV xuất từ Excel bằng mã hoá khác UTF-8 | Xuất lại bằng "CSV UTF-8"; mã đã đọc utf-8-sig để xử lý ký tự BOM |
| Dòng bị bỏ qua "thiếu cảnh" | Ô canh có dấu phẩy mà không bọc nháy kép | Để bảng tính tự xuất, đừng sửa CSV bằng tay trong trình soạn thảo |
| Hình ảnh của cả bộ lệch màu nhau | Mỗi dòng viết câu lệnh kiểu khác | Dùng chung một khuôn câu cho mỗi nhóm hàng |
| Tên tệp lộn xộn, ghi đè nhau | Mã sản phẩm trùng hoặc có dấu cách | Làm sạch cột ma trước khi chạy |
| Chạy rất chậm vào buổi tối | Máy vẽ đang bận việc khác | Chạy qua đêm, hoặc chia đợt |
Ước thời gian
Plus khoảng 6–10 giây mỗi hình ảnh, Pro 20–25 giây, Max 25–40 giây, chưa tính lúc máy bận. Đợt 400 hình ảnh ở ví dụ trên chạy tuần tự mất khoảng hai đến ba giờ khi máy rảnh. Cho chạy từ tối, sáng ra kiểm hình ảnh. Đoạn mã chạy tuần tự có chủ ý; giải thích vì sao không nên chạy song song ở bài xử lý lỗi 503 và hàng đợi.
Sau khi có hình ảnh
Tệp webp ra thẳng từ API đã nhẹ, nhưng mỗi sàn có yêu cầu khổ, dung lượng riêng. Cách đổi cỡ, đặt tên cho dễ tìm và viết thẻ alt có ở bài lưu tệp, đặt tên, nén webp, thẻ alt. Tạo khoá ở trang khoá API.



