[INFO] Connected to 192.168.1.8:50007
[INFO] Whisper model loaded successfully.
[INFO] Listing available input devices:
[INFO] 0: Microsoft Sound Mapper - Input
[INFO] 1: Microphone (2- HyperX SoloCast)
[INFO] 2: Voicemeeter Out B2 (VB-Audio Vo
[INFO] 3: CABLE-B Output (VB-Audio Cable
[INFO] 4: Microphone (Razer Barracuda X)
[INFO] 5: Voicemeeter Out A3 (VB-Audio Vo
[INFO] 6: CABLE-A Output (VB-Audio Cable
[INFO] 7: Voicemeeter Out A4 (VB-Audio Vo
[INFO] 8: Voicemeeter Out A5 (VB-Audio Vo
[INFO] 9: CABLE-D Output (VB-Audio Cable
[INFO] 10: Microphone (USBAudio1.0)
[INFO] 11: Voicemeeter Out A1 (VB-Audio Vo
[INFO] 12: Virtual Mic (Virtual Mic for Au
[INFO] 13: Voicemeeter Out B1 (VB-Audio Vo
[INFO] 14: Voicemeeter Out B3 (VB-Audio Vo
[INFO] 15: Microphone (3- VR P10 Dongle)
[INFO] 16: Voicemeeter Out A2 (VB-Audio Vo
[INFO] 17: CABLE-C Output (VB-Audio Cable
[INFO] 37: Primary Sound Capture Driver
[INFO] 38: Microphone (2- HyperX SoloCast)
[INFO] 39: Voicemeeter Out B2 (VB-Audio Voicemeeter VAIO)
[INFO] 40: CABLE-B Output (VB-Audio Cable B)
[INFO] 41: Microphone (Razer Barracuda X)
[INFO] 42: Voicemeeter Out A3 (VB-Audio Voicemeeter VAIO)
[INFO] 43: CABLE-A Output (VB-Audio Cable A)
[INFO] 44: Voicemeeter Out A4 (VB-Audio Voicemeeter VAIO)
[INFO] 45: Voicemeeter Out A5 (VB-Audio Voicemeeter VAIO)
[INFO] 46: CABLE-D Output (VB-Audio Cable D)
[INFO] 47: Microphone (USBAudio1.0)
[INFO] 48: Voicemeeter Out A1 (VB-Audio Voicemeeter VAIO)
[INFO] 49: Virtual Mic (Virtual Mic for AudioRelay)
[INFO] 50: Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)
[INFO] 51: Voicemeeter Out B3 (VB-Audio Voicemeeter VAIO)
[INFO] 52: Microphone (3- VR P10 Dongle)
[INFO] 53: Voicemeeter Out A2 (VB-Audio Voicemeeter VAIO)
[INFO] 54: CABLE-C Output (VB-Audio Cable C)
[INFO] 92: Voicemeeter Out B2 (VB-Audio Voicemeeter VAIO)
[INFO] 93: CABLE-B Output (VB-Audio Cable B)
[INFO] 94: Microphone (Razer Barracuda X)
[INFO] 95: Voicemeeter Out A3 (VB-Audio Voicemeeter VAIO)
[INFO] 96: CABLE-A Output (VB-Audio Cable A)
[INFO] 97: Voicemeeter Out A4 (VB-Audio Voicemeeter VAIO)
[INFO] 98: Microphone (2- HyperX SoloCast)
[INFO] 99: Voicemeeter Out A5 (VB-Audio Voicemeeter VAIO)
[INFO] 100: CABLE-D Output (VB-Audio Cable D)
[INFO] 101: Microphone (USBAudio1.0)
[INFO] 102: Voicemeeter Out A1 (VB-Audio Voicemeeter VAIO)
[INFO] 103: Virtual Mic (Virtual Mic for AudioRelay)
[INFO] 104: Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)
[INFO] 105: Voicemeeter Out B3 (VB-Audio Voicemeeter VAIO)
[INFO] 106: Microphone (3- VR P10 Dongle)
[INFO] 107: Voicemeeter Out A2 (VB-Audio Voicemeeter VAIO)
[INFO] 108: CABLE-C Output (VB-Audio Cable C)
[INFO] 109: CABLE-D Output (VB-Audio CABLE-D)
[INFO] 112: Input (Voicemeeter Point 3)
[INFO] 114: Input (Voicemeeter Point 6)
[INFO] 115: Voicemeeter Out 1 (Voicemeeter Point 1)
[INFO] 116: Voicemeeter Out 4 (Voicemeeter Point 4)
[INFO] 117: Voicemeeter Out 7 (Voicemeeter Point 7)
[INFO] 119: Input (Voicemeeter Point 1)
[INFO] 121: Input (Voicemeeter Point 4)
[INFO] 123: Input (Voicemeeter Point 7)
[INFO] 124: Voicemeeter Out 2 (Voicemeeter Point 2)
[INFO] 125: Voicemeeter Out 5 (Voicemeeter Point 5)
[INFO] 126: Voicemeeter Out 8 (Voicemeeter Point 8)
[INFO] 128: Input (Voicemeeter Point 2)
[INFO] 130: Input (Voicemeeter Point 5)
[INFO] 132: Input (Voicemeeter Point 8)
[INFO] 133: Voicemeeter Out 3 (Voicemeeter Point 3)
[INFO] 134: Voicemeeter Out 6 (Voicemeeter Point 6)
[INFO] 135: CABLE-A Output (VB-Audio CABLE-A)
[INFO] 140: Microphone (Logi C270 HD WebCam)
[INFO] 141: CABLE-B Output (VB-Audio CABLE-B)
[INFO] 143: Microphone (USBAudio1.0)
[INFO] 144: Microphone (USB Audio Device)
[INFO] 145: Microphone (VR P10 Dongle)
[INFO] 147: Line In (Realtek USB Audio)
[INFO] 148: Microphone (Realtek USB Audio)
[INFO] 149: Stereo Mix (Realtek USB Audio)
[INFO] 153: Microphone (HyperX SoloCast)
[INFO] 154: CABLE-C Output (VB-Audio CABLE-C)
[INFO] 156: Microphone (Logi C270 HD WebCam)
[INFO] 157: Microphone (Razer Barracuda X)
[INFO] 159: Microphone (VDVAD Wave)
[INFO] 161: Virtual Mic (AudioRelay Wave)
[INFO] User selected microphone: Microphone (USBAudio1.0)
[INFO] Confirmation handler thread started.
[INFO] Processing thread started.
[INFO] Flask web server started on http://0.0.0.0:5000
[INFO] Global keyboard listener started.
[INFO] Audio stream started.
[INFO] Selected microphone: Microphone (USBAudio1.0)
[INFO] Handling microphone switch event.
[WARNING] Microphone switch requested. Please restart the script to apply changes.
[INFO] Activation key pressed. Start recording.
[INFO] Activation key released. Stop recording.
[INFO] Audio data added to processing queue.
[INFO] Converted audio bytes to NumPy array with shape (56000,).
[INFO] Starting Whisper transcription.
[INFO] Whisper recognized text: 'Alright, we are casting this, how's that work? Nope, no, not for the fourth cast due to a card being at me .'
[INFO] No known alias found. Canceling message.
[INFO] Could not extract account or message from recognized text: 'Alright, we are casting this, how's that work? Nope, no, not for the fourth cast due to a card being at me .'
[INFO] Activation key pressed. Start recording.
[INFO] Activation key released. Stop recording.
[INFO] Audio data added to processing queue.
[INFO] Converted audio bytes to NumPy array with shape (72000,).
[INFO] Starting Whisper transcription.
[INFO] Whisper recognized text: 'message from dark yo what's up guys how's it going'
[INFO] No known alias found. Canceling message.
[INFO] Could not extract account or message from recognized text: 'message from dark yo what's up guys how's it going'
[INFO] Activation key pressed. Start recording.
[INFO] Activation key released. Stop recording.
[INFO] Audio data added to processing queue.
[INFO] Converted audio bytes to NumPy array with shape (64000,).
[INFO] Starting Whisper transcription.
[INFO] Whisper recognized text: 'message from doc yeah what's up guys'
[INFO] Extracted account 'd0k6cn1y3d' and message 'yeah what's up guys' from recognized text.
[INFO] Introduced grammatical error by removing punctuation.
[INFO] Message with errors: 'yeah whats up guys'
[INFO] [CONFIRMATION 1] Transcribed Message for 'd0k6cn1y3d': 'yeah whats up guys'
[INFO] No input received for Step 1. Proceeding to Step 2.
[INFO] [CONFIRMATION 2] Account: 'd0k6cn1y3d'
[INFO] Message: 'yeah whats up guys'
[INFO] User canceled the message at Step 2.

I still need a console chat that I can type into below the console, additionally, it's not simulating a `` press until I click stop yet, and it'sstill popping up with confirmation of me changing mic. I only want to see that in the console. additionally, I would like it to start the web app before I pick a microphone, incase im doing it solely from the web app.

import sounddevice as sd
import socket
import json
import threading
import sys
import time
import random
import logging
import ctypes
import os
import re
from fuzzywuzzy import fuzz, process  # For fuzzy matching
import keyboard  # For global key listening
import wave  # For saving audio files (optional)
import whisper
import numpy as np
import traceback
import warnings  # For suppressing warnings
import queue  # For thread-safe queues
from collections import deque

# Flask imports
from flask import Flask, render_template_string, request, jsonify

# Only import msvcrt if on Windows
if os.name == 'nt':
    import msvcrt
else:
    import select  # For Unix-based systems

######################## Configuration #########################
# Configuration for sending data to the second PC
SECOND_PC_IP = "192.168.1.8"  # Replace with the actual IP address of the second PC
PORT = 50007  # Must match the port on the second PC

# Path to the Whisper model
WHISPER_MODEL_SIZE = "small"  # Options: tiny, base, small, medium, large

# Twitch account aliases (including common variations)
TWITCH_ACCOUNTS = {
    'hugeb0ss': ['huge', 'boss', 'baws'],
    'killerbigproblem8874': ['killer'],
    'd0k6cn1y3d': ['dok', 'doc'],
    # Add more accounts and their aliases if needed
}

# Activation key (Backtick)
ACTIVATION_KEY = '`'  # Set to the backtick key

# Log file path
LOG_FILE = "speech_sender.log"

# Optional: Path to save recorded audio for debugging
SAVE_AUDIO = False
AUDIO_SAVE_PATH = "recorded_audio.wav"

# Similarity threshold for fuzzy matching
FUZZY_THRESHOLD = 80  # Adjust based on testing

# Target microphone names for recording
TARGET_MIC_NAMES = [
    'Microphone (USBAudio1.0)',  # Replace with your microphone names
    # Add any other microphones you prefer to use for recording
]

# Timeout settings
INITIAL_CONFIRMATION_TIMEOUT = 8  # Seconds for first confirmation
FINAL_CONFIRMATION_TIMEOUT = 20  # Seconds for second confirmation
SEND_DELAY_MIN = 5  # Minimum seconds to wait before sending without input
SEND_DELAY_MAX = 25  # Maximum seconds to wait before sending without input

######################## Logging Setup #########################
# Configure Logging
logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,  # Set to INFO to reduce verbosity
    format='%(asctime)s - %(levelname)s - %(message)s'
)

# Suppress Whisper warnings
warnings.filterwarnings("ignore", category=UserWarning)

######################## Admin Check #########################
def is_running_as_admin():
    """Check if the script is running with administrative privileges."""
    try:
        return ctypes.windll.shell32.IsUserAnAdmin()
    except:
        return False

def restart_as_admin():
    """Restart the script with administrative privileges."""
    try:
        # Get the absolute path of the script
        script = os.path.abspath(__file__)

        # Get the executable (python) path
        executable = sys.executable

        # Prepare the command
        params = ' '.join([f'"{arg}"' for arg in sys.argv[1:]])

        # Restart the script with admin rights
        ctypes.windll.shell32.ShellExecuteW(
            None, "runas", executable, f'"{script}" {params}', None, 1)
        logging.info("Restarted script with administrative privileges.")
    except Exception as e:
        logging.error(f"Failed to restart as admin: {e}")
        print(f"Failed to restart as admin: {e}", flush=True)
    sys.exit()

######################## Global Variables ######################
selected_device_name = ''
sock = None
whisper_model = None
device_index = None  # To keep track of the selected device

# Queue for processing messages sequentially
processing_queue = queue.Queue()

# Queue for confirmation requests
confirmation_queue = queue.Queue()

# Log deque for web console (thread-safe with max length)
log_deque = deque(maxlen=1000)
log_lock = threading.Lock()

# Event to signal microphone switch
microphone_switch_event = threading.Event()

# Flask app initialization
app = Flask(__name__)

# HTML Template for the web interface
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
    <title>Speech Sender</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #logs { border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: scroll; background-color: #f9f9f9; }
        button { padding: 10px 20px; margin: 5px; }
        select { padding: 5px; margin: 5px; }
        textarea { width: 100%; }
    </style>
    <script>
        function startRecording() {
            fetch('/start_recording', {method: 'POST'})
            .then(response => response.json())
            .then(data => console.log(data.message));
        }

        function stopRecording() {
            fetch('/stop_recording', {method: 'POST'})
            .then(response => response.json())
            .then(data => console.log(data.message));
        }

        function switchMicrophone() {
            const deviceSelect = document.getElementById('microphoneSelect');
            const selectedDevice = deviceSelect.value;
            fetch('/switch_microphone', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ device_name: selectedDevice })
            })
            .then(response => response.json())
            .then(data => console.log(data.message));
        }

        function sendMessage() {
            const message = document.getElementById('message').value;

            fetch('/send_console_message', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ message: message })
            })
            .then(response => response.json())
            .then(data => {
                alert(data.message);
                document.getElementById('message').value = '';
            });
        }

        function fetchLogs() {
            fetch('/logs')
            .then(response => response.json())
            .then(data => {
                const logsDiv = document.getElementById('logs');
                logsDiv.innerHTML = data.logs.join('<br>');
                logsDiv.scrollTop = logsDiv.scrollHeight;
            });
        }

        setInterval(fetchLogs, 1000); // Fetch logs every second
    </script>
</head>
<body>
    <h1>Speech Sender Control Panel</h1>
    <div>
        <button onclick="startRecording()">Start Recording</button>
        <button onclick="stopRecording()">Stop Recording</button>
    </div>
    <div>
        <h3>Select Microphone</h3>
        <select id="microphoneSelect">
            {% for device in devices %}
                <option value="{{ device }}">{{ device }}</option>
            {% endfor %}
        </select>
        <button onclick="switchMicrophone()">Switch Microphone</button>
    </div>
    <div>
        <h3>Send Console Message</h3>
        <form id="sendMessageForm">
            <label for="message">Message:</label><br>
            <textarea id="message" name="message" rows="4" cols="50"></textarea><br><br>
            <button type="button" onclick="sendMessage()">Send Message</button>
        </form>
    </div>
    <div>
        <h3>Console Logs</h3>
        <div id="logs"></div>
    </div>
</body>
</html>
"""

######################## Class Definitions #######################
class AudioRecorder:
    def __init__(self, device_index, callback):
        self.device_index = device_index
        self.callback = callback
        self.stream = None
        self.lock = threading.Lock()

    def start_stream(self):
        with self.lock:
            if self.stream:
                self.stream.close()
            try:
                self.stream = sd.RawInputStream(
                    samplerate=16000,
                    blocksize=8000,  # Adjusted to prevent input overflow
                    dtype='int16',
                    channels=1,
                    device=self.device_index,
                    callback=self.callback
                )
                self.stream.start()
                log_message("Audio stream started.", 'INFO')
                print("[INFO] Audio stream started.", flush=True)
            except Exception as e:
                log_message(f"Failed to start audio stream: {e}", 'ERROR')
                print(f"[ERROR] Failed to start audio stream: {e}", flush=True)

    def stop_stream(self):
        with self.lock:
            if self.stream:
                self.stream.stop()
                self.stream.close()
                self.stream = None
                log_message("Audio stream stopped.", 'INFO')
                print("[INFO] Audio stream stopped.", flush=True)

    def switch_device(self, new_device_index):
        with self.lock:
            self.device_index = new_device_index
            self.start_stream()

######################## Function Definitions ###################
def log_message(message, level='INFO'):
    """Add a log message to both the logger and the web console deque."""
    # Log to the logging system
    if level.upper() == 'DEBUG':
        logging.debug(message)
    elif level.upper() == 'INFO':
        logging.info(message)
    elif level.upper() == 'WARNING':
        logging.warning(message)
    elif level.upper() == 'ERROR':
        logging.error(message)
    elif level.upper() == 'CRITICAL':
        logging.critical(message)
    else:
        logging.info(message)

    # Only add to the web console log deque if level >= INFO
    if level.upper() in ['INFO', 'WARNING', 'ERROR', 'CRITICAL']:
        with log_lock:
            log_deque.append(f"[{level.upper()}] {message}")

def audio_callback(indata, frames, time_info, status):
    """Callback function to collect audio data."""
    if status:
        log_message(f"Stream status: {status}", 'WARNING')
        print(f"[WARNING] Stream status: {status}", flush=True)
    global is_recording, recorded_audio
    if is_recording:
        try:
            # Convert indata to bytes and append
            audio_data = bytes(indata)
            recorded_audio.append(audio_data)
        except Exception as e:
            log_message(f"Error processing audio data: {e}", 'ERROR')
            print(f"[ERROR] Error processing audio data: {e}", flush=True)

def introduce_errors(text):
    """Introduce errors into the text: misspellings a certain percentage of the time, and minor grammatical errors otherwise."""
    rand = random.random()
    if rand < 0.3:  # 30% chance
        # Introduce a misspelling
        words = text.split()
        if len(words) == 0:
            return text  # No words to process

        # Randomly select one word to alter
        word_to_alter_index = random.randint(0, len(words) - 1)
        word_to_alter = words[word_to_alter_index]
        original_word = word_to_alter  # For logging

        # Decide the type of misspelling to introduce
        if len(word_to_alter) > 1 and random.random() < 0.5:
            # Swap two adjacent letters
            if len(word_to_alter) >= 2:
                idx = random.randint(0, len(word_to_alter) - 2)
                word_to_alter = (
                    word_to_alter[:idx] +
                    word_to_alter[idx+1] +
                    word_to_alter[idx] +
                    word_to_alter[idx+2:]
                )
        else:
            # Misspell the word by changing a letter
            if len(word_to_alter) >= 1:
                idx = random.randint(0, len(word_to_alter) - 1)
                word_to_alter = (
                    word_to_alter[:idx] +
                    random.choice('abcdefghijklmnopqrstuvwxyz') +
                    word_to_alter[idx+1:]
                )
        # Replace the word in the list
        words[word_to_alter_index] = word_to_alter
        new_text = ' '.join(words)
        log_message(f"Introduced misspelling in word '{original_word}' -> '{word_to_alter}'", 'INFO')
    else:
        # Introduce a grammatical error (remove punctuation)
        new_text = re.sub(r'[^\w\s]', '', text)
        log_message("Introduced grammatical error by removing punctuation.", 'INFO')
    return new_text.strip()

def extract_account_and_message(recognized_text):
    """Extract target account and message from recognized text."""
    target_account = None
    message = ""

    # Normalize the recognized text
    recognized_text_lower = recognized_text.lower()

    # Check for aliases
    for account, aliases in TWITCH_ACCOUNTS.items():
        for alias in aliases:
            pattern = r'\b' + re.escape(alias.lower()) + r'\b'
            match = re.search(pattern, recognized_text_lower)
            if match:
                target_account = account
                message = recognized_text[match.end():].strip()
                # Optionally, remove leading connectors
                connectors = ['for', 'to', 'from', 'message', 'send', 'notify',
                              'hi', 'this is', 'okay', 'so']
                connector_pattern = r'^(' + '|'.join(connectors) + r')\s+'
                message = re.sub(connector_pattern, '', message, flags=re.IGNORECASE).strip()
                log_message(f"Extracted account '{account}' and message '{message}' from recognized text.", 'INFO')
                return target_account, message

    # If no alias found, cancel the message
    log_message("No known alias found. Canceling message.", 'INFO')
    print("[INFO] Unrecognized account. Message canceled.", flush=True)
    return None, None

def handle_user_confirmation(account, message):
    """Place a confirmation request into the confirmation queue."""
    confirmation_queue.put((account, message))

def confirmation_handler():
    """Thread function to handle user confirmations."""
    while True:
        try:
            account, message = confirmation_queue.get()
            if not account or not message:
                log_message("Invalid confirmation request, skipping.", 'WARNING')
                confirmation_queue.task_done()
                continue

            # Step 1: Initial Confirmation (8 seconds)
            log_message(f"\n[CONFIRMATION 1] Transcribed Message for '{account}': '{message}'", 'INFO')
            print(f"\n[CONFIRMATION 1] Transcribed Message for '{account}': '{message}'", flush=True)
            logging.info(f"Transcribed Message for '{account}': '{message}'")

            response1 = get_input_timeout("Confirm this message? (Y/N): ", INITIAL_CONFIRMATION_TIMEOUT)

            if response1 is None:
                # No input, proceed to Step 2
                log_message("No input received for Step 1. Proceeding to Step 2.", 'INFO')
                print("[INFO] No input received for Step 1. Proceeding to Step 2.", flush=True)
            else:
                response1 = response1.strip().lower()
                log_message(f"Step 1 Response: '{response1}'", 'INFO')

                if response1 == 'n':
                    log_message("Message canceled by user at Step 1.", 'INFO')
                    print("[INFO] Message canceled by user at Step 1.", flush=True)
                    confirmation_queue.task_done()
                    continue
                elif response1 != 'y':
                    log_message("Invalid input at Step 1. Proceeding to Step 2.", 'WARNING')
                    print("[WARNING] Invalid input at Step 1. Proceeding to Step 2.", flush=True)

            # Step 2: Final Confirmation (20 seconds)
            # Display the account it's going to send to
            log_message(f"\n[CONFIRMATION 2] Account: '{account}'", 'INFO')
            log_message(f"Message: '{message}'", 'INFO')
            print(f"\n[CONFIRMATION 2] Account: '{account}'", flush=True)
            print(f"Message: '{message}'", flush=True)
            logging.info(f"Account: '{account}', Message: '{message}'")

            response2 = get_input_timeout("Is this correct? (Y/N) or enter corrected message: ", FINAL_CONFIRMATION_TIMEOUT)

            if response2 is None:
                # No input received, assume message is good and send after random delay
                log_message("No input received for Step 2. Assuming message is correct.", 'INFO')
                print("[INFO] No input received for Step 2. Assuming message is correct.", flush=True)
                delay = random.randint(SEND_DELAY_MIN, SEND_DELAY_MAX)
                log_message(f"Waiting for {delay} seconds before sending message to account '{account}'.", 'INFO')
                print(f"[INFO] Waiting for {delay} seconds before sending message to account '{account}'.", flush=True)
                time.sleep(delay)
                send_to_second_pc(account, message)
                log_message(f"Message sent to account '{account}': '{message}'", 'INFO')
                print(f"[INFO] Message sent to account '{account}': '{message}'", flush=True)
            else:
                response2 = response2.strip()

                if response2.lower() == 'y':
                    # Send immediately
                    log_message("User confirmed to send the message immediately.", 'INFO')
                    print("[INFO] Sending message immediately as per Step 2 confirmation.", flush=True)
                    send_to_second_pc(account, message)
                    log_message(f"Message sent to account '{account}': '{message}'", 'INFO')
                    print(f"[INFO] Message sent to account '{account}': '{message}'", flush=True)
                elif response2.lower() == 'n':
                    # Cancel the message
                    log_message("User canceled the message at Step 2.", 'INFO')
                    print("[INFO] Message canceled by user at Step 2.", flush=True)
                else:
                    # Assume the user entered a corrected message
                    corrected_message = response2
                    if corrected_message:
                        log_message(f"User provided a corrected message: '{corrected_message}'", 'INFO')
                        print(f"[INFO] Sending corrected message: '{corrected_message}'", flush=True)
                        send_to_second_pc(account, corrected_message)
                        log_message(f"Corrected message sent to account '{account}': '{corrected_message}'", 'INFO')
                        print(f"[INFO] Corrected message sent to account '{account}': '{corrected_message}'", flush=True)
                    else:
                        # Empty input, assume message is correct and send after delay
                        log_message("Empty input provided at Step 2. Assuming message is correct.", 'WARNING')
                        print("[WARNING] Empty input provided at Step 2. Assuming message is correct.", flush=True)
                        delay = random.randint(SEND_DELAY_MIN, SEND_DELAY_MAX)
                        log_message(f"Waiting for {delay} seconds before sending message to account '{account}'.", 'INFO')
                        print(f"[INFO] Waiting for {delay} seconds before sending message to account '{account}'.", flush=True)
                        time.sleep(delay)
                        send_to_second_pc(account, message)
                        log_message(f"Message sent to account '{account}': '{message}'", 'INFO')
                        print(f"[INFO] Message sent to account '{account}': '{message}'", flush=True)

            confirmation_queue.task_done()
        except Exception as e:
            log_message(f"Exception in confirmation_handler: {e}", 'ERROR')
            print(f"[ERROR] Exception in confirmation_handler: {e}", flush=True)
            traceback.print_exc()
            confirmation_queue.task_done()
            continue  # Ensure the loop continues

def process_queue():
    """Worker thread to process messages sequentially."""
    while True:
        try:
            # Get the next audio data from the queue
            audio_bytes = processing_queue.get()
            if not isinstance(audio_bytes, bytes):
                log_message("Received non-bytes item in processing queue, skipping.", 'WARNING')
                processing_queue.task_done()
                continue

            if len(audio_bytes) == 0:
                log_message("Received empty audio data, skipping transcription.", 'WARNING')
                print("[WARNING] Received empty audio data, skipping transcription.", flush=True)
                processing_queue.task_done()
                continue

            # Transcribe with Whisper
            recognized_text = transcribe_with_whisper(audio_bytes)

            if not recognized_text:
                log_message("No complete text recognized.", 'INFO')
                print("[INFO] No complete text recognized.", flush=True)
                processing_queue.task_done()
                continue  # Move to next message

            # Extract account and message
            target_account, message = extract_account_and_message(recognized_text)

            if not target_account or not message:
                log_message(f"Could not extract account or message from recognized text: '{recognized_text}'", 'INFO')
                print(f"[INFO] Could not extract account or message from recognized text: '{recognized_text}'", flush=True)
                processing_queue.task_done()
                continue  # Move to next message

            # Introduce errors into the message (after alias detection)
            message_with_errors = introduce_errors(message)
            log_message(f"Message with errors: '{message_with_errors}'", 'INFO')

            # Handle confirmation and sending
            handle_user_confirmation(target_account, message_with_errors)

            processing_queue.task_done()
        except Exception as e:
            log_message(f"Exception in process_queue: {e}", 'ERROR')
            print(f"[ERROR] Exception in process_queue: {e}", flush=True)
            traceback.print_exc()
            processing_queue.task_done()
            continue  # Ensure the loop continues

def get_input_timeout(prompt, timeout):
    """
    Get user input with a timeout. Returns None if no input is received within the timeout.
    This function uses msvcrt for Windows and select for Unix-based systems.
    """
    print(prompt, end='', flush=True)
    start_time = time.time()
    input_str = ''

    if os.name == 'nt':
        while True:
            if msvcrt.kbhit():
                char = msvcrt.getche()
                if char in [b'\r', b'\n']:
                    print('')  # Move to the next line
                    break
                elif char == b'\x08':  # Backspace
                    input_str = input_str[:-1]
                    # Move the cursor back, overwrite with space, move back again
                    print(' \b', end='', flush=True)
                else:
                    try:
                        decoded_char = char.decode()
                        input_str += decoded_char
                    except UnicodeDecodeError:
                        pass  # Ignore non-decodable characters
            if time.time() - start_time > timeout:
                print('')  # Move to the next line
                return None
            time.sleep(0.05)
    else:
        # For Unix-based systems
        import sys
        import select
        while True:
            ready, _, _ = select.select([sys.stdin], [], [], max(0, start_time + timeout - time.time()))
            if ready:
                line = sys.stdin.readline()
                if line:
                    input_str = line.strip()
                    break
            else:
                return None

    return input_str if input_str else None

def transcribe_with_whisper(audio_bytes):
    """Transcribe audio using Whisper."""
    try:
        # Convert audio_bytes to a NumPy array
        audio_np = np.frombuffer(audio_bytes, np.int16).astype(np.float32) / 32768.0  # Normalize
        log_message(f"Converted audio bytes to NumPy array with shape {audio_np.shape}.", 'INFO')

        if audio_np.size == 0:
            log_message("Audio data is empty after conversion.", 'WARNING')
            return ''

        # Transcribe
        log_message("Starting Whisper transcription.", 'INFO')
        result = whisper_model.transcribe(audio_np)
        recognized_text = result.get('text', '').strip()
        log_message(f"Whisper recognized text: '{recognized_text}'", 'INFO')

        return recognized_text
    except Exception as e:
        log_message(f"Exception in transcribe_with_whisper: {e}", 'ERROR')
        print(f"[ERROR] Exception in transcribe_with_whisper: {e}", flush=True)
        traceback.print_exc()
        return ''

def send_to_second_pc(account, text):
    """Send recognized text to the second PC via TCP, specifying the Twitch account."""
    try:
        if text and account:
            # Format the message as JSON for reliable parsing
            message_dict = {
                "account": account,
                "message": text
            }
            message_json = json.dumps(message_dict) + '\n'  # Adding newline as delimiter
            sock.sendall(message_json.encode('utf-8'))
            log_message(f"Sent message to {SECOND_PC_IP}:{PORT} - Account: {account}, Message: {text}", 'INFO')
            print(f"[INFO] Sent message to {SECOND_PC_IP}:{PORT} - Account: {account}, Message: {text}", flush=True)
        else:
            log_message("No text or account specified to send.", 'WARNING')
            print("[WARNING] No text or account specified to send.", flush=True)
    except socket.error as se:
        log_message(f"Socket error: {se}", 'ERROR')
        print(f"[ERROR] Socket error: {se}", flush=True)
    except Exception as e:
        log_message(f"Failed to send message: {e}", 'ERROR')
        print(f"[ERROR] Failed to send message: {e}", flush=True)

def list_input_devices():
    """List all available input devices."""
    try:
        input_devices = sd.query_devices()
        log_message("Listing available input devices:", 'INFO')
        for idx, device in enumerate(input_devices):
            if device['max_input_channels'] > 0:
                log_message(f"{idx}: {device['name']}", 'INFO')
                print(f"{idx}: {device['name']}", flush=True)
        return input_devices
    except Exception as e:
        log_message(f"Failed to list input devices: {e}", 'ERROR')
        print(f"[ERROR] Failed to list input devices: {e}", flush=True)
        sys.exit(1)

def select_microphone(input_devices, selected_device=None):
    """Select the microphone matching TARGET_MIC_NAMES or cancel if not found.

    If selected_device is provided, selects that device.
    """
    global device_index, selected_device_name, audio_recorder

    if selected_device:
        # Attempt to find the device by name
        for idx, device in enumerate(input_devices):
            if device['name'] == selected_device:
                if device['max_input_channels'] > 0:
                    device_index = idx
                    selected_device_name = device['name']
                    log_message(f"Selected microphone: {selected_device_name}", 'INFO')
                    print(f"[INFO] Selected microphone: {selected_device_name}", flush=True)
                    # Switch device in AudioRecorder
                    if audio_recorder:
                        audio_recorder.switch_device(device_index)
                    return True
        log_message(f"Microphone '{selected_device}' not found.", 'WARNING')
        print(f"[WARNING] Microphone '{selected_device}' not found.", flush=True)
        return False
    else:
        # Select based on TARGET_MIC_NAMES
        for idx, device in enumerate(input_devices):
            if device['max_input_channels'] > 0:
                device_name_lower = device['name'].lower()
                for target_name in TARGET_MIC_NAMES:
                    if target_name.lower() in device_name_lower:
                        device_index = idx
                        selected_device_name = device['name']
                        log_message(f"Selected microphone: {selected_device_name}", 'INFO')
                        print(f"[INFO] Selected microphone: {selected_device_name}", flush=True)
                        # Initialize AudioRecorder
                        if not audio_recorder:
                            audio_recorder = AudioRecorder(device_index, audio_callback)
                            audio_recorder.start_stream()
                        else:
                            audio_recorder.switch_device(device_index)
                        return True

        # If none matched, prompt user to select
        print("[WARNING] No matching microphone found. Please ensure the correct microphone is connected.", flush=True)
        log_message("No matching microphone found. Exiting.", 'WARNING')
        sys.exit(1)

def save_audio_file(audio_data):
    """Save recorded audio data to a WAV file for debugging."""
    try:
        with wave.open(AUDIO_SAVE_PATH, 'wb') as wf:
            wf.setnchannels(1)  # Mono
            wf.setsampwidth(2)  # 16 bits per sample
            wf.setframerate(16000)  # Sampling rate
            wf.writeframes(audio_data)
        log_message(f"Audio saved to {AUDIO_SAVE_PATH}", 'INFO')
        print(f"[INFO] Audio saved to {AUDIO_SAVE_PATH}", flush=True)
    except Exception as e:
        log_message(f"Failed to save audio file: {e}", 'ERROR')
        print(f"[ERROR] Failed to save audio file: {e}", flush=True)

def connect_to_second_pc():
    """Establish connection to the second PC with retries."""
    global sock
    while True:
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.connect((SECOND_PC_IP, PORT))
            log_message(f"Connected to {SECOND_PC_IP}:{PORT}", 'INFO')
            print(f"[INFO] Connected to {SECOND_PC_IP}:{PORT}", flush=True)
            break
        except Exception as e:
            log_message(f"Failed to connect to {SECOND_PC_IP}:{PORT} - {e}", 'ERROR')
            print(f"[ERROR] Failed to connect to {SECOND_PC_IP}:{PORT} - {e}", flush=True)
            log_message("Retrying in 5 seconds...", 'INFO')
            print("[INFO] Retrying in 5 seconds...", flush=True)
            time.sleep(5)

def activation_key_event():
    """Listen for activation key events using the keyboard library."""
    keyboard.on_press_key(ACTIVATION_KEY, lambda _: start_recording())
    keyboard.on_release_key(ACTIVATION_KEY, lambda _: stop_recording())

def start_recording():
    """Start recording audio and simulate pressing the activation key."""
    global is_recording, recorded_audio, audio_recorder
    if not is_recording:
        is_recording = True
        recorded_audio = []  # Reset the buffer
        keyboard.press(ACTIVATION_KEY)  # Simulate key press
        log_message("Activation key pressed. Start recording.", 'INFO')
        print("[INFO] Activation key pressed. Start recording.", flush=True)

def stop_recording():
    """Stop recording audio and simulate releasing the activation key."""
    global is_recording, recorded_audio, audio_recorder
    if is_recording:
        is_recording = False
        keyboard.release(ACTIVATION_KEY)  # Simulate key release
        log_message("Activation key released. Stop recording.", 'INFO')
        print("[INFO] Activation key released. Stop recording.", flush=True)

        # Introduce a short delay before processing
        time.sleep(0.5)

        # Optionally, save the audio for debugging
        if SAVE_AUDIO:
            audio_data = b''.join(recorded_audio)
            save_audio_file(audio_data)

        # Add raw audio data to processing queue
        audio_data = b''.join(recorded_audio)
        processing_queue.put(audio_data)
        log_message("Audio data added to processing queue.", 'INFO')
        print("[INFO] Audio data added to processing queue.", flush=True)

def handle_console_input():
    """Thread function to handle console-based message input."""
    while True:
        try:
            user_input = input()
            if user_input.strip() == '':
                continue
            send_to_second_pc('console', user_input.strip())
        except EOFError:
            break
        except Exception as e:
            log_message(f"Error in handle_console_input: {e}", 'ERROR')
            print(f"[ERROR] Error in handle_console_input: {e}", flush=True)

def handle_exception(exc_type, exc_value, exc_traceback):
    """Log any uncaught exceptions."""
    if issubclass(exc_type, KeyboardInterrupt):
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
        return
    logging.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
    traceback.print_exception(exc_type, exc_value, exc_traceback)
    print(f"[CRITICAL] Uncaught exception: {exc_value}", flush=True)
    log_message(f"Uncaught exception: {exc_value}", 'CRITICAL')

sys.excepthook = handle_exception

######################## Flask Routes ###########################

@app.route('/')
def index():
    """Render the main page with controls and logs."""
    input_devices = sd.query_devices()
    device_names = [device['name'] for device in input_devices if device['max_input_channels'] > 0]
    twitch_accounts_list = list(TWITCH_ACCOUNTS.keys())
    return render_template_string(HTML_TEMPLATE, devices=device_names, twitch_accounts=twitch_accounts_list)

@app.route('/start_recording', methods=['POST'])
def web_start_recording():
    """Start recording via the web interface."""
    start_recording()
    return jsonify({"message": "Recording started."})

@app.route('/stop_recording', methods=['POST'])
def web_stop_recording():
    """Stop recording via the web interface."""
    stop_recording()
    return jsonify({"message": "Recording stopped."})

@app.route('/switch_microphone', methods=['POST'])
def web_switch_microphone():
    """Switch microphone via the web interface."""
    data = request.get_json()
    device_name = data.get('device_name', '').strip()
    if not device_name:
        return jsonify({"message": "No device name provided."}), 400

    input_devices = sd.query_devices()
    success = select_microphone(input_devices, selected_device=device_name)
    if success:
        return jsonify({"message": f"Switched to microphone '{device_name}'."})
    else:
        return jsonify({"message": f"Microphone '{device_name}' not found."}), 404

@app.route('/send_console_message', methods=['POST'])
def send_console_message_route():
    """Send a message directly from the web interface to the console."""
    data = request.get_json()
    message = data.get('message', '').strip()

    if not message:
        return jsonify({"message": "Message is required."}), 400

    send_to_second_pc('console', message)
    return jsonify({"message": "Message sent to console."})

@app.route('/logs')
def get_logs():
    """Provide the latest logs for the web console."""
    with log_lock:
        logs = list(log_deque)
    return jsonify({"logs": logs})

######################## Main Function ##########################

def main():
    global sock, whisper_model, device_index, selected_device_name, audio_recorder

    # Check if the script is already elevated
    if not is_running_as_admin():
        log_message("Script not running as admin. Attempting to restart as admin.", 'INFO')
        print("This script requires administrative privileges to function correctly.", flush=True)
        print("Attempting to restart with administrative rights...", flush=True)
        restart_as_admin()

    # Initialize the TCP socket with retry mechanism
    try:
        connect_to_second_pc()
    except Exception as e:
        log_message(f"Unable to establish connection after retries: {e}", 'ERROR')
        print(f"[ERROR] Unable to establish connection after retries: {e}", flush=True)
        sys.exit(1)

    # Load Whisper model
    try:
        whisper_model = whisper.load_model(WHISPER_MODEL_SIZE)
        log_message("Whisper model loaded successfully.", 'INFO')
        print("[INFO] Whisper model loaded successfully.", flush=True)
    except Exception as e:
        log_message(f"Failed to load Whisper model: {e}", 'ERROR')
        print(f"[ERROR] Failed to load Whisper model: {e}", flush=True)
        sys.exit(1)

    # List input devices and allow the user to select the desired microphone
    input_devices = list_input_devices()
    print("\nPlease select a microphone by entering the corresponding number:", flush=True)
    for idx, device in enumerate(input_devices):
        if device['max_input_channels'] > 0:
            print(f"{idx}: {device['name']}", flush=True)
    while True:
        try:
            user_input = int(input("Microphone number: ").strip())
            if 0 <= user_input < len(input_devices) and input_devices[user_input]['max_input_channels'] > 0:
                device_index = user_input
                selected_device_name = input_devices[user_input]['name']
                log_message(f"User selected microphone: {selected_device_name}", 'INFO')
                print(f"[INFO] Selected microphone: {selected_device_name}", flush=True)
                break
            else:
                print("[ERROR] Invalid selection. Please enter a valid microphone number.", flush=True)
        except ValueError:
            print("[ERROR] Please enter a valid number.", flush=True)

    # Initialize AudioRecorder
    audio_recorder = AudioRecorder(device_index, audio_callback)
    audio_recorder.start_stream()

    # Start the confirmation handler thread
    confirmation_thread = threading.Thread(target=confirmation_handler, daemon=True)
    confirmation_thread.start()
    log_message("Confirmation handler thread started.", 'INFO')
    print("[INFO] Confirmation handler thread started.", flush=True)

    # Start the processing thread
    processing_thread = threading.Thread(target=process_queue, daemon=True)
    processing_thread.start()
    log_message("Processing thread started.", 'INFO')
    print("[INFO] Processing thread started.", flush=True)

    # Start the Flask web server in a separate thread
    flask_thread = threading.Thread(target=run_flask_app, daemon=True)
    flask_thread.start()
    log_message("Flask web server started on http://0.0.0.0:5000", 'INFO')
    print("[INFO] Flask web server started on http://0.0.0.0:5000", flush=True)

    # Start the global activation key listener
    try:
        activation_key_event()
        log_message("Global keyboard listener started.", 'INFO')
        print("[INFO] Global keyboard listener started.", flush=True)
    except Exception as e:
        log_message(f"Failed to start global keyboard listener: {e}", 'ERROR')
        print(f"[ERROR] Failed to start global keyboard listener: {e}", flush=True)
        sys.exit(1)

    # Start the console input handler thread
    console_input_thread = threading.Thread(target=handle_console_input, daemon=True)
    console_input_thread.start()
    log_message("Console input handler thread started.", 'INFO')
    print("[INFO] Console input handler thread started.", flush=True)

    # Start the audio stream with the selected microphone
    while True:
        try:
            # The AudioRecorder handles the audio stream
            # Just keep the main thread alive
            time.sleep(1)
        except KeyboardInterrupt:
            print("\n[INFO] Stopped by user.", flush=True)
            log_message("Script stopped by user.", 'INFO')
            break
        except Exception as e:
            log_message(f"Exception in main loop: {e}", 'ERROR')
            print(f"[ERROR] Exception in main loop: {e}", flush=True)
            traceback.print_exc()
            log_message("Retrying in 5 seconds...", 'WARNING')
            print("[WARNING] Retrying in 5 seconds...", flush=True)
            time.sleep(5)  # Wait before retrying

    # Clean up resources
    try:
        audio_recorder.stop_stream()
    except Exception as e:
        log_message(f"Failed to stop audio stream: {e}", 'ERROR')
        print(f"[ERROR] Failed to stop audio stream: {e}", flush=True)

    try:
        sock.close()
        log_message("Socket closed.", 'INFO')
        print("[INFO] Socket closed.", flush=True)
    except Exception as e:
        log_message(f"Failed to close socket: {e}", 'ERROR')
        print(f"[ERROR] Failed to close socket: {e}", flush=True)
    print("[INFO] Script terminated.", flush=True)
    log_message("Script terminated.", 'INFO')

######################## Threading Setup #######################
def run_flask_app():
    """Run the Flask app."""
    app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False)

######################## Entry Point ############################
if __name__ == "__main__":
    main()