Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files
poetic/providers/__init__.py
CHANGED
|
@@ -1,50 +1,97 @@
|
|
| 1 |
"""Provider registry — one seam per capability so models swap by config, not code.
|
| 2 |
|
| 3 |
Selection order per capability:
|
| 4 |
-
1. Explicit env var
|
| 5 |
-
2. POETIC_MODE bundle
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
import os
|
| 10 |
from functools import cache
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
def resolve_mode() -> str:
|
| 14 |
mode = os.environ.get("POETIC_MODE")
|
| 15 |
-
if mode in
|
| 16 |
return mode
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
def _resolve(capability: str, env_var: str):
|
| 21 |
-
name = os.environ.get(env_var) or resolve_mode()
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
elif name == "api":
|
| 25 |
-
from poetic.providers import hf_api as module
|
| 26 |
-
else:
|
| 27 |
raise ValueError(
|
| 28 |
-
f"Unknown {env_var}={name!r}; expected
|
| 29 |
)
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
@cache
|
| 34 |
def get_interpreter():
|
| 35 |
-
return _resolve("
|
| 36 |
|
| 37 |
|
| 38 |
@cache
|
| 39 |
def get_painter():
|
| 40 |
-
return _resolve("
|
| 41 |
|
| 42 |
|
| 43 |
@cache
|
| 44 |
def get_singer():
|
| 45 |
-
return _resolve("
|
| 46 |
|
| 47 |
|
| 48 |
@cache
|
| 49 |
def get_animator():
|
| 50 |
-
return _resolve("
|
|
|
|
| 1 |
"""Provider registry — one seam per capability so models swap by config, not code.
|
| 2 |
|
| 3 |
Selection order per capability:
|
| 4 |
+
1. Explicit per-capability env var, e.g. INTERPRET_PROVIDER=minicpm_api.
|
| 5 |
+
2. POETIC_MODE bundle default for the rest.
|
| 6 |
+
|
| 7 |
+
Provider names map to a module under poetic/providers/:
|
| 8 |
+
- "minicpm_api" -> OpenBMB's free hosted MiniCPM endpoint (interpret; no GPU)
|
| 9 |
+
- "modal" -> Modal serverless GPU (paint/sing/animate; sponsor prize)
|
| 10 |
+
- "local" -> on-Space ZeroGPU pipelines (local_zerogpu)
|
| 11 |
+
- "api" -> HF Inference Providers / Spaces (hf_api)
|
| 12 |
+
|
| 13 |
+
POETIC_MODE bundles pick sensible defaults per capability so a single env var
|
| 14 |
+
flips the whole app. The default bundle is "hosted": MiniCPM API + Modal —
|
| 15 |
+
no ZeroGPU quota, qualifies for Best Use of Modal.
|
| 16 |
"""
|
| 17 |
|
| 18 |
import os
|
| 19 |
from functools import cache
|
| 20 |
|
| 21 |
+
_MODULE_BY_NAME = {
|
| 22 |
+
"local": "local_zerogpu",
|
| 23 |
+
"api": "hf_api",
|
| 24 |
+
"minicpm_api": "minicpm_api",
|
| 25 |
+
"modal": "modal_gpu",
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
# Per-capability defaults for each POETIC_MODE bundle.
|
| 29 |
+
_BUNDLES = {
|
| 30 |
+
"hosted": { # no ZeroGPU: free MiniCPM API + Modal serverless
|
| 31 |
+
"interpret": "minicpm_api",
|
| 32 |
+
"paint": "modal",
|
| 33 |
+
"sing": "modal",
|
| 34 |
+
"animate": "modal",
|
| 35 |
+
},
|
| 36 |
+
"local": { # everything on-Space ZeroGPU
|
| 37 |
+
"interpret": "local",
|
| 38 |
+
"paint": "local",
|
| 39 |
+
"sing": "local",
|
| 40 |
+
"animate": "local",
|
| 41 |
+
},
|
| 42 |
+
"api": { # HF Inference Providers / Spaces everywhere
|
| 43 |
+
"interpret": "api",
|
| 44 |
+
"paint": "api",
|
| 45 |
+
"sing": "api",
|
| 46 |
+
"animate": "api",
|
| 47 |
+
},
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
|
| 51 |
def resolve_mode() -> str:
|
| 52 |
mode = os.environ.get("POETIC_MODE")
|
| 53 |
+
if mode in _BUNDLES:
|
| 54 |
return mode
|
| 55 |
+
# Default: on ZeroGPU hardware use local; otherwise the hosted bundle.
|
| 56 |
+
return "local" if os.environ.get("SPACES_ZERO_GPU") else "hosted"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
_CLASS_BY_CAPABILITY = {
|
| 60 |
+
"interpret": "Interpreter",
|
| 61 |
+
"paint": "Painter",
|
| 62 |
+
"sing": "Singer",
|
| 63 |
+
"animate": "Animator",
|
| 64 |
+
}
|
| 65 |
|
| 66 |
|
| 67 |
def _resolve(capability: str, env_var: str):
|
| 68 |
+
name = os.environ.get(env_var) or _BUNDLES[resolve_mode()][capability]
|
| 69 |
+
module_name = _MODULE_BY_NAME.get(name)
|
| 70 |
+
if module_name is None:
|
|
|
|
|
|
|
|
|
|
| 71 |
raise ValueError(
|
| 72 |
+
f"Unknown {env_var}={name!r}; expected one of {sorted(_MODULE_BY_NAME)}"
|
| 73 |
)
|
| 74 |
+
import importlib
|
| 75 |
+
|
| 76 |
+
module = importlib.import_module(f"poetic.providers.{module_name}")
|
| 77 |
+
return getattr(module, _CLASS_BY_CAPABILITY[capability])()
|
| 78 |
|
| 79 |
|
| 80 |
@cache
|
| 81 |
def get_interpreter():
|
| 82 |
+
return _resolve("interpret", "INTERPRET_PROVIDER")
|
| 83 |
|
| 84 |
|
| 85 |
@cache
|
| 86 |
def get_painter():
|
| 87 |
+
return _resolve("paint", "PAINT_PROVIDER")
|
| 88 |
|
| 89 |
|
| 90 |
@cache
|
| 91 |
def get_singer():
|
| 92 |
+
return _resolve("sing", "SING_PROVIDER")
|
| 93 |
|
| 94 |
|
| 95 |
@cache
|
| 96 |
def get_animator():
|
| 97 |
+
return _resolve("animate", "ANIMATE_PROVIDER")
|
poetic/providers/__pycache__/__init__.cpython-312.pyc
CHANGED
|
Binary files a/poetic/providers/__pycache__/__init__.cpython-312.pyc and b/poetic/providers/__pycache__/__init__.cpython-312.pyc differ
|
|
|
poetic/providers/__pycache__/minicpm_api.cpython-312.pyc
ADDED
|
Binary file (2.85 kB). View file
|
|
|
poetic/providers/minicpm_api.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Interpret via OpenBMB's free hosted MiniCPM endpoint — no GPU, no quota.
|
| 2 |
+
|
| 3 |
+
The hackathon provides a free MiniCPM4.1-8B OpenAI-compatible API. Using it for
|
| 4 |
+
读诗 means the Space needs no GPU for interpretation at all, and keeps the
|
| 5 |
+
sponsor (OpenBMB) model in the pipeline for the Best MiniCPM Build prize.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
import requests
|
| 12 |
+
|
| 13 |
+
from poetic.interpretation import Interpretation, build_interpret_contents, parse_interpretation
|
| 14 |
+
from poetic.prompts import INTERPRET_SYSTEM_PROMPT
|
| 15 |
+
|
| 16 |
+
DEFAULT_BASE_URL = "http://35.203.155.71:8001"
|
| 17 |
+
DEFAULT_MODEL = "MiniCPM4.1-8B"
|
| 18 |
+
|
| 19 |
+
# MiniCPM4.1 is a reasoning model; strip any <think>…</think> the server still emits.
|
| 20 |
+
_THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class Interpreter:
|
| 24 |
+
def interpret(self, poem: str, correction: str | None = None) -> Interpretation:
|
| 25 |
+
base = os.environ.get("MINICPM_BASE_URL", DEFAULT_BASE_URL).rstrip("/")
|
| 26 |
+
key = os.environ.get("OPENBMB_API_KEY") or os.environ.get("MINICPM_API_KEY")
|
| 27 |
+
response = requests.post(
|
| 28 |
+
f"{base}/v1/chat/completions",
|
| 29 |
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
| 30 |
+
json={
|
| 31 |
+
"model": os.environ.get("MINICPM_MODEL", DEFAULT_MODEL),
|
| 32 |
+
"messages": [
|
| 33 |
+
{"role": "system", "content": INTERPRET_SYSTEM_PROMPT},
|
| 34 |
+
{"role": "user", "content": build_interpret_contents(poem, correction)},
|
| 35 |
+
],
|
| 36 |
+
"max_tokens": 1024,
|
| 37 |
+
"temperature": 0.7,
|
| 38 |
+
# Disable chain-of-thought so the reply is clean JSON.
|
| 39 |
+
"chat_template_kwargs": {"enable_thinking": False},
|
| 40 |
+
},
|
| 41 |
+
timeout=float(os.environ.get("MINICPM_TIMEOUT", "60")),
|
| 42 |
+
)
|
| 43 |
+
response.raise_for_status()
|
| 44 |
+
content = response.json()["choices"][0]["message"]["content"]
|
| 45 |
+
return parse_interpretation(_THINK_RE.sub("", content).strip())
|
poetic/providers/modal_gpu.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Heavy pipelines (paint / sing / animate) on Modal serverless GPU.
|
| 2 |
+
|
| 3 |
+
Modal is a hackathon sponsor (Best Use of Modal). Running FLUX.2 Klein,
|
| 4 |
+
ACE-Step and Wan2.2 as Modal functions removes the ZeroGPU daily-quota ceiling
|
| 5 |
+
and keeps the Space itself GPU-free — the Space only needs to call out.
|
| 6 |
+
|
| 7 |
+
The Modal app (deployed separately via `modal deploy modal_app.py`) exposes
|
| 8 |
+
three web endpoints; these adapters POST to them. Configure with:
|
| 9 |
+
MODAL_PAINT_URL / MODAL_SING_URL / MODAL_ANIMATE_URL (+ MODAL_TOKEN optional)
|
| 10 |
+
|
| 11 |
+
NOTE: scaffolding for the approved compute-swap. The Modal app + live wiring is
|
| 12 |
+
the next build step; these adapters fail loudly until the URLs are set.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import base64
|
| 16 |
+
import io
|
| 17 |
+
import os
|
| 18 |
+
import tempfile
|
| 19 |
+
|
| 20 |
+
import requests
|
| 21 |
+
from PIL import Image
|
| 22 |
+
|
| 23 |
+
_TIMEOUT = float(os.environ.get("MODAL_TIMEOUT", "600"))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _endpoint(env_var: str) -> str:
|
| 27 |
+
url = os.environ.get(env_var)
|
| 28 |
+
if not url:
|
| 29 |
+
raise RuntimeError(
|
| 30 |
+
f"{env_var} is not set — deploy the Modal app and set its endpoint URLs. "
|
| 31 |
+
"Until then use a different provider (e.g. PAINT_PROVIDER=api)."
|
| 32 |
+
)
|
| 33 |
+
return url
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _headers() -> dict:
|
| 37 |
+
token = os.environ.get("MODAL_TOKEN")
|
| 38 |
+
return {"Authorization": f"Bearer {token}"} if token else {}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Painter:
|
| 42 |
+
def paint(self, brief: str) -> Image.Image:
|
| 43 |
+
resp = requests.post(
|
| 44 |
+
_endpoint("MODAL_PAINT_URL"),
|
| 45 |
+
headers=_headers(),
|
| 46 |
+
json={"brief": brief, "width": 768, "height": 1024},
|
| 47 |
+
timeout=_TIMEOUT,
|
| 48 |
+
)
|
| 49 |
+
resp.raise_for_status()
|
| 50 |
+
return Image.open(io.BytesIO(resp.content)).convert("RGB")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Singer:
|
| 54 |
+
def sing(self, tags: str, lyrics: str, duration: float = 40.0) -> str:
|
| 55 |
+
resp = requests.post(
|
| 56 |
+
_endpoint("MODAL_SING_URL"),
|
| 57 |
+
headers=_headers(),
|
| 58 |
+
json={"tags": tags, "lyrics": lyrics, "duration": duration},
|
| 59 |
+
timeout=_TIMEOUT,
|
| 60 |
+
)
|
| 61 |
+
resp.raise_for_status()
|
| 62 |
+
out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
| 63 |
+
out.write(resp.content)
|
| 64 |
+
out.close()
|
| 65 |
+
return out.name
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class Animator:
|
| 69 |
+
def animate(self, image: Image.Image, brief: str) -> str:
|
| 70 |
+
buf = io.BytesIO()
|
| 71 |
+
image.convert("RGB").save(buf, format="PNG")
|
| 72 |
+
resp = requests.post(
|
| 73 |
+
_endpoint("MODAL_ANIMATE_URL"),
|
| 74 |
+
headers=_headers(),
|
| 75 |
+
json={
|
| 76 |
+
"brief": brief,
|
| 77 |
+
"image_b64": base64.b64encode(buf.getvalue()).decode(),
|
| 78 |
+
},
|
| 79 |
+
timeout=_TIMEOUT,
|
| 80 |
+
)
|
| 81 |
+
resp.raise_for_status()
|
| 82 |
+
out = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
| 83 |
+
out.write(resp.content)
|
| 84 |
+
out.close()
|
| 85 |
+
return out.name
|