วิธีสร้างวิดีโอ AI พร้อมเสียงในตัวด้วย Gemini Omni Flash ผ่าน Google Flow API

อ่าน 8 นาที • 15 มิถุนายน 2026 (12 พฤศจิกายน 2026)

useapi.net เป็น API ของบุคคลที่สาม (third-party) ไม่มีส่วนเกี่ยวข้องกับ Google และไม่ได้รับการรับรองจาก Google

สารบัญ

  1. บทนำ
  2. โมเดลที่รองรับ
  3. ราคา
  4. สร้างวิดีโอด้วยการเรียก API สองครั้ง
  5. โหมดอ้างอิง
  6. เสียงพูดและตัวละคร
  7. ต่อความยาววิดีโอด้วย Omni 1.2
  8. สร้างวิดีโอเป็นชุดด้วยสคริปต์
  9. ตัวอย่าง
  10. คำถามที่พบบ่อย
  11. สรุป

บทนำ

Gemini Omni Flash สร้างวิดีโอ AI ที่มีเสียงในตัวได้ด้วย curl คำสั่งเดียวผ่าน Google Flow API ของ useapi.net พร้อมเสียงพูดที่ซิงก์กับภาพ ตัวละคร และการแก้ไขวิดีโอจากวิดีโอ บนแพ็กเกจเว็บไซต์ Google Flow ของคุณ (Google AI: Omni 1.1 Flash บน Plus, Pro หรือ Ultra และ Omni 1.2 Flash บนทุกแพ็กเกจ บัญชีฟรียาวได้ไม่เกิน 10 วินาที) วิดีโอ 720p ยาว 10 วินาทีราคา $0.12 บน Google AI Ultra $199.99 เทียบกับ $1.01 บน Gemini API ทางการ (ข้อมูล ณ พฤศจิกายน 2026) โดยไม่ต้องมีโปรเจกต์ Google Cloud หรือ API key แพ็กเกจเดียวกันนี้ยังสร้างวิดีโอ Omni ใน Google Vids ได้ด้วย โดยไม่ใช้เครดิต Flow

ในหน้าเว็บ Google Flow โมเดลนี้ชื่อ Omni 1.1 Flash และค่าใน API คือ model: "omni-flash" ส่วน Omni 1.2 Flash ที่ Google เปิดตัวเมื่อ 5 พฤศจิกายน 2026 เป็นอีกค่าหนึ่ง คือ model: "omni-flash-1.2" ยาวได้ทุกความยาวตั้งแต่ 3 ถึง 15 วินาที คิดเครดิตต่อวินาที และเป็นโมเดลที่ต่อความยาววิดีโอได้ครั้งละไม่เกิน 10 วินาที โค้ดที่ส่ง omni-flash จะยังได้ 1.1 ต่อไป ถ้าต้องการวิดีโอ 15 วินาที การต่อความยาวพร้อมเสียงพูด และราคาต่อวิดีโอใน Google Flow และ Google Vids ดูวิธีใช้ Gemini Omni 1.2 Flash API

Omni Flash ใช้ Google Flow API ตัวเดียวกันกับ Veo เลือกด้วยฟิลด์เดียว คือ model: "omni-flash-1.2" หรือ model: "omni-flash" แต่เป็นโมเดลคนละแบบ Veo เก่งเรื่องสร้างวิดีโอจากข้อความล้วน ๆ ส่วน Omni Flash มีเสียงในตัวและรับข้อมูลอ้างอิงได้หลากหลาย:

  • เสียงถูกสร้างไปพร้อมกับวิดีโอ ทั้งเสียงบรรยาย บทพูด และเสียงบรรยากาศ มาจากโมเดลเดียวกัน ไม่ได้แปะเพิ่มทีหลัง
  • เสียงพูด มีพรีเซ็ตเสียงของระบบ 30 เสียง และเสียงที่คุณสร้างเองจากบทพูดและคำอธิบายการแสดงของคุณ ส่งผ่าน referenceAudio_1..5
  • ภาพและวิดีโออ้างอิง ภาพอ้างอิงสูงสุด 10 ภาพบน Omni 1.2 Flash (7 ภาพบน Omni 1.1 Flash) สำหรับวิดีโอจากภาพอ้างอิง (R2V) และการแก้ไขวิดีโอจากวิดีโอ (V2V) ของวิดีโอที่อัปโหลดหรือสร้างไว้ ผ่าน referenceVideo_1
  • ตัวละครที่ใช้ซ้ำได้ รวมภาพอ้างอิงกับเสียงเป็นตัวละครที่มีชื่อ แล้วใช้ซ้ำในหลายวิดีโอ
  • วิดีโอที่ยาวกว่า Omni 1.1 Flash สร้างได้ 4, 6, 8 หรือ 10 วินาที และ Omni 1.2 Flash ได้ทุกความยาวตั้งแต่ 3 ถึง 15 วินาที (Veo ยาวสุด 8) และ Omni 1.2 ต่อความยาววิดีโอได้ครั้งละไม่เกิน 10 วินาที

คู่มือนี้แสดงขั้นตอนเรียก API สองครั้งด้วย curl ที่คัดลอกไปใช้ได้เลย โหมดอ้างอิงที่ทำให้ Omni Flash ต่างจากโมเดลอื่น แล้วตามด้วยสคริปต์ Node.js ที่รันได้ทันทีสำหรับสร้างวิดีโอเป็นชุดจากรายการ prompt ถ้าต้องการสร้างวิดีโอจากข้อความอย่างเดียว ดูคู่มือ Veo 3.1

โมเดลที่รองรับ

คู่มือนี้ใช้โมเดลวิดีโอที่มีเสียงในตัวของ Google คือ Omni 1.1 Flash (model: "omni-flash") และ Omni 1.2 Flash (model: "omni-flash-1.2") สำหรับ Omni 1.1 เครดิตที่ใช้เพิ่มตามความยาววิดีโอ:

ความยาว เครดิตต่อวิดีโอ
4s 7
6s 10
8s 12
10s 15
แก้ไข V2V 20

นี่คือราคาที่ความละเอียดเริ่มต้น 720p ถ้าส่ง resolution: "360p" จะใช้ราวครึ่งหนึ่ง คือ 4 / 5 / 6 / 7 เครดิตตามความยาว และ 10 สำหรับการแก้ไข V2V และวิดีโอ 360p ยังอัปสเกลเป็น 720p ทีหลังได้ด้วย POST /videos/upscale

Omni 1.2 Flash (model: "omni-flash-1.2") รับความยาวเป็นจำนวนวินาทีเต็มตั้งแต่ 3 ถึง 15 และคิดราคาต่อวินาที เท่ากันทุกแพ็กเกจ:

ความยาว 720p 360p
3s 9 3
8s 24 8
10s 30 10
15s 60 20

ถึง 10 วินาทีคิด 3 เครดิตต่อวินาทีที่ 720p และ 1 เครดิตที่ 360p ทุกวินาทีที่เกิน 10 คิดสองเท่า การแก้ไข V2V ราคาเท่ากับวิดีโอที่ยาวเท่าช่วงที่แก้ และ 1.2 แก้ไขคลิปได้สูงสุด 15 วินาที บัญชี Google ฟรีใช้ 1.2 สร้างวิดีโอยาวไม่เกิน 10 วินาทีได้

Omni Flash รับห้าโหมดบน POST /videos คือ วิดีโอจากข้อความ (T2V) วิดีโอจากภาพอ้างอิง (R2V) ด้วย referenceImage_1..10 สูงสุด 10 ภาพบน Omni 1.2 Flash (7 ภาพบน Omni 1.1 Flash) วิดีโอจากภาพที่เริ่มจากเฟรมแรกผ่าน startImage (I2V) วิดีโอจากภาพระหว่างเฟรมเริ่มกับเฟรมจบผ่าน startImage + endImage (I2V-FL) และการแก้ไขวิดีโอจากวิดีโอ (V2V) ของ MP4 ที่อัปโหลดผ่าน referenceVideo_1 ดูโหมดอ้างอิงด้านล่าง

ราคา

Omni 1.1 Flash ต้องใช้แพ็กเกจ Google AI แบบเสียเงิน (Plus, Pro หรือ Ultra) ส่วน Omni 1.2 Flash ใช้บนบัญชีฟรีได้ ยาวไม่เกิน 10 วินาที และ useapi.net ไม่คิดเงินต่อวิดีโอ วิดีโอแต่ละคลิปใช้เครดิต Flow ตามความยาว (ดูโมเดลที่รองรับ) ราคาต่อวิดีโอบนทุกแพ็กเกจเทียบกับ Gemini API ทางการอยู่ในตารางราคา Google Flow

แพ็กเกจ Google AI ยังมาพร้อมโควตารายเดือนใน Google Vids ซึ่งสร้างวิดีโอยาวสูงสุด 15 วินาทีด้วย Gemini Omni 1.2 Flash (ประกาศของ Google) และไม่ใช้เครดิต Flow บน Ultra $199.99 (Google เรียกว่า Ultra 20x) ได้วิดีโอ 10,000 วินาทีต่อเดือน หรือคลิป 1080p ยาว 10 วินาทีอีกราว 1,000 คลิป เพิ่มจากคลิป Omni 1.1 Flash ยาว 10 วินาทีราว 1,666 คลิป (หรือคลิป Omni 1.2 Flash ที่ 720p ราว 833 คลิป) ที่เครดิต Flow 25,000 เครดิตซื้อได้ วินาทีเหล่านี้ใช้ร่วมกันทุกคนในแพ็กเกจครอบครัวของ Google แพ็กเกจอื่นได้น้อยกว่า (ดูหน้า Gemini ใน Google Vids ของ Google) และบัญชีฟรีที่เราทดสอบไม่ได้เลย Google Vids API ใช้โควตานี้โดยไม่มี captcha เชื่อมบัญชีได้ที่ Setup Google Vids

สร้างวิดีโอด้วยการเรียก API สองครั้ง

คุณต้องมี API token ของ useapi.net และบัญชี Google Flowที่เชื่อมไว้แล้ว บนแพ็กเกจ Google AI แบบเสียเงิน (Plus, Pro หรือ Ultra) สำหรับตัวอย่าง Omni 1.1 Flash ด้านล่าง (Omni 1.2 Flash รันบนบัญชีฟรีได้ด้วย ยาวไม่เกิน 10 วินาที) งานวิดีโอเป็นแบบ asynchronous ส่ง async: true แล้วคำขอสร้างจะคืน jobid กลับมาทันที จากนั้นคุณ poll จนวิดีโอเสร็จ

  1. ส่งงาน — POST https://api.useapi.net/v1/google-flow/videos ด้วย model: "omni-flash":
curl -X POST "https://api.useapi.net/v1/google-flow/videos" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A lighthouse keeper narrates the coming storm, waves crashing below",
    "model": "omni-flash",
    "aspectRatio": "landscape",
    "duration": 10,
    "async": true
  }'

Omni Flash มีเสียงในตัว จึงสร้างเสียงประกอบทั้งหมด ทั้งเสียงบรรยายของผู้ดูแลประภาคารและเสียงคลื่นซัด จาก prompt ได้โดยตรง ไม่ต้องใส่พารามิเตอร์เสียง ในโหมด async คำตอบกลับมาทันทีพร้อม 201 Created, jobid ตัวพิมพ์เล็ก และ status: "created":

{
  "jobid": "j1731859234567v-u12345-email:jo***@gmail.com-bot:google-flow",
  "type": "video",
  "status": "created",
  "created": "2026-06-15T12:34:56.789Z",
  "request": {
    "async": true,
    "prompt": "A lighthouse keeper narrates the coming storm, waves crashing below",
    "model": "omni-flash",
    "aspectRatio": "landscape",
    "duration": 10
  }
}
  1. poll ผลลัพธ์ — GET https://api.useapi.net/v1/google-flow/jobs/{jobId}:
curl "https://api.useapi.net/v1/google-flow/jobs/JOBID" \
  -H "Authorization: Bearer $USEAPI_TOKEN"

status เปลี่ยนจาก created → started → completed เมื่อถึง completed ไฟล์ MP4 ที่เสร็จแล้วจะอยู่ใน response.media[].videoUrl (prompt เดียวที่ใช้ count > 1 จะคืนหนึ่งรายการต่อวิดีโอ):

{
  "jobid": "j1731859234567v-...",
  "type": "video",
  "status": "completed",
  "response": {
    "media": [
      {
        "mediaGenerationId": "user:12345-email:6a6f...-video:a1d95d21-...",
        "videoUrl": "https://flow-content.google/video/a1d95d21-...?Expires=...",
        "thumbnailUrl": "https://flow-content.google/image/a1d95d21-...?Expires=..."
      }
    ],
    "remainingCredits": 18760
  }
}

โดยทั่วไป Omni Flash เสร็จใน 60–180 วินาที videoUrl ที่มีลายเซ็นใช้ได้ราว 6 ชั่วโมง จึงควรดาวน์โหลดทันที ไม่อยาก poll? ใส่ replyUrl ใน body ของคำขอสร้าง เพื่อรับ webhook callback เมื่องานเสร็จ

โหมดอ้างอิง

จุดเด่นของ Omni Flash คือรับข้อมูลอ้างอิงได้มาก ข้อมูลอ้างอิงทั้งหมดมาจาก endpoint อัปโหลด POST /assets/email (หรือสำหรับตัวละคร มาจาก POST /characters) คำตอบของการอัปโหลดเก็บ id อ้างอิงซ้อนไว้ที่ mediaGenerationId.mediaGenerationId สตริงที่ซ้อนอยู่นี้คือค่าที่คุณส่งกลับไป โหมดที่ omni-flash รับ (จากเอกสาร POST /videos):

โหมด พารามิเตอร์ ขีดจำกัดของ Omni Flash
วิดีโอจากภาพอ้างอิง (R2V) referenceImage_1..10 ภาพอ้างอิงสูงสุด 10 ภาพบน omni-flash-1.2 และ 7 ภาพบน omni-flash
วิดีโอจากภาพ (I2V) startImage เฟรมเริ่มหนึ่งเฟรม
วิดีโอจากภาพ เฟรมแรก + เฟรมสุดท้าย (I2V-FL) startImage + endImage สองเฟรมนี้เท่านั้น ใส่ข้อมูลอ้างอิงอื่นไม่ได้
แก้ไขวิดีโอจากวิดีโอ (V2V) referenceVideo_1 (+ startFrameIndex_1 / endFrameIndex_1 ถ้าต้องการ) MP4 ที่อัปโหลดหนึ่งไฟล์
ตัวละคร character_1..10 (จาก POST /characters) รวมกับ referenceImage_* ได้ทั้งหมด 10 บน omni-flash-1.2 และ 7 บน omni-flash
เสียงบรรยาย referenceAudio_1..5 สูงสุด 5 (R2V) / สูงสุด 3 (แก้ไข V2V)

วิดีโอจากภาพอ้างอิง (R2V) อัปโหลดภาพหนึ่งภาพขึ้นไป แล้วส่งเป็น referenceImage_1, referenceImage_2, … (ช่อง _4.._7 ใช้ได้เฉพาะ Omni Flash) ภาพอ้างอิงใช้ร่วมกับ startImage/endImage ไม่ได้

# 1. Upload a reference image — the id is nested one level deep:
curl -X POST "https://api.useapi.net/v1/google-flow/assets/john%40gmail.com" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: image/jpeg" \
  --data-binary @subject.jpeg
# Response: { "mediaGenerationId": { "mediaGenerationId": "user:12345-email:...-image:..." }, ... }

# 2. Generate, narrated by a system voice
curl -X POST "https://api.useapi.net/v1/google-flow/videos" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "omni-flash",
    "prompt": "the subject walks toward camera and introduces herself",
    "duration": 8,
    "referenceImage_1": "user:12345-email:...-image:...",
    "referenceAudio_1": "Kore"
  }'

แก้ไขวิดีโอจากวิดีโอ (V2V) อัปโหลด MP4 ด้วย Content-Type: video/mp4 ไปที่ POST /assets แล้วส่ง id ที่ได้เป็น referenceVideo_1 เพื่อแก้ไขวิดีโอนั้น โหมดนี้มีเฉพาะ Omni Flash และจะเปลี่ยนคำขอเป็นโหมด V2V ไม่รับ duration (ความยาวผลลัพธ์เท่ากับช่วงที่ตัดจากอินพุต สูงสุด 10 วินาทีบน omni-flash และ 15 วินาทีบน omni-flash-1.2) ถ้าต้องการ ตัดช่วงของวิดีโอต้นทางบนไทม์ไลน์เสมือน 24 fps ด้วย startFrameIndex_1 (0–239) และ endFrameIndex_1 (1–240 โดย 240 = 10 วินาที) หรือสูงสุด 359 และ 360 (15 วินาที) บน omni-flash-1.2:

curl -X POST "https://api.useapi.net/v1/google-flow/videos" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "omni-flash",
    "prompt": "restyle the scene as a rainy neon-lit night, add ambient city sound",
    "referenceVideo_1": "user:12345-email:...-video:...",
    "startFrameIndex_1": 0,
    "endFrameIndex_1": 240,
    "async": true
  }'

คุณยังใส่เครื่องหมาย @ ใน prompt ที่ชี้ไปยังช่องที่คุณส่งมาได้ด้วย เช่น @referenceImage_1, @character_1, @referenceAudio_1 เครื่องหมายเหล่านี้ใส่หรือไม่ใส่ก็ได้ และแต่ละตัวต้องมีพารามิเตอร์ใน body ที่ตรงกัน (@referenceVideo_* ใช้ใน prompt ไม่ได้ ให้ส่งเป็นฟิลด์ใน body) ดู Inline @-mention markers

เสียงพูดและตัวละคร

การเรียกตั้งค่าสั้น ๆ สองครั้งเปิดใช้ความสามารถด้านเสียงและตัวตนของ Omni Flash

เสียงที่สร้างเอง POST /voices บันทึกเสียงที่สร้างจากพรีเซ็ตของระบบหนึ่งใน 30 เสียง พร้อม dialog และ voicePerformance ของคุณเอง ฟิลด์ voice ในคำตอบคือ id อ้างอิงที่คุณส่งเป็น referenceAudio_1..5 บน POST /videos:

curl -X POST "https://api.useapi.net/v1/google-flow/voices" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "voice": "Achernar",
    "dialog": "Welcome to the demo.",
    "voicePerformance": "Cheerful, energetic delivery",
    "displayName": "Cheerful Narrator"
  }'
# Response: { "voice": "user:12345-email:...-voice:...-mid:...", ... }

แต่ละช่อง referenceAudio_* รับได้ทั้งชื่อเสียงของระบบ (ไม่สนตัวพิมพ์เล็กใหญ่ หนึ่งใน 30 พรีเซ็ต เช่น Aoede, Kore, Puck, Zephyr) หรือ id เสียงของผู้ใช้จากการเรียกด้านบน การสร้างเสียงต้องผ่าน reCAPTCHA (แก้ให้อัตโนมัติ ดูคำถามที่พบบ่อย)

ตัวละครที่ใช้ซ้ำได้ POST /characters รวมภาพอ้างอิง 1–2 ภาพกับเสียง (ถ้าต้องการ) เป็นตัวละครที่มีชื่อ ฟิลด์ character ในคำตอบคือค่าที่คุณส่งเป็น character_1..10:

curl -X POST "https://api.useapi.net/v1/google-flow/characters" \
  -H "Authorization: Bearer $USEAPI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Carol",
    "imageReference_1": "user:12345-email:...-image:abc123...",
    "voice": "user:12345-email:...-voice:...-mid:..."
  }'
# Response: { "character": "user:12345-email:...-character:...-imgs:1-voice:...", ... }

บน Omni Flash ตัวละครใช้ได้ทุกความยาว และนับรวมในโควตาภาพอ้างอิง 10 ภาพร่วมกับ referenceImage_* (referenceImages.length + Σ(character.imgs) ≤ 10, Omni 1.1: 7) POST /characters ไม่ต้องใช้ captcha ตัวละครและเสียงใช้กับ Veo ได้ด้วย แต่มีโควตาน้อยกว่า (3 ภาพ 1 เสียง เฉพาะ 8 วินาที) ดู What can use a character?

ต่อความยาววิดีโอด้วย Omni 1.2

POST /videos/extend เพิ่มความยาวครั้งละ 3 ถึง 10 วินาทีด้วย Omni 1.2 Flash ให้วิดีโอใดก็ได้ที่สั้นกว่า 30 วินาที ใส่เสียง ภาพอ้างอิง หรือตัวละครได้สูงสุด 3 รายการ และคืนคลิปต่อเนื่องคลิปเดียว (เฉพาะแพ็กเกจเสียเงิน) ขั้นตอนในวิธีใช้ Gemini Omni 1.2 Flash API ต่อความยาววิดีโอ 10 วินาทีสองครั้งจนได้ 30 วินาทีพร้อมสองเสียง ดูตัวอย่างด้านล่าง

สร้างวิดีโอเป็นชุดด้วยสคริปต์

กว่าจะได้เทคที่ใช่ต้องลองหลายครั้ง และการรันทีละครั้งด้วยมือก็น่าเบื่อ สคริปต์ Node.js ด้านล่างอ่านรายการ prompt จาก prompts.json อัปโหลดภาพอ้างอิง เฟรมเริ่มและเฟรมจบ หรือวิดีโอต้นทาง ส่ง id ของ character_* และ resolution ต่อไปตรง ๆ ส่งงานแต่ละงานในโหมด async ด้วย model: "omni-flash" แล้ว poll และดาวน์โหลด MP4 ทุกไฟล์ที่เสร็จ คุณจึงส่งงานไว้เป็นชุด แล้วค่อยกลับมาเลือกคลิปที่ดีที่สุด สคริปต์จะหยุดพร้อมคำเตือนก่อนส่งชุดค่าที่ API จะปฏิเสธ เช่น เฟรมจบที่ไม่มีเฟรมเริ่ม

คุณต้องมี Node.js v21 ขึ้นไป วาง prompts.json และ omni-flash.mjs ไว้ในโฟลเดอร์เดียวกัน แล้วรัน node ./omni-flash.mjs API_TOKEN EMAIL โดย API_TOKEN คือ API token ของ useapi.net และ EMAIL คืออีเมลของบัญชี Google Flowที่เชื่อมไว้ สคริปต์ค้นหาบัญชีจากอีเมลให้เอง และตรวจฟิลด์ health ก่อนส่งงาน

อยาก clone ไปรันบนเครื่องตัวเอง? ไฟล์ omni-flash.mjs และ prompts.json ฉบับเต็มอยู่บน GitHub ใน useapi/google-flow-api

ใช้ Claude Code หรือ Codex อยู่? agent skill omni-flash-video ให้ agent ทำทั้งหมดนี้แทนคุณ แค่บอกว่าต้องการวิดีโอแบบไหนและภาพอยู่ที่ไหน agent จะอัปโหลดภาพ เขียนคำขอ รอ และบันทึกไฟล์ .mp4 ให้ ติดตั้งด้วย npx skills add useapi/google-flow-api --skill omni-flash-video

ขยาย prompts.json
[
    {
        "prompt": "Audio-native text-to-video. By default model omni-flash with a landscape 8-second clip will be used."
    },
    {
        "duration": 10,
        "prompt": "A 10-second clip (omni-flash only). Omni Flash is audio-native, so narration, dialogue and sound come from the prompt. To pin a specific voice, add referenceAudio_1 together with a referenceImage_* or character_*, as in the next prompt."
    },
    {
        "duration": 8,
        "referenceImage_1": "./subject.jpeg",
        "referenceAudio_1": "Kore",
        "prompt": "Reference-to-video (R2V): the uploaded image is a reference, narrated by 'Kore'. Up to 7 referenceImage_* on omni-flash, 10 on omni-flash-1.2."
    },
    {
        "duration": 8,
        "startImage": "./first_image.jpeg",
        "endImage": "./last_image.jpeg",
        "prompt": "Image-to-video with a start frame (startImage) and an end frame (endImage). The two frames are the only input — no referenceImage_*, character_* or referenceAudio_* alongside them."
    },
    {
        "referenceVideo_1": "./source.mp4",
        "startFrameIndex_1": 0,
        "endFrameIndex_1": 240,
        "prompt": "Video-to-video edit (V2V): omni-flash restyles the uploaded MP4. No duration here — output matches the trim window (max 10 s). For all parameters see https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos"
    }
]
ขยายสคริปต์ omni-flash.mjs
/*

Script version 1.3, November 11, 2026

Script to batch-generate audio-native videos with Gemini Omni Flash using the Google Flow API by useapi.net
Uses the POST /videos endpoint in async mode (model: omni-flash by default, or omni-flash-1.2 per prompt) and polls GET /jobs/{jobId}.
For more details visit https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos

Installation Instructions:
==========================

You need Node.js v21 or newer installed to run this script. Download and install Node.js from:

- Windows, macOS, Linux: https://nodejs.org/

After installation, verify by running the following command in a terminal:

   node -v

Running the Script:
===================

Usage: node omni-flash.mjs <API_TOKEN> <EMAIL> [PROMPTS_FILE]

Replace API_TOKEN with your actual useapi.net API token, see https://useapi.net/docs/start-here/setup-useapi
Replace EMAIL with configured Google Flow email account, see https://useapi.net/docs/start-here/setup-google-flow
If optional PROMPTS_FILE not provided prompts.json will be used.

Example:
--------

node omni-flash.mjs user:1234-abcdefhijklmnopqrstuv [email protected]

This command executes the script using API token user:1234-abcdefhijklmnopqrstuv with [email protected] Google Flow account email.

Changelog:
==========

- June 15, 2026: Initial release.
- September 11, 2026: Adds resolution, startImage / endImage (start and end frames, local files uploaded for you) and character_1..7. Accepts .jpg and upper-case file extensions. Stops with a warning before submitting combinations the API rejects, such as endImage without startImage or referenceAudio_* on its own. A prompt whose local startImage, endImage, referenceImage_* or referenceVideo_1 file fails to upload is skipped and logged to omni-flash_errors.txt instead of being submitted without it. Saves a downloaded video under its final name only once it has been fully received, so a failed download leaves no partial MP4 for the next run to skip as "already exists".
- November 11, 2026: referenceImage_8..10 and character_8..10: omni-flash-1.2 takes up to 10 references (omni-flash 7).

*/

import readline from 'node:readline';
import fs from 'fs/promises';
import path from 'path';
import { writeFile } from 'node:fs/promises';
import { Readable } from 'node:stream';


// Constants
const RESULTS_FILE = 'omni-flash_results.txt';
const ERRORS_FILE = 'omni-flash_errors.txt';
const DEFAULT_PROMPTS_FILE = 'prompts.json';
const MODEL = 'omni-flash';
// Reference slots: up to 10 on omni-flash-1.2, 7 on omni-flash, 3 on Veo (referenceImage_* and character_* together).
const imageRefKeys = Array.from({ length: 10 }, (_, i) => `referenceImage_${i + 1}`);
const characterKeys = Array.from({ length: 10 }, (_, i) => `character_${i + 1}`);
const SLEEP_429 = 30 * 1000; // in milliseconds
const SLEEP_POLL = 15 * 1000; // in milliseconds

const urlAccounts = 'https://api.useapi.net/v1/google-flow/accounts';
const urlVideos = 'https://api.useapi.net/v1/google-flow/videos';
const urlJobs = 'https://api.useapi.net/v1/google-flow/jobs/';
const urlUploadAsset = 'https://api.useapi.net/v1/google-flow/assets/';

// Google Flow accepts png, jpeg and webp images, and mp4 video (for omni-flash V2V edit).
const supportedImageExtensions = ['png', 'jpg', 'jpeg', 'webp'];
const supportedVideoExtensions = ['mp4'];

// { filename: mediaGenerationId }
const uploadedFiles = {};

// Utility to sleep for given milliseconds
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Function to fetch configured Google Flow API accounts
async function fetchAccounts(apiToken) {
    const response = await fetch(urlAccounts, {
        headers: {
            'Accept': 'application/json',
            'Authorization': `Bearer ${apiToken}`
        }
    });

    if (!response.ok) {
        console.error(`⛔ Error fetching accounts (HTTP ${response.status}): ${response.statusText}`);
        process.exit(1);
    }

    return response.json();
}

const elapsedTimeSec = (start) => (Date.now() - start) / 1000;

// Map a file extension to the Content-Type required by POST /assets
const contentTypeForExt = (ext) =>
    ext === 'png' ? 'image/png' :
    ext === 'webp' ? 'image/webp' :
    ext === 'mp4' ? 'video/mp4' :
    'image/jpeg';

async function uploadAsset(apiToken, email, filename) {

    // Check if already uploaded
    if (uploadedFiles.hasOwnProperty(filename))
        return uploadedFiles[filename];

    const startTime = Date.now();

    console.log(`⬆️  Account ${email} uploading file…`, filename);

    const body = new Blob([await fs.readFile(filename)]);

    const fileExt = filename.split('.').pop().toLowerCase();

    const response = await fetch(`${urlUploadAsset}${encodeURIComponent(email)}`, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Authorization': `Bearer ${apiToken}`,
            'Content-Type': contentTypeForExt(fileExt)
        },
        body
    });

    if (response.ok) {
        const json = await response.json();
        // POST /assets returns the reference id nested as mediaGenerationId.mediaGenerationId
        const mediaGenerationId = json?.mediaGenerationId?.mediaGenerationId;
        console.log(`🆗 mediaGenerationId (${elapsedTimeSec(startTime)} sec)`, mediaGenerationId);
        uploadedFiles[filename] = mediaGenerationId;
    }
    else {
        console.error(`❗ Unable to upload file HTTP ${response.status} (${elapsedTimeSec(startTime)} sec)`, await response.text());
        // Do not attempt to upload failed file again
        uploadedFiles[filename] = undefined;
    }

    return uploadedFiles[filename];
}

async function submit(apiToken, url, body, index, prompt) {
    const createResponse = await fetch(url, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${apiToken}`
        },
        body
    });

    const createBody = await createResponse.text();

    // Async POST /videos returns 201 Created with a jobid to poll.
    if (createResponse.status == 201) {
        const json = JSON.parse(createBody);
        // 201 async payload uses lowercase "jobid"; sync 200 uses "jobId". Accept either.
        const jobId = json.jobid ?? json.jobId;
        if (jobId) {
            await fs.appendFile(RESULTS_FILE, `${jobId},#${index}:${prompt}\n`);
            console.log(`✅ jobId`, jobId);
            return 201;
        } else {
            const error = `No jobid found in HTTP 201 response`;
            console.log(`❓ ${error}`, createBody);
            await fs.appendFile(ERRORS_FILE, `${error},#${index}:${prompt}\n`);
            return 500;
        }
    } else {
        switch (createResponse.status) {
            case 429:
                console.log(`🔄️ Retry on HTTP ${createResponse.status}`, createBody);
                break;
            case 503:
                console.log(`🔄️ Service unavailable, retry on HTTP ${createResponse.status}`, createBody);
                break;
            case 400:
                console.log(`🛑 Rejected request (validation or content policy)`, createBody);
                await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${prompt}\n`);
                break;
            case 402:
                console.log(`🛑 No subscription / insufficient credits`, createBody);
                break;
            default:
                console.log(`❗ FAILED with HTTP ${createResponse.status}`, createBody);
                await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${prompt}\n`);
        }
        return createResponse.status;
    }
}

// Submit a single prompt to POST /videos with model omni-flash (or the prompt's own model) in async mode.
// startImage = start frame (I2V), endImage = end frame (I2V-FL, requires startImage) — the frames are the only input;
// referenceImage_1..10 = R2V image refs; character_1..10 = POST /characters ids (10 on omni-flash-1.2, 7 on omni-flash);
// referenceVideo_1 = V2V edit (no duration); referenceAudio_1..5 = system voice name or POST /voices user-voice id;
// resolution = 720p (default) or 360p; model = 'omni-flash' (Omni 1.1, the default) or 'omni-flash-1.2' (3–15 s, per-second credits).
async function submitVideo(apiToken, email, prompt, index) {
    const {
        prompt: text, model = MODEL, aspectRatio, duration, resolution, count,
        startImage, endImage,
        referenceVideo_1, startFrameIndex_1, endFrameIndex_1,
        referenceAudio_1, referenceAudio_2, referenceAudio_3, referenceAudio_4, referenceAudio_5
    } = prompt;

    console.log(`🚀 ${model} » Prompt #${index} • account ${email} …`);

    // Upload any local image/video files and swap the paths for their mediaGenerationId.
    const resolveRef = async (ref) =>
        ref && (ref.startsWith('./') || ref.includes('.'))
            && [...supportedImageExtensions, ...supportedVideoExtensions].includes(ref.split('.').pop().toLowerCase())
            ? await uploadAsset(apiToken, email, ref)
            : ref;

    const startImageId = await resolveRef(startImage);
    const endImageId = await resolveRef(endImage);
    const refImages = {};
    for (const key of imageRefKeys)
        refImages[key] = await resolveRef(prompt[key]);
    const refVideo_1 = await resolveRef(referenceVideo_1);

    // A local file that failed to upload would be left out of the body, and the prompt would still be
    // generated (and charged) without it. Skip the prompt instead. uploadAsset remembers the failed file,
    // so a later prompt using it is skipped too, without another upload attempt.
    const failedUploads = [
        ['startImage', startImage, startImageId], ['endImage', endImage, endImageId],
        ...imageRefKeys.map(key => [key, prompt[key], refImages[key]]),
        ['referenceVideo_1', referenceVideo_1, refVideo_1]
    ].filter(([, file, id]) => file && !id);
    for (const [param, file] of failedUploads) {
        console.error(`🛑 Prompt #${index} skipped: ${param} ${file} failed to upload`);
        await fs.appendFile(ERRORS_FILE, `Upload failed for ${param} ${file},#${index}:${text}\n`);
    }
    if (failedUploads.length)
        return;

    // V2V edit does not accept duration — output matches the trim window.
    const isV2V = !!refVideo_1;

    const body = JSON.stringify({
        model,
        email,
        prompt: text,
        aspectRatio,
        duration: isV2V ? undefined : duration,
        resolution,
        count,
        startImage: startImageId,
        endImage: endImageId,
        ...refImages,
        ...Object.fromEntries(characterKeys.map(key => [key, prompt[key]])),
        referenceVideo_1: refVideo_1,
        startFrameIndex_1,
        endFrameIndex_1,
        referenceAudio_1,
        referenceAudio_2,
        referenceAudio_3,
        referenceAudio_4,
        referenceAudio_5,
        async: true
    });

    return await submit(apiToken, urlVideos, body, index, text);
}

// Function to download videos
async function download(apiToken) {
    if (! await fileExists(RESULTS_FILE)) return;

    try {
        const resultsContent = await fs.readFile(RESULTS_FILE, 'utf8');
        const lines = resultsContent.trim().split('\n');

        for (const line of lines) {
            const [jobId, prompt] = line.split(',');

            console.log(`👉 ${jobId}`);

            while (true) {
                const response = await fetch(`${urlJobs}${jobId}`, {
                    headers: {
                        'Accept': 'application/json',
                        'Authorization': `Bearer ${apiToken}`
                    }
                });

                if (!response.ok) {
                    console.log(`🛑 Poll failed ${jobId} (HTTP ${response.status}):\n${prompt}\n`, await response.text());
                    break;
                }

                const jobBody = await response.json();
                const { status, error, response: jobResponse } = jobBody;

                if (status == 'failed') {
                    console.error(`🛑 FAILED ${jobId} (${error}):\n${prompt}\n`);
                    break;
                }

                if (status == 'completed') {
                    const media = jobResponse?.media ?? [];

                    if (media.length == 0)
                        console.error(`🛑 Completed but no media for ${jobId}:\n${prompt}\n`);

                    // A single prompt with count > 1 returns multiple videos.
                    for (let i = 0; i < media.length; i++) {
                        const url = media[i]?.videoUrl;
                        const videoFilename = `${jobId.replace(/[:*]/g, '_')}_${i + 1}.mp4`;

                        try {
                            await fs.access(videoFilename);
                            console.log(`⚠️ ${videoFilename} already exists. Skipping download.`);
                            continue;
                        } catch {
                            // File does not exist, proceed with downloading
                        }

                        if (url) {
                            console.log(`✅ Downloading ${url} to ${videoFilename}`);
                            // Stream into a temporary file and rename it once the whole video has arrived, so a download
                            // that drops mid-stream leaves no partial file behind for the next run to skip as "already exists".
                            const tempFilename = `${videoFilename}.part`;
                            try {
                                const videoResponse = await fetch(url);
                                if (!videoResponse.ok) {
                                    console.error(`⛔ Unable to download ${jobId} (HTTP ${videoResponse.status}):\n${prompt}\n`, url);
                                    continue;
                                }
                                const stream = Readable.fromWeb(videoResponse.body);
                                await writeFile(tempFilename, stream);
                                await fs.rename(tempFilename, videoFilename);
                            } catch (err) {
                                await fs.rm(tempFilename, { force: true });
                                console.error(`⛔ Error during download: ${err}`);
                            }
                        } else
                            console.error(`🛑 No videoUrl for ${jobId} media #${i + 1}:\n${prompt}\n`);
                    }

                    break;
                }

                console.log(`⌛ ${jobId} status (${status}) and is still in progress, waiting…`);
                await sleep(SLEEP_POLL);
            }
        }
    } catch (error) {
        console.log(`⛔ Error during download:`, error.stack || error);
    }
}

// Main function
async function main() {
    const apiToken = process.argv[2];
    const email = process.argv[3];
    const promptFile = process.argv[4] || DEFAULT_PROMPTS_FILE;

    if (!apiToken || !email) {
        console.error('Usage: node omni-flash.mjs <API_TOKEN> <EMAIL> [PROMPTS_FILE]');
        process.exit(1);
    }

    console.info('Script v1.3');

    console.info('Node version is: ' + process.version);

    try {
        if (await fileExists(RESULTS_FILE)) {
            let user_input;
            while (!['y', 'n'].includes(user_input)) {
                user_input = (await promptUser(`❔ ${RESULTS_FILE} file detected. Do you want to download the results now? (y/n): `))?.toLowerCase();
                if (user_input == 'y') {
                    await download(apiToken);
                    await fs.unlink(RESULTS_FILE);
                }
            }
        }

        const start = new Date();
        try {
            console.info('START EXECUTION', start);
            await execute(apiToken, email, promptFile); // Pass the promptFile to execute function
        }
        finally {
            console.info('COMPLETED', new Date());
            console.info('EXECUTION ELAPSED', diffInMinutesAndSeconds(start, new Date()));
        }

        try {
            console.info('START DOWNLOAD', start);
            await download(apiToken);
        }
        finally {
            console.info('TOTAL ELAPSED', diffInMinutesAndSeconds(start, new Date()));
        }
    } catch (error) {
        console.error('⛔ Error during execution:', error.stack || error);
    }
}

// Modify the execute function to accept promptFile as a parameter
async function execute(apiToken, email, promptFile) {
    const accounts = await fetchAccounts(apiToken);

    console.info(`Configured Google Flow API accounts (${Object.keys(accounts).length}):`, Object.keys(accounts).join(', '));

    if (Object.keys(accounts).length <= 0) {
        console.error(`⛔ No configured Google Flow accounts found. Please refer to https://useapi.net/docs/start-here/setup-google-flow`);
        process.exit(1);
    }

    if (!accounts[email]) {
        console.error(`⛔ Account ${email} not found. Please refer to https://useapi.net/docs/start-here/setup-google-flow`);
        process.exit(1);
    }

    if (accounts[email].health && accounts[email].health !== 'OK') {
        console.error(`⛔ Account ${email} health is '${accounts[email].health}'. Please resolve and update the account, see https://useapi.net/docs/start-here/setup-google-flow`);
        process.exit(1);
    }

    const promptData = await fs.readFile(promptFile, 'utf8');
    const prompts = JSON.parse(promptData);
    console.log(`Total number of prompts to process`, prompts.length);

    let warnings = [];

    // Parameters accepted by this script for the omni-flash POST /videos endpoint.
    // See https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos for the full parameter set.
    const supportedParams = [
        'prompt', 'model', 'aspectRatio', 'duration', 'resolution', 'count',
        'startImage', 'endImage',
        ...imageRefKeys, ...characterKeys,
        'referenceVideo_1', 'startFrameIndex_1', 'endFrameIndex_1',
        'referenceAudio_1', 'referenceAudio_2', 'referenceAudio_3', 'referenceAudio_4', 'referenceAudio_5'
    ];

    const audioRefKeys = ['referenceAudio_1', 'referenceAudio_2', 'referenceAudio_3', 'referenceAudio_4', 'referenceAudio_5'];

    const invalidKeys = (prompt) => Object.keys(prompt).filter(key => !key.startsWith('__') && !supportedParams.includes(key))

    // Validate a local file path reference (skip already-uploaded mediaGenerationId strings).
    const looksLikePath = (ref) => ref && (ref.startsWith('./') || (ref.includes('.') && !ref.startsWith('user:')));

    for (let i = 1; i <= prompts.length; i++) {
        const prompt = prompts[i - 1];
        const { prompt: text, referenceVideo_1, duration, startImage, endImage } = prompt;

        const validateFile = async (file, allowedExts) => {
            if (looksLikePath(file)) {
                try {
                    await fs.access(file);
                } catch {
                    warnings.push(`⚠️  File '${file}' does not exist. Prompt ${i}`);
                }

                const ext = file.split('.').pop().toLowerCase();

                if (!allowedExts.includes(ext))
                    warnings.push(`⚠️  File ${file} extension ${ext} not supported. Prompt ${i}`);
            }
        };

        const notSupported = invalidKeys(prompt);
        if (notSupported.length)
            warnings.push(`⚠️  Following params not supported: ${notSupported.join(',')}. Prompt ${i}`);

        if (!text)
            warnings.push(`⚠️  prompt is required. Prompt ${i}`);

        // V2V edit (referenceVideo_1) does not accept duration — output matches the trim window.
        if (referenceVideo_1 && duration)
            warnings.push(`⚠️  duration is not accepted with referenceVideo_1 (V2V edit). Prompt ${i}`);

        if (endImage && !startImage)
            warnings.push(`⚠️  endImage requires startImage (end-frame-only is not supported). Prompt ${i}`);

        // Start/end frames (I2V, I2V-FL) are the only input in that mode — the API rejects anything alongside them.
        if (startImage || endImage) {
            if (referenceVideo_1)
                warnings.push(`⚠️  startImage/endImage cannot be combined with referenceVideo_1 (V2V edit). Prompt ${i}`);
            if ([...imageRefKeys, ...characterKeys].some(k => prompt[k]))
                warnings.push(`⚠️  startImage/endImage cannot be combined with referenceImage_* or character_*. Prompt ${i}`);
            if (audioRefKeys.some(k => prompt[k]))
                warnings.push(`⚠️  startImage/endImage cannot be combined with referenceAudio_*. Prompt ${i}`);
        }

        // A voice needs a referenceImage_* or character_* to attach to (or a V2V edit) — the API rejects referenceAudio_* on its own.
        // With start/end frames the check above already applies.
        if (!startImage && !endImage && !referenceVideo_1 && audioRefKeys.some(k => prompt[k]) && ![...imageRefKeys, ...characterKeys].some(k => prompt[k]))
            warnings.push(`⚠️  referenceAudio_* requires a referenceImage_* or character_* (or referenceVideo_1 for a V2V edit). Prompt ${i}`);

        await Promise.all([
            validateFile(referenceVideo_1, supportedVideoExtensions),
            ...['startImage', 'endImage', ...imageRefKeys].map(k => validateFile(prompt[k], supportedImageExtensions))
        ]);
    }

    if (warnings.length > 0) {
        warnings.forEach(warning => console.warn(warning));
        console.error(`⛔ Execution stopped due to warnings.`);
        process.exit(1);
    }

    for (let i = 0; i < prompts.length; i++) {
        const prompt = prompts[i];
        while (true) {
            const responseCode = await submitVideo(apiToken, email, prompt, i + 1);
            if (responseCode == 429 || responseCode == 503)
                await sleep(SLEEP_429);
            else
                if (responseCode == 402) {
                    process.exit(1);
                } else
                    break;
        }
    }
}

// Utility function to check if a file exists
async function fileExists(path) {
    try {
        await fs.access(path);
        return true;
    } catch {
        return false;
    }
}

// Function to prompt user input
async function promptUser(query) {
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    return new Promise((resolve) => rl.question(query, answer => {
        rl.close();
        resolve(answer);
    }));
}

function diffInMinutesAndSeconds(date1, date2) {
    const diffInSeconds = Math.floor((date2 - date1) / 1000);
    return `${Math.floor(diffInSeconds / 60)} minutes ${diffInSeconds % 60} seconds`;
};

main();

ตัวอย่าง

วิดีโอด้านล่างเป็นผลลัพธ์จริงของ Gemini Omni Flash ที่สร้างผ่าน Google Flow API นี้ ทั้ง model: "omni-flash-1.2" และ model: "omni-flash" การต่อความยาว เสียงพูด ภาพอ้างอิง ตัวละคร และการแก้ไข V2V

ต่อความยาวด้วย Omni 1.2 » วิดีโอ 10 วินาทีต่อความยาวสองครั้งจนได้ 30 วินาที พร้อมสองเสียง

จาก Google Flow: Omni 1.2 Flash, 15-Second Videos, and Extend with Voices ใช้ omni-flash-1.2 สำหรับ 10 วินาทีแรก แล้วเรียก POST /videos/extend สองครั้งพร้อม referenceAudio_1

แก้ไข V2V » เปลี่ยน MP4 ที่อัปโหลดเป็นการ์ตูน คงการเคลื่อนไหวและมุมกล้องเดิม

จาก Google Flow: Gemini Omni Flash แก้ไขด้วย referenceVideo_1

Inline @-mentions » วิดีโอ R2V แนวฟิล์มนัวร์ด้วย @character_1, @referenceImage_1, @referenceImage_2 และเสียงที่สร้างเอง

จาก Google Flow: Inline @-mentions ใช้เครื่องหมาย @ ใน prompt ผูกข้อมูลอ้างอิงแต่ละรายการ

ตัวละคร + แก้ไข V2V » ใส่ตัวละครที่ใช้ซ้ำได้สองตัวลงในวิดีโอฟิล์มนัวร์

จาก Google Flow: Inline @-mentions แก้ไข V2V ด้วย referenceVideo_1 พร้อม @character_* ใน prompt ดูขั้นตอนตั้งค่าเสียงและตัวละครได้ที่ Reusable Voices and Characters

คำถามที่พบบ่อย

  • Gemini Omni Flash คืออะไร? ผ่าน useapi.net คุณรัน Gemini Omni Flash ซึ่งเป็นโมเดลวิดีโอที่มีเสียงในตัวของ Google ใน Google Flow ได้ด้วยการตั้ง model: "omni-flash-1.2" (Omni 1.2 Flash) หรือ model: "omni-flash" (Omni 1.1 Flash) บน Google Flow API ต่างจากโมเดลวิดีโอที่ไม่มีเสียง โมเดลนี้สร้างเสียงประกอบ เสียงบรรยาย บทพูด และเสียงบรรยากาศ ไปในวิดีโอเดียวกัน และรับภาพ วิดีโอ และเสียงอ้างอิง รวมถึงตัวละครที่ใช้ซ้ำได้
  • Omni Flash ต่างจาก Veo 3.1 อย่างไร? ผ่าน useapi.net ทั้งสองโมเดลรันบน Google Flow API ตัวเดียวกัน เลือกด้วยฟิลด์ model Veo ให้ภาพคมกว่าสำหรับวิดีโอจากข้อความและวิดีโอจากภาพแบบปกติ ส่วน Omni Flash มีเสียงในตัวและรับข้อมูลอ้างอิงได้หลากหลาย ทั้งแก้ไขวิดีโอจากวิดีโอ (referenceVideo_1) ใส่หลายเสียงต่อวิดีโอ ใช้ตัวละครซ้ำได้ทุกความยาว และสร้างวิดีโอที่ยาวกว่า Veo ดูขีดจำกัดที่แน่นอนในโหมดอ้างอิง
  • ใช้เสียงของตัวเอง วิดีโออ้างอิง หรือเสียงอ้างอิงได้ไหม? ผ่าน useapi.net ได้ ใส่เสียงบรรยายด้วยเสียงในตัวของระบบ หรือเสียงที่สร้างเองจาก POST /voices ส่งเป็น referenceAudio_1..5 และอัปโหลด MP4 ด้วย POST /assets เพื่อแก้ไขเป็น referenceVideo_1 (V2V) ดูขีดจำกัดของแต่ละโหมดในโหมดอ้างอิงและเสียงพูดและตัวละคร
  • Omni Flash มี API ทางการไหม? ผ่าน useapi.net Omni 1.1 Flash ถูกกว่า API ของ Google เองมาก วิดีโอ 720p ยาว 8 วินาทีราคา $0.10 บน Google AI Ultra $199.99 เทียบกับ $0.81 บน Gemini API ทางการ ที่ขายโมเดลนี้แบบคิดต่อวินาทีมาตั้งแต่ 30 มิถุนายน 2026 (ข้อมูล ณ พฤศจิกายน 2026) โดยรันผ่าน REST endpoint มาตรฐานบนแพ็กเกจเว็บไซต์ Google Flow ของคุณ
  • Omni Flash สร้างวิดีโอยาวกว่า 10 วินาทีได้ไหม? ผ่าน useapi.net ได้ Omni 1.2 Flash (model: "omni-flash-1.2") สร้างได้สูงสุด 15 วินาทีในคำสั่งเดียว และ POST /videos/extend เพิ่มได้ครั้งละไม่เกิน 10 วินาทีพร้อมเสียงพูด วิดีโอ 720p ยาว 8 วินาทีราคา $0.19 บน Google AI Ultra $199.99 เทียบกับ $0.56 บน API ของ Google เอง (ข้อมูล ณ พฤศจิกายน 2026) ขีดจำกัดความยาวและราคาทั้งหมดใน Google Flow และ Google Vids อยู่ในวิธีใช้ Gemini Omni 1.2 Flash API
  • ต้องแก้ captcha เองไหม? ผ่าน useapi.net คุณไม่ต้องแก้เองเลย Google Flow ขอ reCAPTCHA ทุกครั้งที่สร้างวิดีโอและทุกครั้งที่สร้างเสียงใหม่ และ worker ของ useapi.net แก้ให้อัตโนมัติ บัญชี Google Flow บัญชีแรกของคุณได้แก้ฟรี 300 ครั้งด้วยคีย์ AntiCaptcha, CapSolver และ EzCaptcha ของเราเอง หลังจากนั้นคุณเพิ่มคีย์ผู้ให้บริการของคุณเองด้วย POST /accounts/captcha-providers ส่วน POST /characters ไม่ต้องใช้ captcha

สรุป

ติดต่อเราได้ที่ Discord Server หรือ Telegram Channel สำหรับทุกคำถามและข้อสงสัยด้านการใช้งาน

ดูตัวอย่างโค้ดได้ใน GitHub repo ของเรา

ผู้ช่วย AI ยังทำงานกับ Google Flow แทนคุณได้ด้วย Google Flow MCP server แบบโฮสต์ให้ Claude Code, Cursor และ VS Code แชตกับ Google Flow Agent ซึ่งสร้างภาพและวิดีโอบนบัญชีของคุณ

เริ่มใช้งาน API

การสมัครสมาชิกเดือนละ $15 ใช้ได้กับทุก API บน useapi.net ใช้ Token เดียวกับทุกบริการ เชื่อมบัญชีของคุณเองได้สูงสุด 3 บัญชีต่อบริการ แต่ละบัญชีใช้แพ็กเกจของเว็บไซต์นั้นเอง ส่วน useapi.net ดูแล REST API การกระจายโหลด และการ polling ให้ ต้องการบัญชีมากกว่านี้? สมัครเพิ่มแต่ละครั้งเดือนละ $10 และได้เพิ่มอีก 3 บัญชีต่อบริการ

สมัครสมาชิก — $15/เดือน ชำระด้วยคริปโต

ไม่มีบัตรเครดิตที่ใช้จ่ายต่างประเทศได้? สมัครด้วยคริปโตก็ได้ (การชำระด้วยคริปโตขอคืนเงินไม่ได้) ดูราคาและแพ็กเกจสำหรับปริมาณมากได้ที่หน้าการสมัครสมาชิก ถ้าชำระผ่าน Stripe ขอคืนเงินเต็มจำนวนได้ภายใน 14 วันหลังซื้อครั้งแรก หากสร้างงานสำเร็จไม่ถึง 50 ครั้ง สมัครไว้แล้ว? เปลี่ยนจำนวนได้ที่ Stripe Customer Portal โดยไม่ต้องซื้อใหม่