DEV Community

Alain Airom (Ayrom)
Alain Airom (Ayrom)

Posted on

Building an End-to-End Multilingual Speech and LLM Pipeline with IBM Granite Speech 5.0-2B and Bob

Testing the latest granite πŸͺ¨ speech 5 capabilities on Apple Silicon.

Introduction

As speech-to-text workflows transition toward more deeply integrated architectures, developers need models that don't just transcribe audio mechanically, but understand context, intent, and translation across multiple languages natively. Enter IBM Granite Speech 5.0-2B (ibm-granite/granite-speech-5.0-2b)β€”a high-performance, open-weights model leveraging a two-pass design (a CTC speech encoder coupled directly with a Granite-4.0-1B LLM backbone). Released under the permissive Apache 2.0 licence, it delivers state-of-the-art multilingual Automatic Speech Recognition (ASR) and Speech Translation (AST) across English, French, German, Spanish, Portuguese, Italian, and Japanese.

In this guide, we will walk through how to construct a complete, production-ready end-to-end demonstration application. We'll use Bobβ€”our local developer workspace toolβ€”to orchestrate the project structure, and connect Granite Speech's powerful transcription and translation features directly to downstream LLM backends (such as Ollama, llama.cpp, or OpenAI) via a unified Gradio web UI.


IBM Granite Speech 5

Excerpt from Hugging Face model card

Granite Speech 5.0 2B is a compact and efficient speech-language model, specifically designed for multilingual automatic speech recognition (ASR) and bidirectional automatic speech translation (AST) for English, French, German, Spanish, Portuguese and Japanese.

The model was trained on 390,000 hours of audio from public corpora for ASR and AST as well as synthetic datasets tailored to support Japanese ASR, keyword-biased ASR and speech translation. Granite Speech 5.0 2B was trained by modality aligning an intermediate checkpoint of granite-4.0-1b-base to speech on publicly available open source corpora containing audio inputs and text targets. Compared to its predecessor granite-speech-4.1-2b, this model has the same parameter count and provides additional capabilities and improvements:

  • Higher transcription accuracy for English short-form ASR and increased robustness to noise, reverberation and distant microphone speech
  • Supports input language id prompting for multilingual ASR
  • Improved long-form and conversational speech translation
  • Increased keyword list size (up to 2000) for multilingual keyword-biased ASR and AST

The mode card page proposes several sample test codes and implementations runnable as is.

  • Sample code using Transformers;
  # Usage with transformers
  # pip install transformers torchaudio soundfile

  import torch
  import torchaudio
  from huggingface_hub import hf_hub_download
  from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor

  device = "cuda" if torch.cuda.is_available() else "cpu"

  model_name = "ibm-granite/granite-speech-5.0-2b"
  processor = AutoProcessor.from_pretrained(model_name)
  tokenizer = processor.tokenizer
  model = AutoModelForSpeechSeq2Seq.from_pretrained(
      model_name, device_map=device, torch_dtype=torch.bfloat16
  )

  # Load audio
  audio_path = hf_hub_download(repo_id=model_name, filename="multilingual_sample.wav")
  wav, sr = torchaudio.load(audio_path, normalize=True)
  assert wav.shape[0] == 1 and sr == 16000  # mono, 16kHz

  # Create text prompt
  user_prompt = "<|audio|>transcribe the speech with proper punctuation and capitalization."
  chat = [
      {"role": "user", "content": user_prompt},
  ]
  prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)

  # Run the processor + model
  model_inputs = processor(prompt, wav, device=device, return_tensors="pt").to(device)
  model_outputs = model.generate(
      **model_inputs, max_new_tokens=200, do_sample=False, num_beams=1
  )

  # Transformers includes the input IDs in the response
  num_input_tokens = model_inputs["input_ids"].shape[-1]
  new_tokens = model_outputs[0, num_input_tokens:].unsqueeze(0)
  output_text = tokenizer.batch_decode(
      new_tokens, add_special_tokens=False, skip_special_tokens=True
  )
  print(f"STT output = {output_text[0]}")

Enter fullscreen mode Exit fullscreen mode
  • Sample code using vLLM;

    # usage vllm
    # pip install vllm
    
    from transformers import AutoTokenizer
    from vllm import LLM, SamplingParams
    from vllm.assets.audio import AudioAsset
    
    model_id = "ibm-granite/granite-speech-5.0-2b"
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    
    def get_prompt(question: str, has_audio: bool):
        """Build the input prompt to send to vLLM."""
        if has_audio:
            question = f"<|audio|>{question}"
        chat = [
            {
                "role": "user",
                "content": question
            }
        ]
        return tokenizer.apply_chat_template(chat, tokenize=False)
    
    model = LLM(
        model=model_id,
        max_model_len=2048, # This may be needed for lower resource devices.
        limit_mm_per_prompt={"audio": 1},
    )
    
    question = "can you transcribe the speech into a written format?"
    prompt_with_audio = get_prompt(
        question=question,
        has_audio=True,
    )
    audio = AudioAsset("mary_had_lamb").audio_and_sample_rate
    
    inputs = {
        "prompt": prompt_with_audio,
        "multi_modal_data": {
            "audio": audio,
        }
    }
    
    outputs = model.generate(
        inputs,
        sampling_params=SamplingParams(
            temperature=0.0,
            max_tokens=64,
        ),
    )
    print(f"Audio Example - Question: {question}")
    print(f"Generated text: {outputs[0].outputs[0].text}")
    
    

Sample Application Features and Functions

The application implements the following features;

  • Speech Transcription: upload a recorded audio file or speak directly into your microphone. The system converts speech to accurate written text using IBM's Granite Speech 5.0-2B model β€” trained specifically for high-quality transcription in real-world conditions.

  • Multilingual Translation: spoken audio in English, French, German, Spanish, Portuguese, or Japanese is automatically translated into any other supported language. Ideal for international teams, multilingual content production, and cross-border customer support.

The example above is a sample translation from English to French. Other languages are available too, such as German for another test;

  Der Bezirksarzt nΓ€herte sich mit der GleichgΓΌltigkeit eines Mannes, der es gewohnt ist, die HΓ€lfte seiner Zeit unter den Toten zu verbringen. Er hob dann das ΓΌber das Gesicht gelegte Blatt und ΓΆffnete nur die Lippen.
Enter fullscreen mode Exit fullscreen mode
  • AI-Assisted Analysis: once a transcript is produced, it is forwarded to an AI language model with your custom instruction β€” "summarise this meeting," "extract all action items," "identify the customer's main concern." The AI response appears alongside the transcript.

  • One-Click Full Pipeline: the Full Pipeline tab combines transcription and AI analysis into a single button click. Provide audio and an instruction; the system handles everything end-to-end and saves a structured result file automatically.

  • Live Configuration: switch AI backends, change models, or adjust processing settings from within the interface β€” no restart required. Configurations take effect immediately, enabling rapid A/B comparison of different AI backends.

  • Hardware Intelligence: the system automatically detects the capabilities of the host machine β€” GPU type, available memory, CPU cores β€” and self-optimises its processing parameters accordingly, ensuring the best performance on every device.


Architecture

The entire pipeline is structured to ensure modularity, separating hardware capability detection, audio processing, configuration management, and multi-backend LLM interfacing.

Implementation

Project Layout & Core Setup

Project's file and organization:

granite-speech5-test/
β”œβ”€β”€ app.py                  # Gradio demo application (entry point)
β”œβ”€β”€ requirements.txt        # Python dependencies
β”œβ”€β”€ .env.example            # Environment variable template
β”œβ”€β”€ config/
β”‚   └── config.yaml         # Application configuration
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ speech_processor.py # Granite Speech 5.0-2B wrapper
β”‚   β”œβ”€β”€ hardware_detector.py# Hardware capability detection
β”‚   β”œβ”€β”€ llm_client.py       # Multi-backend LLM abstraction
β”‚   └── config_manager.py   # Configuration management
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ test_speech_processor.py
β”‚   β”œβ”€β”€ test_hardware_detector.py
β”‚   β”œβ”€β”€ test_llm_client.py
β”‚   └── test_config_manager.py
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ setup.sh            # One-time environment setup
β”‚   β”œβ”€β”€ start.sh            # Launch app in background
β”‚   β”œβ”€β”€ stop.sh             # Graceful shutdown
β”‚   └── cleanup.sh          # Remove artefacts / venv
β”œβ”€β”€ input/                  # Put audio files here
β”œβ”€β”€ output/                 # JSON results written here
└── Docs/
    β”œβ”€β”€ Quickstart.md
    └── Architecture.md
Enter fullscreen mode Exit fullscreen mode

Hardware Detection & Optimization Advisor

Before loading models, the application queries host resources to assign the optimal compute device (CUDA GPU, Apple Silicon MPS, or CPU) and precision dtype:

"""
Hardware capability detection and optimisation advisor.

Interrogates the host machine for CPU cores, RAM, GPU VRAM and Apple Silicon
presence, then returns a structured HardwareProfile and suggested model-loading
parameters.

No external heavy dependencies – falls back gracefully when optional packages
(torch, psutil) are unavailable so that the module can be imported during the
install phase.
"""

from __future__ import annotations

import logging
import os
import platform
import subprocess
import sys
from dataclasses import dataclass, field
from typing import Optional

...

# Optional torch import at module level so tests can patch it cleanly.
try:
    import torch  # type: ignore
except ImportError:
    torch = None  # type: ignore

logger = logging.getLogger(__name__)
Enter fullscreen mode Exit fullscreen mode

Core Speech Processing Wrapper

The core speech module (GraniteSpeechProcessor) handles raw audio files, normalizes inputs to 16 kHz mono waveforms, and executes inference using the proper prompt templates specified by the model card:

"""
IBM Granite Speech 5.0-2B β€” Core speech processing module.

Model reference: https://huggingface.co/ibm-granite/granite-speech-5.0-2b
Architecture: two-pass design (CTC speech encoder β†’ Granite-4.0-1B LLM backbone)
Supported tasks: ASR (transcription) and AST (translation) in 6 languages.

Usage constraints (Apache 2.0 licence, no additional restrictions):
- Audio must be 16 kHz mono; longer recordings should be split into ≀2-minute segments.
- Prompt tokens use the <|audio|> placeholder which the processor replaces with audio embeddings.

Performance guide
-----------------
On CPU (M-series or x86) a 2-second clip takes ~60-120 s with float32 because the
Granite-4.0-1B LLM backbone must run greedy-decode token-by-token.  To reach
real-time speed, one of the following is needed:

  1. CUDA GPU (any card β‰₯ 4 GB VRAM) β†’ bfloat16 β†’ 1-5Γ— real-time
  2. Apple Silicon with MPS           β†’ float16  β†’ ~5Γ— real-time
  3. INT-8 dynamic quantisation       β†’ ~2Γ— CPU speedup (built-in, no GPU)
  4. Reduce max_new_tokens            β†’ linear speedup (safe for short clips)
  5. ONNX runtime (WebGPU / CoreML)   β†’ best CPU/edge throughput

All optimisation flags are controlled via config/config.yaml [speech.perf].
"""

from __future__ import annotations

import logging
import time
from contextlib import contextmanager
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

import numpy as np
import torch

from src.speech_processor import GraniteSpeechProcessor, PerfConfig
from src.hardware_detector import detect_hardware

hw = detect_hardware()

# Initialise processor with performance profile
processor = GraniteSpeechProcessor(
    device=hw.recommended_device,
    perf=PerfConfig(int8_quantize=False, profile=True)
)
...
# Transcribe audio file
result = processor.transcribe("input/meeting.wav", language="en", verbatim=False)
print("Transcript:", result.text)
print(f"Real-Time Factor (RTFx): {result.realtime_factor:.1f}Γ—")

# Translate speech (e.g., French audio to English text)
translation = processor.translate("input/french_speech.wav", source_language="fr", target_language="en")
print("Translation:", translation.text)
Enter fullscreen mode Exit fullscreen mode

Downstream Processing with Multi-Backend LLMs

Once a transcript is extracted, you can pipe it directly into local or cloud LLM backends using a uniform interface:

"""
LLM backend abstraction layer.

Provides a unified LLMClient interface over multiple local and cloud backends:

  Local
  -----
  β€’ Ollama   – http://localhost:11434 (default)
  β€’ LM Studio – http://localhost:1234  (OpenAI-compatible endpoint)
  β€’ llama.cpp server – http://localhost:9931  (project rule: llama.cpp port)

  Cloud
  -----
  β€’ OpenAI   – https://api.openai.com/v1
  β€’ Anthropic – Claude via Messages API
  β€’ IBM watsonx.ai – https://us-south.ml.cloud.ibm.com (optional)

All backends expose the same .chat(messages) β†’ str interface so the
application layer never needs to know which backend is active.

Usage
-----
>>> from src.llm_client import build_llm_client
>>> llm = build_llm_client(backend="ollama", model="granite3.3:8b")
>>> llm.chat([{"role": "user", "content": "Summarise this transcript: …"}])
"""

from __future__ import annotations

import json
import logging
import time
import urllib.request  # imported at module level for testability
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Optional

logger = logging.getLogger(__name__)

...

# Build a local Ollama client instance pointing to Granite or Llama models
llm = build_llm_client(backend="ollama", model="granite4.2:latest")

response = llm.chat([
    {
        "role": "user", 
        "content": f"Transcript:\n{result.text}\n\nInstruction: Summarise the key action items concisely."
    }
])

print("LLM Reply:", response.text)
print(f"Latency: {response.latency_s:.2f}s | Tokens used: {response.input_tokens} β†’ {response.output_tokens}")
Enter fullscreen mode Exit fullscreen mode

End-to-End Application Process

The user interface combines transcription, translation, and LLM interaction tabs into a unified local control center (app.py).

"""
IBM Granite Speech 5.0-2B β€” End-to-End Demo Application (Gradio UI).

Tabs
────
1. Transcribe   – upload or record audio β†’ Granite Speech β†’ text output
2. Translate    – speech translation between supported language pairs
3. LLM Interact – send a transcript to the configured LLM backend
4. Config       – inspect and switch settings at runtime
5. Hardware     – display host hardware profile

Run
───
    python app.py
    # or via scripts/start.sh
"""

from __future__ import annotations

import json
import logging
import os
import time
import urllib.request
import urllib.error
from datetime import datetime
from pathlib import Path
from typing import Optional, Tuple

import gradio as gr

from src.config_manager import get_config, reload_config
from src.hardware_detector import detect_hardware
from src.llm_client import build_llm_client
from src.speech_processor import (
    GraniteSpeechProcessor,
    PerfConfig,
    SpeechResult,
    SUPPORTED_LANGUAGES,
)

logger = logging.getLogger(__name__)

...

# ── Entry point ───────────────────────────────────────────────────────────────

def main() -> None:
    cfg = get_config()

    # Ensure output directory exists
    Path(cfg.app.output_dir).mkdir(parents=True, exist_ok=True)

    # Warm up hardware detection
    logger.info("Detecting hardware …")
    hw = _get_hardware_profile()
    logger.info("\n%s", hw.summary_str())

    demo = build_ui()

    # Build SSL kwargs β€” when cert/key are present the app serves HTTPS,
    # which is required for the browser microphone API to work on localhost.
    ssl_kwargs: dict = {}
    root = Path(__file__).parent
    certfile = cfg.app.ssl_certfile or str(root / "certs" / "cert.pem")
    keyfile = cfg.app.ssl_keyfile or str(root / "certs" / "key.pem")
    if Path(certfile).exists() and Path(keyfile).exists():
        ssl_kwargs = {
            "ssl_certfile": certfile,
            "ssl_keyfile": keyfile,
            # ssl_verify=False tells Gradio's internal httpx health-check to
            # skip cert validation for its own loopback call β€” required when
            # using a self-signed certificate, otherwise Gradio crashes on
            # startup with CERTIFICATE_VERIFY_FAILED before the browser even
            # connects.
            "ssl_verify": False,
        }
        scheme = "https"
        logger.info("TLS enabled β€” serving over HTTPS (microphone will work in browser)")
    else:
        scheme = "http"
        logger.warning(
            "No TLS cert found at %s β€” microphone requires HTTPS.\n"
            "  Run: openssl req -x509 -newkey rsa:2048 -nodes "
            "-keyout certs/key.pem -out certs/cert.pem -days 3650 "
            '-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"',
            certfile,
        )

    logger.info("App URL: %s://localhost:%d", scheme, cfg.app.server_port)
    demo.launch(
        server_port=cfg.app.server_port,
        share=cfg.app.share,
        server_name="0.0.0.0",
        **ssl_kwargs,
    )


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Launch the app via your terminal and use the full pipeline.


Conclusion

By combining IBM Granite Speech 5.0-2B with a modular application architecture, developers gain an either a cloud based or an air-gappable environment, highly accurate pipeline capable of bridging audio input with advanced reasoning engines[cite: 5]. Whether you're targeting low-latency GPU edge nodes, running offline speech translation on Apple Silicon, or hooking local models into Ollama, this reference pattern gives you total data sovereignty and performance observability out of the box.

Thanks for reading πŸŽ™οΈ

Links

Top comments (0)