Skip to content

Instantly share code, notes, and snippets.

@antoniolg
Created August 4, 2026 09:10
Show Gist options
  • Select an option

  • Save antoniolg/ca12a24cd935c87ef912d202479de9e9 to your computer and use it in GitHub Desktop.

Select an option

Save antoniolg/ca12a24cd935c87ef912d202479de9e9 to your computer and use it in GitHub Desktop.
MiniMax H3 fácil: genera vídeos desde Terminal sin instalar dependencias

MiniMax H3 fácil: genera un vídeo sin saber programar

Este pequeño programa pregunta qué vídeo quieres, solicita tu API key sin mostrarla ni guardarla, envía la petición a MiniMax H3 y descarga el MP4 automáticamente.

Antes de empezar

  1. Crea una cuenta en MiniMax Platform.
  2. En Account Management → API Keys, crea una API key.
  3. Añade saldo o créditos a tu cuenta.
  4. Instala Python 3 si todavía no lo tienes.

La API key es como una contraseña: no la envíes a nadie, no la pegues en un documento y no la publiques en GitHub.

Uso en Mac

  1. Descarga minimax_h3_facil.py desde este Gist.
  2. Abre Terminal.
  3. Escribe python3, deja un espacio, arrastra el archivo minimax_h3_facil.py a la ventana y pulsa Intro.

Debería quedar parecido a esto:

python3 /Users/tu-nombre/Downloads/minimax_h3_facil.py

Uso en Windows

  1. Descarga minimax_h3_facil.py desde este Gist.
  2. Abre la carpeta de descargas.
  3. Haz clic derecho dentro de la carpeta y elige Abrir en Terminal.
  4. Ejecuta:
py minimax_h3_facil.py

Si py no funciona, prueba python minimax_h3_facil.py.

Qué te preguntará

  • La descripción del vídeo.
  • La duración, entre 4 y 15 segundos.
  • Si lo quieres horizontal, vertical o cuadrado.
  • Calidad 768P o 2K.
  • Una imagen de referencia opcional. Puedes arrastrarla al Terminal.
  • El nombre del MP4.
  • Confirmación antes de consumir créditos.
  • Tu API key, oculta y sin guardarse.

El programa muestra una estimación orientativa para 768P. Las tarifas pueden cambiar: comprueba siempre el panel de facturación de MiniMax antes de confirmar.

Probar sin gastar dinero

Este comando valida el programa y no llama a MiniMax:

python3 minimax_h3_facil.py --dry-run --prompt "Un zorro camina por un bosque al amanecer"

Consejos para describir el vídeo

Incluye, por este orden:

  1. Qué aparece.
  2. Qué sucede.
  3. Cómo se mueve la cámara.
  4. El estilo visual y la iluminación.
  5. El sonido, la música o la voz.
  6. Lo que no quieres que aparezca.

Ejemplo:

Plano cinematográfico horizontal. Una astronauta camina por el centro de Madrid al amanecer mientras pequeños drones vuelan a su alrededor. La cámara la sigue a baja altura. Luz dorada, aspecto realista y movimiento natural. Música electrónica esperanzadora y sonido ambiente de la ciudad. Sin texto ni logotipos.

Privacidad y coste

  • El programa no guarda la API key.
  • La descripción, las imágenes de referencia y el vídeo se procesan en los servidores de MiniMax.
  • Generar consume créditos. Cancelar después de enviar la tarea puede no evitar el cobro.
  • La estimación incluida se basa en una observación del 4 de agosto de 2026 y no sustituye la tarifa actual de MiniMax.
  • Consulta la documentación oficial de generación de vídeo y las condiciones de MiniMax.

Solución de problemas

  • “Python no se reconoce”: instala Python 3 y marca la opción para añadirlo al PATH en Windows.
  • “La API key no es válida”: crea una nueva key desde tu cuenta de MiniMax y asegúrate de tener acceso a H3.
  • “No encuentro la imagen”: arrastra el archivo directamente al Terminal cuando el programa la solicite.
  • La ventana parece parada: un vídeo puede tardar varios minutos; el programa mostrará el estado periódicamente.

Este ejemplo usa únicamente la biblioteca estándar de Python; no hace falta ejecutar pip install.

#!/usr/bin/env python3
"""Genera un vídeo con MiniMax H3 sin instalar librerías adicionales."""
from __future__ import annotations
import argparse
import base64
import getpass
import json
import mimetypes
import os
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
CREATE_URL = "https://api.minimax.io/v2/video_generation"
QUERY_URL = "https://api.minimax.io/v2/query/video_generation/{task_id}"
MODEL = "MiniMax-H3"
POLL_SECONDS = 10
TERMINAL_FAILURES = {"failed", "cancelled", "FAILED", "CANCELLED"}
# Estimación observada el 4 de agosto de 2026. No es una tarifa contractual.
OBSERVED_768P_USD_PER_SECOND = 0.104
class FriendlyError(RuntimeError):
"""Error que se puede mostrar a una persona sin un traceback."""
def api_json(
url: str,
*,
api_key: str,
method: str = "GET",
payload: dict[str, Any] | None = None,
timeout: int = 60,
) -> dict[str, Any]:
data = None if payload is None else json.dumps(payload).encode("utf-8")
request = urllib.request.Request(
url,
data=data,
method=method,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"User-Agent": "minimax-h3-facil/1.0",
},
)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.load(response)
except urllib.error.HTTPError as exc:
try:
detail = json.load(exc)
message = detail.get("message") or detail.get("base_resp", {}).get("status_msg")
except Exception:
message = exc.reason
if exc.code in {401, 403}:
raise FriendlyError("La API key no es válida o no tiene permiso para usar H3.") from exc
if exc.code == 429:
raise FriendlyError("MiniMax está limitando las peticiones. Espera un minuto y vuelve a probar.") from exc
raise FriendlyError(f"MiniMax devolvió el error HTTP {exc.code}: {message}") from exc
except urllib.error.URLError as exc:
raise FriendlyError(f"No se pudo conectar con MiniMax: {exc.reason}") from exc
def image_data_url(path: Path) -> str:
mime, _ = mimetypes.guess_type(path.name)
if mime not in {"image/jpeg", "image/png", "image/webp", "image/heic", "image/heif"}:
raise FriendlyError("La referencia debe ser JPG, PNG, WEBP, HEIC o HEIF.")
if path.stat().st_size > 20 * 1024 * 1024:
raise FriendlyError("La imagen de referencia supera 20 MB. Redúcela antes de continuar.")
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
return f"data:{mime};base64,{encoded}"
def download(url: str, output: Path) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
partial = output.with_suffix(output.suffix + ".part")
request = urllib.request.Request(url, headers={"User-Agent": "minimax-h3-facil/1.0"})
try:
with urllib.request.urlopen(request, timeout=300) as response, partial.open("wb") as target:
while chunk := response.read(1024 * 1024):
target.write(chunk)
if not partial.is_file() or partial.stat().st_size == 0:
raise FriendlyError("MiniMax devolvió un vídeo vacío.")
partial.replace(output)
except Exception:
partial.unlink(missing_ok=True)
raise
def ask(question: str, default: str | None = None) -> str:
suffix = f" [{default}]" if default is not None else ""
value = input(f"{question}{suffix}: ").strip()
return value or (default or "")
def ask_choice(question: str, choices: dict[str, str], default: str) -> str:
print(f"\n{question}")
for key, label in choices.items():
marker = " (recomendado)" if key == default else ""
print(f" {key}. {label}{marker}")
while True:
value = ask("Elige una opción", default)
if value in choices:
return choices[value]
print("Escribe uno de los números de la lista.")
def ask_duration(default: int = 10) -> int:
while True:
raw = ask("Duración en segundos, de 4 a 15", str(default))
try:
value = int(raw)
except ValueError:
print("La duración debe ser un número entero.")
continue
if 4 <= value <= 15:
return value
print("La duración debe estar entre 4 y 15 segundos.")
def clean_dropped_path(value: str) -> Path:
# Terminal suele añadir comillas al arrastrar un archivo.
return Path(value.strip().strip("'\"")).expanduser().resolve()
def interactive_values(args: argparse.Namespace) -> dict[str, Any]:
print("\nMiniMax H3 fácil")
print("Genera un MP4 a partir de una descripción. Pulsa Ctrl+C para cancelar.\n")
prompt = args.prompt or ask(
"Describe el vídeo",
"Un astronauta camina por Madrid al amanecer, estilo cinematográfico, música épica",
)
duration = args.duration or ask_duration()
ratio = args.ratio or ask_choice(
"¿Qué formato quieres?",
{"1": "16:9 horizontal", "2": "9:16 vertical", "3": "1:1 cuadrado"},
"1",
).split()[0]
resolution = args.resolution or ask_choice(
"¿Qué calidad quieres?",
{"1": "768P normal", "2": "2K alta (más cara)"},
"1",
).split()[0]
reference: Path | None = args.reference.resolve() if args.reference else None
if reference is None and sys.stdin.isatty():
raw_reference = ask("Imagen de referencia (opcional; puedes arrastrarla aquí)")
reference = clean_dropped_path(raw_reference) if raw_reference else None
if reference is not None and not reference.is_file():
raise FriendlyError(f"No encuentro la imagen de referencia: {reference}")
output = args.output
if output is None:
output = clean_dropped_path(ask("Nombre del vídeo de salida", "video-h3.mp4"))
if output.suffix.lower() != ".mp4":
output = output.with_suffix(".mp4")
return {
"prompt": prompt.strip(),
"duration": duration,
"ratio": ratio,
"resolution": resolution,
"reference": reference,
"output": output.resolve(),
}
def validate(values: dict[str, Any]) -> None:
if not values["prompt"]:
raise FriendlyError("La descripción no puede estar vacía.")
if len(values["prompt"]) > 7000:
raise FriendlyError("La descripción supera el máximo de 7000 caracteres.")
if not 4 <= values["duration"] <= 15:
raise FriendlyError("La duración debe estar entre 4 y 15 segundos.")
if values["ratio"] not in {"16:9", "9:16", "1:1"}:
raise FriendlyError("El formato debe ser 16:9, 9:16 o 1:1.")
if values["resolution"] not in {"768P", "2K"}:
raise FriendlyError("La calidad debe ser 768P o 2K.")
def build_payload(values: dict[str, Any]) -> dict[str, Any]:
content: list[dict[str, Any]] = [{"type": "text", "text": values["prompt"]}]
if values["reference"] is not None:
content.append(
{
"type": "image_url",
"image_url": {"url": image_data_url(values["reference"])},
"role": "reference_image",
}
)
return {
"model": MODEL,
"content": content,
"duration": values["duration"],
"resolution": values["resolution"],
"ratio": values["ratio"],
}
def confirm_cost(values: dict[str, Any], assume_yes: bool) -> None:
print("\nResumen")
print(f" Duración: {values['duration']} s")
print(f" Formato: {values['ratio']}")
print(f" Calidad: {values['resolution']}")
print(f" Salida: {values['output']}")
if values["resolution"] == "768P":
estimate = values["duration"] * OBSERVED_768P_USD_PER_SECOND
print(f" Coste orientativo observado: ${estimate:.2f} USD")
else:
print(" Coste: consulta el precio actual de 2K en tu cuenta de MiniMax")
print(" IMPORTANTE: MiniMax puede cambiar las tarifas; tu panel de facturación manda.")
if assume_yes:
return
answer = ask("¿Quieres enviar la generación y consumir créditos? (s/N)", "N").lower()
if answer not in {"s", "si", "sí", "y", "yes"}:
raise KeyboardInterrupt
def generate(values: dict[str, Any], api_key: str) -> str:
created = api_json(CREATE_URL, api_key=api_key, method="POST", payload=build_payload(values))
task_id = str(created.get("task_id", ""))
if not task_id:
raise FriendlyError(f"MiniMax no devolvió un identificador de tarea: {created}")
print(f"\nPetición enviada. Tarea {task_id}.")
print("La generación puede tardar varios minutos. Puedes dejar esta ventana abierta.")
while True:
task_response = api_json(QUERY_URL.format(task_id=task_id), api_key=api_key)
task = task_response.get("task", {})
status = str(task.get("status", "unknown"))
print(f" Estado: {status}", flush=True)
if status.lower() == "succeeded":
url = task.get("content", {}).get("url")
if not url:
raise FriendlyError("La tarea terminó, pero MiniMax no devolvió la URL del vídeo.")
print("Descargando el vídeo...")
download(url, values["output"])
return task_id
if status in TERMINAL_FAILURES or status.lower() in {"failed", "cancelled"}:
raise FriendlyError(f"MiniMax no pudo generar el vídeo: {task.get('error') or status}")
time.sleep(POLL_SECONDS)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Genera un vídeo con MiniMax H3 de forma interactiva.",
epilog="Ejemplo: python minimax_h3_facil.py --dry-run --prompt \"Un bosque mágico\"",
)
parser.add_argument("--prompt", help="Descripción del vídeo")
parser.add_argument("--duration", type=int, help="Duración de 4 a 15 segundos")
parser.add_argument("--ratio", choices=("16:9", "9:16", "1:1"), help="Formato del vídeo")
parser.add_argument("--resolution", choices=("768P", "2K"), help="Calidad")
parser.add_argument("--reference", type=Path, help="Imagen de referencia opcional")
parser.add_argument("-o", "--output", type=Path, help="Archivo MP4 de salida")
parser.add_argument("-n", "--dry-run", action="store_true", help="Validar sin gastar créditos")
parser.add_argument("--yes", action="store_true", help="No pedir confirmación antes de gastar")
return parser.parse_args()
def main() -> int:
try:
args = parse_args()
if not sys.stdin.isatty() and not args.prompt:
raise FriendlyError("Falta --prompt. Usa --help para ver un ejemplo.")
values = interactive_values(args)
validate(values)
confirm_cost(values, assume_yes=args.yes or args.dry_run)
if args.dry_run:
print("\nTodo correcto. No se ha enviado nada ni se han consumido créditos.")
return 0
api_key = os.environ.get("MINIMAX_API_KEY", "").strip()
if not api_key:
api_key = getpass.getpass("\nPega tu API key de MiniMax (no se mostrará ni se guardará): ").strip()
if not api_key:
raise FriendlyError("No se ha introducido ninguna API key.")
task_id = generate(values, api_key)
metadata = {
"model": MODEL,
"task_id": task_id,
"prompt": values["prompt"],
"duration": values["duration"],
"resolution": values["resolution"],
"ratio": values["ratio"],
"reference": str(values["reference"] or ""),
"output": str(values["output"]),
}
values["output"].with_suffix(".mp4.json").write_text(
json.dumps(metadata, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(f"\n✅ Vídeo guardado en:\n{values['output']}")
return 0
except KeyboardInterrupt:
print("\nCancelado. No se ha enviado ninguna petición nueva.")
return 130
except (FriendlyError, OSError, json.JSONDecodeError) as exc:
print(f"\nError: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment