File size: 15,478 Bytes
b840c7e f58e011 b840c7e 8d36590 167fa74 8d36590 4be97a5 8d36590 8c740c9 4be97a5 8c740c9 167fa74 8c740c9 4be97a5 8d36590 8c740c9 4be97a5 8d36590 8c740c9 167fa74 8c740c9 8d36590 8c740c9 167fa74 8d36590 f672f51 8d36590 f672f51 8d36590 f672f51 8d36590 f672f51 8d36590 f672f51 b840c7e f58e011 b840c7e 8f5b2eb b840c7e 8f5b2eb b840c7e c5017ba b840c7e 8f5b2eb 6775e28 c5017ba 8f5b2eb 6775e28 c5017ba 8f5b2eb c5017ba 8f5b2eb af4112e 8f5b2eb 6775e28 c5017ba b840c7e 6775e28 f58e011 b840c7e 1e1da0c b840c7e f58e011 c5017ba f58e011 c5017ba f58e011 c5017ba f58e011 c5017ba f58e011 c5017ba f58e011 4be97a5 f58e011 b840c7e f58e011 b840c7e f58e011 b840c7e 1e1da0c b840c7e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | import base64
import io
import os
import struct
import wave
# SNAC token constants for Maya1
CODE_START_TOKEN_ID = 128257
CODE_END_TOKEN_ID = 128258
CODE_TOKEN_OFFSET = 128266
SNAC_MIN_ID = 128266
SNAC_MAX_ID = 156937
SNAC_TOKENS_PER_FRAME = 7
SOH_ID = 128259
EOH_ID = 128260
SOA_ID = 128261
BOS_ID = 128000
TEXT_EOT_ID = 128009
class EndpointHandler:
def __init__(self, path=""):
# allow overriding device and dtype via environment variables for local CPU testing
# `MAYA_DEVICE`: 'cpu' or 'auto' (default 'auto')
# `MAYA_TORCH_DTYPE`: 'bf16' or 'fp32' (default 'bf16')
# `MAYA_USE_FAKE`: if '1', use a tiny fake pipeline for smoke testing (no HF downloads)
device_override = os.getenv("MAYA_DEVICE", "auto")
dtype_override = os.getenv("MAYA_TORCH_DTYPE", "bf16")
use_fake = os.getenv("MAYA_USE_FAKE", "0") == "1"
# map string to torch dtype will be set after importing torch
if use_fake:
# Minimal fake components for quick local smoke tests. Generates a short sine tone.
self.model = None
self.tokenizer = None
# keep device as plain string for fake mode
self.device = "cpu"
self.snac = None
# no external libs required for fake mode
self.sf = None
return
# import heavy inference dependencies lazily so fake-mode doesn't require them
try:
import soundfile as sf
import torch
from snac import SNAC
from transformers import AutoModelForCausalLM, AutoTokenizer
except Exception as e:
raise RuntimeError(
f"Failed to import inference dependencies: {e}.\n"
"Install the packages listed in `inference_endpoints/requirements.txt`."
)
# map string to torch dtype
torch_dtype = torch.bfloat16 if dtype_override == "bf16" else torch.float32
# load Maya1 text-to-voice model
# force CPU device_map when requested to avoid trying to use GPUs
device_map_arg = "auto"
if device_override == "cpu":
device_map_arg = "cpu"
# Allow loading from HuggingFace Hub via environment variable
# MAYA_MODEL_ID: HuggingFace model ID (e.g., "maya-research/maya1" or similar Maya model)
model_id = os.getenv("MAYA_MODEL_ID", "")
# Determine model path: use env var if set, otherwise local path
local_path = path if path else "/repository"
# Check if trying to load from self (circular reference)
# Only block if it's exactly "binhqd/maya1" - other repos ending in /maya1 are fine
if model_id == "binhqd/maya1":
print(f"⚠️ MAYA_MODEL_ID is set to {model_id} which is this repository!")
print("This creates a circular reference. Checking local files instead...")
model_id = "" # Force local loading
if model_id:
# Load from HuggingFace Hub
model_path = model_id
print(f"Loading Maya1 model from HuggingFace Hub: {model_id}")
else:
# Load from local repository
model_path = local_path
# Check if this is a valid model directory
config_path = os.path.join(model_path, "config.json")
if not os.path.exists(config_path):
raise RuntimeError(
f"❌ Model configuration not found at: {config_path}\n\n"
f"The repository appears to be missing model weights and configuration.\n\n"
f"To fix this, you have 3 options:\n\n"
f"1. 🧪 TESTING MODE (Quick Start):\n"
f" Set: MAYA_USE_FAKE=1\n"
f" This generates test audio (sine wave) without requiring model files.\n\n"
f"2. 🌐 USE EXISTING MODEL FROM HUGGINGFACE:\n"
f" Set: MAYA_MODEL_ID=<actual-model-id>\n"
f" Example: MAYA_MODEL_ID=maya-research/maya1\n"
f" (Must be a valid HuggingFace model repository with weights)\n\n"
f"3. 📦 UPLOAD YOUR MODEL FILES:\n"
f" Upload these files to your repository (binhqd/maya1):\n"
f" - config.json\n"
f" - model.safetensors (or pytorch_model.bin)\n"
f" - tokenizer.json\n"
f" - tokenizer_config.json\n"
f" - generation_config.json (optional)\n\n"
f"Current path: {model_path}\n"
f"Files found: {os.listdir(model_path) if os.path.exists(model_path) else 'path does not exist'}\n\n"
f"⚠️ Note: MAYA_MODEL_ID=binhqd/maya1 won't work (this repository)\n"
f" because it doesn't contain model weights.\n"
)
try:
self.model = AutoModelForCausalLM.from_pretrained(
model_path, torch_dtype=torch_dtype, device_map=device_map_arg
)
self.tokenizer = AutoTokenizer.from_pretrained(model_path)
except Exception as e:
raise RuntimeError(
f"Failed to load Maya1 model from {model_path}.\n"
f"Error: {e}\n\n"
f"Please ensure:\n"
f"1. The model repository contains valid model files, OR\n"
f"2. Set MAYA_MODEL_ID to a valid HuggingFace model ID, OR\n"
f"3. Set MAYA_USE_FAKE=1 for testing without a real model"
)
# determine device from model parameters (safer than using `model.device`)
try:
self.device = next(self.model.parameters()).device
except StopIteration:
# fallback to CPU if model has no parameters
self.device = torch.device("cpu")
# load SNAC model (audio decoder) 24 kHz
self.snac = (
SNAC.from_pretrained("hubertsiuzdak/snac_24khz").eval().to(self.device)
)
self.sf = sf
def build_prompt(self, description: str, text: str) -> str:
"""Build formatted prompt for Maya1."""
soh_token = self.tokenizer.decode([SOH_ID])
eoh_token = self.tokenizer.decode([EOH_ID])
soa_token = self.tokenizer.decode([SOA_ID])
sos_token = self.tokenizer.decode([CODE_START_TOKEN_ID])
eot_token = self.tokenizer.decode([TEXT_EOT_ID])
bos_token = self.tokenizer.bos_token
formatted_text = f'<description="{description}"> {text}'
prompt = (
soh_token + bos_token + formatted_text + eot_token +
eoh_token + soa_token + sos_token
)
return prompt
def extract_snac_codes(self, token_ids: list) -> list:
"""Extract SNAC codes from generated tokens."""
try:
eos_idx = token_ids.index(CODE_END_TOKEN_ID)
except ValueError:
eos_idx = len(token_ids)
snac_codes = [
token_id for token_id in token_ids[:eos_idx]
if SNAC_MIN_ID <= token_id <= SNAC_MAX_ID
]
return snac_codes
def unpack_snac_from_7(self, snac_tokens: list) -> list:
"""Unpack 7-token SNAC frames to 3 hierarchical levels."""
if snac_tokens and snac_tokens[-1] == CODE_END_TOKEN_ID:
snac_tokens = snac_tokens[:-1]
frames = len(snac_tokens) // SNAC_TOKENS_PER_FRAME
snac_tokens = snac_tokens[:frames * SNAC_TOKENS_PER_FRAME]
if frames == 0:
return [[], [], []]
l1, l2, l3 = [], [], []
for i in range(frames):
slots = snac_tokens[i*7:(i+1)*7]
l1.append((slots[0] - CODE_TOKEN_OFFSET) % 4096)
l2.extend([
(slots[1] - CODE_TOKEN_OFFSET) % 4096,
(slots[4] - CODE_TOKEN_OFFSET) % 4096,
])
l3.extend([
(slots[2] - CODE_TOKEN_OFFSET) % 4096,
(slots[3] - CODE_TOKEN_OFFSET) % 4096,
(slots[5] - CODE_TOKEN_OFFSET) % 4096,
(slots[6] - CODE_TOKEN_OFFSET) % 4096,
])
return [l1, l2, l3]
def __call__(self, data):
"""
HF Endpoints format:
{
"inputs": "text to speak" or {"text": "...", "description": "..."},
"description": "... optional voice description ...",
"generation_args": { optional dict for text generation params }
}
Returns dict with base64 audio:
{
"audio_base64": "<base64-encoded WAV data>",
"sampling_rate": 24000
}
"""
# Extract inputs (HF always provides this key)
inputs = data.get("inputs", "")
# Get additional parameters from top level
description = data.get("description", "")
generation_args = data.get("generation_args", {})
# Parse inputs
if isinstance(inputs, dict):
# inputs is a dict with text and description
text = inputs.get("text", "")
description = inputs.get(
"description", description
) # override if in inputs
elif isinstance(inputs, str):
# inputs is just the text string
text = inputs
else:
# Try to convert to string
text = str(inputs) if inputs else ""
if not text:
return {"error": f"No text provided. Received: {data}"}
# Use default description if not provided
if not description:
description = "Realistic male voice in the 30s age with american accent. Normal pitch, warm timbre, conversational pacing."
# If running in fake mode (quick smoke test), synthesize a sine tone
if getattr(self, "snac", None) is None and getattr(self, "model", None) is None:
# generate a 1-second 24kHz sine wave and write a 16-bit WAV using stdlib
sr = 24000
duration = 1.0
n_samples = int(sr * duration)
freq = 220.0
# generate samples without numpy
waveform = [
int(
0.1
* 32767
* __import__("math").sin(
2 * __import__("math").pi * freq * (i / sr)
)
)
for i in range(n_samples)
]
buf = io.BytesIO()
with wave.open(buf, "wb") as wf:
wf.setnchannels(1)
wf.setsampwidth(2) # 16-bit
wf.setframerate(sr)
# pack samples as little-endian signed 16-bit
frames = struct.pack("<" + ("h" * len(waveform)), *waveform)
wf.writeframes(frames)
wav_bytes = buf.getvalue()
b64 = base64.b64encode(wav_bytes).decode("utf-8")
return [{"blob": b64, "content-type": "audio/wav"}]
# Build properly formatted prompt for Maya1
prompt = self.build_prompt(description, text)
# Tokenize
import torch
tokenizer_inputs = self.tokenizer(prompt, return_tensors="pt")
if torch.cuda.is_available():
tokenizer_inputs = {k: v.to(self.device) for k, v in tokenizer_inputs.items()}
else:
tokenizer_inputs = tokenizer_inputs.to(self.device)
# Set default generation args if not provided
default_gen_args = {
"max_new_tokens": 2048,
"min_new_tokens": 28,
"temperature": 0.4,
"top_p": 0.9,
"repetition_penalty": 1.1,
"do_sample": True,
"eos_token_id": CODE_END_TOKEN_ID,
"pad_token_id": self.tokenizer.pad_token_id,
}
default_gen_args.update(generation_args)
# Generate tokens
with torch.inference_mode():
outputs = self.model.generate(**tokenizer_inputs, **default_gen_args)
# Extract generated tokens (everything after the input prompt)
generated_ids = outputs[0, tokenizer_inputs["input_ids"].shape[1]:].tolist()
# Extract SNAC audio tokens
snac_tokens = self.extract_snac_codes(generated_ids)
if len(snac_tokens) < 7:
return {"error": f"Not enough SNAC tokens generated: {len(snac_tokens)}"}
# Unpack SNAC tokens to 3 hierarchical levels
levels = self.unpack_snac_from_7(snac_tokens)
# Convert to tensors
codes_tensor = [
torch.tensor(level, dtype=torch.long, device=self.device).unsqueeze(0)
for level in levels
]
# Generate final audio with SNAC decoder
with torch.inference_mode():
z_q = self.snac.quantizer.from_codes(codes_tensor)
audio = self.snac.decoder(z_q)[0, 0].cpu().numpy()
# Trim warmup samples (first 2048 samples)
if len(audio) > 2048:
audio = audio[2048:]
# Save audio to WAV
buf = io.BytesIO()
self.sf.write(buf, audio, 24000, format="WAV")
wav_bytes = buf.getvalue()
b64 = base64.b64encode(wav_bytes).decode("utf-8")
# Return in HuggingFace Inference Endpoints format
# The endpoint expects a list with blob data
return [{"blob": b64, "content-type": "audio/wav"}]
# Module-level convenience functions for hosting platforms (Hugging Face Endpoints)
# The platform typically expects top-level `init` and `predict` (or `run`) callables
# so we provide thin wrappers around the EndpointHandler class.
_HANDLER = None
def init(model_id: str = None):
"""Initialize the module-level handler.
model_id: optional path or model identifier to pass to EndpointHandler.
If omitted the handler will look for environment variables `HF_MODEL_ID` or
`MODEL_ID` and otherwise instantiate with a blank path (which may be
appropriate when the model files are bundled with the repo).
"""
global _HANDLER
if _HANDLER is not None:
return
model_path = model_id or os.getenv("HF_MODEL_ID") or os.getenv("MODEL_ID") or ""
_HANDLER = EndpointHandler(path=model_path)
def predict(payload):
"""Predict/predict wrapper for hosted endpoints.
Accepts a dict (recommended) or a JSON-ish payload. If a list is passed,
the first element is used. If a plain string is provided it is treated as
the `text` field.
Returns the same dict structure produced by EndpointHandler.__call__.
"""
global _HANDLER
if _HANDLER is None:
init()
data = payload
# handle list payloads (common in some platform wrappers)
if isinstance(payload, (list, tuple)) and len(payload) > 0:
data = payload[0]
# allow raw JSON string -> dict
if isinstance(data, str):
try:
import json
data = json.loads(data)
except Exception:
# treat plain string as the text to synthesize
data = {"text": data}
if not isinstance(data, dict):
# best-effort normalization
data = {"text": str(data)}
try:
return _HANDLER(data)
except Exception as e:
return {"error": str(e)}
def run(payload):
"""Alias for predict for platforms that expect `run` entrypoint."""
return predict(payload)
|