Any-to-Any
Transformers
Safetensors
English
gemma4
image-text-to-text
decisions
typed-decisions
audio
vision
calibration
on-device
smaug
abacusai
Instructions to use abacusai/Smaug-Decisions with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use abacusai/Smaug-Decisions with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("abacusai/Smaug-Decisions") model = AutoModelForMultimodalLM.from_pretrained("abacusai/Smaug-Decisions", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download smaug_decide.py from abacusai/Smaug-Decisions: direct link, hf CLI and curl.
- Browser
- Download file 7.07 kB
-
https://huggingface.co/abacusai/Smaug-Decisions/resolve/main/smaug_decide.py
- Command line
-
hf download hf://abacusai/Smaug-Decisions/smaug_decide.py
-
curl -L -o smaug_decide.py https://huggingface.co/abacusai/Smaug-Decisions/resolve/main/smaug_decide.py
7.07 kB
| """Calibrated typed decisions over audio, images or text by Abacus.AI: one forward pass, a probability per option, no generated text. | |
| from smaug_decide import SmaugDecide | |
| model = SmaugDecide.from_pretrained('abacusai/Smaug-Decisions') | |
| model('clip.wav', 'Has the speaker finished their turn?') # -> {'Yes': 0.93, 'No': 0.07} | |
| model('clip.wav', 'Which genre is playing?', ['rock', 'jazz', 'classical']) # -> one-answer: probabilities sum to 1 | |
| model('clip.wav', 'Which sounds are present?', ['speech', 'music', 'alarm'], multi=True) # -> select-all: one probability each | |
| model('photo.jpg', 'Is there a person in the picture?') # images and plain text work the same way | |
| model('The order never arrived.', 'How upset is the customer?', ['calm', 'annoyed', 'angry']) | |
| The readout is the model's own LM-head logits for the option letters A..T at the answer position (softcapped), softmax for one-answer | |
| questions and sigmoid(z - b) for select-all questions; `b` is the learned select-all threshold stored in readout.json / head.pt. | |
| Runs on NVIDIA GPUs (bfloat16), Apple Silicon (MPS) and CPU; the device is picked automatically. | |
| """ | |
| import json | |
| import os | |
| import numpy as np | |
| import soundfile as sf | |
| import torch | |
| from huggingface_hub import snapshot_download | |
| from PIL import Image | |
| from transformers import AutoModelForMultimodalLM, AutoProcessor | |
| LETTERS = 'ABCDEFGHIJKLMNOPQRST' | |
| SAMPLE_RATE, MAX_AUDIO_SECONDS = 16000, 30.0 # the model hears 16 kHz mono; longer clips keep their last 30 s | |
| MAX_TOKENS, AUDIO_TOKENS_PER_SECOND, IMAGE_TOKENS = 3072, 25, 560 # images at 560 soft tokens, as trained and evaluated | |
| PREAMBLE = ('You make decisions for apps. Read the rules, the context and the input, then answer the question by choosing ' | |
| 'from the lettered options.') | |
| IMAGE_SUFFIXES = ('.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp') | |
| AUDIO_SUFFIXES = ('.wav', '.flac', '.ogg', '.mp3', '.m4a', '.aiff', '.aif') | |
| WEIGHT_FILES = ['*.json', '*.jinja', 'model*.safetensors', 'head.pt'] # the checkpoint at the repo root | |
| def trim(s, n): | |
| if len(s) <= n: | |
| return s | |
| return s[:int(n * 0.7)] + ' ... ' + s[-int(n * 0.3):] | |
| def render(question, options, descriptions, multi, media_tag, rules, context, text_input, char_budget): | |
| """The training prompt, verbatim: preamble, optional rules / context, the input, the lettered options, the answer instruction.""" | |
| parts = [PREAMBLE] | |
| if rules: | |
| parts.append(f'Rules: {trim(rules, char_budget // 2)}') | |
| if context: | |
| parts.append(f'Context: {trim(context, char_budget)}') | |
| if media_tag: | |
| parts.append(f'Input: {media_tag}') | |
| elif text_input: | |
| parts.append(f'Input: {trim(text_input, char_budget)}') | |
| lines = [f'Question: {question}' + (' Select all that apply.' if multi else '')] | |
| for k, (opt, desc) in enumerate(zip(options, descriptions)): | |
| lines.append(f'{LETTERS[k]}) {opt}' + (f' — {desc}' if desc else '')) | |
| parts.append('\n'.join(lines)) | |
| parts.append('Answer with the letters of all options that apply.' if multi else 'Answer with the letter of the best option.') | |
| return '<bos><|turn>user\n' + '\n'.join(parts) + '<turn|>\n<|turn>model\n' | |
| def load_audio(x): | |
| """A path, or a (samples, sample_rate) pair, or a 1-D float array at 16 kHz -> mono float32 at 16 kHz, last 30 s.""" | |
| if isinstance(x, str): | |
| w, sr = sf.read(x, dtype='float32') | |
| elif isinstance(x, tuple): | |
| w, sr = x | |
| else: | |
| w, sr = x, SAMPLE_RATE | |
| w = np.asarray(w, dtype=np.float32) | |
| if w.ndim > 1: | |
| w = w.mean(1) | |
| if sr != SAMPLE_RATE: # linear resampling is enough for a reference script; use a proper resampler for production audio | |
| n = int(round(len(w) * SAMPLE_RATE / sr)) | |
| w = np.interp(np.linspace(0.0, len(w) - 1, n), np.arange(len(w)), w).astype(np.float32) | |
| return w[-int(MAX_AUDIO_SECONDS * SAMPLE_RATE):] | |
| class SmaugDecide: | |
| def __init__(self, model, processor, readout, device): | |
| self.model, self.proc, self.device = model.eval(), processor, device | |
| tok = processor.tokenizer | |
| self.letter_ids = torch.tensor([tok(L, add_special_tokens=False)['input_ids'][0] for L in LETTERS], device=device) | |
| self.cap, self.b = readout.get('final_logit_softcapping'), float(readout['multi_bias_b']) | |
| def from_pretrained(cls, name_or_path, device=None): | |
| path = name_or_path if os.path.isdir(name_or_path) else snapshot_download(name_or_path, allow_patterns=WEIGHT_FILES) | |
| device = device or ('cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu') | |
| model = AutoModelForMultimodalLM.from_pretrained(path, dtype=torch.bfloat16).to(device) | |
| readout = json.load(open(os.path.join(path, 'readout.json'))) | |
| return cls(model, AutoProcessor.from_pretrained(path), readout, device) | |
| def __call__(self, inp, question, options=('Yes', 'No'), descriptions=None, multi=False, rules=None, context=None): | |
| """inp: an audio path / (samples, sr) / 16 kHz array, an image path / PIL image, or a plain string. Returns {option: probability}.""" | |
| options = list(options) | |
| assert 2 <= len(options) <= len(LETTERS), f'2 to {len(LETTERS)} options' | |
| descriptions = list(descriptions) if descriptions else [None] * len(options) | |
| audio = image = text_input = None | |
| if isinstance(inp, Image.Image) or (isinstance(inp, str) and inp.lower().endswith(IMAGE_SUFFIXES)): | |
| image = (inp if isinstance(inp, Image.Image) else Image.open(inp)).convert('RGB') | |
| elif isinstance(inp, str) and not inp.lower().endswith(AUDIO_SUFFIXES) and not os.path.exists(inp): | |
| text_input = inp | |
| else: | |
| audio = load_audio(inp) | |
| media_tokens = len(audio) / SAMPLE_RATE * AUDIO_TOKENS_PER_SECOND + 2 if audio is not None else (IMAGE_TOKENS + 2 if image is not None else 0) | |
| tag = '<|audio|>' if audio is not None else '<|image|>' if image is not None else None | |
| budget = int(max(400, (MAX_TOKENS - media_tokens - 80 - 10 * len(options)) * 3.5)) | |
| text = render(question, options, descriptions, multi, tag, rules, context, text_input, budget) | |
| kw = {} | |
| if audio is not None: | |
| kw['audio'] = [audio] | |
| if image is not None: | |
| kw['images'] = [image]; kw['images_kwargs'] = {'max_soft_tokens': IMAGE_TOKENS} | |
| enc = self.proc(text=[text], return_tensors='pt', add_special_tokens=False, **kw).to(self.device) | |
| h = self.model.model(**enc, use_cache=False).last_hidden_state[0, -1].float() # hidden state at the answer position | |
| z = h @ self.model.lm_head.weight[self.letter_ids[:len(options)]].float().T # LM-head logits of the letters only | |
| if self.cap: | |
| z = torch.tanh(z / self.cap) * self.cap | |
| p = torch.sigmoid(z - self.b) if multi else torch.softmax(z, -1) | |
| return dict(zip(options, p.tolist())) | |