Upload comic/modal_backend.py with huggingface_hub
Browse files- comic/modal_backend.py +82 -0
comic/modal_backend.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Real Modal backends: Gemma (vLLM OpenAI endpoint) + FLUX (resident pipeline).
|
| 2 |
+
|
| 3 |
+
Config via env (set after deploying the two Modal apps in serve/):
|
| 4 |
+
COMIC_GEMMA_URL - base URL of the vLLM OpenAI server, ending in /v1
|
| 5 |
+
COMIC_GEMMA_MODEL - served model name (default "gemma-comic")
|
| 6 |
+
COMIC_GEMMA_KEY - token if the endpoint uses proxy-auth (else "EMPTY")
|
| 7 |
+
COMIC_FLUX_APP - Modal app name for FLUX (default comic-flux)
|
| 8 |
+
COMIC_FLUX_CLS - Modal class name (default FluxRenderer)
|
| 9 |
+
|
| 10 |
+
Pattern mirrors the wisdom2 reference deployment: an OpenAI client to the vLLM /v1
|
| 11 |
+
endpoint for the writer, and a modal.Cls handle for the artist. The generous timeout
|
| 12 |
+
lets the first call after scale-to-zero ride through the GPU cold boot.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
|
| 19 |
+
from .backends import WriterBackend, ArtistBackend
|
| 20 |
+
|
| 21 |
+
DEFAULT_GEMMA_URL = "https://keshav-public07--comic-gemma-serve.modal.run/v1"
|
| 22 |
+
DEFAULT_GEMMA_MODEL = "gemma-comic"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ModalWriter(WriterBackend):
|
| 26 |
+
def __init__(self, base_url=None, model=None, api_key=None,
|
| 27 |
+
temperature=0.9, max_tokens=16384, timeout=900):
|
| 28 |
+
from openai import OpenAI
|
| 29 |
+
|
| 30 |
+
base_url = base_url or os.environ.get("COMIC_GEMMA_URL", DEFAULT_GEMMA_URL)
|
| 31 |
+
self.model = model or os.environ.get("COMIC_GEMMA_MODEL", DEFAULT_GEMMA_MODEL)
|
| 32 |
+
self.temperature = temperature
|
| 33 |
+
# Bibles/panel batches are large JSON; give plenty of output room.
|
| 34 |
+
self.max_tokens = max_tokens
|
| 35 |
+
# 15 min: absorbs the ~850s first-deploy cold boot without timing out.
|
| 36 |
+
self._client = OpenAI(
|
| 37 |
+
base_url=base_url,
|
| 38 |
+
api_key=api_key or os.environ.get("COMIC_GEMMA_KEY", "EMPTY"),
|
| 39 |
+
timeout=timeout,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
def chat(self, messages: list) -> str:
|
| 43 |
+
resp = self._client.chat.completions.create(
|
| 44 |
+
model=self.model,
|
| 45 |
+
messages=messages,
|
| 46 |
+
stream=False,
|
| 47 |
+
temperature=self.temperature,
|
| 48 |
+
max_tokens=self.max_tokens,
|
| 49 |
+
# Ask vLLM for a JSON object directly when the server supports it; the
|
| 50 |
+
# prompts also demand strict JSON, so this is belt-and-braces.
|
| 51 |
+
response_format={"type": "json_object"},
|
| 52 |
+
)
|
| 53 |
+
return resp.choices[0].message.content or ""
|
| 54 |
+
|
| 55 |
+
def warm(self) -> bool:
|
| 56 |
+
try:
|
| 57 |
+
self._client.models.list()
|
| 58 |
+
return True
|
| 59 |
+
except Exception:
|
| 60 |
+
return False
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class ModalArtist(ArtistBackend):
|
| 64 |
+
def __init__(self, app=None, cls=None):
|
| 65 |
+
import modal
|
| 66 |
+
|
| 67 |
+
app = app or os.environ.get("COMIC_FLUX_APP", "comic-flux")
|
| 68 |
+
cls = cls or os.environ.get("COMIC_FLUX_CLS", "FluxRenderer")
|
| 69 |
+
Renderer = modal.Cls.from_name(app, cls)
|
| 70 |
+
self._obj = Renderer()
|
| 71 |
+
|
| 72 |
+
def render(self, prompt: str, seed: int = 0) -> bytes:
|
| 73 |
+
return self._obj.render.remote(prompt, seed)
|
| 74 |
+
|
| 75 |
+
def render_batch(self, prompts: list, seeds: list) -> list:
|
| 76 |
+
return self._obj.render_batch.remote(list(prompts), list(seeds))
|
| 77 |
+
|
| 78 |
+
def warm(self) -> bool:
|
| 79 |
+
try:
|
| 80 |
+
return bool(self._obj.warm.remote())
|
| 81 |
+
except Exception:
|
| 82 |
+
return False
|