Merge pull request 'STT context, refresh improvements, calendar timezone fix' (#16) from dev into main
This commit was merged in pull request #16.
This commit is contained in:
+1
-1
@@ -193,7 +193,7 @@ class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Minimum gap between app-resume refreshes to avoid hammering the server.
|
// Minimum gap between app-resume refreshes to avoid hammering the server.
|
||||||
static const _resumeCooldown = Duration(minutes: 5);
|
static const _resumeCooldown = Duration(seconds: 30);
|
||||||
DateTime? _lastResumeRefresh;
|
DateTime? _lastResumeRefresh;
|
||||||
int? _prevTabIndex;
|
int? _prevTabIndex;
|
||||||
|
|
||||||
|
|||||||
@@ -40,16 +40,20 @@ class VoiceApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// POST WebM/Opus audio bytes and return the transcript string.
|
/// POST WebM/Opus audio bytes and return the transcript string.
|
||||||
|
/// [context] is optional recent conversation text passed as initial_prompt
|
||||||
|
/// to Whisper, reducing mishearings of domain-specific words.
|
||||||
/// Returns empty string on empty or error response.
|
/// Returns empty string on empty or error response.
|
||||||
Future<String> transcribe(Uint8List audioBytes) async {
|
Future<String> transcribe(Uint8List audioBytes, {String? context}) async {
|
||||||
try {
|
try {
|
||||||
final formData = FormData.fromMap({
|
final fields = <String, dynamic>{
|
||||||
'audio': MultipartFile.fromBytes(
|
'audio': MultipartFile.fromBytes(
|
||||||
audioBytes,
|
audioBytes,
|
||||||
filename: 'audio.m4a',
|
filename: 'audio.m4a',
|
||||||
contentType: DioMediaType('audio', 'mp4'),
|
contentType: DioMediaType('audio', 'mp4'),
|
||||||
),
|
),
|
||||||
});
|
if (context != null && context.isNotEmpty) 'context': context,
|
||||||
|
};
|
||||||
|
final formData = FormData.fromMap(fields);
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
'/api/voice/transcribe',
|
'/api/voice/transcribe',
|
||||||
data: formData,
|
data: formData,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class VoiceRepository {
|
|||||||
const VoiceRepository(this._api);
|
const VoiceRepository(this._api);
|
||||||
|
|
||||||
Future<VoiceStatus> checkStatus() => _api.checkStatus();
|
Future<VoiceStatus> checkStatus() => _api.checkStatus();
|
||||||
Future<String> transcribe(Uint8List audioBytes) => _api.transcribe(audioBytes);
|
Future<String> transcribe(Uint8List audioBytes, {String? context}) =>
|
||||||
|
_api.transcribe(audioBytes, context: context);
|
||||||
Future<Uint8List> synthesise(String text) => _api.synthesise(text);
|
Future<Uint8List> synthesise(String text) => _api.synthesise(text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ class VoiceNotifier extends Notifier<VoiceState> {
|
|||||||
int _lastSeenLength = 0;
|
int _lastSeenLength = 0;
|
||||||
bool _streamComplete = false;
|
bool _streamComplete = false;
|
||||||
|
|
||||||
|
// Last complete assistant response — passed to Whisper as initial_prompt
|
||||||
|
// to reduce STT mishearings of domain-specific words.
|
||||||
|
String _lastAssistantContent = '';
|
||||||
|
|
||||||
// TTS playback queue
|
// TTS playback queue
|
||||||
final _ttsQueue = Queue<Uint8List>();
|
final _ttsQueue = Queue<Uint8List>();
|
||||||
bool _ttsPlaying = false;
|
bool _ttsPlaying = false;
|
||||||
@@ -205,6 +209,7 @@ class VoiceNotifier extends Notifier<VoiceState> {
|
|||||||
_dispatchSentences(flush: isComplete);
|
_dispatchSentences(flush: isComplete);
|
||||||
|
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
|
_lastAssistantContent = fullContent;
|
||||||
_streamComplete = true;
|
_streamComplete = true;
|
||||||
_checkRestartListening();
|
_checkRestartListening();
|
||||||
}
|
}
|
||||||
@@ -278,8 +283,10 @@ class VoiceNotifier extends Notifier<VoiceState> {
|
|||||||
|
|
||||||
if (!state.voiceModeActive) return;
|
if (!state.voiceModeActive) return;
|
||||||
|
|
||||||
final transcript =
|
final transcript = await ref.read(voiceRepositoryProvider).transcribe(
|
||||||
await ref.read(voiceRepositoryProvider).transcribe(bytes);
|
bytes,
|
||||||
|
context: _lastAssistantContent.isNotEmpty ? _lastAssistantContent : null,
|
||||||
|
);
|
||||||
|
|
||||||
if (!state.voiceModeActive) return;
|
if (!state.voiceModeActive) return;
|
||||||
|
|
||||||
|
|||||||
@@ -67,10 +67,13 @@ class CalendarScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _AgendaList(
|
child: RefreshIndicator(
|
||||||
events:
|
onRefresh: () async => ref.invalidate(calendarProvider),
|
||||||
cal.eventsByDay[dateOnly(cal.selectedDay)] ?? [],
|
child: _AgendaList(
|
||||||
notifier: notifier,
|
events:
|
||||||
|
cal.eventsByDay[dateOnly(cal.selectedDay)] ?? [],
|
||||||
|
notifier: notifier,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -106,7 +109,12 @@ class _AgendaList extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (events.isEmpty) {
|
if (events.isEmpty) {
|
||||||
return const Center(child: Text('No events'));
|
return ListView(
|
||||||
|
children: const [
|
||||||
|
SizedBox(height: 80),
|
||||||
|
Center(child: Text('No events')),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
|||||||
@@ -16,12 +16,27 @@ class ChatScreen extends ConsumerStatefulWidget {
|
|||||||
ConsumerState<ChatScreen> createState() => _ChatScreenState();
|
ConsumerState<ChatScreen> createState() => _ChatScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatScreenState extends ConsumerState<ChatScreen> {
|
class _ChatScreenState extends ConsumerState<ChatScreen>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
final _controller = TextEditingController();
|
final _controller = TextEditingController();
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
ref.invalidate(messagesProvider(widget.conversationId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
// Exit voice mode if the user navigates away mid-session.
|
// Exit voice mode if the user navigates away mid-session.
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ class _NewsScreenState extends ConsumerState<NewsScreen> {
|
|||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () async => ref.invalidate(newsProvider),
|
||||||
|
child: ListView.builder(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
itemCount: news.items.length + 1,
|
itemCount: news.items.length + 1,
|
||||||
@@ -128,6 +130,7 @@ class _NewsScreenState extends ConsumerState<NewsScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user