Downloads · 30 days
9
16% of all-time downloads
RamsesCamas/TinyOmni-ES-0.8B
TinyOmni-ES-0.8B is a image-text-to-text model from RamsesCamas. Use it for the image-text-to-text task on the model card, and read the license before you ship it in a product. It is set up for transformers. The card lists the license as apache-2.0.
Un modelo multimodal unificado de 0.8B parámetros que procesa audio, imagen, video y texto en español a través de un solo transformer.
Downloads · 30 days
9
16% of all-time downloads
All-time downloads
55
Public
Parameters
853M
1.7 GB on disk
Likes
0
Public
Click a slice to open those files.
.safetensors1.7 GB · 98%
From the Hugging Face model README
Un modelo multimodal unificado de 0.8B parámetros que procesa audio, imagen, video y texto en español a través de un solo transformer.
TinyOmni-ES combina Qwen3.5-0.8B (vision-language model con arquitectura híbrida DeltaNet+Attention) con un AudioProjector entrenado sobre Whisper-small, inyectando tokens de audio via masked_scatter — el mismo mecanismo que Qwen3.5 usa nativamente para imagen y video. Todas las modalidades fluyen por las mismas capas de atención, habilitando verdadera fusión cross-modal.
Audio WAV → Whisper-small encoder → AudioProjector (768→1024) → <|audio_pad|> tokens ─┐
Imagen → ViT nativo de Qwen3.5 ─────────────────────────── → <|image_pad|> tokens ──┤
Video → ViT nativo de Qwen3.5 ─────────────────────────── → <|video_pad|> tokens ──┤
Texto → Embedding layer ────────────────────────────────── → text tokens ───────────┘
↓
Qwen3.5 language model (24 capas)
20 DeltaNet (linear attention) + 4 full attention
↓
Texto generado
Componentes:
Qwen3_5ForConditionalGeneration) — 853M params| Modalidad | Métrica | TinyOmni-ES | Qwen3.5 Vanilla | Delta |
|---|---|---|---|---|
| Audio | WER (Common Voice ES) | 21.24% | N/A | — |
| Vision | Keyword Overlap (XM3600) | 0.279 | 0.071 | +295% |
| Vision | BERTScore F1 (XM3600) | 0.781 | 0.624 | +25% |
| Texto | Calidad factual | Correcta | Alucina | Corregido |
Entrenamiento:
from transformers import Qwen3_5ForConditionalGeneration, AutoProcessor
from PIL import Image
import torch
model = Qwen3_5ForConditionalGeneration.from_pretrained(
"RamsesCamas/TinyOmni-ES-0.8B",
torch_dtype=torch.bfloat16,
device_map="auto",
)
processor = AutoProcessor.from_pretrained("RamsesCamas/TinyOmni-ES-0.8B")
image = Image.open("foto.jpg").convert("RGB")
messages = [{"role": "user", "content": [
{"type": "image", "image": image},
{"type": "text", "text": "Describe esta imagen en español."},
]}]
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = processor(text=[text], images=[image], return_tensors="pt").to(model.device)
output = model.generate(**inputs, max_new_tokens=200, do_sample=False)
response = processor.decode(output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
print(response)
# → "Calle de construcción con tierra y piedra en el patio de un edificio"
El audio requiere cargar el AudioProjector por separado:
import torch
from transformers import Qwen3_5ForConditionalGeneration, AutoProcessor, WhisperModel, WhisperProcessor
from huggingface_hub import hf_hub_download
# Cargar modelo principal
model = Qwen3_5ForConditionalGeneration.from_pretrained(
"RamsesCamas/TinyOmni-ES-0.8B", torch_dtype=torch.bfloat16, device_map="auto")
processor = AutoProcessor.from_pretrained("RamsesCamas/TinyOmni-ES-0.8B")
tokenizer = processor.tokenizer
# Cargar Whisper encoder
whisper = WhisperModel.from_pretrained("openai/whisper-small", torch_dtype=torch.float16).encoder
whisper = whisper.to(model.device).eval()
whisper_proc = WhisperProcessor.from_pretrained("openai/whisper-small")
# Cargar AudioProjector
# Descargar audio_projector.py y audio_projector.pt del repo
proj_code = hf_hub_download("RamsesCamas/TinyOmni-ES-0.8B", "audio_projector.py")
proj_weights = hf_hub_download("RamsesCamas/TinyOmni-ES-0.8B", "audio_projector.pt")
import importlib.util
spec = importlib.util.spec_from_file_location("audio_projector", proj_code)
ap_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ap_module)
projector = ap_module.AudioProjector(whisper_dim=768, qwen_dim=1024, hidden_dim=2048, dropout=0.1)
state = torch.load(proj_weights, map_location="cpu", weights_only=True)
projector.load_state_dict(state, strict=False)
projector = projector.to(model.device).eval()
# Audio tokens
audio_pad_id = tokenizer.convert_tokens_to_ids("<|audio_pad|>")
audio_start_id = tokenizer.convert_tokens_to_ids("<|audio_start|>")
audio_end_id = tokenizer.convert_tokens_to_ids("<|audio_end|>")
# Cargar audio (16kHz mono)
import librosa
audio, sr = librosa.load("audio.mp3", sr=16000)
# Procesar
mel = whisper_proc(audio, sampling_rate=16000, return_tensors="pt").input_features.to(model.device)
with torch.no_grad():
features = whisper.forward(mel.half()).last_hidden_state
audio_embeds = projector(features.float())
# Construir input con audio tokens
num_tokens = audio_embeds.shape[1]
prompt = "Transcribe el siguiente audio en español:\n"
audio_ids = [audio_start_id] + [audio_pad_id] * num_tokens + [audio_end_id]
prompt_ids = tokenizer(prompt, add_special_tokens=False).input_ids
input_ids = torch.tensor([audio_ids + prompt_ids], device=model.device)
# Inyectar audio embeddings
embed_layer = model.model.get_input_embeddings()
inputs_embeds = embed_layer(input_ids)
audio_mask = (input_ids == audio_pad_id).unsqueeze(-1).expand_as(inputs_embeds)
audio_flat = audio_embeds.reshape(-1, audio_embeds.shape[-1]).to(inputs_embeds.dtype)
inputs_embeds = inputs_embeds.masked_scatter(audio_mask, audio_flat)
# Generar
with torch.no_grad():
output = model.generate(inputs_embeds=inputs_embeds, max_new_tokens=128, do_sample=False)
print(tokenizer.decode(output[0], skip_special_tokens=True))
# → "Posteriormente, Barrett fue reemplazado por Diego Suárez."
from transformers import Qwen3_5ForConditionalGeneration, AutoProcessor
import torch
model = Qwen3_5ForConditionalGeneration.from_pretrained(
"RamsesCamas/TinyOmni-ES-0.8B", torch_dtype=torch.bfloat16, device_map="auto")
processor = AutoProcessor.from_pretrained("RamsesCamas/TinyOmni-ES-0.8B")
messages = [{"role": "user", "content": [
{"type": "text", "text": "¿Cuál es la capital de Francia?"},
]}]
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = processor.tokenizer(text, return_tensors="pt").to(model.device)
output = model.generate(**inputs, max_new_tokens=100, do_sample=False)
generated = output[0][inputs["input_ids"].shape[1]:]
print(processor.tokenizer.decode(generated, skip_special_tokens=True))
# → "La capital de Francia es la París."
Qwen3_5ForConditionalGenerationmasked_scatter en <|audio_pad|> — mismo patrón que imagen/video nativos de Qwen3.5<|audio_start|> 248070, <|audio_pad|> 248076, <|audio_end|> 248071) — el modelo fue diseñado para soportar audiomasked_scatter unifica todas las modalidades — audio, imagen y video se inyectan con el mismo mecanismo en el espacio de embeddingsrope_deltas: Qwen3.5 VL cachea rope_deltas entre forward passes. Cuando TRL cambia batch size (generation vs logprob), produce tensor mismatch. Fix: register_forward_pre_hook que resetea rope_deltas=None<think>/<answer> no aprendido — GRPO no puede descubrir formatos que el modelo base nunca generaopenai/whisper-small, librosa@misc{tinyomni-es-2026,
title={TinyOmni-ES: Unified Multimodal Encoder for Spanish Audio-Visual-Text Processing},
author={Ramses Camas},
year={2026},
url={https://huggingface.co/RamsesCamas/TinyOmni-ES-0.8B}
}
Apache 2.0 (misma que Qwen3.5-0.8B)