# ==================================================================================================
#
#   ██╗ ███████╗  ██████╗  ██████╗ ██╗██████╗ ██╗   ██╗██╗████████╗
#   ██║ ██╔════╝ ██╔═══██╗██╔═══██╗██║██╔══██╗██║   ██║██║╚══██╔══╝
#   ██║ ███████╗ ██║   ██║██║   ██║██║██████╔╝██║   ██║██║   ██║
#   ██║ ╚════██║ ██║   ██║██║   ██║██║██╔═══╝ ╚██╗ ██╔╝██║   ██║
#   ██║ ███████║ ╚██████╔╝╚██████╔╝██║██║      ╚████╔╝ ██║   ██║
#   ╚═╝ ╚══════╝  ╚═════╝  ╚═════╝ ╚═╝╚═╝       ╚═══╝  ╚═╝   ╚═╝
#   iOS Interview Co-pilot - v1.9 (Final)
#
# ==================================================================================================
#
#   SECTION 1: DOCUMENTATION (Unchanged)
#
# ==================================================================================================
#   SECTION 2: SETUP & USAGE
# ==================================================================================================
#
# 1. PREREQUISITES:
#    - Python 3.8+ installed.
#
# 2. INSTALL DEPENDENCIES:
#    pip install PyQt6 sounddevice webrtcvad-wheels groq google-genai numpy
#
# 3. SET API KEYS:
#    Set the following as environment variables on your system:
#    - GROQ_API_KEY
#    - GOOGLE_API_KEY
#
# 4. CREATE KNOWLEDGE BASE:
#    - Create a file named `knowledge_base.txt` in the same directory as this script.
#    - Paste all of your iOS developer notes into this file.
#
# 5. CONFIGURE AUDIO DEVICES:
#    - Run the script. If the default audio devices don't work, an error will appear
#      in the transcript window.
#    - Click the "⚙️ Settings" button.
#    - Select your microphone and the correct loopback/stereo mix device from the dropdowns.
#    - You can also now select the AI Analysis Model from this dialog.
#    - Click "OK". The settings will be applied. Audio will restart if devices were changed.
#
# 6. RUN THE SCRIPT:
#    python copilot.py
#
# ==================================================================================================

# --- PyQt6 must be imported first ---
from PyQt6.QtWidgets import (QApplication, QMainWindow, QTextEdit, QPushButton,
                             QVBoxLayout, QWidget, QHBoxLayout, QLabel, QProgressBar,
                             QDialog, QComboBox, QDialogButtonBox, QFormLayout)
from PyQt6.QtCore import QThread, pyqtSignal, Qt
from PyQt6.QtGui import QColor, QPalette

import sys, os, threading, queue, io, wave, time
from collections import deque
import numpy as np

# --- 3rd Party Libraries ---
import sounddevice as sd
import webrtcvad
from groq import Groq
from google import genai
from google.genai import types

# ==============================================================================
#   SECTION 3: SCRIPT CODE
# ==============================================================================

# Default audio device names. These can be changed in the settings.
DEFAULT_MIC_NAME = "Microphone (USB Audio Device)"
DEFAULT_SPEAKER_NAME = "Speakers (Realtek(R) Audio)"

# --- Configuration ---
CONFIG = {
    "SAMPLE_RATE": 16000,
    "MY_MIC_AMPLIFICATION_FACTOR": 20, # Should normally be 20, reduced for testing
    "BUFFER_DURATION_SECONDS": 60,
    "VAD_AGGRESSIVENESS": 3,
    "VAD_FRAME_MS": 30,
    "WINDOW_OPACITY": 0.9,
    # "TRANSCRIPTION_MODEL": "distil-whisper-large-v3-en",
    "TRANSCRIPTION_MODEL": "whisper-large-v3",
    "ANALYSIS_MODEL": "gemini-2.5-flash", # Default model, can be changed in UI settings
    "AVAILABLE_ANALYSIS_MODELS": [
        "gemini-2.5-flash",
        "gemini-2.5-pro",
        "gemini-2.5-flash-lite-preview-06-17"
    ],
    "KNOWLEDGE_BASE_FILE": "knowledge_base.txt"
}
CONFIG["BLOCK_SIZE"] = int(CONFIG["SAMPLE_RATE"] * (CONFIG["VAD_FRAME_MS"] / 1000.0))

# --- Load API Keys ---
try:
    GROQ_API_KEY, GOOGLE_API_KEY = os.environ["GROQ_API_KEY"], os.environ["GOOGLE_API_KEY"]
except KeyError as e:
    print(f"FATAL ERROR: Environment variable {e} not set."); sys.exit(1)

# --- Audio/Worker Threads ---
class AudioRecorder(threading.Thread):
    def __init__(self, device_index, audio_queue):
        super().__init__(daemon=True)
        self.device_index, self.audio_queue, self._running = device_index, audio_queue, threading.Event()
        self._running.set()

    def stop(self):
        self._running.clear()

    def _callback(self, indata, frames, time, status):
        if status: print(f"Audio Stream Status: {status}", file=sys.stderr)
        self.audio_queue.put(indata.copy())
    def run(self):
        try:
            with sd.InputStream(samplerate=CONFIG["SAMPLE_RATE"], blocksize=CONFIG["BLOCK_SIZE"], device=self.device_index, channels=1, dtype='int16', callback=self._callback) as stream:
                while self._running.is_set(): sd.sleep(100)
        except Exception as e:
            print(f"FATAL ERROR in audio thread for device {self.device_index}: {e}")

class TranscriptionWorker(QThread):
    transcript_ready = pyqtSignal(str); error = pyqtSignal(str)
    def __init__(self, my_audio_data, interviewer_audio_data, start_time):
        super().__init__()
        self.my_audio_data = my_audio_data
        self.interviewer_audio_data = interviewer_audio_data
        self.groq_client = Groq(api_key=GROQ_API_KEY)
        self.start_time = start_time
    def run(self):
        try:
            now = time.time(); print(f"[PROFILING] {now - self.start_time:.3f}s - Starting VAD processing...")
            my_speech_times = self._get_speech_timestamps(self.my_audio_data)
            now = time.time(); print(f"[PROFILING] {now - self.start_time:.3f}s - VAD 'Me' complete.")
            interviewer_speech_times = self._get_speech_timestamps(self.interviewer_audio_data)
            now = time.time(); print(f"[PROFILING] {now - self.start_time:.3f}s - VAD 'Interviewer' complete.")
            merged_audio_file = self._merge_audio(self.my_audio_data, self.interviewer_audio_data)
            now = time.time(); print(f"[PROFILING] {now - self.start_time:.3f}s - Audio merged. Starting transcription...")
            transcript_segments = self._transcribe(merged_audio_file)
            now = time.time(); print(f"[PROFILING] {now - self.start_time:.3f}s - Transcription complete. Starting diarization...")
            labeled_transcript = self._diarize(transcript_segments, my_speech_times, interviewer_speech_times)
            now = time.time(); print(f"[PROFILING] {now - self.start_time:.3f}s - Diarization complete. Emitting transcript.")
            self.transcript_ready.emit(labeled_transcript)
        except Exception as e: self.error.emit(f"Transcription Error: {e}")
    def _get_speech_timestamps(self, audio_data):
        vad, sr = webrtcvad.Vad(CONFIG["VAD_AGGRESSIVENESS"]), CONFIG["SAMPLE_RATE"]
        fd_ms, bps = CONFIG["VAD_FRAME_MS"], 2; f_size = int(sr * (fd_ms / 1000.0) * bps)
        ts, is_s, s_start = [], False, 0
        for i in range(0, len(audio_data), f_size):
            frame = audio_data[i:i+f_size]
            if len(frame) < f_size: break
            try:
                if vad.is_speech(frame, sr):
                    if not is_s: is_s, s_start = True, (i / bps) / sr
                elif is_s: is_s = False; ts.append((s_start, (i / bps) / sr))
            except: continue
        if is_s: ts.append((s_start, (len(audio_data) / bps) / sr))
        return ts
    @staticmethod
    def _merge_audio(my_data, int_data):
        my_np, int_np = np.frombuffer(my_data, dtype=np.int16), np.frombuffer(int_data, dtype=np.int16)
        min_len = min(len(my_np), len(int_np)); merged_np = np.clip((my_np[:min_len].astype(np.float32) + int_np[:min_len].astype(np.float32)) / 2, -32768, 32767).astype(np.int16)
        mem_file = io.BytesIO()
        with wave.open(mem_file, 'wb') as wf: wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(CONFIG["SAMPLE_RATE"]); wf.writeframes(merged_np.tobytes())
        mem_file.seek(0); return mem_file
    def _transcribe(self, audio_file): return self.groq_client.audio.transcriptions.create(file=("interview.wav", audio_file.read()), model=CONFIG["TRANSCRIPTION_MODEL"], language="en", response_format="verbose_json", temperature=0.0).segments
    def _diarize(self, segments, my_times, int_times):
        txt = ""
        for seg in segments:
            s_start, s_end = seg['start'], seg['end']
            my_o = sum(max(0, min(s_end, t_e) - max(s_start, t_s)) for t_s, t_e in my_times)
            int_o = sum(max(0, min(s_end, t_e) - max(s_start, t_s)) for t_s, t_e in int_times)
            spk = "Interviewer" if my_o < int_o + 0.1 else "Me"; txt += f"**{spk}:** {seg['text'].strip()}\n\n"
        return txt

class GeminiWorker(QThread):
    chunk_received, analysis_finished, error = pyqtSignal(str), pyqtSignal(), pyqtSignal(str)
    def __init__(self, prompt, start_time):
        super().__init__()
        self.prompt = prompt
        self.start_time = start_time
        self.first_chunk_logged = False
    def run(self):
        try:
            now = time.time(); print(f"[PROFILING] {now - self.start_time:.3f}s - Gemini: Calling API...")
            client = genai.Client(api_key=GOOGLE_API_KEY); contents = [types.Content(role="user", parts=[types.Part.from_text(text=self.prompt)])]

            # Dynamically set thinking_budget based on the selected model
            current_model = CONFIG["ANALYSIS_MODEL"]
            thinking_budget = 128 if "gemini-2.5-pro" in current_model else 0
            if thinking_budget > 0: print(f"Using thinking_budget={thinking_budget} for {current_model}")

            generate_content_config = types.GenerateContentConfig(
                thinking_config=types.ThinkingConfig(
                    thinking_budget=thinking_budget,
                ),
                response_mime_type="text/plain",
            )
            stream = client.models.generate_content_stream(model=current_model, contents=contents, config=generate_content_config)
            for chunk in stream:
                if chunk.text:
                    if not self.first_chunk_logged:
                        now = time.time(); print(f"[PROFILING] {now - self.start_time:.3f}s - Gemini: FIRST CHUNK RECEIVED!")
                        self.first_chunk_logged = True
                    self.chunk_received.emit(chunk.text)
        except Exception as e: self.error.emit(f"Gemini Error: {e}")
        finally: self.analysis_finished.emit()

# --- Settings Dialog ---
class SettingsDialog(QDialog):
    def __init__(self, parent=None, current_mic="", current_loopback="", current_model=""):
        super().__init__(parent)
        self.setWindowTitle("Settings")
        self.setModal(True)
        self.mic_name, self.loopback_name, self.analysis_model = None, None, None

        # --- Device Discovery ---
        self.mics, self.loopbacks = self._get_devices()

        # --- UI Elements ---
        self.mic_combo = QComboBox()
        self.mic_combo.addItems([name for name, idx in self.mics])
        if current_mic in [name for name, idx in self.mics]:
            self.mic_combo.setCurrentText(current_mic)

        self.loopback_combo = QComboBox()
        self.loopback_combo.addItems([name for name, idx in self.loopbacks])
        if current_loopback in [name for name, idx in self.loopbacks]:
            self.loopback_combo.setCurrentText(current_loopback)

        self.model_combo = QComboBox()
        self.model_combo.addItems(CONFIG["AVAILABLE_ANALYSIS_MODELS"])
        if current_model in CONFIG["AVAILABLE_ANALYSIS_MODELS"]:
            self.model_combo.setCurrentText(current_model)

        # --- Layout ---
        layout = QFormLayout(self)
        layout.addRow("Your Microphone:", self.mic_combo)
        layout.addRow("Interviewer's Audio (Loopback):", self.loopback_combo)
        layout.addRow("AI Analysis Model:", self.model_combo)

        # --- Buttons ---
        self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
        self.button_box.accepted.connect(self.accept)
        self.button_box.rejected.connect(self.reject)
        layout.addWidget(self.button_box)

    def _get_devices(self):
        devices = sd.query_devices()
        mics = [(d['name'], i) for i, d in enumerate(devices) if d['max_input_channels'] > 0]
        # A bit more robust loopback detection
        loopbacks = [(d['name'], i) for i, d in enumerate(devices) if d['max_input_channels'] > 0 and
                     ('loopback' in d['name'].lower() or
                      'stereo mix' in d['name'].lower() or
                      'what u hear' in d['name'].lower() or
                      'monitor' in d['name'].lower())]
        # Add all input devices as a fallback for loopback if none are found
        if not loopbacks:
            loopbacks = mics
        return mics, loopbacks

    def accept(self):
        self.mic_name = self.mic_combo.currentText()
        self.loopback_name = self.loopback_combo.currentText()
        self.analysis_model = self.model_combo.currentText()
        super().accept()

# --- Main Application UI ---
class InterviewCopilot(QMainWindow):
    volume_updated = pyqtSignal(str, int)
    def __init__(self):
        super().__init__()
        self.is_analyzing, self.knowledge_base_content = False, ""
        self.analysis_start_time = 0
        self.my_mic_name = DEFAULT_MIC_NAME
        self.interviewer_loopback_name = DEFAULT_SPEAKER_NAME
        self.my_mic_device_idx, self.interviewer_loopback_device_idx = None, None
        self.my_recorder_thread, self.interviewer_recorder_thread = None, None

        self.setWindowTitle("iOS Interview Co-pilot (v1.9 - Final)"); self.setGeometry(100, 100, 1000, 700); self.setWindowOpacity(CONFIG["WINDOW_OPACITY"])
        self.setup_ui()
        self.load_knowledge_base()
        self.setup_and_start_audio()

    def setup_ui(self):
        central_widget = QWidget(); self.setCentralWidget(central_widget); main_layout = QVBoxLayout(central_widget)
        volume_layout = QHBoxLayout(); me_volume_layout = QVBoxLayout(); interviewer_volume_layout = QVBoxLayout()
        me_volume_layout.addWidget(QLabel("Me (Mic Activity)")); self.me_volume_bar = QProgressBar(); self.me_volume_bar.setRange(0, 100); self.me_volume_bar.setTextVisible(False); me_volume_layout.addWidget(self.me_volume_bar)
        interviewer_volume_layout.addWidget(QLabel("Interviewer (Speaker Activity)")); self.interviewer_volume_bar = QProgressBar(); self.interviewer_volume_bar.setRange(0, 100); self.interviewer_volume_bar.setTextVisible(False); interviewer_volume_layout.addWidget(self.interviewer_volume_bar)
        volume_layout.addLayout(me_volume_layout); volume_layout.addLayout(interviewer_volume_layout); main_layout.addLayout(volume_layout)
        content_layout = QHBoxLayout(); left_layout = QVBoxLayout(); right_layout = QVBoxLayout()
        left_layout.addWidget(QLabel("Live Transcript")); self.scribe_window = QTextEdit(); self.scribe_window.setReadOnly(True); self.scribe_window.setMarkdown("**Ready to start...**"); left_layout.addWidget(self.scribe_window)
        right_layout.addWidget(QLabel("AI Assistant Notes")); self.prompter_window = QTextEdit(); self.prompter_window.setReadOnly(True); self.prompter_window.setMarkdown("*Your cheat sheet will appear here...*"); right_layout.addWidget(self.prompter_window)
        content_layout.addLayout(left_layout, 33); content_layout.addLayout(right_layout, 67); main_layout.addLayout(content_layout)
        button_layout = QHBoxLayout()
        self.analyze_button = QPushButton(f"Analyze Last {int(CONFIG['BUFFER_DURATION_SECONDS']/60)} Minutes")
        self.analyze_button.clicked.connect(self.on_analyze_triggered)
        self.save_button = QPushButton("Save Debug Audio")
        self.save_button.clicked.connect(self.save_debug_audio)
        self.settings_button = QPushButton("⚙️ Settings")
        self.settings_button.setToolTip("Open Audio Settings")
        self.settings_button.clicked.connect(self.open_settings_dialog)
        button_layout.addWidget(self.analyze_button)
        button_layout.addWidget(self.save_button)
        button_layout.addWidget(self.settings_button)
        main_layout.addLayout(button_layout)
        palette = self.palette(); palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53)); palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white); palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25)); palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.white); palette.setColor(QPalette.ColorRole.Button, QColor(200, 200, 200)); palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.black); self.setPalette(palette)
        self.volume_updated.connect(self.update_volume_meter)

    def load_knowledge_base(self):
        try:
            with open(CONFIG["KNOWLEDGE_BASE_FILE"], 'r', encoding='utf-8') as f: self.knowledge_base_content = f.read()
            if not self.knowledge_base_content: self.scribe_window.append("\n**WARNING:** 'knowledge_base.txt' is empty!")
            print(f"Knowledge base loaded ({len(self.knowledge_base_content)} chars).")
        except FileNotFoundError: self.scribe_window.append(f"\n**FATAL ERROR:** '{CONFIG['KNOWLEDGE_BASE_FILE']}' not found.")

    def update_volume_meter(self, speaker_name, volume): self.me_volume_bar.setValue(volume) if speaker_name == "Me" else self.interviewer_volume_bar.setValue(volume)

    def setup_and_start_audio(self):
        try:
            self.my_mic_device_idx, self.interviewer_loopback_device_idx = find_sounddevice_indices(
                self.my_mic_name, self.interviewer_loopback_name
            )
            self.start_recording()
        except Exception as e:
            self.scribe_window.append(f"\n**AUDIO ERROR:** {e}\n\nPlease check your audio devices in **⚙️ Settings**.")
            print(f"Initial audio setup failed: {e}")

    def open_settings_dialog(self):
        dialog = SettingsDialog(self, self.my_mic_name, self.interviewer_loopback_name, CONFIG["ANALYSIS_MODEL"])
        if dialog.exec():
            new_mic, new_loopback, new_model = dialog.mic_name, dialog.loopback_name, dialog.analysis_model

            # Check for changes and update accordingly
            audio_changed = new_mic != self.my_mic_name or new_loopback != self.interviewer_loopback_name
            model_changed = new_model != CONFIG["ANALYSIS_MODEL"]

            if model_changed:
                print(f"New analysis model selected: '{new_model}'")
                CONFIG["ANALYSIS_MODEL"] = new_model
                self.scribe_window.append(f"\n*AI Analysis Model set to: **{new_model}***")

            if audio_changed:
                print(f"New audio settings selected. Mic: '{new_mic}', Loopback: '{new_loopback}'")
                self.my_mic_name = new_mic
                self.interviewer_loopback_name = new_loopback
                self.scribe_window.append("\n*Audio settings changed. Restarting recording...*")
                self.restart_recording()

    def stop_recording(self):
        print("Stopping recording threads...")
        if self.my_recorder_thread and self.my_recorder_thread.is_alive():
            self.my_recorder_thread.stop()
            self.my_recorder_thread.join(timeout=1)
        if self.interviewer_recorder_thread and self.interviewer_recorder_thread.is_alive():
            self.interviewer_recorder_thread.stop()
            self.interviewer_recorder_thread.join(timeout=1)
        self.my_recorder_thread, self.interviewer_recorder_thread = None, None
        print("Recording stopped.")

    def restart_recording(self):
        self.stop_recording()
        self.setup_and_start_audio()

    def start_recording(self):
        buffer_size_samples = int(CONFIG["SAMPLE_RATE"] * CONFIG["BUFFER_DURATION_SECONDS"])
        self.my_audio_buffer = deque(maxlen=buffer_size_samples * 2)
        self.interviewer_audio_buffer = deque(maxlen=buffer_size_samples * 2)
        self.my_audio_queue, self.interviewer_audio_queue = queue.Queue(), queue.Queue()
        self.my_recorder_thread = AudioRecorder(self.my_mic_device_idx, self.my_audio_queue)
        self.interviewer_recorder_thread = AudioRecorder(self.interviewer_loopback_device_idx, self.interviewer_audio_queue)
        self.my_recorder_thread.start()
        self.interviewer_recorder_thread.start()
        threading.Thread(target=self._fill_buffer, args=(self.my_audio_queue, self.my_audio_buffer, "Me"), daemon=True).start()
        threading.Thread(target=self._fill_buffer, args=(self.interviewer_audio_queue, self.interviewer_audio_buffer, "Interviewer"), daemon=True).start()
        print("Recording started.")

    def _fill_buffer(self, q, buffer, speaker_name):
        while True:
            data_np = q.get()
            if speaker_name == "Me":
                amp_factor = CONFIG["MY_MIC_AMPLIFICATION_FACTOR"]
                if amp_factor != 1.0:
                    amplified_np = np.clip(data_np.astype(np.float32) * amp_factor, -32768, 32767).astype(np.int16)
                    buffer.extend(amplified_np.tobytes())
                    data_np = amplified_np # Use amplified data for volume calculation
                else: buffer.extend(data_np.tobytes())
            else: buffer.extend(data_np.tobytes())
            rms = np.sqrt(np.mean(data_np.astype(np.float64)**2)) if data_np.size > 0 else 0
            self.volume_updated.emit(speaker_name, int(min(1.0, rms / 3000) * 100))

    def save_debug_audio(self):
        print("Saving debug audio files... (Buffer is already amplified)")
        my_data, int_data = bytes(self.my_audio_buffer), bytes(self.interviewer_audio_buffer)
        self._write_wav_file("debug_me.wav", my_data); self._write_wav_file("debug_interviewer.wav", int_data)
        if my_data and int_data: self._write_wav_file("debug_merged.wav", TranscriptionWorker._merge_audio(my_data, int_data).read())
        self.scribe_window.append("\n*Debug audio files saved.*"); print(" -> Debug files saved.")

    def _write_wav_file(self, filename, audio_data):
        with wave.open(filename, 'wb') as wf: wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(CONFIG["SAMPLE_RATE"]); wf.writeframes(audio_data)

    def on_analyze_triggered(self):
        if self.is_analyzing: return
        self.is_analyzing = True
        self.analysis_start_time = time.time()
        print(f"\n[PROFILING] {self.analysis_start_time - self.analysis_start_time:.3f}s - Analysis triggered. Capturing audio buffers...")
        self.analyze_button.setText("Analyzing..."); self.analyze_button.setEnabled(False)
        self.scribe_window.setMarkdown("*Processing audio...*"); self.prompter_window.setMarkdown("*Generating notes...*")
        my_data, interviewer_data = bytes(self.my_audio_buffer), bytes(self.interviewer_audio_buffer)
        self.transcription_worker = TranscriptionWorker(my_data, interviewer_data, self.analysis_start_time)
        self.transcription_worker.transcript_ready.connect(self.on_transcript_received); self.transcription_worker.error.connect(self.on_worker_error); self.transcription_worker.start()

    def on_transcript_received(self, transcript):
        now = time.time(); print(f"[PROFILING] {now - self.analysis_start_time:.3f}s - Transcript received. Triggering Gemini analysis...")
        self.scribe_window.setMarkdown(transcript); self.trigger_gemini_analysis(transcript)
    def trigger_gemini_analysis(self, transcript):
        prompt = f"""
-- KNOWLEDGE BASE START ---
{self.knowledge_base_content}
-- KNOWLEDGE BASE END ---
You are a world-class AI assistant for an experienced iOS Developer who is in a job interview RIGHT NOW. Your task is to act as an instant "cheat sheet" retriever.
Above is the developer's personal, comprehensive knowledge base on iOS development.
Below is the most recent {CONFIG["BUFFER_DURATION_SECONDS"]} seconds of transcript of the interview conversation that is happening right now.
Transcription is done by Whisper-v3, so it is NOT very accurate, and may(and sometimes will) contain errors, especially when it comes to technical terms (e.g. "Combine" may be transcribed as "combined", "SwiftUI" as "swift UI", etc., or even more severe and less obvious mistakes).
--- TRANSCRIPT START ---
{transcript}
--- TRANSCRIPT END ---
The interviewer has just asked their last question.
Based on the interviewer's final question and the immediate context, find the most relevant, concise Q A D block from the KNOWLEDGE BASE.
Your output MUST be **Conversational, but not too much:** You can see that the KNOWLEDGE BASE is a bit conversational already, but not everywhere and sometimes lacks it. Your task is to **PERFECTLY MIMIC** the conversational style that is **already present** in the KNOWLEDGE BASE, but add more of **same** conversational style where needed. Add some conversational words where needed(e.g. "Well...", "So..." in the beginning of an answer, or "also", "and" or others that fit well in between sentences to "glue" them and make the answer "seem" more natural after the "." sign pause), at the same time all explanations should remain same. The answer that you will provide is supposed to be spoken "as is" live on the interview, so it shouldn't feel robotic or AI-generated.
So basically I expect you to provide me a relevant "A" block, but slightly reworked as I've described just above.
Answer should not contain any bullet points.
Answer should naturally continue the conversation based on the interviewer's final question & immediate context.
See the question about differences between classes and structs in the KNOWLEDGE BASE? That's a good example of conversational style.
See the question about Encapsulation? The answer for it looks not conversational enough. Here's the example of what I would expect from you as an output:
---
Encapsulation is all about access control modifiers, so we can apply them to a method, or a property, or the entire object like class, struct or enum. And how that works is very simple: so if something is private – it is accessible only in current scope. And if it's fileprivate – it's accessible in the current scope + the current file. And if it's internal — same story as fileprivate, but + the current module. And if it's public - again, same story as internal, but + all other modules in the project. And open is same as public but it also allows inheritance.
And internal encapsulation is the default one. What else can I tell... Also things that are public or open – they are included in the protocol most of the time, because they need to be accessed somehow.
And encapsulation can be very flexible, for example we can make some property public private(set), so it will be public for getting but private for setting.
---
"""
        self.prompter_window.clear()
        self.gemini_worker = GeminiWorker(prompt, self.analysis_start_time)
        self.gemini_worker.chunk_received.connect(self.prompter_window.insertPlainText)
        self.gemini_worker.analysis_finished.connect(self.on_analysis_finished); self.gemini_worker.error.connect(self.on_worker_error); self.gemini_worker.start()

    def on_analysis_finished(self):
        now = time.time(); print(f"[PROFILING] {now - self.analysis_start_time:.3f}s - Gemini: Full analysis complete. UI reset.")
        self.is_analyzing = False; self.analyze_button.setText(f"Analyze Last {int(CONFIG['BUFFER_DURATION_SECONDS']/60)} Minutes"); self.analyze_button.setEnabled(True)
    def on_worker_error(self, error_message):
        if self.is_analyzing:
            now = time.time(); print(f"[PROFILING] {now - self.analysis_start_time:.3f}s - WORKER ERROR OCCURRED.")
        print(f"ERROR: {error_message}"); self.prompter_window.setMarkdown(f"**An error occurred:**\n\n{error_message}"); self.on_analysis_finished()

# --- Script Execution Logic ---
def find_sounddevice_indices(mic_name, speaker_name):
    print(f"Searching for audio devices: Mic='{mic_name}', Speaker='{speaker_name}'")
    devices = sd.query_devices()
    mic_idx, loopback_idx = None, None

    # Find Mic by exact name first, then by substring
    for i, d in enumerate(devices):
        if mic_name == d['name'] and d['max_input_channels'] > 0: mic_idx = i; break
    if mic_idx is None:
        for i, d in enumerate(devices):
            if mic_name in d['name'] and d['max_input_channels'] > 0: mic_idx = i; break

    # Find Loopback device by exact name, then by substring/keywords
    for i, d in enumerate(devices):
        if speaker_name == d['name'] and d['max_input_channels'] > 0: loopback_idx = i; break
    if loopback_idx is None:
        for i, d in enumerate(devices):
            if speaker_name in d['name'] and d['max_input_channels'] > 0: loopback_idx = i; break
    
    # Fallback to keyword search if specific name not found
    if loopback_idx is None:
        print("Specific loopback device not found, checking for common names...")
        speaker_name_lower = speaker_name.lower()
        for i, d in enumerate(devices):
            if d['max_input_channels'] > 0 and i != mic_idx and ('loopback' in d['name'].lower() or 'stereo mix' in d['name'].lower() or 'what u hear' in d['name'].lower()):
                loopback_idx = i; break
        if loopback_idx is None:
            for i, d in enumerate(devices):
                if d['max_input_channels'] > 0 and i != mic_idx and f"monitor of {speaker_name_lower}" in d['name'].lower():
                    loopback_idx = i; break
    
    if mic_idx is None: raise RuntimeError(f"Could not find Mic '{mic_name}'")
    if loopback_idx is None: raise RuntimeError(f"Could not find a loopback device for '{speaker_name}'.\n -> TIP: Try another device in settings. Ensure 'Stereo Mix' or similar is enabled in OS Sound settings.")
    
    print(f"  [OK] Found Mic: '{devices[mic_idx]['name']}' at index {mic_idx}")
    print(f"  [OK] Found Loopback: '{devices[loopback_idx]['name']}' at index {loopback_idx}")
    return (mic_idx, loopback_idx)

if __name__ == '__main__':
    try:
        app = QApplication(sys.argv)
        copilot = InterviewCopilot()
        copilot.show()
        sys.exit(app.exec())
    except Exception as e:
        # This will catch critical errors during QApplication setup, etc.
        # Most app-specific errors are now handled inside the main window.
        print(f"\nFATAL UNHANDLED ERROR: {e}")
        # A simple fallback error dialog if PyQt can even run
        try:
            app = QApplication(sys.argv)
            error_dialog = QTextEdit(f"<h1>Application Failed to Start</h1><hr><pre>{e}</pre>")
            error_dialog.setWindowTitle("Fatal Error"); error_dialog.setReadOnly(True); error_dialog.resize(600, 200); error_dialog.show()
            sys.exit(app.exec())
        except:
             # If QApplication itself fails, just exit.
             sys.exit(1)


Right now script is using configs predefined in the beginning of the script, but some configs are already migrated to the UI.
I wanna progressively migrate this way of configuring to a UI-based settings.
What I wanna make UI-configurable just for now:
Selection of 
```
    # "TRANSCRIPTION_MODEL": "whisper-large-v3-turbo",
    # "TRANSCRIPTION_MODEL": "distil-whisper-large-v3-en",
    "TRANSCRIPTION_MODEL": "whisper-large-v3",
```
from list of 3 available models.
Then also add the following functionality:
Logged time "from request to response" for both Groq and Gemini APIs for last 50 queries should be saved into project root as json, xml or any other format that you think will be the most appropriate. The AI model used for the request should also be specified.
Each time I click "Analyze last X minutes" button, script should save to the root project directory 6 following graphs (for 3 Groq models and 3 Gemini models) for both Groq and Gemini APIs:
By X there should be the number of the request (1 . . . 50, where 50 is the latest one), and by Y there should be the time required to make a response in seconds. In each graph there also should be specified an Average time-to-response of all 50 requests.
By having those graphs - I will be able to analyze which model type is the fastest one in both Gemini and Groq api.

I expect to receive from you a full new script which will replace the old one.