Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ GitHub Release 的发布说明;版本与发布流程见 [docs/RELEASE.md](docs

## [Unreleased]

## [0.0.4-beta.1] - 2026-10-04

第四个测试版:松开空格立即结束录音,已录音频继续完整转写;加入离线中英标点恢复,重新整理 App 首页与设置页。

- 修复 Streaming Paraformer 最后不足一块的已录音频没有解码,导致末尾丢字;松手只停止麦克风,排空 PCM 后再结束识别。取消仍丢弃本次语音。
- 移除松手后的 300ms 实际录音,采集结束后即恢复原媒体音量;保留模型计算所需的纯静音尾垫,不再采集用户语音。
- 新增 CT-Transformer INT8 本地标点模型,最终转写时恢复逗号与问号;“标点用空格代替”在标点恢复后生效,修复紧贴中文的英文标点漏转换,保留数字小数和网址。
- PCM 批量拷贝、复用推理输入块;缓冲溢出报错,避免把缺字结果当成功上屏。
- 重新设计 App 首页、偏好设置、键盘内设置面板和工具面板,浅色与暗色各一套。首页在设置完成前是三步引导(进度条、当前步骤高亮、底部按钮执行当前步骤),完成后收起为“已就绪”卡片和试打区。偏好设置改为分组卡片列表,每项有专属图标;键盘内设置首屏放四个快捷开关和外观、键盘高度。强调色只用于可交互元素和开启状态,图标、箭头和状态标签改为中性色,开关从绿色改为强调色。首页文案统一为中文,不再出现半中半英。键盘按键本身保持既有设计。详见 `docs/APP_UI_SPEC.md`。
- 按键触感改为系统键盘同款短击(`KEYBOARD_TAP`),删除键手势和按住空格进入语音改为单下清脆点击,不再使用余震较长的长按震动。
- 移除按键皮肤(自定义强调色、圆角、不透明度、按键字号)和单手模式。升级后已保存的这些值不再生效,键盘恢复默认外观和全宽布局;导出的数据不再包含这些字段,导入旧备份时忽略。
- 新增真实 native 音频到编辑器的端到端验收和可重复脚本,保存 JSON、日志、截图与布局 XML。详见 `docs/BETA4_VOICE.md`。


## [0.0.3-beta.1] - 2026-10-04

第三个公开测试版:空格滑动移光标、密码管理器的自动填充直接出现在键盘上、选词后的表情联想、26 键字母键上的数字和符号(上滑输入),以及语音输入的去语气词和“标点用空格代替”。仍是测试版,欢迎在 Issues 反馈问题。从 0.0.2-beta.1 覆盖安装即可,用户数据保留;升级后首次启动会重新部署词典,这段时间候选由备用词库提供。
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
2. 执行 `git lfs pull`,确认语音模型和 AAR 不是文本指针。
3. 先阅读 [README.md](README.md)、[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
和 [docs/TEST_ARCHITECTURE.md](docs/TEST_ARCHITECTURE.md)。
4. 修改 App 界面时按 [App 界面开发规范](docs/APP_UI_SPEC.md) 实现尺寸、间距、对齐、点击区及大字体适配;先复用现有令牌和资源,不自行引入一套数值。

## 分支与提交

Expand Down Expand Up @@ -47,6 +48,8 @@ python3 scripts/release_check.py check

如果改动了布局或 Insets,再运行 `visual_check.ps1`,并在 PR 中说明测试设备的
Android 版本、窗口宽度和是否使用浮动键盘。
App 界面同时按 `docs/APP_UI_SPEC.md` 检查普通屏、320dp 大字体和宽屏;运行
`scripts/beta4_e2e.py` 并保存截图、UI XML、结果 JSON 和日志作为可重复的验收工件。

## Pull Request

Expand Down
1 change: 1 addition & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
| Rime 基础数据 | `app/src/main/assets/rime/` | Rime / luna-pinyin / essay 等上游数据 | 以各目录 `AUTHORS` / 上游许可为准 | 已保留上游 AUTHORS;发布前不得删除这些归属文件 |
| sherpa-onnx Android runtime | `app/libs/sherpa-onnx-1.13.6.aar` | https://github.com/k2-fsa/sherpa-onnx | Apache-2.0 | APK 包含 `assets/licenses/sherpa-onnx-Apache-2.0.txt` |
| 中英双语 Streaming Paraformer INT8 语音模型 | `app/src/main/assets/models/voice/bilingual-paraformer/` | https://huggingface.co/csukuangfj/sherpa-onnx-streaming-paraformer-bilingual-zh-en | Apache-2.0 | 只打包 `encoder.int8.onnx`、`decoder.int8.onnx` 和 `tokens.txt`;APK 包含 `assets/licenses/paraformer-model-Apache-2.0.txt`,`manifest.json` 固定模型 ID/版本与文件哈希 |
| CT-Transformer 中英标点 INT8 模型 | `app/src/main/assets/models/voice/punctuation/` | https://modelscope.cn/models/iic/punc_ct-transformer_zh-cn-common-vocab272727-pytorch 、https://github.com/k2-fsa/sherpa-onnx/releases/tag/punctuation-models | Apache-2.0 | 固定 `2024-04-12-int8` 导出,清单包含文件哈希;APK 附 `ct-transformer-Apache-2.0.txt` |
| Microsoft Fluent Emoji | `app/src/main/assets/emoji/fluent/` | https://github.com/microsoft/fluentui-emoji | MIT | 仅打包当前实际使用的基础情绪表情资源;APK 包含 `assets/licenses/fluent-emoji-MIT.txt` |

## 发布前检查
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.3-beta.1
0.0.4-beta.1
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import llc.slacker.openime.data.CustomSymbolRepository
import llc.slacker.openime.data.QuickPhraseRepository
import llc.slacker.openime.floating.FloatingWindowController
import llc.slacker.openime.keyboard.ImeKeyboardView
import llc.slacker.openime.theme.AccentPalette
import llc.slacker.openime.theme.ImeAppearance
import llc.slacker.openime.theme.ImeGeometryTokens
import llc.slacker.openime.theme.ImeSurfacePolicy
Expand Down Expand Up @@ -448,10 +447,9 @@ class AuditInteractionInstrumentedTest {
@Test
fun settingsSlidersExposeCurrentValuesToTouchAndAccessibility() = withKeyboard { harness, _, keyboard ->
harness.awaitMain {
// Corner radius, opacity and key font size live on the skin sub-page.
keyboard.showPanel(Panel.SKIN_SETTINGS)
keyboard.showPanel(Panel.SETTINGS)
val settings = keyboard.findViewWithTag<ViewGroup>("settings-panel")
listOf("圆角", "不透明度", "按键字号").forEach { label ->
listOf("键盘高度", "浮动宽度", "浮动透明度").forEach { label ->
val slider = settings.findViewWithTag<View>("settings-slider:$label")
assertTrue("$label must keep a 48dp touch target", slider.minimumHeight >= keyboard.resources.displayMetrics.density * 48f)
assertTrue("$label must expose its current value", slider.contentDescription.toString().contains(label))
Expand Down Expand Up @@ -545,30 +543,6 @@ class AuditInteractionInstrumentedTest {
}
}

@Test
fun customAccentControlExposesSelectionAndNoPresetClaimsIt() = withKeyboard { harness, _, keyboard ->
harness.awaitMain {
keyboard.setSkin(96, 10, 18, "#123456")
keyboard.showPanel(Panel.SKIN_SETTINGS)
val custom = keyboard.findViewWithTag<View>("accent-custom")
assertTrue(custom.contentDescription.toString().contains("已选中"))
assertTrue("The custom row must stay a 48dp target", custom.minimumHeight >= keyboard.resources.displayMetrics.density * 48f)
AccentPalette.presets.forEach { (hex, label) ->
assertNull(
"$label must not look selected while a custom accent is active",
keyboard.findViewWithTag<View>("accent-selected-mark:$hex"),
)
}
// And the other way round: a preset takes the selection from the custom row.
keyboard.setSkin(96, 10, 18, "#1D9BF0")
keyboard.showPanel(Panel.SKIN_SETTINGS)
assertTrue(
keyboard.findViewWithTag<View>("accent-custom").contentDescription.toString().contains("未选中"),
)
true
}
}

@Test
fun persistedSettingsSnapshotAppliesVisualAndInteractionStateTogether() = withKeyboard { harness, _, keyboard ->
harness.awaitMain {
Expand All @@ -579,53 +553,20 @@ class AuditInteractionInstrumentedTest {
haptic = true,
popup = true,
fuzzy = true,
opacity = 92,
radius = 14,
fontSize = 19,
primaryColor = "#123456",
)
keyboard.showPanel(Panel.SKIN_SETTINGS)
assertTrue(
"The persisted custom accent must be what the skin page reports as selected",
keyboard.findViewWithTag<View>("accent-custom").contentDescription.toString().contains("已选中"),
)
keyboard.showPanel(Panel.FUZZY_SETTINGS)
val toggle = keyboard.findViewWithTag<View>("toggle")
assertTrue(toggle.contentDescription.toString().contains("已开启"))
// An enabled switch is painted with the accent, so this proves the
// persisted colour reached the theme, not just the settings text.
// persisted appearance reached the theme, not just the settings text.
assertEquals(
Color.parseColor("#123456"),
(toggle.background as GradientDrawable).color?.defaultColor,
ImeTheme.IOS.tokens(ImeAppearance.LIGHT).primary,
switchTrackColor(toggle),
)
true
}
}

@Test
fun presetAccentSelectionHasAVisibleNonColorMark() = withKeyboard { harness, _, keyboard ->
harness.awaitMain {
keyboard.setSkin(96, 10, 18, "#1D9BF0")
keyboard.showPanel(Panel.SKIN_SETTINGS)
assertTrue(
"Selected accent must expose a visible check mark in addition to color",
keyboard.findViewWithTag<View>("accent-selected-mark:#1D9BF0") != null,
)
true
}
}

@Test
fun presetAccentSwatchesExposeAVisibleKeyboardFocusState() = withKeyboard { harness, _, keyboard ->
harness.awaitMain {
keyboard.showPanel(Panel.SKIN_SETTINGS)
val swatch = keyboard.findViewWithTag<View>("accent-swatch")
assertTrue("Accent swatches must be keyboard-focusable", swatch.isFocusable)
assertTrue("Accent swatches must expose a focusable background", swatch.background is StateListDrawable)
true
}
}

@Test
fun emptyCandidateOverflowActionIsDisabledUntilCandidatesExist() = withKeyboard { harness, _, keyboard ->
harness.awaitMain {
Expand Down Expand Up @@ -712,9 +653,12 @@ class AuditInteractionInstrumentedTest {
keyboard.showPanel(Panel.TOOLS)
val card = keyboard.findViewWithTag<View>("tool:表情")
assertTrue("Tool card must be keyboard-focusable", card.isFocusable)
assertTrue("Tool card must expose pressed and focus feedback", card.background is StateListDrawable)
// The column is the target; its icon tile mirrors the column's state.
val tile = (card as ViewGroup).getChildAt(0)
assertTrue("Tool tile must expose pressed and focus feedback", tile.background is StateListDrawable)
assertTrue("Tool tile must follow the card's pressed state", tile.isDuplicateParentStateEnabled)
assertEquals("表情", card.contentDescription)
val label = (card as ViewGroup).getChildAt(1)
val label = card.getChildAt(1)
assertTrue(
"Visual tool label must not create a duplicate node",
label.importantForAccessibility == View.IMPORTANT_FOR_ACCESSIBILITY_NO,
Expand Down Expand Up @@ -805,19 +749,23 @@ class AuditInteractionInstrumentedTest {
val panel = keyboard.findViewWithTag<ViewGroup>("fuzzy-settings-panel")
val row = panel.findViewWithTag<ViewGroup>("setting-row")
val toggle = panel.findViewWithTag<View>("toggle")
val icon = panel.findViewWithTag<View>("setting-icon")
// The keyboard's compact rows carry no icon; the preferences page's do.
val icon: View? = panel.findViewWithTag<View>("setting-icon")
assertTrue("Settings row must be keyboard-focusable", row.isFocusable)
assertTrue("Settings row must expose its current state", row.contentDescription.toString().contains("已关闭"))
assertTrue("The visual switch must not create a duplicate accessibility node", toggle.importantForAccessibility == View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
assertTrue(
"Visual switch track minimum must be wider than tall",
toggle.minimumWidth > toggle.minimumHeight,
"Visual switch track must be wider than tall",
ImeGeometryTokens.SWITCH_WIDTH_DP > ImeGeometryTokens.SWITCH_HEIGHT_DP,
)
assertTrue("Setting icon must remain decorative", icon.importantForAccessibility == View.IMPORTANT_FOR_ACCESSIBILITY_NO)
val offColor = (toggle.background as GradientDrawable).color?.defaultColor
assertTrue("Switch keeps a 48dp touch target", toggle.minimumHeight >= keyboard.resources.displayMetrics.density * 48f)
if (icon != null) {
assertTrue("Setting icon must remain decorative", icon.importantForAccessibility == View.IMPORTANT_FOR_ACCESSIBILITY_NO)
}
val offColor = switchTrackColor(toggle)
assertTrue(row.performClick())
assertTrue(row.contentDescription.toString().contains("已开启"))
val onColor = (toggle.background as GradientDrawable).color?.defaultColor
val onColor = switchTrackColor(toggle)
assertTrue("Toggle background must follow its enabled state", offColor != onColor)
true
}
Expand Down Expand Up @@ -1446,4 +1394,8 @@ class AuditInteractionInstrumentedTest {
event.recycle()
}
}

/** The switch track is painted inside its 48dp target by an inset wrapper. */
private fun switchTrackColor(toggle: View): Int? =
((toggle.background as android.graphics.drawable.InsetDrawable).drawable as GradientDrawable).color?.defaultColor
}
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,5 @@ class ClipboardRetentionInstrumentedTest {
override fun onHapticChanged(enabled: Boolean) = Unit
override fun onPopupChanged(enabled: Boolean) = Unit
override fun onFuzzyChanged(enabled: Boolean) = Unit
override fun onSkinChanged(opacity: Int, radius: Int, fontSize: Int, primaryColor: String) = Unit
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,19 @@ class SetupAccessibilityInstrumentedTest {
R.id.open_app_settings,
R.id.voice_permission,
).forEach { id ->
val card = activity.findViewById<ViewGroup>(id)
val card = activity.findViewById<View>(id)
assertTrue("Setup card must be clickable", card.isClickable)
assertTrue("Setup card must be focusable", card.isFocusable)
assertNotNull("Setup card must describe its current action", card.contentDescription)
if (android.os.Build.VERSION.SDK_INT >= 30) {
assertNotNull("Setup card must expose its current state", card.stateDescription)
}
for (index in 0 until card.childCount) {
val children = (card as? ViewGroup)?.let { group -> (0 until group.childCount).map(group::getChildAt) }.orEmpty()
children.forEach { child ->
assertEquals(
"Setup card visuals must not create duplicate nodes",
View.IMPORTANT_FOR_ACCESSIBILITY_NO,
card.getChildAt(index).importantForAccessibility,
child.importantForAccessibility,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,9 @@ class SpaceCursorAndLetterHintsInstrumentedTest {
var found: View? = null
fun walk(view: View) {
if (found != null) return
if (view.tag == "toggle" && view.contentDescription?.startsWith("$label,") == true) found = view
// The keyboard panel shows some settings as quick-toggle tiles.
val switchLike = view.tag == "toggle" || view.tag == "quick-tile"
if (switchLike && view.contentDescription?.startsWith("$label,") == true) found = view
if (view is ViewGroup) for (i in 0 until view.childCount) walk(view.getChildAt(i))
}
walk(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,5 @@ class TextEditControlsInstrumentedTest {
override fun onHapticChanged(enabled: Boolean) = Unit
override fun onPopupChanged(enabled: Boolean) = Unit
override fun onFuzzyChanged(enabled: Boolean) = Unit
override fun onSkinChanged(opacity: Int, radius: Int, fontSize: Int, primaryColor: String) = Unit
}
}
Binary file added app/src/debug/assets/voice-beta4.wav
Binary file not shown.
5 changes: 5 additions & 0 deletions app/src/debug/java/llc/slacker/openime/E2ETestReceiver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class E2ETestReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
val command = intent.getStringExtra(EXTRA_COMMAND) ?: return
if (command == "voice-audio-e2e" || command == "voice-punctuation-e2e") {
val pending = goAsync()
VoiceAudioE2E.replay(context.applicationContext, command == "voice-punctuation-e2e", pending::finish)
return
}
if (command.startsWith("clipboard64:")) {
runCatching {
val text = String(
Expand Down
Loading
Loading