@tduncan / Cortana / commits / a114eee

feat: implement background audio support for TTS using audio_service and just_audio_background

abdulmominsakib committed Jun 11, 2026 at 23:57 UTC a114eeea2e48641d8c696511951635cefc7d3bf7
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.