32 files changed
+842
-1198
android/app/src/main/AndroidManifest.xml
+18
@@ -5,6 +5,7 @@
5
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
6
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
7
<uses-permission android:name="android.permission.WAKE_LOCK" />
8
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
9
10
<application
11
android:label="LocalMind"
@@ -57,6 +58,23 @@
58
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
59
android:value="Used to keep the local AI model inference process alive when the user switches apps, preventing the OS from terminating the heavy background computation." />
60
</service>
61
+ <service
62
+ android:name="com.ryanheise.audioservice.AudioService"
63
+ android:foregroundServiceType="mediaPlayback"
64
+ android:exported="true"
65
+ tools:ignore="Instantiable">
66
+ <intent-filter>
67
+ <action android:name="android.media.browse.MediaBrowserService" />
68
+ </intent-filter>
69
+ </service>
70
+
71
+ <receiver
72
+ android:name="com.ryanheise.audioservice.MediaButtonReceiver"
73
+ android:exported="true">
74
+ <intent-filter>
75
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
76
+ </intent-filter>
77
+ </receiver>
78
</application>
79
<!-- Required to query activities that can process text, see:
80
https://developer.android.com/training/package-visibility and
android/app/src/main/kotlin/pro/momin/localmind/MainActivity.kt
+2
-2
@@ -3,11 +3,11 @@ package pro.momin.localmind
3
import androidx.annotation.NonNull
4
import android.app.ActivityManager
5
import android.content.Context
6
-import io.flutter.embedding.android.FlutterActivity
6
+import com.ryanheise.audioservice.AudioServiceActivity
7
import io.flutter.embedding.engine.FlutterEngine
8
import io.flutter.plugin.common.MethodChannel
9
10
-class MainActivity : FlutterActivity() {
10
+class MainActivity : AudioServiceActivity() {
11
private val CHANNEL = "localmind/chat_background"
12
private val MEMORY_CHANNEL = "localmind/device_memory"
13
ios/Podfile.lock
+27
@@ -1,4 +1,9 @@
1
PODS:
2
+ - audio_service (0.0.1):
3
+ - Flutter
4
+ - FlutterMacOS
5
+ - audio_session (0.0.1):
6
+ - Flutter
7
- audioplayers_darwin (0.0.1):
8
- Flutter
9
- FlutterMacOS
@@ -55,6 +60,9 @@ PODS:
60
- Flutter
61
- haptic_feedback (0.5.1):
62
- Flutter
63
+ - just_audio (0.0.1):
64
+ - Flutter
65
+ - FlutterMacOS
66
- large_file_handler (0.0.1):
67
- Flutter
68
- MediaPipeTasksGenAI (0.10.33):
@@ -74,6 +82,9 @@ PODS:
82
- FlutterMacOS
83
- sherpa_onnx_ios (1.13.2):
84
- Flutter
85
+ - sqflite_darwin (0.0.4):
86
+ - Flutter
87
+ - FlutterMacOS
88
- SwiftyGif (5.4.5)
89
- url_launcher_ios (0.0.1):
90
- Flutter
@@ -81,6 +92,8 @@ PODS:
92
- Flutter
93
94
DEPENDENCIES:
95
+ - audio_service (from `.symlinks/plugins/audio_service/darwin`)
96
+ - audio_session (from `.symlinks/plugins/audio_session/ios`)
97
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
98
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
99
- clipboard (from `.symlinks/plugins/clipboard/ios`)
@@ -92,11 +105,13 @@ DEPENDENCIES:
105
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
106
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
107
- haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
108
+ - just_audio (from `.symlinks/plugins/just_audio/darwin`)
109
- large_file_handler (from `.symlinks/plugins/large_file_handler/ios`)
110
- objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`)
111
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
112
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
113
- sherpa_onnx_ios (from `.symlinks/plugins/sherpa_onnx_ios/ios`)
114
+ - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
115
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
116
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
117
@@ -111,6 +126,10 @@ SPEC REPOS:
126
- SwiftyGif
127
128
EXTERNAL SOURCES:
129
+ audio_service:
130
+ :path: ".symlinks/plugins/audio_service/darwin"
131
+ audio_session:
132
+ :path: ".symlinks/plugins/audio_session/ios"
133
audioplayers_darwin:
134
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
135
background_downloader:
@@ -133,6 +152,8 @@ EXTERNAL SOURCES:
152
:path: ".symlinks/plugins/flutter_tts/ios"
153
haptic_feedback:
154
:path: ".symlinks/plugins/haptic_feedback/ios"
155
+ just_audio:
156
+ :path: ".symlinks/plugins/just_audio/darwin"
157
large_file_handler:
158
:path: ".symlinks/plugins/large_file_handler/ios"
159
objectbox_flutter_libs:
@@ -143,12 +164,16 @@ EXTERNAL SOURCES:
164
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
165
sherpa_onnx_ios:
166
:path: ".symlinks/plugins/sherpa_onnx_ios/ios"
167
+ sqflite_darwin:
168
+ :path: ".symlinks/plugins/sqflite_darwin/darwin"
169
url_launcher_ios:
170
:path: ".symlinks/plugins/url_launcher_ios/ios"
171
wakelock_plus:
172
:path: ".symlinks/plugins/wakelock_plus/ios"
173
174
SPEC CHECKSUMS:
175
+ audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910
176
+ audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
177
audioplayers_darwin: f15e209a3e856d1a7edcf98dc029f484fead2242
178
background_downloader: a05c77d32a0d70615b9c04577aa203535fc924ff
179
clipboard: 1dfb1b37c0b722d20b0ca76a89bb2e7194e85811
@@ -162,6 +187,7 @@ SPEC CHECKSUMS:
187
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
188
flutter_tts: 91ad1884033cd95ef6e9338754adad54f01f090f
189
haptic_feedback: a366a27ec760f0e865651605b95de8a9bb04ef01
190
+ just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
191
large_file_handler: e3686e246052b0f03955789d3a04604574f80afe
192
MediaPipeTasksGenAI: 8eed8e6d03fe26d30c4205cb5295ec7b394432ad
193
MediaPipeTasksGenAIC: cb098ecea44e248efe8fe7e2f62b4cb0df781802
@@ -171,6 +197,7 @@ SPEC CHECKSUMS:
197
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
198
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
199
sherpa_onnx_ios: 9f536d001daeaca36fc812fa1b1234870df3bc91
200
+ sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
201
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
202
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
203
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
lib/app.dart
+7
-2
@@ -276,13 +276,18 @@ class AppShell extends StatelessWidget {
276
? const Color(0xFF1A1A1A)
277
: const Color(0xFFE5E5E5),
278
),
279
- Expanded(child: child),
279
+ Expanded(
280
+ child: child,
281
+ ),
282
],
283
),
284
);
285
}
286
285
- return Scaffold(body: child, drawer: const ConversationDrawer());
287
+ return Scaffold(
288
+ body: child,
289
+ drawer: const ConversationDrawer(),
290
+ );
291
},
292
);
293
}
lib/features/chat/views/chat_screen.dart
+2
-4
@@ -10,7 +10,6 @@ import '../../conversations/providers/conversation_providers.dart' as conv;
10
import '../../models/screens/model_picker_sheet.dart';
11
import '../../personas/providers/personas_providers.dart';
12
import '../../servers/providers/server_providers.dart';
13
-import '../../tts/views/components/tts_player_bar.dart';
13
import 'package:shadcn_ui/shadcn_ui.dart';
14
15
import '../../../core/models/enums.dart';
@@ -29,6 +28,7 @@ import 'components/chat_input_bar.dart';
28
import 'components/chat_settings_sheet.dart';
29
import 'components/edit_message_dialog.dart';
30
import 'components/notification_permission_banner.dart';
31
+import '../../tts/views/components/tts_player_bar.dart';
32
33
class ChatScreen extends ConsumerStatefulWidget {
34
const ChatScreen({super.key});
@@ -314,9 +314,6 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
314
),
315
),
316
const NotificationPermissionBanner(),
317
- const TtsPlayerBar(
318
- margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
319
- ),
317
if (connectionStatus == ConnectionStatus.disconnected ||
318
connectionStatus == ConnectionStatus.error)
319
_ConnectionBanner(status: connectionStatus),
@@ -333,6 +330,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
330
}
331
},
332
),
333
+ const TtsPlayerBar(),
334
Expanded(
335
child: Stack(
336
children: [
lib/features/chat/views/components/message_action_bar.dart
+25
-12
@@ -32,12 +32,13 @@ class _MessageActionBarState extends ConsumerState<MessageActionBar> {
32
void _toggleTts() async {
33
final ttsNotifier = ref.read(tts.ttsProvider.notifier);
34
final ttsState = ref.read(tts.ttsProvider);
35
- final isThisPlaying = ttsState.playingContent == widget.content && ttsState.isSpeaking;
35
+ final isThisActive = ttsState.playingContent == widget.content &&
36
+ (ttsState.isSpeaking || ttsState.isPaused);
37
37
- if (isThisPlaying) {
38
- await ttsNotifier.stop();
38
+ if (isThisActive) {
39
+ ttsNotifier.togglePauseResume();
40
} else {
40
- if (ttsState.isSpeaking) {
41
+ if (ttsState.isSpeaking || ttsState.isPaused) {
42
await ttsNotifier.stop();
43
}
44
try {
@@ -50,7 +51,9 @@ class _MessageActionBarState extends ConsumerState<MessageActionBar> {
51
Widget build(BuildContext context) {
52
final l10n = AppLocalizations.of(context)!;
53
final ttsState = ref.watch(tts.ttsProvider);
53
- final isThisPlaying = ttsState.playingContent == widget.content && ttsState.isSpeaking;
54
+ final isThisActive = ttsState.playingContent == widget.content &&
55
+ (ttsState.isSpeaking || ttsState.isPaused);
56
+ final isThisPlaying = ttsState.playingContent == widget.content && ttsState.isSpeaking && !ttsState.isPaused;
57
final isThisInitializing = ttsState.playingContent == widget.content && ttsState.isInitializing;
58
59
return Row(
@@ -103,11 +106,11 @@ class _MessageActionBarState extends ConsumerState<MessageActionBar> {
106
],
107
const SizedBox(width: 4),
108
_ActionButton(
106
- icon: isThisPlaying
107
- ? Icons.stop_circle
109
+ icon: isThisActive
110
+ ? (isThisPlaying ? Icons.pause_circle_filled : Icons.play_circle_filled)
111
: (isThisInitializing ? Icons.hourglass_top : Icons.volume_up),
109
- label: isThisPlaying
110
- ? l10n.stop
112
+ label: isThisActive
113
+ ? (isThisPlaying ? l10n.pause : l10n.resume)
114
: (isThisInitializing ? l10n.initializing : l10n.read_aloud),
115
onTap: isThisInitializing ? null : _toggleTts,
116
),
@@ -148,7 +151,9 @@ class _MessageActionBarState extends ConsumerState<MessageActionBar> {
151
void _showMoreOptions(BuildContext context) {
152
final sheetL10n = AppLocalizations.of(context)!;
153
final ttsState = ref.read(tts.ttsProvider);
151
- final isThisPlaying = ttsState.playingContent == widget.content && ttsState.isSpeaking;
154
+ final isThisActive = ttsState.playingContent == widget.content &&
155
+ (ttsState.isSpeaking || ttsState.isPaused);
156
+ final isThisPlaying = ttsState.playingContent == widget.content && ttsState.isSpeaking && !ttsState.isPaused;
157
158
showShadSheet(
159
context: context,
@@ -169,8 +174,16 @@ class _MessageActionBarState extends ConsumerState<MessageActionBar> {
174
},
175
),
176
ListTile(
172
- leading: Icon(isThisPlaying ? Icons.stop : Icons.volume_up),
173
- title: Text(isThisPlaying ? sheetL10n.stop_reading : sheetL10n.read_aloud),
177
+ leading: Icon(
178
+ isThisActive
179
+ ? (isThisPlaying ? Icons.pause_circle_filled : Icons.play_circle_filled)
180
+ : Icons.volume_up,
181
+ ),
182
+ title: Text(
183
+ isThisActive
184
+ ? (isThisPlaying ? sheetL10n.pause : sheetL10n.resume)
185
+ : sheetL10n.read_aloud,
186
+ ),
187
onTap: () {
188
Navigator.of(context).pop();
189
_toggleTts();
lib/features/sidebar/components/github_repo_card.dart
+40
-50
@@ -1,6 +1,5 @@
1
import 'package:flutter/material.dart';
2
import 'package:hugeicons/hugeicons.dart';
3
-import 'package:shadcn_ui/shadcn_ui.dart';
3
import 'package:url_launcher/url_launcher.dart';
4
5
import '../../../l10n/app_localizations.dart';
@@ -23,69 +22,60 @@ class GitHubRepoCard extends StatelessWidget {
22
final theme = Theme.of(context);
23
final isDark = theme.brightness == Brightness.dark;
24
26
- // Using standard colors from theme or defaults to ensure consistent premium look
25
final cardBg = isDark ? const Color(0xFF1F1F1F) : const Color(0xFFF9FAFB);
26
final borderColor = isDark ? const Color(0xFF374151) : const Color(0xFFE5E7EB);
27
final accentColor = isDark ? const Color(0xFF3B82F6) : const Color(0xFF2563EB);
28
29
return Padding(
30
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
33
- child: ShadCard(
34
- padding: const EdgeInsets.all(16),
35
- backgroundColor: cardBg,
36
- border: ShadBorder.all(color: borderColor),
37
- radius: BorderRadius.circular(12),
38
- title: Row(
39
- children: [
40
- HugeIcon(
41
- icon: HugeIcons.strokeRoundedGithub,
42
- size: 20,
43
- color: isDark ? Colors.white : Colors.black,
44
- ),
45
- const SizedBox(width: 8),
46
- Text(
47
- l10n.open_source,
48
- style: TextStyle(
49
- fontSize: 14,
50
- fontWeight: FontWeight.w600,
51
- color: isDark ? Colors.white : Colors.black,
52
- ),
53
- ),
54
- ],
55
- ),
56
- description: Padding(
57
- padding: const EdgeInsets.only(top: 4),
58
- child: Text(
59
- l10n.open_source_desc,
60
- style: TextStyle(
61
- fontSize: 12,
62
- color: isDark ? const Color(0xFFA0A0A0) : const Color(0xFF666666),
63
- height: 1.4,
64
- ),
65
- ),
31
+ child: Container(
32
+ decoration: BoxDecoration(
33
+ color: cardBg,
34
+ border: Border.all(color: borderColor),
35
+ borderRadius: BorderRadius.circular(10),
36
),
67
- footer: Padding(
68
- padding: const EdgeInsets.only(top: 12),
69
- child: ShadButton.ghost(
70
- width: double.infinity,
71
- padding: EdgeInsets.zero,
72
- onPressed: _launchUrl,
37
+ child: InkWell(
38
+ onTap: _launchUrl,
39
+ borderRadius: BorderRadius.circular(10),
40
+ child: Padding(
41
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
42
child: Row(
74
- mainAxisAlignment: MainAxisAlignment.center,
43
children: [
76
- Text(
77
- l10n.star_on_github,
78
- style: TextStyle(
79
- fontSize: 12,
80
- fontWeight: FontWeight.w600,
81
- color: accentColor,
44
+ HugeIcon(
45
+ icon: HugeIcons.strokeRoundedGithub,
46
+ size: 20,
47
+ color: isDark ? Colors.white70 : Colors.black87,
48
+ ),
49
+ const SizedBox(width: 10),
50
+ Expanded(
51
+ child: Column(
52
+ crossAxisAlignment: CrossAxisAlignment.start,
53
+ mainAxisSize: MainAxisSize.min,
54
+ children: [
55
+ Text(
56
+ l10n.open_source,
57
+ style: TextStyle(
58
+ fontSize: 13,
59
+ fontWeight: FontWeight.w600,
60
+ color: isDark ? Colors.white : Colors.black,
61
+ ),
62
+ ),
63
+ const SizedBox(height: 2),
64
+ Text(
65
+ l10n.star_on_github,
66
+ style: TextStyle(
67
+ fontSize: 11,
68
+ fontWeight: FontWeight.w500,
69
+ color: accentColor,
70
+ ),
71
+ ),
72
+ ],
73
),
74
),
84
- const SizedBox(width: 4),
75
Icon(
76
Icons.open_in_new,
77
size: 14,
88
- color: accentColor,
78
+ color: isDark ? Colors.white54 : Colors.black54,
79
),
80
],
81
),
lib/features/sidebar/sidebar_widget.dart
+2
-3
@@ -7,12 +7,12 @@ import 'package:shadcn_ui/shadcn_ui.dart';
7
import '../../core/routes/app_routes.dart';
8
import '../../l10n/app_localizations.dart';
9
import '../chat/providers/chat_providers.dart';
10
-import '../tts/views/components/tts_player_bar.dart';
10
import 'components/active_server_indicator.dart';
11
import 'components/conversation_drawer_header.dart';
12
import 'components/drawer_nav_item.dart';
13
import 'components/github_repo_card.dart';
14
import 'components/sidebar_search_button.dart';
15
+import '../tts/views/components/tts_player_bar.dart';
16
17
class SidebarWidget extends ConsumerWidget {
18
const SidebarWidget({super.key});
@@ -156,9 +156,8 @@ class SidebarWidget extends ConsumerWidget {
156
),
157
),
158
159
- // TTS Mini Player (only visible when TTS is active)
159
+ // Duplicate Player controls
160
const TtsPlayerBar(),
161
- const SizedBox(height: 4),
161
162
// Bottom Section
163
const GitHubRepoCard(),
lib/features/tts/providers/tts_providers.dart
+180
-61
@@ -3,8 +3,9 @@ import 'dart:io';
3
import 'dart:isolate';
4
import 'dart:typed_data';
5
6
-import 'package:audioplayers/audioplayers.dart';
7
-import 'package:flutter/services.dart';
6
+import 'package:audio_session/audio_session.dart';
7
+import 'package:just_audio/just_audio.dart';
8
+import 'package:just_audio_background/just_audio_background.dart';
9
import 'package:flutter_riverpod/flutter_riverpod.dart';
10
import 'package:flutter_tts/flutter_tts.dart';
11
@@ -21,32 +22,40 @@ final ttsProvider = NotifierProvider<TtsNotifier, TtsState>(() {
22
23
class TtsState {
24
final bool isSpeaking;
25
+ final bool isPaused;
26
final bool isInitializing;
27
final String? error;
28
final EngineId? activeEngine;
29
final String? playingContent;
30
+ final bool isPreview;
31
32
const TtsState({
33
this.isSpeaking = false,
34
+ this.isPaused = false,
35
this.isInitializing = false,
36
this.error,
37
this.activeEngine,
38
this.playingContent,
39
+ this.isPreview = false,
40
});
41
42
TtsState copyWith({
43
bool? isSpeaking,
44
+ bool? isPaused,
45
bool? isInitializing,
46
String? error,
47
EngineId? activeEngine,
48
String? playingContent,
49
+ bool? isPreview,
50
}) {
51
return TtsState(
52
isSpeaking: isSpeaking ?? this.isSpeaking,
53
+ isPaused: isPaused ?? this.isPaused,
54
isInitializing: isInitializing ?? this.isInitializing,
55
error: error,
56
activeEngine: activeEngine ?? this.activeEngine,
57
playingContent: playingContent,
58
+ isPreview: isPreview ?? this.isPreview,
59
);
60
}
61
}
@@ -78,10 +87,27 @@ class TtsNotifier extends Notifier<TtsState> {
87
int _currentSessionId = 0;
88
89
StreamSubscription<void>? _playerCompleteSubscription;
81
- bool _isProcessingChunk = false;
90
int _playbackSessionId = 0;
91
Completer<SendPort>? _workerPortCompleter;
92
93
+ bool _isPreview = false;
94
+ final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
95
+ final Map<int, AudioSource> _playlistBuffer = {};
96
+ int _nextPlaylistIndexToAdd = 0;
97
+ StreamSubscription<int?>? _currentIndexSubscription;
98
+
99
+ // Progress tracking
100
+ final Map<int, Duration> _chunkDurations = {};
101
+ Duration _totalDuration = Duration.zero;
102
+ Duration _chunkOffset = Duration.zero;
103
+ final StreamController<void> _onChunkChanged = StreamController<void>.broadcast();
104
+
105
+ AudioPlayer get player => _player;
106
+ Stream<Duration> get onPositionChanged => _player.positionStream;
107
+ Duration get totalDuration => _totalDuration;
108
+ Duration get chunkOffset => _chunkOffset;
109
+ Stream<void> get onChunkChanged => _onChunkChanged.stream;
110
+
111
@override
112
TtsState build() {
113
ref.keepAlive();
@@ -92,8 +118,20 @@ class TtsNotifier extends Notifier<TtsState> {
118
}
119
});
120
95
- _playerCompleteSubscription = _player.onPlayerComplete.listen((_) {
96
- _onChunkFinished();
121
+ _player.setAudioSource(_playlist);
122
+
123
+ _playerCompleteSubscription = _player.playerStateStream.listen((playerState) {
124
+ if (playerState.processingState == ProcessingState.completed) {
125
+ _onPlaybackFinished();
126
+ }
127
+ });
128
+
129
+ _currentIndexSubscription = _player.currentIndexStream.listen((index) {
130
+ if (index != null && index != _currentChunkIndex) {
131
+ _currentChunkIndex = index;
132
+ _onChunkChanged.add(null);
133
+ _enqueueNextChunks();
134
+ }
135
});
136
137
_setupWorkerListener();
@@ -104,6 +142,8 @@ class TtsNotifier extends Notifier<TtsState> {
142
_workerIsolate?.kill();
143
_mainReceivePort.close();
144
_playerCompleteSubscription?.cancel();
145
+ _currentIndexSubscription?.cancel();
146
+ _onChunkChanged.close();
147
if (!_isPlayerDisposed) {
148
_isPlayerDisposed = true;
149
try {
@@ -117,7 +157,7 @@ class TtsNotifier extends Notifier<TtsState> {
157
}
158
159
void _setupWorkerListener() {
120
- _mainReceivePort.listen((message) {
160
+ _mainReceivePort.listen((message) async {
161
if (message is SendPort) {
162
_workerSendPort = message;
163
_workerPortCompleter?.complete(message);
@@ -133,16 +173,14 @@ class TtsNotifier extends Notifier<TtsState> {
173
}
174
if (message.wavBytes != null) {
175
_synthesizedChunks[message.chunkIndex!] = message.wavBytes!;
136
- if (message.chunkIndex == _currentChunkIndex && state.isSpeaking) {
137
- _playNextChunk();
138
- }
176
+ _computeChunkDuration(message.chunkIndex!, message.wavBytes!);
177
+ await _addChunkToPlaylist(message.chunkIndex!, message.wavBytes!);
178
} else {
179
Log.error(
180
'Worker error for chunk ${message.chunkIndex}: ${message.error}',
181
);
143
- if (message.chunkIndex == _currentChunkIndex) {
144
- _onChunkFinished();
145
- }
182
+ _synthesizedChunks[message.chunkIndex!] = Uint8List(0);
183
+ await _addChunkToPlaylist(message.chunkIndex!, Uint8List(0));
184
}
185
} else if (message.error != null) {
186
Log.error('Worker general error: ${message.error}');
@@ -168,31 +206,56 @@ class TtsNotifier extends Notifier<TtsState> {
206
);
207
}
208
209
+ void _computeChunkDuration(int index, Uint8List wavBytes) {
210
+ if (wavBytes.length < 44) return;
211
+ final data = ByteData.view(wavBytes.buffer, wavBytes.offsetInBytes, 44);
212
+ final sampleRate = data.getUint32(24, Endian.little);
213
+ final dataSize = data.getUint32(40, Endian.little);
214
+ final sampleCount = dataSize ~/ 2;
215
+ final durationMs = (sampleCount * 1000) ~/ sampleRate;
216
+ _chunkDurations[index] = Duration(milliseconds: durationMs);
217
+ _totalDuration = Duration.zero;
218
+ for (final d in _chunkDurations.values) {
219
+ _totalDuration += d;
220
+ }
221
+ }
222
+
223
+ void _resetProgress() {
224
+ _chunkDurations.clear();
225
+ _totalDuration = Duration.zero;
226
+ _chunkOffset = Duration.zero;
227
+ }
228
+
229
+ void _deleteChunkFile(int sessionId, int chunkIndex) {
230
+ if (chunkIndex < 0) return;
231
+ try {
232
+ final tempFile = File(
233
+ '${Directory.systemTemp.path}/tts_chunk_${sessionId}_$chunkIndex.wav',
234
+ );
235
+ if (tempFile.existsSync()) tempFile.deleteSync();
236
+ } catch (_) {}
237
+ }
238
+
239
+ void _cleanupSessionFiles(int sessionId, int chunkCount) {
240
+ for (var i = 0; i < chunkCount; i++) {
241
+ _deleteChunkFile(sessionId, i);
242
+ }
243
+ }
244
+
245
void _onPlaybackFinished() {
246
if (_isStopping) return;
247
if (_currentSessionId != _playbackSessionId) return;
248
+ _cleanupSessionFiles(_currentSessionId, _chunks.length);
249
+ _resetProgress();
250
state = state.copyWith(
251
isSpeaking: false,
252
+ isPaused: false,
253
isInitializing: false,
254
playingContent: null,
255
+ isPreview: false,
256
);
257
}
258
181
- void _onChunkFinished() {
182
- if (_isStopping || _isProcessingChunk) return;
183
- _isProcessingChunk = true;
184
- try {
185
- _currentChunkIndex++;
186
- if (_currentChunkIndex < _chunks.length) {
187
- _playNextChunk();
188
- } else {
189
- _onPlaybackFinished();
190
- }
191
- } finally {
192
- _isProcessingChunk = false;
193
- }
194
- }
195
-
259
List<String> _splitText(String text) {
260
if (text.length <= 250) return [text];
261
@@ -336,7 +399,7 @@ class TtsNotifier extends Notifier<TtsState> {
399
}
400
}
401
339
- Future<void> speak(String text, {EngineId? engineId, String? voiceId}) async {
402
+ Future<void> speak(String text, {EngineId? engineId, String? voiceId, bool isPreview = false}) async {
403
if (text.trim().isEmpty) return;
404
405
_isStopping = false;
@@ -357,12 +420,17 @@ class TtsNotifier extends Notifier<TtsState> {
420
421
if (engineId == EngineId.system) {
422
_flutterTts ??= FlutterTts();
423
+ // Clear any previous handlers to prevent stale callbacks
424
+ _flutterTts!.setCompletionHandler(() {});
425
+ _flutterTts!.setErrorHandler((_) {});
426
+ _flutterTts!.setCancelHandler(() {});
427
state = state.copyWith(
428
isSpeaking: true,
429
isInitializing: false,
430
error: null,
431
activeEngine: EngineId.system,
432
playingContent: text,
433
+ isPreview: isPreview,
434
);
435
try {
436
await _flutterTts!.setLanguage('en-US');
@@ -374,6 +442,20 @@ class TtsNotifier extends Notifier<TtsState> {
442
state = state.copyWith(isSpeaking: false, playingContent: null);
443
}
444
});
445
+ _flutterTts!.setErrorHandler((msg) {
446
+ if (!_isStopping) {
447
+ state = state.copyWith(
448
+ isSpeaking: false,
449
+ playingContent: null,
450
+ error: msg.toString(),
451
+ );
452
+ }
453
+ });
454
+ _flutterTts!.setCancelHandler(() {
455
+ if (!_isStopping) {
456
+ state = state.copyWith(isSpeaking: false, playingContent: null);
457
+ }
458
+ });
459
await _flutterTts!.awaitSpeakCompletion(true);
460
await _flutterTts!.speak(text);
461
} catch (e) {
@@ -387,6 +469,7 @@ class TtsNotifier extends Notifier<TtsState> {
469
return;
470
}
471
472
+ _resetProgress();
473
_chunks = _splitText(text);
474
_currentChunkIndex = 0;
475
_currentVoice = resolvedVoiceId;
@@ -395,18 +478,33 @@ class TtsNotifier extends Notifier<TtsState> {
478
_currentSessionId++;
479
_playbackSessionId = _currentSessionId;
480
481
+ _isPreview = isPreview;
482
+ _playlistBuffer.clear();
483
+ _nextPlaylistIndexToAdd = 0;
484
+ await _playlist.clear();
485
+
486
+ try {
487
+ AudioSession.instance.then((session) {
488
+ session.configure(const AudioSessionConfiguration.speech());
489
+ });
490
+ } catch (e) {
491
+ Log.error('Error configuring audio session: $e');
492
+ }
493
+
494
state = state.copyWith(
495
isSpeaking: true,
496
+ isPaused: false,
497
isInitializing: true,
498
error: null,
499
activeEngine: engineId,
500
playingContent: text,
501
+ isPreview: isPreview,
502
);
503
504
try {
505
await _initEngine(engineId, voiceId: resolvedVoiceId);
506
state = state.copyWith(isInitializing: false);
409
- await _playNextChunk();
507
+ _enqueueNextChunks();
508
} catch (e, st) {
509
if (_isPlayerDisposed) return;
510
Log.error('TTS speak error: $e\n$st');
@@ -419,41 +517,37 @@ class TtsNotifier extends Notifier<TtsState> {
517
}
518
}
519
422
- Future<void> _playNextChunk() async {
423
- if (_isStopping || _currentChunkIndex >= _chunks.length) return;
424
-
425
- _enqueueNextChunks();
426
-
427
- if (!_synthesizedChunks.containsKey(_currentChunkIndex)) {
428
- Log.debug('Waiting for chunk $_currentChunkIndex to be synthesized...');
429
- return;
430
- }
431
-
432
- final wavBytes = _synthesizedChunks[_currentChunkIndex]!;
433
- _synthesizedChunks.remove(_currentChunkIndex);
434
-
435
- try {
436
- Log.debug(
437
- 'Playing chunk ${_currentChunkIndex + 1}/${_chunks.length}: "${_chunks[_currentChunkIndex]}"',
438
- );
520
+ Future<void> _addChunkToPlaylist(int chunkIndex, Uint8List wavBytes) async {
521
+ if (_isStopping || chunkIndex >= _chunks.length) return;
522
440
- await _player.stop();
523
+ final tempDir = Directory.systemTemp;
524
+ final tempFile = File(
525
+ '${tempDir.path}/tts_chunk_${_currentSessionId}_$chunkIndex.wav',
526
+ );
527
+ await tempFile.writeAsBytes(wavBytes);
528
+
529
+ final source = AudioSource.file(
530
+ tempFile.path,
531
+ tag: _isPreview ? null : MediaItem(
532
+ id: 'tts_chunk_${_currentSessionId}_$chunkIndex',
533
+ album: 'LocalMind TTS',
534
+ title: _chunks[chunkIndex],
535
+ artist: _currentEngine == EngineId.kitten ? 'Kitten TTS' : 'Piper TTS',
536
+ ),
537
+ );
538
442
- final tempDir = Directory.systemTemp;
443
- final tempFile = File(
444
- '${tempDir.path}/tts_chunk_${_currentSessionId}_$_currentChunkIndex.wav',
445
- );
446
- await tempFile.writeAsBytes(wavBytes);
539
+ _playlistBuffer[chunkIndex] = source;
540
+ while (_playlistBuffer.containsKey(_nextPlaylistIndexToAdd)) {
541
+ final src = _playlistBuffer.remove(_nextPlaylistIndexToAdd)!;
542
+ await _playlist.add(src);
543
448
- await _player.play(DeviceFileSource(tempFile.path));
449
- } catch (e) {
450
- Log.error('Error playing chunk $_currentChunkIndex: $e');
451
- _currentChunkIndex++;
452
- if (_currentChunkIndex < _chunks.length) {
453
- _playNextChunk();
454
- } else {
455
- _onPlaybackFinished();
544
+ if (_nextPlaylistIndexToAdd == 0) {
545
+ if (!state.isPaused) {
546
+ _player.play();
547
+ }
548
}
549
+
550
+ _nextPlaylistIndexToAdd++;
551
}
552
}
553
@@ -486,22 +580,47 @@ class TtsNotifier extends Notifier<TtsState> {
580
}
581
}
582
583
+ void pause() {
584
+ if (state.isPaused || !state.isSpeaking) return;
585
+ _player.pause();
586
+ state = state.copyWith(isPaused: true);
587
+ }
588
+
589
+ void resume() {
590
+ if (!state.isPaused) return;
591
+ _player.play();
592
+ state = state.copyWith(isPaused: false);
593
+ }
594
+
595
+ void togglePauseResume() {
596
+ if (state.isPaused) {
597
+ resume();
598
+ } else if (state.isSpeaking) {
599
+ pause();
600
+ }
601
+ }
602
+
603
Future<void> stop() async {
604
_isStopping = true;
605
if (_isPlayerDisposed) return;
606
try {
607
await _player.stop();
608
+ await _playlist.clear();
609
} catch (_) {}
610
await _flutterTts?.stop();
611
+ _cleanupSessionFiles(_currentSessionId, _chunks.length);
612
+ _resetProgress();
613
state = state.copyWith(
614
isSpeaking: false,
615
+ isPaused: false,
616
isInitializing: false,
617
playingContent: null,
618
+ isPreview: false,
619
);
620
}
621
622
Future<void> previewVoice(Voice voice) async {
504
- await speak(ttsPreviewSample, engineId: voice.engine, voiceId: voice.id);
623
+ await speak(ttsPreviewSample, engineId: voice.engine, voiceId: voice.id, isPreview: true);
624
}
625
}
626
lib/features/tts/views/components/tts_player_bar.dart
+180
-91
@@ -1,23 +1,33 @@
1
+import 'dart:math' as math;
2
import 'package:flutter/material.dart';
3
import 'package:flutter_riverpod/flutter_riverpod.dart';
4
import 'package:localmind/l10n/app_localizations.dart';
5
import '../../providers/tts_providers.dart' as tts;
6
6
-/// Mini TTS player bar shown when speech is playing or
7
-/// initializing. Displays a waveform icon, a content preview, and a stop button.
7
class TtsPlayerBar extends ConsumerWidget {
8
final EdgeInsetsGeometry margin;
10
-
9
+
10
const TtsPlayerBar({
12
- super.key,
13
- this.margin = const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
11
+ super.key,
12
+ this.margin = const EdgeInsets.only(left: 16, right: 16, bottom: 8),
13
});
14
15
+ String? _truncateContent(String? content) {
16
+ if (content == null || content.isEmpty) return null;
17
+ if (content.length <= 50) return content;
18
+ return '${content.substring(0, 47)}...';
19
+ }
20
+
21
@override
22
Widget build(BuildContext context, WidgetRef ref) {
23
final l10n = AppLocalizations.of(context)!;
24
final ttsState = ref.watch(tts.ttsProvider);
20
- ref.listen(tts.ttsProvider, (prev, next) {
25
+ final notifier = ref.read(tts.ttsProvider.notifier);
26
+
27
+ final theme = Theme.of(context);
28
+ final isDark = theme.brightness == Brightness.dark;
29
+
30
+ ref.listen<tts.TtsState>(tts.ttsProvider, (prev, next) {
31
if (next.error != null && prev?.error != next.error) {
32
ScaffoldMessenger.of(context).showSnackBar(
33
SnackBar(
@@ -28,10 +38,8 @@ class TtsPlayerBar extends ConsumerWidget {
38
);
39
}
40
});
31
- final theme = Theme.of(context);
32
- final isDark = theme.brightness == Brightness.dark;
41
34
- if (!ttsState.isSpeaking && !ttsState.isInitializing) {
42
+ if ((!ttsState.isSpeaking && !ttsState.isInitializing) || ttsState.isPreview) {
43
return const SizedBox.shrink();
44
}
45
@@ -43,87 +51,134 @@ class TtsPlayerBar extends ConsumerWidget {
51
child: Container(
52
width: double.infinity,
53
margin: margin,
46
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
54
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
55
decoration: BoxDecoration(
48
- color: isDark ? const Color(0xFF1E293B) : const Color(0xFFEFF6FF),
49
- borderRadius: BorderRadius.circular(10),
56
+ color: theme.colorScheme.surface,
57
+ borderRadius: BorderRadius.circular(12),
58
border: Border.all(
51
- color: isDark ? const Color(0xFF334155) : const Color(0xFFBFDBFE),
59
+ color: theme.colorScheme.outline.withValues(alpha: 0.2),
60
),
61
+ boxShadow: [
62
+ BoxShadow(
63
+ color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.05),
64
+ blurRadius: 10,
65
+ offset: const Offset(0, 4),
66
+ ),
67
+ ],
68
),
54
- child: Row(
69
+ child: Column(
70
+ mainAxisSize: MainAxisSize.min,
71
children: [
56
- _AnimatedWaveIndicator(isActive: ttsState.isSpeaking),
57
- const SizedBox(width: 10),
58
- Expanded(
59
- child: Column(
60
- crossAxisAlignment: CrossAxisAlignment.start,
61
- mainAxisSize: MainAxisSize.min,
62
- children: [
63
- Text(
64
- preview ??
65
- (ttsState.isInitializing ? l10n.loading : 'Playing'),
66
- style: TextStyle(
67
- fontSize: 12,
68
- fontWeight: FontWeight.w500,
69
- color: isDark ? Colors.white70 : Colors.black87,
72
+ Row(
73
+ children: [
74
+ GestureDetector(
75
+ onTap: () => notifier.togglePauseResume(),
76
+ child: Tooltip(
77
+ message: ttsState.isPaused ? l10n.resume : l10n.pause,
78
+ child: Container(
79
+ padding: const EdgeInsets.all(6),
80
+ decoration: BoxDecoration(
81
+ color: theme.colorScheme.secondary.withValues(alpha: 0.1),
82
+ borderRadius: BorderRadius.circular(8),
83
+ ),
84
+ child: Icon(
85
+ ttsState.isPaused
86
+ ? Icons.play_arrow_rounded
87
+ : Icons.pause_rounded,
88
+ size: 20,
89
+ color: theme.colorScheme.primary,
90
+ ),
91
),
71
- maxLines: 1,
72
- overflow: TextOverflow.ellipsis,
92
),
74
- if (ttsState.isInitializing)
75
- Text(
76
- l10n.initializing,
77
- style: TextStyle(
78
- fontSize: 10,
79
- color: isDark ? Colors.white38 : Colors.black45,
93
+ ),
94
+ const SizedBox(width: 12),
95
+ Expanded(
96
+ child: Column(
97
+ crossAxisAlignment: CrossAxisAlignment.start,
98
+ mainAxisSize: MainAxisSize.min,
99
+ children: [
100
+ Text(
101
+ preview ??
102
+ (ttsState.isInitializing
103
+ ? l10n.loading
104
+ : 'Playing'),
105
+ style: TextStyle(
106
+ fontSize: 12,
107
+ fontWeight: FontWeight.w600,
108
+ color: theme.colorScheme.onSurface,
109
+ ),
110
+ maxLines: 1,
111
+ overflow: TextOverflow.ellipsis,
112
),
81
- ),
82
- ],
83
- ),
84
- ),
85
- const SizedBox(width: 8),
86
- GestureDetector(
87
- onTap: () => ref.read(tts.ttsProvider.notifier).stop(),
88
- child: Tooltip(
89
- message: l10n.stop,
90
- child: Container(
91
- padding: const EdgeInsets.all(4),
92
- decoration: BoxDecoration(
93
- color: Colors.red.withValues(alpha: 0.1),
94
- borderRadius: BorderRadius.circular(6),
113
+ if (ttsState.isInitializing)
114
+ Padding(
115
+ padding: const EdgeInsets.only(top: 2),
116
+ child: Text(
117
+ l10n.initializing,
118
+ style: TextStyle(
119
+ fontSize: 10,
120
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
121
+ ),
122
+ ),
123
+ ),
124
+ if (!ttsState.isInitializing)
125
+ Padding(
126
+ padding: const EdgeInsets.only(top: 4),
127
+ child: ClipRRect(
128
+ borderRadius: BorderRadius.circular(2),
129
+ child: LinearProgressIndicator(
130
+ backgroundColor: theme.colorScheme.secondary,
131
+ valueColor: AlwaysStoppedAnimation<Color>(
132
+ theme.colorScheme.primary,
133
+ ),
134
+ minHeight: 3,
135
+ ),
136
+ ),
137
+ ),
138
+ ],
139
),
96
- child: Icon(
97
- Icons.stop_circle,
98
- size: 20,
99
- color: isDark ? Colors.red[300] : Colors.red[600],
140
+ ),
141
+ const SizedBox(width: 12),
142
+ if (!ttsState.isInitializing)
143
+ _WaveformIndicator(isPlaying: !ttsState.isPaused),
144
+ const SizedBox(width: 12),
145
+ GestureDetector(
146
+ onTap: () => notifier.stop(),
147
+ child: Tooltip(
148
+ message: l10n.stop,
149
+ child: Container(
150
+ padding: const EdgeInsets.all(6),
151
+ decoration: BoxDecoration(
152
+ color: theme.colorScheme.error.withValues(alpha: 0.1),
153
+ borderRadius: BorderRadius.circular(8),
154
+ ),
155
+ child: Icon(
156
+ Icons.stop_rounded,
157
+ size: 20,
158
+ color: theme.colorScheme.error,
159
+ ),
160
),
161
),
162
),
103
- ),
104
- ],
105
- ),
163
+ ],
164
+ ),
165
+ ],
166
),
107
- );
108
- }
109
-
110
- String? _truncateContent(String? content) {
111
- if (content == null || content.isEmpty) return null;
112
- if (content.length <= 50) return content;
113
- return '${content.substring(0, 47)}...';
167
+ ),
168
+ );
169
}
170
}
171
117
-/// An animated audio wave indicator with a subtle bounce effect.
118
-class _AnimatedWaveIndicator extends StatefulWidget {
119
- final bool isActive;
120
- const _AnimatedWaveIndicator({required this.isActive});
172
+class _WaveformIndicator extends StatefulWidget {
173
+ final bool isPlaying;
174
+
175
+ const _WaveformIndicator({required this.isPlaying});
176
177
@override
123
- State<_AnimatedWaveIndicator> createState() => _AnimatedWaveIndicatorState();
178
+ State<_WaveformIndicator> createState() => _WaveformIndicatorState();
179
}
180
126
-class _AnimatedWaveIndicatorState extends State<_AnimatedWaveIndicator>
181
+class _WaveformIndicatorState extends State<_WaveformIndicator>
182
with SingleTickerProviderStateMixin {
183
late AnimationController _controller;
184
@@ -131,21 +186,23 @@ class _AnimatedWaveIndicatorState extends State<_AnimatedWaveIndicator>
186
void initState() {
187
super.initState();
188
_controller = AnimationController(
134
- duration: const Duration(milliseconds: 800),
189
vsync: this,
190
+ duration: const Duration(milliseconds: 1000),
191
);
137
- if (widget.isActive) {
138
- _controller.repeat(reverse: true);
192
+ if (widget.isPlaying) {
193
+ _controller.repeat();
194
}
195
}
196
197
@override
143
- void didUpdateWidget(_AnimatedWaveIndicator oldWidget) {
198
+ void didUpdateWidget(covariant _WaveformIndicator oldWidget) {
199
super.didUpdateWidget(oldWidget);
145
- if (widget.isActive && !_controller.isAnimating) {
146
- _controller.repeat(reverse: true);
147
- } else if (!widget.isActive && _controller.isAnimating) {
148
- _controller.stop();
200
+ if (widget.isPlaying != oldWidget.isPlaying) {
201
+ if (widget.isPlaying) {
202
+ _controller.repeat();
203
+ } else {
204
+ _controller.stop();
205
+ }
206
}
207
}
208
@@ -155,21 +212,53 @@ class _AnimatedWaveIndicatorState extends State<_AnimatedWaveIndicator>
212
super.dispose();
213
}
214
215
+ Widget _buildBar(double height, Color color) {
216
+ return Container(
217
+ width: 3,
218
+ height: height,
219
+ decoration: BoxDecoration(
220
+ color: color,
221
+ borderRadius: BorderRadius.circular(1.5),
222
+ ),
223
+ );
224
+ }
225
+
226
@override
227
Widget build(BuildContext context) {
160
- return AnimatedBuilder(
161
- animation: _controller,
162
- builder: (context, child) {
163
- final scale = widget.isActive ? 1.0 + _controller.value * 0.15 : 1.0;
164
- return Transform.scale(
165
- scale: scale,
166
- child: Icon(
167
- Icons.graphic_eq,
168
- size: 18,
169
- color: widget.isActive ? const Color(0xFF3B82F6) : Colors.grey,
170
- ),
171
- );
172
- },
228
+ final theme = Theme.of(context);
229
+ final barColor = theme.colorScheme.primary;
230
+
231
+ return Container(
232
+ height: 16,
233
+ alignment: Alignment.bottomCenter,
234
+ child: AnimatedBuilder(
235
+ animation: _controller,
236
+ builder: (context, child) {
237
+ final t = _controller.value;
238
+
239
+ final h1 = widget.isPlaying
240
+ ? 4.0 + 12.0 * (0.5 + 0.5 * math.sin(t * 2 * math.pi))
241
+ : 4.0;
242
+ final h2 = widget.isPlaying
243
+ ? 4.0 + 12.0 * (0.5 + 0.5 * math.sin(t * 2 * math.pi + math.pi / 3))
244
+ : 6.0;
245
+ final h3 = widget.isPlaying
246
+ ? 4.0 + 12.0 * (0.5 + 0.5 * math.sin(t * 2 * math.pi + 2 * math.pi / 3))
247
+ : 4.0;
248
+
249
+ return Row(
250
+ mainAxisSize: MainAxisSize.min,
251
+ crossAxisAlignment: CrossAxisAlignment.end,
252
+ children: [
253
+ _buildBar(h1, barColor),
254
+ const SizedBox(width: 2),
255
+ _buildBar(h2, barColor),
256
+ const SizedBox(width: 2),
257
+ _buildBar(h3, barColor),
258
+ ],
259
+ );
260
+ },
261
+ ),
262
);
263
}
264
}
lib/features/tts/views/tts_model_manager_screen.dart
+66
-9
@@ -80,13 +80,32 @@ class _SystemVoicesList extends ConsumerWidget {
80
@override
81
Widget build(BuildContext context, WidgetRef ref) {
82
final l10n = AppLocalizations.of(context)!;
83
+ final theme = Theme.of(context);
84
return Padding(
85
padding: const EdgeInsets.only(top: 12),
85
- child: Text(
86
- l10n.tts_system_desc,
87
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
88
- color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
89
- ),
86
+ child: Column(
87
+ crossAxisAlignment: CrossAxisAlignment.start,
88
+ children: [
89
+ Text(
90
+ l10n.tts_system_desc,
91
+ style: theme.textTheme.bodySmall?.copyWith(
92
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
93
+ ),
94
+ ),
95
+ const SizedBox(height: 12),
96
+ ShadAlert(
97
+ icon: Icon(
98
+ Icons.info_outline,
99
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
100
+ ),
101
+ description: Text(
102
+ l10n.tts_other_services_background_note,
103
+ style: theme.textTheme.labelSmall?.copyWith(
104
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
105
+ ),
106
+ ),
107
+ ),
108
+ ],
109
),
110
);
111
}
@@ -135,6 +154,22 @@ class _KittenEngineCard extends ConsumerWidget {
154
child: Column(
155
crossAxisAlignment: CrossAxisAlignment.start,
156
children: [
157
+ Padding(
158
+ padding: const EdgeInsets.only(top: 12),
159
+ child: ShadBadge.secondary(
160
+ child: Row(
161
+ mainAxisSize: MainAxisSize.min,
162
+ children: [
163
+ const Icon(Icons.music_note, size: 12),
164
+ const SizedBox(width: 4),
165
+ Text(
166
+ l10n.tts_supports_background,
167
+ style: const TextStyle(fontSize: 10),
168
+ ),
169
+ ],
170
+ ),
171
+ ),
172
+ ),
173
Padding(
174
padding: const EdgeInsets.only(top: 12),
175
child: Text(
@@ -351,6 +386,22 @@ class _PiperEngineCard extends ConsumerWidget {
386
child: Column(
387
crossAxisAlignment: CrossAxisAlignment.start,
388
children: [
389
+ Padding(
390
+ padding: const EdgeInsets.only(top: 12),
391
+ child: ShadBadge.secondary(
392
+ child: Row(
393
+ mainAxisSize: MainAxisSize.min,
394
+ children: [
395
+ const Icon(Icons.music_note, size: 12),
396
+ const SizedBox(width: 4),
397
+ Text(
398
+ l10n.tts_supports_background,
399
+ style: const TextStyle(fontSize: 10),
400
+ ),
401
+ ],
402
+ ),
403
+ ),
404
+ ),
405
Padding(
406
padding: const EdgeInsets.only(top: 12),
407
child: Text(
@@ -756,6 +807,14 @@ class _VoiceChipsState extends ConsumerState<_VoiceChips> {
807
808
@override
809
Widget build(BuildContext context) {
810
+ ref.listen<tts.TtsState>(tts.ttsProvider, (previous, next) {
811
+ if (!next.isSpeaking && (previous?.isSpeaking ?? false)) {
812
+ if (mounted && _playingVoice != null) {
813
+ setState(() => _playingVoice = null);
814
+ }
815
+ }
816
+ });
817
+
818
final l10n = AppLocalizations.of(context)!;
819
final theme = Theme.of(context);
820
final settings = ref.watch(settingsProvider);
@@ -938,13 +997,11 @@ class _VoiceChipsState extends ConsumerState<_VoiceChips> {
997
return;
998
}
999
try {
941
- await ref.read(tts.ttsProvider.notifier).previewVoice(voice);
1000
if (mounted) setState(() => _playingVoice = voice);
943
- Future.delayed(const Duration(seconds: 2), () {
944
- if (mounted) setState(() => _playingVoice = null);
945
- });
1001
+ await ref.read(tts.ttsProvider.notifier).previewVoice(voice);
1002
} catch (e) {
1003
if (mounted) {
1004
+ setState(() => _playingVoice = null);
1005
ScaffoldMessenger.of(
1006
context,
1007
).showSnackBar(SnackBar(content: Text(l10n.preview_failed(e.toString()))));
lib/l10n/app_ar.arb
+4
-447
@@ -1,674 +1,339 @@
1
{
2
"@@locale": "ar",
3
-
3
"app_name": "LocalMind",
5
-
4
"app_tagline": "ذكاؤك. جهازك. قواعدك.",
7
-
5
"app_version": "1.0.0",
9
-
6
"cancel": "إلغاء",
11
-
7
"confirm": "تأكيد",
13
-
8
"delete": "حذف",
15
-
9
"save": "حفظ",
17
-
10
"retry": "إعادة المحاولة",
19
-
11
"close": "إغلاق",
21
-
12
"done": "تم",
23
-
13
"continue_action": "متابعة",
25
-
14
"skip": "تخطي",
27
-
15
"install": "تثبيت",
29
-
16
"download": "تنزيل",
31
-
17
"resume": "استئناف",
33
-
18
"pause": "إيقاف مؤقت",
35
-
19
"stop": "إيقاف",
37
-
20
"edit": "تعديل",
39
-
21
"preview": "معاينة",
41
-
22
"unload": "تفريغ",
43
-
23
"load": "تحميل",
45
-
24
"rename": "إعادة تسمية",
47
-
25
"pin": "تثبيت",
49
-
26
"unpin": "إلغاء التثبيت",
51
-
27
"share": "مشاركة",
53
-
28
"copy": "نسخ",
55
-
29
"copied": "تم النسخ!",
57
-
30
"copied_to_clipboard": "تم النسخ إلى الحافظة",
59
-
31
"select": "اختيار",
61
-
32
"active": "نشط",
63
-
33
"all": "الكل",
65
-
34
"none": "لا شيء",
67
-
35
"none_selected": "لم يتم اختيار شيء",
69
-
36
"online": "متصل",
71
-
37
"offline": "غير متصل",
73
-
38
"error": "خطأ",
75
-
39
"unknown_error": "خطأ غير معروف",
77
-
40
"not_now": "ليس الآن",
79
-
41
"enable": "تفعيل",
81
-
42
"proceed_anyway": "متابعة على أي حال",
83
-
43
"test_connection": "اختبار الاتصال",
85
-
44
"testing": "جارٍ الاختبار...",
87
-
45
"connection_successful": "تم الاتصال بنجاح!",
89
-
46
"connection_failed": "فشل الاتصال. تحقق من إعداداتك.",
91
-
47
"save_continue": "حفظ ومتابعة",
93
-
48
"save_changes": "حفظ التغييرات",
95
-
49
"finish_setup": "إنهاء الإعداد",
97
-
50
"start_new_chat": "بدء محادثة جديدة",
99
-
51
"cannot_undo": "لا يمكن التراجع عن هذا الإجراء.",
101
-
52
"ram_warning": "تحذير الذاكرة",
103
-
53
"recommended": "موصى به",
105
-
54
"may_be_large": "قد يكون كبيراً جداً لهذا الجهاز",
107
-
55
"calculating": "جارٍ الحساب...",
109
-
56
"download_failed": "فشل التنزيل",
111
-
57
"downloaded": "تم التنزيل",
113
-
58
"not_downloaded": "لم يتم التنزيل",
115
-
59
"installed": "مثبت",
117
-
60
"not_installed": "غير مثبت",
119
-
61
"loading": "جارٍ التحميل...",
121
-
62
"thinking": "تفكير",
123
-
63
"processing": "جارٍ المعالجة...",
125
-
64
"initializing": "جارٍ التهيئة...",
127
-
65
"ready": "جاهز",
129
-
66
"preparing_app": "جارٍ تجهيز التطبيق...",
131
-
67
"initializing_services": "جارٍ تهيئة الخدمات...",
133
-
68
"configuring_server": "جارٍ إعداد الخادم...",
135
-
69
"startup_failed": "فشل بدء التشغيل",
137
-
70
"something_went_wrong": "حدث خطأ ما",
139
-
71
"delete_model_title": "حذف النموذج",
141
-
72
"delete_model_body": "هل أنت متأكد من حذف {name}؟",
143
-
73
"delete_model_body_with_size": "هل أنت متأكد من حذف {name}؟ سيؤدي ذلك إلى تحرير مساحة تقارب {size}.\n\nيمكنك تنزيل هذا النموذج مرة أخرى لاحقاً إذا احتجت إليه.",
145
-
74
"delete_voice_title": "حذف الصوت",
147
-
75
"delete_voice_body": "هل أنت متأكد من حذف {name}؟ سيؤدي ذلك إلى تحرير مساحة تقارب {size}.\n\nيمكنك تنزيل هذا الصوت مرة أخرى لاحقاً إذا احتجت إليه.",
149
-
76
"delete_server_title": "حذف الخادم",
151
-
77
"delete_server_body": "هل أنت متأكد من حذف \"{name}\"؟ لا يمكن التراجع عن هذا.",
153
-
78
"delete_conversation_title": "حذف المحادثة؟",
155
-
79
"delete_conversation_body": "هل أنت متأكد من حذف \"{title}\"؟ لا يمكن التراجع عن هذا.",
157
-
80
"delete_message_title": "حذف الرسالة؟",
159
-
81
"delete_persona_title": "حذف \"{name}\"؟",
161
-
82
"delete_persona_body": "لا يمكن التراجع عن هذا.",
163
-
83
"clear_conversation_title": "مسح المحادثة؟",
165
-
84
"clear_conversation_body": "سيؤدي هذا إلى حذف جميع الرسائل في هذه المحادثة.",
167
-
85
"clear": "مسح",
169
-
86
"label_completed": "اكتمل {label}",
171
-
87
"error_with_message": "خطأ: {error}",
173
-
88
"preview_failed": "فشلت المعاينة: {error}",
175
-
89
"loading_model": "جارٍ تحميل {modelId}...",
177
-
90
"model_loaded": "تم تحميل النموذج: {modelId} ({backend})",
179
-
91
"no_model_loaded": "لم يتم تحميل أي نموذج. اضغط على \"إدارة النماذج المحلية\" لتنزيل وتحميل نموذج.",
181
-
92
"loading_model_error": "خطأ: {error}",
183
-
93
"delete_conversation": "حذف المحادثة؟",
185
-
94
"nav_history": "السجل",
187
-
95
"nav_servers": "الخوادم",
189
-
96
"nav_local_models": "النماذج المحلية",
191
-
97
"nav_tts": "تحويل النص إلى كلام",
193
-
98
"nav_personas": "الشخصيات",
195
-
99
"nav_settings": "الإعدادات",
197
-
100
"nav_new_chat": "محادثة جديدة",
199
-
101
"search_hint": "البحث في المحادثات...",
201
-
102
"no_server_selected": "لم يتم تحديد خادم",
203
-
103
"switch_server": "تغيير الخادم",
205
-
104
"switch_server_subtitle": "اختر خادماً للاتصال به",
207
-
105
"manage_servers": "إدارة الخوادم",
209
-
106
"open_source": "مفتوح المصدر",
211
-
107
"open_source_desc": "LocalMind هو تطبيق مفتوح المصدر. تابع تقدمنا أو ساهم على GitHub.",
213
-
108
"star_on_github": "نجمة على GitHub",
215
-
109
"add_more": "أضف المزيد",
217
-
110
"on_github": "على GitHub",
219
-
111
"could_not_open_github": "تعذر فتح GitHub.",
221
-
112
"openai_compatible_api": "واجهة متوافقة مع OpenAI",
223
-
113
"https_requires_ssl": "يتطلب HTTPS استخدام SSL",
225
-
114
"most_local_setups_use_http": "تستخدم معظم الإعدادات المحلية http://",
227
-
115
"settings_title": "الإعدادات",
229
-
116
"settings_appearance": "المظهر",
231
-
117
"settings_language": "اللغة",
233
-
118
"language_system_default": "إعدادات النظام",
235
-
119
"settings_tts": "تحويل النص إلى كلام",
237
-
120
"settings_behavior": "السلوك",
239
-
121
"settings_on_device": "الاستدلال المحلي",
241
-
122
"settings_default_server": "الخادم الافتراضي",
243
-
123
"settings_default_persona": "الشخصية الافتراضية",
245
-
124
"settings_privacy": "الخصوصية",
247
-
125
"settings_data_management": "إدارة البيانات",
249
-
126
"settings_about": "حول",
251
-
127
"theme": "السمة",
253
-
128
"theme_system": "النظام",
255
-
129
"theme_light": "فاتح",
257
-
130
"theme_dark": "داكن",
259
-
131
"theme_claude": "Claude",
261
-
132
"font_size": "حجم الخط",
263
-
133
"font_size_desc": "ضبط حجم النص في المحادثة.",
265
-
134
"font_preview": "النص التجريبي لمعاينة حجم الخط الجديد.",
267
-
135
"code_theme_dark": "سمة الكود (داكن)",
269
-
136
"code_theme_light": "سمة الكود (فاتح)",
271
-
137
"code_theme_desc": "اختر سمة تمييز الصياغة لكتل الأكواد.",
273
-
138
"tts_engine": "محرك تحويل النص إلى كلام",
275
-
139
"tts_engine_system": "محرك النظام",
277
-
140
"tts_engine_kitten": "Kitten TTS",
279
-
141
"voice": "الصوت",
281
-
142
"voice_female": "أنثى",
283
-
143
"voice_male": "ذكر",
285
-
144
"voice_other": "آخر",
287
-
145
"tts_speed": "سرعة القراءة",
289
-
146
"tts_speed_desc": "ضبط معدل التشغيل.",
291
-
147
"manage_tts_models": "إدارة نماذج TTS",
293
-
148
"manage_on_device_models": "إدارة النماذج المحلية",
295
-
149
"enable_smart_reply": "الردود الذكية المحلية",
297
-
150
"streaming_responses": "الردود المتدفقة",
299
-
151
"auto_generate_titles": "إنشاء العناوين تلقائياً",
301
-
152
"send_on_enter": "إرسال عند الضغط على Enter",
303
-
153
"show_system_messages": "إظهار رسائل النظام",
305
-
154
"haptic_feedback": "الاستجابة اللمسية",
307
-
155
"enable_mcp": "تفعيل MCP",
309
-
156
"new_chat_mcp_default": "تفعيل MCP افتراضياً في المحادثات الجديدة",
311
-
157
"show_data_indicator": "إظهار مؤشر البيانات",
313
-
158
"privacy_info": "\"LocalMind لا يطلع على بياناتك أبداً\"",
315
-
159
"delete_all_conversations": "حذف جميع المحادثات",
317
-
160
"reset_settings_defaults": "إعادة الإعدادات إلى الوضع الافتراضي",
319
-
161
"chat_title": "LocalMind",
321
-
162
"chat_parameters_tooltip": "معلمات المحادثة",
323
-
163
"change_persona": "تغيير الشخصية",
325
-
164
"set_persona": "تعيين شخصية",
327
-
165
"remove_persona": "إزالة الشخصية",
329
-
166
"clear_conversation": "مسح المحادثة",
331
-
167
"connection_error": "خطأ في الاتصال. تحقق من الخادم الخاص بك.",
333
-
168
"disconnected": "تم قطع الاتصال بالخادم.",
335
-
169
"configure": "إعداد",
337
-
170
"select_model": "اختيار نموذج",
339
-
171
"select_persona": "اختيار شخصية",
341
-
172
"start_conversation": "ابدأ محادثة",
343
-
173
"recent_chats": "المحادثات الأخيرة",
345
-
174
"see_all": "عرض الكل",
347
-
175
"quick_write": "ساعدني في كتابة دالة",
349
-
176
"quick_explain": "اشرح هذا الكود",
351
-
177
"quick_debug": "صحح لي هذا الخطأ",
353
-
178
"quick_async": "كيف أستخدم async/await؟",
355
-
179
"history_missing_title": "السجل مفقود",
357
-
180
"history_missing_desc": "إما أن رسائل هذه المحادثة قد حُذفت أو أن سجل المحادثة تالف.",
359
-
181
"technical_details": "التفاصيل التقنية",
361
-
182
"last_error": "آخر خطأ:",
363
-
183
"copy_info": "نسخ المعلومات",
365
-
184
"conversation_id": "معرف المحادثة",
367
-
185
"created_at": "تاريخ الإنشاء",
369
-
186
"expected_messages": "الرسائل المتوقعة",
371
-
187
"debug_dialog_desc": "معلومات تشخيصية للمساعدة في تحديد مشاكل المزامنة.",
373
-
188
"chat_input_hint": "اسأل أي شيء",
375
-
189
"send_message_tooltip": "إرسال الرسالة",
377
-
190
"stop_generation_tooltip": "إيقاف التوليد",
379
-
191
"attach_images_tooltip": "إرفاق صور",
381
-
192
"tool_label": "أداة: {toolCallId}",
383
-
193
"tool_unknown": "أداة: غير معروفة",
385
-
194
"message_options": "خيارات الرسالة",
387
-
195
"copy_markdown": "نسخ كـ Markdown",
389
-
196
"copied_markdown": "تم النسخ كـ Markdown",
391
-
197
"read_aloud": "القراءة بصوت عالٍ",
393
-
198
"stop_reading": "إيقاف القراءة",
395
-
199
"more": "المزيد",
397
-
200
"character_count": "{length} حرف",
399
-
201
"edit_message": "تعديل الرسالة",
401
-
202
"edit_message_desc": "سيؤدي الحفظ إلى إزالة رد المساعد أدناه وإعادة التوليد.",
403
-
203
"save_regenerate": "حفظ وإعادة التوليد",
405
-
204
"chat_settings_title": "إعدادات المحادثة",
407
-
205
"reset_defaults": "إعادة الإعدادات الافتراضية",
409
-
206
"parameters_tab": "المعلمات",
411
-
207
"mcp_tab": "MCP",
413
-
208
"temperature": "درجة الحرارة",
415
-
209
"temperature_desc": "التحكم في العشوائية: أعلى = إبداعي، أقل = مركز",
417
-
210
"top_p": "Top P",
419
-
211
"top_p_desc": "حد أخذ العينات النووي",
421
-
212
"max_tokens": "الحد الأقصى للرموز",
423
-
213
"max_tokens_desc": "حد الاستجابة",
425
-
214
"context_length": "طول السياق",
427
-
215
"context_length_desc": "نافذة السجل",
429
-
216
"mcp_disabled_warning": "MCP معطل عالمياً. فعّله في الإعدادات لاستخدام هذه الميزات.",
431
-
217
"mcp_enable_chat": "تفعيل MCP لهذه المحادثة",
433
-
218
"auto_execute_tools": "تشغيل الأدوات تلقائياً",
435
-
219
"beta_label": "تجريبي",
437
-
220
"experimental_label": "اختباري",
439
-
221
"add_ephemeral_mcp": "إضافة خادم MCP مؤقت",
441
-
222
"mcp_label_placeholder": "التسمية",
443
-
223
"mcp_url_placeholder": "الرابط (https://...)",
445
-
224
"active_integrations": "التكاملات النشطة",
447
-
225
"enable_notifications": "تفعيل الإشعارات",
449
-
226
"enable_notifications_desc": "احصل على إشعار عند اكتمال تنزيل النماذج.",
451
-
227
"chat_history_title": "سجل المحادثات",
453
-
228
"conversation_just_now": "الآن",
455
-
229
"conversation_minutes_ago": "منذ {minutes} دقيقة",
457
-
230
"conversation_hours_ago": "منذ {hours} ساعة",
459
-
231
"conversation_yesterday": "أمس",
461
-
232
"conversation_days_ago": "منذ {days} يوم",
463
-
233
"conversation_date": "{year}/{month}/{day}",
465
-
234
"options_tooltip": "خيارات",
467
-
235
"no_results_found": "لا توجد نتائج",
469
-
236
"no_conversations_yet": "لا توجد محادثات بعد",
471
-
237
"try_different_search": "جرّب مصطلح بحث مختلف",
473
-
238
"start_new_conversation": "ابدأ محادثة جديدة",
475
-
239
"rename_conversation": "إعادة تسمية المحادثة",
477
-
240
"enter_new_title": "أدخل عنواناً جديداً",
479
-
241
"pinned_section": "مثبت",
481
-
242
"today_section": "اليوم",
483
-
243
"yesterday_section": "أمس",
485
-
244
"previous_7_days": "آخر 7 أيام",
487
-
245
"previous_30_days": "آخر 30 يوماً",
489
-
246
"older_section": "الأقدم",
491
-
247
"onboarding_choose_language": "اختر اللغة",
493
-
248
"onboarding_choose_language_desc": "اختر لغتك المفضلة. يمكنك تغيير هذا في أي وقت من الإعدادات.",
495
-
249
"onboarding_localmind": "LOCALMIND",
497
-
250
"onboarding_connect_server": "اتصل\nبخادمك",
499
-
251
"onboarding_connect_desc": "اتصل بـ LM Studio أو Ollama أو\nOpenRouter لبدء تجربة الذكاء\nالاصطناعي الخاصة بك.",
501
-
252
"onboarding_welcome": "مرحباً بك في LocalMind",
503
-
253
"server_type_on_device": "محلي",
505
-
254
"server_type_on_device_sub": "لا يحتاج خادماً",
507
-
255
"server_type_lm_studio": "LM Studio",
509
-
256
"server_type_lm_studio_sub": "API محلي",
511
-
257
"server_type_ollama": "Ollama",
513
-
258
"server_type_ollama_sub": "محرك أوامر",
515
-
259
"server_type_openrouter": "OpenRouter",
517
-
260
"server_type_openrouter_sub": "سحابة موحدة",
519
-
261
"ready_continue": "جاهز للمتابعة",
521
-
262
"waiting_selection": "بانتظار الاختيار",
523
-
263
"setup_connection": "إعداد الاتصال",
525
-
264
"setup_connection_desc": "قم بإعداد خادم {server} لبدء المحادثة.",
527
-
265
"server_name": "اسم الخادم",
529
-
266
"name_required": "الاسم مطلوب",
531
-
267
"name_max_50": "الحد الأقصى 50 حرفاً",
533
-
268
"host_label": "المضيف / عنوان IP",
535
-
269
"host_required": "المضيف مطلوب",
537
-
270
"port_label": "المنفذ",
539
-
271
"port_required": "المنفذ مطلوب",
541
-
272
"port_invalid": "يجب أن يكون رقماً",
543
-
273
"port_range": "أدخل منفذاً صالحاً (1-65535)",
545
-
274
"api_key_required": "مفتاح API *",
547
-
275
"api_key_optional": "مفتاح API (اختياري)",
549
-
276
"api_key_required_openrouter": "مفتاح API مطلوب لـ OpenRouter",
551
-
277
"api_key_format": "مفاتيح OpenRouter API تبدأ بـ sk-",
553
-
278
"my_server_hint": "خادمي",
555
-
279
"name_length_validation": "يجب ألا يتجاوز الاسم 50 حرفاً",
557
-
280
"host_valid": "أدخل اسماً مضيفاً أو عنوان IP صالحاً",
559
-
281
"api_key_hint_openrouter": "sk-...",
561
-
282
"api_key_hint_generic": "للخوادم الموثقة",
563
-
283
"update_server": "تحديث الخادم",
565
-
284
"save_server": "حفظ الخادم",
567
-
285
"server_updated": "تم تحديث الخادم",
569
-
286
"server_added": "تمت إضافة الخادم",
571
-
287
"download_model_title": "تنزيل نموذج",
573
-
288
"download_model_desc": "اختر نموذجاً للتنزيل.\nسيتم تشغيله محلياً على جهازك.",
575
-
289
"on_device_android_only": "الاستدلال المحلي متاح حالياً على Android فقط.",
577
-
290
"total_ram": "إجمالي الذاكرة",
579
-
291
"available": "متاح",
581
-
292
"ram_min_required": "{fileSize} جيجابايت ذاكرة كحد أدنى",
583
-
293
"download_progress": "{percent}% • {speed}",
585
-
294
"eta_label": "الوقت المتبقي: {eta}",
587
-
295
"paused_progress": "متوقف مؤقتاً - {percent}%",
589
-
296
"ram_warning_body_download": "هذا النموذج يتطلب {ram} جيجابايت ذاكرة على الأقل، ولكن جهازك يحتوي على {totalMemory}. قد لا يعمل بشكل صحيح أو قد يتسبب في تعطل التطبيق.",
591
-
297
"ram_warning_body_load": "جهازك يحتوي على {availableRAM} ذاكرة متاحة، لكن هذا النموذج يوصي بـ {ram} جيجابايت على الأقل. قد يفشل تحميله أو يسبب عدم استقرار.",
593
-
298
"choose_theme": "اختر السمة",
595
-
299
"choose_theme_desc": "خصص مظهر التطبيق. يمكنك دائماً تغيير هذا لاحقاً في الإعدادات.",
597
-
300
"theme_card_system": "النظام",
599
-
301
"theme_card_system_sub": "يطابق إعدادات جهازك",
601
-
302
"theme_card_light": "فاتح",
603
-
303
"theme_card_light_sub": "نظيف ومشرق",
605
-
304
"theme_card_dark": "داكن",
607
-
305
"theme_card_dark_sub": "مريح للعينين",
609
-
306
"theme_card_claude": "Claude",
611
-
307
"theme_card_claude_sub": "سمة دافئة بلون الخوخ",
613
-
308
"stay_updated": "ابق على اطلاع",
615
-
309
"stay_updated_desc": "احصل على إشعارات عند اكتمال تنزيل نماذج الذكاء الاصطناعي أو عند انتهاء المهام الطويلة.",
617
-
310
"notification_benefit_downloads": "تقدم تنزيل النماذج",
619
-
311
"notification_benefit_completions": "اكتمال التوليد",
621
-
312
"notification_benefit_background": "حالة المهام الخلفية",
623
-
313
"allow_notifications": "السماح بالإشعارات",
625
-
314
"servers_title": "الخوادم",
627
-
315
"no_servers_yet": "لا توجد خوادم بعد",
629
-
316
"no_servers_desc": "أضف خادمك الأول لبدء المحادثة مع نماذج الذكاء الاصطناعي.",
631
-
317
"add_server": "إضافة خادم",
633
-
318
"switched_to_server": "تم التبديل إلى {name}",
635
-
319
"edit_server": "تعديل الخادم",
637
-
320
"add_server_title": "إضافة خادم",
639
-
321
"server_type_label": "نوع الخادم",
641
-
322
"server_icon_label": "أيقونة الخادم",
643
-
323
"default_icon": "الأيقونة الافتراضية",
645
-
324
"server_type_lm_studio_display": "LM Studio",
647
-
325
"server_type_openai_display": "متوافق مع OpenAI",
649
-
326
"server_type_ollama_display": "Ollama",
651
-
327
"server_type_openrouter_display": "OpenRouter",
653
-
328
"server_type_on_device_display": "محلي",
655
-
329
"server_address_openrouter": "openrouter.ai",
657
-
330
"server_address_on_device": "استدلال محلي",
659
-
331
"server_address_format": "{host}:{port}",
661
-
332
"default_badge": "افتراضي",
663
-
333
"set_as_default": "تعيين كافتراضي",
665
-
334
"select_icon": "اختيار أيقونة",
667
-
335
"select_icon_desc": "اختر أيقونة لخادمك",
669
-
336
"search_icons_hint": "البحث في الأيقونات...",
671
-
337
"server_icon_stack": "Server Stack",
338
"server_icon_stack2": "Server Stack 02",
339
"server_icon_stack3": "Server Stack 03",
@@ -720,173 +385,89 @@
385
"server_icon_refresh": "Refresh",
386
"server_icon_hard_drive": "Hard Drive",
387
"server_icon_drive": "Drive",
723
-
388
"personas_title": "الشخصيات",
725
-
389
"persona_category_general": "عام",
727
-
390
"persona_category_coding": "برمجة",
729
-
391
"persona_category_education": "تعليم",
731
-
392
"persona_category_creative": "إبداعي",
733
-
393
"persona_builtin_section": "مضمنة",
735
-
394
"persona_my_section": "شخصياتي",
737
-
395
"clone_edit": "نسخ وتعديل",
739
-
396
"builtin_badge": "مضمنة",
741
-
397
"no_personas_found": "لم يتم العثور على شخصيات",
743
-
398
"no_personas_desc": "أنشئ شخصيتك الأولى لتخصيص سلوك الذكاء الاصطناعي.",
745
-
399
"edit_persona": "تعديل الشخصية",
747
-
400
"create_persona": "إنشاء شخصية",
749
-
401
"create_persona_button": "إنشاء",
751
-
402
"emoji_label": "رمز تعبيري",
753
-
403
"name_label": "الاسم",
755
-
404
"my_persona_hint": "شخصيتي",
757
-
405
"category_label": "الفئة",
759
-
406
"description_optional": "الوصف (اختياري)",
761
-
407
"description_hint": "ماذا تفعل هذه الشخصية...",
763
-
408
"system_prompt": "موجه النظام",
765
-
409
"character_count_max": "{currentLen}/4000",
767
-
410
"no_prompt_placeholder": "لا يوجد موجه بعد...",
769
-
411
"prompt_hint": "أنت مساعد مفيد...",
771
-
412
"prompt_required": "موجه النظام مطلوب",
773
-
413
"prompt_max_chars": "الحد الأقصى 4000 حرف",
775
-
414
"advanced_settings": "الإعدادات المتقدمة",
777
-
415
"temperature_label": "درجة الحرارة (0.0-2.0)",
779
-
416
"top_p_label": "Top P (0.0-1.0)",
781
-
417
"temp_hint": "0.7",
783
-
418
"top_p_hint": "0.9",
785
-
419
"range_0_2": "0.0-2.0",
787
-
420
"range_0_1": "0.0-1.0",
789
-
421
"persona_updated": "تم تحديث الشخصية",
791
-
422
"persona_created": "تم إنشاء الشخصية",
793
-
423
"tts_models_title": "نماذج تحويل النص إلى كلام",
795
-
424
"always_available": "متاح دائماً",
797
-
425
"tts_system_desc": "يستخدم محرك تحويل النص إلى كلام المدمج في جهازك.\nلا يتطلب أي تنزيلات. اختيار الصوت يستخدم إعدادات نظام جهازك.",
799
-
426
"downloading_status": "جارٍ التنزيل...",
801
-
427
"tts_kitten_desc": "TTS عصبي فائق السرعة مع 8 أصوات معبرة.\nيتطلب تنزيل {size}.",
803
-
428
"tts_piper_desc": "أصوات Piper سريعة دون اتصال مع صوتين معبرين.\nيتطلب تنزيل {size} لكل صوت.",
805
-
429
"engine_spec": "{sizeMb} ميجابايت · {ramMb} ميجابايت ذاكرة · {voiceCount} أصوات",
807
-
430
"on_device_models_title": "النماذج المحلية",
809
-
431
"available_models": "النماذج المتاحة",
811
-
432
"device_memory": "ذاكرة الجهاز",
813
-
433
"ram_usage": "استخدام الذاكرة",
815
-
434
"memory_healthy": "جيد",
817
-
435
"memory_critical": "حرج",
819
-
436
"memory_low": "منخفض",
821
-
437
"ram_used": "{percent}% مستخدم",
823
-
438
"available_ram": "الذاكرة المتاحة",
825
-
439
"total_capacity": "السعة الإجمالية",
827
-
440
"loaded_status": "تم التحميل",
829
-
441
"inference_backend": "محطة الاستدلال",
831
-
442
"backend_ios_notice": "محطة CPU فقط متاحة على iOS.",
833
-
443
"backend_cpu_desc": "يعمل على جميع الأجهزة. الأكثر توافقاً.",
835
-
444
"backend_gpu_desc": "تسريع OpenCL. أسرع على الأجهزة المدعومة.",
837
-
445
"backend_npu_desc": "NPU من الشركة المصنعة (Qualcomm/MediaTek). أسرع استدلال.",
839
-
446
"select_model_title": "اختيار نموذج",
841
-
447
"refresh_models": "تحديث النماذج",
843
-
448
"search_models_hint": "البحث في النماذج...",
845
-
449
"no_server_connected": "لا يوجد خادم متصل",
847
-
450
"add_server_first": "أضف خادماً أولاً لرؤية النماذج المتاحة.",
849
-
451
"failed_load_models": "فشل تحميل النماذج",
851
-
452
"no_models_available": "لا توجد نماذج متاحة",
853
-
453
"no_models_match": "لا توجد نماذج تطابق \"{searchQuery}\"",
855
-
454
"model_load_failed": "فشل تحميل النموذج: {error}",
857
-
455
"model_unloaded_ollama": "سيتم تفريغ {name} بعد انتهاء وقت الاحتفاظ",
859
-
456
"model_unloaded_success": "تم تفريغ {name} بنجاح",
861
-
457
"model_unload_failed": "فشل تفريغ النموذج: {error}",
863
-
458
"unload_from_server": "تفريغ من الخادم",
865
-
459
"context_chip": "{ctx} سياق",
867
-
460
"download_notification_title": "جارٍ تنزيل {modelName}...",
869
-
461
"download_complete_notification": "اكتمل التنزيل!",
871
-
462
"download_complete_body": "تم تنزيل {modelName} بنجاح.",
873
-
463
"download_failed_notification": "فشل التنزيل: {error}",
875
-
464
"download_failed_body": "فشل تنزيل {modelName}.",
877
-
465
"engine_name_system": "TTS النظام",
879
-
466
"engine_tagline_system": "محرك الجهاز المدمج",
881
-
467
"engine_name_kitten": "Kitten TTS",
883
-
468
"engine_tagline_kitten": "TTS عصبي عالي السرعة",
885
-
469
"engine_name_sherpa": "Sherpa ONNX VITS",
887
-
470
"engine_tagline_sherpa": "أصوات Piper دون اتصال",
889
-
471
"voice_jasper": "Jasper",
472
"voice_bella": "Bella",
473
"voice_bruno": "Bruno",
@@ -897,56 +478,32 @@
478
"voice_kiki": "Kiki",
479
"voice_lessac": "Lessac (أمريكي)",
480
"voice_ryan": "Ryan (أمريكي)",
900
-
481
"model_qwen_3": "Qwen 3 0.6B",
902
-
482
"model_qwen_3_desc": "أصغر نموذج محادثة عام. استجابات سريعة، استخدام منخفض للذاكرة.",
904
-
483
"model_license_apache": "Apache-2.0",
906
-
484
"model_qwen_25": "Qwen 2.5 1.5B Instruct",
908
-
485
"model_qwen_25_desc": "جودة وحجم متوازنان. مناسب للمحادثة العامة.",
910
-
486
"model_deepseek": "DeepSeek R1 Distill Qwen 1.5B",
912
-
487
"model_deepseek_desc": "نموذج استدلال وسلسلة أفكار. الأفضل للمهام المنطقية.",
914
-
488
"model_license_mit": "MIT",
916
-
489
"model_gemma": "Gemma 4 E2B Instruct",
918
-
490
"model_gemma_desc": "نموذج Google الرائد. أعلى جودة، يتطلب ذاكرة أكبر.",
920
-
491
"export_header": "*تم التصدير من LocalMind — {date}*",
922
-
492
"export_role_user": "## 👤 المستخدم",
924
-
493
"export_role_assistant": "## 🤖 المساعد",
926
-
494
"export_role_system": "## ⚙️ النظام",
928
-
495
"export_role_tool": "## 🔧 الأداة",
930
-
496
"export_text_user": "[المستخدم]",
932
-
497
"export_text_assistant": "[المساعد]",
934
-
498
"export_text_system": "[النظام]",
936
-
499
"export_text_tool": "[الأداة]",
938
-
500
"export_label_user": "المستخدم",
940
-
501
"export_label_assistant": "المساعد",
942
-
502
"export_label_system": "النظام",
944
-
503
"export_label_tool": "الأداة",
946
-
504
"select_model_hint": "اختر نموذجاً لبدء المحادثة",
948
-
505
"test_notification_title": "إشعار تجريبي",
950
-
951
- "test_notification_body": "هذا إشعار تجريبي لتقدم تنزيل النموذج."
952
-}
506
+ "test_notification_body": "هذا إشعار تجريبي لتقدم تنزيل النموذج.",
507
+ "tts_supports_background": "يدعم التشغيل في الخلفية كصوت أصلي",
508
+ "tts_other_services_background_note": "ملاحظة: تدعم خدمات TTS الأخرى التشغيل في الخلفية كصوت أصلي."
509
+}
\ No newline at end of file
lib/l10n/app_bn.arb
+4
-2
@@ -503,5 +503,7 @@
503
"voice_rosie": "Rosie",
504
"voice_ryan": "Ryan (US)",
505
"waiting_selection": "নির্বাচনের জন্য অপেক্ষা করা হচ্ছে",
506
- "yesterday_section": "গতকাল"
507
-}
506
+ "yesterday_section": "গতকাল",
507
+ "tts_supports_background": "নেটিভ অডিও হিসাবে ব্যাকগ্রাউন্ড প্লেব্যাক সমর্থন করে",
508
+ "tts_other_services_background_note": "দ্রষ্টব্য: অন্যান্য টিটিএস পরিষেবাগুলি নেটিভ অডিও হিসাবে ব্যাকগ্রাউন্ড প্লেব্যাক সমর্থন করে।"
509
+}
\ No newline at end of file
lib/l10n/app_en.arb
+9
-505
@@ -1,351 +1,281 @@
1
{
2
"@@locale": "en",
3
-
3
"app_name": "LocalMind",
4
"@app_name": {
5
"description": "Application display name"
6
},
8
-
7
"app_tagline": "Your AI. Your Device. Your Rules.",
8
"@app_tagline": {
9
"description": "Application tagline shown in settings and splash"
10
},
13
-
11
"app_version": "1.0.0",
12
"@app_version": {
13
"description": "Application version number"
14
},
18
-
15
"cancel": "Cancel",
16
"@cancel": {
17
"description": "Generic cancel button label"
18
},
23
-
19
"confirm": "Confirm",
20
"@confirm": {
21
"description": "Generic confirm button label"
22
},
28
-
23
"delete": "Delete",
24
"@delete": {
25
"description": "Generic delete button label"
26
},
33
-
27
"save": "Save",
28
"@save": {
29
"description": "Generic save button label"
30
},
38
-
31
"retry": "Retry",
32
"@retry": {
33
"description": "Generic retry button label"
34
},
43
-
35
"close": "Close",
36
"@close": {
37
"description": "Generic close button label"
38
},
48
-
39
"done": "Done",
40
"@done": {
41
"description": "Generic done button label"
42
},
53
-
43
"continue_action": "Continue",
44
"@continue_action": {
45
"description": "Generic continue button label"
46
},
58
-
47
"skip": "Skip",
48
"@skip": {
49
"description": "Generic skip button label"
50
},
63
-
51
"install": "Install",
52
"@install": {
53
"description": "Install action button"
54
},
68
-
55
"download": "Download",
56
"@download": {
57
"description": "Download action button"
58
},
73
-
59
"resume": "Resume",
60
"@resume": {
61
"description": "Resume download button"
62
},
78
-
63
"pause": "Pause",
64
"@pause": {
65
"description": "Pause download button"
66
},
83
-
67
"stop": "Stop",
68
"@stop": {
69
"description": "Stop action button"
70
},
88
-
71
"edit": "Edit",
72
"@edit": {
73
"description": "Edit action label"
74
},
93
-
75
"preview": "Preview",
76
"@preview": {
77
"description": "Preview action label"
78
},
98
-
79
"unload": "Unload",
80
"@unload": {
81
"description": "Unload model action"
82
},
103
-
83
"load": "Load",
84
"@load": {
85
"description": "Load model action"
86
},
108
-
87
"rename": "Rename",
88
"@rename": {
89
"description": "Rename action label"
90
},
113
-
91
"pin": "Pin",
92
"@pin": {
93
"description": "Pin conversation action"
94
},
118
-
95
"unpin": "Unpin",
96
"@unpin": {
97
"description": "Unpin conversation action"
98
},
123
-
99
"share": "Share",
100
"@share": {
101
"description": "Share action label"
102
},
128
-
103
"copy": "Copy",
104
"@copy": {
105
"description": "Copy action label"
106
},
133
-
107
"copied": "Copied!",
108
"@copied": {
109
"description": "Copied confirmation text"
110
},
138
-
111
"copied_to_clipboard": "Copied to clipboard",
112
"@copied_to_clipboard": {
113
"description": "Snackbar message after copy"
114
},
143
-
115
"select": "Select",
116
"@select": {
117
"description": "Select action button"
118
},
148
-
119
"active": "Active",
120
"@active": {
121
"description": "Active status badge"
122
},
153
-
123
"all": "All",
124
"@all": {
125
"description": "All filter label"
126
},
158
-
127
"none": "None",
128
"@none": {
129
"description": "No selection label"
130
},
163
-
131
"none_selected": "None selected",
132
"@none_selected": {
133
"description": "Dropdown placeholder when nothing selected"
134
},
168
-
135
"online": "Online",
136
"@online": {
137
"description": "Connection status online"
138
},
173
-
139
"offline": "Offline",
140
"@offline": {
141
"description": "Connection status offline"
142
},
178
-
143
"error": "Error",
144
"@error": {
145
"description": "Error status text"
146
},
183
-
147
"unknown_error": "Unknown error",
148
"@unknown_error": {
149
"description": "Fallback unknown error message"
150
},
188
-
151
"not_now": "Not Now",
152
"@not_now": {
153
"description": "Dismiss action for notifications"
154
},
193
-
155
"enable": "Enable",
156
"@enable": {
157
"description": "Enable action button"
158
},
198
-
159
"proceed_anyway": "Proceed Anyway",
160
"@proceed_anyway": {
161
"description": "Proceed despite warning"
162
},
203
-
163
"test_connection": "Test Connection",
164
"@test_connection": {
165
"description": "Button to test server connection"
166
},
208
-
167
"testing": "Testing...",
168
"@testing": {
169
"description": "Button label while testing connection"
170
},
213
-
171
"connection_successful": "Connection successful!",
172
"@connection_successful": {
173
"description": "Connection test success message"
174
},
218
-
175
"connection_failed": "Connection failed. Check your settings.",
176
"@connection_failed": {
177
"description": "Connection test failure message"
178
},
223
-
179
"save_continue": "Save & Continue",
180
"@save_continue": {
181
"description": "Save and proceed button"
182
},
228
-
183
"save_changes": "Save Changes",
184
"@save_changes": {
185
"description": "Save changes button"
186
},
233
-
187
"finish_setup": "Finish Setup",
188
"@finish_setup": {
189
"description": "Complete onboarding button"
190
},
238
-
191
"start_new_chat": "Start New Chat",
192
"@start_new_chat": {
193
"description": "Button to start a new chat"
194
},
243
-
195
"cannot_undo": "This action cannot be undone.",
196
"@cannot_undo": {
197
"description": "Warning for destructive actions"
198
},
248
-
199
"ram_warning": "RAM Warning",
200
"@ram_warning": {
201
"description": "RAM warning dialog title"
202
},
253
-
203
"recommended": "RECOMMENDED",
204
"@recommended": {
205
"description": "Recommended badge on model cards"
206
},
258
-
207
"may_be_large": "May be too large for this device",
208
"@may_be_large": {
209
"description": "Warning when model may exceed device RAM"
210
},
263
-
211
"calculating": "Calculating...",
212
"@calculating": {
213
"description": "ETA placeholder during download"
214
},
268
-
215
"download_failed": "Download failed",
216
"@download_failed": {
217
"description": "Download failure status"
218
},
273
-
219
"downloaded": "Downloaded",
220
"@downloaded": {
221
"description": "Download completed status"
222
},
278
-
223
"not_downloaded": "Not downloaded",
224
"@not_downloaded": {
225
"description": "Model not yet downloaded"
226
},
283
-
227
"installed": "Installed",
228
"@installed": {
229
"description": "Installed status"
230
},
288
-
231
"not_installed": "Not installed",
232
"@not_installed": {
233
"description": "Not installed status"
234
},
293
-
235
"loading": "Loading...",
236
"@loading": {
237
"description": "Generic loading indicator text"
238
},
298
-
239
"thinking": "Thinking",
240
"@thinking": {
241
"description": "Model thinking indicator"
242
},
303
-
243
"processing": "Processing...",
244
"@processing": {
245
"description": "Processing indicator text"
246
},
308
-
247
"initializing": "Initializing...",
248
"@initializing": {
249
"description": "Initializing status"
250
},
313
-
251
"ready": "Ready",
252
"@ready": {
253
"description": "Ready status text"
254
},
318
-
255
"preparing_app": "Preparing app...",
256
"@preparing_app": {
257
"description": "Bootstrap stage message"
258
},
323
-
259
"initializing_services": "Initializing services...",
260
"@initializing_services": {
261
"description": "Bootstrap stage message"
262
},
328
-
263
"configuring_server": "Configuring server...",
264
"@configuring_server": {
265
"description": "Bootstrap stage message"
266
},
333
-
267
"startup_failed": "Startup failed",
268
"@startup_failed": {
269
"description": "Bootstrap error message"
270
},
338
-
271
"something_went_wrong": "Something went wrong",
272
"@something_went_wrong": {
273
"description": "Generic error heading on bootstrap screen"
274
},
343
-
275
"delete_model_title": "Delete Model",
276
"@delete_model_title": {
277
"description": "Delete model dialog title"
278
},
348
-
279
"delete_model_body": "Are you sure you want to delete {name}?",
280
"@delete_model_body": {
281
"description": "Delete model confirmation dialog body",
@@ -356,7 +286,6 @@
286
}
287
}
288
},
359
-
289
"delete_model_body_with_size": "Are you sure you want to delete {name}? This will free up approximately {size} of space.\n\nYou can download this model again later if needed.",
290
"@delete_model_body_with_size": {
291
"description": "Delete model dialog body with size info",
@@ -371,12 +300,10 @@
300
}
301
}
302
},
374
-
303
"delete_voice_title": "Delete Voice",
304
"@delete_voice_title": {
305
"description": "Delete voice confirmation dialog title"
306
},
379
-
307
"delete_voice_body": "Are you sure you want to delete {name}? This will free up approximately {size} of space.\n\nYou can download this voice again later if needed.",
308
"@delete_voice_body": {
309
"description": "Delete voice confirmation dialog body",
@@ -391,12 +318,10 @@
318
}
319
}
320
},
394
-
321
"delete_server_title": "Delete Server",
322
"@delete_server_title": {
323
"description": "Delete server confirmation dialog title"
324
},
399
-
325
"delete_server_body": "Are you sure you want to delete \"{name}\"? This cannot be undone.",
326
"@delete_server_body": {
327
"description": "Delete server confirmation body",
@@ -407,12 +332,10 @@
332
}
333
}
334
},
410
-
335
"delete_conversation_title": "Delete conversation?",
336
"@delete_conversation_title": {
337
"description": "Delete conversation dialog title"
338
},
415
-
339
"delete_conversation_body": "Are you sure you want to delete \"{title}\"? This cannot be undone.",
340
"@delete_conversation_body": {
341
"description": "Delete conversation dialog body",
@@ -423,12 +346,10 @@
346
}
347
}
348
},
426
-
349
"delete_message_title": "Delete message?",
350
"@delete_message_title": {
351
"description": "Delete message dialog title"
352
},
431
-
353
"delete_persona_title": "Delete \"{name}\"?",
354
"@delete_persona_title": {
355
"description": "Delete persona dialog title",
@@ -439,27 +360,22 @@
360
}
361
}
362
},
442
-
363
"delete_persona_body": "This cannot be undone.",
364
"@delete_persona_body": {
365
"description": "Delete persona dialog body"
366
},
447
-
367
"clear_conversation_title": "Clear conversation?",
368
"@clear_conversation_title": {
369
"description": "Clear conversation dialog title"
370
},
452
-
371
"clear_conversation_body": "This will delete all messages in this conversation.",
372
"@clear_conversation_body": {
373
"description": "Clear conversation confirmation body"
374
},
457
-
375
"clear": "Clear",
376
"@clear": {
377
"description": "Clear action button"
378
},
462
-
379
"label_completed": "{label} completed",
380
"@label_completed": {
381
"description": "Completion status message",
@@ -470,7 +386,6 @@
386
}
387
}
388
},
473
-
389
"error_with_message": "Error: {error}",
390
"@error_with_message": {
391
"description": "Error display with message",
@@ -481,7 +396,6 @@
396
}
397
}
398
},
484
-
399
"preview_failed": "Preview failed: {error}",
400
"@preview_failed": {
401
"description": "Audio preview failed message",
@@ -492,7 +406,6 @@
406
}
407
}
408
},
495
-
409
"loading_model": "Loading {modelId}...",
410
"@loading_model": {
411
"description": "Model loading status",
@@ -503,7 +416,6 @@
416
}
417
}
418
},
506
-
419
"model_loaded": "Model loaded: {modelId} ({backend})",
420
"@model_loaded": {
421
"description": "Model loaded status in settings",
@@ -518,12 +430,10 @@
430
}
431
}
432
},
521
-
433
"no_model_loaded": "No model loaded. Tap \"Manage On-Device Models\" to download and load a model.",
434
"@no_model_loaded": {
435
"description": "Status when no on-device model is loaded"
436
},
526
-
437
"loading_model_error": "Error: {error}",
438
"@loading_model_error": {
439
"description": "Model loading error status",
@@ -534,487 +444,390 @@
444
}
445
}
446
},
537
-
447
"delete_conversation": "Delete conversation?",
448
"@delete_conversation": {
449
"description": "Delete conversation dialog title"
450
},
542
-
451
"nav_history": "History",
452
"@nav_history": {
453
"description": "Navigation item for chat history"
454
},
547
-
455
"nav_servers": "Servers",
456
"@nav_servers": {
457
"description": "Navigation item for servers"
458
},
552
-
459
"nav_local_models": "Local Models",
460
"@nav_local_models": {
461
"description": "Navigation item for local on-device models"
462
},
557
-
463
"nav_tts": "Text To Speech",
464
"@nav_tts": {
465
"description": "Navigation item for TTS models"
466
},
562
-
467
"nav_personas": "Personas",
468
"@nav_personas": {
469
"description": "Navigation item for personas"
470
},
567
-
471
"nav_settings": "Settings",
472
"@nav_settings": {
473
"description": "Navigation item for settings"
474
},
572
-
475
"nav_new_chat": "New Chat",
476
"@nav_new_chat": {
477
"description": "Button to start a new chat"
478
},
577
-
479
"search_hint": "Search conversations...",
480
"@search_hint": {
481
"description": "Search field placeholder for conversations"
482
},
582
-
483
"no_server_selected": "No server selected",
484
"@no_server_selected": {
485
"description": "Placeholder when no server is selected"
486
},
587
-
487
"switch_server": "Switch Server",
488
"@switch_server": {
489
"description": "Switch server sheet title"
490
},
592
-
491
"switch_server_subtitle": "Choose a server to connect to",
492
"@switch_server_subtitle": {
493
"description": "Switch server sheet subtitle"
494
},
597
-
495
"manage_servers": "Manage Servers",
496
"@manage_servers": {
497
"description": "Button to manage servers list"
498
},
602
-
499
"open_source": "Open Source",
500
"@open_source": {
501
"description": "GitHub repo card title"
502
},
607
-
503
"open_source_desc": "LocalMind is open source. Follow our progress or contribute on GitHub.",
504
"@open_source_desc": {
505
"description": "GitHub repo card description"
506
},
612
-
507
"star_on_github": "Star on GitHub",
508
"@star_on_github": {
509
"description": "Button to star repo on GitHub"
510
},
617
-
511
"add_more": "Add more",
512
"@add_more": {
513
"description": "Label for the decorative GitHub tile on onboarding"
514
},
622
-
515
"on_github": "on GitHub",
516
"@on_github": {
517
"description": "Secondary label for the decorative GitHub tile on onboarding"
518
},
627
-
519
"could_not_open_github": "Could not open GitHub.",
520
"@could_not_open_github": {
521
"description": "Snackbar text shown when opening the GitHub repo fails"
522
},
632
-
523
"settings_title": "Settings",
524
"@settings_title": {
525
"description": "Settings screen title"
526
},
637
-
527
"settings_appearance": "Appearance",
528
"@settings_appearance": {
529
"description": "Settings section header for appearance"
530
},
642
-
531
"settings_language": "Language",
532
"@settings_language": {
533
"description": "Settings section label for language selection"
534
},
647
-
535
"language_system_default": "System Default",
536
"@language_system_default": {
537
"description": "Option to use the device's system language"
538
},
652
-
539
"settings_tts": "Text-to-Speech",
540
"@settings_tts": {
541
"description": "Settings section header for TTS"
542
},
657
-
543
"settings_behavior": "Behavior",
544
"@settings_behavior": {
545
"description": "Settings section header for behavior"
546
},
662
-
547
"settings_on_device": "On-Device Inference",
548
"@settings_on_device": {
549
"description": "Settings section header for on-device"
550
},
667
-
551
"settings_default_server": "Default Server",
552
"@settings_default_server": {
553
"description": "Settings section header for default server"
554
},
672
-
555
"settings_default_persona": "Default Persona",
556
"@settings_default_persona": {
557
"description": "Settings section header for default persona"
558
},
677
-
559
"settings_privacy": "Privacy",
560
"@settings_privacy": {
561
"description": "Settings section header for privacy"
562
},
682
-
563
"settings_data_management": "Data Management",
564
"@settings_data_management": {
565
"description": "Settings section header for data management"
566
},
687
-
567
"settings_about": "About",
568
"@settings_about": {
569
"description": "Settings section header for about"
570
},
692
-
571
"theme": "Theme",
572
"@theme": {
573
"description": "Theme selection label"
574
},
697
-
575
"theme_system": "System",
576
"@theme_system": {
577
"description": "System theme option"
578
},
702
-
579
"theme_light": "Light",
580
"@theme_light": {
581
"description": "Light theme option"
582
},
707
-
583
"theme_dark": "Dark",
584
"@theme_dark": {
585
"description": "Dark theme option"
586
},
712
-
587
"theme_claude": "Claude",
588
"@theme_claude": {
589
"description": "Claude theme option"
590
},
717
-
591
"font_size": "Font Size",
592
"@font_size": {
593
"description": "Font size slider label"
594
},
722
-
595
"font_size_desc": "Adjust text size in chat.",
596
"@font_size_desc": {
597
"description": "Font size setting description"
598
},
727
-
599
"font_preview": "The quick brown fox jumps over the lazy dog.",
600
"@font_preview": {
601
"description": "Font preview sample text"
602
},
732
-
603
"code_theme_dark": "Code Theme (Dark)",
604
"@code_theme_dark": {
605
"description": "Dark code theme dropdown label"
606
},
737
-
607
"code_theme_light": "Code Theme (Light)",
608
"@code_theme_light": {
609
"description": "Light code theme dropdown label"
610
},
742
-
611
"code_theme_desc": "Choose syntax highlighting theme for code blocks.",
612
"@code_theme_desc": {
613
"description": "Code theme setting description"
614
},
747
-
615
"tts_engine": "TTS Engine",
616
"@tts_engine": {
617
"description": "TTS engine dropdown label"
618
},
752
-
619
"tts_engine_system": "System TTS",
620
"@tts_engine_system": {
621
"description": "System TTS engine option"
622
},
757
-
623
"tts_engine_kitten": "Kitten TTS",
624
"@tts_engine_kitten": {
625
"description": "Kitten TTS engine option"
626
},
762
-
627
"voice": "Voice",
628
"@voice": {
629
"description": "Voice selection label"
630
},
767
-
631
"voice_female": "Female",
632
"@voice_female": {
633
"description": "Female voice gender label"
634
},
772
-
635
"voice_male": "Male",
636
"@voice_male": {
637
"description": "Male voice gender label"
638
},
777
-
639
"voice_other": "Other",
640
"@voice_other": {
641
"description": "Other voice gender label"
642
},
782
-
643
"tts_speed": "TTS Speed",
644
"@tts_speed": {
645
"description": "TTS speed slider label"
646
},
787
-
647
"tts_speed_desc": "Adjust the playback rate.",
648
"@tts_speed_desc": {
649
"description": "TTS speed setting description"
650
},
792
-
651
"manage_tts_models": "Manage TTS Models",
652
"@manage_tts_models": {
653
"description": "Button to manage TTS models"
654
},
797
-
655
"manage_on_device_models": "Manage On-Device Models",
656
"@manage_on_device_models": {
657
"description": "Button to manage on-device models"
658
},
802
-
659
"enable_smart_reply": "On-Device Smart Replies",
660
"@enable_smart_reply": {
661
"description": "Toggle for on-device smart reply suggestions"
662
},
807
-
663
"streaming_responses": "Streaming Responses",
664
"@streaming_responses": {
665
"description": "Toggle for streaming responses"
666
},
812
-
667
"auto_generate_titles": "Auto-generate Titles",
668
"@auto_generate_titles": {
669
"description": "Toggle for auto-generating conversation titles"
670
},
817
-
671
"send_on_enter": "Send on Enter",
672
"@send_on_enter": {
673
"description": "Toggle for send on enter behavior"
674
},
822
-
675
"show_system_messages": "Show System Messages",
676
"@show_system_messages": {
677
"description": "Toggle for system messages visibility"
678
},
827
-
679
"haptic_feedback": "Haptic Feedback",
680
"@haptic_feedback": {
681
"description": "Toggle for haptic feedback"
682
},
832
-
683
"enable_mcp": "Enable MCP",
684
"@enable_mcp": {
685
"description": "Toggle for MCP feature"
686
},
837
-
687
"new_chat_mcp_default": "New Chat MCP Default",
688
"@new_chat_mcp_default": {
689
"description": "Toggle for MCP default in new chats"
690
},
842
-
691
"show_data_indicator": "Show Data Indicator",
692
"@show_data_indicator": {
693
"description": "Toggle for data indicator visibility"
694
},
847
-
695
"privacy_info": "\"LocalMind never sees your data\"",
696
"@privacy_info": {
697
"description": "Privacy information text"
698
},
852
-
699
"delete_all_conversations": "Delete All Conversations",
700
"@delete_all_conversations": {
701
"description": "Dangerous action button to clear all conversations"
702
},
857
-
703
"reset_settings_defaults": "Reset Settings to Defaults",
704
"@reset_settings_defaults": {
705
"description": "Button to reset all settings"
706
},
862
-
707
"chat_title": "LocalMind",
708
"@chat_title": {
709
"description": "Fallback chat screen title"
710
},
867
-
711
"chat_parameters_tooltip": "Chat Parameters",
712
"@chat_parameters_tooltip": {
713
"description": "Tooltip for chat parameters button"
714
},
872
-
715
"change_persona": "Change Persona",
716
"@change_persona": {
717
"description": "Menu item to change persona"
718
},
877
-
719
"set_persona": "Set Persona",
720
"@set_persona": {
721
"description": "Menu item to set persona"
722
},
882
-
723
"remove_persona": "Remove Persona",
724
"@remove_persona": {
725
"description": "Menu item to remove persona"
726
},
887
-
727
"clear_conversation": "Clear Conversation",
728
"@clear_conversation": {
729
"description": "Menu item to clear conversation"
730
},
892
-
731
"connection_error": "Connection error. Check your server.",
732
"@connection_error": {
733
"description": "Connection error banner text"
734
},
897
-
735
"disconnected": "Disconnected from server.",
736
"@disconnected": {
737
"description": "Disconnected banner text"
738
},
902
-
739
"configure": "Configure",
740
"@configure": {
741
"description": "Configure action button"
742
},
907
-
743
"select_model": "Select Model",
744
"@select_model": {
745
"description": "Model selection placeholder"
746
},
912
-
747
"select_persona": "Select Persona",
748
"@select_persona": {
749
"description": "Persona selection sheet title"
750
},
917
-
751
"start_conversation": "Start a conversation",
752
"@start_conversation": {
753
"description": "Empty state heading"
754
},
922
-
755
"recent_chats": "Recent chats",
756
"@recent_chats": {
757
"description": "Recent chats section label"
758
},
927
-
759
"see_all": "See all",
760
"@see_all": {
761
"description": "See all link text"
762
},
932
-
763
"quick_write": "Help me write a function",
764
"@quick_write": {
765
"description": "Quick prompt chip text"
766
},
937
-
767
"quick_explain": "Explain this code",
768
"@quick_explain": {
769
"description": "Quick prompt chip text"
770
},
942
-
771
"quick_debug": "Debug this for me",
772
"@quick_debug": {
773
"description": "Quick prompt chip text"
774
},
947
-
775
"quick_async": "How do I use async/await?",
776
"@quick_async": {
777
"description": "Quick prompt chip text"
778
},
952
-
779
"history_missing_title": "History Missing",
780
"@history_missing_title": {
781
"description": "Corrupted chat error title"
782
},
957
-
783
"history_missing_desc": "Either the messages in this chat were deleted or the history record is corrupted.",
784
"@history_missing_desc": {
785
"description": "Corrupted chat error description"
786
},
962
-
787
"technical_details": "Technical Details",
788
"@technical_details": {
789
"description": "Button for debug details"
790
},
967
-
791
"last_error": "Last Error:",
792
"@last_error": {
793
"description": "Diagnostic label"
794
},
972
-
795
"copy_info": "Copy Info",
796
"@copy_info": {
797
"description": "Button to copy debug info"
798
},
977
-
799
"conversation_id": "Conversation ID",
800
"@conversation_id": {
801
"description": "Debug row label"
802
},
982
-
803
"created_at": "Created At",
804
"@created_at": {
805
"description": "Debug row label"
806
},
987
-
807
"expected_messages": "Expected Messages",
808
"@expected_messages": {
809
"description": "Debug row label"
810
},
992
-
811
"debug_dialog_desc": "Diagnostic information to help identify synchronization issues.",
812
"@debug_dialog_desc": {
813
"description": "Debug dialog description"
814
},
997
-
815
"chat_input_hint": "Ask anything",
816
"@chat_input_hint": {
817
"description": "Chat input field hint"
818
},
1002
-
819
"send_message_tooltip": "Send message",
820
"@send_message_tooltip": {
821
"description": "Tooltip for send button"
822
},
1007
-
823
"stop_generation_tooltip": "Stop generation",
824
"@stop_generation_tooltip": {
825
"description": "Tooltip for stop button"
826
},
1012
-
827
"attach_images_tooltip": "Attach images",
828
"@attach_images_tooltip": {
829
"description": "Tooltip for attach button"
830
},
1017
-
831
"tool_label": "Tool: {toolCallId}",
832
"@tool_label": {
833
"description": "Tool call label in chat bubble",
@@ -1025,42 +838,34 @@
838
}
839
}
840
},
1028
-
841
"tool_unknown": "Tool: Unknown",
842
"@tool_unknown": {
843
"description": "Unknown tool call fallback"
844
},
1033
-
845
"message_options": "Message options",
846
"@message_options": {
847
"description": "Message actions sheet title"
848
},
1038
-
849
"copy_markdown": "Copy as Markdown",
850
"@copy_markdown": {
851
"description": "Copy as markdown option"
852
},
1043
-
853
"copied_markdown": "Copied as Markdown",
854
"@copied_markdown": {
855
"description": "Copied as markdown snackbar"
856
},
1048
-
857
"read_aloud": "Read Aloud",
858
"@read_aloud": {
859
"description": "Read aloud action"
860
},
1053
-
861
"stop_reading": "Stop Reading",
862
"@stop_reading": {
863
"description": "Stop reading action"
864
},
1058
-
865
"more": "More",
866
"@more": {
867
"description": "More actions label"
868
},
1063
-
869
"character_count": "{length} characters",
870
"@character_count": {
871
"description": "Character count display",
@@ -1071,147 +876,118 @@
876
}
877
}
878
},
1074
-
879
"edit_message": "Edit message",
880
"@edit_message": {
881
"description": "Edit message dialog title"
882
},
1079
-
883
"edit_message_desc": "Saving will remove the assistant response below and regenerate.",
884
"@edit_message_desc": {
885
"description": "Edit message dialog description"
886
},
1084
-
887
"save_regenerate": "Save & regenerate",
888
"@save_regenerate": {
889
"description": "Save and regenerate button"
890
},
1089
-
891
"chat_settings_title": "Chat Settings",
892
"@chat_settings_title": {
893
"description": "Chat settings sheet title"
894
},
1094
-
895
"reset_defaults": "Reset Defaults",
896
"@reset_defaults": {
897
"description": "Reset to defaults button"
898
},
1099
-
899
"parameters_tab": "Parameters",
900
"@parameters_tab": {
901
"description": "Chat settings parameters tab"
902
},
1104
-
903
"mcp_tab": "MCP",
904
"@mcp_tab": {
905
"description": "Chat settings MCP tab"
906
},
1109
-
907
"temperature": "Temperature",
908
"@temperature": {
909
"description": "Temperature slider label"
910
},
1114
-
911
"temperature_desc": "Controls randomness: Higher = Creative, Lower = Focused",
912
"@temperature_desc": {
913
"description": "Temperature description"
914
},
1119
-
915
"top_p": "Top P",
916
"@top_p": {
917
"description": "Top P slider label"
918
},
1124
-
919
"top_p_desc": "Nucleus sampling threshold",
920
"@top_p_desc": {
921
"description": "Top P description"
922
},
1129
-
923
"max_tokens": "Max Tokens",
924
"@max_tokens": {
925
"description": "Max tokens input label"
926
},
1134
-
927
"max_tokens_desc": "Response limit",
928
"@max_tokens_desc": {
929
"description": "Max tokens description"
930
},
1139
-
931
"context_length": "Context Length",
932
"@context_length": {
933
"description": "Context length input label"
934
},
1144
-
935
"context_length_desc": "History window",
936
"@context_length_desc": {
937
"description": "Context length description"
938
},
1149
-
939
"mcp_disabled_warning": "MCP is disabled globally. Enable it in Settings to use these features.",
940
"@mcp_disabled_warning": {
941
"description": "Warning when MCP is disabled"
942
},
1154
-
943
"mcp_enable_chat": "Enable MCP for this chat",
944
"@mcp_enable_chat": {
945
"description": "Toggle to enable MCP for current chat"
946
},
1159
-
947
"auto_execute_tools": "Auto-execute tools",
948
"@auto_execute_tools": {
949
"description": "Toggle for auto-executing tools"
950
},
1164
-
951
"beta_label": "Beta",
952
"@beta_label": {
953
"description": "Badge label for beta features"
954
},
1169
-
955
"experimental_label": "Experimental",
956
"@experimental_label": {
957
"description": "Badge label for experimental features"
958
},
1174
-
959
"add_ephemeral_mcp": "Add Ephemeral MCP Server",
960
"@add_ephemeral_mcp": {
961
"description": "Section label for adding temp MCP server"
962
},
1179
-
963
"mcp_label_placeholder": "Label",
964
"@mcp_label_placeholder": {
965
"description": "MCP label field placeholder"
966
},
1184
-
967
"mcp_url_placeholder": "URL (https://...)",
968
"@mcp_url_placeholder": {
969
"description": "MCP URL field placeholder"
970
},
1189
-
971
"active_integrations": "Active Integrations",
972
"@active_integrations": {
973
"description": "Active MCP integrations section"
974
},
1194
-
975
"enable_notifications": "Enable Notifications",
976
"@enable_notifications": {
977
"description": "Notification banner title"
978
},
1199
-
979
"enable_notifications_desc": "Get notified when models finish downloading.",
980
"@enable_notifications_desc": {
981
"description": "Notification banner subtitle"
982
},
1204
-
983
"chat_history_title": "Chat History",
984
"@chat_history_title": {
985
"description": "Chat history screen title"
986
},
1209
-
987
"conversation_just_now": "Just now",
988
"@conversation_just_now": {
989
"description": "Recent conversation timestamp"
990
},
1214
-
991
"conversation_minutes_ago": "{minutes}m ago",
992
"@conversation_minutes_ago": {
993
"description": "Minutes ago timestamp",
@@ -1222,7 +998,6 @@
998
}
999
}
1000
},
1225
-
1001
"conversation_hours_ago": "{hours}h ago",
1002
"@conversation_hours_ago": {
1003
"description": "Hours ago timestamp",
@@ -1233,12 +1008,10 @@
1008
}
1009
}
1010
},
1236
-
1011
"conversation_yesterday": "Yesterday",
1012
"@conversation_yesterday": {
1013
"description": "Yesterday timestamp"
1014
},
1241
-
1015
"conversation_days_ago": "{days}d ago",
1016
"@conversation_days_ago": {
1017
"description": "Days ago timestamp",
@@ -1249,7 +1022,6 @@
1022
}
1023
}
1024
},
1252
-
1025
"conversation_date": "{month}/{day}/{year}",
1026
"@conversation_date": {
1027
"description": "Full date timestamp",
@@ -1268,172 +1040,138 @@
1040
}
1041
}
1042
},
1271
-
1043
"options_tooltip": "Options",
1044
"@options_tooltip": {
1045
"description": "Options menu tooltip"
1046
},
1276
-
1047
"no_results_found": "No results found",
1048
"@no_results_found": {
1049
"description": "Search empty state"
1050
},
1281
-
1051
"no_conversations_yet": "No conversations yet",
1052
"@no_conversations_yet": {
1053
"description": "Empty conversations state"
1054
},
1286
-
1055
"try_different_search": "Try a different search term",
1056
"@try_different_search": {
1057
"description": "Search empty state subtitle"
1058
},
1291
-
1059
"start_new_conversation": "Start a new conversation",
1060
"@start_new_conversation": {
1061
"description": "Empty state subtitle when no conversations"
1062
},
1296
-
1063
"rename_conversation": "Rename conversation",
1064
"@rename_conversation": {
1065
"description": "Rename dialog title"
1066
},
1301
-
1067
"enter_new_title": "Enter new title",
1068
"@enter_new_title": {
1069
"description": "Rename text field hint"
1070
},
1306
-
1071
"pinned_section": "PINNED",
1072
"@pinned_section": {
1073
"description": "Pinned conversations section header"
1074
},
1311
-
1075
"today_section": "TODAY",
1076
"@today_section": {
1077
"description": "Today section header"
1078
},
1316
-
1079
"yesterday_section": "YESTERDAY",
1080
"@yesterday_section": {
1081
"description": "Yesterday section header"
1082
},
1321
-
1083
"previous_7_days": "PREVIOUS 7 DAYS",
1084
"@previous_7_days": {
1085
"description": "Previous 7 days section header"
1086
},
1326
-
1087
"previous_30_days": "PREVIOUS 30 DAYS",
1088
"@previous_30_days": {
1089
"description": "Previous 30 days section header"
1090
},
1331
-
1091
"older_section": "OLDER",
1092
"@older_section": {
1093
"description": "Older conversations section header"
1094
},
1336
-
1095
"onboarding_choose_language": "Choose Language",
1096
"@onboarding_choose_language": {
1097
"description": "Onboarding title to select application language"
1098
},
1341
-
1099
"onboarding_choose_language_desc": "Select your preferred language. You can change this anytime in settings.",
1100
"@onboarding_choose_language_desc": {
1101
"description": "Onboarding description for language selection"
1102
},
1346
-
1103
"onboarding_localmind": "LOCALMIND",
1104
"@onboarding_localmind": {
1105
"description": "Onboarding screen branding text"
1106
},
1351
-
1107
"onboarding_connect_server": "Connect Your\nServer",
1108
"@onboarding_connect_server": {
1109
"description": "Onboarding heading for server connection"
1110
},
1356
-
1111
"onboarding_connect_desc": "Connect to LM Studio, Ollama, or\nOpenRouter to start your private AI\nexperience.",
1112
"@onboarding_connect_desc": {
1113
"description": "Onboarding server connection description"
1114
},
1361
-
1115
"openai_compatible_api": "OpenAI-compatible API",
1116
"@openai_compatible_api": {
1117
"description": "Subtitle for the OpenAI compatible server card in onboarding"
1118
},
1366
-
1119
"https_requires_ssl": "HTTPS requires SSL",
1120
"@https_requires_ssl": {
1121
"description": "Hint shown when a user enters an https URL for a local server"
1122
},
1371
-
1123
"most_local_setups_use_http": "Most local setups use http://",
1124
"@most_local_setups_use_http": {
1125
"description": "Hint shown with the HTTPS warning chip"
1126
},
1376
-
1127
"onboarding_welcome": "Welcome to LocalMind",
1128
"@onboarding_welcome": {
1129
"description": "Onboarding welcome message"
1130
},
1381
-
1131
"server_type_on_device": "On-Device",
1132
"@server_type_on_device": {
1133
"description": "On-device server type card title"
1134
},
1386
-
1135
"server_type_on_device_sub": "NO SERVER NEEDED",
1136
"@server_type_on_device_sub": {
1137
"description": "On-device server type card subtitle"
1138
},
1391
-
1139
"server_type_lm_studio": "LM Studio",
1140
"@server_type_lm_studio": {
1141
"description": "LM Studio server type card title"
1142
},
1396
-
1143
"server_type_lm_studio_sub": "LOCAL API",
1144
"@server_type_lm_studio_sub": {
1145
"description": "LM Studio server type card subtitle"
1146
},
1401
-
1147
"server_type_ollama": "Ollama",
1148
"@server_type_ollama": {
1149
"description": "Ollama server type card title"
1150
},
1406
-
1151
"server_type_ollama_sub": "CLI ENGINE",
1152
"@server_type_ollama_sub": {
1153
"description": "Ollama server type card subtitle"
1154
},
1411
-
1155
"server_type_openrouter": "OpenRouter",
1156
"@server_type_openrouter": {
1157
"description": "OpenRouter server type card title"
1158
},
1416
-
1159
"server_type_openrouter_sub": "UNIFIED CLOUD",
1160
"@server_type_openrouter_sub": {
1161
"description": "OpenRouter server type card subtitle"
1162
},
1421
-
1163
"ready_continue": "READY TO CONTINUE",
1164
"@ready_continue": {
1165
"description": "Onboarding ready status"
1166
},
1426
-
1167
"waiting_selection": "WAITING FOR SELECTION",
1168
"@waiting_selection": {
1169
"description": "Onboarding waiting status"
1170
},
1431
-
1171
"setup_connection": "Setup Connection",
1172
"@setup_connection": {
1173
"description": "Onboarding setup screen title"
1174
},
1436
-
1175
"setup_connection_desc": "Configure your {server} server to start chatting.",
1176
"@setup_connection_desc": {
1177
"description": "Onboarding setup description",
@@ -1444,142 +1182,114 @@
1182
}
1183
}
1184
},
1447
-
1185
"server_name": "Server Name",
1186
"@server_name": {
1187
"description": "Server name field label"
1188
},
1452
-
1189
"name_required": "Name required",
1190
"@name_required": {
1191
"description": "Validation: name required"
1192
},
1457
-
1193
"name_max_50": "Max 50 characters",
1194
"@name_max_50": {
1195
"description": "Validation: max 50 chars"
1196
},
1462
-
1197
"host_label": "Host / IP Address",
1198
"@host_label": {
1199
"description": "Host field label"
1200
},
1467
-
1201
"host_required": "Host required",
1202
"@host_required": {
1203
"description": "Validation: host required"
1204
},
1472
-
1205
"port_label": "Port",
1206
"@port_label": {
1207
"description": "Port field label"
1208
},
1477
-
1209
"port_required": "Port required",
1210
"@port_required": {
1211
"description": "Validation: port required"
1212
},
1482
-
1213
"port_invalid": "Must be a number",
1214
"@port_invalid": {
1215
"description": "Validation: invalid port number"
1216
},
1487
-
1217
"port_range": "Enter a valid port (1-65535)",
1218
"@port_range": {
1219
"description": "Validation: port range"
1220
},
1492
-
1221
"api_key_required": "API Key *",
1222
"@api_key_required": {
1223
"description": "Required API key label"
1224
},
1497
-
1225
"api_key_optional": "API Key (Optional)",
1226
"@api_key_optional": {
1227
"description": "Optional API key label"
1228
},
1502
-
1229
"api_key_required_openrouter": "API Key required for OpenRouter",
1230
"@api_key_required_openrouter": {
1231
"description": "Validation: API key for OpenRouter"
1232
},
1507
-
1233
"api_key_format": "OpenRouter API keys start with sk-",
1234
"@api_key_format": {
1235
"description": "Validation: API key format"
1236
},
1512
-
1237
"my_server_hint": "My Server",
1238
"@my_server_hint": {
1239
"description": "Server name field hint"
1240
},
1517
-
1241
"name_length_validation": "Name must be 50 characters or less",
1242
"@name_length_validation": {
1243
"description": "Validation: name too long"
1244
},
1522
-
1245
"host_valid": "Enter a valid hostname or IP address",
1246
"@host_valid": {
1247
"description": "Validation: invalid host"
1248
},
1527
-
1249
"api_key_hint_openrouter": "sk-...",
1250
"@api_key_hint_openrouter": {
1251
"description": "OpenRouter API key hint"
1252
},
1532
-
1253
"api_key_hint_generic": "For authenticated servers",
1254
"@api_key_hint_generic": {
1255
"description": "Generic API key hint"
1256
},
1537
-
1257
"update_server": "Update Server",
1258
"@update_server": {
1259
"description": "Update server button"
1260
},
1542
-
1261
"save_server": "Save Server",
1262
"@save_server": {
1263
"description": "Save server button"
1264
},
1547
-
1265
"server_updated": "Server updated",
1266
"@server_updated": {
1267
"description": "Server update snackbar"
1268
},
1552
-
1269
"server_added": "Server added",
1270
"@server_added": {
1271
"description": "Server added snackbar"
1272
},
1557
-
1273
"download_model_title": "Download a Model",
1274
"@download_model_title": {
1275
"description": "Onboarding download model screen title"
1276
},
1562
-
1277
"download_model_desc": "Choose a model to download.\nIt will run locally on your device.",
1278
"@download_model_desc": {
1279
"description": "Onboarding download model description"
1280
},
1567
-
1281
"on_device_android_only": "On-device inference is currently available on Android only.",
1282
"@on_device_android_only": {
1283
"description": "Platform limitation notice"
1284
},
1572
-
1285
"total_ram": "Total RAM",
1286
"@total_ram": {
1287
"description": "Total RAM label"
1288
},
1577
-
1289
"available": "Available",
1290
"@available": {
1291
"description": "Available label"
1292
},
1582
-
1293
"ram_min_required": "{fileSize} GB RAM min",
1294
"@ram_min_required": {
1295
"description": "Minimum RAM requirement label",
@@ -1590,7 +1300,6 @@
1300
}
1301
}
1302
},
1593
-
1303
"download_progress": "{percent}% • {speed}",
1304
"@download_progress": {
1305
"description": "Download progress display",
@@ -1605,7 +1314,6 @@
1314
}
1315
}
1316
},
1608
-
1317
"eta_label": "ETA: {eta}",
1318
"@eta_label": {
1319
"description": "Estimated time remaining label",
@@ -1616,7 +1324,6 @@
1324
}
1325
}
1326
},
1619
-
1327
"paused_progress": "Paused - {percent}%",
1328
"@paused_progress": {
1329
"description": "Paused download status with progress",
@@ -1627,7 +1334,6 @@
1334
}
1335
}
1336
},
1630
-
1337
"ram_warning_body_download": "This model requires at least {ram} GB RAM, but your device has {totalMemory}. It may not run correctly or could cause the app to crash.",
1338
"@ram_warning_body_download": {
1339
"description": "RAM warning during download",
@@ -1642,7 +1348,6 @@
1348
}
1349
}
1350
},
1645
-
1351
"ram_warning_body_load": "Your device has {availableRAM} available RAM, but this model recommends at least {ram} GB. Loading it might fail or cause instability.",
1352
"@ram_warning_body_load": {
1353
"description": "RAM warning during model load",
@@ -1657,107 +1362,86 @@
1362
}
1363
}
1364
},
1660
-
1365
"choose_theme": "Choose Theme",
1366
"@choose_theme": {
1367
"description": "Onboarding theme selection title"
1368
},
1665
-
1369
"choose_theme_desc": "Personalize the app appearance. You can always change this later in settings.",
1370
"@choose_theme_desc": {
1371
"description": "Onboarding theme selection description"
1372
},
1670
-
1373
"theme_card_system": "System",
1374
"@theme_card_system": {
1375
"description": "System theme card title"
1376
},
1675
-
1377
"theme_card_system_sub": "Matches your device settings",
1378
"@theme_card_system_sub": {
1379
"description": "System theme card subtitle"
1380
},
1680
-
1381
"theme_card_light": "Light",
1382
"@theme_card_light": {
1383
"description": "Light theme card title"
1384
},
1685
-
1385
"theme_card_light_sub": "Clean and bright",
1386
"@theme_card_light_sub": {
1387
"description": "Light theme card subtitle"
1388
},
1690
-
1389
"theme_card_dark": "Dark",
1390
"@theme_card_dark": {
1391
"description": "Dark theme card title"
1392
},
1695
-
1393
"theme_card_dark_sub": "Easy on the eyes",
1394
"@theme_card_dark_sub": {
1395
"description": "Dark theme card subtitle"
1396
},
1700
-
1397
"theme_card_claude": "Claude",
1398
"@theme_card_claude": {
1399
"description": "Claude theme card title"
1400
},
1705
-
1401
"theme_card_claude_sub": "A warm, peach-tinted theme",
1402
"@theme_card_claude_sub": {
1403
"description": "Claude theme card subtitle"
1404
},
1710
-
1405
"stay_updated": "Stay Updated",
1406
"@stay_updated": {
1407
"description": "Onboarding notification screen heading"
1408
},
1715
-
1409
"stay_updated_desc": "Get notified when your AI models finish downloading or when long-running tasks complete.",
1410
"@stay_updated_desc": {
1411
"description": "Onboarding notification screen description"
1412
},
1720
-
1413
"notification_benefit_downloads": "Model download progress",
1414
"@notification_benefit_downloads": {
1415
"description": "Notification benefit list item"
1416
},
1725
-
1417
"notification_benefit_completions": "Generation completions",
1418
"@notification_benefit_completions": {
1419
"description": "Notification benefit list item"
1420
},
1730
-
1421
"notification_benefit_background": "Background tasks status",
1422
"@notification_benefit_background": {
1423
"description": "Notification benefit list item"
1424
},
1735
-
1425
"allow_notifications": "Allow Notifications",
1426
"@allow_notifications": {
1427
"description": "Allow notifications button"
1428
},
1740
-
1429
"servers_title": "Servers",
1430
"@servers_title": {
1431
"description": "Server list screen title"
1432
},
1745
-
1433
"no_servers_yet": "No Servers Yet",
1434
"@no_servers_yet": {
1435
"description": "Empty server list title"
1436
},
1750
-
1437
"no_servers_desc": "Add your first server to start chatting with AI models.",
1438
"@no_servers_desc": {
1439
"description": "Empty server list description"
1440
},
1755
-
1441
"add_server": "Add Server",
1442
"@add_server": {
1443
"description": "Add server button"
1444
},
1760
-
1445
"switched_to_server": "Switched to {name}",
1446
"@switched_to_server": {
1447
"description": "Server switch snackbar",
@@ -1768,67 +1452,54 @@
1452
}
1453
}
1454
},
1771
-
1455
"edit_server": "Edit Server",
1456
"@edit_server": {
1457
"description": "Edit server screen title"
1458
},
1776
-
1459
"add_server_title": "Add Server",
1460
"@add_server_title": {
1461
"description": "Add server screen title"
1462
},
1781
-
1463
"server_type_label": "Server Type",
1464
"@server_type_label": {
1465
"description": "Server type section label"
1466
},
1786
-
1467
"server_icon_label": "Server Icon",
1468
"@server_icon_label": {
1469
"description": "Server icon section label"
1470
},
1791
-
1471
"default_icon": "Default icon",
1472
"@default_icon": {
1473
"description": "Default icon fallback text"
1474
},
1796
-
1475
"server_type_lm_studio_display": "LM Studio",
1476
"@server_type_lm_studio_display": {
1477
"description": "LM Studio server type display"
1478
},
1801
-
1479
"server_type_openai_display": "OpenAI Compatible",
1480
"@server_type_openai_display": {
1481
"description": "OpenAI compatible server type display"
1482
},
1806
-
1483
"server_type_ollama_display": "Ollama",
1484
"@server_type_ollama_display": {
1485
"description": "Ollama server type display"
1486
},
1811
-
1487
"server_type_openrouter_display": "OpenRouter",
1488
"@server_type_openrouter_display": {
1489
"description": "OpenRouter server type display"
1490
},
1816
-
1491
"server_type_on_device_display": "On-Device",
1492
"@server_type_on_device_display": {
1493
"description": "On-device server type display"
1494
},
1821
-
1495
"server_address_openrouter": "openrouter.ai",
1496
"@server_address_openrouter": {
1497
"description": "OpenRouter default address"
1498
},
1826
-
1499
"server_address_on_device": "Local inference",
1500
"@server_address_on_device": {
1501
"description": "On-device server address"
1502
},
1831
-
1503
"server_address_format": "{host}:{port}",
1504
"@server_address_format": {
1505
"description": "Server address display format",
@@ -1843,392 +1514,314 @@
1514
}
1515
}
1516
},
1846
-
1517
"default_badge": "Default",
1518
"@default_badge": {
1519
"description": "Default server badge"
1520
},
1851
-
1521
"set_as_default": "Set as Default",
1522
"@set_as_default": {
1523
"description": "Set as default menu item"
1524
},
1856
-
1525
"select_icon": "Select Icon",
1526
"@select_icon": {
1527
"description": "Icon picker sheet title"
1528
},
1861
-
1529
"select_icon_desc": "Choose an icon for your server",
1530
"@select_icon_desc": {
1531
"description": "Icon picker sheet description"
1532
},
1866
-
1533
"search_icons_hint": "Search icons...",
1534
"@search_icons_hint": {
1535
"description": "Icon search field placeholder"
1536
},
1871
-
1537
"server_icon_stack": "Server Stack",
1538
"@server_icon_stack": {
1539
"description": "Server icon name"
1540
},
1876
-
1541
"server_icon_stack2": "Server Stack 02",
1542
"@server_icon_stack2": {
1543
"description": "Server icon name"
1544
},
1881
-
1545
"server_icon_stack3": "Server Stack 03",
1546
"@server_icon_stack3": {
1547
"description": "Server icon name"
1548
},
1886
-
1549
"server_icon_cloud": "Cloud",
1550
"@server_icon_cloud": {
1551
"description": "Server icon name"
1552
},
1891
-
1553
"server_icon_cloud_server": "Cloud Server",
1554
"@server_icon_cloud_server": {
1555
"description": "Server icon name"
1556
},
1896
-
1557
"server_icon_mcp": "MCP Server",
1558
"@server_icon_mcp": {
1559
"description": "Server icon name"
1560
},
1901
-
1561
"server_icon_database": "Database",
1562
"@server_icon_database": {
1563
"description": "Server icon name"
1564
},
1906
-
1565
"server_icon_database1": "Database 01",
1566
"@server_icon_database1": {
1567
"description": "Server icon name"
1568
},
1911
-
1569
"server_icon_database2": "Database 02",
1570
"@server_icon_database2": {
1571
"description": "Server icon name"
1572
},
1916
-
1573
"server_icon_cpu": "CPU",
1574
"@server_icon_cpu": {
1575
"description": "Server icon name"
1576
},
1921
-
1577
"server_icon_chip": "Chip",
1578
"@server_icon_chip": {
1579
"description": "Server icon name"
1580
},
1926
-
1581
"server_icon_chip2": "Chip 02",
1582
"@server_icon_chip2": {
1583
"description": "Server icon name"
1584
},
1931
-
1585
"server_icon_computer": "Computer",
1586
"@server_icon_computer": {
1587
"description": "Server icon name"
1588
},
1936
-
1589
"server_icon_laptop": "Laptop",
1590
"@server_icon_laptop": {
1591
"description": "Server icon name"
1592
},
1941
-
1593
"server_icon_terminal": "Computer Terminal",
1594
"@server_icon_terminal": {
1595
"description": "Server icon name"
1596
},
1946
-
1597
"server_icon_code": "Code",
1598
"@server_icon_code": {
1599
"description": "Server icon name"
1600
},
1951
-
1601
"server_icon_ai_brain": "AI Brain",
1602
"@server_icon_ai_brain": {
1603
"description": "Server icon name"
1604
},
1956
-
1605
"server_icon_ai_brain2": "AI Brain 02",
1606
"@server_icon_ai_brain2": {
1607
"description": "Server icon name"
1608
},
1961
-
1609
"server_icon_ai_cloud": "AI Cloud",
1610
"@server_icon_ai_cloud": {
1611
"description": "Server icon name"
1612
},
1966
-
1613
"server_icon_ai_network": "AI Network",
1614
"@server_icon_ai_network": {
1615
"description": "Server icon name"
1616
},
1971
-
1617
"server_icon_ai_chat": "AI Chat",
1618
"@server_icon_ai_chat": {
1619
"description": "Server icon name"
1620
},
1976
-
1621
"server_icon_cellular": "Cellular Network",
1622
"@server_icon_cellular": {
1623
"description": "Server icon name"
1624
},
1981
-
1625
"server_icon_plug1": "Plug 01",
1626
"@server_icon_plug1": {
1627
"description": "Server icon name"
1628
},
1986
-
1629
"server_icon_plug2": "Plug 02",
1630
"@server_icon_plug2": {
1631
"description": "Server icon name"
1632
},
1991
-
1633
"server_icon_bot": "Bot",
1634
"@server_icon_bot": {
1635
"description": "Server icon name"
1636
},
1996
-
1637
"server_icon_bot2": "Bot 02",
1638
"@server_icon_bot2": {
1639
"description": "Server icon name"
1640
},
2001
-
1641
"server_icon_robotic": "Robotic",
1642
"@server_icon_robotic": {
1643
"description": "Server icon name"
1644
},
2006
-
1645
"server_icon_rocket": "Rocket",
1646
"@server_icon_rocket": {
1647
"description": "Server icon name"
1648
},
2011
-
1649
"server_icon_star": "Star",
1650
"@server_icon_star": {
1651
"description": "Server icon name"
1652
},
2016
-
1653
"server_icon_settings1": "Settings 01",
1654
"@server_icon_settings1": {
1655
"description": "Server icon name"
1656
},
2021
-
1657
"server_icon_settings2": "Settings 02",
1658
"@server_icon_settings2": {
1659
"description": "Server icon name"
1660
},
2026
-
1661
"server_icon_home1": "Home 01",
1662
"@server_icon_home1": {
1663
"description": "Server icon name"
1664
},
2031
-
1665
"server_icon_home2": "Home 02",
1666
"@server_icon_home2": {
1667
"description": "Server icon name"
1668
},
2036
-
1669
"server_icon_folder1": "Folder 01",
1670
"@server_icon_folder1": {
1671
"description": "Server icon name"
1672
},
2041
-
1673
"server_icon_folder2": "Folder 02",
1674
"@server_icon_folder2": {
1675
"description": "Server icon name"
1676
},
2046
-
1677
"server_icon_file1": "File 01",
1678
"@server_icon_file1": {
1679
"description": "Server icon name"
1680
},
2051
-
1681
"server_icon_lock": "Lock",
1682
"@server_icon_lock": {
1683
"description": "Server icon name"
1684
},
2056
-
1685
"server_icon_key": "Key 01",
1686
"@server_icon_key": {
1687
"description": "Server icon name"
1688
},
2061
-
1689
"server_icon_link": "Link 01",
1690
"@server_icon_link": {
1691
"description": "Server icon name"
1692
},
2066
-
1693
"server_icon_globe": "Globe",
1694
"@server_icon_globe": {
1695
"description": "Server icon name"
1696
},
2071
-
1697
"server_icon_api": "API",
1698
"@server_icon_api": {
1699
"description": "Server icon name"
1700
},
2076
-
1701
"server_icon_arrow_right": "Arrow Right 01",
1702
"@server_icon_arrow_right": {
1703
"description": "Server icon name"
1704
},
2081
-
1705
"server_icon_check": "Check Circle",
1706
"@server_icon_check": {
1707
"description": "Server icon name"
1708
},
2086
-
1709
"server_icon_alert": "Alert Circle",
1710
"@server_icon_alert": {
1711
"description": "Server icon name"
1712
},
2091
-
1713
"server_icon_info": "Info Circle",
1714
"@server_icon_info": {
1715
"description": "Server icon name"
1716
},
2096
-
1717
"server_icon_zap": "Zap",
1718
"@server_icon_zap": {
1719
"description": "Server icon name"
1720
},
2101
-
1721
"server_icon_cloud_upload": "Cloud Upload",
1722
"@server_icon_cloud_upload": {
1723
"description": "Server icon name"
1724
},
2106
-
1725
"server_icon_cloud_download": "Cloud Download",
1726
"@server_icon_cloud_download": {
1727
"description": "Server icon name"
1728
},
2111
-
1729
"server_icon_refresh": "Refresh",
1730
"@server_icon_refresh": {
1731
"description": "Server icon name"
1732
},
2116
-
1733
"server_icon_hard_drive": "Hard Drive",
1734
"@server_icon_hard_drive": {
1735
"description": "Server icon name"
1736
},
2121
-
1737
"server_icon_drive": "Drive",
1738
"@server_icon_drive": {
1739
"description": "Server icon name"
1740
},
2126
-
1741
"personas_title": "Personas",
1742
"@personas_title": {
1743
"description": "Persona list screen title"
1744
},
2131
-
1745
"persona_category_general": "General",
1746
"@persona_category_general": {
1747
"description": "General persona category"
1748
},
2136
-
1749
"persona_category_coding": "Coding",
1750
"@persona_category_coding": {
1751
"description": "Coding persona category"
1752
},
2141
-
1753
"persona_category_education": "Education",
1754
"@persona_category_education": {
1755
"description": "Education persona category"
1756
},
2146
-
1757
"persona_category_creative": "Creative",
1758
"@persona_category_creative": {
1759
"description": "Creative persona category"
1760
},
2151
-
1761
"persona_builtin_section": "BUILT-IN",
1762
"@persona_builtin_section": {
1763
"description": "Built-in personas section label"
1764
},
2156
-
1765
"persona_my_section": "MY PERSONAS",
1766
"@persona_my_section": {
1767
"description": "My personas section label"
1768
},
2161
-
1769
"clone_edit": "Clone & Edit",
1770
"@clone_edit": {
1771
"description": "Clone and edit persona action"
1772
},
2166
-
1773
"builtin_badge": "Built-in",
1774
"@builtin_badge": {
1775
"description": "Built-in persona badge"
1776
},
2171
-
1777
"no_personas_found": "No personas found",
1778
"@no_personas_found": {
1779
"description": "Empty persona list title"
1780
},
2176
-
1781
"no_personas_desc": "Create your first persona to customize AI behavior.",
1782
"@no_personas_desc": {
1783
"description": "Empty persona list subtitle"
1784
},
2181
-
1785
"edit_persona": "Edit Persona",
1786
"@edit_persona": {
1787
"description": "Edit persona screen title"
1788
},
2186
-
1789
"create_persona": "Create Persona",
1790
"@create_persona": {
1791
"description": "Create persona screen title"
1792
},
2191
-
1793
"create_persona_button": "Create",
1794
"@create_persona_button": {
1795
"description": "Create persona action button"
1796
},
2196
-
1797
"emoji_label": "Emoji",
1798
"@emoji_label": {
1799
"description": "Emoji selection label"
1800
},
2201
-
1801
"name_label": "Name",
1802
"@name_label": {
1803
"description": "Name field label"
1804
},
2206
-
1805
"my_persona_hint": "My Persona",
1806
"@my_persona_hint": {
1807
"description": "Persona name hint"
1808
},
2211
-
1809
"category_label": "Category",
1810
"@category_label": {
1811
"description": "Category field label"
1812
},
2216
-
1813
"description_optional": "Description (optional)",
1814
"@description_optional": {
1815
"description": "Optional description label"
1816
},
2221
-
1817
"description_hint": "What this persona does...",
1818
"@description_hint": {
1819
"description": "Description field hint"
1820
},
2226
-
1821
"system_prompt": "System Prompt",
1822
"@system_prompt": {
1823
"description": "System prompt section label"
1824
},
2231
-
1825
"character_count_max": "{currentLen}/4000",
1826
"@character_count_max": {
1827
"description": "Character counter for system prompt",
@@ -2239,92 +1832,74 @@
1832
}
1833
}
1834
},
2242
-
1835
"no_prompt_placeholder": "No prompt yet...",
1836
"@no_prompt_placeholder": {
1837
"description": "Preview placeholder when no prompt"
1838
},
2247
-
1839
"prompt_hint": "You are a helpful assistant...",
1840
"@prompt_hint": {
1841
"description": "System prompt text field hint"
1842
},
2252
-
1843
"prompt_required": "System prompt is required",
1844
"@prompt_required": {
1845
"description": "Validation: prompt required"
1846
},
2257
-
1847
"prompt_max_chars": "Max 4000 characters",
1848
"@prompt_max_chars": {
1849
"description": "Validation: prompt max chars"
1850
},
2262
-
1851
"advanced_settings": "Advanced Settings",
1852
"@advanced_settings": {
1853
"description": "Expandable advanced settings section"
1854
},
2267
-
1855
"temperature_label": "Temperature (0.0-2.0)",
1856
"@temperature_label": {
1857
"description": "Temperature field label in personas"
1858
},
2272
-
1859
"top_p_label": "Top P (0.0-1.0)",
1860
"@top_p_label": {
1861
"description": "Top P field label in personas"
1862
},
2277
-
1863
"temp_hint": "0.7",
1864
"@temp_hint": {
1865
"description": "Temperature field hint"
1866
},
2282
-
1867
"top_p_hint": "0.9",
1868
"@top_p_hint": {
1869
"description": "Top P field hint"
1870
},
2287
-
1871
"range_0_2": "0.0-2.0",
1872
"@range_0_2": {
1873
"description": "Range validation error (temperature)"
1874
},
2292
-
1875
"range_0_1": "0.0-1.0",
1876
"@range_0_1": {
1877
"description": "Range validation error (top P)"
1878
},
2297
-
1879
"persona_updated": "Persona updated",
1880
"@persona_updated": {
1881
"description": "Persona update snackbar"
1882
},
2302
-
1883
"persona_created": "Persona created",
1884
"@persona_created": {
1885
"description": "Persona creation snackbar"
1886
},
2307
-
1887
"tts_models_title": "Text To Speech Models",
1888
"@tts_models_title": {
1889
"description": "TTS model manager screen title"
1890
},
2312
-
1891
"always_available": "Always available",
1892
"@always_available": {
1893
"description": "Always available status"
1894
},
2317
-
1895
"tts_system_desc": "Uses your device's built-in text-to-speech engine.\nNo downloads required. Voice selection uses your device's system settings.",
1896
"@tts_system_desc": {
1897
"description": "System TTS engine description"
1898
},
2322
-
1899
"downloading_status": "Downloading...",
1900
"@downloading_status": {
1901
"description": "Downloading status text"
1902
},
2327
-
1903
"tts_kitten_desc": "Lightning-fast neural TTS with 8 expressive voices.\nRequires {size} download.",
1904
"@tts_kitten_desc": {
1905
"description": "Kitten TTS engine description",
@@ -2335,7 +1910,6 @@
1910
}
1911
}
1912
},
2338
-
1913
"tts_piper_desc": "Fast offline Piper voices with 2 expressive voices.\nRequires {size} download per voice.",
1914
"@tts_piper_desc": {
1915
"description": "Piper TTS engine description",
@@ -2346,7 +1920,6 @@
1920
}
1921
}
1922
},
2349
-
1923
"engine_spec": "{sizeMb} MB · {ramMb} MB RAM · {voiceCount} voices",
1924
"@engine_spec": {
1925
"description": "Engine specification display",
@@ -2365,42 +1938,34 @@
1938
}
1939
}
1940
},
2368
-
1941
"on_device_models_title": "On-Device Models",
1942
"@on_device_models_title": {
1943
"description": "On-device model manager screen title"
1944
},
2373
-
1945
"available_models": "Available Models",
1946
"@available_models": {
1947
"description": "Available models section title"
1948
},
2378
-
1949
"device_memory": "Device Memory",
1950
"@device_memory": {
1951
"description": "Device memory card title"
1952
},
2383
-
1953
"ram_usage": "RAM Usage",
1954
"@ram_usage": {
1955
"description": "RAM usage label"
1956
},
2388
-
1957
"memory_healthy": "Healthy",
1958
"@memory_healthy": {
1959
"description": "Memory status healthy"
1960
},
2393
-
1961
"memory_critical": "Critical",
1962
"@memory_critical": {
1963
"description": "Memory status critical"
1964
},
2398
-
1965
"memory_low": "Low",
1966
"@memory_low": {
1967
"description": "Memory status low"
1968
},
2403
-
1969
"ram_used": "{percent}% used",
1970
"@ram_used": {
1971
"description": "RAM usage percentage",
@@ -2411,82 +1976,66 @@
1976
}
1977
}
1978
},
2414
-
1979
"available_ram": "Available RAM",
1980
"@available_ram": {
1981
"description": "Available RAM stat label"
1982
},
2419
-
1983
"total_capacity": "Total Capacity",
1984
"@total_capacity": {
1985
"description": "Total capacity stat label"
1986
},
2424
-
1987
"loaded_status": "Loaded",
1988
"@loaded_status": {
1989
"description": "Model loaded status"
1990
},
2429
-
1991
"inference_backend": "Inference Backend",
1992
"@inference_backend": {
1993
"description": "Backend selector section title"
1994
},
2434
-
1995
"backend_ios_notice": "Only CPU backend is available on iOS.",
1996
"@backend_ios_notice": {
1997
"description": "iOS backend limitation notice"
1998
},
2439
-
1999
"backend_cpu_desc": "Works on all devices. Most compatible.",
2000
"@backend_cpu_desc": {
2001
"description": "CPU backend description"
2002
},
2444
-
2003
"backend_gpu_desc": "OpenCL acceleration. Faster on supported devices.",
2004
"@backend_gpu_desc": {
2005
"description": "GPU backend description"
2006
},
2449
-
2007
"backend_npu_desc": "Vendor NPU (Qualcomm/MediaTek). Fastest inference.",
2008
"@backend_npu_desc": {
2009
"description": "NPU backend description"
2010
},
2454
-
2011
"select_model_title": "Select Model",
2012
"@select_model_title": {
2013
"description": "Model picker sheet title"
2014
},
2459
-
2015
"refresh_models": "Refresh models",
2016
"@refresh_models": {
2017
"description": "Refresh models tooltip"
2018
},
2464
-
2019
"search_models_hint": "Search models...",
2020
"@search_models_hint": {
2021
"description": "Model search field hint"
2022
},
2469
-
2023
"no_server_connected": "No server connected",
2024
"@no_server_connected": {
2025
"description": "Model picker empty state title"
2026
},
2474
-
2027
"add_server_first": "Add a server first to see available models.",
2028
"@add_server_first": {
2029
"description": "Model picker empty state subtitle"
2030
},
2479
-
2031
"failed_load_models": "Failed to load models",
2032
"@failed_load_models": {
2033
"description": "Model load error title"
2034
},
2484
-
2035
"no_models_available": "No models available",
2036
"@no_models_available": {
2037
"description": "Empty models state"
2038
},
2489
-
2039
"no_models_match": "No models match \"{searchQuery}\"",
2040
"@no_models_match": {
2041
"description": "No search results for models",
@@ -2497,7 +2046,6 @@
2046
}
2047
}
2048
},
2500
-
2049
"model_load_failed": "Failed to load model: {error}",
2050
"@model_load_failed": {
2051
"description": "Model load failure snackbar",
@@ -2508,7 +2056,6 @@
2056
}
2057
}
2058
},
2511
-
2059
"model_unloaded_ollama": "{name} will be unloaded once the keep-alive time passes",
2060
"@model_unloaded_ollama": {
2061
"description": "Ollama model unloaded status",
@@ -2519,7 +2066,6 @@
2066
}
2067
}
2068
},
2522
-
2069
"model_unloaded_success": "{name} unloaded successfully",
2070
"@model_unloaded_success": {
2071
"description": "Model unloaded success snackbar",
@@ -2530,7 +2076,6 @@
2076
}
2077
}
2078
},
2533
-
2079
"model_unload_failed": "Failed to unload model: {error}",
2080
"@model_unload_failed": {
2081
"description": "Model unload failure snackbar",
@@ -2541,12 +2086,10 @@
2086
}
2087
}
2088
},
2544
-
2089
"unload_from_server": "Unload from server",
2090
"@unload_from_server": {
2091
"description": "Unload model tooltip"
2092
},
2549
-
2093
"context_chip": "{ctx} ctx",
2094
"@context_chip": {
2095
"description": "Context length chip",
@@ -2557,7 +2100,6 @@
2100
}
2101
}
2102
},
2560
-
2103
"download_notification_title": "Downloading {modelName}...",
2104
"@download_notification_title": {
2105
"description": "Download notification title",
@@ -2568,12 +2110,10 @@
2110
}
2111
}
2112
},
2571
-
2113
"download_complete_notification": "Download complete!",
2114
"@download_complete_notification": {
2115
"description": "Download complete notification title"
2116
},
2576
-
2117
"download_complete_body": "{modelName} has been downloaded successfully.",
2118
"@download_complete_body": {
2119
"description": "Download complete notification body",
@@ -2584,7 +2124,6 @@
2124
}
2125
}
2126
},
2587
-
2127
"download_failed_notification": "Download failed: {error}",
2128
"@download_failed_notification": {
2129
"description": "Download failed notification title",
@@ -2595,7 +2134,6 @@
2134
}
2135
}
2136
},
2598
-
2137
"download_failed_body": "Failed to download {modelName}.",
2138
"@download_failed_body": {
2139
"description": "Download failed notification body",
@@ -2606,137 +2144,110 @@
2144
}
2145
}
2146
},
2609
-
2147
"engine_name_system": "System TTS",
2148
"@engine_name_system": {
2149
"description": "System TTS engine display name"
2150
},
2614
-
2151
"engine_tagline_system": "Built-in device engine",
2152
"@engine_tagline_system": {
2153
"description": "System TTS engine tagline"
2154
},
2619
-
2155
"engine_name_kitten": "Kitten TTS",
2156
"@engine_name_kitten": {
2157
"description": "Kitten TTS engine display name"
2158
},
2624
-
2159
"engine_tagline_kitten": "High-speed neural TTS",
2160
"@engine_tagline_kitten": {
2161
"description": "Kitten TTS engine tagline"
2162
},
2629
-
2163
"engine_name_sherpa": "Sherpa ONNX VITS",
2164
"@engine_name_sherpa": {
2165
"description": "Sherpa ONNX engine display name"
2166
},
2634
-
2167
"engine_tagline_sherpa": "Offline Piper voices",
2168
"@engine_tagline_sherpa": {
2169
"description": "Sherpa ONNX engine tagline"
2170
},
2639
-
2171
"voice_jasper": "Jasper",
2172
"@voice_jasper": {
2173
"description": "Kitten TTS voice name"
2174
},
2644
-
2175
"voice_bella": "Bella",
2176
"@voice_bella": {
2177
"description": "Kitten TTS voice name"
2178
},
2649
-
2179
"voice_bruno": "Bruno",
2180
"@voice_bruno": {
2181
"description": "Kitten TTS voice name"
2182
},
2654
-
2183
"voice_luna": "Luna",
2184
"@voice_luna": {
2185
"description": "Kitten TTS voice name"
2186
},
2659
-
2187
"voice_hugo": "Hugo",
2188
"@voice_hugo": {
2189
"description": "Kitten TTS voice name"
2190
},
2664
-
2191
"voice_rosie": "Rosie",
2192
"@voice_rosie": {
2193
"description": "Kitten TTS voice name"
2194
},
2669
-
2195
"voice_leo": "Leo",
2196
"@voice_leo": {
2197
"description": "Kitten TTS voice name"
2198
},
2674
-
2199
"voice_kiki": "Kiki",
2200
"@voice_kiki": {
2201
"description": "Kitten TTS voice name"
2202
},
2679
-
2203
"voice_lessac": "Lessac (US)",
2204
"@voice_lessac": {
2205
"description": "Piper voice name"
2206
},
2684
-
2207
"voice_ryan": "Ryan (US)",
2208
"@voice_ryan": {
2209
"description": "Piper voice name"
2210
},
2689
-
2211
"model_qwen_3": "Qwen 3 0.6B",
2212
"@model_qwen_3": {
2213
"description": "On-device model name"
2214
},
2694
-
2215
"model_qwen_3_desc": "Smallest general-purpose chat model. Fast responses, low memory usage.",
2216
"@model_qwen_3_desc": {
2217
"description": "Qwen 3 model description"
2218
},
2699
-
2219
"model_license_apache": "Apache-2.0",
2220
"@model_license_apache": {
2221
"description": "Apache license label"
2222
},
2704
-
2223
"model_qwen_25": "Qwen 2.5 1.5B Instruct",
2224
"@model_qwen_25": {
2225
"description": "On-device model name"
2226
},
2709
-
2227
"model_qwen_25_desc": "Balanced quality and size. Good for general conversation.",
2228
"@model_qwen_25_desc": {
2229
"description": "Qwen 2.5 model description"
2230
},
2714
-
2231
"model_deepseek": "DeepSeek R1 Distill Qwen 1.5B",
2232
"@model_deepseek": {
2233
"description": "On-device model name"
2234
},
2719
-
2235
"model_deepseek_desc": "Reasoning and chain-of-thought model. Best for logical tasks.",
2236
"@model_deepseek_desc": {
2237
"description": "DeepSeek model description"
2238
},
2724
-
2239
"model_license_mit": "MIT",
2240
"@model_license_mit": {
2241
"description": "MIT license label"
2242
},
2729
-
2243
"model_gemma": "Gemma 4 E2B Instruct",
2244
"@model_gemma": {
2245
"description": "On-device model name"
2246
},
2734
-
2247
"model_gemma_desc": "Google flagship model. Highest quality, requires more RAM.",
2248
"@model_gemma_desc": {
2249
"description": "Gemma model description"
2250
},
2739
-
2251
"export_header": "*Exported from LocalMind — {date}*",
2252
"@export_header": {
2253
"description": "Markdown export header",
@@ -2747,79 +2258,72 @@
2258
}
2259
}
2260
},
2750
-
2261
"export_role_user": "## 👤 User",
2262
"@export_role_user": {
2263
"description": "Markdown export user role label"
2264
},
2755
-
2265
"export_role_assistant": "## 🤖 Assistant",
2266
"@export_role_assistant": {
2267
"description": "Markdown export assistant role label"
2268
},
2760
-
2269
"export_role_system": "## ⚙️ System",
2270
"@export_role_system": {
2271
"description": "Markdown export system role label"
2272
},
2765
-
2273
"export_role_tool": "## 🔧 Tool",
2274
"@export_role_tool": {
2275
"description": "Markdown export tool role label"
2276
},
2770
-
2277
"export_text_user": "[USER]",
2278
"@export_text_user": {
2279
"description": "Text export user role prefix"
2280
},
2775
-
2281
"export_text_assistant": "[ASSISTANT]",
2282
"@export_text_assistant": {
2283
"description": "Text export assistant role prefix"
2284
},
2780
-
2285
"export_text_system": "[SYSTEM]",
2286
"@export_text_system": {
2287
"description": "Text export system role prefix"
2288
},
2785
-
2289
"export_text_tool": "[TOOL]",
2290
"@export_text_tool": {
2291
"description": "Text export tool role prefix"
2292
},
2790
-
2293
"export_label_user": "USER",
2294
"@export_label_user": {
2295
"description": "Export user role label"
2296
},
2795
-
2297
"export_label_assistant": "ASSISTANT",
2298
"@export_label_assistant": {
2299
"description": "Export assistant role label"
2300
},
2800
-
2301
"export_label_system": "SYSTEM",
2302
"@export_label_system": {
2303
"description": "Export system role label"
2304
},
2805
-
2305
"export_label_tool": "TOOL",
2306
"@export_label_tool": {
2307
"description": "Export tool role label"
2308
},
2810
-
2309
"select_model_hint": "Select a model to start chatting",
2310
"@select_model_hint": {
2311
"description": "Fallback text when no model is selected"
2312
},
2815
-
2313
"test_notification_title": "Test notification",
2314
"@test_notification_title": {
2315
"description": "Test notification title"
2316
},
2820
-
2317
"test_notification_body": "This is a test notification for model download progress.",
2318
"@test_notification_body": {
2319
"description": "Test notification body"
2320
+ },
2321
+ "tts_supports_background": "Supports background playback as native audio",
2322
+ "@tts_supports_background": {
2323
+ "description": "TTS background playback support status"
2324
+ },
2325
+ "tts_other_services_background_note": "Note: The other TTS services support background playback as native audio.",
2326
+ "@tts_other_services_background_note": {
2327
+ "description": "TTS background playback support status"
2328
}
2825
-}
2329
+}
\ No newline at end of file
lib/l10n/app_es.arb
+4
-2
@@ -503,5 +503,7 @@
503
"voice_rosie": "Rosie",
504
"voice_ryan": "Ryan (US)",
505
"waiting_selection": "ESPERANDO SELECCIÓN",
506
- "yesterday_section": "AYER"
507
-}
506
+ "yesterday_section": "AYER",
507
+ "tts_supports_background": "Soporta reproducción en segundo plano como audio nativo",
508
+ "tts_other_services_background_note": "Nota: Los otros servicios de TTS soportan reproducción en segundo plano como audio nativo."
509
+}
\ No newline at end of file
lib/l10n/app_hi.arb
+4
-2
@@ -503,5 +503,7 @@
503
"voice_rosie": "Rosie",
504
"voice_ryan": "Ryan (US)",
505
"waiting_selection": "चयन की प्रतीक्षा की जा रही है",
506
- "yesterday_section": "कल"
507
-}
506
+ "yesterday_section": "कल",
507
+ "tts_supports_background": "मूल ऑडियो के रूप में पृष्ठभूमि प्लेबैक का समर्थन करता है",
508
+ "tts_other_services_background_note": "नोट: अन्य टीटीएस सेवाएं मूल ऑडियो के रूप में पृष्ठभूमि प्लेबैक का समर्थन करती हैं।"
509
+}
\ No newline at end of file
lib/l10n/app_it.arb
+4
-2
@@ -503,5 +503,7 @@
503
"export_label_tool": "STRUMENTO",
504
"select_model_hint": "Seleziona un modello per iniziare a chattare",
505
"test_notification_title": "Notifica di prova",
506
- "test_notification_body": "Questa è una notifica di prova per l'avanzamento del download del modello."
507
-}
506
+ "test_notification_body": "Questa è una notifica di prova per l'avanzamento del download del modello.",
507
+ "tts_supports_background": "Supporta la riproduzione in background come audio nativo",
508
+ "tts_other_services_background_note": "Nota: Gli altri servizi TTS supportano la riproduzione in background come audio nativo."
509
+}
\ No newline at end of file
lib/l10n/app_ja.arb
+4
-2
@@ -503,5 +503,7 @@
503
"export_label_tool": "ツール",
504
"select_model_hint": "モデルを選択してチャットを開始",
505
"test_notification_title": "テスト通知",
506
- "test_notification_body": "これはモデルのダウンロード進捗用のテスト通知です。"
507
-}
506
+ "test_notification_body": "これはモデルのダウンロード進捗用のテスト通知です。",
507
+ "tts_supports_background": "ネイティブオーディオとしてバックグラウンド再生をサポート",
508
+ "tts_other_services_background_note": "注意: 他のTTSサービスはネイティブオーディオとしてバックグラウンド再生をサポートしています。"
509
+}
\ No newline at end of file
lib/l10n/app_localizations.dart
+12
@@ -3133,6 +3133,18 @@ abstract class AppLocalizations {
3133
/// In en, this message translates to:
3134
/// **'This is a test notification for model download progress.'**
3135
String get test_notification_body;
3136
+
3137
+ /// TTS background playback support status
3138
+ ///
3139
+ /// In en, this message translates to:
3140
+ /// **'Supports background playback as native audio'**
3141
+ String get tts_supports_background;
3142
+
3143
+ /// TTS background playback support status
3144
+ ///
3145
+ /// In en, this message translates to:
3146
+ /// **'Note: The other TTS services support background playback as native audio.'**
3147
+ String get tts_other_services_background_note;
3148
}
3149
3150
class _AppLocalizationsDelegate
lib/l10n/app_localizations_ar.dart
+7
@@ -1628,4 +1628,11 @@ class AppLocalizationsAr extends AppLocalizations {
1628
1629
@override
1630
String get test_notification_body => 'هذا إشعار تجريبي لتقدم تنزيل النموذج.';
1631
+
1632
+ @override
1633
+ String get tts_supports_background => 'يدعم التشغيل في الخلفية كصوت أصلي';
1634
+
1635
+ @override
1636
+ String get tts_other_services_background_note =>
1637
+ 'ملاحظة: تدعم خدمات TTS الأخرى التشغيل في الخلفية كصوت أصلي.';
1638
}
lib/l10n/app_localizations_bn.dart
+8
@@ -1638,4 +1638,12 @@ class AppLocalizationsBn extends AppLocalizations {
1638
@override
1639
String get test_notification_body =>
1640
'এটি মডেল ডাউনলোডের অগ্রগতির জন্য একটি পরীক্ষা নোটিফিকেশন।';
1641
+
1642
+ @override
1643
+ String get tts_supports_background =>
1644
+ 'নেটিভ অডিও হিসাবে ব্যাকগ্রাউন্ড প্লেব্যাক সমর্থন করে';
1645
+
1646
+ @override
1647
+ String get tts_other_services_background_note =>
1648
+ 'দ্রষ্টব্য: অন্যান্য টিটিএস পরিষেবাগুলি নেটিভ অডিও হিসাবে ব্যাকগ্রাউন্ড প্লেব্যাক সমর্থন করে।';
1649
}
lib/l10n/app_localizations_en.dart
+8
@@ -1631,4 +1631,12 @@ class AppLocalizationsEn extends AppLocalizations {
1631
@override
1632
String get test_notification_body =>
1633
'This is a test notification for model download progress.';
1634
+
1635
+ @override
1636
+ String get tts_supports_background =>
1637
+ 'Supports background playback as native audio';
1638
+
1639
+ @override
1640
+ String get tts_other_services_background_note =>
1641
+ 'Note: The other TTS services support background playback as native audio.';
1642
}
lib/l10n/app_localizations_es.dart
+8
@@ -1644,4 +1644,12 @@ class AppLocalizationsEs extends AppLocalizations {
1644
@override
1645
String get test_notification_body =>
1646
'Esta es una notificación de prueba para el progreso de descarga de modelos.';
1647
+
1648
+ @override
1649
+ String get tts_supports_background =>
1650
+ 'Soporta reproducción en segundo plano como audio nativo';
1651
+
1652
+ @override
1653
+ String get tts_other_services_background_note =>
1654
+ 'Nota: Los otros servicios de TTS soportan reproducción en segundo plano como audio nativo.';
1655
}
lib/l10n/app_localizations_hi.dart
+8
@@ -1635,4 +1635,12 @@ class AppLocalizationsHi extends AppLocalizations {
1635
@override
1636
String get test_notification_body =>
1637
'यह मॉडल डाउनलोड प्रगति के लिए एक परीक्षण सूचना है।';
1638
+
1639
+ @override
1640
+ String get tts_supports_background =>
1641
+ 'मूल ऑडियो के रूप में पृष्ठभूमि प्लेबैक का समर्थन करता है';
1642
+
1643
+ @override
1644
+ String get tts_other_services_background_note =>
1645
+ 'नोट: अन्य टीटीएस सेवाएं मूल ऑडियो के रूप में पृष्ठभूमि प्लेबैक का समर्थन करती हैं।';
1646
}
lib/l10n/app_localizations_it.dart
+8
@@ -1644,4 +1644,12 @@ class AppLocalizationsIt extends AppLocalizations {
1644
@override
1645
String get test_notification_body =>
1646
'Questa è una notifica di prova per l\'avanzamento del download del modello.';
1647
+
1648
+ @override
1649
+ String get tts_supports_background =>
1650
+ 'Supporta la riproduzione in background come audio nativo';
1651
+
1652
+ @override
1653
+ String get tts_other_services_background_note =>
1654
+ 'Nota: Gli altri servizi TTS supportano la riproduzione in background come audio nativo.';
1655
}
lib/l10n/app_localizations_ja.dart
+7
@@ -1611,4 +1611,11 @@ class AppLocalizationsJa extends AppLocalizations {
1611
1612
@override
1613
String get test_notification_body => 'これはモデルのダウンロード進捗用のテスト通知です。';
1614
+
1615
+ @override
1616
+ String get tts_supports_background => 'ネイティブオーディオとしてバックグラウンド再生をサポート';
1617
+
1618
+ @override
1619
+ String get tts_other_services_background_note =>
1620
+ '注意: 他のTTSサービスはネイティブオーディオとしてバックグラウンド再生をサポートしています。';
1621
}
lib/l10n/app_localizations_zh.dart
+6
@@ -1606,4 +1606,10 @@ class AppLocalizationsZh extends AppLocalizations {
1606
1607
@override
1608
String get test_notification_body => '这是用于展示模型下载进度的测试通知。';
1609
+
1610
+ @override
1611
+ String get tts_supports_background => '支持后台播放(原生音频格式)';
1612
+
1613
+ @override
1614
+ String get tts_other_services_background_note => '注意:其他TTS服务支持后台播放(原生音频格式)。';
1615
}
lib/l10n/app_zh.arb
+4
-2
@@ -503,5 +503,7 @@
503
"voice_rosie": "Rosie",
504
"voice_ryan": "Ryan (美音)",
505
"waiting_selection": "等待选择",
506
- "yesterday_section": "昨天"
507
-}
506
+ "yesterday_section": "昨天",
507
+ "tts_supports_background": "支持后台播放(原生音频格式)",
508
+ "tts_other_services_background_note": "注意:其他TTS服务支持后台播放(原生音频格式)。"
509
+}
\ No newline at end of file
lib/main.dart
+6
@@ -1,8 +1,14 @@
1
import 'package:flutter/material.dart';
2
+import 'package:just_audio_background/just_audio_background.dart';
3
4
import 'bootstrap/bootstrap_host.dart';
5
6
Future<void> main() async {
7
WidgetsFlutterBinding.ensureInitialized();
8
+ await JustAudioBackground.init(
9
+ androidNotificationChannelId: 'com.abdulmominsakib.localmind.channel.audio',
10
+ androidNotificationChannelName: 'LocalMind Audio TTS Playback',
11
+ androidNotificationOngoing: true,
12
+ );
13
runApp(const BootstrapHost());
14
}
pubspec.lock
+120
@@ -49,6 +49,38 @@ packages:
49
url: "https://pub.dev"
50
source: hosted
51
version: "2.13.1"
52
+ audio_service:
53
+ dependency: transitive
54
+ description:
55
+ name: audio_service
56
+ sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044
57
+ url: "https://pub.dev"
58
+ source: hosted
59
+ version: "0.18.18"
60
+ audio_service_platform_interface:
61
+ dependency: transitive
62
+ description:
63
+ name: audio_service_platform_interface
64
+ sha256: "6283782851f6c8b501b60904a32fc7199dc631172da0629d7301e66f672ab777"
65
+ url: "https://pub.dev"
66
+ source: hosted
67
+ version: "0.1.3"
68
+ audio_service_web:
69
+ dependency: transitive
70
+ description:
71
+ name: audio_service_web
72
+ sha256: b8ea9243201ee53383157fbccf13d5d2a866b5dda922ec19d866d1d5d70424df
73
+ url: "https://pub.dev"
74
+ source: hosted
75
+ version: "0.1.4"
76
+ audio_session:
77
+ dependency: "direct main"
78
+ description:
79
+ name: audio_session
80
+ sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac"
81
+ url: "https://pub.dev"
82
+ source: hosted
83
+ version: "0.1.25"
84
audioplayers:
85
dependency: "direct main"
86
description:
@@ -422,6 +454,14 @@ packages:
454
url: "https://pub.dev"
455
source: hosted
456
version: "4.5.2"
457
+ flutter_cache_manager:
458
+ dependency: transitive
459
+ description:
460
+ name: flutter_cache_manager
461
+ sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
462
+ url: "https://pub.dev"
463
+ source: hosted
464
+ version: "3.4.1"
465
flutter_gemma:
466
dependency: "direct main"
467
description:
@@ -725,6 +765,38 @@ packages:
765
url: "https://pub.dev"
766
source: hosted
767
version: "4.12.0"
768
+ just_audio:
769
+ dependency: "direct main"
770
+ description:
771
+ name: just_audio
772
+ sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e
773
+ url: "https://pub.dev"
774
+ source: hosted
775
+ version: "0.9.46"
776
+ just_audio_background:
777
+ dependency: "direct main"
778
+ description:
779
+ name: just_audio_background
780
+ sha256: "3900825701164577db65337792bc122b66f9eb1245ee47e7ae8244ae5ceb8030"
781
+ url: "https://pub.dev"
782
+ source: hosted
783
+ version: "0.0.1-beta.17"
784
+ just_audio_platform_interface:
785
+ dependency: transitive
786
+ description:
787
+ name: just_audio_platform_interface
788
+ sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a"
789
+ url: "https://pub.dev"
790
+ source: hosted
791
+ version: "4.6.0"
792
+ just_audio_web:
793
+ dependency: transitive
794
+ description:
795
+ name: just_audio_web
796
+ sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663"
797
+ url: "https://pub.dev"
798
+ source: hosted
799
+ version: "0.4.16"
800
large_file_handler:
801
dependency: transitive
802
description:
@@ -1069,6 +1141,14 @@ packages:
1141
url: "https://pub.dev"
1142
source: hosted
1143
version: "3.2.1"
1144
+ rxdart:
1145
+ dependency: transitive
1146
+ description:
1147
+ name: rxdart
1148
+ sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
1149
+ url: "https://pub.dev"
1150
+ source: hosted
1151
+ version: "0.28.0"
1152
serial_csv:
1153
dependency: transitive
1154
description:
@@ -1298,6 +1378,46 @@ packages:
1378
url: "https://pub.dev"
1379
source: hosted
1380
version: "1.10.2"
1381
+ sqflite:
1382
+ dependency: transitive
1383
+ description:
1384
+ name: sqflite
1385
+ sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
1386
+ url: "https://pub.dev"
1387
+ source: hosted
1388
+ version: "2.4.2+1"
1389
+ sqflite_android:
1390
+ dependency: transitive
1391
+ description:
1392
+ name: sqflite_android
1393
+ sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
1394
+ url: "https://pub.dev"
1395
+ source: hosted
1396
+ version: "2.4.2+3"
1397
+ sqflite_common:
1398
+ dependency: transitive
1399
+ description:
1400
+ name: sqflite_common
1401
+ sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
1402
+ url: "https://pub.dev"
1403
+ source: hosted
1404
+ version: "2.5.8"
1405
+ sqflite_darwin:
1406
+ dependency: transitive
1407
+ description:
1408
+ name: sqflite_darwin
1409
+ sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
1410
+ url: "https://pub.dev"
1411
+ source: hosted
1412
+ version: "2.4.2"
1413
+ sqflite_platform_interface:
1414
+ dependency: transitive
1415
+ description:
1416
+ name: sqflite_platform_interface
1417
+ sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
1418
+ url: "https://pub.dev"
1419
+ source: hosted
1420
+ version: "2.4.0"
1421
sqlite3:
1422
dependency: transitive
1423
description:
pubspec.yaml
+3
@@ -63,6 +63,9 @@ dependencies:
63
wakelock_plus: ^1.2.8
64
flutter_local_notifications: ^18.0.1
65
audioplayers: ^6.4.0
66
+ just_audio: ^0.9.43
67
+ just_audio_background: ^0.0.1-beta.17
68
+ audio_session: ^0.1.25
69
device_info_plus: ^12.4.0
70
sherpa_onnx: ^1.13.2
71
archive: ^4.0.2
session_tts-audio-player-enhancement_2026-06-10-224608.md
new
+55
@@ -0,0 +1,55 @@
1
+# Session Summary: TTS Audio Player Enhancement
2
+
3
+**Date:** 2026-06-10
4
+**Duration:** ~1-2 hours
5
+**Files changed:** 6 (296 insertions, 142 deletions)
6
+
7
+## Recap of Key Actions
8
+
9
+1. **Analysis** — Mapped the existing TTS architecture: three engines (System/Kitten/Piper), chunked streaming via `sherpa_onnx` isolate + `audioplayers`, and a mini player bar with stop-only support.
10
+
11
+2. **Plan** — Proposed and refined a plan to add pause/resume, a progress bar with timestamps, persistent player across routes, and background audio support.
12
+
13
+3. **Implementation:**
14
+ - Added `isPaused` state, `pause()`/`resume()`/`togglePauseResume()` methods to `TtsNotifier`.
15
+ - Added progress tracking — WAV header parsing to compute per-chunk durations, `_totalDuration` and `_chunkOffset` for elapsed time calculation.
16
+ - Configured `AudioContextConfig(stayAwake: true, focus: gain)` for lock-screen background playback.
17
+ - Rewrote `TtsPlayerBar` from a `ConsumerWidget` to a `ConsumerStatefulWidget` subscribing to player streams, showing play/pause, elapsed/total time, linear progress bar, and stop.
18
+ - Moved `TtsPlayerBar` from `ChatScreen`/`SidebarWidget` to `AppShell`, making it persistent across all ShellRoutes.
19
+ - Updated `MessageActionBar` "Read Aloud" button to show pause/play/resume states.
20
+ - Removed old imports and widget references from `ChatScreen` and `SidebarWidget`.
21
+
22
+4. **Code Review** — Sub-agent identified 5 issues which were all fixed:
23
+ - `_chunkDurations` changed from `List<Duration>` to `Map<int, Duration>` to avoid null crashes on out-of-order chunk arrivals.
24
+ - Stale system TTS completion handler race fixed by clearing the old handler before setting a new one.
25
+ - Temp WAV file leak addressed with `_deleteChunkFile()` / `_cleanupSessionFiles()` called on stop and playback finish.
26
+ - Stale position on chunk boundaries fixed with an `_onChunkChanged` broadcast stream.
27
+ - `ref.listen` moved from `build()` to `initState()` using `ref.listenManual`.
28
+
29
+## Total Cost
30
+
31
+Not available — no billing or token usage data is exposed in this environment.
32
+
33
+## Efficiency Insights
34
+
35
+- **Sub-agent for code review was effective** — catching 5 issues that would have been subtle runtime bugs (null crash, race condition, file leak).
36
+- **Riverpod pattern issues** — one finding (`ref.listen` in build) was a common anti-pattern that's easy to miss when rapidly iterating.
37
+- **Chunked architecture complexity** — the streaming/chunked approach adds significant complexity for progress tracking and position calculation. A single-file synthesis approach would be simpler but trades off latency-to-first-audio.
38
+
39
+## Possible Process Improvements
40
+
41
+- **Run code review sub-agent proactively** after any batch of changes, not just when explicitly asked.
42
+- **Track durations via the worker** — instead of parsing WAV headers in the main isolate, have the worker return `(wavBytes, sampleCount, sampleRate)` directly for cleaner progress computation.
43
+- **Audio session configuration** should be tested on physical iOS/Android devices to verify lock-screen and background behavior works as expected.
44
+- **Lock-screen media controls** (`audio_service` package) could be a follow-up for full media notification integration.
45
+
46
+## Conversation Turns
47
+
48
+~7 user turns.
49
+
50
+## Other Observations
51
+
52
+- iOS `UIBackgroundModes: audio` and Android `WAKE_LOCK` were already configured in the project, which simplified background audio setup.
53
+- The existing `AudioPlayerWidget` (for chat attachments) served as a good reference for the progress bar + timestamps pattern.
54
+- The `audioplayers ^6.6.0` package already provides `AudioContextConfig` with `stayAwake` and `focus` — no extra platform packages needed.
55
+- All 6 modified files pass `dart analyze` with zero issues.