Ir al contenido principal

Integra la entrega de imágenes y vídeo en una app de compañeros IA

Crea imágenes y clips aprobados de compañeros: fija la escena, pide presupuestos, recupera tareas asíncronas y entrega recursos revisados sin duplicados.

Autor
Lydia
Publicado
Lectura
15 min de lectura
Actualizado
Un hombre adulto se inclina hacia el ocular de un telescopio mientras una mujer con abrigo rojo sonríe a su lado junto a una puerta cálidamente iluminada.

Un usuario le dice a Rowan, un personaje compañero adulto ficticio: «Enséñame el observatorio esta noche». El modelo de chat puede responder en el papel del personaje, pero tu aplicación tiene que crear la imagen, mantener la coherencia de la escena y adjuntar un recurso real cuando exista. Mantén el chat ágil mientras los trabajos de imagen y vídeo se ejecutan por separado.

Da al usuario el control de dos compras:

  • Aprobar la imagen fija: muestra una tarjeta de escena gratuita, pide el presupuesto de la imagen y créala solo después de que el usuario acepte el precio.
  • Aprobar el clip: muestra la imagen fija terminada, pide el presupuesto de su animación y crea el vídeo solo después de que el usuario acepte esa imagen y el precio.

Guarda cada solicitud antes de enviarla, usa una clave distinta para cada etapa y entrega desde tu propia copia almacenada. Una llamada a una herramienta inicia la tarjeta de escena; nunca autoriza una compra ni anuncia un adjunto que no existe.

Como presupuesto concreto, una edición de Qwen en 1k ($0.036) más un clip de Seedance 2.5 de cinco segundos en 720p suman $1.4225, sin contar los cargos por imágenes de referencia adicionales, los tokens de chat ni los reintentos. Pide un presupuesto (quote) antes de mostrar cualquier precio a un usuario.

El flujo completo, en orden:

text
1. Chat model calls request_scene_image(scene, details)
2. Server validates owner and scene -> free draft card -> tool result back to the chat
3. Gate 1: user approves the still's price -> Qwen edit task (own idempotency key)
4. Callback or poll -> still ready -> copy it to your own storage
5. Gate 2: user accepts the still -> quote the clip -> user approves its price
6. Seedance task (own key) -> callback or poll -> ready_for_review
7. Review the whole clip -> copy to your storage -> delivered

Convierte una petición de chat en una imagen que la app pueda crear

Un usuario puede escribir: «Envíame otra foto con el abrigo verde», mientras ya se está generando un vídeo con abrigo azul marino. La respuesta del chat y la solicitud de medios no deberían compartir una única descripción mutable. La app necesita saber qué adulto ficticio, qué abrigo, qué escenario y si el usuario quiere una imagen fija o un clip. Si no, «otra» puede crear sin querer una cara nueva, reutilizar una escena obsoleta o comprar un vídeo cuando el usuario pidió una foto.

Mantén pequeños los datos permanentes del personaje: el retrato aprobado, la mayoría de edad, notas de aspecto estables y el personaje elegido por el usuario. Mantén aparte la escena actual: abrigo, estancia, vista de cámara y acción solicitada. La conversación puede sugerir una escena, pero tu aplicación resuelve el personaje almacenado y los recursos de origen aprobados. No dejes que el modelo de chat invente una URL de referencia ni un ID de tarea, ni que afirme que una imagen existe.

Para el ejemplo del observatorio, convierte la petición en una descripción explícita de la escena antes de pedir el presupuesto: Rowan, el retrato aprobado, abrigo azul marino, balcón del observatorio, plano medio, una imagen fija. Para una petición posterior de «hazlo moverse», usa la imagen fija aceptada del observatorio y un pequeño giro de cabeza. No envíes toda la conversación al modelo de imagen; las descripciones sin relación de otros personajes o de atuendos anteriores pueden entrar en conflicto con la petición actual.

La interfaz puede mostrar un resumen compacto de la escena mientras se genera la imagen. Si un usuario cambia verde por azul marino, crea una revisión nueva y etiqueta en consecuencia el resultado pendiente anterior. Una revisión antigua ya terminada puede permanecer en su historial privado, pero no debe aparecer en silencio como respuesta a la última solicitud.

En un producto de compañía NSFW, verifica por separado la idoneidad de la imagen y del vídeo. Que una tarea de imagen acepte contenido para adultos no implica que el siguiente modelo de vídeo acepte la misma escena. Consulta la página del modelo elegido para cada etapa y presenta con honestidad una tarea no disponible en lugar de prometer un clip para adultos que el modelo elegido no admite.

Pruébalo primero en el Playground

Abre el Playground de Qwen Image 2.1 edit. En Images elige Add a file y sube el retrato aprobado del personaje, describe la nueva escena en Prompt (el prompt de abajo sirve), ajusta Resolution a 1k y Aspect ratio a 16:9, pulsa Run model y evalúa la imagen fija. Aprueba esa composición y abre el Playground de Seedance 2.5 image-to-video: añade la imagen fija como Input image, describe el movimiento en Prompt y pulsa Run model otra vez. Closing frame es opcional.

Deja que el chat pida medios con una llamada a herramienta

Da al modelo de chat una herramienta request_scene_image que pida a tu aplicación preparar una tarjeta de escena. Usa el usuario con sesión iniciada y el retrato aprobado del personaje de tu aplicación, no IDs inventados en la respuesta del chat.

  • El modelo de chat: describe la escena y pregunta si el usuario quiere que se cree.
  • Tu aplicación: comprueba la propiedad del personaje, muestra el precio y espera la aprobación.
  • La tarea de medios: crea la imagen solo tras la aprobación; la animación es una compra aparte.

Grok 4.6 chat admite llamadas a herramientas en el formato de OpenAI. La API no guarda memoria de la conversación, así que reenvía la ficha del personaje y el contexto de escritura de ficción en cada solicitud; las recetas de chat muestran el formato del mensaje de herramienta.

json
{
  "type": "function",
  "function": {
    "name": "request_scene_image",
    "description": "Ask the app to prepare a picture of the character. Call only when the user asks to see something. The app shows a free scene card first; nothing is made or charged until the user approves a quoted price.",
    "parameters": {
      "type": "object",
      "properties": {
        "scene": {"type": "string", "enum": ["observatory-balcony", "kitchen-morning", "rooftop-dusk"]},
        "details": {"type": "string", "description": "Clothing, props and mood the user asked for, in one sentence."}
      },
      "required": ["scene"]
    }
  }
}

Responde a la llamada con un mensaje tool que indique que hay una tarjeta borrador esperando aprobación, para que el modelo escriba una frase honesta («Aquí tienes la tarjeta de escena, ¿quieres que la cree?») en vez de afirmar que ya hizo una entrega.

Mantén el personaje y la escena en registros separados

Mantén el retrato aprobado separado de la escena solicitada: un abrigo azul marino, un balcón de observatorio y un pequeño giro hacia un telescopio. El retrato ancla al personaje; la imagen de escena aceptada se convierte en el fotograma inicial del vídeo. No sustituyas el retrato del personaje con cada escena nueva.

Guarda un registro por solicitud antes de enviarla (son campos de tu aplicación, no una carga útil de la API):

json
{
  "mediaRequestId": "scene-104-r1",
  "ownerId": "application-user-id",
  "sceneRevision": 1,
  "referenceAssetId": "approved-rowan-portrait",
  "approvedStillAssetId": null,
  "model": "alibaba/qwen-image-2.1/edit",
  "inputSnapshot": {},
  "idempotencyKey": "<uuid generated for this request>",
  "taskId": null,
  "state": "draft",
  "deliveredAssetId": null
}

Si el usuario cambia el abrigo a verde, crea la revisión 2 con una clave nueva; nunca edites la solicitud del abrigo azul mientras se ejecuta.

Guarda tú mismo la imagen fija aprobada. El enlace de resultado de una tarea dura unos 20 minutos y el archivo generado unos 14 días, así que copia la imagen fija aceptada en tu propio almacenamiento y entrega a Seedance una URL HTTPS pública estable que devuelva el archivo de imagen en sí (un enlace a una página para compartir falla). Pasar el enlace de resultado de Qwen directamente a la etapa de vídeo solo funciona hasta que caduca. Reutiliza la imagen fija guardada para las revisiones de movimiento; cambiar a la vez el retrato y el movimiento dificulta corregir cualquiera de los dos.

Ajusta por separado los controles de imagen y de vídeo

La ruta Qwen Image 2.1 edit acepta un prompt y de una a diez image_urls. Las imágenes de referencia adicionales aumentan el precio de 1k; 2k cuesta $0.048 con de una a diez imágenes de referencia. Para la primera escena usa un retrato aprobado, 1k, 16:9, PNG y la expansión de prompt desactivada.

Use the supplied portrait as the identity reference for the same fictional adult Rowan. Create a medium shot on an observatory balcony, wearing a navy coat. Keep both hands relaxed and visible. A telescope rests on its tripod beside Rowan. Leave open space on the right for a small head turn. Warm doorway light, natural night background, no text or extra people.

Compara el rostro y el atuendo con el retrato aprobado antes de ofrecer la imagen fija al usuario. En la aprobación 2, el usuario acepta esa imagen fija y el precio del clip antes de la animación. Si el telescopio flota o el lado derecho no deja espacio para el movimiento, corrígelo en la etapa de imagen; el vídeo no puede reparar un mal primer fotograma.

Un adulto ficticio con abrigo azul marino está en el balcón de un observatorio, con las manos relajadas, junto a un telescopio sobre un trípode bien apoyado.

Ilustración de un primer fotograma propuesto: el personaje tiene espacio para un pequeño giro y el telescopio está físicamente apoyado. No es una salida de Qwen ni una prueba de generación.

La ruta Seedance 2.5 image-to-video acepta image_url, de 4 a 30 segundos, 480p, 720p o 1080p, un interruptor de audio y un seed opcional. aspect_ratio solo admite adaptive, de modo que el clip sigue la forma de la imagen fija; recorta antes la imagen fija al formato de entrega. La ruta reference-to-video tiene un contrato distinto, así que no mezcles sus campos.

Pide el presupuesto en el momento de decidir

Las ediciones de Qwen cuestan $0.036 en 1k, $0.048 en 2k y $0.072 en 1.5k, con una imagen de referencia incluida. Las imágenes de referencia adicionales aumentan los precios de 1k y 1.5k. Seedance cuesta $0.1234 por segundo en 480p, $0.2773 en 720p y $0.6823 en 1080p, con audio activado o desactivado a la misma tarifa.

Reutiliza la imagen fija aceptada al reintentar el movimiento.

PlanCoste
Solo imagen fija en 1k$0.036
Imagen fija + clip de 5 s en 480p$0.653
Imagen fija + clip de 5 s en 720p$1.4225
Imagen fija + clip de 5 s en 1080p$3.4475
Imagen fija + dos intentos de clip en 720p$2.809

Estos importes incluyen una referencia en la edición de 1k y excluyen los cargos por imágenes de referencia adicionales, los tokens de chat y cualquier edición extra de la imagen fija.

Muestra el precio de la imagen fija antes de generarla y el precio del clip después de que el usuario acepte la imagen fija, no en cada turno de chat. Un presupuesto es gratuito, no reserva nada y sigue siendo válido cinco minutos para esa cuenta, clave de API, modelo y entrada; si cambias la duración, la resolución o las referencias, necesitas uno nuevo. Una tarea enviada retiene su coste estimado de inmediato, y esa retención es lo máximo que puede cobrarse. Reutilizar la imagen aprobada mantiene una revisión de movimiento en el cargo del vídeo.

Un cliente para la etapa de vídeo que sobrevive a una respuesta perdida

Instala spicyapi con python -m pip install spicyapi, define SPICY_API_KEY y APPROVED_STILL_URL y ejecuta esto tras aprobar la imagen fija. Conserva el archivo de ejecución guardado para reanudar el mismo intento. Para otra toma creativa, usa un nombre de ejecución nuevo.

python
import json
import os
import time
import uuid
from pathlib import Path
from spicyapi import SpicyClient, output_assets

client = SpicyClient()

def run(model, inputs, name):
    path = Path(name + ".json")
    request = {"model": model, "input": inputs}
    saved = json.loads(path.read_text()) if path.exists() else {"request": request, "key": str(uuid.uuid4())}
    if saved["request"] != request:
        raise ValueError("Changed input needs a new run name; preserve the old attempt.")
    def save():
        temporary = path.with_suffix(".tmp")
        temporary.write_text(json.dumps(saved))
        temporary.chmod(0o600)
        temporary.replace(path)
    if not saved.get("taskId"):
        if time.time() - saved.get("sentAt", time.time()) >= 23 * 3600:
            raise RuntimeError("Reconcile the saved attempt before submitting again.")
        if "quote" not in saved:
            saved["quote"] = client.quote_task(model=model, input_data=inputs)
            if input(f"Maximum ${saved['quote']['maxCharge']}. Create? [y/N] ") != "y":
                raise SystemExit("Stopped without creating this task.")
        saved.setdefault("sentAt", time.time())
        save()
        quote = saved["quote"]
        task = client.create_task(model=model, input_data=inputs,
            idempotency_key=saved["key"], quote_id=quote["quoteId"], expected_cost=quote["estimatedCost"])
        saved["taskId"] = task["taskId"]
        save()
    print("Task:", saved["taskId"])
    for _ in range(360):
        task = client.get_task(saved["taskId"])
        if task["state"] in {"failed", "expired", "canceled"}:
            raise RuntimeError(str(task.get("errorMessage", task["state"])))
        if task["state"] == "succeeded":
            assets = output_assets(task)
            if assets and assets[0].get("unavailable"):
                raise RuntimeError("Output unavailable; keep the task ID for reconciliation.")
            if assets and assets[0].get("url") and not assets[0].get("pending"):
                return assets[0]
        time.sleep(5)
    raise TimeoutError("Rerun the same saved attempt to resume; no new task is needed.")

asset = run("bytedance/seedance-2.5/image-to-video", {
    "image_url": os.environ["APPROVED_STILL_URL"],
    "prompt": "The adult character slowly turns toward the telescope. Hold the camera steady. Keep the navy coat and balcony unchanged.",
    "duration_seconds": 5, "resolution": "720p", "aspect_ratio": "adaptive", "generate_audio": True,
}, "companion-video")
print(asset["url"])

El SDK de Python y la documentación de tareas cubren la integración en producción.

Una función de actualización para polling y callbacks

Usa un único manejador de la aplicación tanto para un resultado consultado por polling como para un evento de finalización verificado. Debe adjuntar el recurso una sola vez, mantenerlo privado y avisar al chat solo cuando esté listo para revisión. Un evento duplicado no debe crear otra entrada en la galería.

En una app de compañeros en producción, envía la finalización a un webhook verificado y mantén un barrido periódico de estado para los callbacks que nunca llegan. Los cuerpos de los callbacks incluyen el prompt y enlaces de resultado firmados, así que trátalos como privados; la documentación de tareas explica la configuración. Los webhooks con un barrido de conciliación (véase el artículo «async-task-api-polling-vs-webhook») son lo adecuado para producción; para un prototipo basta el polling. Usa el despacho acotado cuando muchos usuarios piden medios a la vez.

Estados de entrega, fallos y cancelación

Usa draft, submitted, generating, ready_for_review, delivered, failed y rejected. El succeeded de la API significa que empieza el tratamiento del resultado; no significa que el usuario lo vea. Revisa el clip completo en busca de deriva del rostro, objetos que se mueven y cortes no solicitados antes de pasar a delivered.

Resultado de la APIQué mostrarCargo
succeeded, recurso listorevisar y luego entregarleer settled y cost
succeeded, recurso pending«aún en preparación», consultar de nuevo en unos segundostodavía no es definitivo
succeeded, recurso unavailablerestaurar una copia ya guardada u ofrecer una nueva generación deliberadaleer cost
failed o expiredun fallo claro y un botón de reintentola retención normalmente se libera; algunos rechazos de contenido se cobran cuando la página del modelo lo indica

Un presupuesto caducado necesita uno nuevo y una nueva aprobación; recupera una solicitud ya aceptada con su clave original. Una tarea aceptada no se puede cancelar. Si el usuario dice «déjalo», oculta el adjunto pendiente y deja que el trabajo termine; no prometas que cerrar el chat libera el cargo. Un reintento es una solicitud nueva con su propio presupuesto.

Mantén privados los medios privados del compañero

Comprueba que una escena pertenece al usuario con sesión iniciada antes de servirla. Copia los medios aceptados a un almacenamiento con tus propias reglas de acceso; los enlaces de resultado temporales no deben convertirse en direcciones públicas de la galería.

Para el borrado, trata por separado la copia de la plataforma y la tuya. Acorta cuánto tiempo conserva la plataforma un resultado con el ajuste de retención por tarea y, cuando un usuario borre una escena o su cuenta, destruye el contenido almacenado de cada tarea terminada (el registro de facturación permanece). Una retención de 0 puede eliminar el resultado antes de que tu worker lo copie. Borra tu propia copia y revoca por separado su acceso de entrega; los detalles están en la documentación de retención. Usa los controles de retención y una canalización de almacenamiento duradero adecuados al producto.

Prueba cuatro fallos con fixtures

Simula un doble clic, una respuesta de aceptación perdida, un evento de éxito duplicado y un recurso no disponible. Deberías obtener una única solicitud lógica, recuperación con la clave original, un solo adjunto en la galería y un estado honesto de no disponible. Ninguna de estas pruebas requiere una generación de pago. La coherencia del rostro entre imágenes fijas y clips es una cuestión visual, así que inspecciona cada salida. No dejes que un mensaje de chat que diga «tu vídeo está listo» prevalezca sobre el estado real de la tarea o del recurso. La ilustración del observatorio es una escena editorial, no una salida de Qwen ni de Seedance.

Deja que un Agent se encargue de la llamada

Usa un Agent cuando quieras que prepare las entradas y devuelva el resultado mientras tú apruebas el precio y las decisiones creativas.

  • CLI: con un input.json como {"image_urls": ["<approved portrait URL>"], "prompt": "<the scene prompt above>", "resolution": "1k", "aspect_ratio": "16:9", "output_format": "png", "enable_prompt_expansion": false}, ejecuta npx @spicyapi/cli tasks create --model alibaba/qwen-image-2.1/edit --input-file input.json --wait. La CLI confirma el precio; conserva el ID de tarea devuelto para la recuperación.
  • MCP: pide a tu asistente que use spicyapi_task_create para alibaba/qwen-image-2.1/edit y luego spicyapi_task_wait. El servidor MCP pide confirmación del precio antes de crear la tarea.
  • Skill: instálalo con npx skills add spicyapi-ai/spicy-skill y pide después a $spicyapi que ejecute el mismo flujo y se detenga para tu aprobación. El Skill usa una conexión CLI o MCP ya configurada.

También en English · 日本語 · 한국어 · Deutsch · Français · Português (Brasil) · Русский · Italiano · Polski