abstractvoice 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,861 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLI example using AbstractVoice with a text-generation API.
4
+
5
+ This example shows how to use AbstractVoice to create a CLI application
6
+ that interacts with an LLM API for text generation.
7
+ """
8
+
9
+ import argparse
10
+ import cmd
11
+ import json
12
+ import re
13
+ import sys
14
+ import requests
15
+ from abstractvoice import VoiceManager
16
+
17
+
18
+ # ANSI color codes
19
+ class Colors:
20
+ BLUE = "\033[94m"
21
+ CYAN = "\033[96m"
22
+ GREEN = "\033[92m"
23
+ YELLOW = "\033[93m"
24
+ RED = "\033[91m"
25
+ BOLD = "\033[1m"
26
+ UNDERLINE = "\033[4m"
27
+ END = "\033[0m"
28
+
29
+
30
+ class VoiceREPL(cmd.Cmd):
31
+ """Voice-enabled REPL for LLM interaction."""
32
+
33
+ intro = "" # Will be set in __init__ to include help
34
+ prompt = f"{Colors.GREEN}> {Colors.END}"
35
+
36
+ # Override cmd module settings
37
+ ruler = "" # No horizontal rule line
38
+ use_rawinput = True
39
+
40
+ def __init__(self, api_url="http://localhost:11434/api/chat",
41
+ model="granite3.3:2b", debug_mode=False):
42
+ super().__init__()
43
+
44
+ # Debug mode
45
+ self.debug_mode = debug_mode
46
+
47
+ # API settings
48
+ self.api_url = api_url
49
+ self.model = model
50
+ self.temperature = 0.4
51
+ self.max_tokens = 4096
52
+
53
+ # Initialize voice manager
54
+ self.voice_manager = VoiceManager(debug_mode=debug_mode)
55
+
56
+ # Settings
57
+ self.use_tts = True
58
+ self.voice_mode = "off" # off, full, wait, stop, ptt
59
+ self.voice_mode_active = False # Is voice recognition running?
60
+
61
+ # System prompt
62
+ self.system_prompt = """
63
+ You are a Helpful Voice Assistant. By design, your answers are short and more conversational, unless specifically asked to detail something.
64
+ You only speak, so never use any text formatting or markdown. Write for a speaker.
65
+ """
66
+
67
+ # Message history
68
+ self.messages = [{"role": "system", "content": self.system_prompt}]
69
+
70
+ # Token counting
71
+ self.system_tokens = 0
72
+ self.user_tokens = 0
73
+ self.assistant_tokens = 0
74
+ self._count_system_tokens()
75
+
76
+ if self.debug_mode:
77
+ print(f"Initialized with API URL: {api_url}")
78
+ print(f"Using model: {model}")
79
+
80
+ # Set intro with help information
81
+ self.intro = self._get_intro()
82
+
83
+ def _get_intro(self):
84
+ """Generate intro message with help."""
85
+ intro = f"\n{Colors.BOLD}Welcome to AbstractVoice CLI REPL{Colors.END}\n"
86
+ intro += f"API: {self.api_url} | Model: {self.model}\n"
87
+ intro += f"\n{Colors.CYAN}Quick Start:{Colors.END}\n"
88
+ intro += " • Type messages to chat with the LLM\n"
89
+ intro += " • Use /voice <mode> to enable voice input\n"
90
+ intro += " • Type /help for full command list\n"
91
+ intro += " • Type /exit or /q to quit\n"
92
+ return intro
93
+
94
+ def _count_system_tokens(self):
95
+ """Count tokens in the system prompt."""
96
+ self._count_tokens(self.system_prompt, "system")
97
+
98
+ def parseline(self, line):
99
+ """Parse the line to extract command and arguments.
100
+
101
+ Override to handle / prefix for commands. This ensures /voice, /help, etc.
102
+ are recognized as commands by stripping the leading / before parsing.
103
+ """
104
+ line = line.strip()
105
+
106
+ # If line starts with /, remove it for command processing
107
+ if line.startswith('/'):
108
+ line = line[1:].strip()
109
+
110
+ # Call parent parseline to do the actual parsing
111
+ return super().parseline(line)
112
+
113
+ def default(self, line):
114
+ """Handle regular text input.
115
+
116
+ Only 'stop' is recognized as a command without /
117
+ All other commands MUST use / prefix.
118
+ """
119
+ # Skip empty lines
120
+ if not line.strip():
121
+ return
122
+
123
+ # ONLY 'stop' is recognized without / (for voice mode convenience)
124
+ if line.strip().lower() == "stop":
125
+ return self.do_stop("")
126
+
127
+ # Check if in voice mode - don't send to LLM
128
+ if self.voice_mode_active:
129
+ if self.debug_mode:
130
+ print(f"Voice mode active ({self.voice_mode}). Use /voice off or say 'stop' to exit.")
131
+ return
132
+
133
+ # Everything else goes to LLM
134
+ self.process_query(line.strip())
135
+
136
+ def process_query(self, query):
137
+ """Process a query and get a response from the LLM."""
138
+ if not query:
139
+ return
140
+
141
+ # Count user message tokens
142
+ self._count_tokens(query, "user")
143
+
144
+ # Create the message
145
+ user_message = {"role": "user", "content": query}
146
+ self.messages.append(user_message)
147
+
148
+ if self.debug_mode:
149
+ print(f"Sending request to API: {self.api_url}")
150
+
151
+ try:
152
+ # Structure the payload with system prompt outside the messages array
153
+ payload = {
154
+ "model": self.model,
155
+ "messages": self.messages,
156
+ "stream": False, # Disable streaming for simplicity
157
+ "temperature": self.temperature,
158
+ "max_tokens": self.max_tokens
159
+ }
160
+
161
+ # Make API request
162
+ response = requests.post(self.api_url, json=payload)
163
+ response.raise_for_status()
164
+
165
+ # Try to parse response
166
+ try:
167
+ # First, try to parse as JSON
168
+ response_data = response.json()
169
+
170
+ # Check for different API formats
171
+ if "message" in response_data and "content" in response_data["message"]:
172
+ # Ollama format
173
+ response_text = response_data["message"]["content"].strip()
174
+ elif "choices" in response_data and len(response_data["choices"]) > 0:
175
+ # OpenAI format
176
+ response_text = response_data["choices"][0]["message"]["content"].strip()
177
+ else:
178
+ # Some other format
179
+ response_text = str(response_data).strip()
180
+
181
+ except Exception as e:
182
+ if self.debug_mode:
183
+ print(f"Error parsing JSON response: {e}")
184
+
185
+ # Handle streaming or non-JSON response
186
+ response_text = response.text.strip()
187
+
188
+ # Try to extract content from streaming format if possible
189
+ if response_text.startswith("{") and "content" in response_text:
190
+ try:
191
+ # Extract the last message if multiple streaming chunks
192
+ lines = response_text.strip().split("\n")
193
+ last_complete_line = lines[-1]
194
+ for i in range(len(lines) - 1, -1, -1):
195
+ if '"done":true' in lines[i]:
196
+ last_complete_line = lines[i]
197
+ break
198
+
199
+ # Parse the message content
200
+ import json
201
+ data = json.loads(last_complete_line)
202
+ if "message" in data and "content" in data["message"]:
203
+ full_content = ""
204
+ for line in lines:
205
+ try:
206
+ chunk = json.loads(line)
207
+ if "message" in chunk and "content" in chunk["message"]:
208
+ full_content += chunk["message"]["content"]
209
+ except:
210
+ pass
211
+ response_text = full_content.strip()
212
+ except Exception as e:
213
+ if self.debug_mode:
214
+ print(f"Error extracting content from streaming response: {e}")
215
+
216
+ # Count assistant message tokens
217
+ self._count_tokens(response_text, "assistant")
218
+
219
+ # Add to message history
220
+ self.messages.append({"role": "assistant", "content": response_text})
221
+
222
+ # Display the response with color
223
+ print(f"{Colors.CYAN}{response_text}{Colors.END}")
224
+
225
+ # Speak the response if voice manager is available
226
+ if self.voice_manager:
227
+ self.voice_manager.speak(response_text)
228
+
229
+ except Exception as e:
230
+ print(f"Error: {e}")
231
+ if self.debug_mode:
232
+ import traceback
233
+ traceback.print_exc()
234
+
235
+ def _count_tokens(self, text, role):
236
+ """Count tokens in text."""
237
+ try:
238
+ import tiktoken
239
+
240
+ # Initialize the tokenizer
241
+ encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
242
+
243
+ # Count tokens
244
+ token_count = len(encoding.encode(text))
245
+
246
+ # Update the token counts based on role
247
+ if role == "system":
248
+ self.system_tokens = token_count
249
+ elif role == "user":
250
+ self.user_tokens += token_count
251
+ elif role == "assistant":
252
+ self.assistant_tokens += token_count
253
+
254
+ # Calculate total tokens
255
+ total_tokens = self.system_tokens + self.user_tokens + self.assistant_tokens
256
+
257
+ if self.debug_mode:
258
+ print(f"{role.capitalize()} tokens: {token_count}")
259
+ print(f"Total tokens: {total_tokens}")
260
+
261
+ except ImportError:
262
+ # If tiktoken is not available, just don't count tokens
263
+ pass
264
+ except Exception as e:
265
+ if self.debug_mode:
266
+ print(f"Error counting tokens: {e}")
267
+ pass
268
+
269
+ def _clean_response(self, text):
270
+ """Clean LLM response text."""
271
+ patterns = [
272
+ r"user:.*", r"<\|user\|>.*",
273
+ r"assistant:.*", r"<\|assistant\|>.*",
274
+ r"<\|end\|>.*"
275
+ ]
276
+
277
+ for pattern in patterns:
278
+ text = re.sub(pattern, "", text, flags=re.DOTALL)
279
+
280
+ return text.strip()
281
+
282
+ def do_voice(self, arg):
283
+ """Control voice input mode.
284
+
285
+ Modes:
286
+ off - Disable voice input
287
+ full - Continuous listening, interrupts TTS on speech detection
288
+ wait - Pause listening while TTS is speaking (recommended)
289
+ stop - Only stops TTS on 'stop' keyword (planned)
290
+ ptt - Push-to-talk mode (planned)
291
+ """
292
+ arg = arg.lower().strip()
293
+
294
+ # Handle legacy "on" argument
295
+ if arg == "on":
296
+ arg = "wait"
297
+
298
+ if arg in ["off", "full", "wait", "stop", "ptt"]:
299
+ # If switching from one mode to another, stop current mode first
300
+ if self.voice_mode_active and arg != "off":
301
+ self._voice_stop_callback()
302
+
303
+ self.voice_mode = arg
304
+ self.voice_manager.set_voice_mode(arg)
305
+
306
+ if arg == "off":
307
+ if self.voice_mode_active:
308
+ self._voice_stop_callback()
309
+ else:
310
+ # Start voice recognition for non-off modes
311
+ self.voice_mode_active = True
312
+
313
+ # Start listening with callbacks
314
+ self.voice_manager.listen(
315
+ on_transcription=self._voice_callback,
316
+ on_stop=lambda: self._voice_stop_callback()
317
+ )
318
+
319
+ # Print mode-specific instructions
320
+ if arg == "full":
321
+ print("Voice mode: FULL - Continuous listening, interrupts TTS on speech.")
322
+ print("Say 'stop' to exit.")
323
+ elif arg == "wait":
324
+ print("Voice mode: WAIT - Pauses listening while speaking (recommended).")
325
+ print("Say 'stop' to exit.")
326
+ elif arg == "stop":
327
+ print("Voice mode: STOP (Planned) - Only stops TTS on 'stop' keyword.")
328
+ print("Currently same as WAIT mode.")
329
+ elif arg == "ptt":
330
+ print("Voice mode: PTT (Planned) - Push-to-talk functionality.")
331
+ print("Currently same as WAIT mode.")
332
+ else:
333
+ print("Usage: /voice off | full | wait | stop | ptt")
334
+ print(" off - Disable voice input")
335
+ print(" full - Continuous listening, interrupts TTS on speech")
336
+ print(" wait - Pause listening while speaking (recommended)")
337
+ print(" stop - Only stop TTS on 'stop' keyword (planned)")
338
+ print(" ptt - Push-to-talk mode (planned)")
339
+
340
+ def _voice_callback(self, text):
341
+ """Callback for voice recognition."""
342
+ # Print what the user said
343
+ print(f"\n> {text}")
344
+
345
+ # Check if the user said 'stop' to exit voice mode
346
+ if text.lower() == "stop":
347
+ self._voice_stop_callback()
348
+ # Don't process "stop" as a query
349
+ return
350
+
351
+ # Mode-specific handling
352
+ if self.voice_mode == "stop":
353
+ # In 'stop' mode, don't interrupt TTS - just queue the message
354
+ # But since we're in callback, TTS interrupt is already paused
355
+ pass
356
+ elif self.voice_mode == "ptt":
357
+ # In PTT mode, process immediately
358
+ pass
359
+ # 'full' mode has default behavior
360
+
361
+ # Process the user's query
362
+ self.process_query(text)
363
+
364
+ def _voice_stop_callback(self):
365
+ """Callback when voice mode is stopped."""
366
+ self.voice_mode = "off"
367
+ self.voice_mode_active = False
368
+ self.voice_manager.stop_listening()
369
+ print("Voice mode disabled.")
370
+
371
+ def do_tts(self, arg):
372
+ """Toggle text-to-speech."""
373
+ arg = arg.lower().strip()
374
+
375
+ if arg == "on":
376
+ self.use_tts = True
377
+ print("TTS enabled" if self.debug_mode else "")
378
+ elif arg == "off":
379
+ self.use_tts = False
380
+ print("TTS disabled" if self.debug_mode else "")
381
+ else:
382
+ print("Usage: /tts on | off")
383
+
384
+ def do_speed(self, arg):
385
+ """Set the TTS speed multiplier."""
386
+ if not arg.strip():
387
+ print(f"Current TTS speed: {self.voice_manager.get_speed()}x")
388
+ return
389
+
390
+ try:
391
+ speed = float(arg.strip())
392
+ if 0.5 <= speed <= 2.0:
393
+ self.voice_manager.set_speed(speed)
394
+ print(f"TTS speed set to {speed}x")
395
+ else:
396
+ print("Speed should be between 0.5 and 2.0")
397
+ except ValueError:
398
+ print("Usage: /speed <number> (e.g., /speed 1.5)")
399
+
400
+ def do_tts_model(self, arg):
401
+ """Change TTS model.
402
+
403
+ Available models (quality ranking):
404
+ vits - BEST quality (requires espeak-ng)
405
+ fast_pitch - Good quality (works everywhere)
406
+ glow-tts - Alternative fallback
407
+ tacotron2-DDC - Legacy
408
+
409
+ Usage:
410
+ /tts_model vits
411
+ /tts_model fast_pitch
412
+ """
413
+ model_shortcuts = {
414
+ 'vits': 'tts_models/en/ljspeech/vits',
415
+ 'fast_pitch': 'tts_models/en/ljspeech/fast_pitch',
416
+ 'glow-tts': 'tts_models/en/ljspeech/glow-tts',
417
+ 'tacotron2-DDC': 'tts_models/en/ljspeech/tacotron2-DDC',
418
+ }
419
+
420
+ arg = arg.strip()
421
+ if not arg:
422
+ print("Usage: /tts_model <model_name>")
423
+ print("Available models: vits (best), fast_pitch, glow-tts, tacotron2-DDC")
424
+ return
425
+
426
+ # Get full model name
427
+ model_name = model_shortcuts.get(arg, arg)
428
+
429
+ print(f"Changing TTS model to: {model_name}")
430
+ try:
431
+ self.voice_manager.set_tts_model(model_name)
432
+ print("✓ TTS model changed successfully")
433
+ except Exception as e:
434
+ print(f"✗ Error changing model: {e}")
435
+
436
+ def do_whisper(self, arg):
437
+ """Change Whisper model."""
438
+ model = arg.strip()
439
+ if not model:
440
+ print(f"Current Whisper model: {self.voice_manager.get_whisper()}")
441
+ return
442
+
443
+ self.voice_manager.set_whisper(model)
444
+
445
+ def do_clear(self, arg):
446
+ """Clear chat history."""
447
+ self.messages = [{"role": "system", "content": self.system_prompt}]
448
+ # Reset token counters
449
+ self.system_tokens = 0
450
+ self.user_tokens = 0
451
+ self.assistant_tokens = 0
452
+ # Recalculate system tokens
453
+ self._count_system_tokens()
454
+ print("History cleared")
455
+
456
+ def do_system(self, arg):
457
+ """Set the system prompt."""
458
+ if arg.strip():
459
+ self.system_prompt = arg.strip()
460
+ self.messages = [{"role": "system", "content": self.system_prompt}]
461
+ print(f"System prompt set to: {self.system_prompt}")
462
+ else:
463
+ print(f"Current system prompt: {self.system_prompt}")
464
+
465
+ def do_exit(self, arg):
466
+ """Exit the REPL."""
467
+ self.voice_manager.cleanup()
468
+ if self.debug_mode:
469
+ print("Goodbye!")
470
+ return True
471
+
472
+ def do_q(self, arg):
473
+ """Alias for exit."""
474
+ return self.do_exit(arg)
475
+
476
+ def do_quit(self, arg):
477
+ """Alias for exit."""
478
+ return self.do_exit(arg)
479
+
480
+ def do_stop(self, arg):
481
+ """Stop voice recognition or TTS playback."""
482
+ # If in voice mode, exit voice mode
483
+ if self.voice_mode_active:
484
+ self._voice_stop_callback()
485
+ return
486
+
487
+ # Even if not in voice mode, stop any ongoing TTS
488
+ if self.voice_manager:
489
+ self.voice_manager.stop_speaking()
490
+ # Do not show the "Stopped speech playback" message
491
+ return
492
+
493
+ def do_pause(self, arg):
494
+ """Pause current TTS playback.
495
+
496
+ Usage: /pause
497
+ """
498
+ if self.voice_manager:
499
+ if self.voice_manager.pause_speaking():
500
+ print("TTS playback paused. Use /resume to continue.")
501
+ else:
502
+ print("No active TTS playback to pause.")
503
+ else:
504
+ print("Voice manager not initialized.")
505
+
506
+ def _reset_terminal(self):
507
+ """Reset terminal state to prevent I/O blocking."""
508
+ import sys
509
+ import os
510
+
511
+ try:
512
+ # Flush all output streams
513
+ sys.stdout.flush()
514
+ sys.stderr.flush()
515
+
516
+ # Force terminal to reset input state
517
+ if hasattr(sys.stdin, 'flush'):
518
+ sys.stdin.flush()
519
+
520
+ # On Unix-like systems, reset terminal
521
+ if os.name == 'posix':
522
+ os.system('stty sane 2>/dev/null')
523
+
524
+ except Exception:
525
+ # Ignore errors in terminal reset
526
+ pass
527
+
528
+ def do_resume(self, arg):
529
+ """Resume paused TTS playback.
530
+
531
+ Usage: /resume
532
+ """
533
+ if self.voice_manager:
534
+ if self.voice_manager.is_paused():
535
+ result = self.voice_manager.resume_speaking()
536
+ if result:
537
+ print("TTS playback resumed.")
538
+ else:
539
+ print("TTS was paused but playback already completed.")
540
+ # Reset terminal after resume operation
541
+ self._reset_terminal()
542
+ else:
543
+ print("No paused TTS playback to resume.")
544
+ else:
545
+ print("Voice manager not initialized.")
546
+
547
+ # If neither voice mode nor TTS is active - don't show any message
548
+ pass
549
+
550
+ def do_help(self, arg):
551
+ """Show help information."""
552
+ print("Commands:")
553
+ print(" /exit, /q, /quit Exit REPL")
554
+ print(" /clear Clear history")
555
+ print(" /tts on|off Toggle TTS")
556
+ print(" /voice <mode> Voice input: off|full|wait|stop|ptt")
557
+ print(" /speed <number> Set TTS speed (0.5-2.0, default: 1.0, pitch preserved)")
558
+ print(" /tts_model <model> Switch TTS model: vits(best)|fast_pitch|glow-tts|tacotron2-DDC")
559
+ print(" /whisper <model> Switch Whisper model: tiny|base|small|medium|large")
560
+ print(" /system <prompt> Set system prompt")
561
+ print(" /stop Stop voice mode or TTS playback")
562
+ print(" /pause Pause current TTS playback")
563
+ print(" /resume Resume paused TTS playback")
564
+ print(" /tokens Display token usage stats")
565
+ print(" /help Show this help")
566
+ print(" /save <filename> Save chat history to file")
567
+ print(" /load <filename> Load chat history from file")
568
+ print(" /model <name> Change the LLM model")
569
+ print(" /temperature <val> Set temperature (0.0-2.0, default: 0.7)")
570
+ print(" /max_tokens <num> Set max tokens (default: 4096)")
571
+ print(" stop Stop voice mode or TTS (voice command)")
572
+ print(" <message> Send to LLM (text mode)")
573
+ print()
574
+ print("Note: ALL commands must start with / except 'stop'")
575
+ print("In voice mode, say 'stop' to exit voice mode.")
576
+
577
+ def emptyline(self):
578
+ """Handle empty line input."""
579
+ # Do nothing when an empty line is entered
580
+ pass
581
+
582
+ def do_tokens(self, arg):
583
+ """Display token usage information."""
584
+ try:
585
+ # Always recalculate tokens to ensure accuracy
586
+ self._reset_and_recalculate_tokens()
587
+
588
+ total_tokens = self.system_tokens + self.user_tokens + self.assistant_tokens
589
+
590
+ print(f"{Colors.YELLOW}Token usage:{Colors.END}")
591
+ print(f" System prompt: {self.system_tokens} tokens")
592
+ print(f" User messages: {self.user_tokens} tokens")
593
+ print(f" AI responses: {self.assistant_tokens} tokens")
594
+ print(f" {Colors.BOLD}Total: {total_tokens} tokens{Colors.END}")
595
+ except Exception as e:
596
+ if self.debug_mode:
597
+ print(f"Error displaying token count: {e}")
598
+ print("Token counting is not available.")
599
+ pass
600
+
601
+ def do_save(self, filename):
602
+ """Save chat history to file."""
603
+ try:
604
+ # Add .mem extension if not specified
605
+ if not filename.endswith('.mem'):
606
+ filename = f"{filename}.mem"
607
+
608
+ # Prepare memory file structure
609
+ memory_data = {
610
+ "header": {
611
+ "timestamp_utc": self._get_current_timestamp(),
612
+ "model": self.model,
613
+ "version": __import__('abstractvoice').__version__ # Get version from package __init__.py
614
+ },
615
+ "system_prompt": self.system_prompt,
616
+ "token_stats": {
617
+ "system": self.system_tokens,
618
+ "user": self.user_tokens,
619
+ "assistant": self.assistant_tokens,
620
+ "total": self.system_tokens + self.user_tokens + self.assistant_tokens
621
+ },
622
+ "settings": {
623
+ "tts_speed": self.voice_manager.get_speed(),
624
+ "whisper_model": self.voice_manager.get_whisper(),
625
+ "temperature": self.temperature,
626
+ "max_tokens": self.max_tokens
627
+ },
628
+ "messages": self.messages
629
+ }
630
+
631
+ # Save to file with pretty formatting
632
+ with open(filename, 'w') as f:
633
+ json.dump(memory_data, f, indent=2)
634
+
635
+ print(f"Chat history saved to {filename}")
636
+ except Exception as e:
637
+ if self.debug_mode:
638
+ print(f"Error saving chat history: {e}")
639
+ print(f"Failed to save chat history to {filename}")
640
+
641
+ def _get_current_timestamp(self):
642
+ """Get current timestamp in the format YYYY-MM-DD HH-MM-SS."""
643
+ from datetime import datetime
644
+ return datetime.utcnow().strftime("%Y-%m-%d %H-%M-%S")
645
+
646
+ def do_load(self, filename):
647
+ """Load chat history from file."""
648
+ try:
649
+ # Add .mem extension if not specified
650
+ if not filename.endswith('.mem'):
651
+ filename = f"{filename}.mem"
652
+
653
+ if self.debug_mode:
654
+ print(f"Attempting to load from: {filename}")
655
+
656
+ with open(filename, 'r') as f:
657
+ memory_data = json.load(f)
658
+
659
+ if self.debug_mode:
660
+ print(f"Successfully loaded JSON data from {filename}")
661
+
662
+ # Handle both formats: new .mem format and legacy format (just messages array)
663
+ if isinstance(memory_data, dict) and "messages" in memory_data:
664
+ # New .mem format
665
+ if self.debug_mode:
666
+ print("Processing .mem format with messages")
667
+
668
+ # Update model if specified
669
+ if "header" in memory_data and "model" in memory_data["header"]:
670
+ old_model = self.model
671
+ self.model = memory_data["header"]["model"]
672
+ print(f"Model changed from {old_model} to {self.model}")
673
+
674
+ # Update system prompt
675
+ if "system_prompt" in memory_data:
676
+ self.system_prompt = memory_data["system_prompt"]
677
+ if self.debug_mode:
678
+ print(f"Updated system prompt: {self.system_prompt}")
679
+
680
+ # Load messages
681
+ if "messages" in memory_data and isinstance(memory_data["messages"], list):
682
+ self.messages = memory_data["messages"]
683
+ if self.debug_mode:
684
+ print(f"Loaded {len(self.messages)} messages")
685
+ else:
686
+ print("Invalid messages format in memory file")
687
+ return
688
+
689
+ # Recompute token stats if available
690
+ self._reset_and_recalculate_tokens()
691
+
692
+ # Restore settings if available
693
+ if "settings" in memory_data:
694
+ try:
695
+ settings = memory_data["settings"]
696
+
697
+ # Restore TTS speed
698
+ if "tts_speed" in settings:
699
+ speed = settings.get("tts_speed", 1.0)
700
+ self.voice_manager.set_speed(speed)
701
+ # Don't need to update the voice manager immediately as the
702
+ # speed will be used in the next speak() call
703
+ print(f"TTS speed set to {speed}x")
704
+
705
+ # Restore Whisper model
706
+ if "whisper_model" in settings:
707
+ whisper_model = settings.get("whisper_model", "tiny")
708
+ self.voice_manager.set_whisper(whisper_model)
709
+
710
+ # Restore temperature
711
+ if "temperature" in settings:
712
+ temp = settings.get("temperature", 0.4)
713
+ self.temperature = temp
714
+ print(f"Temperature set to {temp}")
715
+
716
+ # Restore max_tokens
717
+ if "max_tokens" in settings:
718
+ tokens = settings.get("max_tokens", 4096)
719
+ self.max_tokens = tokens
720
+ print(f"Max tokens set to {tokens}")
721
+
722
+ except Exception as e:
723
+ if self.debug_mode:
724
+ print(f"Error restoring settings: {e}")
725
+ # Continue loading even if settings restoration fails
726
+
727
+ elif isinstance(memory_data, list):
728
+ # Legacy format (just an array of messages)
729
+ self.messages = memory_data
730
+
731
+ # Reset token counts and recalculate
732
+ self._reset_and_recalculate_tokens()
733
+
734
+ # Extract system prompt if present
735
+ for msg in self.messages:
736
+ if isinstance(msg, dict) and msg.get("role") == "system":
737
+ self.system_prompt = msg.get("content", self.system_prompt)
738
+ break
739
+ else:
740
+ print("Invalid memory file format")
741
+ return
742
+
743
+ # Ensure there's a system message
744
+ self._ensure_system_message()
745
+
746
+ print(f"Chat history loaded from {filename}")
747
+
748
+ except FileNotFoundError:
749
+ print(f"File not found: {filename}")
750
+ except json.JSONDecodeError as e:
751
+ if self.debug_mode:
752
+ print(f"Invalid JSON format in {filename}: {e}")
753
+ print(f"Invalid JSON format in {filename}")
754
+ except Exception as e:
755
+ if self.debug_mode:
756
+ print(f"Error loading chat history: {str(e)}")
757
+ import traceback
758
+ traceback.print_exc()
759
+ print(f"Failed to load chat history from {filename}")
760
+
761
+ def _reset_and_recalculate_tokens(self):
762
+ """Reset token counts and recalculate for all messages."""
763
+ self.system_tokens = 0
764
+ self.user_tokens = 0
765
+ self.assistant_tokens = 0
766
+
767
+ # Count tokens for all messages
768
+ for msg in self.messages:
769
+ if isinstance(msg, dict) and "content" in msg and "role" in msg:
770
+ self._count_tokens(msg["content"], msg["role"])
771
+
772
+ def _ensure_system_message(self):
773
+ """Ensure there's a system message at the start of messages."""
774
+ has_system = False
775
+ for msg in self.messages:
776
+ if isinstance(msg, dict) and msg.get("role") == "system":
777
+ has_system = True
778
+ break
779
+
780
+ if not has_system:
781
+ # Prepend a system message if none exists
782
+ self.messages.insert(0, {"role": "system", "content": self.system_prompt})
783
+
784
+ def do_model(self, model_name):
785
+ """Change the LLM model."""
786
+ if not model_name:
787
+ print(f"Current model: {self.model}")
788
+ return
789
+
790
+ old_model = self.model
791
+ self.model = model_name
792
+ print(f"Model changed from {old_model} to {model_name}")
793
+
794
+ # Don't add a system message about model change
795
+
796
+ def do_temperature(self, arg):
797
+ """Set the temperature parameter for the LLM."""
798
+ if not arg.strip():
799
+ print(f"Current temperature: {self.temperature}")
800
+ return
801
+
802
+ try:
803
+ temp = float(arg.strip())
804
+ if 0.0 <= temp <= 2.0:
805
+ old_temp = self.temperature
806
+ self.temperature = temp
807
+ print(f"Temperature changed from {old_temp} to {temp}")
808
+ else:
809
+ print("Temperature should be between 0.0 and 2.0")
810
+ except ValueError:
811
+ print("Usage: temperature <number> (e.g., temperature 0.7)")
812
+
813
+ def do_max_tokens(self, arg):
814
+ """Set the max_tokens parameter for the LLM."""
815
+ if not arg.strip():
816
+ print(f"Current max_tokens: {self.max_tokens}")
817
+ return
818
+
819
+ try:
820
+ tokens = int(arg.strip())
821
+ if tokens > 0:
822
+ old_tokens = self.max_tokens
823
+ self.max_tokens = tokens
824
+ print(f"Max tokens changed from {old_tokens} to {tokens}")
825
+ else:
826
+ print("Max tokens should be a positive integer")
827
+ except ValueError:
828
+ print("Usage: max_tokens <number> (e.g., max_tokens 2048)")
829
+
830
+ def parse_args():
831
+ """Parse command line arguments."""
832
+ parser = argparse.ArgumentParser(description="AbstractVoice CLI Example")
833
+ parser.add_argument("--debug", action="store_true", help="Enable debug mode")
834
+ parser.add_argument("--api", default="http://localhost:11434/api/chat",
835
+ help="LLM API URL")
836
+ parser.add_argument("--model", default="granite3.3:2b",
837
+ help="LLM model name")
838
+ return parser.parse_args()
839
+
840
+
841
+ def main():
842
+ """Entry point for the application."""
843
+ try:
844
+ # Parse command line arguments
845
+ args = parse_args()
846
+
847
+ # Initialize and run REPL
848
+ repl = VoiceREPL(
849
+ api_url=args.api,
850
+ model=args.model,
851
+ debug_mode=args.debug
852
+ )
853
+ repl.cmdloop()
854
+ except KeyboardInterrupt:
855
+ print("\nExiting...")
856
+ except Exception as e:
857
+ print(f"Application error: {e}")
858
+
859
+
860
+ if __name__ == "__main__":
861
+ main()