"""Reading Jais-ASR output: the leading `[...]` decision tag.
Every generation starts with a bracketed decision:
"[EG] النص هنا" -> a transcript, in Egyptian Arabic
"[msa] النص هنا" -> a transcript, in Modern Standard Arabic
"[IFM-NoTranscript]" -> a deliberate abstention: no transcribable speech
The rule is general: the transcript never begins with a bracketed span. Anything the
model writes inside leading `[...]` spans is decision metadata, routed to `.tag` (and
to the API's `reasoning` field), never into `.text`:
* a span whose body opens with 'IF' (any case) is the refusal branch: `.refused`
is True and `.text` is empty -- nothing after a refusal is a transcript.
* otherwise the first alphabetic component that case-folds onto a known tag sets
`.language`; a span naming no supported language leaves `.language` as None.
* no leading span -> `.tag` is None and `.text` is the whole output, untouched.
from postprocess import split_text
r = split_text(raw)
r.text # the transcript
r.language # canonical tag ("EG", "msa", ...) or None
r.refused # True when the model abstained
r.tag # the leading tag as emitted, e.g. "[EG]", or None
For well-formed output the exact token-id fast path (`split_token_ids`) gives the same
answer with no string work; the text rule is the authority otherwise.
This module is pure python: no torch, no transformers, no vllm at import time.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
__all__ = [
"LANGUAGE_TAGS",
"REFUSAL_TAG",
"Transcription",
"split_text",
"split_token_ids",
"classify_tag_body",
"stream_split",
"verify_against_tokenizer",
]
# ---------------------------------------------------------------------------------
# The decision inventory. Token ids are those of this repo's tokenizer;
# `verify_against_tokenizer` below re-derives every one of them.
# ---------------------------------------------------------------------------------
BRACKET_OPEN_ID = 60 # '['
BRACKET_CLOSE_ID = 62 # ']'
THINK_OPEN_ID = 250029 #
THINK_CLOSE_ID = 250030 # '', the last token of every rendered prompt
REFUSAL_TAG = "[IFM-NoTranscript]"
REFUSAL_TOKEN_IDS = (
60,
5126,
46,
131126,
195862,
62,
) # '[' 'IF' 'M' '-No' 'Transcript' ']'
#: decision-token id -> tag surface. 24 tags, all distinct.
LANGUAGE_TAG_IDS = {
226606: "msa", # Modern Standard Arabic
272: "en",
2075: "fr",
273: "es",
286: "it",
7646: "EG",
8048: "SA",
6101: "MA",
29354: "IQ",
23129: "JO",
20056: "SY",
27526: "QA",
64489: "DZ",
23852: "LB",
5594: "OM",
28472: "YE",
43852: "TN",
17604: "MR",
25893: "KW",
9373: "SD",
12933: "LY",
5402: "AE",
4295: "PS",
41905: "BH",
}
LANGUAGE_TAGS = tuple(LANGUAGE_TAG_IDS.values())
#: longest well-formed prefix, in tokens -- the id fast path never looks further
MAX_TAG_TOKENS = len(REFUSAL_TOKEN_IDS)
#: a decision span is SHORT. 64 chars is far beyond any real tag; anything longer
#: is not tag-shaped and stays in the transcript untouched.
TAG_SPAN_MAX_CHARS = 64
#: total leading-metadata budget (a few stacked spans at most)
TAG_ZONE_MAX_CHARS = 160
# one bracketed span (no newline inside; the first ']' closes it); used with .match(pos)
_SPAN_RE = re.compile(r"[ \t]*\[([^\]\n]{0,%d})\]" % TAG_SPAN_MAX_CHARS)
_CANONICAL = {t.lower(): t for t in LANGUAGE_TAGS}
@dataclass(frozen=True)
class Transcription:
"""What the model decided, and what it transcribed."""
text: str
language: str | None = None
refused: bool = False
tag: str | None = None
thinking: str = ""
truncated: bool = False
def __bool__(self) -> bool: # `if result:` is False on an abstention
return not self.refused and bool(self.text)
def has_prefilled_tag(prompt_ids):
"""Only a complete known [language] suffix counts as a prompt prefill."""
return (
len(prompt_ids) >= 3
and int(prompt_ids[-3]) == BRACKET_OPEN_ID
and int(prompt_ids[-2]) in LANGUAGE_TAG_IDS
and int(prompt_ids[-1]) == BRACKET_CLOSE_ID
)
def classify_tag_body(body: str):
"""(language | None, refused) for one span body. See the module contract."""
b = body.strip()
if b[:2].upper() == "IF":
return None, True
for comp in re.split(r"[^A-Za-z]+", b):
if comp and comp.lower() in _CANONICAL:
return _CANONICAL[comp.lower()], False
return None, False
def _leading_spans(text: str):
"""Parse the leading metadata zone -> (spans, refused, language, end_pos)."""
spans, refused, language, pos = [], False, None, 0
while pos <= TAG_ZONE_MAX_CHARS:
m = _SPAN_RE.match(text, pos)
if not m:
break
body = m.group(1)
spans.append(f"[{body}]")
lang, ref = classify_tag_body(body)
refused = refused or ref
if language is None and lang is not None:
language = lang
pos = m.end()
return spans, refused, language, pos
# ---------------------------------------------------------------------------------
# token-id path -- exact fast path for WELL-FORMED output
# ---------------------------------------------------------------------------------
def tag_span_len(token_ids) -> int:
"""Tokens spanned by a WELL-FORMED leading tag, else 0 (the text rule then decides)."""
ids = list(token_ids[:MAX_TAG_TOKENS])
if not ids or ids[0] != BRACKET_OPEN_ID:
return 0
if tuple(ids[: len(REFUSAL_TOKEN_IDS)]) == REFUSAL_TOKEN_IDS:
return len(REFUSAL_TOKEN_IDS)
if len(ids) >= 3 and ids[1] in LANGUAGE_TAG_IDS and ids[2] == BRACKET_CLOSE_ID:
return 3
return 0
def split_token_ids(token_ids, *, strip: bool = True):
"""-> (tag or None, refused, remaining_token_ids). Exact-form fast path only;
anything else returns (None, False, ids) and callers fall back to `split_text`."""
ids = list(token_ids)
n = tag_span_len(ids)
if n == 0:
return None, False, ids
if n == len(REFUSAL_TOKEN_IDS):
return REFUSAL_TAG, True, ([] if strip else ids)
return f"[{LANGUAGE_TAG_IDS[ids[1]]}]", False, (ids[n:] if strip else ids)
# ---------------------------------------------------------------------------------
# text path -- the authority
# ---------------------------------------------------------------------------------
def _split_think(text: str):
"""Generated-side think block (enable_thinking=True): the prompt opens
'', so the generation carries thought text up to the first
''. Without that marker this is a no-op."""
head = text.lstrip()
if head.startswith(""):
text = head[len("") :]
idx = text.find("")
if idx < 0:
return "", text
return text[:idx].strip(), text[idx + len("") :]
def split_text(text: str, *, strip: bool = True) -> Transcription:
"""Split a decoded generation per the general contract (module docstring)."""
thinking, text = _split_think(text)
spans, refused, language, pos = _leading_spans(text)
if not spans:
return Transcription(text=text, thinking=thinking)
tag = " ".join(spans)
if refused:
return Transcription(
text="" if strip else text, refused=True, tag=tag, thinking=thinking
)
rest = text[pos:].lstrip() if strip else text
return Transcription(text=rest, language=language, tag=tag, thinking=thinking)
def stream_split(text: str):
"""Streaming view of the contract over the text-so-far.
-> (reasoning_so_far, content_so_far, holding)
* while the head could still be inside the metadata zone (only whitespace yet, or
a short unclosed '[' span) the undecided tail is HELD (both strings frozen);
* a refusal-branch output puts its closed leading span(s) in reasoning; nothing
after a refusal is ever content;
* otherwise reasoning is the closed leading span(s), content starts at the first
non-metadata character. Both returned strings only ever grow (delta-safe).
"""
spans, refused, _lang, pos = _leading_spans(text)
if refused:
return text[:pos], "", False
rest = text[pos:]
head = rest.lstrip(" \t")
ws = len(rest) - len(head)
if not spans:
if head == "" and len(text) <= TAG_ZONE_MAX_CHARS:
return "", "", True # nothing decisive yet
if (
head.startswith("[")
and "]" not in head
and "\n" not in head
and len(head) <= TAG_SPAN_MAX_CHARS + 2
):
return "", "", True # a span may be forming
return "", text, False # no metadata: all content
if (
head.startswith("[")
and "]" not in head
and "\n" not in head
and len(head) <= TAG_SPAN_MAX_CHARS + 2
and pos + ws <= TAG_ZONE_MAX_CHARS
):
return text[:pos], "", True # another span may be forming
return text[:pos], text[pos + ws :], False
def verify_against_tokenizer(tokenizer) -> None:
"""Assert the id table matches this tokenizer. Cheap; call it in your tests."""
enc = lambda s: tokenizer(s, add_special_tokens=False).input_ids # noqa: E731
assert (
tuple(enc(REFUSAL_TAG)) == REFUSAL_TOKEN_IDS
), f"{REFUSAL_TAG} -> {enc(REFUSAL_TAG)}, expected {list(REFUSAL_TOKEN_IDS)}"
assert tuple(enc("")) == (
THINK_CLOSE_ID,
), f" -> {enc('')}, expected [{THINK_CLOSE_ID}]"
for tid, tag in LANGUAGE_TAG_IDS.items():
ids = enc(f"[{tag}] ")
assert (
ids[0] == BRACKET_OPEN_ID and ids[1] == tid
), f"[{tag}] -> {ids[:3]}, expected decision id {tid} at position 1"
assert len(set(LANGUAGE_TAG_IDS)) == 24
# =================================================================================
# vLLM integration -- metadata becomes `message.reasoning`, transcript is `content`
#
# vllm serve --trust-remote-code --reasoning-parser jais_asr_tag
#
# Registration happens on import inside a vLLM process; the vllm_plugin/ package in
# this repo does that automatically at server start.
# =================================================================================
try: # pragma: no cover - only meaningful inside a vLLM process
from vllm.reasoning.abs_reasoning_parsers import (
ReasoningParser,
ReasoningParserManager,
)
from vllm.entrypoints.openai.engine.protocol import DeltaMessage
@ReasoningParserManager.register_module("jais_asr_tag")
class JaisASRTagParser(ReasoningParser):
"""reasoning = the leading decision metadata; content = the transcript only.
Same general contract as split_text: any leading bracketed span(s) go to
`reasoning`; a refusal-branch output puts its leading tag in `reasoning`
(the same value as `.tag`) with empty content; no leading span -> empty
reasoning.
"""
def __init__(self, tokenizer, *args, **kwargs):
template = kwargs.get("chat_template_kwargs") or {}
self.language_prefill = template.get("language_prefill")
self._prefill_emitted = False
super().__init__(tokenizer, *args, **kwargs)
# ReasoningConfig auto-initialization tokenizes these at server start;
# they are the prompt's think-block delimiters, already closed in every
# rendered prompt, so the thinking-budget machinery stays inert.
@property
def reasoning_start_str(self) -> str:
return ""
@property
def reasoning_end_str(self) -> str:
return ""
def is_reasoning_end(self, input_ids) -> bool:
ids = list(input_ids)
# vLLM also calls this with PROMPT ids (seeding per-request state) and
# with prompt+output ids; every rendered prompt ends with ''
# (+ an optional prefill), so only what follows the LAST one is output.
for i in range(len(ids) - 1, -1, -1):
if ids[i] == THINK_CLOSE_ID:
ids = ids[i + 1 :]
break
else:
if ids and ids[0] == 0: # BOS-led, no anchor: a bare prompt
return False
text = self.model_tokenizer.decode(ids[:48])
_r, content, holding = stream_split(text)
return (not holding) and content != ""
def extract_content_ids(self, input_ids):
ids = list(input_ids)
text = self.model_tokenizer.decode(ids)
r = split_text(text)
if r.refused:
return []
if r.tag is None:
return ids
# the space after ']' merges into the first content token, so keep the
# boundary token: cut after the last prefix that fits INSIDE the tag zone
cut = len(text) - len(r.text)
keep = 0
for k in range(1, len(ids) + 1):
if len(self.model_tokenizer.decode(ids[:k])) > cut:
break
keep = k
return ids[keep:]
def extract_reasoning(self, model_output, request):
r = split_text(model_output)
tag = r.tag
if tag is None and not r.refused:
kw = getattr(request, "chat_template_kwargs", None) or {}
if kw.get("language_prefill"):
tag = f"[{kw['language_prefill']}]" # tag was prefilled into the prompt
reasoning = "\n".join(p for p in (r.thinking, tag or "") if p) or None
if r.refused:
return reasoning, ""
if tag is None and not r.thinking:
return None, model_output
return reasoning, r.text
def extract_reasoning_streaming(
self,
previous_text,
current_text,
delta_text,
previous_token_ids,
current_token_ids,
delta_token_ids,
):
pr, pc, _ = stream_split(previous_text)
cr, cc, _holding = stream_split(current_text)
dr = cr[len(pr) :] if cr.startswith(pr) else cr
dc = cc[len(pc) :] if cc.startswith(pc) else cc
if self.language_prefill and not self._prefill_emitted:
dr = f"[{self.language_prefill}]" + ("\n" + dr if dr else "")
self._prefill_emitted = True
if not dr and not dc:
return None
return DeltaMessage(reasoning=dr or None, content=dc or None)
__all__.append("JaisASRTagParser")
except (
Exception
) as exc: # vLLM absent, or an API shift -- the pure-python half still works
import importlib.util
if importlib.util.find_spec("vllm") is not None:
import logging
logging.getLogger(__name__).warning(
"jais_asr_tag reasoning parser NOT registered (%s: %s); "
"`--reasoning-parser jais_asr_tag` will fail with a KeyError",
type(exc).__name__,
exc,
)