Show / hide languages:

🍎 Mac: Working with API Keys

Five real services your Mac can talk to with one HTTP request. Free APIs first (NVIDIA, Gemini, LINE), then paid ones (Replicate, HeyGen). Each section: where to get the key, how to store it, curl example, Python example.

บริการจริง 5 ตัวที่ Mac ของคุณคุยด้วยได้ด้วย HTTP request เดียว เริ่มจาก API ฟรี (NVIDIA, Gemini, LINE) แล้วค่อยถึงตัวเสียเงิน (Replicate, HeyGen) แต่ละหัวข้อ: รับ key จากที่ไหน เก็บอย่างไร ตัวอย่าง curl ตัวอย่าง Python

五个真实可用的服务,你的 Mac 用一个 HTTP 请求就能聊。先讲免费 API(NVIDIA、Gemini、LINE),再讲付费的(Replicate、HeyGen)。每节都讲:去哪里拿 key、怎么存、curl 例子、Python 例子。

NVIDIA NIM Google Gemini LINE Replicate HeyGen curl + Python

🧭 What an API key is

🇬🇧 English

An API is a service you call over the internet. You send an HTTP request; the server sends a response. An API key is a long random string that proves you're allowed to call it. The service uses the key to know who you are, count your usage, and bill you. Treat it like a password: never paste it into a public chat, never commit it to git, never email it.

🇹🇭 ไทย

API คือบริการที่เรียกผ่านอินเทอร์เน็ต คุณส่ง HTTP request เซิร์ฟเวอร์ส่ง response กลับ API key คือสตริงสุ่มยาว ๆ ที่ใช้ พิสูจน์ว่าคุณมีสิทธิ์เรียก บริการนั้น เซิร์ฟเวอร์ใช้ key เพื่อระบุตัวคุณ นับการใช้งาน และคิดเงิน ปฏิบัติต่อ key เหมือนรหัสผ่าน อย่าวางในแชทสาธารณะ อย่า commit เข้า git อย่าส่งทางอีเมล

🇨🇳 中文

API 是一个你通过互联网调用的服务。你发一个 HTTP 请求,服务器回一个响应。API key 是一长串随机字符串,用来 证明你有权限 调用这个服务。服务器用它识别你、统计用量、计费。把它当密码:不要贴到公开聊天里、不要 commit 到 git、不要用邮件发。

💡 The five APIs in this guide

All five are real services we use to build the krueng.ai pipeline. NVIDIA NIM hosts Llama and Nemotron — free $5 of credits, no card. Google Gemini is multimodal (text + image input) — generous free tier. LINE sends messages to Thai students — 500 free push/month. Replicate runs FLUX, SDXL, Seedance — small free credit. HeyGen generates talking-head videos — paid, but the cheapest avatar API.

ทั้งห้าตัวเป็นบริการจริงที่ใช้สร้าง pipeline ของ krueng.ai NVIDIA NIM ให้บริการ Llama และ Nemotron — เครดิตฟรี $5 ไม่ต้องบัตร Google Gemini รับทั้ง text และรูป — free tier ใจป้ำ LINE ส่งข้อความให้นักเรียนไทย — ฟรี 500 push/เดือน Replicate รัน FLUX, SDXL, Seedance — เครดิตฟรีน้อยหน่อย HeyGen สร้างวิดีโอ avatar — เสียเงิน แต่ถูกที่สุดในกลุ่ม avatar API

这五个都是我们搭 krueng.ai 流水线时真在用的服务。NVIDIA NIM 托管 Llama 和 Nemotron — 免费 $5 credit、不要信用卡。Google Gemini 多模态(文字 + 图)— 慷慨的免费档。LINE 给泰国学生发消息 — 每月免费 500 push。Replicate 跑 FLUX、SDXL、Seedance — 免费 credit 少一点。HeyGen 生成数字人视频 — 收费,但是同类最便宜。

🔐 The Big Idea — never hardcode the key

Every example below reads the key from an environment variable. Three reasons: (1) you can share the code without redacting; (2) git can't accidentally commit something you never typed into a file; (3) the same code works on your Mac, a teammate's Mac, and a cloud server — only the env var changes.

ทุกตัวอย่างข้างล่างอ่าน key จาก environment variable เหตุผลสามข้อ: (1) แชร์โค้ดได้โดยไม่ต้องลบ key ก่อน (2) git accidentally commit ไม่ได้ในเมื่อ key ไม่ได้พิมพ์อยู่ในไฟล์ (3) โค้ดเดียวกันรันได้บน Mac คุณ, Mac เพื่อน, และเซิร์ฟเวอร์คลาวด์ — เปลี่ยนแค่ค่า env var

下面每个例子都从 环境变量 里读 key。三个原因:(1) 可以直接分享代码不用先删 key;(2) git 不可能不小心 commit 你从来没写进文件的东西;(3) 同一份代码在你 Mac、同事 Mac、云服务器上都能跑 — 只换环境变量。

1 Use a .env file (per-project)

Make a file called .env in your project folder. One KEY=value per line. Add it to .gitignore immediately.

สร้างไฟล์ .env ในโฟลเดอร์โปรเจกต์ บรรทัดละ KEY=value เพิ่ม .gitignore ทันที

在项目文件夹里建一个 .env。每行一个 KEY=value立刻加进 .gitignore

# .env ← never commit this! NVIDIA_API_KEY=nvapi-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx GOOGLE_GEMINI_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXX LINE_ACCESS_TOKEN=long_token_from_developers.line.biz REPLICATE_API_TOKEN=r8_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx HEYGEN_KEY=Y2FlOGZmM...long_base64_string

Then make sure .gitignore contains:

แล้วทำให้แน่ใจว่า .gitignore มีบรรทัด:

然后确认 .gitignore 里有:

.env .env.*

2 Load it in your shell or Python

For one-off curl commands, source the file. For Python, use python-dotenv or just os.environ if you already exported.

สำหรับ curl ครั้งเดียว ใช้ source ไฟล์ สำหรับ Python ใช้ python-dotenv หรือ os.environ ถ้า export แล้ว

一次性的 curl 命令,source 一下文件。Python 用 python-dotenv,或者已经 export 过就用 os.environ

$ set -a; source .env; set +a # export every line of .env into the shell $ echo "$NVIDIA_API_KEY" # confirm it loaded (don't paste the output anywhere!)

🚨 What if I already committed a key?

Rotate it immediately on the provider's dashboard, even if the repo is private. Once a key has been pushed to GitHub it must be considered leaked — bots scrape public commits within minutes, and "I unpushed it" doesn't unring the bell. Generate a new one, update your .env, and remove the old commit only as a courtesy (the key is already gone).

หมุน key ใหม่ทันที ในแดชบอร์ดของผู้ให้บริการ แม้ repo จะเป็น private ก็ตาม เมื่อ key ถูก push ขึ้น GitHub แล้ว ให้ถือว่ารั่ว — bot จะ scrape commits สาธารณะภายในไม่กี่นาที "ฉัน unpush แล้ว" ก็เอาคืนไม่ได้ สร้าง key ใหม่ แก้ .env และลบ commit เก่าเป็นมารยาทเฉย ๆ (key นั้นรั่วไปแล้ว)

立刻去厂商后台轮换,哪怕 repo 是 private 也照样。一旦 key 被 push 到 GitHub,就当它已经泄露 — 几分钟内就会有 bot 爬公开 commit,"我已经 unpush 了" 也救不回来。重新生成一个、更新 .env,旧的 commit 删不删都行(那个 key 已经废了)。

Before you start

macOS already has curl and Python 3. Two small extras make life nicer: httpie (a friendlier curl) and the Python requests library.

macOS มี curl และ Python 3 อยู่แล้ว มีอีกสองตัวที่ช่วยให้ชีวิตสบายขึ้น: httpie (curl ที่อ่านง่ายกว่า) และ library requests ของ Python

macOS 自带 curl 和 Python 3。再装两个小工具会更舒服:httpie(更友好的 curl)和 Python 的 requests 库。

$ brew install httpie # optional but easier to read $ python3 -m venv .venv && source .venv/bin/activate $ pip install requests python-dotenv # the only two Python deps in this whole guide

🟢 Tech 1 — NVIDIA NIM (free $5 credit)

🟢 NVIDIA Build / NIM free $5 credit · no card

NVIDIA hosts a lot of popular open models — Llama 3.1, Mixtral, Nemotron — on its own infrastructure and exposes them through an OpenAI-compatible chat completions endpoint. You sign in with email, get $5 of credits, never enter a card. Perfect for hello-world.

NVIDIA host โมเดล open source ยอดนิยมไว้เยอะ — Llama 3.1, Mixtral, Nemotron — บนเซิร์ฟเวอร์ตัวเอง และเปิดให้เรียกผ่าน endpoint แบบเดียวกับ OpenAI chat completions ลงทะเบียนด้วยอีเมล รับเครดิต $5 ไม่ต้องกรอกบัตร เหมาะกับการลองครั้งแรก

NVIDIA 在自家服务器上托管了不少流行的开源模型 — Llama 3.1、Mixtral、Nemotron — 用一个跟 OpenAI 兼容的 chat completions 接口对外暴露。邮箱注册、送 $5 credit、不用填卡。最适合 hello-world。

your Mac curl / python Bearer key integrate.api.nvidia.com /v1/chat/completions model picker Llama 3.1 on H100s OpenAI-style request, runs on NVIDIA's GPUs
Same request shape as OpenAI's API — only the URL and the model name change.

Get the key

  1. Go to build.nvidia.com and sign in.
  2. เข้า build.nvidia.com แล้ว sign in
  3. 打开 build.nvidia.com 登录。
  4. Pick any model card (e.g. Llama 3.1 70B), click "Get API Key".
  5. Copy the key — starts with nvapi-. Paste it into your .env as NVIDIA_API_KEY.

curl example

$ curl https://integrate.api.nvidia.com/v1/chat/completions \ -H "Authorization: Bearer $NVIDIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "meta/llama-3.1-70b-instruct", "messages": [{"role": "user", "content": "Say hello in Thai."}], "max_tokens": 64 }' { "choices": [{ "message": { "role": "assistant", "content": "สวัสดีครับ! / สวัสดีค่ะ!" }, ... }], "usage": {"prompt_tokens": 14, "completion_tokens": 12} }

Python example

# nvidia_chat.py import os, requests from dotenv import load_dotenv load_dotenv() def chat(prompt: str) -> str: r = requests.post( "https://integrate.api.nvidia.com/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ['NVIDIA_API_KEY']}"}, json={ "model": "meta/llama-3.1-70b-instruct", "messages": [{"role": "user", "content": prompt}], "max_tokens": 256, }, timeout=60, ) r.raise_for_status() return r.json()["choices"][0]["message"]["content"] if __name__ == "__main__": print(chat("Write a haiku about Doi Suthep at dawn."))
  • load_dotenv() reads your .env into os.environ. Without this line, os.environ['NVIDIA_API_KEY'] will raise KeyError unless you already exported the var in your shell.
  • Header auth — Bearer <key>. Most OpenAI-compatible APIs use this exact pattern. X-Api-Key is the other common shape (used by HeyGen below); a few APIs (Gemini) put the key in the URL.
  • raise_for_status() turns HTTP 4xx/5xx into a Python exception. Without it, a 401 (bad key) silently returns an error JSON and your r.json()['choices'] blows up with a confusing KeyError. Always include this line.

💡 Model picker

The model field is the only thing you change to swap models. Free models at NVIDIA today include meta/llama-3.1-70b-instruct, mistralai/mixtral-8x22b-instruct-v0.1, nvidia/nemotron-3-super-120b-a12b. All take the same OpenAI-style messages list. The full catalog lives on the dashboard.

field model เป็นสิ่งเดียวที่เปลี่ยนเมื่อจะสลับโมเดล โมเดลฟรีบน NVIDIA ตอนนี้มี meta/llama-3.1-70b-instruct, mistralai/mixtral-8x22b-instruct-v0.1, nvidia/nemotron-3-super-120b-a12b ทุกตัวรับ messages list แบบ OpenAI เหมือนกัน รายการเต็มดูได้ในแดชบอร์ด

model 字段是切换模型时唯一要改的。NVIDIA 上现在的免费模型有 meta/llama-3.1-70b-instructmistralai/mixtral-8x22b-instruct-v0.1nvidia/nemotron-3-super-120b-a12b。全都接受 OpenAI 风格的 messages 列表。完整目录看后台。

Tech 2 — Google Gemini (generous free tier)

Gemini API free · 15 requests / minute

Google's Gemini is multimodal — it accepts text and images in the same request and returns text. The free tier is the most generous of any frontier-grade model: 15 requests/minute, 1500/day for gemini-2.0-flash. The catch: the request format is not OpenAI-compatible. Messages become contents; system prompts become systemInstruction; the key goes in the query string.

Gemini ของ Google เป็น multimodal — รับทั้ง text และ image ใน request เดียว แล้วตอบ text กลับ free tier ใจป้ำที่สุดในกลุ่มโมเดลระดับท็อป: 15 requests/นาที, 1500/วัน สำหรับ gemini-2.0-flash ข้อแลก: รูปแบบ request ไม่ compatible กับ OpenAI messages กลายเป็น contents system prompt กลายเป็น systemInstruction และ key ใส่ใน query string

Google 的 Gemini 是多模态的 — 一次请求里可以同时给文字和图,返回文字。免费档是同级模型里最慷慨的:gemini-2.0-flash 每分钟 15 次、每天 1500 次。代价:请求格式跟 OpenAI 兼容。messages 变成 contents,system prompt 变成 systemInstruction,key 放在 query string 里。

Get the key

  1. Go to aistudio.google.com/app/apikey and sign in with a Google account.
  2. Click "Create API key" → pick or create a project → copy the key.
  3. Paste it into .env as GOOGLE_GEMINI_API_KEY. Keys start with AIzaSy…

curl example

$ curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=$GOOGLE_GEMINI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contents": [{"role":"user","parts":[{"text":"Translate to Lanna Thai: I love rice noodles."}]}], "generationConfig": {"temperature": 0.7, "maxOutputTokens": 200} }'

Python example

# gemini_chat.py import os, requests from dotenv import load_dotenv load_dotenv() MODEL = "gemini-2.0-flash" URL = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent" def chat(prompt: str, system: str = None) -> str: payload = { "contents": [{"role": "user", "parts": [{"text": prompt}]}], "generationConfig": {"temperature": 0.7, "maxOutputTokens": 512}, } if system: payload["systemInstruction"] = {"parts": [{"text": system}]} r = requests.post( URL, params={"key": os.environ["GOOGLE_GEMINI_API_KEY"]}, json=payload, timeout=60, ) r.raise_for_status() return r.json()["candidates"][0]["content"]["parts"][0]["text"] if __name__ == "__main__": print(chat("Suggest a name for a Thai coffee shop in Chiang Mai.", system="You are a creative Thai-English bilingual copywriter."))
  • Gemini's contents, not messages. Each entry has a role (user or model) and a parts array of text/image chunks. To send an image, add another part: {"inlineData": {"mimeType": "image/png", "data": "<base64>"}}.
  • System prompt is its own field. Unlike OpenAI, Gemini doesn't take {"role":"system"} in the contents array. The system instruction is a top-level systemInstruction object. Easy to miss.
  • Key in the query string. Gemini reads the key from ?key=..., not the Authorization header. requests handles URL-encoding for you via the params dict. (Header auth via x-goog-api-key also works but is less common in examples.)
  • Response shape is nested. The text lives at candidates[0].content.parts[0].text. If you ask for n > 1 candidates, walk the whole array. safetyRatings and finishReason sit alongside if the model blocks output.

💬 Tech 3 — LINE Messaging API (free push, free webhook)

💬 LINE Messaging API free · 500 push / month

If your students use LINE — and in Thailand, they all do — this is how you broadcast a message from your Mac. Push sends to a single user you already know; broadcast sends to every follower of your Official Account. Both use the same key (the channel access token) and the same Bearer auth pattern as NVIDIA.

ถ้านักเรียนใช้ LINE — ในไทยใช้กันทุกคน — นี่คือวิธี broadcast ข้อความจาก Mac คุณ Push ส่งหา user คนเดียวที่รู้จักอยู่แล้ว Broadcast ส่งหา follower ทุกคนของ Official Account ทั้งสองใช้ key เดียวกัน (channel access token) และใช้ Bearer auth แบบเดียวกับ NVIDIA

如果你的学生在用 LINE — 泰国基本都在用 — 这就是从 Mac 上发消息的方式。Push 发给某个你已经知道的 user。Broadcast 发给 Official Account 的全部 follower。两者用同一个 key(channel access token),跟 NVIDIA 一样用 Bearer auth。

Get the key

  1. Sign in at developers.line.biz/console with a LINE account.
  2. Create a Provider, then a Messaging API channel under it.
  3. In the channel → Messaging API tab → scroll to Channel access token → click Issue. Copy as LINE_ACCESS_TOKEN.
  4. In Basic settings tab → Channel secret (only needed if you're verifying inbound webhook signatures). Copy as LINE_SECRET.

curl — broadcast a text

$ curl https://api.line.me/v2/bot/message/broadcast \ -H "Authorization: Bearer $LINE_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"messages":[{"type":"text","text":"สวัสดีตอนเช้า ☀️ Word of the day: resilient."}]}'

Python — push to one user

# line_push.py import os, requests from dotenv import load_dotenv load_dotenv() def push(user_id: str, text: str): return requests.post( "https://api.line.me/v2/bot/message/push", headers={"Authorization": f"Bearer {os.environ['LINE_ACCESS_TOKEN']}"}, json={"to": user_id, "messages": [{"type": "text", "text": text}]}, timeout=15, ).json() # a richer message — text + image card def push_image(user_id: str, caption: str, image_url: str): payload = { "to": user_id, "messages": [ {"type": "text", "text": caption}, {"type": "image", "originalContentUrl": image_url, "previewImageUrl": image_url}, ], } return requests.post( "https://api.line.me/v2/bot/message/push", headers={"Authorization": f"Bearer {os.environ['LINE_ACCESS_TOKEN']}"}, json=payload, timeout=15, ).json()
  • Three endpoints, one auth. /push needs the recipient's userId; /broadcast sends to all followers (no to); /reply is used inside a webhook handler with a replyToken the server gave you. Pick the one that fits the trigger.
  • The messages array can hold up to 5 messages. A push counts as one billable message per array, not per element — so combine related parts (caption + image) into one push when you can.
  • Image URLs must be HTTPS and publicly reachable. LINE doesn't proxy your local file — host it on S3, GitHub Pages, or any HTTPS bucket. previewImageUrl is the thumbnail; can be the same URL or a smaller version.

💡 How to get a userId

The recipient has to add your Official Account as a friend first. Then, when they send you any message, your webhook receives an event with their source.userId — that's the one you save to push to them later. For a one-off broadcast to everyone who's already a friend, skip push entirely and use /broadcast.

ผู้รับต้องเพิ่ม Official Account ของคุณเป็นเพื่อนก่อน เมื่อเขาส่งข้อความมา webhook ของคุณจะได้ event ที่มี source.userId — เก็บค่านี้ไว้ส่ง push หาเขาทีหลัง ถ้าจะ broadcast ครั้งเดียวให้ทุกคนที่เป็นเพื่อนอยู่แล้ว ข้าม push ไปเลย ใช้ /broadcast

收件人得先把你的 Official Account 加为好友。然后他给你发任何消息,你的 webhook 会收到一个带 source.userId 的事件 — 把这个 id 存下来,以后就能 push 给他。要一次性发给所有已加好友的人,跳过 push,直接用 /broadcast

🎨 Tech 4 — Replicate (image & video models)

🎨 Replicate

Replicate hosts thousands of open ML models behind one consistent REST API. Image generation (FLUX, SDXL), video (Seedance, Veo), audio, speech-to-text — all in the same shape: POST /predictions, poll until status: "succeeded", then read the output URLs. New accounts get a small free credit; after that it's pay-per-call but tiny: a FLUX image is ~$0.003, a 5-second Seedance clip is ~$0.30.

Replicate host โมเดล ML open source หลายพันตัวไว้หลัง REST API เดียวกัน สร้างรูป (FLUX, SDXL) วิดีโอ (Seedance, Veo) เสียง speech-to-text — รูปแบบเดียวกันหมด: POST /predictions แล้ว poll จนกว่า status จะเป็น "succeeded" ค่อยอ่าน output URL บัญชีใหม่ได้เครดิตฟรีนิดหน่อย หลังจากนั้น pay-per-call แต่ถูก: FLUX รูปนึง ~$0.003 คลิป Seedance 5 วินาที ~$0.30

Replicate 用一个统一的 REST API 托管了上千个开源 ML 模型。图像生成(FLUX、SDXL)、视频(Seedance、Veo)、音频、语音转文字 — 全是同一套:POST /predictions,轮询到 status: "succeeded",再读 output URL。新账号送一点免费 credit,之后按次付费,但很便宜:FLUX 一张图 ~$0.003,Seedance 5 秒视频 ~$0.30。

Get the key

  1. Sign in at replicate.com with GitHub.
  2. Go to replicate.com/account/api-tokens, create a token. Starts with r8_.
  3. Save as REPLICATE_API_TOKEN.

curl — generate one FLUX image

$ curl https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions \ -H "Authorization: Bearer $REPLICATE_API_TOKEN" \ -H "Content-Type: application/json" \ -H "Prefer: wait" \ -d '{"input": {"prompt": "a Lanna temple at sunset, watercolor", "aspect_ratio": "16:9"}}' { "id": "abcdef123…", "status": "succeeded", "output": ["https://replicate.delivery/.../out-0.webp"] }

Python example

# replicate_image.py — submit, poll, download import os, time, requests from dotenv import load_dotenv load_dotenv() HEADERS = { "Authorization": f"Bearer {os.environ['REPLICATE_API_TOKEN']}", "Content-Type": "application/json", } def generate(prompt: str) -> str: # 1. submit r = requests.post( "https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions", headers=HEADERS, json={"input": {"prompt": prompt, "aspect_ratio": "16:9"}}, timeout=30, ) r.raise_for_status() job = r.json() poll_url = job["urls"]["get"] # 2. poll until done while job["status"] in ("starting", "processing"): time.sleep(2) job = requests.get(poll_url, headers=HEADERS, timeout=15).json() if job["status"] != "succeeded": raise RuntimeError(f"job failed: {job.get('error')}") # 3. download the result image_url = job["output"][0] with requests.get(image_url, timeout=60) as img: open("out.webp", "wb").write(img.content) return "out.webp" if __name__ == "__main__": print(generate("Krasue ghost floating over a rice paddy, ink wash"))
  • Inputs are model-specific. Each model card on replicate.com lists its input schema. FLUX takes prompt + aspect_ratio. SDXL takes prompt + width + height. Seedance video takes prompt + image + duration. The wrapper is the same; the keys inside input differ.
  • Polling URL comes back in the response. Don't construct it from the id — Replicate returns the exact URL to poll at urls.get. This lets them route to whichever region is running your job.
  • Four statuses. startingprocessingsucceeded / failed / canceled. Sleep 2–3 seconds between polls. For models that finish in <60s, the Prefer: wait header in the curl example skips the polling entirely and waits inline — try that first.
  • output is sometimes a list, sometimes a string. FLUX returns a list; some others return a single URL. Read the model card. Output URLs expire after 1 hour — if you want to keep the result, download it like step 3 does, don't just save the URL.

🎬 Tech 5 — HeyGen (paid avatar video)

🎬 HeyGen Video Generation

HeyGen turns a script + a chosen avatar + a chosen voice into an MP4 of a talking person. It's the most expensive API in this guide, but for TEFL videos it's currently the cheapest "looks human" option — every other route runs through stitching tools or much more expensive models. The free tier gives one watermarked test video; after that you need a paid plan and the API is enabled separately on the dashboard.

HeyGen เอา script + avatar + เสียง → ออกมาเป็น MP4 ของคนพูด เป็น API แพงที่สุดในคู่มือนี้ แต่สำหรับวิดีโอ TEFL นี่คือตัวที่ "ดูเป็นคน" ที่ถูกที่สุดในตลาดตอนนี้ ทางอื่นต้องประกอบเอง หรือใช้โมเดลที่แพงกว่ามาก free tier ให้ทดสอบได้วิดีโอนึง (มี watermark) จากนั้นต้องสมัครแบบเสียเงิน และต้องเปิด API ในแดชบอร์ดอีกที

HeyGen 把脚本 + 选定的虚拟人 + 选定的声音变成一段会说话的人的 MP4。这是本指南里最贵的 API,但 TEFL 视频场景里目前是 "看起来像真人" 的最便宜方案 — 其他路线要么得拼接、要么模型贵得多。免费档给一个带水印的测试视频,之后要付费套餐,而且 API 要在后台单独开启。

Get the key

  1. Sign up at app.heygen.com, pick the Creator plan (cheapest with API).
  2. Settings → API → create a key. Save as HEYGEN_KEY.
  3. In Settings → Avatars, copy an avatar_id you want to use. Settings → Voices, copy a voice_id.

Python — submit, poll, download

# heygen_video.py import os, time, requests from dotenv import load_dotenv load_dotenv() HEADERS = {"X-Api-Key": os.environ["HEYGEN_KEY"], "Content-Type": "application/json"} def submit(script: str, avatar_id: str, voice_id: str) -> str: payload = { "video_inputs": [{ "character": {"type": "avatar", "avatar_id": avatar_id}, "voice": {"type": "text", "input_text": script, "voice_id": voice_id}, }], "dimension": {"width": 1920, "height": 1080}, } r = requests.post("https://api.heygen.com/v2/video/generate", headers=HEADERS, json=payload, timeout=30) r.raise_for_status() return r.json()["data"]["video_id"] def wait_for(video_id: str) -> str: while True: time.sleep(10) r = requests.get(f"https://api.heygen.com/v1/video_status.get?video_id={video_id}", headers=HEADERS, timeout=15).json() status = r["data"]["status"] if status == "completed": return r["data"]["video_url"] if status == "failed": raise RuntimeError(r["data"].get("error", "unknown")) if __name__ == "__main__": vid = submit( script="Today's word is resilient — strong enough to bounce back.", avatar_id="June_sitting_office_front", voice_id="gl4Yi7XihIvUsls3wXMs", ) print("submitted", vid) print("video URL:", wait_for(vid))
  • X-Api-Key header — not Bearer. HeyGen uses its own header name. Easy way to lose 20 minutes if you copy-paste from an NVIDIA example.
  • Returns a video_id, not the video. Renders take 5–15 minutes. You poll /v1/video_status.get until completed. Don't redirect or open a browser tab to the response — there's nothing to see yet.
  • Statuses: pendingprocessingcompleted / failed. For long-running pipelines store the video_id immediately and poll in a separate process — don't keep a Python script hanging on the network for 15 minutes.

⚠️ Two gotchas this Mac page paid for in production

(a) The Thai Edu-Friend voice gl4Yi7XihIvUsls3wXMs is one of only three HeyGen voices that respect <break time="0.6s"/> SSML tags — the others silently drop them, mangling pacing on bilingual scripts. (b) GET /v2/user/remaining_quota reports seconds of video left, not a render count. A 60-second video costs 60 units regardless of complexity. Check ≥ 150 seconds remaining before submitting a long render.

(a) เสียง Thai Edu-Friend gl4Yi7XihIvUsls3wXMs เป็นหนึ่งในสามเสียง HeyGen ที่เคารพ tag SSML <break time="0.6s"/> เสียงอื่นจะข้าม tag เงียบ ๆ ทำให้จังหวะของสคริปต์ไทย-อังกฤษพัง (b) GET /v2/user/remaining_quota รายงาน วินาทีของวิดีโอที่เหลือ ไม่ใช่จำนวนวิดีโอ วิดีโอ 60 วินาทีจ่าย 60 หน่วยไม่ว่าจะซับซ้อนแค่ไหน ก่อนส่ง render ยาว ๆ ตรวจให้แน่ใจว่าเหลืออย่างน้อย 150 วินาที

(a) Thai Edu-Friend 这个声音 gl4Yi7XihIvUsls3wXMs 是 HeyGen 仅有的三个支持 <break time="0.6s"/> SSML 标签的声音之一 — 其他声音会默默吃掉这个标签,把双语脚本的节奏搞乱。(b) GET /v2/user/remaining_quota 报告的是 剩余视频秒数,不是渲染次数。60 秒视频不管多复杂都扣 60 单位。提交长视频前先确认剩余 ≥ 150 秒。

🏗️ Going to production — AWS Parameter Store

For your own Mac, .env is perfect. Once you deploy the same code to a Lambda or an EC2 box, you don't want .env files riding along in your container image — and you definitely don't want to bake keys into the AMI. The standard pattern is AWS Systems Manager Parameter Store: store each key as a SecureString parameter, give the runtime role permission to read it, fetch it on first use, cache it for the process lifetime. The Lambdas behind krueng.ai all use this pattern.

บนเครื่อง Mac คุณ .env เพียงพอแล้ว แต่พอ deploy โค้ดเดียวกันไป Lambda หรือ EC2 คุณไม่อยากให้ .env ติดไปกับ container image แน่ ๆ และไม่อยาก bake key เข้า AMI แน่นอน รูปแบบมาตรฐานคือ AWS Systems Manager Parameter Store: เก็บแต่ละ key เป็น SecureString ให้สิทธิ์ role ของ runtime ให้อ่านได้ ดึงค่ามาตอนใช้ครั้งแรก แล้ว cache ไว้ตลอด lifetime ของ process Lambda เบื้องหลัง krueng.ai ใช้ pattern นี้ทั้งหมด

在你自己的 Mac 上,.env 完全够用。但当你把同一份代码部署到 Lambda 或 EC2 时,你不会希望 .env 跟着容器镜像一起走 — 更不会想把 key 烤进 AMI。标准做法是 AWS Systems Manager Parameter Store:每个 key 存为 SecureString 参数,给运行时 role 读取权限,第一次用时取,进程生命周期内缓存。krueng.ai 背后的 Lambda 全用这个模式。

# Read NVIDIA_API_KEY from Parameter Store (instead of .env) in production import boto3, functools ssm = boto3.client("ssm", region_name="us-west-2") @functools.lru_cache(maxsize=32) def get_key(name: str) -> str: return ssm.get_parameter(Name=name, WithDecryption=True)["Parameter"]["Value"] api_key = get_key("NVIDIA_API_KEY")
  • @lru_cache = one SSM call per param. SSM has a per-account rate limit (40 GetParameter/sec by default). Without caching, a Lambda that fetches three keys per request will hit the wall under load.
  • Param names match .env keys exactly. The script you wrote locally with os.environ['NVIDIA_API_KEY'] needs one find-and-replace to get_key('NVIDIA_API_KEY') for production. That's the whole port.

💡 Local and production from the same code

A common pattern: wrap the lookup. Try os.environ[name] first, fall back to SSM. Locally the env var wins; in Lambda it's empty and SSM wins. One function, two environments, zero conditionals at the call sites.

pattern ที่ใช้กันบ่อย: ห่อ lookup ด้วย function ลอง os.environ[name] ก่อน ถ้าไม่มีค่อย fallback ไป SSM บนเครื่อง local ใช้ env var ใน Lambda env var ว่าง เลยตกไปที่ SSM function เดียว สองสภาพแวดล้อม ไม่มี if ที่ call site

常见模式:把查找包一层。先试 os.environ[name],没有再回退到 SSM。本地走环境变量,Lambda 里环境变量是空的就走 SSM。一个函数、两套环境,调用处不用任何 if。

📊 Quick reference

ServiceAuth headerFree?Notes
NVIDIA NIM Authorization: Bearer … $5 credit, no card OpenAI-compatible. Free models include Llama 3.1 70B.
Google Gemini ?key=… in URL 15 RPM, 1500/day free Multimodal. Not OpenAI-shaped — uses contents/parts.
LINE Messaging Authorization: Bearer … 500 push/month free One key (access token) sends to all your followers.
Replicate Authorization: Bearer … Small free credit Submit + poll pattern. Output URLs expire in 1 hour.
HeyGen X-Api-Key: … 1 watermarked video Paid for real use. Renders take 5–15 min — poll separately.

🩹 When things go wrong

HTTP 401 Unauthorized

Your key is wrong, missing, or revoked. Check (1) is $NVIDIA_API_KEY actually set in this shell? Run echo "${#NVIDIA_API_KEY}" — should print a number around 40–80. If it prints 0, your .env didn't load. (2) Did you commit and rotate the key recently? You might be using the old one. (3) Are you copy-pasting an example with the wrong header — Authorization: Bearer vs X-Api-Key?

key ผิด หาย หรือถูก revoke ตรวจ (1) $NVIDIA_API_KEY set ใน shell นี้จริงไหม รัน echo "${#NVIDIA_API_KEY}" ควรได้เลขประมาณ 40–80 ถ้าได้ 0 แสดงว่า .env ไม่ได้โหลด (2) เพิ่ง commit แล้ว rotate ใช่ไหม อาจจะใช้ตัวเก่าอยู่ (3) copy-paste ตัวอย่างที่ header ผิดหรือเปล่า — Authorization: Bearer vs X-Api-Key?

key 错了、丢了、被撤销了。检查 (1) $NVIDIA_API_KEY 在当前 shell 里真的设了吗?跑 echo "${#NVIDIA_API_KEY}" — 应该输出 40 到 80 之间的数。输出 0 说明 .env 没加载。(2) 最近是不是 commit 后又轮换过?可能你还在用旧的。(3) 是不是把示例里的 header 抄错了 — Authorization: Bearer vs X-Api-Key

HTTP 429 Too Many Requests

You hit a rate limit. Free tiers are typically per-minute and per-day. Read the response header Retry-After for the cool-down (seconds). For polling loops (Replicate, HeyGen), use time.sleep(2) or longer between polls — don't hammer.

ชน rate limit แล้ว free tier ปกติจำกัดต่อนาทีและต่อวัน อ่าน response header Retry-After เพื่อรู้ว่าต้องรอกี่วินาที สำหรับ polling loop (Replicate, HeyGen) ใช้ time.sleep(2) ขึ้นไประหว่างรอบ อย่ายิงรัว ๆ

触发限流了。免费档通常按分钟和按天双重限制。看响应头 Retry-After,那是冷却秒数。轮询循环(Replicate、HeyGen)里两次之间 time.sleep(2) 或更久 — 别死磕。

SSL / certificate errors on first request

Common on a fresh Python install: requests can't find the system root certs. Fix with pip install --upgrade certifi and, if you're using the Python from python.org installer on Mac, run open /Applications/Python\ 3.X/Install\ Certificates.command. Don't disable verification — that hides real issues.

เจอบ่อยกับ Python ที่เพิ่งติดตั้ง: requests หา root cert ของระบบไม่เจอ แก้ด้วย pip install --upgrade certifi และถ้าใช้ Python จาก installer ของ python.org รัน open /Applications/Python\ 3.X/Install\ Certificates.command ห้ามปิด verification — มันซ่อนปัญหาจริง

刚装好的 Python 上常见:requests 找不到系统根证书。pip install --upgrade certifi 修一下;如果用的是 python.org 安装包的 Python,跑 open /Applications/Python\ 3.X/Install\ Certificates.command。不要关闭验证 — 那只是把真正的问题藏起来。

Response is JSON but my code crashes with KeyError

Always call r.raise_for_status() before r.json(). A 4xx response body is a JSON error object that doesn't have your expected keys — without raise_for_status you'll see KeyError: 'choices' 50 lines deep instead of the real "invalid_api_key" message right at the top of the response.

เรียก r.raise_for_status() ก่อน r.json() เสมอ body ของ 4xx เป็น JSON error object ที่ไม่มี key ที่คุณคาด — ไม่มี raise_for_status จะเห็น KeyError: 'choices' ลึก 50 บรรทัด แทนที่จะเห็น "invalid_api_key" ที่หัว response

r.json() 之前一定先 r.raise_for_status()。4xx 响应体是个 JSON 错误对象,里面没有你期望的 key — 不调 raise_for_status 的话,你会在 50 行深的地方看到 KeyError: 'choices',而不是响应顶部那条真正的 "invalid_api_key"