Skip to content

Commit 507e5ee

Browse files
committed
FIX: 收敛近期体验与配置修复
- 修复 CI 平台差异测试、ASR 推荐模型、字幕编辑无音频提示 - 收敛查词引用单引号归一化与订阅/发布配置 - 升级 StoreKit 插件补丁版本并更新任务记录
1 parent e34a537 commit 507e5ee

26 files changed

Lines changed: 446 additions & 53 deletions

.github/workflows/release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ jobs:
340340
SUPABASE_URL: ${{ vars.SUPABASE_URL }}
341341
SUPABASE_PUBLISHABLE_KEY: ${{ vars.SUPABASE_PUBLISHABLE_KEY }}
342342
GOOGLE_WEB_CLIENT_ID: ${{ vars.GOOGLE_WEB_CLIENT_ID }}
343+
REVENUECAT_API_KEY_APPLE: ${{ secrets.REVENUECAT_API_KEY_APPLE }}
343344
run: |
344345
# 避免 Homebrew rsync 抢占(Apple toolchain 需要系统 rsync)
345346
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
@@ -353,6 +354,9 @@ jobs:
353354
if [[ -n "$POSTHOG_API_KEY" ]]; then
354355
DART_DEFINES+=("--dart-define=POSTHOG_API_KEY=${POSTHOG_API_KEY}")
355356
fi
357+
if [[ -n "$REVENUECAT_API_KEY_APPLE" ]]; then
358+
DART_DEFINES+=("--dart-define=REVENUECAT_API_KEY_APPLE=${REVENUECAT_API_KEY_APPLE}")
359+
fi
356360
# 先用 flutter 编译 dart 代码,--no-codesign 跳过签名
357361
flutter build ios --release --no-codesign --flavor=prod \
358362
--build-name="$BUILD_NAME" \

TASKS.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,73 @@
11
# Echo Loop 任务清单
22

3-
> 最后更新:2026-07-06(隐藏本地转录入口
3+
> 最后更新:2026-07-06(macOS 订阅入口统一走 Apple 平台开关
44
> 当前焦点:Android 结束录音闪退(离线 ASR / Silero VAD)——**仍未解决**
55
6+
## 已完成:macOS 订阅入口统一走 Apple 平台开关
7+
8+
用户反馈 `flutter run -d macos --dart-define-from-file=.dev.env --flavor prod` 看不到会员和订阅入口。排查确认 macOS 被临时排除在 RevenueCat Apple key 映射外,导致 `subscriptionAvailabilityProvider=false`,设置页账号分组隐藏订阅入口。
9+
10+
- [x] **Apple 平台统一**`revenueCatApiKey` 改为 iOS/macOS 都使用 `REVENUECAT_API_KEY_APPLE`,Android 继续使用 `REVENUECAT_API_KEY_GOOGLE`,Web/未知平台仍不启用。
11+
- [x] **映射可测化**:新增 `revenueCatApiKeyForPlatform` 纯函数,避免平台 key 规则散落,并补单测锁定 iOS/macOS 同用 Apple key。
12+
- [x] **验证**`flutter analyze lib/config/revenuecat_config.dart test/config/revenuecat_config_test.dart` 0 问题;`flutter test test/config/revenuecat_config_test.dart` 全过(3 例)。
13+
14+
**完成时间**: 2026-07-06
15+
16+
## 已完成:升级 in_app_purchase_storekit 补丁版本
17+
18+
用户反馈 macOS 运行时 `in_app_purchase_storekit` 输出 StoreKit deprecated warnings,先升级当前依赖约束内可解析的补丁版本,确认是否能缓解第三方插件警告。
19+
20+
- [x] **依赖锁定升级**`pubspec.lock``in_app_purchase_storekit``0.4.10` 升级到 `0.4.10+1`,未带出其他依赖升级。
21+
- [x] **验证结果**`flutter build macos --debug --dart-define-from-file=.dev.env --flavor dev` 构建成功,产物为 `build/macos/Build/Products/Debug-dev/Echo Loop Dev.app`
22+
- [x] **残留说明**`0.4.10+1` 仍包含 StoreKit 1 API,macOS 15 SDK 下仍会输出 `SKProduct` / `SKPayment` / `SKPaymentTransactionObserver` 等 deprecated warnings;这属于上游插件未完全迁移 StoreKit 2,不影响本次构建通过。
23+
24+
**完成时间**: 2026-07-06
25+
26+
## 已完成:字幕编辑页无音频时显示明确提示
27+
28+
用户反馈字幕编辑页中部分 item 只有字幕、没有本地音频时,波形区不应显示「波形生成失败」和「重试」,应提示没有找到音频。
29+
30+
- [x] **状态区分**`SubtitleEditorState` 新增 `waveformAudioMissing`,将 `audioPath == null` 与真实波形提取失败分开处理。
31+
- [x] **UI 提示**`SubtitleWaveformView` 无音频时显示「没有找到音频,仍可在下方编辑字幕。」,不显示失败态与重试按钮;真实波形失败仍保留重试。
32+
- [x] **测试**:补充 controller 回归测试锁定无音频状态,补充 widget 回归测试锁定无音频提示与无重试按钮。
33+
- [x] **验证**`flutter analyze lib/features/subtitle_editor/subtitle_editor_controller.dart lib/features/subtitle_editor/subtitle_simple_editor_screen.dart lib/features/subtitle_editor/subtitle_waveform_view.dart lib/l10n/app_localizations.dart lib/l10n/app_localizations_en.dart lib/l10n/app_localizations_zh.dart test/features/subtitle_editor/subtitle_editor_controller_test.dart test/features/subtitle_editor/subtitle_waveform_view_test.dart` 0 问题;`flutter test test/features/subtitle_editor/subtitle_editor_controller_test.dart test/features/subtitle_editor/subtitle_waveform_view_test.dart` 全过(67 例)。
34+
35+
**完成时间**: 2026-07-06
36+
37+
## 已完成:修复查词归一化引用单引号
38+
39+
用户反馈选中 `'onto something'` 这类被单引号包住的引用表达时,查词 key 会保留末尾 `'`,导致词典标题与查询表达异常。尾部撇号应只在 `s'` 复数所有格中保留,普通引用尾引号应剥离;词内 `'s` 缩写/所有格继续保留。
40+
41+
- [x] **归一化规则收敛**`normalizeDictionaryQueryForPrompt` 在剥离首尾标点后,对尾部直撇号做二次判断,仅保留 `dogs'` / `James'` 这类 `s'` 所有格,`'onto something'` 归一化为 `onto something`
42+
- [x] **收藏命中修边同步**`trimSavedRange` 不再一律保留尾部撇号,避免正文收藏/意群命中时把引用尾引号画进下划线区间。
43+
- [x] **后端同步**`../fluency-frontend``@repo/ai` 归一化方法与 prefill 词典写库 key 同步同一规则,避免线上查询和预填充缓存分叉。
44+
- [x] **回归测试**:补充 normalizeWord / prompt 归一化测试,以及句内命中区间不覆盖引用尾单引号的纯逻辑测试。
45+
- [x] **验证**`flutter analyze lib/utils/text_normalize.dart lib/widgets/practice/sentence_word_selection.dart test/utils/text_normalize_test.dart test/widgets/practice/sentence_word_selection_test.dart` 0 问题;`flutter test test/utils/text_normalize_test.dart test/widgets/practice/sentence_word_selection_test.dart` 全过(67 例);`../fluency-frontend``@repo/ai` text-normalize 测试、typecheck、prefill dictionary normalize 单测、prefill `tsc --noEmit` 均通过。
46+
47+
**完成时间**: 2026-07-06
48+
49+
## 已完成:ASR 推荐模型改为 Balanced
50+
51+
用户反馈语音识别设置页推荐模型应从 Fast 改为 Balanced,避免默认推荐低准确率档位。
52+
53+
- [x] **推荐策略收敛**`AsrModelManager.recommendModel` 默认返回 `whisper-base-en-int8`(Balanced),不再按低配设备回退到 Tiny/Fast。
54+
- [x] **回归测试**:补充低内存参数下仍推荐 Balanced 的单测,锁定默认推荐档位。
55+
- [x] **验证**`flutter analyze lib/services/asr/asr_model_manager.dart test/services/asr/asr_model_manager_test.dart` 0 问题;`flutter test test/services/asr/asr_model_manager_test.dart` 全过(3 例)。
56+
57+
**完成时间**: 2026-07-06
58+
59+
## 已完成:修复 GitHub CI Linux 测试失败
60+
61+
最新 GitHub Actions `CI` run `28747157143``test` job 失败:`3835 tests passed, 9 failed, 13 skipped`。失败集中在 Ubuntu runner 平台差异触发的测试断言不稳,而不是业务实现回归。
62+
63+
- [x] **客户端平台 header 测试收敛**`client_info_test` 明确 Linux/未知平台按 fail-open 返回空平台并省略 `x-app-platform`,支持平台仍校验公共 header。
64+
- [x] **设置页平台文案测试固定目标平台**:TTS 入口 `Apple AI` 文案测试显式模拟 iOS,避免 Linux 测试宿主进入不同平台摘要分支。
65+
- [x] **ASR 设置页平台分支断言**`asr_settings_screen_test` 按 iOS/macOS 与 Linux 的后端选择器差异分别断言 `Echo Loop AI` 文案数量。
66+
- [x] **完成返回测试隔离语音能力**:收藏复习、难句补练的完成返回回归测试显式关闭跟读评分,避免 Linux speech gate 改写完成弹窗路径。
67+
- [x] **验证**`flutter analyze test/services/client_info_test.dart test/screens/settings_screen_test.dart test/screens/asr_settings_screen_test.dart test/screens/bookmark_review_screen_test.dart test/screens/review_difficult_practice_screen_test.dart` 0 问题;`flutter test test/services/client_info_test.dart test/screens/settings_screen_test.dart test/screens/asr_settings_screen_test.dart test/screens/bookmark_review_screen_test.dart test/screens/review_difficult_practice_screen_test.dart` 全过。
68+
69+
**完成时间**: 2026-07-06
70+
671
## 已完成:隐藏管理字幕里的本地转录入口
772

873
用户反馈本地转录效果不好,先从「管理字幕」弹窗隐藏本地转录入口,避免继续暴露给用户;底层本地转录实现、状态展示、取消逻辑和测试代码保留,方便后续恢复或继续排查。

lib/config/revenuecat_config.dart

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ library;
1414

1515
import 'dart:io' show Platform;
1616

17-
import 'package:flutter/foundation.dart' show kIsWeb;
17+
import 'package:flutter/foundation.dart' show kIsWeb, visibleForTesting;
1818

1919
/// Apple App Store 平台的 RevenueCat 公开 API Key(iOS / macOS)。
2020
const _revenueCatApiKeyApple = String.fromEnvironment(
@@ -37,14 +37,35 @@ const revenueCatEntitlementId = String.fromEnvironment(
3737

3838
/// 当前平台应使用的 RevenueCat API Key(不可用平台返回空串)。
3939
///
40-
/// 注意:macOS 虽与 iOS 共用 Apple key,但 macOS IAP 尚未验证(PLAN.md
41-
/// Milestone 5 Phase 4),当前刻意不返回 key —— 使 macOS 上
42-
/// [isRevenueCatConfigured] 为 false,RC 不初始化、订阅入口不展示。
43-
/// macOS 购买流程验证通过后恢复 `|| Platform.isMacOS` 即可整体启用。
40+
/// iOS 与 macOS 都属于 Apple App Store / StoreKit 购买通道,统一由
41+
/// `REVENUECAT_API_KEY_APPLE` 控制;Android 由 Google Play key 控制。
4442
String get revenueCatApiKey {
45-
if (kIsWeb) return '';
46-
if (Platform.isIOS) return _revenueCatApiKeyApple;
47-
if (Platform.isAndroid) return _revenueCatApiKeyGoogle;
43+
return revenueCatApiKeyForPlatform(
44+
isWeb: kIsWeb,
45+
isIOS: !kIsWeb && Platform.isIOS,
46+
isMacOS: !kIsWeb && Platform.isMacOS,
47+
isAndroid: !kIsWeb && Platform.isAndroid,
48+
appleKey: _revenueCatApiKeyApple,
49+
googleKey: _revenueCatApiKeyGoogle,
50+
);
51+
}
52+
53+
/// 根据目标平台选择 RevenueCat public key。
54+
///
55+
/// 抽成纯函数便于测试,避免平台判断散落在 UI / 购买服务中。Apple 生态内
56+
/// iOS 与 macOS 共用同一组 StoreKit / RevenueCat 配置。
57+
@visibleForTesting
58+
String revenueCatApiKeyForPlatform({
59+
required bool isWeb,
60+
required bool isIOS,
61+
required bool isMacOS,
62+
required bool isAndroid,
63+
required String appleKey,
64+
required String googleKey,
65+
}) {
66+
if (isWeb) return '';
67+
if (isIOS || isMacOS) return appleKey;
68+
if (isAndroid) return googleKey;
4869
return '';
4970
}
5071

lib/features/subtitle_editor/subtitle_editor_controller.dart

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ class SubtitleEditorState {
5454
/// 不阻断整个编辑流程。与「加载中(waveform == null 且未失败)」显式区分,
5555
/// 避免失败被误显示成永久「加载 0%」。
5656
final bool waveformFailed;
57+
58+
/// 当前 item 没有本地音频文件,无法生成波形。
59+
///
60+
/// 这不是波形提取失败:用户仍可编辑已有字幕,但没有可重试的本地音频输入。
61+
final bool waveformAudioMissing;
5762
final double playbackSpeed;
5863
final double waveformZoomScale;
5964

@@ -93,6 +98,7 @@ class SubtitleEditorState {
9398
this.waveform,
9499
this.waveformProgress = 0,
95100
this.waveformFailed = false,
101+
this.waveformAudioMissing = false,
96102
this.playbackSpeed = 1.0,
97103
this.waveformZoomScale = 1.0,
98104
this.maxWaveformZoomScale = 1.0,
@@ -130,6 +136,7 @@ class SubtitleEditorState {
130136
Waveform? waveform,
131137
double? waveformProgress,
132138
bool? waveformFailed,
139+
bool? waveformAudioMissing,
133140
double? playbackSpeed,
134141
double? waveformZoomScale,
135142
double? maxWaveformZoomScale,
@@ -161,6 +168,7 @@ class SubtitleEditorState {
161168
waveform: waveform ?? this.waveform,
162169
waveformProgress: waveformProgress ?? this.waveformProgress,
163170
waveformFailed: waveformFailed ?? this.waveformFailed,
171+
waveformAudioMissing: waveformAudioMissing ?? this.waveformAudioMissing,
164172
playbackSpeed: playbackSpeed ?? this.playbackSpeed,
165173
waveformZoomScale: waveformZoomScale ?? this.waveformZoomScale,
166174
maxWaveformZoomScale: maxWaveformZoomScale ?? this.maxWaveformZoomScale,
@@ -1226,7 +1234,11 @@ class SubtitleEditorController extends StateNotifier<SubtitleEditorState> {
12261234
/// 重新尝试提取波形(失败态下用户点「重试」)。
12271235
Future<void> retryWaveform() async {
12281236
if (_loadingWaveform) return;
1229-
state = state.copyWith(waveformFailed: false, waveformProgress: 0);
1237+
state = state.copyWith(
1238+
waveformFailed: false,
1239+
waveformAudioMissing: false,
1240+
waveformProgress: 0,
1241+
);
12301242
await _loadWaveform();
12311243
}
12321244

@@ -1236,7 +1248,7 @@ class SubtitleEditorController extends StateNotifier<SubtitleEditorState> {
12361248
try {
12371249
final audioPath = await state.audioItem.getFullAudioPath();
12381250
if (audioPath == null) {
1239-
if (mounted) state = state.copyWith(waveformFailed: true);
1251+
if (mounted) state = state.copyWith(waveformAudioMissing: true);
12401252
return;
12411253
}
12421254
final dataDir = await getAppDataDirectory();
@@ -1248,7 +1260,11 @@ class SubtitleEditorController extends StateNotifier<SubtitleEditorState> {
12481260
if (await waveFile.exists()) {
12491261
final waveform = await JustWaveform.parse(waveFile);
12501262
if (!mounted) return;
1251-
state = state.copyWith(waveform: waveform, waveformProgress: 1);
1263+
state = state.copyWith(
1264+
waveform: waveform,
1265+
waveformProgress: 1,
1266+
waveformAudioMissing: false,
1267+
);
12521268
return;
12531269
}
12541270

@@ -1263,6 +1279,7 @@ class SubtitleEditorController extends StateNotifier<SubtitleEditorState> {
12631279
state = state.copyWith(
12641280
waveform: progress.waveform,
12651281
waveformProgress: progress.progress,
1282+
waveformAudioMissing: false,
12661283
);
12671284
}
12681285
} catch (_) {

lib/features/subtitle_editor/subtitle_simple_editor_screen.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class _SubtitleSimpleEditorScreenState
151151
waveform: state.waveform,
152152
extractionProgress: state.waveformProgress,
153153
extractionFailed: state.waveformFailed,
154+
audioMissing: state.waveformAudioMissing,
154155
onRetryExtraction: () =>
155156
unawaited(controller.retryWaveform()),
156157
duration: state.totalDuration,

lib/features/subtitle_editor/subtitle_waveform_view.dart

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ class SubtitleWaveformView extends StatefulWidget {
145145
/// 波形提取是否失败:为 true 时波形区显示失败态 + 重试,而非「加载中」。
146146
final bool extractionFailed;
147147

148+
/// 当前字幕 item 没有本地音频:显示不可重试的无音频提示。
149+
final bool audioMissing;
150+
148151
/// 点击「重试」重新提取波形。
149152
final VoidCallback onRetryExtraction;
150153

@@ -186,6 +189,7 @@ class SubtitleWaveformView extends StatefulWidget {
186189
required this.waveform,
187190
required this.extractionProgress,
188191
this.extractionFailed = false,
192+
this.audioMissing = false,
189193
this.onRetryExtraction = _ignoreRetry,
190194
required this.duration,
191195
required this.sentences,
@@ -321,7 +325,9 @@ class _SubtitleWaveformViewState extends State<SubtitleWaveformView> {
321325
child: SizedBox(
322326
height: 112,
323327
width: double.infinity,
324-
child: widget.extractionFailed
328+
child: widget.audioMissing
329+
? const _WaveformAudioMissing()
330+
: widget.extractionFailed
325331
? _WaveformFailed(onRetry: widget.onRetryExtraction)
326332
: (waveform == null ||
327333
duration == null ||
@@ -785,6 +791,42 @@ class _WaveformLoading extends StatelessWidget {
785791
}
786792
}
787793

794+
/// 无本地音频态:字幕仍可编辑,但没有音频输入可用于生成波形。
795+
class _WaveformAudioMissing extends StatelessWidget {
796+
const _WaveformAudioMissing();
797+
798+
@override
799+
Widget build(BuildContext context) {
800+
final theme = Theme.of(context);
801+
final l10n = AppLocalizations.of(context)!;
802+
return Center(
803+
child: Padding(
804+
padding: const EdgeInsets.symmetric(horizontal: 16),
805+
child: Row(
806+
mainAxisSize: MainAxisSize.min,
807+
children: [
808+
Icon(
809+
Icons.music_off,
810+
size: 18,
811+
color: theme.colorScheme.onSurfaceVariant,
812+
),
813+
const SizedBox(width: 8),
814+
Flexible(
815+
child: Text(
816+
l10n.waveformAudioMissing,
817+
style: theme.textTheme.bodySmall?.copyWith(
818+
color: theme.colorScheme.onSurfaceVariant,
819+
),
820+
textAlign: TextAlign.center,
821+
),
822+
),
823+
],
824+
),
825+
),
826+
);
827+
}
828+
}
829+
788830
/// 波形提取失败态:提示失败 + 重试按钮。
789831
///
790832
/// 波形只是编辑辅助,失败不阻断编辑——下方句子列表仍可正常增删改。

lib/l10n/app_en.arb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
}
114114
},
115115
"waveformLoadFailed": "Waveform unavailable. You can still edit subtitles below.",
116+
"waveformAudioMissing": "No audio found. You can still edit subtitles below.",
116117
"waveformRetry": "Retry",
117118
"playbackSpeed": "Playback Speed",
118119
"loopPlayback": "Loop Playback",

lib/l10n/app_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,12 @@ abstract class AppLocalizations {
542542
/// **'Waveform unavailable. You can still edit subtitles below.'**
543543
String get waveformLoadFailed;
544544

545+
/// No description provided for @waveformAudioMissing.
546+
///
547+
/// In en, this message translates to:
548+
/// **'No audio found. You can still edit subtitles below.'**
549+
String get waveformAudioMissing;
550+
545551
/// No description provided for @waveformRetry.
546552
///
547553
/// In en, this message translates to:

lib/l10n/app_localizations_en.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ class AppLocalizationsEn extends AppLocalizations {
251251
String get waveformLoadFailed =>
252252
'Waveform unavailable. You can still edit subtitles below.';
253253

254+
@override
255+
String get waveformAudioMissing =>
256+
'No audio found. You can still edit subtitles below.';
257+
254258
@override
255259
String get waveformRetry => 'Retry';
256260

lib/l10n/app_localizations_zh.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ class AppLocalizationsZh extends AppLocalizations {
246246
@override
247247
String get waveformLoadFailed => '波形生成失败,仍可在下方编辑字幕。';
248248

249+
@override
250+
String get waveformAudioMissing => '没有找到音频,仍可在下方编辑字幕。';
251+
249252
@override
250253
String get waveformRetry => '重试';
251254

0 commit comments

Comments
 (0)