File size: 8,918 Bytes
47075fe
 
608d187
 
 
 
 
 
 
 
 
 
 
 
47075fe
608d187
 
 
22839f9
 
 
608d187
 
 
 
7add96f
 
 
 
 
 
 
 
608d187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7add96f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608d187
 
 
 
 
 
 
 
 
 
 
 
 
 
22839f9
 
 
 
 
 
 
 
608d187
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
---
license: apache-2.0
language:
- ja
base_model: Qwen/Qwen3-ASR-1.7B
pipeline_tag: automatic-speech-recognition
library_name: transformers
tags:
- automatic-speech-recognition
- japanese
- furigana
- ruby
- ctc
- qwen3-asr
---

# Ruby-ASR-1.7B: Japanese ASR with inline furigana + a CTC mora-reading head

πŸ“„ Paper: [arXiv:2609.27289](https://arxiv.org/abs/2609.27289) Β·
πŸ’» Code: [hshi-speech/Ruby-ASR-1.7B](https://github.com/hshi-speech/Ruby-ASR-1.7B)

Ruby-ASR-1.7B is a pair of Japanese fine-tunes of
[Qwen/Qwen3-ASR-1.7B](https://huggingface.co/Qwen/Qwen3-ASR-1.7B), shipped as
two variants in this repo:

| model | subfolder | style |
|---|---|---|
| **Ruby-ASR-sub** | `subtitle/` | subtitle-style transcription |
| **Ruby-ASR-ver** | `verbatim/` | verbatim-style transcription |

Use `subtitle` when prioritizing concise, readable orthographic transcription;
use `verbatim` when prioritizing script-aware and lexical-reading accuracy
(see [Evaluation](#evaluation)).

Both variants share the same architecture, mora vocab, and layout, and each
adds **two complementary outputs**:

1. **Ruby (furigana) transcription** β€” the standard Qwen3-ASR seq2seq decoder
   emits the transcript with inline bracket furigana:
   `δΈƒ[γͺγͺ]εΉ΄ι–“[ねんかん]でロスにも友達[ともだけ]γ§γγŸγ—γ€`
   Every annotated word carries its reading, so downstream text display
   (HTML `<ruby>`), reading-aware subtitling, and pronunciation-controlled TTS
   all get the reading for free.
2. **Mora reading (CTC)** β€” an auxiliary branch on the audio encoder
   (2-layer Transformer adapter + linear head, trained jointly with CTC loss)
   that outputs the **actual pronunciation** as kana morae directly from the
   encoder, without running the LLM decoder:
   `γƒŠγƒŠγƒγƒ³γ‚«γƒ³γƒ‡γƒ­γ‚Ήγƒ‹γƒ’γƒˆγƒ’γƒ€γƒγƒ‡γ‚­γ‚Ώγ‚·`

Each variant's sharded main weights are a **stock Qwen3-ASR checkpoint** (the
CTC branch is stored separately under `ctc/`), so Mode 2 works with unmodified
vLLM and the official [`qwen-asr`](https://github.com/QwenLM/Qwen3-ASR)
toolkit. Mode 1 is provided by the
[`ruby_asr`](https://github.com/hshi-speech/Ruby-ASR-1.7B) package.

## Repo layout

```
subtitle/  verbatim/                    (same layout in both)
β”œβ”€β”€ model-0000x-of-00003.safetensors    sharded Qwen3-ASR weights (CTC stripped) β€” Mode 2
β”œβ”€β”€ model.safetensors.index.json
β”œβ”€β”€ config.json, generation_config.json
β”œβ”€β”€ tokenizer.json, tokenizer_config.json, vocab.json, merges.txt,
β”‚   added_tokens.json, special_tokens_map.json, chat_template.json,
β”‚   preprocessor_config.json
└── ctc/
    β”œβ”€β”€ model.safetensors               the CTC branch (ctc_adapter.* + ctc_head.*) β€” Mode 1
    β”œβ”€β”€ config.json                     adapter hyper-params (transformer, 2 layers, nhead 8), blank id
    └── mora_vocab.json                 {mora: id}; id 0 = <blank>, ids 1-26 = a-z, then morae
```

## Mode 1 β€” mora reading via encoder + CTC

No LLM decoding: audio β†’ encoder frames β†’ CTC adapter β†’ head β†’ greedy collapse.
Fast, and reflects the *actual pronunciation* (e.g. numbers and kanji resolved
to what was said). Loads straight from this repo:

```python
# pip install git+https://github.com/hshi-speech/Ruby-ASR-1.7B.git
from ruby_asr import MoraCTCRecognizer

rec = MoraCTCRecognizer.from_pretrained("hshispeech/Ruby-ASR-1.7B",
                                        subfolder="subtitle")   # or "verbatim"
print(rec.transcribe("audio.wav")[0])
# γƒŠγƒŠγƒγƒ³γ‚«γƒ³γƒ‡γƒ­γ‚Ήγƒ‹γƒ’γƒˆγƒ’γƒ€γƒγƒ‡γ‚­γ‚Ώγ‚·
```

Output alphabet: katakana morae (`キョ`, `ッ`, `ン`, `γƒΌ` are single tokens)
plus `a`–`z` for embedded English. Long vowels are unified to `γƒΌ`
(`γƒˆγ‚¦γ‚­γƒ§γ‚¦` β†’ `γƒˆγƒΌγ‚­γƒ§γƒΌ`), matching the pyopenjtalk `g2p(kana=True)`
convention the training targets were built with.

## Mode 2 β€” ruby transcription (transformers)

Download the variant you need, then load it like stock Qwen3-ASR:

```python
# pip install qwen-asr
import torch
from huggingface_hub import snapshot_download
from qwen_asr import Qwen3ASRModel

root = snapshot_download("hshispeech/Ruby-ASR-1.7B", allow_patterns=["subtitle/*"])
model = Qwen3ASRModel.from_pretrained(f"{root}/subtitle",
                                      dtype=torch.bfloat16, device_map="cuda")
print(model.transcribe(audio="audio.wav")[0].text)
# δΈƒ[γͺγͺ]εΉ΄ι–“[ねんかん]でロスにも友達[ともだけ]γ§γγŸγ—γ€
```

Post-process the bracket output (surface / reading / HTML `<ruby>`) with the
`ruby_asr` helpers:

```python
from ruby_asr import to_surface, to_reading, to_html, brackets_to_parens
text = "δΈƒ[γͺγͺ]εΉ΄ι–“[ねんかん]でロスにも友達[ともだけ]γ§γγŸγ—γ€"
to_surface(text)   # δΈƒεΉ΄ι–“γ§γƒ­γ‚Ήγ«γ‚‚ε‹ι”γ§γγŸγ—γ€
to_reading(text)   # γͺγͺγ­γ‚“γ‹γ‚“γ§γƒ­γ‚Ήγ«γ‚‚γ¨γ‚‚γ γ‘γ§γγŸγ—γ€
to_html(brackets_to_parens(text))  # <ruby><rb>δΈƒ</rb><rt>γͺγͺ</rt></ruby>…
```

## Mode 2 β€” ruby transcription (vLLM)

```bash
pip install "qwen-asr[vllm]"     # or your own vLLM >= 0.14 install
hf download hshispeech/Ruby-ASR-1.7B --include "subtitle/*" --local-dir Ruby-ASR-1.7B
vllm serve Ruby-ASR-1.7B/subtitle --served-model-name ruby-asr --max-model-len 8192
```

```python
import base64
from openai import OpenAI

client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="EMPTY")
audio_b64 = base64.b64encode(open("audio.wav", "rb").read()).decode()
resp = client.chat.completions.create(
    model="ruby-asr",
    messages=[
        {"role": "system", "content": ""},
        {"role": "user", "content": [
            {"type": "input_audio",
             "input_audio": {"data": audio_b64, "format": "wav"}}]},
    ],
    temperature=0.0, max_tokens=512)
print(resp.choices[0].message.content)
# language Japanese<asr_text>δΈƒ[γͺγͺ]εΉ΄ι–“[ねんかん]でロスにも友達[ともだけ]γ§γγŸγ—γ€
```

> **Use `/v1/chat/completions`, not `/v1/audio/transcriptions`.** The furigana
> is the model's learned behaviour under the standard Qwen3-ASR chat prompt
> (empty system + audio). The transcriptions endpoint builds a different
> internal prompt and returns plain text without brackets.

## Training objective

Fine-tuned from Qwen3-ASR-1.7B with a joint loss `0.3 Β· CTC + 0.7 Β· CE`: the
standard cross-entropy over the seq2seq ruby transcript plus a CTC loss over
mora targets from the auxiliary encoder branch. The seq2seq path never sees
the CTC adapter, so ruby decoding is bit-identical to a checkpoint without
the branch.

## Evaluation

Character-count-weighted averages (W.Avg.) over five Japanese benchmarks β€”
B5K, CSJ, JSUT-Book, Common Voice 8, and TEDx β€” weighted by the number of
reference characters per test set. All values are percentages; lower is
better.

| Model           |  Raw CER |   SA-CER | Kana CER |
| --------------- | -------: | -------: | -------: |
| ReazonSpeech-k2 |    10.35 |     8.51 |     5.64 |
| Qwen3-ASR-1.7B  |    10.78 |     8.59 |     5.74 |
| Kana-Whisper    |      N/A |      N/A |     4.66 |
| Ruby-ASR-ver    |     9.10 | **6.59** | **3.75** |
| Ruby-ASR-sub    | **8.51** |     6.64 |     4.01 |

On this weighted aggregate, **Ruby-ASR-sub** achieves the best Raw CER
(8.51%, a 21.1% relative reduction over Qwen3-ASR), and **Ruby-ASR-ver** the
best SA-CER (6.59%) and Kana CER (3.75%) β€” relative reductions of 23.3% and
34.7% over Qwen3-ASR. Compared with Kana-Whisper, Ruby-ASR-ver reduces
weighted Kana CER from 4.66% to 3.75%, a 19.5% relative reduction.

Metrics:

- **Raw CER** β€” normalized orthographic transcription, with the original
  script preserved.
- **SA-CER** β€” script-aware: kana regions are scored by reading, kanji
  regions by orthography.
- **Kana CER** β€” lexical-reading transcription.
- Ruby-ASR and Kana-Whisper are scored on their direct reading outputs; the
  orthographic baselines are converted through the shared G2P pipeline.

## Limitations

- Japanese-focused fine-tune; other Qwen3-ASR languages were not part of
  training and quality on them is untested.
- Mode 1 output is pronunciation kana only β€” no kanji, punctuation, or word
  boundaries.
- Furigana brackets are learned behaviour, not constrained decoding: rare
  formatting slips (e.g. an empty `[]` on numbers) are possible; the parser in
  the code repo handles them.

## Citation

```bibtex
@misc{shi2026rubyasrevidencepreservingsupervisionjoint,
  title         = {Ruby-ASR: Evidence-Preserving Supervision for Joint Orthographic and Lexical-Reading Recognition},
  author        = {Hao Shi and Yun Liu and Xuehao Yang and Jun Liu and Chuanbo Hua and Xuanjun Chen and Lianbo Liu and Shiao Zhu and Zixiong Su},
  year          = {2026},
  eprint        = {2609.27289},
  archivePrefix = {arXiv},
  primaryClass  = {cs.CL},
  url           = {https://arxiv.org/abs/2609.27289},
}
```

## License

Apache-2.0, same as the Qwen3-ASR base model.