Skip to content

Commit df69757

Browse files
authored
Merge pull request #39 from actlaboratory/master
Release 1.1.0
2 parents 7920335 + 9929a42 commit df69757

8 files changed

Lines changed: 269 additions & 80 deletions

File tree

addon/doc/en/readme.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ Set speech engines which will be used as Japanese and non-Japanese speeches, res
3939

4040
Currently, it is not supported to use the same speech engine for different languages. For example, when you are using SAPI5 speech engine for non-Japanese, you cannot use SAPI5 for Japanese.
4141

42+
### Volume / Rate adjustment options
43+
44+
You can fine-tune the volume and rate for each language.
45+
46+
The adjustment is specified in a range of plus or minus 100, relative to UML's own rate and volume settings.
47+
48+
A value of 100 represents the amount of change when the slider is moved to its maximum on top of each speech engine's configured value. -100 represents the amount of change when the slider is moved to its minimum.
49+
50+
For example, if UML's configured rate is 50, a language with a "rate adjustment" of 20 will be spoken at rate 70 by its corresponding speech engine. Conversely, a "rate adjustment" of -20 results in rate 30.
51+
52+
Adjusted values are automatically clamped to the maximum / minimum supported by each speech engine.
53+
4254
## Recommended settings
4355

4456
It is strongly recommended to check the "Trust voice's language when processing characters and symbols" checkbox, which is located under NVDA's speech settings. It is a bit hard to describe, but it results in better reading.
@@ -47,7 +59,7 @@ It is strongly recommended to check the "Trust voice's language when processing
4759

4860
Some combinations of speech engines will not work properly. In most cases, it is due to the synthDriver's incorrect implementations, and it is not fixable no matter how Universal Multilingual tries its best.
4961

50-
In order to change settings of each speech engine, you need to switch to the speech engine from NVDA's speech settings first, then modify to your preference there. Universal Multilingual is responsible only for switching speech engines, and does not support any features which hook into the behaviors of speech engines under its control.
62+
In order to change settings other than "rate" and "volume" for each speech engine, you need to switch to the speech engine from NVDA's speech settings first, then modify to your preference there.
5163

5264
Some events like cap pitch changing have not been supported yet. The support is planned in a near future.
5365

@@ -61,6 +73,14 @@ For email support, please send an email to "support@actlab.org".
6173

6274
## Changelog
6375

76+
### 2026/03/08 Version 1.1.0
77+
78+
1. Fixed a bug where speech would freeze and not recover in certain situations. ( [#33](https://github.com/actlaboratory/UML/pull/33) , by [@mo29cg](https://github.com/mo29cg) )
79+
2. You can now fine-tune the rate and volume for each speech engine. These settings are accessible from both the UML settings dialog and the Settings Ring. ( [#34](https://github.com/actlaboratory/UML/pull/34) , by [@mo29cg](https://github.com/mo29cg) )
80+
3. UML now only prompts for reload when necessary after changing settings.
81+
4. As an internal change, the text processing method has been changed to use ExtensionPoints. This reduces the chance of breaking the behavior of other add-ons.
82+
5. Tested using NVDA 2026 alpha version and updated LastTestedNVDAVersion.
83+
6484
### 2025/11/19 Version 1.0.5
6585

6686
1. Fixed a bug where speech would freeze due to an NVDA internal code change.

addon/doc/ja/readme.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@
3838

3939
現在、複数の言語で同じ音声エンジンを使用することはサポートしていません。たとえば、日本語以外の音声エンジンでSAPI5を使っているときに、日本語をSAPI5にすることはできません。
4040

41+
### 音量調整 / 速度調整オプション
42+
43+
言語ごとに、音量や速度を微調整することができます。
44+
45+
微調整は、UML本体の速度と音量設定を基準として、プラスマイナス 100 の範囲で指定できます。
46+
47+
100 という値は、それぞれの音声エンジンの設定値の上で、スライダーを最大まで調整した場合の変化量です。 -100 は、スライダーを最小まで調整した場合の変化量です。
48+
49+
たとえば、 UML の設定値が「速度 50」のとき、「速度調整」を「20」にした言語は、対応する音声エンジンで読み上げる際に「速度 70」で読み上げられます。逆に、「速度調整」を「-20」にすると、「速度30」で読み上げられます。
50+
51+
微調整後の値は、各音声エンジンが対応している最大値 / 最長値で自動的に丸められます。
52+
4153
## お勧めの設定
4254

4355
NVDAの音声設定で、「記号と文字の説明に音声の言語を使用」のチェックボックスにチェックを入れておくことを推奨しています。言葉で説明するのがなかなか難しいのですが、チェックを付けるほうが意図した読み上げになります。
@@ -46,7 +58,7 @@ NVDAの音声設定で、「記号と文字の説明に音声の言語を使用
4658

4759
音声エンジンの組み合わせによっては、正しく動作しない可能性があります。多くの場合、それは音声エンジンドライバの間違った実装による問題であり、Universal Multilingualがどんなに頑張っても解決することはできません。
4860

49-
使用する音声エンジンの設定を個別で変更するには、NVDAの音声設定で、その音声エンジンを使用する設定に変更したあとで、お好みの設定に調整する必要があります。Universal Multilingualは音声を出し分けるだけであり、コントロール下にある音声エンジンの設定に干渉する機能は搭載していません
61+
使用する音声エンジンの「速度」と「音量」以外の設定を個別で変更するには、NVDAの音声設定で、その音声エンジンを使用する設定に変更したあとで、お好みの設定に調整する必要があります。
5062

5163
大文字のピッチ変更など、一部のイベントにまだ対応できていません。そのうち対応したいと思っています。
5264

@@ -60,6 +72,14 @@ GitHubのアカウントを持っている方は、 [Universal Multilingualのis
6072

6173
## 更新履歴
6274

75+
### 2026/03/08 Version 1.1.0
76+
77+
1. 一部の状況に置いて、読み上げがフリーズして戻ってこなくなる不具合を修正しました ( [#33](https://github.com/actlaboratory/UML/pull/33) , by (@mo29cg)[https://github.com/mo29cg])
78+
2. 音声エンジンごとに、速度と音量を微調整できるようになりました。これらの設定は、 UML の設定ダイアログのほか、簡単音声設定からもアクセス可能です ( [#34](https://github.com/actlaboratory/UML/pull/34) , by (@mo29cg)[https://github.com/mo29cg])
79+
3. UML の設定を変更したとき、必要な場合だけ UML の再読み込みを問い合わせるようにしました。
80+
4. 内部処理の変更として、テキストの処理方法を ExtensionPoints ベースに変更しました。これにより、他のアドオンの動作を壊す可能性が減りました。
81+
5. NVDA 2026 アルファ版での動作を確認し、 LastTestedNVDAVersion を更新しました。
82+
6383
### 2025/11/19 Version 1.0.5
6484
1. NVDAの内部コードが変更されていたことに伴い、読み上げがフリーズしていた不具合を修正しました。
6585

addon/globalPlugins/UML/__init__.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ def _(x): return x
2727
"japanese": "string(default=_)",
2828
"fallback": "string(default=_)",
2929
"checkForUpdatesOnStartup": "boolean(default=True)",
30+
"volumeOffset_ja": "integer(default=0, min=-100, max=100)",
31+
"volumeOffset_en": "integer(default=0, min=-100, max=100)",
32+
"rateOffset_ja": "integer(default=0, min=-100, max=100)",
33+
"rateOffset_en": "integer(default=0, min=-100, max=100)",
3034
}
3135
config.conf.spec["UML_global"] = confspec
3236

@@ -87,6 +91,10 @@ def settings(self, evt):
8791
"primary_language": config.conf["UML_global"]["primaryLanguage"],
8892
"strategy": config.conf["UML_global"]["strategy"],
8993
"engineMap": engineMap,
94+
"volumeOffset_ja": config.conf["UML_global"]["volumeOffset_ja"],
95+
"volumeOffset_en": config.conf["UML_global"]["volumeOffset_en"],
96+
"rateOffset_ja": config.conf["UML_global"]["rateOffset_ja"],
97+
"rateOffset_en": config.conf["UML_global"]["rateOffset_en"],
9098
}
9199
dlg = SettingsDialog(opts)
92100
ret = dlg.ShowModal()
@@ -97,11 +105,24 @@ def settings(self, evt):
97105
def _saveSettings(self, data):
98106
# If the new settings fail, revert to the previous one.
99107
backup = list(config.conf["UML_global"].items())
108+
old_japanese = config.conf["UML_global"]["japanese"]
109+
old_fallback = config.conf["UML_global"]["fallback"]
100110
config.conf["UML_global"]["primaryLanguage"] = data["primary_language"]
101111
config.conf["UML_global"]["strategy"] = data["strategy"]
102112
config.conf["UML_global"]["japanese"] = data["engineMap"]["ja"]
103113
config.conf["UML_global"]["fallback"] = data["engineMap"]["en"]
104-
if synthDriverHandler.getSynth().name == "UML":
114+
config.conf["UML_global"]["volumeOffset_ja"] = data["volumeOffset_ja"]
115+
config.conf["UML_global"]["volumeOffset_en"] = data["volumeOffset_en"]
116+
config.conf["UML_global"]["rateOffset_ja"] = data["rateOffset_ja"]
117+
config.conf["UML_global"]["rateOffset_en"] = data["rateOffset_en"]
118+
synth = synthDriverHandler.getSynth()
119+
if synth and synth.name == "UML":
120+
synth._applySettings()
121+
engine_changed = (
122+
data["engineMap"]["ja"] != old_japanese
123+
or data["engineMap"]["en"] != old_fallback
124+
)
125+
if engine_changed and synthDriverHandler.getSynth().name == "UML":
105126
self._askHotReload(backup)
106127

107128
def _askHotReload(self, backup):

addon/globalPlugins/UML/settings.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,49 @@ def __init__(self, opts):
7272
self, wx.ID_ANY, label=sbtnLabel, name=sbtnLabel)
7373
self.selectEngineButton.Bind(wx.EVT_BUTTON, self.onSynthSelect)
7474

75-
msz.Add(enginesLabel)
76-
msz.Add(self.enginesList, 0, wx.EXPAND)
77-
msz.Add(self.selectEngineButton, 1, wx.ALIGN_RIGHT)
78-
msz.AddSpacer(40)
75+
msz.Add(enginesLabel)
76+
msz.Add(self.enginesList, 0, wx.EXPAND)
77+
msz.Add(self.selectEngineButton, 1, wx.ALIGN_RIGHT)
78+
msz.AddSpacer(20)
79+
80+
adjLabel = wx.StaticText(self, wx.ID_ANY, label=_("Voice adjustments"))
81+
msz.Add(adjLabel)
82+
msz.AddSpacer(5)
83+
84+
offsetMin = -100
85+
offsetMax = 100
86+
self.volumeOffsets = {}
87+
self.rateOffsets = {}
88+
for lang in self.langValues:
89+
code = lang["internal"]
90+
name = lang["user"]
91+
92+
volName = _("%s volume adjustment (%d to %d)") % (
93+
name, offsetMin, offsetMax)
94+
volLabel = wx.StaticText(self, wx.ID_ANY, label=volName)
95+
self.volumeOffsets[code] = wx.SpinCtrl(
96+
self, wx.ID_ANY, min=offsetMin, max=offsetMax,
97+
initial=opts.get("volumeOffset_%s" % code, 0),
98+
name=volName)
99+
vsz = wx.BoxSizer(wx.HORIZONTAL)
100+
vsz.Add(volLabel, 1)
101+
vsz.Add(self.volumeOffsets[code], 1)
102+
msz.Add(vsz, 0, wx.EXPAND)
103+
104+
rateName = _("%s rate adjustment (%d to %d)") % (
105+
name, offsetMin, offsetMax)
106+
rateLabel = wx.StaticText(self, wx.ID_ANY, label=rateName)
107+
self.rateOffsets[code] = wx.SpinCtrl(
108+
self, wx.ID_ANY, min=offsetMin, max=offsetMax,
109+
initial=opts.get("rateOffset_%s" % code, 0),
110+
name=rateName)
111+
rsz = wx.BoxSizer(wx.HORIZONTAL)
112+
rsz.Add(rateLabel, 1)
113+
rsz.Add(self.rateOffsets[code], 1)
114+
msz.Add(rsz, 0, wx.EXPAND)
115+
msz.AddSpacer(5)
116+
117+
msz.AddSpacer(20)
79118

80119
ok = wx.Button(self, wx.ID_OK, _("OK"))
81120
ok.SetDefault()
@@ -125,12 +164,17 @@ def _findSynthDisplayName(self, synths, target):
125164
# end for
126165
return ""
127166

128-
def GetData(self):
129-
return {
130-
"primary_language": self.langValues[self.langList.GetSelection()]["internal"],
131-
"strategy": self.strategyValues[self.strategyList.GetSelection()]["internal"],
132-
"engineMap": self.engineMap,
133-
}
167+
def GetData(self):
168+
data = {
169+
"primary_language": self.langValues[self.langList.GetSelection()]["internal"],
170+
"strategy": self.strategyValues[self.strategyList.GetSelection()]["internal"],
171+
"engineMap": self.engineMap,
172+
}
173+
for lang in self.langValues:
174+
code = lang["internal"]
175+
data["volumeOffset_%s" % code] = self.volumeOffsets[code].GetValue()
176+
data["rateOffset_%s" % code] = self.rateOffsets[code].GetValue()
177+
return data
134178

135179
def onSynthSelect(self, evt):
136180
selected = self.enginesList.GetFocusedItem()

addon/locale/ja/LC_MESSAGES/nvda.po

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ msgstr "エンジン"
170170
msgid "Select engine"
171171
msgstr "エンジンを選択"
172172

173+
#: addon\globalPlugins\UML\settings.py:80
174+
msgid "Voice adjustments"
175+
msgstr "音声調整"
176+
177+
#: addon\globalPlugins\UML\settings.py:92
178+
#, python-format
179+
msgid "%s volume adjustment (%d to %d)"
180+
msgstr "%s 音量調整 (%d から %d)"
181+
182+
#: addon\globalPlugins\UML\settings.py:104
183+
#, python-format
184+
msgid "%s rate adjustment (%d to %d)"
185+
msgstr "%s 速度調整 (%d から %d)"
186+
173187
#: addon\globalPlugins\UML\settings.py:117
174188
msgid "Not set"
175189
msgstr "未設定"

0 commit comments

Comments
 (0)