yooi commited on
Commit
c1c71f3
·
verified ·
1 Parent(s): 2e69373

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 (INTERPRET_PROVIDER / PAINT_PROVIDER / SING_PROVIDER / ANIMATE_PROVIDER)
5
- 2. POETIC_MODE bundle ("local" = on-Space ZeroGPU pipelines, "api" = remote inference)
6
- 3. Default mode: "local" when running on ZeroGPU, else "api".
 
 
 
 
 
 
 
 
 
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 ("local", "api"):
16
  return mode
17
- return "local" if os.environ.get("SPACES_ZERO_GPU") else "api"
 
 
 
 
 
 
 
 
 
18
 
19
 
20
  def _resolve(capability: str, env_var: str):
21
- name = os.environ.get(env_var) or resolve_mode()
22
- if name == "local":
23
- from poetic.providers import local_zerogpu as module
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 'local' or 'api'"
29
  )
30
- return getattr(module, capability)()
 
 
 
31
 
32
 
33
  @cache
34
  def get_interpreter():
35
- return _resolve("Interpreter", "INTERPRET_PROVIDER")
36
 
37
 
38
  @cache
39
  def get_painter():
40
- return _resolve("Painter", "PAINT_PROVIDER")
41
 
42
 
43
  @cache
44
  def get_singer():
45
- return _resolve("Singer", "SING_PROVIDER")
46
 
47
 
48
  @cache
49
  def get_animator():
50
- return _resolve("Animator", "ANIMATE_PROVIDER")
 
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