| import base64 |
| import io |
| import os |
| import struct |
| import wave |
|
|
| |
| 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=""): |
| |
| |
| |
| |
| device_override = os.getenv("MAYA_DEVICE", "auto") |
| dtype_override = os.getenv("MAYA_TORCH_DTYPE", "bf16") |
| use_fake = os.getenv("MAYA_USE_FAKE", "0") == "1" |
|
|
| |
|
|
| if use_fake: |
| |
| self.model = None |
| self.tokenizer = None |
| |
| self.device = "cpu" |
| self.snac = None |
| |
| self.sf = None |
| return |
|
|
| |
| 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`." |
| ) |
|
|
| |
| torch_dtype = torch.bfloat16 if dtype_override == "bf16" else torch.float32 |
|
|
| |
| |
| device_map_arg = "auto" |
| if device_override == "cpu": |
| device_map_arg = "cpu" |
|
|
| |
| |
| model_id = os.getenv("MAYA_MODEL_ID", "") |
|
|
| |
| local_path = path if path else "/repository" |
|
|
| |
| |
| 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 = "" |
|
|
| if model_id: |
| |
| model_path = model_id |
| print(f"Loading Maya1 model from HuggingFace Hub: {model_id}") |
| else: |
| |
| model_path = local_path |
|
|
| |
| 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" |
| ) |
|
|
| |
| try: |
| self.device = next(self.model.parameters()).device |
| except StopIteration: |
| |
| self.device = torch.device("cpu") |
|
|
| |
| 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 |
| } |
| """ |
| |
| inputs = data.get("inputs", "") |
|
|
| |
| description = data.get("description", "") |
| generation_args = data.get("generation_args", {}) |
|
|
| |
| if isinstance(inputs, dict): |
| |
| text = inputs.get("text", "") |
| description = inputs.get( |
| "description", description |
| ) |
| elif isinstance(inputs, str): |
| |
| text = inputs |
| else: |
| |
| text = str(inputs) if inputs else "" |
|
|
| if not text: |
| return {"error": f"No text provided. Received: {data}"} |
| |
| |
| if not description: |
| description = "Realistic male voice in the 30s age with american accent. Normal pitch, warm timbre, conversational pacing." |
|
|
| |
| if getattr(self, "snac", None) is None and getattr(self, "model", None) is None: |
| |
| sr = 24000 |
| duration = 1.0 |
| n_samples = int(sr * duration) |
| freq = 220.0 |
| |
| 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) |
| wf.setframerate(sr) |
| |
| 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"}] |
|
|
| |
| prompt = self.build_prompt(description, text) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| with torch.inference_mode(): |
| outputs = self.model.generate(**tokenizer_inputs, **default_gen_args) |
|
|
| |
| generated_ids = outputs[0, tokenizer_inputs["input_ids"].shape[1]:].tolist() |
|
|
| |
| snac_tokens = self.extract_snac_codes(generated_ids) |
| |
| if len(snac_tokens) < 7: |
| return {"error": f"Not enough SNAC tokens generated: {len(snac_tokens)}"} |
|
|
| |
| levels = self.unpack_snac_from_7(snac_tokens) |
| |
| |
| codes_tensor = [ |
| torch.tensor(level, dtype=torch.long, device=self.device).unsqueeze(0) |
| for level in levels |
| ] |
|
|
| |
| with torch.inference_mode(): |
| z_q = self.snac.quantizer.from_codes(codes_tensor) |
| audio = self.snac.decoder(z_q)[0, 0].cpu().numpy() |
|
|
| |
| if len(audio) > 2048: |
| audio = audio[2048:] |
|
|
| |
| buf = io.BytesIO() |
| self.sf.write(buf, audio, 24000, format="WAV") |
| wav_bytes = buf.getvalue() |
| b64 = base64.b64encode(wav_bytes).decode("utf-8") |
|
|
| |
| |
| return [{"blob": b64, "content-type": "audio/wav"}] |
| |
| |
| |
| _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 |
| |
| if isinstance(payload, (list, tuple)) and len(payload) > 0: |
| data = payload[0] |
|
|
| |
| if isinstance(data, str): |
| try: |
| import json |
|
|
| data = json.loads(data) |
| except Exception: |
| |
| data = {"text": data} |
|
|
| if not isinstance(data, dict): |
| |
| 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) |
|
|