austinsd commited on
Commit
d6b7165
·
verified ·
1 Parent(s): dd96d96

Add handler.py

Browse files
Files changed (1) hide show
  1. handler.py +121 -0
handler.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HuggingFace Inference Endpoints — Custom Handler for MIDISim
3
+ -------------------------------------------------------------
4
+ Contract:
5
+ __init__(path: str) — called once at container startup; path = repo root
6
+ __call__(data: dict) — called per request
7
+
8
+ Request payload (JSON):
9
+ { "inputs": "<base64-encoded MIDI bytes>" }
10
+
11
+ Response:
12
+ [float, float, ...] — 512-dim embedding vector (JSON array)
13
+
14
+ The endpoint is registered as pipeline_tag: "feature-extraction" so the
15
+ standard HF Inference Endpoints routing sends POST / with {"inputs": ...}.
16
+ """
17
+
18
+ import base64
19
+ import logging
20
+ import os
21
+ import tempfile
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import numpy as np
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ MODEL_FILENAME = (
30
+ "midisim_small_pre_trained_model_2_epochs_43117_steps_0.3148_loss_0.9229_acc.pth"
31
+ )
32
+ MAX_SEQ_LEN = int(os.getenv("MIDISIM_MAX_SEQ_LEN", "1024"))
33
+
34
+
35
+ class EndpointHandler:
36
+ def __init__(self, path: str = ""):
37
+ """
38
+ Load the MIDISim model once at container startup.
39
+ `path` is the local directory where the repo files were downloaded.
40
+ """
41
+ import torch
42
+ import midisim as ms
43
+
44
+ self.ms = ms
45
+ repo_dir = Path(path) if path else Path(".")
46
+
47
+ # Model checkpoint is in the repo root
48
+ model_path = repo_dir / MODEL_FILENAME
49
+ if not model_path.exists():
50
+ logger.info(f"Downloading model checkpoint from HF Hub...")
51
+ model_path = ms.download_model(
52
+ filename=MODEL_FILENAME,
53
+ local_dir=str(repo_dir),
54
+ verbose=False,
55
+ )
56
+
57
+ device = "cuda" if torch.cuda.is_available() else "cpu"
58
+ logger.info(f"Loading MIDISim model on {device} ...")
59
+ self.model, self.ctx, self.dtype = ms.load_model(
60
+ model_path=str(model_path),
61
+ depth=8,
62
+ device=device,
63
+ verbose=False,
64
+ )
65
+ logger.info("MIDISim model ready.")
66
+
67
+ def __call__(self, data: dict) -> Any:
68
+ """
69
+ Parameters
70
+ ----------
71
+ data : dict
72
+ Must contain key "inputs" with a base64-encoded MIDI file string.
73
+
74
+ Returns
75
+ -------
76
+ list[float] — 512-dimensional embedding vector, or {"error": str} on failure.
77
+ """
78
+ midi_b64 = data.get("inputs", "")
79
+ if not midi_b64:
80
+ return {"error": "No 'inputs' field in request body"}
81
+
82
+ tmp_path = None
83
+ try:
84
+ midi_bytes = base64.b64decode(midi_b64)
85
+ with tempfile.NamedTemporaryFile(suffix=".mid", delete=False) as f:
86
+ f.write(midi_bytes)
87
+ tmp_path = f.name
88
+
89
+ # Tokenise — transpose_factor=0 → single sequence (no augmentation)
90
+ token_seqs = self.ms.midi_to_tokens(
91
+ tmp_path,
92
+ max_seq_len=MAX_SEQ_LEN,
93
+ transpose_factor=0,
94
+ verbose=False,
95
+ )
96
+ if not token_seqs:
97
+ return {"error": "midi_to_tokens returned empty sequence"}
98
+
99
+ # Encode → shape (1, 512) as numpy float32
100
+ emb = self.ms.get_embeddings_bf16(
101
+ self.model,
102
+ token_seqs,
103
+ seq_len=MAX_SEQ_LEN,
104
+ verbose=False,
105
+ show_progress_bar=False,
106
+ return_numpy=True,
107
+ )
108
+
109
+ vec = emb[0].astype(np.float32).tolist()
110
+ logger.info(f"Embedding: dim={len(vec)}, norm={np.linalg.norm(emb[0]):.4f}")
111
+ return vec
112
+
113
+ except Exception as e:
114
+ logger.error(f"Handler error: {e}")
115
+ return {"error": str(e)}
116
+
117
+ finally:
118
+ if tmp_path and os.path.exists(tmp_path):
119
+ os.remove(tmp_path)
120
+
121
+ # Made with Bob