diff --git a/gui.py b/gui.py deleted file mode 100644 index 4467caf..0000000 --- a/gui.py +++ /dev/null @@ -1,3962 +0,0 @@ -# new_gui.py - -import os -import json -import requests -import threading -import math -import random -from datetime import datetime -from io import BytesIO -from PIL import Image -import time -from pathlib import Path -import uuid -import shutil -import networkx as nx -import re -import sys -import webbrowser -import base64 -from PyQt6.QtCore import Qt, QRect, QTimer, QRectF, QPointF, QSize, pyqtSignal, QEvent, QPropertyAnimation, QEasingCurve -from PyQt6.QtGui import QFont, QColor, QPainter, QPen, QBrush, QFontDatabase, QTextCursor, QAction, QKeySequence, QTextCharFormat, QLinearGradient, QRadialGradient, QPainterPath, QImage, QPixmap -from PyQt6.QtWidgets import QWidget, QApplication, QMainWindow, QSplitter, QVBoxLayout, QHBoxLayout, QTextEdit, QFrame, QLineEdit, QPushButton, QLabel, QComboBox, QMenu, QFileDialog, QMessageBox, QScrollArea, QToolTip, QSizePolicy, QCheckBox, QGraphicsDropShadowEffect - -from config import ( - AI_MODELS, - SYSTEM_PROMPT_PAIRS, - SHOW_CHAIN_OF_THOUGHT_IN_CONTEXT -) - -# Add import for the HTML viewing functionality -from shared_utils import open_html_in_browser, generate_image_from_text - -# Define global color palette for consistent styling - Cyberpunk theme -COLORS = { - # Backgrounds - darker, moodier - 'bg_dark': '#0A0E1A', # Deep blue-black - 'bg_medium': '#111827', # Slate dark - 'bg_light': '#1E293B', # Lighter slate - - # Primary accents - neon but muted - 'accent_cyan': '#06B6D4', # Cyan (primary) - 'accent_cyan_hover': '#0891B2', - 'accent_cyan_active': '#0E7490', - - # Secondary accents - 'accent_pink': '#EC4899', # Hot pink (secondary) - 'accent_purple': '#A855F7', # Purple (tertiary) - 'accent_yellow': '#FBBF24', # Amber for warnings - 'accent_green': '#10B981', # Emerald (rabbithole) - - # Text colors - 'text_normal': '#CBD5E1', # Slate-200 - 'text_dim': '#64748B', # Slate-500 - 'text_bright': '#F1F5F9', # Slate-50 - 'text_glow': '#38BDF8', # Sky-400 (glowing text) - 'text_error': '#EF4444', # Red-500 - - # Borders and effects - 'border': '#1E293B', # Slate-800 - 'border_glow': '#06B6D4', # Glowing cyan borders - 'border_highlight': '#334155', # Slate-700 - 'shadow': 'rgba(6, 182, 212, 0.2)', # Cyan glow shadows - - # Legacy color mappings for compatibility - 'accent_blue': '#06B6D4', # Map old blue to cyan - 'accent_blue_hover': '#0891B2', - 'accent_blue_active': '#0E7490', - 'accent_orange': '#F59E0B', # Amber-500 - 'chain_of_thought': '#10B981', # Emerald - 'user_header': '#06B6D4', # Cyan - 'ai_header': '#A855F7', # Purple - 'system_message': '#F59E0B', # Amber -} - - -def apply_glow_effect(widget, color, blur_radius=15, offset=(0, 2)): - """Apply a glowing drop shadow effect to a widget""" - shadow = QGraphicsDropShadowEffect() - shadow.setBlurRadius(blur_radius) - shadow.setColor(QColor(color)) - shadow.setOffset(offset[0], offset[1]) - widget.setGraphicsEffect(shadow) - return shadow - - -class GlowButton(QPushButton): - """Enhanced button with glow effect on hover""" - - def __init__(self, text, glow_color=COLORS['accent_cyan'], parent=None): - super().__init__(text, parent) - self.glow_color = glow_color - self.base_blur = 8 - self.hover_blur = 20 - - # Create shadow effect - self.shadow = QGraphicsDropShadowEffect() - self.shadow.setBlurRadius(self.base_blur) - self.shadow.setColor(QColor(glow_color)) - self.shadow.setOffset(0, 2) - self.setGraphicsEffect(self.shadow) - - # Track hover state for animation - self.setMouseTracking(True) - - def enterEvent(self, event): - """Increase glow on hover""" - self.shadow.setBlurRadius(self.hover_blur) - self.shadow.setColor(QColor(self.glow_color)) - super().enterEvent(event) - - def leaveEvent(self, event): - """Decrease glow when not hovering""" - self.shadow.setBlurRadius(self.base_blur) - super().leaveEvent(event) - -# Load custom fonts -def load_fonts(): - """Load custom fonts for the application""" - font_dir = Path("fonts") - font_dir.mkdir(exist_ok=True) - - # List of fonts to load - these would need to be included with the application - fonts = [ - ("IosevkaTerm-Regular.ttf", "Iosevka Term"), - ("IosevkaTerm-Bold.ttf", "Iosevka Term"), - ("IosevkaTerm-Italic.ttf", "Iosevka Term"), - ] - - loaded_fonts = [] - for font_file, font_name in fonts: - font_path = font_dir / font_file - if font_path.exists(): - font_id = QFontDatabase.addApplicationFont(str(font_path)) - if font_id >= 0: - if font_name not in loaded_fonts: - loaded_fonts.append(font_name) - print(f"Loaded font: {font_name} from {font_file}") - else: - print(f"Failed to load font: {font_file}") - else: - print(f"Font file not found: {font_path}") - - return loaded_fonts - - -# ═══════════════════════════════════════════════════════════════════════════════ -# ATMOSPHERIC EFFECT WIDGETS -# ═══════════════════════════════════════════════════════════════════════════════ - -class DepthGauge(QWidget): - """Vertical gauge showing conversation depth/turn progress""" - - def __init__(self, parent=None): - super().__init__(parent) - self.current_turn = 0 - self.max_turns = 10 - self.setFixedWidth(24) - self.setMinimumHeight(100) - - # Animation - self.pulse_offset = 0 - self.pulse_timer = QTimer(self) - self.pulse_timer.timeout.connect(self._animate_pulse) - self.pulse_timer.start(50) - - def _animate_pulse(self): - self.pulse_offset = (self.pulse_offset + 2) % 360 - self.update() - - def set_progress(self, current, maximum): - """Update the gauge progress""" - self.current_turn = current - self.max_turns = max(maximum, 1) - self.update() - - def paintEvent(self, event): - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - - w, h = self.width(), self.height() - margin = 4 - gauge_width = w - margin * 2 - gauge_height = h - margin * 2 - - # Background track - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(QColor(COLORS['bg_dark'])) - painter.drawRoundedRect(margin, margin, gauge_width, gauge_height, 4, 4) - - # Border - painter.setPen(QPen(QColor(COLORS['border_glow']), 1)) - painter.setBrush(Qt.BrushStyle.NoBrush) - painter.drawRoundedRect(margin, margin, gauge_width, gauge_height, 4, 4) - - # Calculate fill height (fills from bottom to top) - progress = min(self.current_turn / self.max_turns, 1.0) - fill_height = int(gauge_height * progress) - fill_y = margin + gauge_height - fill_height - - if fill_height > 0: - # Gradient fill - gradient = QLinearGradient(0, fill_y, 0, margin + gauge_height) - - # Color shifts based on depth - deeper = more purple/pink - if progress < 0.33: - gradient.setColorAt(0, QColor(COLORS['accent_cyan'])) - gradient.setColorAt(1, QColor(COLORS['accent_cyan']).darker(130)) - elif progress < 0.66: - gradient.setColorAt(0, QColor(COLORS['accent_purple'])) - gradient.setColorAt(1, QColor(COLORS['accent_cyan'])) - else: - gradient.setColorAt(0, QColor(COLORS['accent_pink'])) - gradient.setColorAt(1, QColor(COLORS['accent_purple'])) - - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(gradient) - painter.drawRoundedRect(margin + 2, fill_y, gauge_width - 4, fill_height, 2, 2) - - # Pulsing glow line at top of fill - pulse_alpha = int(100 + 80 * math.sin(math.radians(self.pulse_offset))) - glow_color = QColor(COLORS['accent_cyan']) - glow_color.setAlpha(pulse_alpha) - painter.setPen(QPen(glow_color, 2)) - painter.drawLine(margin + 2, fill_y, margin + gauge_width - 2, fill_y) - - # Turn counter text - painter.setPen(QColor(COLORS['text_dim'])) - font = painter.font() - font.setPixelSize(9) - painter.setFont(font) - text = f"{self.current_turn}" - painter.drawText(self.rect(), Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, text) - - -class SignalIndicator(QWidget): - """Signal strength/latency indicator""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setFixedSize(80, 20) - self.signal_strength = 1.0 # 0.0 to 1.0 - self.latency_ms = 0 - self.is_active = False - - # Animation for activity - self.bar_offset = 0 - self.activity_timer = QTimer(self) - self.activity_timer.timeout.connect(self._animate) - - def _animate(self): - self.bar_offset = (self.bar_offset + 1) % 5 - self.update() - - def set_active(self, active): - """Set whether we're actively waiting for a response""" - self.is_active = active - if active: - self.activity_timer.start(100) - else: - self.activity_timer.stop() - self.update() - - def set_latency(self, latency_ms): - """Update the latency display""" - self.latency_ms = latency_ms - # Calculate signal strength based on latency - if latency_ms < 500: - self.signal_strength = 1.0 - elif latency_ms < 1500: - self.signal_strength = 0.75 - elif latency_ms < 3000: - self.signal_strength = 0.5 - else: - self.signal_strength = 0.25 - self.update() - - def paintEvent(self, event): - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - - # Draw signal bars - bar_heights = [4, 7, 10, 13, 16] - bar_width = 4 - spacing = 2 - start_x = 5 - base_y = 18 - - for i, bar_h in enumerate(bar_heights): - x = start_x + i * (bar_width + spacing) - y = base_y - bar_h - - # Determine if this bar should be lit - threshold = (i + 1) / len(bar_heights) - is_lit = self.signal_strength >= threshold - - if self.is_active: - # Animated pattern when active - is_lit = ((i + self.bar_offset) % 5) < 3 - color = QColor(COLORS['accent_cyan']) if is_lit else QColor(COLORS['bg_light']) - else: - if is_lit: - # Color based on signal strength - if self.signal_strength > 0.7: - color = QColor(COLORS['accent_green']) - elif self.signal_strength > 0.4: - color = QColor(COLORS['accent_yellow']) - else: - color = QColor(COLORS['accent_pink']) - else: - color = QColor(COLORS['bg_light']) - - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(color) - painter.drawRoundedRect(x, y, bar_width, bar_h, 1, 1) - - # Draw latency text - painter.setPen(QColor(COLORS['text_dim'])) - font = painter.font() - font.setPixelSize(9) - painter.setFont(font) - - if self.is_active: - text = "···" - elif self.latency_ms > 0: - text = f"{self.latency_ms}ms" - else: - text = "IDLE" - - painter.drawText(40, 3, 40, 16, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, text) - - -class NetworkGraphWidget(QWidget): - nodeSelected = pyqtSignal(str) - nodeHovered = pyqtSignal(str) - - def __init__(self): - super().__init__() - - # Graph data - self.nodes = [] - self.edges = [] - self.node_positions = {} - self.node_colors = {} - self.node_labels = {} - self.node_sizes = {} - - # Edge animation data - self.growing_edges = {} # Dictionary to track growing edges: {(source, target): growth_progress} - self.edge_growth_speed = 0.05 # Increased speed of edge growth animation (was 0.02) - - # Visual settings - self.margin = 50 - self.selected_node = None - self.hovered_node = None - self.animation_progress = 0 - self.animation_timer = QTimer(self) - self.animation_timer.timeout.connect(self.update_animation) - self.animation_timer.start(50) # 20 FPS animation - - # Mycelial node settings - self.hyphae_count = 5 # Number of hyphae per node - self.hyphae_length_factor = 0.4 # Length of hyphae relative to node radius - self.hyphae_variation = 0.3 # Random variation in hyphae - - # Node colors - use global color palette with mycelial theme - self.node_colors_by_type = { - 'main': '#8E9DCC', # Soft blue-purple - 'rabbithole': '#7FB069', # Soft green - 'fork': '#F2C14E', # Soft yellow - 'branch': '#F78154' # Soft orange - } - - # Collision dynamics - self.node_velocities = {} # Store velocities for each node - self.repulsion_strength = 0.5 # Strength of repulsion between nodes - self.attraction_strength = 0.1 # Strength of attraction along edges - self.damping = 0.8 # Damping factor to prevent oscillation - self.apply_physics = True # Toggle for physics simulation - - # Set up the widget - self.setMinimumSize(300, 300) - self.setMouseTracking(True) - - def add_edge(self, source, target): - """Add an edge with growth animation""" - if (source, target) not in self.edges: - self.edges.append((source, target)) - # Initialize edge growth at 0 - self.growing_edges[(source, target)] = 0.0 - # Force update to start animation immediately - self.update() - - def update_animation(self): - """Update animation state""" - self.animation_progress = (self.animation_progress + 0.05) % 1.0 - - # Update growing edges - edges_to_remove = [] - has_growing_edges = False - - for edge, progress in self.growing_edges.items(): - if progress < 1.0: - self.growing_edges[edge] = min(progress + self.edge_growth_speed, 1.0) - has_growing_edges = True - else: - # Mark fully grown edges for removal from animation tracking - edges_to_remove.append(edge) - - # Remove fully grown edges from tracking - for edge in edges_to_remove: - if edge in self.growing_edges: - self.growing_edges.pop(edge) - - # Apply collision dynamics if enabled - if self.apply_physics and len(self.nodes) > 1: - self.apply_collision_dynamics() - - # Update the widget - self.update() - - def apply_collision_dynamics(self): - """Apply collision dynamics to prevent node overlap""" - # Initialize velocities if needed - for node_id in self.nodes: - if node_id not in self.node_velocities: - self.node_velocities[node_id] = (0, 0) - - # Calculate repulsive forces between nodes - new_velocities = {} - for node_id in self.nodes: - if node_id not in self.node_positions: - continue - - vx, vy = self.node_velocities.get(node_id, (0, 0)) - x1, y1 = self.node_positions[node_id] - - # Apply repulsion between nodes - for other_id in self.nodes: - if other_id == node_id or other_id not in self.node_positions: - continue - - x2, y2 = self.node_positions[other_id] - - # Calculate distance - dx = x1 - x2 - dy = y1 - y2 - distance = max(0.1, math.sqrt(dx*dx + dy*dy)) # Avoid division by zero - - # Get node sizes - size1 = math.sqrt(self.node_sizes.get(node_id, 400)) - size2 = math.sqrt(self.node_sizes.get(other_id, 400)) - min_distance = (size1 + size2) / 2 - - # Apply repulsive force if nodes are too close - if distance < min_distance * 2: - # Normalize direction vector - nx = dx / distance - ny = dy / distance - - # Calculate repulsion strength (stronger when closer) - strength = self.repulsion_strength * (1.0 - distance / (min_distance * 2)) - - # Apply force - vx += nx * strength - vy += ny * strength - - # Apply attraction along edges - for edge in self.edges: - source, target = edge - - # Skip edges that are still growing - if (source, target) in self.growing_edges and self.growing_edges[(source, target)] < 1.0: - continue - - if source == node_id and target in self.node_positions: - # This node is the source, attract towards target - x2, y2 = self.node_positions[target] - dx = x2 - x1 - dy = y2 - y1 - distance = max(0.1, math.sqrt(dx*dx + dy*dy)) - - # Normalize and apply attraction - vx += (dx / distance) * self.attraction_strength - vy += (dy / distance) * self.attraction_strength - - elif target == node_id and source in self.node_positions: - # This node is the target, attract towards source - x2, y2 = self.node_positions[source] - dx = x2 - x1 - dy = y2 - y1 - distance = max(0.1, math.sqrt(dx*dx + dy*dy)) - - # Normalize and apply attraction - vx += (dx / distance) * self.attraction_strength - vy += (dy / distance) * self.attraction_strength - - # Apply damping to prevent oscillation - vx *= self.damping - vy *= self.damping - - # Store new velocity - new_velocities[node_id] = (vx, vy) - - # Update positions based on velocities - for node_id, (vx, vy) in new_velocities.items(): - if node_id in self.node_positions: - # Skip the main node to keep it centered - if node_id == 'main': - continue - - x, y = self.node_positions[node_id] - self.node_positions[node_id] = (x + vx, y + vy) - - # Update velocities for next frame - self.node_velocities = new_velocities - - def paintEvent(self, event): - """Paint the network graph""" - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - - # Get widget dimensions - width = self.width() - height = self.height() - - # Set background with subtle gradient - gradient = QLinearGradient(0, 0, 0, height) - gradient.setColorAt(0, QColor('#1A1A1E')) # Dark blue-gray - gradient.setColorAt(1, QColor('#0F0F12')) # Darker at bottom - painter.fillRect(0, 0, width, height, gradient) - - # Draw subtle grid lines - painter.setPen(QPen(QColor(COLORS['border']).darker(150), 0.5, Qt.PenStyle.DotLine)) - grid_size = 40 - for x in range(0, width, grid_size): - painter.drawLine(x, 0, x, height) - for y in range(0, height, grid_size): - painter.drawLine(0, y, width, y) - - # Calculate center point and scale factor - center_x = width / 2 - center_y = height / 2 - scale = min(width, height) / 500 - - # Draw edges first so they appear behind nodes - for edge in self.edges: - source, target = edge - if source in self.node_positions and target in self.node_positions: - src_x, src_y = self.node_positions[source] - dst_x, dst_y = self.node_positions[target] - - # Transform coordinates to screen space - screen_src_x = center_x + src_x * scale - screen_src_y = center_y + src_y * scale - screen_dst_x = center_x + dst_x * scale - screen_dst_y = center_y + dst_y * scale - - # Get growth progress for this edge (default to 1.0 if not growing) - growth_progress = self.growing_edges.get((source, target), 1.0) - - # Calculate the actual destination based on growth progress - if growth_progress < 1.0: - # Interpolate between source and destination - actual_dst_x = screen_src_x + (screen_dst_x - screen_src_x) * growth_progress - actual_dst_y = screen_src_y + (screen_dst_y - screen_src_y) * growth_progress - else: - actual_dst_x = screen_dst_x - actual_dst_y = screen_dst_y - - # Draw mycelial connection (multiple thin lines with variations) - source_color = QColor(self.node_colors.get(source, self.node_colors_by_type['main'])) - target_color = QColor(self.node_colors.get(target, self.node_colors_by_type['main'])) - - # Number of filaments per connection - num_filaments = 3 - - for i in range(num_filaments): - # Create a path with multiple segments for organic look - path = QPainterPath() - path.moveTo(screen_src_x, screen_src_y) - - # Calculate distance between points - distance = math.sqrt((actual_dst_x - screen_src_x)**2 + (actual_dst_y - screen_src_y)**2) - - # Number of segments increases with distance - num_segments = max(3, int(distance / 40)) - - # Create intermediate points with slight random variations - prev_x, prev_y = screen_src_x, screen_src_y - - for j in range(1, num_segments): - # Calculate position along the line - ratio = j / num_segments - - # Base position - base_x = screen_src_x + (actual_dst_x - screen_src_x) * ratio - base_y = screen_src_y + (actual_dst_y - screen_src_y) * ratio - - # Add random variation perpendicular to the line - angle = math.atan2(actual_dst_y - screen_src_y, actual_dst_x - screen_src_x) + math.pi/2 - variation = (random.random() - 0.5) * 10 * scale - - # Variation decreases near endpoints - endpoint_factor = min(ratio, 1 - ratio) * 4 # Maximum at middle - variation *= endpoint_factor - - # Apply variation - point_x = base_x + variation * math.cos(angle) - point_y = base_y + variation * math.sin(angle) - - # Add point to path - path.lineTo(point_x, point_y) - prev_x, prev_y = point_x, point_y - - # Complete the path to destination - path.lineTo(actual_dst_x, actual_dst_y) - - # Create gradient along the path - gradient = QLinearGradient(screen_src_x, screen_src_y, actual_dst_x, actual_dst_y) - - # Make colors more transparent for mycelial effect - source_color_trans = QColor(source_color) - target_color_trans = QColor(target_color) - - # Vary transparency by filament - alpha = 70 + i * 20 - source_color_trans.setAlpha(alpha) - target_color_trans.setAlpha(alpha) - - gradient.setColorAt(0, source_color_trans) - gradient.setColorAt(1, target_color_trans) - - # Animate flow along edge - flow_pos = (self.animation_progress + i * 0.3) % 1.0 - flow_color = QColor(255, 255, 255, 100) - gradient.setColorAt(flow_pos, flow_color) - - # Draw the edge with varying thickness - thickness = 1.0 + (i * 0.5) - pen = QPen(QBrush(gradient), thickness) - pen.setCapStyle(Qt.PenCapStyle.RoundCap) - painter.setPen(pen) - painter.drawPath(path) - - # Draw small nodes along the path for mycelial effect - if growth_progress == 1.0: # Only for fully grown edges - num_nodes = int(distance / 50) - for j in range(1, num_nodes): - ratio = j / num_nodes - node_x = screen_src_x + (screen_dst_x - screen_src_x) * ratio - node_y = screen_src_y + (screen_dst_y - screen_src_y) * ratio - - # Add small random offset - offset_angle = random.random() * math.pi * 2 - offset_dist = random.random() * 5 - node_x += math.cos(offset_angle) * offset_dist - node_y += math.sin(offset_angle) * offset_dist - - # Draw small node - node_color = QColor(source_color) - node_color.setAlpha(100) - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(QBrush(node_color)) - node_size = 1 + random.random() * 2 - painter.drawEllipse(QPointF(node_x, node_y), node_size, node_size) - - # Draw nodes - for node_id in self.nodes: - if node_id in self.node_positions: - x, y = self.node_positions[node_id] - - # Transform coordinates to screen space - screen_x = center_x + x * scale - screen_y = center_y + y * scale - - # Get node properties - node_color = self.node_colors.get(node_id, self.node_colors_by_type['branch']) - node_label = self.node_labels.get(node_id, 'Node') - node_size = self.node_sizes.get(node_id, 400) - - # Scale the node size - radius = math.sqrt(node_size) * scale / 2 - - # Adjust radius for hover/selection - if node_id == self.selected_node: - radius *= 1.1 # Larger when selected - elif node_id == self.hovered_node: - radius *= 1.05 # Slightly larger when hovered - - # Draw node glow for selected/hovered nodes - if node_id == self.selected_node or node_id == self.hovered_node: - glow_radius = radius * 1.5 - glow_color = QColor(node_color) - - for i in range(5): - r = glow_radius - (i * radius * 0.1) - alpha = 40 - (i * 8) - glow_color.setAlpha(alpha) - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(glow_color) - painter.drawEllipse(QPointF(screen_x, screen_y), r, r) - - # Draw mycelial node (irregular shape with hyphae) - painter.setPen(Qt.PenStyle.NoPen) - - # Create gradient fill for node - gradient = QRadialGradient(screen_x, screen_y, radius) - base_color = QColor(node_color) - lighter_color = QColor(node_color).lighter(130) - darker_color = QColor(node_color).darker(130) - - gradient.setColorAt(0, lighter_color) - gradient.setColorAt(0.7, base_color) - gradient.setColorAt(1, darker_color) - - # Fill main node body - painter.setBrush(QBrush(gradient)) - - # Draw irregular node shape - path = QPainterPath() - - # Create irregular circle with random variations - num_points = 20 - start_angle = random.random() * math.pi * 2 - - for i in range(num_points + 1): - angle = start_angle + (i * 2 * math.pi / num_points) - # Vary radius slightly for organic look - variation = 1.0 + (random.random() - 0.5) * 0.2 - point_radius = radius * variation - - x_point = screen_x + math.cos(angle) * point_radius - y_point = screen_y + math.sin(angle) * point_radius - - if i == 0: - path.moveTo(x_point, y_point) - else: - # Use quadratic curves for smoother shape - control_angle = start_angle + ((i - 0.5) * 2 * math.pi / num_points) - control_radius = radius * (1.0 + (random.random() - 0.5) * 0.1) - control_x = screen_x + math.cos(control_angle) * control_radius - control_y = screen_y + math.sin(control_angle) * control_radius - - path.quadTo(control_x, control_y, x_point, y_point) - - # Draw the main node body - painter.drawPath(path) - - # Draw hyphae (mycelial extensions) - hyphae_count = self.hyphae_count - if node_id == 'main': - hyphae_count += 3 # More hyphae for main node - - for i in range(hyphae_count): - # Random angle for hyphae - angle = random.random() * math.pi * 2 - - # Base length varies by node type - base_length = radius * self.hyphae_length_factor - if node_id == 'main': - base_length *= 1.5 - - # Random variation in length - length = base_length * (1.0 + (random.random() - 0.5) * self.hyphae_variation) - - # Calculate end point - end_x = screen_x + math.cos(angle) * (radius + length) - end_y = screen_y + math.sin(angle) * (radius + length) - - # Start point is on the node perimeter - start_x = screen_x + math.cos(angle) * radius * 0.9 - start_y = screen_y + math.sin(angle) * radius * 0.9 - - # Create hyphae path with slight curve - hypha_path = QPainterPath() - hypha_path.moveTo(start_x, start_y) - - # Control point for curve - ctrl_angle = angle + (random.random() - 0.5) * 0.5 # Slight angle variation - ctrl_dist = radius + length * 0.5 - ctrl_x = screen_x + math.cos(ctrl_angle) * ctrl_dist - ctrl_y = screen_y + math.sin(ctrl_angle) * ctrl_dist - - hypha_path.quadTo(ctrl_x, ctrl_y, end_x, end_y) - - # Draw hypha with gradient - hypha_gradient = QLinearGradient(start_x, start_y, end_x, end_y) - - # Hypha color starts as node color and fades out - hypha_start_color = QColor(node_color) - hypha_end_color = QColor(node_color) - hypha_start_color.setAlpha(150) - hypha_end_color.setAlpha(30) - - hypha_gradient.setColorAt(0, hypha_start_color) - hypha_gradient.setColorAt(1, hypha_end_color) - - # Draw hypha with varying thickness - thickness = 1.0 + random.random() * 1.5 - hypha_pen = QPen(QBrush(hypha_gradient), thickness) - hypha_pen.setCapStyle(Qt.PenCapStyle.RoundCap) - painter.setPen(hypha_pen) - painter.drawPath(hypha_path) - - # Add small nodes at the end of some hyphae - if random.random() > 0.5: - small_node_color = QColor(node_color) - small_node_color.setAlpha(100) - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(QBrush(small_node_color)) - small_node_size = 1 + random.random() * 2 - painter.drawEllipse(QPointF(end_x, end_y), small_node_size, small_node_size) - - def draw_arrow_head(self, painter, x1, y1, x2, y2): - """Draw an arrow head at the end of a line""" - # For mycelial style, we don't need arrow heads - pass - - def mousePressEvent(self, event): - """Handle mouse press events""" - if event.button() == Qt.MouseButton.LeftButton: - # Get click position - pos = event.position() - - # Check if a node was clicked - clicked_node = self.get_node_at_position(pos) - if clicked_node: - self.selected_node = clicked_node - self.update() - self.nodeSelected.emit(clicked_node) - - def mouseMoveEvent(self, event): - """Handle mouse move events for hover effects""" - pos = event.position() - hovered_node = self.get_node_at_position(pos) - - if hovered_node != self.hovered_node: - self.hovered_node = hovered_node - self.update() - if hovered_node: - self.nodeHovered.emit(hovered_node) - - # Show tooltip with node info - if hovered_node in self.node_labels: - # Get node type from the ID - node_type = "main" - if "rabbithole_" in hovered_node: - node_type = "rabbithole" - elif "fork_" in hovered_node: - node_type = "fork" - - # Set emoji based on node type - emoji = "🌱" # Default/main - if node_type == "rabbithole": - emoji = "🕳️" # Rabbithole emoji - elif node_type == "fork": - emoji = "🔱" # Fork emoji - - # Show tooltip with emoji and label - QToolTip.showText( - event.globalPosition().toPoint(), - f"{emoji} {self.node_labels[hovered_node]}", - self - ) - - def get_node_at_position(self, pos): - """Get the node at the given position""" - # Calculate center point and scale factor - width = self.width() - height = self.height() - center_x = width / 2 - center_y = height / 2 - scale = min(width, height) / 500 - - # Check each node - for node_id in self.nodes: - if node_id in self.node_positions: - x, y = self.node_positions[node_id] - screen_x = center_x + x * scale - screen_y = center_y + y * scale - - # Get node size - node_size = self.node_sizes.get(node_id, 400) - radius = math.sqrt(node_size) * scale / 2 - - # Check if click is inside the node - distance = math.sqrt((pos.x() - screen_x)**2 + (pos.y() - screen_y)**2) - if distance <= radius: - return node_id - - return None - - def resizeEvent(self, event): - """Handle resize events""" - super().resizeEvent(event) - self.update() - -class NetworkPane(QWidget): - nodeSelected = pyqtSignal(str) - - def __init__(self): - super().__init__() - - # Main layout - layout = QVBoxLayout(self) - layout.setContentsMargins(10, 10, 10, 10) - - # Title - title = QLabel("Propagation Network") - title.setStyleSheet("color: #D4D4D4; font-size: 14px; font-weight: bold; font-family: 'Orbitron', sans-serif;") - layout.addWidget(title, alignment=Qt.AlignmentFlag.AlignCenter) - - # Network view - set to expand to fill available space - self.network_view = NetworkGraphWidget() - self.network_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - layout.addWidget(self.network_view, 1) # Add stretch factor of 1 to make it expand - - # Connect signals - self.network_view.nodeSelected.connect(self.nodeSelected) - - # Initialize graph - self.graph = nx.DiGraph() - self.node_positions = {} - self.node_colors = {} - self.node_labels = {} - self.node_sizes = {} - - # Add main node - self.add_node('main', 'Seed', 'main') - - def add_node(self, node_id, label, node_type='branch'): - """Add a node to the graph""" - try: - # Add the node to the graph - self.graph.add_node(node_id) - - # Set node properties based on type - if node_type == 'main': - color = '#569CD6' # Blue - size = 800 - elif node_type == 'rabbithole': - color = '#B5CEA8' # Green - size = 600 - elif node_type == 'fork': - color = '#DCDCAA' # Yellow - size = 600 - else: - color = '#CE9178' # Orange - size = 400 - - # Store node properties - self.node_colors[node_id] = color - self.node_labels[node_id] = label - self.node_sizes[node_id] = size - - # Calculate position based on existing nodes - self.calculate_node_position(node_id, node_type) - - # Redraw the graph - self.update_graph() - - except Exception as e: - print(f"Error adding node: {e}") - - def add_edge(self, source_id, target_id): - """Add an edge between two nodes""" - try: - # Add the edge to the graph - self.graph.add_edge(source_id, target_id) - - # Redraw the graph - self.update_graph() - - except Exception as e: - print(f"Error adding edge: {e}") - - def calculate_node_position(self, node_id, node_type): - """Calculate position for a new node""" - # Get number of existing nodes - num_nodes = len(self.graph.nodes) - 1 # Exclude the main node - - if node_type == 'main': - # Main node is at center - self.node_positions[node_id] = (0, 0) - else: - # Calculate angle based on node count with better distribution - # Use golden ratio to distribute nodes more evenly - golden_ratio = 1.618033988749895 - angle = 2 * math.pi * golden_ratio * num_nodes - - # Calculate distance from center based on node type and node count - # Increase distance as more nodes are added - base_distance = 200 - count_factor = min(1.0, num_nodes / 20) # Scale up to 20 nodes - - if node_type == 'rabbithole': - distance = base_distance * (1.0 + count_factor * 0.5) - elif node_type == 'fork': - distance = base_distance * (1.2 + count_factor * 0.5) - else: - distance = base_distance * (1.4 + count_factor * 0.5) - - # Calculate position using polar coordinates - x = distance * math.cos(angle) - y = distance * math.sin(angle) - - # Add some random offset for natural appearance - x += random.uniform(-30, 30) - y += random.uniform(-30, 30) - - # Check for potential overlaps with existing nodes and adjust if needed - overlap = True - max_attempts = 5 - attempt = 0 - - while overlap and attempt < max_attempts: - overlap = False - for existing_id, (ex, ey) in self.node_positions.items(): - # Skip comparing with self - if existing_id == node_id: - continue - - # Calculate distance between nodes - dx = x - ex - dy = y - ey - distance = math.sqrt(dx*dx + dy*dy) - - # Get node sizes - new_size = math.sqrt(self.node_sizes.get(node_id, 400)) - existing_size = math.sqrt(self.node_sizes.get(existing_id, 400)) - min_distance = (new_size + existing_size) / 2 - - # If too close, adjust position - if distance < min_distance * 1.5: - overlap = True - # Move away from the overlapping node - angle = math.atan2(dy, dx) - adjustment = min_distance * 1.5 - distance - x += math.cos(angle) * adjustment * 1.2 - y += math.sin(angle) * adjustment * 1.2 - break - - attempt += 1 - - # Store the position - self.node_positions[node_id] = (x, y) - - def update_graph(self): - """Update the network graph visualization""" - if hasattr(self, 'network_view'): - # Update the network view with current graph data - self.network_view.nodes = list(self.graph.nodes()) - self.network_view.edges = list(self.graph.edges()) - self.network_view.node_positions = self.node_positions - self.network_view.node_colors = self.node_colors - self.network_view.node_labels = self.node_labels - self.network_view.node_sizes = self.node_sizes - - # Redraw - self.network_view.update() - -class ImagePreviewPane(QWidget): - """Pane to display generated images with navigation""" - def __init__(self): - super().__init__() - self.current_image_path = None - self.session_images = [] # List of all images generated this session - self.current_index = -1 # Current image index - self.setup_ui() - - def setup_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(10, 10, 10, 10) - layout.setSpacing(10) - - # Title label - self.title = QLabel("🎨 GENERATED IMAGES") - self.title.setStyleSheet(f""" - QLabel {{ - color: {COLORS['accent_purple']}; - font-weight: bold; - font-size: 12px; - padding: 5px; - }} - """) - self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(self.title) - - # Image display label - self.image_label = QLabel("No images generated yet") - self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.image_label.setStyleSheet(f""" - QLabel {{ - background-color: {COLORS['bg_medium']}; - border: 2px dashed {COLORS['border']}; - border-radius: 8px; - color: {COLORS['text_dim']}; - padding: 20px; - min-height: 200px; - }} - """) - self.image_label.setWordWrap(True) - self.image_label.setScaledContents(False) - layout.addWidget(self.image_label, 1) - - # Navigation controls - nav_layout = QHBoxLayout() - nav_layout.setSpacing(8) - - # Previous button - self.prev_button = QPushButton("◀ Prev") - self.prev_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border']}; - border-radius: 4px; - padding: 6px 12px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_light']}; - border-color: {COLORS['accent_purple']}; - }} - QPushButton:disabled {{ - color: {COLORS['text_dim']}; - background-color: {COLORS['bg_dark']}; - }} - """) - self.prev_button.clicked.connect(self.show_previous) - self.prev_button.setEnabled(False) - nav_layout.addWidget(self.prev_button) - - # Position indicator - self.position_label = QLabel("") - self.position_label.setStyleSheet(f""" - QLabel {{ - color: {COLORS['text_dim']}; - font-size: 11px; - }} - """) - self.position_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - nav_layout.addWidget(self.position_label, 1) - - # Next button - self.next_button = QPushButton("Next ▶") - self.next_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border']}; - border-radius: 4px; - padding: 6px 12px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_light']}; - border-color: {COLORS['accent_purple']}; - }} - QPushButton:disabled {{ - color: {COLORS['text_dim']}; - background-color: {COLORS['bg_dark']}; - }} - """) - self.next_button.clicked.connect(self.show_next) - self.next_button.setEnabled(False) - nav_layout.addWidget(self.next_button) - - layout.addLayout(nav_layout) - - # Image info label - self.info_label = QLabel("") - self.info_label.setStyleSheet(f""" - QLabel {{ - color: {COLORS['text_dim']}; - font-size: 10px; - padding: 5px; - }} - """) - self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.info_label.setWordWrap(True) - layout.addWidget(self.info_label) - - # Open in folder button - self.open_button = QPushButton("📂 Open Images Folder") - self.open_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border']}; - border-radius: 4px; - padding: 8px; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_light']}; - border-color: {COLORS['accent_purple']}; - }} - """) - self.open_button.clicked.connect(self.open_images_folder) - layout.addWidget(self.open_button) - - def add_image(self, image_path): - """Add a new image to the session gallery and display it""" - if image_path and os.path.exists(image_path): - # Avoid duplicates - if image_path not in self.session_images: - self.session_images.append(image_path) - # Jump to the new image - self.current_index = len(self.session_images) - 1 - self._display_current() - - def set_image(self, image_path): - """Display an image - also adds to gallery if new""" - self.add_image(image_path) - - def _display_current(self): - """Display the image at current_index""" - if not self.session_images or self.current_index < 0: - self.image_label.setText("No images generated yet") - self.info_label.setText("") - self.position_label.setText("") - self.prev_button.setEnabled(False) - self.next_button.setEnabled(False) - return - - image_path = self.session_images[self.current_index] - self.current_image_path = image_path - - if os.path.exists(image_path): - pixmap = QPixmap(image_path) - if not pixmap.isNull(): - # Scale to fit the label while maintaining aspect ratio - scaled = pixmap.scaled( - self.image_label.size() - QSize(20, 20), - Qt.AspectRatioMode.KeepAspectRatio, - Qt.TransformationMode.SmoothTransformation - ) - self.image_label.setPixmap(scaled) - self.image_label.setStyleSheet(f""" - QLabel {{ - background-color: {COLORS['bg_medium']}; - border: 2px solid {COLORS['accent_purple']}; - border-radius: 8px; - padding: 10px; - }} - """) - - # Update info - filename = os.path.basename(image_path) - self.info_label.setText(f"📁 {filename}") - else: - self.image_label.setText("Failed to load image") - self.info_label.setText("") - else: - self.image_label.setText("Image not found") - self.info_label.setText("") - - # Update navigation - total = len(self.session_images) - current = self.current_index + 1 - self.position_label.setText(f"{current} of {total}") - self.prev_button.setEnabled(self.current_index > 0) - self.next_button.setEnabled(self.current_index < total - 1) - - def show_previous(self): - """Show the previous image""" - if self.current_index > 0: - self.current_index -= 1 - self._display_current() - - def show_next(self): - """Show the next image""" - if self.current_index < len(self.session_images) - 1: - self.current_index += 1 - self._display_current() - - def clear_session(self): - """Clear all session images (e.g., when starting a new conversation)""" - self.session_images = [] - self.current_index = -1 - self.current_image_path = None - self.image_label.setText("No images generated yet") - self.image_label.setStyleSheet(f""" - QLabel {{ - background-color: {COLORS['bg_medium']}; - border: 2px dashed {COLORS['border']}; - border-radius: 8px; - color: {COLORS['text_dim']}; - padding: 20px; - min-height: 200px; - }} - """) - self.info_label.setText("") - self.position_label.setText("") - self.prev_button.setEnabled(False) - self.next_button.setEnabled(False) - - def open_images_folder(self): - """Open the images folder in file explorer""" - import subprocess - images_dir = os.path.join(os.path.dirname(__file__), 'images') - if os.path.exists(images_dir): - subprocess.Popen(f'explorer "{images_dir}"') - else: - # Try to create it - os.makedirs(images_dir, exist_ok=True) - subprocess.Popen(f'explorer "{images_dir}"') - - def resizeEvent(self, event): - """Re-scale image when pane is resized""" - super().resizeEvent(event) - if self.current_image_path: - self._display_current() - - -class VideoPreviewPane(QWidget): - """Pane to display generated videos with navigation""" - def __init__(self): - super().__init__() - self.current_video_path = None - self.session_videos = [] # List of all videos generated this session - self.current_index = -1 # Current video index - self.setup_ui() - - def setup_ui(self): - layout = QVBoxLayout(self) - layout.setContentsMargins(10, 10, 10, 10) - layout.setSpacing(10) - - # Title label - self.title = QLabel("🎬 GENERATED VIDEOS") - self.title.setStyleSheet(f""" - QLabel {{ - color: {COLORS['accent_cyan']}; - font-weight: bold; - font-size: 12px; - padding: 5px; - }} - """) - self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(self.title) - - # Video display area - we'll show a thumbnail or placeholder - self.video_label = QLabel("No videos generated yet") - self.video_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.video_label.setStyleSheet(f""" - QLabel {{ - background-color: {COLORS['bg_medium']}; - border: 2px dashed {COLORS['border']}; - border-radius: 8px; - color: {COLORS['text_dim']}; - padding: 20px; - min-height: 150px; - }} - """) - self.video_label.setWordWrap(True) - layout.addWidget(self.video_label, 1) - - # Play button - self.play_button = QPushButton("▶ Play Video") - self.play_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['accent_cyan']}; - color: {COLORS['bg_dark']}; - border: none; - border-radius: 4px; - padding: 10px 20px; - font-weight: bold; - font-size: 12px; - }} - QPushButton:hover {{ - background-color: {COLORS['accent_purple']}; - }} - QPushButton:disabled {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_dim']}; - }} - """) - self.play_button.clicked.connect(self.play_current_video) - self.play_button.setEnabled(False) - layout.addWidget(self.play_button) - - # Navigation controls - nav_layout = QHBoxLayout() - nav_layout.setSpacing(8) - - # Previous button - self.prev_button = QPushButton("◀ Prev") - self.prev_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border']}; - border-radius: 4px; - padding: 6px 12px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_light']}; - border-color: {COLORS['accent_cyan']}; - }} - QPushButton:disabled {{ - color: {COLORS['text_dim']}; - background-color: {COLORS['bg_dark']}; - }} - """) - self.prev_button.clicked.connect(self.show_previous) - self.prev_button.setEnabled(False) - nav_layout.addWidget(self.prev_button) - - # Position indicator - self.position_label = QLabel("") - self.position_label.setStyleSheet(f""" - QLabel {{ - color: {COLORS['text_dim']}; - font-size: 11px; - }} - """) - self.position_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - nav_layout.addWidget(self.position_label, 1) - - # Next button - self.next_button = QPushButton("Next ▶") - self.next_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border']}; - border-radius: 4px; - padding: 6px 12px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_light']}; - border-color: {COLORS['accent_cyan']}; - }} - QPushButton:disabled {{ - color: {COLORS['text_dim']}; - background-color: {COLORS['bg_dark']}; - }} - """) - self.next_button.clicked.connect(self.show_next) - self.next_button.setEnabled(False) - nav_layout.addWidget(self.next_button) - - layout.addLayout(nav_layout) - - # Video info label - self.info_label = QLabel("") - self.info_label.setStyleSheet(f""" - QLabel {{ - color: {COLORS['text_dim']}; - font-size: 10px; - padding: 5px; - }} - """) - self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.info_label.setWordWrap(True) - layout.addWidget(self.info_label) - - # Open in folder button - self.open_button = QPushButton("📂 Open Videos Folder") - self.open_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border']}; - border-radius: 4px; - padding: 8px; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_light']}; - border-color: {COLORS['accent_cyan']}; - }} - """) - self.open_button.clicked.connect(self.open_videos_folder) - layout.addWidget(self.open_button) - - def add_video(self, video_path): - """Add a new video to the session gallery and display it""" - if video_path and os.path.exists(video_path): - # Avoid duplicates - if video_path not in self.session_videos: - self.session_videos.append(video_path) - # Jump to the new video - self.current_index = len(self.session_videos) - 1 - self._display_current() - - def set_video(self, video_path): - """Display a video - also adds to gallery if new""" - self.add_video(video_path) - - def _display_current(self): - """Display the video at current_index""" - if not self.session_videos or self.current_index < 0: - self.video_label.setText("No videos generated yet") - self.info_label.setText("") - self.position_label.setText("") - self.prev_button.setEnabled(False) - self.next_button.setEnabled(False) - self.play_button.setEnabled(False) - return - - video_path = self.session_videos[self.current_index] - self.current_video_path = video_path - - if os.path.exists(video_path): - filename = os.path.basename(video_path) - # Show video info - self.video_label.setText(f"🎬 {filename}\n\n(Click Play to view)") - self.video_label.setStyleSheet(f""" - QLabel {{ - background-color: {COLORS['bg_medium']}; - border: 2px solid {COLORS['accent_cyan']}; - border-radius: 8px; - color: {COLORS['text_bright']}; - padding: 20px; - min-height: 150px; - }} - """) - self.info_label.setText(f"📁 {filename}") - self.play_button.setEnabled(True) - else: - self.video_label.setText("Video not found") - self.info_label.setText("") - self.play_button.setEnabled(False) - - # Update navigation - total = len(self.session_videos) - current = self.current_index + 1 - self.position_label.setText(f"{current} of {total}") - self.prev_button.setEnabled(self.current_index > 0) - self.next_button.setEnabled(self.current_index < total - 1) - - def show_previous(self): - """Show the previous video""" - if self.current_index > 0: - self.current_index -= 1 - self._display_current() - - def show_next(self): - """Show the next video""" - if self.current_index < len(self.session_videos) - 1: - self.current_index += 1 - self._display_current() - - def play_current_video(self): - """Open the current video in the default video player""" - if self.current_video_path and os.path.exists(self.current_video_path): - import subprocess - import sys - if sys.platform == 'win32': - os.startfile(self.current_video_path) - elif sys.platform == 'darwin': - subprocess.Popen(['open', self.current_video_path]) - else: - subprocess.Popen(['xdg-open', self.current_video_path]) - - def clear_session(self): - """Clear all session videos (e.g., when starting a new conversation)""" - self.session_videos = [] - self.current_index = -1 - self.current_video_path = None - self.video_label.setText("No videos generated yet") - self.video_label.setStyleSheet(f""" - QLabel {{ - background-color: {COLORS['bg_medium']}; - border: 2px dashed {COLORS['border']}; - border-radius: 8px; - color: {COLORS['text_dim']}; - padding: 20px; - min-height: 150px; - }} - """) - self.info_label.setText("") - self.position_label.setText("") - self.prev_button.setEnabled(False) - self.next_button.setEnabled(False) - self.play_button.setEnabled(False) - - def open_videos_folder(self): - """Open the videos folder in file explorer""" - import subprocess - videos_dir = os.path.join(os.path.dirname(__file__), 'videos') - if os.path.exists(videos_dir): - subprocess.Popen(f'explorer "{videos_dir}"') - else: - # Try to create it - os.makedirs(videos_dir, exist_ok=True) - subprocess.Popen(f'explorer "{videos_dir}"') - - -class RightSidebar(QWidget): - """Right sidebar with tabbed interface for Setup and Network Graph""" - nodeSelected = pyqtSignal(str) - - def __init__(self): - super().__init__() - self.setMinimumWidth(300) - self.setup_ui() - - def setup_ui(self): - """Set up the tabbed sidebar interface""" - layout = QVBoxLayout(self) - layout.setContentsMargins(5, 5, 5, 5) - layout.setSpacing(0) - - # Create tab bar at the top (custom styled) - tab_container = QWidget() - tab_container.setStyleSheet(f""" - QWidget {{ - background-color: {COLORS['bg_medium']}; - border-bottom: 1px solid {COLORS['border_glow']}; - }} - """) - tab_layout = QHBoxLayout(tab_container) - tab_layout.setContentsMargins(0, 0, 0, 0) - tab_layout.setSpacing(0) - - # Tab buttons - self.setup_button = QPushButton("⚙ SETUP") - self.graph_button = QPushButton("🌐 GRAPH") - self.image_button = QPushButton("🖼 IMAGE") - self.video_button = QPushButton("🎬 VIDEO") - - # Cyberpunk tab button styling - tab_style = f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_dim']}; - border: none; - border-bottom: 2px solid transparent; - padding: 12px 12px; - font-weight: bold; - font-size: 10px; - letter-spacing: 1px; - text-transform: uppercase; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_light']}; - color: {COLORS['text_normal']}; - }} - QPushButton:checked {{ - background-color: {COLORS['bg_dark']}; - color: {COLORS['accent_cyan']}; - border-bottom: 2px solid {COLORS['accent_cyan']}; - }} - """ - - self.setup_button.setStyleSheet(tab_style) - self.graph_button.setStyleSheet(tab_style) - self.image_button.setStyleSheet(tab_style) - self.video_button.setStyleSheet(tab_style) - - # Make buttons checkable for tab behavior - self.setup_button.setCheckable(True) - self.graph_button.setCheckable(True) - self.image_button.setCheckable(True) - self.video_button.setCheckable(True) - self.setup_button.setChecked(True) # Start with setup tab active - - # Connect tab buttons - self.setup_button.clicked.connect(lambda: self.switch_tab(0)) - self.graph_button.clicked.connect(lambda: self.switch_tab(1)) - self.image_button.clicked.connect(lambda: self.switch_tab(2)) - self.video_button.clicked.connect(lambda: self.switch_tab(3)) - - tab_layout.addWidget(self.setup_button) - tab_layout.addWidget(self.graph_button) - tab_layout.addWidget(self.image_button) - tab_layout.addWidget(self.video_button) - - layout.addWidget(tab_container) - - # Create stacked widget for tab content - from PyQt6.QtWidgets import QStackedWidget - self.stack = QStackedWidget() - self.stack.setStyleSheet(f""" - QStackedWidget {{ - background-color: {COLORS['bg_dark']}; - border: none; - }} - """) - - # Create tab pages - self.control_panel = ControlPanel() - self.network_pane = NetworkPane() - self.image_preview_pane = ImagePreviewPane() - self.video_preview_pane = VideoPreviewPane() - - # Add pages to stack - self.stack.addWidget(self.control_panel) - self.stack.addWidget(self.network_pane) - self.stack.addWidget(self.image_preview_pane) - self.stack.addWidget(self.video_preview_pane) - - layout.addWidget(self.stack, 1) # Stretch to fill - - # Connect network pane signal to forward it - self.network_pane.nodeSelected.connect(self.nodeSelected) - - def switch_tab(self, index): - """Switch between tabs""" - self.stack.setCurrentIndex(index) - - # Update button states - self.setup_button.setChecked(index == 0) - self.graph_button.setChecked(index == 1) - self.image_button.setChecked(index == 2) - self.video_button.setChecked(index == 3) - - def update_image_preview(self, image_path): - """Update the image preview pane with a new image""" - if hasattr(self, 'image_preview_pane'): - self.image_preview_pane.set_image(image_path) - - def update_video_preview(self, video_path): - """Update the video preview pane with a new video""" - if hasattr(self, 'video_preview_pane'): - self.video_preview_pane.set_video(video_path) - - def add_node(self, node_id, label, node_type): - """Forward to network pane""" - self.network_pane.add_node(node_id, label, node_type) - - def add_edge(self, source_id, target_id): - """Forward to network pane""" - self.network_pane.add_edge(source_id, target_id) - - def update_graph(self): - """Forward to network pane""" - self.network_pane.update_graph() - -class ControlPanel(QWidget): - """Control panel with mode, model selections, etc.""" - def __init__(self): - super().__init__() - - # Set up the UI - self.setup_ui() - - # Initialize with models and prompt pairs - self.initialize_selectors() - - def setup_ui(self): - """Set up the user interface for the control panel - vertical sidebar layout""" - # Main layout - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(5, 5, 5, 5) - main_layout.setSpacing(8) - - # Add a title with cyberpunk styling - title = QLabel("═ CONTROL PANEL ═") - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - title.setStyleSheet(f""" - color: {COLORS['accent_cyan']}; - font-size: 12px; - font-weight: bold; - padding: 10px; - background-color: {COLORS['bg_medium']}; - border: 1px solid {COLORS['border_glow']}; - border-radius: 0px; - letter-spacing: 2px; - """) - main_layout.addWidget(title) - - # Create scrollable area for controls - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) - scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) - scroll_area.setStyleSheet(f""" - QScrollArea {{ - border: none; - background-color: transparent; - }} - QScrollBar:vertical {{ - background: {COLORS['bg_medium']}; - width: 10px; - margin: 0px; - }} - QScrollBar::handle:vertical {{ - background: {COLORS['border_glow']}; - min-height: 20px; - border-radius: 0px; - }} - QScrollBar::handle:vertical:hover {{ - background: {COLORS['accent_cyan']}; - }} - QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ - height: 0px; - }} - QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{ - background: none; - }} - """) - - # Container widget for scrollable content - scroll_content = QWidget() - scroll_content.setStyleSheet(f"background-color: transparent;") - - # All controls in vertical layout - controls_layout = QVBoxLayout(scroll_content) - controls_layout.setContentsMargins(5, 5, 5, 5) - controls_layout.setSpacing(10) - - # Mode selection with icon - mode_container = QWidget() - mode_layout = QVBoxLayout(mode_container) - mode_layout.setContentsMargins(0, 0, 0, 0) - mode_layout.setSpacing(5) - - mode_label = QLabel("▸ MODE") - mode_label.setStyleSheet(f"color: {COLORS['text_glow']}; font-size: 10px; font-weight: bold; letter-spacing: 1px;") - mode_layout.addWidget(mode_label) - - self.mode_selector = QComboBox() - self.mode_selector.addItems(["AI-AI", "Human-AI"]) - self.mode_selector.setStyleSheet(self.get_combobox_style()) - mode_layout.addWidget(self.mode_selector) - controls_layout.addWidget(mode_container) - - # Iterations with slider - iterations_container = QWidget() - iterations_layout = QVBoxLayout(iterations_container) - iterations_layout.setContentsMargins(0, 0, 0, 0) - iterations_layout.setSpacing(5) - - iterations_label = QLabel("▸ ITERATIONS") - iterations_label.setStyleSheet(f"color: {COLORS['text_glow']}; font-size: 10px; font-weight: bold; letter-spacing: 1px;") - iterations_layout.addWidget(iterations_label) - - self.iterations_selector = QComboBox() - self.iterations_selector.addItems(["1", "2", "5", "6", "10", "100"]) - self.iterations_selector.setStyleSheet(self.get_combobox_style()) - iterations_layout.addWidget(self.iterations_selector) - controls_layout.addWidget(iterations_container) - - # Number of AIs selection - num_ais_container = QWidget() - num_ais_layout = QVBoxLayout(num_ais_container) - num_ais_layout.setContentsMargins(0, 0, 0, 0) - num_ais_layout.setSpacing(5) - - num_ais_label = QLabel("▸ NUMBER OF AIs") - num_ais_label.setStyleSheet(f"color: {COLORS['text_glow']}; font-size: 10px; font-weight: bold; letter-spacing: 1px;") - num_ais_layout.addWidget(num_ais_label) - - self.num_ais_selector = QComboBox() - self.num_ais_selector.addItems(["1", "2", "3", "4", "5"]) - self.num_ais_selector.setCurrentText("3") # Default to 3 AIs - self.num_ais_selector.setStyleSheet(self.get_combobox_style()) - num_ais_layout.addWidget(self.num_ais_selector) - controls_layout.addWidget(num_ais_container) - - # AI-1 Model selection - self.ai1_container = QWidget() - ai1_layout = QVBoxLayout(self.ai1_container) - ai1_layout.setContentsMargins(0, 0, 0, 0) - ai1_layout.setSpacing(5) - - ai1_label = QLabel("AI-1") - ai1_label.setStyleSheet(f"color: {COLORS['text_dim']}; font-size: 10px;") - ai1_layout.addWidget(ai1_label) - - self.ai1_model_selector = QComboBox() - self.ai1_model_selector.setStyleSheet(self.get_combobox_style()) - ai1_layout.addWidget(self.ai1_model_selector) - controls_layout.addWidget(self.ai1_container) - - # AI-2 Model selection - self.ai2_container = QWidget() - ai2_layout = QVBoxLayout(self.ai2_container) - ai2_layout.setContentsMargins(0, 0, 0, 0) - ai2_layout.setSpacing(5) - - ai2_label = QLabel("AI-2") - ai2_label.setStyleSheet(f"color: {COLORS['text_dim']}; font-size: 10px;") - ai2_layout.addWidget(ai2_label) - - self.ai2_model_selector = QComboBox() - self.ai2_model_selector.setStyleSheet(self.get_combobox_style()) - ai2_layout.addWidget(self.ai2_model_selector) - controls_layout.addWidget(self.ai2_container) - - # AI-3 Model selection - self.ai3_container = QWidget() - ai3_layout = QVBoxLayout(self.ai3_container) - ai3_layout.setContentsMargins(0, 0, 0, 0) - ai3_layout.setSpacing(5) - - ai3_label = QLabel("AI-3") - ai3_label.setStyleSheet(f"color: {COLORS['text_dim']}; font-size: 10px;") - ai3_layout.addWidget(ai3_label) - - self.ai3_model_selector = QComboBox() - self.ai3_model_selector.setStyleSheet(self.get_combobox_style()) - ai3_layout.addWidget(self.ai3_model_selector) - controls_layout.addWidget(self.ai3_container) - - # AI-4 Model selection - self.ai4_container = QWidget() - ai4_layout = QVBoxLayout(self.ai4_container) - ai4_layout.setContentsMargins(0, 0, 0, 0) - ai4_layout.setSpacing(5) - - ai4_label = QLabel("AI-4") - ai4_label.setStyleSheet(f"color: {COLORS['text_dim']}; font-size: 10px;") - ai4_layout.addWidget(ai4_label) - - self.ai4_model_selector = QComboBox() - self.ai4_model_selector.setStyleSheet(self.get_combobox_style()) - ai4_layout.addWidget(self.ai4_model_selector) - controls_layout.addWidget(self.ai4_container) - - # AI-5 Model selection - self.ai5_container = QWidget() - ai5_layout = QVBoxLayout(self.ai5_container) - ai5_layout.setContentsMargins(0, 0, 0, 0) - ai5_layout.setSpacing(5) - - ai5_label = QLabel("AI-5") - ai5_label.setStyleSheet(f"color: {COLORS['text_dim']}; font-size: 10px;") - ai5_layout.addWidget(ai5_label) - - self.ai5_model_selector = QComboBox() - self.ai5_model_selector.setStyleSheet(self.get_combobox_style()) - ai5_layout.addWidget(self.ai5_model_selector) - controls_layout.addWidget(self.ai5_container) - - # Prompt pair selection - prompt_container = QWidget() - prompt_layout = QVBoxLayout(prompt_container) - prompt_layout.setContentsMargins(0, 0, 0, 0) - prompt_layout.setSpacing(5) - - prompt_label = QLabel("Conversation Scenario") - prompt_label.setStyleSheet(f"color: {COLORS['text_dim']}; font-size: 10px;") - prompt_layout.addWidget(prompt_label) - - self.prompt_pair_selector = QComboBox() - self.prompt_pair_selector.setStyleSheet(self.get_combobox_style()) - prompt_layout.addWidget(self.prompt_pair_selector) - controls_layout.addWidget(prompt_container) - - # Action buttons container - action_container = QWidget() - action_layout = QVBoxLayout(action_container) - action_layout.setContentsMargins(0, 0, 0, 0) - action_layout.setSpacing(5) - - action_label = QLabel("▸ OPTIONS") - action_label.setStyleSheet(f"color: {COLORS['text_glow']}; font-size: 10px; font-weight: bold; letter-spacing: 1px;") - action_layout.addWidget(action_label) - - # Auto-generate images checkbox - self.auto_image_checkbox = QCheckBox("Auto-generate images") - self.auto_image_checkbox.setStyleSheet(f""" - QCheckBox {{ - color: {COLORS['text_normal']}; - spacing: 5px; - font-size: 10px; - padding: 4px; - }} - QCheckBox::indicator {{ - width: 18px; - height: 18px; - border: 1px solid {COLORS['border_glow']}; - border-radius: 0px; - background-color: {COLORS['bg_medium']}; - }} - QCheckBox::indicator:checked {{ - background-color: {COLORS['accent_cyan']}; - border: 1px solid {COLORS['accent_cyan']}; - }} - QCheckBox::indicator:hover {{ - border: 1px solid {COLORS['accent_cyan']}; - }} - """) - self.auto_image_checkbox.setToolTip("Automatically generate images from AI responses using Google Gemini 3 Pro Image Preview via OpenRouter") - action_layout.addWidget(self.auto_image_checkbox) - - # Removed: HTML contributions checkbox - - # Actions - buttons in vertical layout - actions_label = QLabel("▸ ACTIONS") - actions_label.setStyleSheet(f"color: {COLORS['text_glow']}; font-size: 10px; font-weight: bold; letter-spacing: 1px;") - action_layout.addWidget(actions_label) - - # Export button with glow - self.export_button = self.create_glow_button("📡 EXPORT", COLORS['accent_purple']) - action_layout.addWidget(self.export_button) - - # View HTML button with glow - opens the styled conversation - self.view_html_button = self.create_glow_button("🌐 VIEW HTML", COLORS['accent_green']) - self.view_html_button.setToolTip("View conversation as shareable HTML") - self.view_html_button.clicked.connect(lambda: open_html_in_browser("conversation_full.html")) - action_layout.addWidget(self.view_html_button) - - # BackroomsBench evaluation button - self.backroomsbench_button = self.create_glow_button("🌀 BACKROOMSBENCH (beta)", COLORS['accent_purple']) - self.backroomsbench_button.setToolTip("Run multi-judge AI evaluation (depth/philosophy)") - action_layout.addWidget(self.backroomsbench_button) - - controls_layout.addWidget(action_container) - - # Add all controls directly to controls_layout (now vertical) - controls_layout.addWidget(mode_container) - controls_layout.addWidget(iterations_container) - controls_layout.addWidget(num_ais_container) - - # Divider - divider1 = QLabel("─" * 20) - divider1.setStyleSheet(f"color: {COLORS['border_glow']}; font-size: 8px;") - controls_layout.addWidget(divider1) - - models_label = QLabel("▸ AI MODELS") - models_label.setStyleSheet(f"color: {COLORS['text_glow']}; font-size: 10px; font-weight: bold; letter-spacing: 1px;") - controls_layout.addWidget(models_label) - - controls_layout.addWidget(self.ai1_container) - controls_layout.addWidget(self.ai2_container) - controls_layout.addWidget(self.ai3_container) - controls_layout.addWidget(self.ai4_container) - controls_layout.addWidget(self.ai5_container) - - # Divider - divider2 = QLabel("─" * 20) - divider2.setStyleSheet(f"color: {COLORS['border_glow']}; font-size: 8px;") - controls_layout.addWidget(divider2) - - scenario_label = QLabel("▸ SCENARIO") - scenario_label.setStyleSheet(f"color: {COLORS['text_glow']}; font-size: 10px; font-weight: bold; letter-spacing: 1px;") - controls_layout.addWidget(scenario_label) - - controls_layout.addWidget(prompt_container) - - # Divider - divider3 = QLabel("─" * 20) - divider3.setStyleSheet(f"color: {COLORS['border_glow']}; font-size: 8px;") - controls_layout.addWidget(divider3) - - controls_layout.addWidget(action_container) - - # Add spacer - controls_layout.addStretch() - - # Set the scroll area widget and add to main layout - scroll_area.setWidget(scroll_content) - main_layout.addWidget(scroll_area, 1) # Stretch to fill - - def get_combobox_style(self): - """Get the style for comboboxes - cyberpunk themed""" - return f""" - QComboBox {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border_glow']}; - border-radius: 0px; - padding: 8px 10px; - min-height: 30px; - font-size: 10px; - }} - QComboBox:hover {{ - border: 1px solid {COLORS['accent_cyan']}; - color: {COLORS['text_bright']}; - }} - QComboBox::drop-down {{ - subcontrol-origin: padding; - subcontrol-position: top right; - width: 20px; - border-left: 1px solid {COLORS['border_glow']}; - border-radius: 0px; - }} - QComboBox::down-arrow {{ - width: 12px; - height: 12px; - image: none; - }} - QComboBox QAbstractItemView {{ - background-color: {COLORS['bg_dark']}; - color: {COLORS['text_normal']}; - selection-background-color: {COLORS['accent_cyan']}; - selection-color: {COLORS['bg_dark']}; - border: 1px solid {COLORS['border_glow']}; - border-radius: 0px; - padding: 4px; - }} - QComboBox QAbstractItemView::item {{ - min-height: 28px; - padding: 4px; - }} - """ - - def get_cyberpunk_button_style(self, accent_color): - """Get cyberpunk-themed button style with given accent color""" - return f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {accent_color}; - border: 2px solid {accent_color}; - border-radius: 3px; - padding: 10px 14px; - font-weight: bold; - font-size: 10px; - letter-spacing: 1px; - text-align: center; - }} - QPushButton:hover {{ - background-color: {accent_color}; - color: {COLORS['bg_dark']}; - border: 2px solid {accent_color}; - }} - QPushButton:pressed {{ - background-color: {COLORS['bg_light']}; - color: {accent_color}; - }} - """ - - def create_glow_button(self, text, accent_color): - """Create a button with glow effect""" - button = GlowButton(text, accent_color) - button.setStyleSheet(self.get_cyberpunk_button_style(accent_color)) - return button - - def initialize_selectors(self): - """Initialize the selector dropdowns with values from config""" - # Add AI models - self.ai1_model_selector.clear() - self.ai2_model_selector.clear() - self.ai3_model_selector.clear() - self.ai4_model_selector.clear() - self.ai5_model_selector.clear() - self.ai1_model_selector.addItems(list(AI_MODELS.keys())) - self.ai2_model_selector.addItems(list(AI_MODELS.keys())) - self.ai3_model_selector.addItems(list(AI_MODELS.keys())) - self.ai4_model_selector.addItems(list(AI_MODELS.keys())) - self.ai5_model_selector.addItems(list(AI_MODELS.keys())) - - # Add prompt pairs - self.prompt_pair_selector.clear() - self.prompt_pair_selector.addItems(list(SYSTEM_PROMPT_PAIRS.keys())) - - # Connect number of AIs selector to update visibility - self.num_ais_selector.currentTextChanged.connect(self.update_ai_selector_visibility) - - # Set initial visibility based on default number of AIs (3) - self.update_ai_selector_visibility("3") - - def update_ai_selector_visibility(self, num_ais_text): - """Show/hide AI model selectors based on number of AIs selected""" - num_ais = int(num_ais_text) - - # AI-1 is always visible - # AI-2 visible if num_ais >= 2 - # AI-3 visible if num_ais >= 3 - # AI-4 visible if num_ais >= 4 - # AI-5 visible if num_ais >= 5 - - self.ai1_container.setVisible(num_ais >= 1) - self.ai2_container.setVisible(num_ais >= 2) - self.ai3_container.setVisible(num_ais >= 3) - self.ai4_container.setVisible(num_ais >= 4) - self.ai5_container.setVisible(num_ais >= 5) - -class ConversationContextMenu(QMenu): - """Context menu for the conversation display""" - rabbitholeSelected = pyqtSignal() - forkSelected = pyqtSignal() - - def __init__(self, parent=None): - super().__init__(parent) - - # Create actions - self.rabbithole_action = QAction("🕳️ Rabbithole", self) - self.fork_action = QAction("🔱 Fork", self) - - # Add actions to menu - # NOTE: Fork/Rabbithole temporarily disabled - needs rebuild - # self.addAction(self.rabbithole_action) - # self.addAction(self.fork_action) - - # Connect actions to signals - # self.rabbithole_action.triggered.connect(self.on_rabbithole_selected) - # self.fork_action.triggered.connect(self.on_fork_selected) - - # Apply styling - self.setStyleSheet(""" - QMenu { - background-color: #2D2D30; - color: #D4D4D4; - border: 1px solid #3E3E42; - } - QMenu::item { - padding: 5px 20px 5px 20px; - } - QMenu::item:selected { - background-color: #3E3E42; - } - """) - - def on_rabbithole_selected(self): - """Signal that rabbithole action was selected""" - if self.parent() and hasattr(self.parent(), 'rabbithole_from_selection'): - cursor = self.parent().conversation_display.textCursor() - selected_text = cursor.selectedText() - if selected_text and hasattr(self.parent(), 'rabbithole_callback'): - self.parent().rabbithole_callback(selected_text) - - def on_fork_selected(self): - """Signal that fork action was selected""" - if self.parent() and hasattr(self.parent(), 'fork_from_selection'): - cursor = self.parent().conversation_display.textCursor() - selected_text = cursor.selectedText() - if selected_text and hasattr(self.parent(), 'fork_callback'): - self.parent().fork_callback(selected_text) - -class ConversationPane(QWidget): - """Left pane containing the conversation and input area""" - def __init__(self): - super().__init__() - - # Set up the UI - self.setup_ui() - - # Connect signals and slots - self.connect_signals() - - # Initialize state - self.conversation = [] - self.input_callback = None - self.rabbithole_callback = None - self.fork_callback = None - self.loading = False - self.loading_dots = 0 - self.loading_timer = QTimer() - self.loading_timer.timeout.connect(self.update_loading_animation) - self.loading_timer.setInterval(300) # Update every 300ms for smoother animation - - # Context menu - self.context_menu = ConversationContextMenu(self) - - # Initialize with empty conversation - self.update_conversation([]) - - # Images list - to prevent garbage collection - self.images = [] - self.image_paths = [] - - # Uploaded image for current message - self.uploaded_image_path = None - self.uploaded_image_base64 = None - - # Create text formats with different colors - self.text_formats = { - "user": QTextCharFormat(), - "ai": QTextCharFormat(), - "system": QTextCharFormat(), - "ai_label": QTextCharFormat(), - "normal": QTextCharFormat(), - "error": QTextCharFormat() - } - - # Configure text formats using global color palette - self.text_formats["user"].setForeground(QColor(COLORS['text_normal'])) - self.text_formats["ai"].setForeground(QColor(COLORS['text_normal'])) - self.text_formats["system"].setForeground(QColor(COLORS['text_normal'])) - self.text_formats["ai_label"].setForeground(QColor(COLORS['accent_blue'])) - self.text_formats["normal"].setForeground(QColor(COLORS['text_normal'])) - self.text_formats["error"].setForeground(QColor(COLORS['text_error'])) - - # Make AI labels bold - self.text_formats["ai_label"].setFontWeight(QFont.Weight.Bold) - - def setup_ui(self): - """Set up the user interface for the conversation pane""" - # Main layout - layout = QVBoxLayout(self) - layout.setContentsMargins(10, 10, 10, 10) - layout.setSpacing(5) # Reduced spacing - - # Title and info area - title_layout = QHBoxLayout() - self.title_label = QLabel("╔═ LIMINAL BACKROOMS ═╗") - self.title_label.setStyleSheet(f""" - color: {COLORS['accent_cyan']}; - font-size: 14px; - font-weight: bold; - padding: 4px; - letter-spacing: 2px; - """) - - self.info_label = QLabel("[ AI-TO-AI PROPAGATION ]") - self.info_label.setStyleSheet(f""" - color: {COLORS['text_glow']}; - font-size: 10px; - padding: 2px; - letter-spacing: 1px; - """) - - title_layout.addWidget(self.title_label) - title_layout.addStretch() - title_layout.addWidget(self.info_label) - - layout.addLayout(title_layout) - - # Conversation display (read-only text edit in a scroll area) - self.conversation_display = QTextEdit() - self.conversation_display.setReadOnly(True) - self.conversation_display.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.conversation_display.customContextMenuRequested.connect(self.show_context_menu) - - # Set font for conversation display - use Iosevka Term for better ASCII art rendering - font = QFont("Iosevka Term", 10) - # Set fallbacks in case Iosevka Term isn't loaded - font.setStyleHint(QFont.StyleHint.Monospace) - self.conversation_display.setFont(font) - - # Apply cyberpunk styling - self.conversation_display.setStyleSheet(f""" - QTextEdit {{ - background-color: {COLORS['bg_dark']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border_glow']}; - border-radius: 0px; - padding: 15px; - selection-background-color: {COLORS['accent_cyan']}; - selection-color: {COLORS['bg_dark']}; - }} - QScrollBar:vertical {{ - background: {COLORS['bg_medium']}; - width: 10px; - margin: 0px; - }} - QScrollBar::handle:vertical {{ - background: {COLORS['border_glow']}; - min-height: 20px; - border-radius: 0px; - }} - QScrollBar::handle:vertical:hover {{ - background: {COLORS['accent_cyan']}; - }} - QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ - height: 0px; - }} - QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{ - background: none; - }} - """) - - # Input area with label - input_container = QWidget() - input_layout = QVBoxLayout(input_container) - input_layout.setContentsMargins(0, 0, 0, 0) - input_layout.setSpacing(2) # Reduced spacing - - input_label = QLabel("Your message:") - input_label.setStyleSheet(f"color: {COLORS['text_dim']}; font-size: 11px;") - input_layout.addWidget(input_label) - - # Input field with modern styling - self.input_field = QTextEdit() - self.input_field.setPlaceholderText("Seed the conversation or just click propagate...") - self.input_field.setMaximumHeight(60) # Reduced height - self.input_field.setFont(font) - self.input_field.setStyleSheet(f""" - QTextEdit {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border_glow']}; - border-radius: 0px; - padding: 8px; - selection-background-color: {COLORS['accent_cyan']}; - selection-color: {COLORS['bg_dark']}; - }} - """) - input_layout.addWidget(self.input_field) - - # Button container for better layout - button_container = QWidget() - button_layout = QHBoxLayout(button_container) - button_layout.setContentsMargins(0, 0, 0, 0) - button_layout.setSpacing(5) # Reduced spacing - - # Upload image button - self.upload_image_button = QPushButton("📎 IMAGE") - self.upload_image_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border_glow']}; - border-radius: 0px; - padding: 6px 10px; - font-weight: bold; - font-size: 10px; - letter-spacing: 1px; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_light']}; - border: 1px solid {COLORS['accent_cyan']}; - color: {COLORS['accent_cyan']}; - }} - QPushButton:pressed {{ - background-color: {COLORS['border_glow']}; - }} - """) - self.upload_image_button.setToolTip("Upload an image to include in your message") - - # Clear button with subtle glow - self.clear_button = GlowButton("CLEAR", COLORS['accent_pink']) - self.clear_button.shadow.setBlurRadius(5) # Subtler glow - self.clear_button.base_blur = 5 - self.clear_button.hover_blur = 12 - self.clear_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['bg_medium']}; - color: {COLORS['text_normal']}; - border: 1px solid {COLORS['border_glow']}; - border-radius: 3px; - padding: 8px 12px; - font-weight: bold; - font-size: 10px; - letter-spacing: 1px; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_light']}; - border: 2px solid {COLORS['accent_pink']}; - color: {COLORS['accent_pink']}; - }} - QPushButton:pressed {{ - background-color: {COLORS['border_glow']}; - }} - """) - - # Submit button with cyberpunk styling and glow effect - self.submit_button = GlowButton("⚡ PROPAGATE", COLORS['accent_cyan']) - self.submit_button.setStyleSheet(f""" - QPushButton {{ - background-color: {COLORS['accent_cyan']}; - color: {COLORS['bg_dark']}; - border: 2px solid {COLORS['accent_cyan']}; - border-radius: 3px; - padding: 8px 20px; - font-weight: bold; - font-size: 11px; - letter-spacing: 2px; - }} - QPushButton:hover {{ - background-color: {COLORS['bg_dark']}; - color: {COLORS['accent_cyan']}; - border: 2px solid {COLORS['accent_cyan']}; - }} - QPushButton:pressed {{ - background-color: {COLORS['accent_cyan_active']}; - color: {COLORS['text_bright']}; - }} - QPushButton:disabled {{ - background-color: {COLORS['border']}; - color: {COLORS['text_dim']}; - border: 2px solid {COLORS['border']}; - }} - """) - - # Add buttons to layout - button_layout.addWidget(self.upload_image_button) - button_layout.addWidget(self.clear_button) - button_layout.addStretch() - button_layout.addWidget(self.submit_button) - - # Add input container to main layout - input_layout.addWidget(button_container) - - # Add widgets to layout with adjusted stretch factors - layout.addWidget(self.conversation_display, 1) # Main conversation area gets most space - layout.addWidget(input_container, 0) # Input area gets minimal space - - def connect_signals(self): - """Connect signals and slots""" - # Submit button - self.submit_button.clicked.connect(self.handle_propagate_click) - - # Upload image button - self.upload_image_button.clicked.connect(self.handle_upload_image) - - # Clear button - self.clear_button.clicked.connect(self.clear_input) - - # Enter key in input field - self.input_field.installEventFilter(self) - - def clear_input(self): - """Clear the input field""" - self.input_field.clear() - self.uploaded_image_path = None - self.uploaded_image_base64 = None - self.upload_image_button.setText("📎 Image") - self.input_field.setFocus() - - def handle_upload_image(self): - """Handle image upload button click""" - # Open file dialog - file_dialog = QFileDialog() - file_path, _ = file_dialog.getOpenFileName( - self, - "Select Image", - "", - "Image Files (*.png *.jpg *.jpeg *.gif *.webp);;All Files (*)" - ) - - if file_path: - try: - # Read and encode the image to base64 - with open(file_path, 'rb') as image_file: - image_data = image_file.read() - image_base64 = base64.b64encode(image_data).decode('utf-8') - - # Determine media type - file_extension = os.path.splitext(file_path)[1].lower() - media_type_map = { - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.gif': 'image/gif', - '.webp': 'image/webp' - } - media_type = media_type_map.get(file_extension, 'image/jpeg') - - # Store the image data - self.uploaded_image_path = file_path - self.uploaded_image_base64 = { - 'data': image_base64, - 'media_type': media_type - } - - # Update button text to show an image is attached - file_name = os.path.basename(file_path) - self.upload_image_button.setText(f"📎 {file_name[:15]}...") - - # Update placeholder text - self.input_field.setPlaceholderText("Add a message about your image (optional)...") - - except Exception as e: - QMessageBox.warning( - self, - "Upload Error", - f"Failed to load image: {str(e)}" - ) - - def eventFilter(self, obj, event): - """Filter events to handle Enter key in input field""" - if obj is self.input_field and event.type() == QEvent.Type.KeyPress: - if event.key() == Qt.Key.Key_Return and not event.modifiers() & Qt.KeyboardModifier.ShiftModifier: - self.handle_propagate_click() - return True - return super().eventFilter(obj, event) - - def handle_propagate_click(self): - """Handle click on the propagate button""" - # Get the input text (might be empty) - input_text = self.input_field.toPlainText().strip() - - # Prepare message data (text + optional image) - message_data = { - 'text': input_text, - 'image': None - } - - # Include image if one was uploaded - if self.uploaded_image_base64: - message_data['image'] = { - 'path': self.uploaded_image_path, - 'base64': self.uploaded_image_base64['data'], - 'media_type': self.uploaded_image_base64['media_type'] - } - - # Clear the input box and image - self.input_field.clear() - self.uploaded_image_path = None - self.uploaded_image_base64 = None - self.upload_image_button.setText("📎 Image") - self.input_field.setPlaceholderText("Seed the conversation or just click propagate...") - - # Always call the input callback, even with empty input - if hasattr(self, 'input_callback') and self.input_callback: - self.input_callback(message_data) - - # Start loading animation - self.start_loading() - - def set_input_callback(self, callback): - """Set callback function for input submission""" - self.input_callback = callback - - def set_rabbithole_callback(self, callback): - """Set callback function for rabbithole creation""" - self.rabbithole_callback = callback - - def set_fork_callback(self, callback): - """Set callback function for fork creation""" - self.fork_callback = callback - - def update_conversation(self, conversation): - """Update conversation display""" - self.conversation = conversation - self.render_conversation() - - def render_conversation(self): - """Render conversation in the display""" - # Save scroll position before re-rendering - scrollbar = self.conversation_display.verticalScrollBar() - old_scroll_value = scrollbar.value() - old_scroll_max = scrollbar.maximum() - was_at_bottom = old_scroll_value >= old_scroll_max - 20 - - # Clear display - self.conversation_display.clear() - - # Create HTML for conversation with modern styling - html = "" - - for i, message in enumerate(self.conversation): - role = message.get("role", "") - content = message.get("content", "") - ai_name = message.get("ai_name", "") - model = message.get("model", "") - - # Handle structured content (with images) - has_image = False - image_base64 = None - generated_image_path = None - text_content = "" - - # Check for generated image path (from AI image generation) - if hasattr(message, "get") and callable(message.get): - generated_image_path = message.get("generated_image_path", None) - if generated_image_path and os.path.exists(generated_image_path): - has_image = True - - if isinstance(content, list): - # Structured content with potential images - for part in content: - if part.get('type') == 'text': - text_content += part.get('text', '') - elif part.get('type') == 'image': - has_image = True - source = part.get('source', {}) - if source.get('type') == 'base64': - image_base64 = source.get('data', '') - else: - # Plain text content - text_content = content - - # Skip empty messages (no text and no image) - if not text_content and not has_image: - continue - - # Handle branch indicators with special styling - if role == 'system' and message.get('_type') == 'branch_indicator': - if "Rabbitholing down:" in content: - html += f'
{code_content}'
- result.append(formatted_code)
- except Exception as e:
- # If there's an error, just add the original escaped content
- print(f"Error processing code block: {e}")
- result.append(part)
- else:
- # Process inline code in non-code-block parts
- inline_parts = re.split(r'(`[^`]+`)', part)
- processed_part = []
-
- for inline_part in inline_parts:
- if inline_part.startswith("`") and inline_part.endswith("`") and len(inline_part) > 2:
- # This is inline code
- code = inline_part[1:-1]
- processed_part.append(f'{code}')
- else:
- processed_part.append(inline_part)
-
- result.append(''.join(processed_part))
-
- return ''.join(result)
-
- def start_loading(self):
- """Start loading animation"""
- self.loading = True
- self.loading_dots = 0
- self.input_field.setEnabled(False)
- self.submit_button.setEnabled(False)
- self.submit_button.setText("Processing")
- self.loading_timer.start()
-
- # Add subtle pulsing animation to the button
- self.pulse_animation = QPropertyAnimation(self.submit_button, b"styleSheet")
- self.pulse_animation.setDuration(1000)
- self.pulse_animation.setLoopCount(-1) # Infinite loop
-
- # Define keyframes for the animation
- normal_style = f"""
- QPushButton {{
- background-color: {COLORS['border']};
- color: {COLORS['text_dim']};
- border: none;
- border-radius: 4px;
- padding: 4px 12px;
- font-weight: bold;
- font-size: 11px;
- }}
- """
-
- pulse_style = f"""
- QPushButton {{
- background-color: {COLORS['border_highlight']};
- color: {COLORS['text_dim']};
- border: none;
- border-radius: 4px;
- padding: 4px 12px;
- font-weight: bold;
- font-size: 11px;
- }}
- """
-
- self.pulse_animation.setStartValue(normal_style)
- self.pulse_animation.setEndValue(pulse_style)
- self.pulse_animation.start()
-
- def stop_loading(self):
- """Stop loading animation"""
- self.loading = False
- self.loading_timer.stop()
- self.input_field.setEnabled(True)
- self.submit_button.setEnabled(True)
- self.submit_button.setText("Propagate")
-
- # Stop the pulsing animation
- if hasattr(self, 'pulse_animation'):
- self.pulse_animation.stop()
-
- # Reset button style
- self.submit_button.setStyleSheet(f"""
- QPushButton {{
- background-color: {COLORS['accent_cyan']};
- color: {COLORS['bg_dark']};
- border: 1px solid {COLORS['accent_cyan']};
- border-radius: 0px;
- padding: 6px 16px;
- font-weight: bold;
- font-size: 11px;
- letter-spacing: 1px;
- }}
- QPushButton:hover {{
- background-color: {COLORS['bg_dark']};
- color: {COLORS['accent_cyan']};
- }}
- QPushButton:pressed {{
- background-color: {COLORS['accent_cyan_active']};
- color: {COLORS['text_bright']};
- }}
- """)
-
- def update_loading_animation(self):
- """Update loading animation dots"""
- self.loading_dots = (self.loading_dots + 1) % 4
- dots = "." * self.loading_dots
- self.submit_button.setText(f"Processing{dots}")
-
- def show_context_menu(self, position):
- """Show context menu at the given position"""
- # Get selected text
- cursor = self.conversation_display.textCursor()
- selected_text = cursor.selectedText()
-
- # Only show context menu if text is selected
- if selected_text:
- # Show the context menu at cursor position
- self.context_menu.exec(self.conversation_display.mapToGlobal(position))
-
- def rabbithole_from_selection(self):
- """Create a rabbithole branch from selected text"""
- cursor = self.conversation_display.textCursor()
- selected_text = cursor.selectedText()
-
- if selected_text and hasattr(self, 'rabbithole_callback'):
- self.rabbithole_callback(selected_text)
-
- def fork_from_selection(self):
- """Create a fork branch from selected text"""
- cursor = self.conversation_display.textCursor()
- selected_text = cursor.selectedText()
-
- if selected_text and hasattr(self, 'fork_callback'):
- self.fork_callback(selected_text)
-
- def append_text(self, text, format_type="normal"):
- """Append text to the conversation display with the specified format"""
- # Check if user is at the bottom before appending (within 20 pixels is considered "at bottom")
- scrollbar = self.conversation_display.verticalScrollBar()
- was_at_bottom = scrollbar.value() >= scrollbar.maximum() - 20
-
- cursor = self.conversation_display.textCursor()
- cursor.movePosition(QTextCursor.MoveOperation.End)
-
- # Apply the format if specified
- if format_type in self.text_formats:
- self.conversation_display.setCurrentCharFormat(self.text_formats[format_type])
-
- # Insert the text
- cursor.insertText(text)
-
- # Reset to normal format after insertion
- if format_type != "normal":
- self.conversation_display.setCurrentCharFormat(self.text_formats["normal"])
-
- # Only auto-scroll if user was already at the bottom
- if was_at_bottom:
- self.conversation_display.setTextCursor(cursor)
- self.conversation_display.ensureCursorVisible()
-
- def clear_conversation(self):
- """Clear the conversation display"""
- self.conversation_display.clear()
- self.images = []
-
- def display_conversation(self, conversation, branch_data=None):
- """Display the conversation in the text edit widget"""
- # Store conversation data (don't clear here - render_conversation handles clearing with scroll preservation)
- self.conversation = conversation
-
- # Check if we're in a branch
- is_branch = branch_data is not None
- branch_type = branch_data.get('type', '') if is_branch else ''
- selected_text = branch_data.get('selected_text', '') if is_branch else ''
-
- # Update title if in a branch
- if is_branch:
- branch_emoji = "🐇" if branch_type == "rabbithole" else "🍴"
- self.title_label.setText(f"{branch_emoji} {branch_type.capitalize()}: {selected_text[:30]}...")
- self.info_label.setText(f"Branch conversation")
- else:
- self.title_label.setText("Liminal Backrooms")
- self.info_label.setText("AI-to-AI conversation")
-
- # Debug: Print conversation to console
- print("\n--- DEBUG: Conversation Content ---")
- for msg in conversation:
- role = msg.get("role", "")
- content = msg.get("content", "")
- if "```" in content:
- print(f"Found code block in {role} message")
- print(f"Content snippet: {content[:100]}...")
- print("--- End Debug ---\n")
-
- # Render conversation
- self.render_conversation()
-
- def display_image(self, image_path):
- """Display an image in the conversation"""
- try:
- # Check if the image path is valid
- if not image_path or not os.path.exists(image_path):
- self.append_text("[Image not found]\n", "error")
- return
-
- # Load the image
- image = QImage(image_path)
- if image.isNull():
- self.append_text("[Invalid image format]\n", "error")
- return
-
- # Create a pixmap from the image
- pixmap = QPixmap.fromImage(image)
-
- # Scale the image to fit the conversation display
- max_width = self.conversation_display.width() - 50
- if pixmap.width() > max_width:
- pixmap = pixmap.scaledToWidth(max_width, Qt.TransformationMode.SmoothTransformation)
-
- # Insert the image into the conversation display
- cursor = self.conversation_display.textCursor()
- cursor.movePosition(QTextCursor.MoveOperation.End)
- cursor.insertImage(pixmap.toImage())
- cursor.insertText("\n\n")
-
- # Store the image to prevent garbage collection
- self.images.append(pixmap)
- self.image_paths.append(image_path)
-
- except Exception as e:
- self.append_text(f"[Error displaying image: {str(e)}]\n", "error")
-
- def export_conversation(self):
- """Export the conversation and all session media to a folder"""
- # Set default directory - custom Dropbox path with fallbacks
- base_dir = r"C:\Users\sjeff\Dropbox\Stephen Work\LiminalBackrooms"
-
- # Fallback if that path doesn't exist
- if not os.path.exists(os.path.dirname(base_dir)):
- documents_path = os.path.join(os.path.expanduser("~"), "Documents")
- if os.path.exists(documents_path):
- base_dir = os.path.join(documents_path, "LiminalBackrooms")
- else:
- base_dir = os.path.join(os.getcwd(), "exports")
-
- # Create the base directory if it doesn't exist
- os.makedirs(base_dir, exist_ok=True)
-
- # Generate a session folder name based on date/time
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- default_folder = os.path.join(base_dir, f"session_{timestamp}")
-
- # Get the folder from a dialog
- folder_name = QFileDialog.getExistingDirectory(
- self,
- "Select Export Folder (or create new)",
- base_dir,
- QFileDialog.Option.ShowDirsOnly
- )
-
- # If user cancelled, offer to create the default folder
- if not folder_name:
- reply = QMessageBox.question(
- self,
- "Create Export Folder?",
- f"Create new export folder?\n\n{default_folder}",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
- )
- if reply == QMessageBox.StandardButton.Yes:
- folder_name = default_folder
- else:
- return
-
- try:
- # Create the export folder
- os.makedirs(folder_name, exist_ok=True)
-
- # Get main window for accessing session data
- main_window = self.window()
-
- # Export conversation as multiple formats
- # Plain text
- text_path = os.path.join(folder_name, "conversation.txt")
- with open(text_path, 'w', encoding='utf-8') as f:
- f.write(self.conversation_display.toPlainText())
-
- # HTML
- html_path = os.path.join(folder_name, "conversation.html")
- with open(html_path, 'w', encoding='utf-8') as f:
- f.write(self.conversation_display.toHtml())
-
- # Full HTML document if it exists
- full_html_path = os.path.join(os.getcwd(), "conversation_full.html")
- if os.path.exists(full_html_path):
- shutil.copy2(full_html_path, os.path.join(folder_name, "conversation_full.html"))
-
- # Copy session images
- images_copied = 0
- if hasattr(main_window, 'right_sidebar') and hasattr(main_window.right_sidebar, 'image_preview_pane'):
- session_images = main_window.right_sidebar.image_preview_pane.session_images
- if session_images:
- images_dir = os.path.join(folder_name, "images")
- os.makedirs(images_dir, exist_ok=True)
- for img_path in session_images:
- if os.path.exists(img_path):
- shutil.copy2(img_path, images_dir)
- images_copied += 1
-
- # Copy session videos
- videos_copied = 0
- if hasattr(main_window, 'session_videos'):
- session_videos = main_window.session_videos
- if session_videos:
- videos_dir = os.path.join(folder_name, "videos")
- os.makedirs(videos_dir, exist_ok=True)
- for vid_path in session_videos:
- if os.path.exists(vid_path):
- shutil.copy2(vid_path, videos_dir)
- videos_copied += 1
-
- # Create a manifest/summary file
- manifest_path = os.path.join(folder_name, "manifest.txt")
- with open(manifest_path, 'w', encoding='utf-8') as f:
- f.write(f"Liminal Backrooms Session Export\n")
- f.write(f"================================\n")
- f.write(f"Exported: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
- f.write(f"Contents:\n")
- f.write(f"- conversation.txt (plain text)\n")
- f.write(f"- conversation.html (HTML format)\n")
- if os.path.exists(os.path.join(folder_name, "conversation_full.html")):
- f.write(f"- conversation_full.html (styled document)\n")
- f.write(f"- images/ ({images_copied} files)\n")
- f.write(f"- videos/ ({videos_copied} files)\n")
-
- # Status message
- status_msg = f"Exported to {folder_name} ({images_copied} images, {videos_copied} videos)"
- main_window.statusBar().showMessage(status_msg)
- print(f"Session exported to {folder_name}")
- print(f" - {images_copied} images")
- print(f" - {videos_copied} videos")
-
- # Show success message
- QMessageBox.information(
- self,
- "Export Complete",
- f"Session exported successfully!\n\n"
- f"Location: {folder_name}\n\n"
- f"• Conversation (txt, html)\n"
- f"• {images_copied} images\n"
- f"• {videos_copied} videos"
- )
-
- except Exception as e:
- error_msg = f"Error exporting session: {str(e)}"
- QMessageBox.critical(self, "Export Error", error_msg)
- print(error_msg)
- import traceback
- traceback.print_exc()
-
-
-class CentralContainer(QWidget):
- """Central container widget with animated background and overlay support"""
-
- def __init__(self, parent=None):
- super().__init__(parent)
-
- # Background animation state
- self.bg_offset = 0
- self.noise_offset = 0
-
- # Animation timer for background
- self.bg_timer = QTimer(self)
- self.bg_timer.timeout.connect(self._animate_bg)
- self.bg_timer.start(80) # ~12 FPS for subtle movement
-
- # Create scanline overlay as child widget
- self.scanline_overlay = ScanlineOverlayWidget(self)
- self.scanline_overlay.hide()
-
- def _animate_bg(self):
- self.bg_offset = (self.bg_offset + 1) % 360
- self.noise_offset = (self.noise_offset + 0.5) % 100
- self.update()
-
- def set_scanlines_enabled(self, enabled):
- """Toggle scanline effect"""
- if enabled:
- # Ensure overlay has correct geometry before showing
- self.scanline_overlay.setGeometry(self.rect())
- self.scanline_overlay.show()
- self.scanline_overlay.raise_()
- self.scanline_overlay.start_animation()
- else:
- self.scanline_overlay.stop_animation()
- self.scanline_overlay.hide()
-
- def resizeEvent(self, event):
- """Update scanline overlay size when container resizes"""
- super().resizeEvent(event)
- self.scanline_overlay.setGeometry(self.rect())
-
- def paintEvent(self, event):
- painter = QPainter(self)
- painter.setRenderHint(QPainter.RenderHint.Antialiasing)
-
- # ═══ ANIMATED BACKGROUND ═══
- # Create shifting gradient with more visible movement
- center_x = self.width() / 2 + math.sin(math.radians(self.bg_offset)) * 100
- center_y = self.height() / 2 + math.cos(math.radians(self.bg_offset * 0.7)) * 60
-
- gradient = QRadialGradient(center_x, center_y, max(self.width(), self.height()) * 0.9)
-
- # More visible atmospheric colors with cyan tint
- pulse = 0.5 + 0.5 * math.sin(math.radians(self.bg_offset * 2))
- center_r = int(10 + 8 * pulse)
- center_g = int(15 + 10 * pulse)
- center_b = int(30 + 15 * pulse)
-
- gradient.setColorAt(0, QColor(center_r, center_g, center_b))
- gradient.setColorAt(0.4, QColor(10, 14, 26))
- gradient.setColorAt(1, QColor(6, 8, 14))
-
- painter.fillRect(self.rect(), gradient)
-
- # Add subtle glow lines at edges
- glow_alpha = int(15 + 10 * pulse)
- glow_color = QColor(6, 182, 212, glow_alpha) # Cyan glow
- painter.setPen(QPen(glow_color, 2))
-
- # Top edge glow
- painter.drawLine(0, 0, self.width(), 0)
- # Bottom edge glow
- painter.drawLine(0, self.height() - 1, self.width(), self.height() - 1)
- # Left edge glow
- painter.drawLine(0, 0, 0, self.height())
- # Right edge glow
- painter.drawLine(self.width() - 1, 0, self.width() - 1, self.height())
-
- # Add subtle noise/grain pattern
- noise_color = QColor(COLORS['accent_cyan'])
- noise_color.setAlpha(8)
- painter.setPen(Qt.PenStyle.NoPen)
- painter.setBrush(noise_color)
-
- # Sparse random dots for grain effect
- random.seed(int(self.noise_offset))
- for _ in range(50):
- x = random.randint(0, self.width())
- y = random.randint(0, self.height())
- painter.drawEllipse(x, y, 1, 1)
-
-
-class ScanlineOverlayWidget(QWidget):
- """Transparent overlay widget for CRT scanline effect"""
-
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
- self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
-
- self.scanline_offset = 0
- self.intensity = 0.25 # More visible scanlines
-
- self.anim_timer = QTimer(self)
- self.anim_timer.timeout.connect(self._animate)
-
- def start_animation(self):
- self.anim_timer.start(100)
-
- def stop_animation(self):
- self.anim_timer.stop()
-
- def _animate(self):
- self.scanline_offset = (self.scanline_offset + 1) % 4
- self.update()
-
- def paintEvent(self, event):
- painter = QPainter(self)
-
- # Draw horizontal scanlines - more visible
- line_alpha = int(255 * self.intensity)
- line_color = QColor(0, 0, 0, line_alpha)
- painter.setPen(QPen(line_color, 1))
-
- # Draw every 2nd line for more visible effect
- for y in range(self.scanline_offset, self.height(), 2):
- painter.drawLine(0, y, self.width(), y)
-
- # Subtle vignette effect at edges
- gradient = QRadialGradient(self.width() / 2, self.height() / 2,
- max(self.width(), self.height()) * 0.7)
- gradient.setColorAt(0, QColor(0, 0, 0, 0))
- gradient.setColorAt(0.7, QColor(0, 0, 0, 0))
- gradient.setColorAt(1, QColor(0, 0, 0, int(255 * self.intensity * 1.5)))
-
- painter.setPen(Qt.PenStyle.NoPen)
- painter.setBrush(gradient)
- painter.drawRect(self.rect())
-
-
-class LiminalBackroomsApp(QMainWindow):
- """Main application window"""
- def __init__(self):
- super().__init__()
-
- # Main app state
- self.conversation = []
- self.turn_count = 0
- self.images = []
- self.image_paths = []
- self.session_videos = [] # Track videos generated this session
- self.branch_conversations = {} # Store branch conversations by ID
- self.active_branch = None # Currently displayed branch
-
- # Set up the UI
- self.setup_ui()
-
- # Connect signals and slots
- self.connect_signals()
-
- # Dark theme
- self.apply_dark_theme()
-
- # Restore splitter state if available
- self.restore_splitter_state()
-
- # Start maximized
- self.showMaximized()
-
- def setup_ui(self):
- """Set up the user interface"""
- self.setWindowTitle("╔═ LIMINAL BACKROOMS v0.7 ═╗")
- self.setGeometry(100, 100, 1600, 900) # Initial size before maximizing
- self.setMinimumSize(1200, 800)
-
- # Create central widget - this will be a custom widget that paints the background
- self.central_container = CentralContainer()
- self.setCentralWidget(self.central_container)
-
- # Main layout for content
- main_layout = QHBoxLayout(self.central_container)
- main_layout.setContentsMargins(10, 10, 10, 10)
- main_layout.setSpacing(5)
-
- # Create horizontal splitter for left and right panes
- self.splitter = QSplitter(Qt.Orientation.Horizontal)
- self.splitter.setHandleWidth(8) # Make the handle wider for easier grabbing
- self.splitter.setChildrenCollapsible(False) # Prevent panes from being collapsed
- self.splitter.setStyleSheet(f"""
- QSplitter::handle {{
- background-color: {COLORS['border']};
- border: 1px solid {COLORS['border_highlight']};
- border-radius: 2px;
- }}
- QSplitter::handle:hover {{
- background-color: {COLORS['accent_blue']};
- }}
- """)
- main_layout.addWidget(self.splitter)
-
- # Create left pane (conversation) and right sidebar (tabbed: setup + network)
- self.left_pane = ConversationPane()
- self.right_sidebar = RightSidebar()
-
- self.splitter.addWidget(self.left_pane)
- self.splitter.addWidget(self.right_sidebar)
-
- # Set initial splitter sizes (70:30 ratio - more space for conversation)
- total_width = 1600 # Based on default window width
- self.splitter.setSizes([int(total_width * 0.70), int(total_width * 0.30)])
-
- # Initialize main conversation as root node
- self.right_sidebar.add_node('main', 'Seed', 'main')
-
- # ═══ SIGNAL INDICATOR ═══
- self.signal_indicator = SignalIndicator()
-
- # Status bar with modern styling
- self.statusBar().setStyleSheet(f"""
- QStatusBar {{
- background-color: {COLORS['bg_dark']};
- color: {COLORS['text_dim']};
- border-top: 1px solid {COLORS['border']};
- padding: 3px;
- font-size: 11px;
- }}
- """)
- self.statusBar().showMessage("Ready")
-
- # Add notification label for agent actions (shows latest notification)
- self.notification_label = QLabel("")
- self.notification_label.setStyleSheet(f"""
- QLabel {{
- color: {COLORS['accent_cyan']};
- font-size: 11px;
- padding: 2px 10px;
- background-color: transparent;
- }}
- """)
- self.notification_label.setMaximumWidth(500)
- self.statusBar().addWidget(self.notification_label, 1)
-
- # Add signal indicator to status bar
- self.statusBar().addPermanentWidget(self.signal_indicator)
-
- # ═══ CRT TOGGLE CHECKBOX ═══
- self.crt_checkbox = QCheckBox("CRT")
- self.crt_checkbox.setStyleSheet(f"""
- QCheckBox {{
- color: {COLORS['text_dim']};
- font-size: 10px;
- spacing: 4px;
- }}
- QCheckBox::indicator {{
- width: 12px;
- height: 12px;
- border: 1px solid {COLORS['border_glow']};
- border-radius: 2px;
- background: {COLORS['bg_dark']};
- }}
- QCheckBox::indicator:checked {{
- background: {COLORS['accent_cyan']};
- }}
- """)
- self.crt_checkbox.setToolTip("Toggle CRT scanline effect")
- self.crt_checkbox.toggled.connect(self.toggle_crt_effect)
- self.statusBar().addPermanentWidget(self.crt_checkbox)
-
- # Set up input callback
- self.left_pane.set_input_callback(self.handle_user_input)
-
- def toggle_crt_effect(self, enabled):
- """Toggle the CRT scanline effect"""
- if hasattr(self, 'central_container'):
- self.central_container.set_scanlines_enabled(enabled)
-
- def set_signal_active(self, active):
- """Set signal indicator to active (waiting for response)"""
- self.signal_indicator.set_active(active)
-
- def update_signal_latency(self, latency_ms):
- """Update signal indicator with response latency"""
- self.signal_indicator.set_latency(latency_ms)
-
- def connect_signals(self):
- """Connect all signals and slots"""
- # Node selection in network view
- self.right_sidebar.nodeSelected.connect(self.on_branch_select)
-
- # Node hover in network view
- if hasattr(self.right_sidebar.network_pane.network_view, 'nodeHovered'):
- self.right_sidebar.network_pane.network_view.nodeHovered.connect(self.on_node_hover)
-
- # Export button
- self.right_sidebar.control_panel.export_button.clicked.connect(self.export_conversation)
-
- # BackroomsBench evaluation button
- self.right_sidebar.control_panel.backroomsbench_button.clicked.connect(self.run_backroomsbench_evaluation)
-
- # Connect context menu actions to the main app methods
- self.left_pane.set_rabbithole_callback(self.branch_from_selection)
- self.left_pane.set_fork_callback(self.fork_from_selection)
-
- # Save splitter state when it moves
- self.splitter.splitterMoved.connect(self.save_splitter_state)
-
- def handle_user_input(self, text):
- """Handle user input from the conversation pane"""
- # Add user message to conversation
- if text:
- user_message = {
- "role": "user",
- "content": text
- }
- self.conversation.append(user_message)
-
- # Update conversation display
- self.left_pane.update_conversation(self.conversation)
-
- # Process the conversation (this will be implemented in main.py)
- if hasattr(self, 'process_conversation'):
- self.process_conversation()
-
- def append_text(self, text, format_type="normal"):
- """Append text to the conversation display with the specified format"""
- self.left_pane.append_text(text, format_type)
-
- def clear_conversation(self):
- """Clear the conversation display and reset images"""
- self.left_pane.clear_conversation()
- self.conversation = []
- self.images = []
- self.image_paths = []
-
- def display_conversation(self, conversation, branch_data=None):
- """Display the conversation in the text edit widget"""
- self.left_pane.display_conversation(conversation, branch_data)
-
- def display_image(self, image_path):
- """Display an image in the conversation"""
- self.left_pane.display_image(image_path)
-
- def export_conversation(self):
- """Export the current conversation"""
- self.left_pane.export_conversation()
-
- def run_shitpostbench_evaluation(self):
- """Run ShitpostBench multi-judge evaluation on current session."""
- from shitpostbench import run_shitpostbench
- from PyQt6.QtWidgets import QMessageBox, QProgressDialog
- from PyQt6.QtCore import Qt, QTimer
- import threading
- import subprocess
-
- # Get current conversation
- conversation = getattr(self, 'main_conversation', [])
- if len(conversation) < 5:
- QMessageBox.warning(
- self,
- "Not Enough Content",
- "Need at least 5 messages for a proper evaluation.\nKeep the chaos going! 🦝"
- )
- return
-
- # Get scenario name
- scenario = self.right_sidebar.control_panel.prompt_pair_selector.currentText()
-
- # Get participants - collect which AIs are active and their models
- participants = []
- selectors = [
- self.right_sidebar.control_panel.ai1_model_selector,
- self.right_sidebar.control_panel.ai2_model_selector,
- self.right_sidebar.control_panel.ai3_model_selector,
- self.right_sidebar.control_panel.ai4_model_selector,
- self.right_sidebar.control_panel.ai5_model_selector,
- ]
- for i, selector in enumerate(selectors, 1):
- model = selector.currentText()
- if model:
- participants.append(f"AI-{i}: {model}")
-
- # Show progress dialog
- progress = QProgressDialog(
- "🏆 Running ShitpostBench...\n\nSending to 3 judges (Opus, Gemini, GPT)",
- None, 0, 0, self
- )
- progress.setWindowTitle("ShitpostBench Evaluation")
- progress.setWindowModality(Qt.WindowModality.WindowModal)
- progress.setMinimumDuration(0)
- progress.show()
-
- # Store result for callback
- self._shitpostbench_result = None
- self._shitpostbench_error = None
- self._shitpostbench_progress = progress
-
- def run_eval():
- try:
- self._shitpostbench_result = run_shitpostbench(
- conversation=conversation,
- scenario_name=scenario,
- participant_models=participants
- )
- except Exception as e:
- print(f"[ShitpostBench] Error: {e}")
- self._shitpostbench_error = str(e)
-
- def check_complete():
- if self._shitpostbench_result is not None:
- # Success - close progress and show result
- progress.close()
- result = self._shitpostbench_result
- self.statusBar().showMessage(
- f"🏆 ShitpostBench complete! {result['summary']['successful_evaluations']}/3 judges filed reports"
- )
- # Open the reports folder
- try:
- subprocess.Popen(f'explorer "{result["output_dir"]}"')
- except Exception:
- pass
- self._shitpostbench_result = None
- self._check_timer.stop()
- elif self._shitpostbench_error is not None:
- # Error
- progress.close()
- QMessageBox.critical(
- self,
- "ShitpostBench Error",
- f"Evaluation failed:\n{self._shitpostbench_error}"
- )
- self._shitpostbench_error = None
- self._check_timer.stop()
-
- # Start background thread
- threading.Thread(target=run_eval, daemon=True).start()
-
- # Poll for completion
- self._check_timer = QTimer()
- self._check_timer.timeout.connect(check_complete)
- self._check_timer.start(500) # Check every 500ms
-
- def run_backroomsbench_evaluation(self):
- """Run BackroomsBench multi-judge evaluation on current session."""
- from backroomsbench import run_backroomsbench
- from PyQt6.QtWidgets import QMessageBox, QProgressDialog
- from PyQt6.QtCore import Qt, QTimer
- import threading
- import subprocess
-
- # Get current conversation
- conversation = getattr(self, 'main_conversation', [])
- if len(conversation) < 5:
- QMessageBox.warning(
- self,
- "Not Enough Content",
- "Need at least 5 messages for a proper evaluation.\nLet the dialogue deepen. 🌀"
- )
- return
-
- # Get scenario name
- scenario = self.right_sidebar.control_panel.prompt_pair_selector.currentText()
-
- # Get participants
- participants = []
- selectors = [
- self.right_sidebar.control_panel.ai1_model_selector,
- self.right_sidebar.control_panel.ai2_model_selector,
- self.right_sidebar.control_panel.ai3_model_selector,
- self.right_sidebar.control_panel.ai4_model_selector,
- self.right_sidebar.control_panel.ai5_model_selector,
- ]
- for i, selector in enumerate(selectors, 1):
- model = selector.currentText()
- if model:
- participants.append(f"AI-{i}: {model}")
-
- # Show progress dialog
- progress = QProgressDialog(
- "🌀 Running BackroomsBench...\n\nSending to 3 judges (Opus, Gemini, GPT)",
- None, 0, 0, self
- )
- progress.setWindowTitle("BackroomsBench Evaluation")
- progress.setWindowModality(Qt.WindowModality.WindowModal)
- progress.setMinimumDuration(0)
- progress.show()
-
- # Store result for callback
- self._backroomsbench_result = None
- self._backroomsbench_error = None
- self._backroomsbench_progress = progress
-
- def run_eval():
- try:
- self._backroomsbench_result = run_backroomsbench(
- conversation=conversation,
- scenario_name=scenario,
- participant_models=participants
- )
- except Exception as e:
- print(f"[BackroomsBench] Error: {e}")
- self._backroomsbench_error = str(e)
-
- def check_complete():
- if self._backroomsbench_result is not None:
- progress.close()
- result = self._backroomsbench_result
- self.statusBar().showMessage(
- f"🌀 BackroomsBench complete! {result['summary']['successful_evaluations']}/3 judges filed reports"
- )
- try:
- subprocess.Popen(f'explorer "{result["output_dir"]}"')
- except Exception:
- pass
- self._backroomsbench_result = None
- self._backrooms_check_timer.stop()
- elif self._backroomsbench_error is not None:
- progress.close()
- QMessageBox.critical(
- self,
- "BackroomsBench Error",
- f"Evaluation failed:\n{self._backroomsbench_error}"
- )
- self._backroomsbench_error = None
- self._backrooms_check_timer.stop()
-
- # Start background thread
- threading.Thread(target=run_eval, daemon=True).start()
-
- # Poll for completion
- self._backrooms_check_timer = QTimer()
- self._backrooms_check_timer.timeout.connect(check_complete)
- self._backrooms_check_timer.start(500)
-
- def on_node_hover(self, node_id):
- """Handle node hover in the network view"""
- if node_id == 'main':
- self.statusBar().showMessage("Main conversation")
- elif node_id in self.branch_conversations:
- branch_data = self.branch_conversations[node_id]
- branch_type = branch_data.get('type', 'branch')
- selected_text = branch_data.get('selected_text', '')
- self.statusBar().showMessage(f"{branch_type.capitalize()}: {selected_text[:50]}...")
-
- def apply_dark_theme(self):
- """Apply dark theme to the application"""
- self.setStyleSheet(f"""
- QMainWindow {{
- background-color: {COLORS['bg_dark']};
- color: {COLORS['text_normal']};
- }}
- QWidget {{
- background-color: {COLORS['bg_dark']};
- color: {COLORS['text_normal']};
- }}
- QToolTip {{
- background-color: {COLORS['bg_light']};
- color: {COLORS['text_normal']};
- border: 1px solid {COLORS['border']};
- padding: 5px;
- }}
- """)
-
- # Add specific styling for branch messages
- branch_header_format = QTextCharFormat()
- branch_header_format.setForeground(QColor(COLORS['ai_header']))
- branch_header_format.setFontWeight(QFont.Weight.Bold)
- branch_header_format.setFontPointSize(11)
-
- branch_inline_format = QTextCharFormat()
- branch_inline_format.setForeground(QColor(COLORS['ai_header']))
- branch_inline_format.setFontItalic(True)
- branch_inline_format.setFontPointSize(10)
-
- # Add formats to the left pane
- self.left_pane.text_formats["branch_header"] = branch_header_format
- self.left_pane.text_formats["branch_inline"] = branch_inline_format
-
- def on_branch_select(self, branch_id):
- """Handle branch selection in the network view"""
- try:
- # Check if branch exists
- if branch_id == 'main':
- # Switch to main conversation
- self.active_branch = None
- # Make sure we have a main_conversation attribute
- if not hasattr(self, 'main_conversation'):
- self.main_conversation = []
- self.conversation = self.main_conversation
- self.left_pane.update_conversation(self.conversation)
- self.statusBar().showMessage("Switched to main conversation")
- return
-
- if branch_id not in self.branch_conversations:
- self.statusBar().showMessage(f"Branch {branch_id} not found")
- return
-
- # Get branch data
- branch_data = self.branch_conversations[branch_id]
-
- # Set active branch
- self.active_branch = branch_id
-
- # Update conversation
- self.conversation = branch_data['conversation']
-
- # Display the conversation with branch metadata
- self.left_pane.display_conversation(self.conversation, branch_data)
-
- # Update status bar
- self.statusBar().showMessage(f"Switched to {branch_data['type']} branch: {branch_id}")
-
- except Exception as e:
- print(f"Error selecting branch: {e}")
- self.statusBar().showMessage(f"Error selecting branch: {e}")
-
- def branch_from_selection(self, selected_text):
- """Create a rabbithole branch from selected text"""
- if not selected_text:
- return
-
- # Create branch
- branch_id = self.create_branch(selected_text, 'rabbithole')
-
- # Switch to branch
- self.on_branch_select(branch_id)
-
- def fork_from_selection(self, selected_text):
- """Create a fork branch from selected text"""
- if not selected_text:
- return
-
- # Create branch
- branch_id = self.create_branch(selected_text, 'fork')
-
- # Switch to branch
- self.on_branch_select(branch_id)
-
- def create_branch(self, selected_text, branch_type="rabbithole", parent_branch=None):
- """Create a new branch in the conversation"""
- try:
- # Generate a unique ID for the branch
- branch_id = str(uuid.uuid4())
-
- # Get parent branch ID
- parent_id = parent_branch if parent_branch else (self.active_branch if self.active_branch else 'main')
-
- # Get current conversation
- if parent_id == 'main':
- # If parent is main, use main conversation
- if not hasattr(self, 'main_conversation'):
- self.main_conversation = []
- current_conversation = self.main_conversation.copy()
- else:
- # Otherwise, use parent branch conversation
- parent_data = self.branch_conversations.get(parent_id)
- if parent_data:
- current_conversation = parent_data['conversation'].copy()
- else:
- current_conversation = []
-
- # Create initial message based on branch type
- if branch_type == 'fork':
- initial_message = {
- "role": "user",
- "content": f"Complete this thought or sentence naturally, continuing forward from exactly this point: '{selected_text}'"
- }
- else: # rabbithole
- initial_message = {
- "role": "user",
- "content": f"Let's explore and expand upon the concept of '{selected_text}' from our previous discussion."
- }
-
- # Create branch conversation with initial message
- branch_conversation = current_conversation.copy()
- branch_conversation.append(initial_message)
-
- # Create branch data
- branch_data = {
- 'id': branch_id,
- 'parent': parent_id,
- 'type': branch_type,
- 'selected_text': selected_text,
- 'conversation': branch_conversation,
- 'turn_count': 0,
- 'created_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- 'history': current_conversation
- }
-
- # Store branch data
- self.branch_conversations[branch_id] = branch_data
-
- # Add node to network graph - make sure parameters are in the correct order
- node_label = f"{branch_type.capitalize()}: {selected_text[:20]}{'...' if len(selected_text) > 20 else ''}"
- self.right_sidebar.add_node(branch_id, node_label, branch_type)
- self.right_sidebar.add_edge(parent_id, branch_id)
-
- # Set active branch to this new branch
- self.active_branch = branch_id
- self.conversation = branch_conversation
-
- # Display the conversation
- self.left_pane.display_conversation(branch_conversation, branch_data)
-
- # Trigger AI response processing for this branch
- if hasattr(self, 'process_branch_conversation'):
- # Add a small delay to ensure UI updates first
- QTimer.singleShot(100, lambda: self.process_branch_conversation(branch_id))
-
- # Return branch ID
- return branch_id
-
- except Exception as e:
- print(f"Error creating branch: {e}")
- self.statusBar().showMessage(f"Error creating branch: {e}")
- return None
-
- def get_branch_path(self, branch_id):
- """Get the full path of branch names from root to the given branch"""
- try:
- path = []
- current_id = branch_id
-
- # Prevent potential infinite loops by tracking visited branches
- visited = set()
-
- while current_id != 'main' and current_id not in visited:
- visited.add(current_id)
- branch_data = self.branch_conversations.get(current_id)
- if not branch_data:
- break
-
- # Get a readable version of the selected text (truncated if needed)
- selected_text = branch_data.get('selected_text', '')
- if selected_text:
- display_text = f"{selected_text[:20]}{'...' if len(selected_text) > 20 else ''}"
- path.append(display_text)
- else:
- path.append(f"{branch_data.get('type', 'Branch').capitalize()}")
-
- # Check for valid parent attribute
- current_id = branch_data.get('parent')
- if not current_id:
- break
-
- path.append('Seed')
- return ' → '.join(reversed(path))
- except Exception as e:
- print(f"Error building branch path: {e}")
- return f"Branch {branch_id}"
-
- def save_splitter_state(self):
- """Save the current splitter state to a file"""
- try:
- # Create settings directory if it doesn't exist
- if not os.path.exists('settings'):
- os.makedirs('settings')
-
- # Save splitter state to file
- with open('settings/splitter_state.json', 'w') as f:
- json.dump({
- 'sizes': self.splitter.sizes()
- }, f)
- except Exception as e:
- print(f"Error saving splitter state: {e}")
-
- def restore_splitter_state(self):
- """Restore the splitter state from a file if available"""
- try:
- if os.path.exists('settings/splitter_state.json'):
- with open('settings/splitter_state.json', 'r') as f:
- state = json.load(f)
- if 'sizes' in state:
- self.splitter.setSizes(state['sizes'])
- except Exception as e:
- print(f"Error restoring splitter state: {e}")
- # Fall back to default sizes
- total_width = self.width()
- self.splitter.setSizes([int(total_width * 0.7), int(total_width * 0.3)])
-
- def process_branch_conversation(self, branch_id):
- """Process the branch conversation using the selected models"""
- # This method will be implemented in main.py to avoid circular imports
- pass
-
- def node_clicked(self, node_id):
- """Handle node click in the network view"""
- print(f"Node clicked: {node_id}")
-
- # Check if this is the main conversation or a branch
- if node_id == 'main':
- # Switch to main conversation
- self.active_branch = None
- self.left_pane.display_conversation(self.main_conversation)
- elif node_id in self.branch_conversations:
- # Switch to branch conversation
- self.active_branch = node_id
- branch_data = self.branch_conversations[node_id]
- conversation = branch_data['conversation']
-
- # Filter hidden messages for display
- visible_conversation = [msg for msg in conversation if not msg.get('hidden', False)]
- self.left_pane.display_conversation(visible_conversation, branch_data)
-
- def initialize_selectors(self):
- """Initialize the AI model selectors and prompt pair selector"""
- pass
-
- # Removed: create_new_living_document
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 9719351..5d0c197 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,175 @@
-# This file is automatically @generated by Poetry 1.5.5 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.1"
+description = "Happy Eyeballs for asyncio"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"},
+ {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"},
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.13.2"
+description = "Async http client/server framework (asyncio)"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155"},
+ {file = "aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c"},
+ {file = "aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636"},
+ {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da"},
+ {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725"},
+ {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5"},
+ {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3"},
+ {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802"},
+ {file = "aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a"},
+ {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204"},
+ {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22"},
+ {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d"},
+ {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f"},
+ {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f"},
+ {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6"},
+ {file = "aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251"},
+ {file = "aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514"},
+ {file = "aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0"},
+ {file = "aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb"},
+ {file = "aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9"},
+ {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613"},
+ {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead"},
+ {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780"},
+ {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a"},
+ {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592"},
+ {file = "aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab"},
+ {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30"},
+ {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40"},
+ {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948"},
+ {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf"},
+ {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782"},
+ {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8"},
+ {file = "aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec"},
+ {file = "aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c"},
+ {file = "aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b"},
+ {file = "aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc"},
+ {file = "aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7"},
+ {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb"},
+ {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3"},
+ {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f"},
+ {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6"},
+ {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e"},
+ {file = "aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7"},
+ {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d"},
+ {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b"},
+ {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8"},
+ {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16"},
+ {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169"},
+ {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248"},
+ {file = "aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e"},
+ {file = "aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45"},
+ {file = "aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be"},
+ {file = "aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742"},
+ {file = "aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293"},
+ {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811"},
+ {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a"},
+ {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4"},
+ {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a"},
+ {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e"},
+ {file = "aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb"},
+ {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded"},
+ {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b"},
+ {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8"},
+ {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04"},
+ {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476"},
+ {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23"},
+ {file = "aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254"},
+ {file = "aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a"},
+ {file = "aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b"},
+ {file = "aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61"},
+ {file = "aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4"},
+ {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b"},
+ {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694"},
+ {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906"},
+ {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9"},
+ {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011"},
+ {file = "aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6"},
+ {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213"},
+ {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49"},
+ {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae"},
+ {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa"},
+ {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4"},
+ {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a"},
+ {file = "aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940"},
+ {file = "aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734"},
+ {file = "aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f"},
+ {file = "aiohttp-3.13.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7fbdf5ad6084f1940ce88933de34b62358d0f4a0b6ec097362dcd3e5a65a4989"},
+ {file = "aiohttp-3.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7c3a50345635a02db61792c85bb86daffac05330f6473d524f1a4e3ef9d0046d"},
+ {file = "aiohttp-3.13.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e87dff73f46e969af38ab3f7cb75316a7c944e2e574ff7c933bc01b10def7f5"},
+ {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2adebd4577724dcae085665f294cc57c8701ddd4d26140504db622b8d566d7aa"},
+ {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e036a3a645fe92309ec34b918394bb377950cbb43039a97edae6c08db64b23e2"},
+ {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:23ad365e30108c422d0b4428cf271156dd56790f6dd50d770b8e360e6c5ab2e6"},
+ {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f9b2c2d4b9d958b1f9ae0c984ec1dd6b6689e15c75045be8ccb4011426268ca"},
+ {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a92cf4b9bea33e15ecbaa5c59921be0f23222608143d025c989924f7e3e0c07"},
+ {file = "aiohttp-3.13.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:070599407f4954021509193404c4ac53153525a19531051661440644728ba9a7"},
+ {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:29562998ec66f988d49fb83c9b01694fa927186b781463f376c5845c121e4e0b"},
+ {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4dd3db9d0f4ebca1d887d76f7cdbcd1116ac0d05a9221b9dad82c64a62578c4d"},
+ {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d7bc4b7f9c4921eba72677cd9fedd2308f4a4ca3e12fab58935295ad9ea98700"},
+ {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dacd50501cd017f8cccb328da0c90823511d70d24a323196826d923aad865901"},
+ {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8b2f1414f6a1e0683f212ec80e813f4abef94c739fd090b66c9adf9d2a05feac"},
+ {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04c3971421576ed24c191f610052bcb2f059e395bc2489dd99e397f9bc466329"},
+ {file = "aiohttp-3.13.2-cp39-cp39-win32.whl", hash = "sha256:9f377d0a924e5cc94dc620bc6366fc3e889586a7f18b748901cf016c916e2084"},
+ {file = "aiohttp-3.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:9c705601e16c03466cb72011bd1af55d68fa65b045356d8f96c216e5f6db0fa5"},
+ {file = "aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca"},
+]
+
+[package.dependencies]
+aiohappyeyeballs = ">=2.5.0"
+aiosignal = ">=1.4.0"
+async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""}
+attrs = ">=17.3.0"
+frozenlist = ">=1.1.1"
+multidict = ">=4.5,<7.0"
+propcache = ">=0.2.0"
+yarl = ">=1.17.0,<2.0"
+
+[package.extras]
+speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+description = "aiosignal: a list of registered asynchronous callbacks"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"},
+ {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"},
+]
+
+[package.dependencies]
+frozenlist = ">=1.1.0"
+typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""}
[[package]]
name = "annotated-types"
@@ -6,17 +177,46 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
+[[package]]
+name = "anthropic"
+version = "0.75.0"
+description = "The official Python library for the anthropic API"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b"},
+ {file = "anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb"},
+]
+
+[package.dependencies]
+anyio = ">=3.5.0,<5"
+distro = ">=1.7.0,<2"
+docstring-parser = ">=0.15,<1"
+httpx = ">=0.25.0,<1"
+jiter = ">=0.4.0,<1"
+pydantic = ">=1.9.0,<3"
+sniffio = "*"
+typing-extensions = ">=4.10,<5"
+
+[package.extras]
+aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"]
+bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"]
+vertex = ["google-auth[requests] (>=2,<3)"]
+
[[package]]
name = "anyio"
version = "4.6.2.post1"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"},
{file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"},
@@ -30,18 +230,96 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
-test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
trio = ["trio (>=0.26.1)"]
+[[package]]
+name = "async-timeout"
+version = "5.0.1"
+description = "Timeout context manager for asyncio programs"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
+ {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+description = "Classes Without Boilerplate"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"},
+ {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"},
+]
+
+[[package]]
+name = "black"
+version = "25.11.0"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"},
+ {file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"},
+ {file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"},
+ {file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"},
+ {file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"},
+ {file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"},
+ {file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"},
+ {file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"},
+ {file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"},
+ {file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"},
+ {file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"},
+ {file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"},
+ {file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"},
+ {file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"},
+ {file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"},
+ {file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"},
+ {file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"},
+ {file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"},
+ {file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"},
+ {file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"},
+ {file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"},
+ {file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"},
+ {file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"},
+ {file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"},
+ {file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"},
+ {file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+pytokens = ">=0.3.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.10)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
[[package]]
name = "certifi"
-version = "2024.6.2"
+version = "2024.7.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
- {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
- {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
+ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
+ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
]
[[package]]
@@ -50,6 +328,7 @@ version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
+groups = ["main"]
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
@@ -143,12 +422,106 @@ files = [
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
+[[package]]
+name = "click"
+version = "8.3.1"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
+ {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["main"]
+markers = "platform_system == \"Windows\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "ddgs"
+version = "9.5.5"
+description = "Dux Distributed Global Search. A metasearch library that aggregates results from diverse web search services."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "ddgs-9.5.5-py3-none-any.whl", hash = "sha256:01fc8017a653ad16501bd8ac9fedcd29291f6500b1f8a7e92a916cdad7fc2fa2"},
+ {file = "ddgs-9.5.5.tar.gz", hash = "sha256:9a09aa9ba24173d14321137c8c1bc54040ed0bf3bad956cb2f9feeda77968770"},
+]
+
+[package.dependencies]
+click = ">=8.1.8"
+lxml = ">=6.0.0"
+primp = ">=0.15.0"
+
+[package.extras]
+dev = ["mypy (>=1.17.1)", "pytest (>=8.4.1)", "pytest-dependency (>=0.6.0)", "ruff (>=0.12.9)"]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+description = "Distro - an OS platform information API"
+optional = false
+python-versions = ">=3.6"
+groups = ["main"]
+files = [
+ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"},
+ {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
+]
+
+[[package]]
+name = "docstring-parser"
+version = "0.17.0"
+description = "Parse Python docstrings in reST, Google and Numpydoc format"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708"},
+ {file = "docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912"},
+]
+
+[package.extras]
+dev = ["pre-commit (>=2.16.0) ; python_version >= \"3.9\"", "pydoctor (>=25.4.0)", "pytest"]
+docs = ["pydoctor (>=25.4.0)"]
+test = ["pytest"]
+
+[[package]]
+name = "eval-type-backport"
+version = "0.2.2"
+description = "Like `typing._eval_type`, but lets older Python versions use newer typing features."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a"},
+ {file = "eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1"},
+]
+
+[package.extras]
+tests = ["pytest"]
+
[[package]]
name = "exceptiongroup"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
+markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -157,31 +530,185 @@ files = [
[package.extras]
test = ["pytest (>=6)"]
+[[package]]
+name = "filelock"
+version = "3.20.0"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"},
+ {file = "filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4"},
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.8.0"
+description = "A list-like structure which implements collections.abc.MutableSequence"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"},
+ {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"},
+ {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"},
+ {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"},
+ {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"},
+ {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"},
+ {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"},
+ {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"},
+ {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"},
+ {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"},
+ {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"},
+ {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"},
+ {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"},
+ {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"},
+ {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"},
+ {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"},
+ {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"},
+ {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"},
+ {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"},
+ {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"},
+ {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"},
+ {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"},
+ {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"},
+ {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"},
+ {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"},
+ {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"},
+ {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"},
+ {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"},
+ {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"},
+ {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"},
+ {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"},
+ {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"},
+ {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"},
+ {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"},
+ {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"},
+ {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"},
+ {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"},
+ {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"},
+ {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"},
+ {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"},
+ {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"},
+ {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"},
+ {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"},
+ {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"},
+ {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"},
+ {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"},
+ {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"},
+ {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"},
+ {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"},
+ {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"},
+ {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"},
+ {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"},
+ {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"},
+ {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"},
+ {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"},
+ {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"},
+ {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"},
+ {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"},
+ {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"},
+ {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"},
+ {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"},
+ {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"},
+ {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"},
+ {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"},
+ {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"},
+ {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"},
+ {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"},
+ {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"},
+ {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"},
+ {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"},
+ {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"},
+ {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"},
+ {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"},
+ {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"},
+ {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"},
+ {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"},
+ {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"},
+ {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"},
+ {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"},
+ {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"},
+ {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"},
+ {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"},
+ {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"},
+ {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"},
+ {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"},
+ {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"},
+ {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"},
+ {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"},
+ {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"},
+ {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"},
+ {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"},
+ {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"},
+ {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"},
+ {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"},
+ {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"},
+ {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"},
+ {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"},
+ {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"},
+ {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"},
+ {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"},
+]
+
[[package]]
name = "h11"
-version = "0.14.0"
+version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
+groups = ["main"]
files = [
- {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
- {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
+ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
]
[[package]]
name = "httpcore"
-version = "1.0.6"
+version = "1.0.9"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
- {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"},
- {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"},
+ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
+ {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
]
[package.dependencies]
certifi = "*"
-h11 = ">=0.13,<0.15"
+h11 = ">=0.16"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
@@ -195,6 +722,7 @@ version = "0.27.2"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
@@ -208,7 +736,7 @@ idna = "*"
sniffio = "*"
[package.extras]
-brotli = ["brotli", "brotlicffi"]
+brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
@@ -220,28 +748,927 @@ version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
+groups = ["main"]
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
+[[package]]
+name = "jiter"
+version = "0.12.0"
+description = "Fast iterable JSON parser."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65"},
+ {file = "jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e"},
+ {file = "jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62"},
+ {file = "jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8"},
+ {file = "jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb"},
+ {file = "jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc"},
+ {file = "jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74"},
+ {file = "jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2"},
+ {file = "jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025"},
+ {file = "jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca"},
+ {file = "jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4"},
+ {file = "jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11"},
+ {file = "jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9"},
+ {file = "jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd"},
+ {file = "jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423"},
+ {file = "jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7"},
+ {file = "jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2"},
+ {file = "jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9"},
+ {file = "jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6"},
+ {file = "jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725"},
+ {file = "jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6"},
+ {file = "jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e"},
+ {file = "jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c"},
+ {file = "jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f"},
+ {file = "jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5"},
+ {file = "jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37"},
+ {file = "jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274"},
+ {file = "jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3"},
+ {file = "jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf"},
+ {file = "jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1"},
+ {file = "jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df"},
+ {file = "jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403"},
+ {file = "jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126"},
+ {file = "jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9"},
+ {file = "jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86"},
+ {file = "jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44"},
+ {file = "jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb"},
+ {file = "jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789"},
+ {file = "jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e"},
+ {file = "jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1"},
+ {file = "jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf"},
+ {file = "jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44"},
+ {file = "jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45"},
+ {file = "jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87"},
+ {file = "jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed"},
+ {file = "jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9"},
+ {file = "jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626"},
+ {file = "jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c"},
+ {file = "jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de"},
+ {file = "jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a"},
+ {file = "jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60"},
+ {file = "jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6"},
+ {file = "jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4"},
+ {file = "jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb"},
+ {file = "jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7"},
+ {file = "jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3"},
+ {file = "jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525"},
+ {file = "jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49"},
+ {file = "jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1"},
+ {file = "jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e"},
+ {file = "jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e"},
+ {file = "jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff"},
+ {file = "jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a"},
+ {file = "jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a"},
+ {file = "jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67"},
+ {file = "jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b"},
+ {file = "jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42"},
+ {file = "jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf"},
+ {file = "jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451"},
+ {file = "jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7"},
+ {file = "jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684"},
+ {file = "jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c"},
+ {file = "jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d"},
+ {file = "jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993"},
+ {file = "jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f"},
+ {file = "jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783"},
+ {file = "jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b"},
+ {file = "jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6"},
+ {file = "jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183"},
+ {file = "jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873"},
+ {file = "jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473"},
+ {file = "jiter-0.12.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c9d28b218d5f9e5f69a0787a196322a5056540cb378cac8ff542b4fa7219966c"},
+ {file = "jiter-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0ee12028daf8cfcf880dd492349a122a64f42c059b6c62a2b0c96a83a8da820"},
+ {file = "jiter-0.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b135ebe757a82d67ed2821526e72d0acf87dd61f6013e20d3c45b8048af927b"},
+ {file = "jiter-0.12.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15d7fafb81af8a9e3039fc305529a61cd933eecee33b4251878a1c89859552a3"},
+ {file = "jiter-0.12.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92d1f41211d8a8fe412faad962d424d334764c01dac6691c44691c2e4d3eedaf"},
+ {file = "jiter-0.12.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a64a48d7c917b8f32f25c176df8749ecf08cec17c466114727efe7441e17f6d"},
+ {file = "jiter-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122046f3b3710b85de99d9aa2f3f0492a8233a2f54a64902b096efc27ea747b5"},
+ {file = "jiter-0.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:27ec39225e03c32c6b863ba879deb427882f243ae46f0d82d68b695fa5b48b40"},
+ {file = "jiter-0.12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26b9e155ddc132225a39b1995b3b9f0fe0f79a6d5cbbeacf103271e7d309b404"},
+ {file = "jiter-0.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab05b7c58e29bb9e60b70c2e0094c98df79a1e42e397b9bb6eaa989b7a66dd0"},
+ {file = "jiter-0.12.0-cp39-cp39-win32.whl", hash = "sha256:59f9f9df87ed499136db1c2b6c9efb902f964bed42a582ab7af413b6a293e7b0"},
+ {file = "jiter-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3719596a1ebe7a48a498e8d5d0c4bf7553321d4c3eee1d620628d51351a3928"},
+ {file = "jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8"},
+ {file = "jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3"},
+ {file = "jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e"},
+ {file = "jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d"},
+ {file = "jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb"},
+ {file = "jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b"},
+ {file = "jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f"},
+ {file = "jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c"},
+ {file = "jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b"},
+]
+
+[[package]]
+name = "lxml"
+version = "6.0.2"
+description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"},
+ {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"},
+ {file = "lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31"},
+ {file = "lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9"},
+ {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8"},
+ {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba"},
+ {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c"},
+ {file = "lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c"},
+ {file = "lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321"},
+ {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1"},
+ {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34"},
+ {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a"},
+ {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c"},
+ {file = "lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b"},
+ {file = "lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0"},
+ {file = "lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5"},
+ {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607"},
+ {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938"},
+ {file = "lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d"},
+ {file = "lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438"},
+ {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964"},
+ {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d"},
+ {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7"},
+ {file = "lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178"},
+ {file = "lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553"},
+ {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb"},
+ {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a"},
+ {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c"},
+ {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7"},
+ {file = "lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46"},
+ {file = "lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078"},
+ {file = "lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285"},
+ {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456"},
+ {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924"},
+ {file = "lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f"},
+ {file = "lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534"},
+ {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564"},
+ {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f"},
+ {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0"},
+ {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192"},
+ {file = "lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0"},
+ {file = "lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092"},
+ {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f"},
+ {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8"},
+ {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f"},
+ {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6"},
+ {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322"},
+ {file = "lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849"},
+ {file = "lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f"},
+ {file = "lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6"},
+ {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77"},
+ {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f"},
+ {file = "lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452"},
+ {file = "lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048"},
+ {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df"},
+ {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1"},
+ {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916"},
+ {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd"},
+ {file = "lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6"},
+ {file = "lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a"},
+ {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679"},
+ {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659"},
+ {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484"},
+ {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2"},
+ {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314"},
+ {file = "lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2"},
+ {file = "lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7"},
+ {file = "lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf"},
+ {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe"},
+ {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d"},
+ {file = "lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d"},
+ {file = "lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5"},
+ {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0"},
+ {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba"},
+ {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0"},
+ {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d"},
+ {file = "lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37"},
+ {file = "lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9"},
+ {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917"},
+ {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f"},
+ {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8"},
+ {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a"},
+ {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c"},
+ {file = "lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b"},
+ {file = "lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed"},
+ {file = "lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8"},
+ {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d"},
+ {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba"},
+ {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601"},
+ {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed"},
+ {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37"},
+ {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338"},
+ {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9"},
+ {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd"},
+ {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d"},
+ {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9"},
+ {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e"},
+ {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d"},
+ {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec"},
+ {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272"},
+ {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f"},
+ {file = "lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312"},
+ {file = "lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca"},
+ {file = "lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c"},
+ {file = "lxml-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a656ca105115f6b766bba324f23a67914d9c728dafec57638e2b92a9dcd76c62"},
+ {file = "lxml-6.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c54d83a2188a10ebdba573f16bd97135d06c9ef60c3dc495315c7a28c80a263f"},
+ {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:1ea99340b3c729beea786f78c38f60f4795622f36e305d9c9be402201efdc3b7"},
+ {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af85529ae8d2a453feee4c780d9406a5e3b17cee0dd75c18bd31adcd584debc3"},
+ {file = "lxml-6.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe659f6b5d10fb5a17f00a50eb903eb277a71ee35df4615db573c069bcf967ac"},
+ {file = "lxml-6.0.2-cp38-cp38-win32.whl", hash = "sha256:5921d924aa5468c939d95c9814fa9f9b5935a6ff4e679e26aaf2951f74043512"},
+ {file = "lxml-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:0aa7070978f893954008ab73bb9e3c24a7c56c054e00566a21b553dc18105fca"},
+ {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694"},
+ {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90"},
+ {file = "lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62"},
+ {file = "lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444"},
+ {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad"},
+ {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48"},
+ {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93"},
+ {file = "lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c"},
+ {file = "lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c"},
+ {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf"},
+ {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6"},
+ {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84"},
+ {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb"},
+ {file = "lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f"},
+ {file = "lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304"},
+ {file = "lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa"},
+ {file = "lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6"},
+ {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba"},
+ {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5"},
+ {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4"},
+ {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"},
+ {file = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"},
+ {file = "lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700"},
+ {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee"},
+ {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f"},
+ {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9"},
+ {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a"},
+ {file = "lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e"},
+ {file = "lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62"},
+]
+
+[package.extras]
+cssselect = ["cssselect (>=0.7)"]
+html-clean = ["lxml_html_clean"]
+html5 = ["html5lib"]
+htmlsoup = ["BeautifulSoup4"]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"},
+ {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"},
+]
+
+[package.dependencies]
+mdurl = ">=0.1,<1.0"
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins (>=0.5.0)"]
+profiling = ["gprof2dot"]
+rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
+[[package]]
+name = "multidict"
+version = "6.7.0"
+description = "multidict implementation"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349"},
+ {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e"},
+ {file = "multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3"},
+ {file = "multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046"},
+ {file = "multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32"},
+ {file = "multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73"},
+ {file = "multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc"},
+ {file = "multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62"},
+ {file = "multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84"},
+ {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0"},
+ {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e"},
+ {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4"},
+ {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648"},
+ {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111"},
+ {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36"},
+ {file = "multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85"},
+ {file = "multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7"},
+ {file = "multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0"},
+ {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc"},
+ {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721"},
+ {file = "multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6"},
+ {file = "multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c"},
+ {file = "multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7"},
+ {file = "multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7"},
+ {file = "multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9"},
+ {file = "multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8"},
+ {file = "multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd"},
+ {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb"},
+ {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6"},
+ {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2"},
+ {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff"},
+ {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b"},
+ {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34"},
+ {file = "multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff"},
+ {file = "multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81"},
+ {file = "multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912"},
+ {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184"},
+ {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45"},
+ {file = "multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa"},
+ {file = "multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7"},
+ {file = "multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e"},
+ {file = "multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546"},
+ {file = "multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4"},
+ {file = "multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1"},
+ {file = "multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d"},
+ {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304"},
+ {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12"},
+ {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62"},
+ {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0"},
+ {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a"},
+ {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8"},
+ {file = "multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4"},
+ {file = "multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b"},
+ {file = "multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec"},
+ {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6"},
+ {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159"},
+ {file = "multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca"},
+ {file = "multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8"},
+ {file = "multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60"},
+ {file = "multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4"},
+ {file = "multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f"},
+ {file = "multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf"},
+ {file = "multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32"},
+ {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036"},
+ {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec"},
+ {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e"},
+ {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64"},
+ {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd"},
+ {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288"},
+ {file = "multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17"},
+ {file = "multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390"},
+ {file = "multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e"},
+ {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00"},
+ {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb"},
+ {file = "multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b"},
+ {file = "multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c"},
+ {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1"},
+ {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b"},
+ {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5"},
+ {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad"},
+ {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c"},
+ {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5"},
+ {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10"},
+ {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754"},
+ {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c"},
+ {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762"},
+ {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6"},
+ {file = "multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d"},
+ {file = "multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6"},
+ {file = "multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792"},
+ {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842"},
+ {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b"},
+ {file = "multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38"},
+ {file = "multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128"},
+ {file = "multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34"},
+ {file = "multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99"},
+ {file = "multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202"},
+ {file = "multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1"},
+ {file = "multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3"},
+ {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d"},
+ {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6"},
+ {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7"},
+ {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb"},
+ {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f"},
+ {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f"},
+ {file = "multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885"},
+ {file = "multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c"},
+ {file = "multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000"},
+ {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63"},
+ {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718"},
+ {file = "multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2"},
+ {file = "multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e"},
+ {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064"},
+ {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e"},
+ {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd"},
+ {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a"},
+ {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96"},
+ {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e"},
+ {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599"},
+ {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394"},
+ {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38"},
+ {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9"},
+ {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0"},
+ {file = "multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13"},
+ {file = "multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd"},
+ {file = "multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827"},
+ {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c"},
+ {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40"},
+ {file = "multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851"},
+ {file = "multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687"},
+ {file = "multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5"},
+ {file = "multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb"},
+ {file = "multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6"},
+ {file = "multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e"},
+ {file = "multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e"},
+ {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32"},
+ {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c"},
+ {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84"},
+ {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329"},
+ {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e"},
+ {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4"},
+ {file = "multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91"},
+ {file = "multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f"},
+ {file = "multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546"},
+ {file = "multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3"},
+ {file = "multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
+ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
+]
+
+[[package]]
+name = "networkx"
+version = "3.4.2"
+description = "Python package for creating and manipulating graphs and networks"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"},
+ {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"},
+]
+
+[package.extras]
+default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"]
+developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"]
+doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.15)", "sphinx (>=7.3)", "sphinx-gallery (>=0.16)", "texext (>=0.6.7)"]
+example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=1.9)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"]
+extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"]
+test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"]
+
+[[package]]
+name = "numpy"
+version = "2.2.6"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"},
+ {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"},
+ {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"},
+ {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"},
+ {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"},
+ {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"},
+ {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"},
+ {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"},
+ {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"},
+ {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"},
+ {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"},
+ {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"},
+ {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"},
+ {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"},
+ {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"},
+ {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"},
+ {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"},
+ {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"},
+ {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"},
+ {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"},
+ {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"},
+ {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"},
+ {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"},
+ {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"},
+ {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"},
+ {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"},
+ {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"},
+ {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"},
+ {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"},
+ {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"},
+ {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"},
+ {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"},
+ {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"},
+ {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"},
+ {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"},
+ {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"},
+ {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"},
+ {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"},
+ {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"},
+ {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"},
+ {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"},
+ {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"},
+ {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"},
+ {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"},
+ {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"},
+ {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"},
+ {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"},
+ {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"},
+ {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"},
+ {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"},
+ {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"},
+ {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"},
+ {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"},
+ {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"},
+ {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"},
+]
+
+[[package]]
+name = "openai"
+version = "2.8.1"
+description = "The official Python library for the openai API"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463"},
+ {file = "openai-2.8.1.tar.gz", hash = "sha256:cb1b79eef6e809f6da326a7ef6038719e35aa944c42d081807bfa1be8060f15f"},
+]
+
+[package.dependencies]
+anyio = ">=3.5.0,<5"
+distro = ">=1.7.0,<2"
+httpx = ">=0.23.0,<1"
+jiter = ">=0.10.0,<1"
+pydantic = ">=1.9.0,<3"
+sniffio = "*"
+tqdm = ">4"
+typing-extensions = ">=4.11,<5"
+
+[package.extras]
+aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"]
+datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
+realtime = ["websockets (>=13,<16)"]
+voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
+
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "pillow"
+version = "11.3.0"
+description = "Python Imaging Library (Fork)"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"},
+ {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"},
+ {file = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"},
+ {file = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"},
+ {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"},
+ {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"},
+ {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"},
+ {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"},
+ {file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"},
+ {file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"},
+ {file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"},
+ {file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"},
+ {file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"},
+ {file = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"},
+ {file = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"},
+ {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"},
+ {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"},
+ {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"},
+ {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"},
+ {file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"},
+ {file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"},
+ {file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"},
+ {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"},
+ {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"},
+ {file = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"},
+ {file = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"},
+ {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"},
+ {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"},
+ {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"},
+ {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"},
+ {file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"},
+ {file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"},
+ {file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"},
+ {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"},
+ {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"},
+ {file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"},
+ {file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"},
+ {file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"},
+ {file = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"},
+ {file = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"},
+ {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"},
+ {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"},
+ {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"},
+ {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"},
+ {file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"},
+ {file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"},
+ {file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"},
+ {file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"},
+ {file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"},
+ {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"},
+ {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"},
+ {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"},
+ {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"},
+ {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"},
+ {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"},
+ {file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"},
+ {file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"},
+ {file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"},
+ {file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"},
+ {file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"},
+ {file = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"},
+ {file = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"},
+ {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"},
+ {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"},
+ {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"},
+ {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"},
+ {file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"},
+ {file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"},
+ {file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"},
+ {file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"},
+ {file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"},
+ {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"},
+ {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"},
+ {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"},
+ {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"},
+ {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"},
+ {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"},
+ {file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"},
+ {file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"},
+ {file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"},
+ {file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"},
+ {file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"},
+ {file = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"},
+ {file = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"},
+ {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"},
+ {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"},
+ {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"},
+ {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"},
+ {file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"},
+ {file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"},
+ {file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"},
+ {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"},
+ {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"},
+ {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"},
+ {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"},
+ {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"},
+ {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"},
+ {file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"},
+ {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"},
+ {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"},
+ {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"},
+ {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"},
+ {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"},
+ {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"},
+ {file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"},
+ {file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"},
+]
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
+fpx = ["olefile"]
+mic = ["olefile"]
+test-arrow = ["pyarrow"]
+tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
+typing = ["typing-extensions ; python_version < \"3.10\""]
+xmp = ["defusedxml"]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"},
+ {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"},
+]
+
+[package.extras]
+docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
+type = ["mypy (>=1.18.2)"]
+
+[[package]]
+name = "primp"
+version = "0.15.0"
+description = "HTTP client that can impersonate web browsers, mimicking their headers and `TLS/JA3/JA4/HTTP2` fingerprints"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "primp-0.15.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1b281f4ca41a0c6612d4c6e68b96e28acfe786d226a427cd944baa8d7acd644f"},
+ {file = "primp-0.15.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:489cbab55cd793ceb8f90bb7423c6ea64ebb53208ffcf7a044138e3c66d77299"},
+ {file = "primp-0.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b45c23f94016215f62d2334552224236217aaeb716871ce0e4dcfa08eb161"},
+ {file = "primp-0.15.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e985a9cba2e3f96a323722e5440aa9eccaac3178e74b884778e926b5249df080"},
+ {file = "primp-0.15.0-cp38-abi3-manylinux_2_34_armv7l.whl", hash = "sha256:6b84a6ffa083e34668ff0037221d399c24d939b5629cd38223af860de9e17a83"},
+ {file = "primp-0.15.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:592f6079646bdf5abbbfc3b0a28dac8de943f8907a250ce09398cda5eaebd260"},
+ {file = "primp-0.15.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a728e5a05f37db6189eb413d22c78bd143fa59dd6a8a26dacd43332b3971fe8"},
+ {file = "primp-0.15.0-cp38-abi3-win_amd64.whl", hash = "sha256:aeb6bd20b06dfc92cfe4436939c18de88a58c640752cf7f30d9e4ae893cdec32"},
+ {file = "primp-0.15.0.tar.gz", hash = "sha256:1af8ea4b15f57571ff7fc5e282a82c5eb69bc695e19b8ddeeda324397965b30a"},
+]
+
+[package.extras]
+dev = ["certifi", "mypy (>=1.14.1)", "pytest (>=8.1.1)", "pytest-asyncio (>=0.25.3)", "ruff (>=0.9.2)", "typing-extensions ; python_full_version < \"3.12.0\""]
+
+[[package]]
+name = "propcache"
+version = "0.4.1"
+description = "Accelerated property cache"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"},
+ {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"},
+ {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"},
+ {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"},
+ {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"},
+ {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"},
+ {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"},
+ {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"},
+ {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"},
+ {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"},
+ {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"},
+ {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"},
+ {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"},
+ {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"},
+ {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"},
+ {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"},
+ {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"},
+ {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"},
+ {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"},
+ {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"},
+ {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"},
+ {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"},
+ {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"},
+ {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"},
+ {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"},
+ {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"},
+ {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"},
+ {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"},
+ {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"},
+ {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"},
+ {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"},
+ {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"},
+ {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"},
+ {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"},
+ {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"},
+ {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"},
+ {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"},
+ {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"},
+ {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"},
+ {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"},
+ {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"},
+ {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"},
+ {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"},
+ {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"},
+ {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"},
+ {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"},
+ {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"},
+ {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"},
+ {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"},
+ {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"},
+ {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"},
+ {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"},
+ {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"},
+ {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"},
+ {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"},
+ {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"},
+ {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"},
+ {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"},
+ {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"},
+ {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"},
+ {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"},
+ {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"},
+ {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"},
+ {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"},
+ {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"},
+ {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"},
+ {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"},
+ {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"},
+ {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"},
+ {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"},
+ {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"},
+ {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"},
+ {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"},
+ {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"},
+ {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"},
+ {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"},
+ {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"},
+ {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"},
+ {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"},
+ {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"},
+ {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"},
+ {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"},
+ {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"},
+ {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"},
+ {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"},
+ {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"},
+ {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"},
+ {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"},
+ {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"},
+ {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"},
+ {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"},
+ {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"},
+ {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"},
+ {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"},
+ {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"},
+ {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"},
+ {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"},
+ {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"},
+ {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"},
+ {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"},
+ {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"},
+ {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"},
+ {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"},
+ {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"},
+ {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"},
+ {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"},
+ {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"},
+ {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"},
+ {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"},
+ {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"},
+ {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"},
+ {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"},
+ {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"},
+ {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"},
+ {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"},
+ {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"},
+ {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"},
+ {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"},
+ {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"},
+ {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"},
+ {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"},
+ {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"},
+]
+
[[package]]
name = "pydantic"
version = "2.9.2"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
@@ -254,7 +1681,7 @@ typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""}
[package.extras]
email = ["email-validator (>=2.0.0)"]
-timezone = ["tzdata"]
+timezone = ["tzdata ; python_version >= \"3.9\" and sys_platform == \"win32\""]
[[package]]
name = "pydantic-core"
@@ -262,6 +1689,7 @@ version = "2.23.4"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
@@ -357,12 +1785,134 @@ files = [
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+[[package]]
+name = "pygments"
+version = "2.19.2"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
+ {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pyqt6"
+version = "6.10.0"
+description = "Python bindings for the Qt cross platform application toolkit"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pyqt6-6.10.0-1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:54b6b022369e4e6ade8cf79c0f988558839df7b2c285f814b4567d15a0fcb756"},
+ {file = "pyqt6-6.10.0-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:0eb82f152a83a8ae39f7d3ba580829ff7c0e8179d19d70f396853c10c8ddc5ac"},
+ {file = "pyqt6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:43e94a0ad4713055b47b4676d23432349845729912e4f3d20ac95935931c5e6f"},
+ {file = "pyqt6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:357da0f1465557dde249a31bc1f152320b7628a644e1d55d2db09b635394f39f"},
+ {file = "pyqt6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:8b5e4ea573733017a76bd12ea1b53351fd7f6dc57f8abf4329c4a41fea6dde04"},
+ {file = "pyqt6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:c2b5fc1a028e95b096f3a5966611cc8194e8e9e69984c41477417e18b5ce1362"},
+ {file = "pyqt6-6.10.0.tar.gz", hash = "sha256:710ecfd720d9a03b2c684881ae37f528e11d17e8f1bf96431d00a6a73f308e36"},
+]
+
+[package.dependencies]
+PyQt6-Qt6 = ">=6.10.0,<6.11.0"
+PyQt6-sip = ">=13.8,<14"
+
+[[package]]
+name = "pyqt6-qt6"
+version = "6.10.1"
+description = "The subset of a Qt installation needed by PyQt6."
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "pyqt6_qt6-6.10.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:4bb2798a95f624b462b70c4f185422235b714b01e55abab32af1740f147948e2"},
+ {file = "pyqt6_qt6-6.10.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0921cc522512cb40dbab673806bc1676924819550e0aec8e3f3fe6907387c5b7"},
+ {file = "pyqt6_qt6-6.10.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:04069aea421703b1269c8a1bcf017e36463af284a044239a4ebda3bde0a629fb"},
+ {file = "pyqt6_qt6-6.10.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:5b9be39e0120e32d0b42cdb844e3ae110ddadd39629c991e511902c06f155aff"},
+ {file = "pyqt6_qt6-6.10.1-py3-none-win_amd64.whl", hash = "sha256:df564d3dc2863b1fde22b39bea9f56ceb2a3ed7d6f0b76d3f96c2d3bc5d71516"},
+ {file = "pyqt6_qt6-6.10.1-py3-none-win_arm64.whl", hash = "sha256:48282e0f99682daf4f1e220cfe9f41255e003af38f7728a30d40c76e55c89816"},
+]
+
+[[package]]
+name = "pyqt6-sip"
+version = "13.10.2"
+description = "The sip module support for PyQt6"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pyqt6_sip-13.10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8132ec1cbbecc69d23dcff23916ec07218f1a9bbbc243bf6f1df967117ce303e"},
+ {file = "pyqt6_sip-13.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f77e89d93747dda71b60c3490b00d754451729fbcbcec840e42084bf061655"},
+ {file = "pyqt6_sip-13.10.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4ffa71ddff6ef031d52cd4f88b8bba08b3516313c023c7e5825cf4a0ba598712"},
+ {file = "pyqt6_sip-13.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:e907394795e61f1174134465c889177f584336a98d7a10beade2437bf5942244"},
+ {file = "pyqt6_sip-13.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a6c2f168773af9e6c7ef5e52907f16297d4efd346e4c958eda54ea9135be18e"},
+ {file = "pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1d3cc9015a1bd8c8d3e86a009591e897d4d46b0c514aede7d2970a2208749cd"},
+ {file = "pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ddd578a8d975bfb5fef83751829bf09a97a1355fa1de098e4fb4d1b74ee872fc"},
+ {file = "pyqt6_sip-13.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:061d4a2eb60a603d8be7db6c7f27eb29d9cea97a09aa4533edc1662091ce4f03"},
+ {file = "pyqt6_sip-13.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:45ac06f0380b7aa4fcffd89f9e8c00d1b575dc700c603446a9774fda2dcfc0de"},
+ {file = "pyqt6_sip-13.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c"},
+ {file = "pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6"},
+ {file = "pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7"},
+ {file = "pyqt6_sip-13.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e"},
+ {file = "pyqt6_sip-13.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92"},
+ {file = "pyqt6_sip-13.10.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1"},
+ {file = "pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca"},
+ {file = "pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b"},
+ {file = "pyqt6_sip-13.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277"},
+ {file = "pyqt6_sip-13.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244"},
+ {file = "pyqt6_sip-13.10.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8a76a06a8e5c5b1f17a3f6f3c834ca324877e07b960b18b8b9bbfd9c536ec658"},
+ {file = "pyqt6_sip-13.10.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9128d770a611200529468397d710bc972f1dcfe12bfcbb09a3ccddcd4d54fa5b"},
+ {file = "pyqt6_sip-13.10.2-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d820a0fae7315932c08f27dc0a7e33e0f50fe351001601a8eb9cf6f22b04562e"},
+ {file = "pyqt6_sip-13.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:3213bb6e102d3842a3bb7e59d5f6e55f176c80880ff0b39d0dac0cfe58313fb3"},
+ {file = "pyqt6_sip-13.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:ce33ff1f94960ad4b08035e39fa0c3c9a67070bec39ffe3e435c792721504726"},
+ {file = "pyqt6_sip-13.10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38b5823dca93377f8a4efac3cbfaa1d20229aa5b640c31cf6ebbe5c586333808"},
+ {file = "pyqt6_sip-13.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5506b9a795098df3b023cc7d0a37f93d3224a9c040c43804d4bc06e0b2b742b0"},
+ {file = "pyqt6_sip-13.10.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e455a181d45a28ee8d18d42243d4f470d269e6ccdee60f2546e6e71218e05bb4"},
+ {file = "pyqt6_sip-13.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:9c67ed66e21b11e04ffabe0d93bc21df22e0a5d7e2e10ebc8c1d77d2f5042991"},
+ {file = "pyqt6_sip-13.10.2.tar.gz", hash = "sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe"},
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"},
+ {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"},
+]
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
+[[package]]
+name = "pytokens"
+version = "0.3.0"
+description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"},
+ {file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"},
+]
+
+[package.extras]
+dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"]
+
[[package]]
name = "replicate"
version = "1.0.2"
description = "Python client for Replicate"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "replicate-1.0.2-py3-none-any.whl", hash = "sha256:808009853d5e49f706c9a67a9f97b712c89e2c75e63947f7cc012b847c28bef2"},
{file = "replicate-1.0.2.tar.gz", hash = "sha256:faa3551a825d9eb2c0bfc0407ddab75b9e02865344168241a1d41a4e73bbc661"},
@@ -376,18 +1926,19 @@ typing-extensions = ">=4.5.0"
[[package]]
name = "requests"
-version = "2.32.3"
+version = "2.32.4"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
- {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
- {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
+ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"},
+ {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"},
]
[package.dependencies]
certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<4"
+charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
@@ -395,23 +1946,194 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+[[package]]
+name = "rich"
+version = "14.2.0"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+optional = false
+python-versions = ">=3.8.0"
+groups = ["main"]
+files = [
+ {file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"},
+ {file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+description = "Tool to Detect Surrounding Shell"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
+ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
+]
+
[[package]]
name = "sniffio"
version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
+[[package]]
+name = "tabulate"
+version = "0.9.0"
+description = "Pretty-print tabular data"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
+ {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
+]
+
+[package.extras]
+widechars = ["wcwidth"]
+
+[[package]]
+name = "together"
+version = "1.5.31"
+description = "Python client for Together's Cloud Platform!"
+optional = false
+python-versions = "<4.0,>=3.10"
+groups = ["main"]
+files = [
+ {file = "together-1.5.31-py3-none-any.whl", hash = "sha256:cf522f39583a22876f4828099bb2e5f5ef9d3305bad80210138f9470a9373d9c"},
+ {file = "together-1.5.31.tar.gz", hash = "sha256:22ad70bc7cbc3b6886249eaa13ebf5005e0d1648ca5e8db35fe57e516d651cd7"},
+]
+
+[package.dependencies]
+aiohttp = ">=3.9.3,<4.0.0"
+black = ">=25.9.0,<26.0.0"
+click = ">=8.1.7,<9.0.0"
+eval-type-backport = ">=0.1.3,<0.3.0"
+filelock = ">=3.13.1,<4.0.0"
+numpy = {version = ">=1.23.5", markers = "python_version < \"3.12\""}
+pillow = ">=11.1.0,<12.0.0"
+pydantic = ">=2.6.3,<3.0.0"
+requests = ">=2.31.0,<3.0.0"
+rich = ">=13.8.1,<15.0.0"
+tabulate = ">=0.9.0,<0.10.0"
+tqdm = ">=4.66.2,<5.0.0"
+typer = ">=0.9,<0.20"
+
+[package.extras]
+pyarrow = ["pyarrow (>=10.0.1)"]
+
+[[package]]
+name = "tomli"
+version = "2.3.0"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"},
+ {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"},
+ {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"},
+ {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"},
+ {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"},
+ {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"},
+ {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"},
+ {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"},
+ {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"},
+ {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"},
+ {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"},
+ {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"},
+ {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"},
+ {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"},
+ {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"},
+ {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"},
+ {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"},
+ {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"},
+ {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"},
+ {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"},
+ {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"},
+ {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"},
+ {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"},
+ {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"},
+ {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"},
+ {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"},
+ {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"},
+ {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"},
+ {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"},
+ {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"},
+ {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"},
+ {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"},
+ {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"},
+ {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"},
+ {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"},
+ {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"},
+ {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"},
+ {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"},
+ {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"},
+ {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"},
+ {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"},
+ {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"},
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+description = "Fast, Extensible Progress Meter"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"},
+ {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"]
+discord = ["requests"]
+notebook = ["ipywidgets (>=6)"]
+slack = ["slack-sdk"]
+telegram = ["requests"]
+
+[[package]]
+name = "typer"
+version = "0.19.2"
+description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9"},
+ {file = "typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+rich = ">=10.11.0"
+shellingham = ">=1.3.0"
+typing-extensions = ">=3.7.4.3"
+
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
@@ -419,22 +2141,168 @@ files = [
[[package]]
name = "urllib3"
-version = "2.2.2"
+version = "2.6.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
- {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
+ {file = "urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f"},
+ {file = "urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1"},
]
[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["zstandard (>=0.18.0)"]
+zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
+
+[[package]]
+name = "yarl"
+version = "1.22.0"
+description = "Yet another URL library"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"},
+ {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"},
+ {file = "yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf"},
+ {file = "yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a"},
+ {file = "yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c"},
+ {file = "yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147"},
+ {file = "yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb"},
+ {file = "yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6"},
+ {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0"},
+ {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda"},
+ {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc"},
+ {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737"},
+ {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467"},
+ {file = "yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea"},
+ {file = "yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"},
+ {file = "yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"},
+ {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511"},
+ {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6"},
+ {file = "yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028"},
+ {file = "yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d"},
+ {file = "yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503"},
+ {file = "yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65"},
+ {file = "yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e"},
+ {file = "yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d"},
+ {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7"},
+ {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967"},
+ {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed"},
+ {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6"},
+ {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e"},
+ {file = "yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca"},
+ {file = "yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b"},
+ {file = "yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376"},
+ {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f"},
+ {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2"},
+ {file = "yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74"},
+ {file = "yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df"},
+ {file = "yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb"},
+ {file = "yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2"},
+ {file = "yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82"},
+ {file = "yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a"},
+ {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124"},
+ {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa"},
+ {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7"},
+ {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d"},
+ {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520"},
+ {file = "yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8"},
+ {file = "yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c"},
+ {file = "yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74"},
+ {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53"},
+ {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a"},
+ {file = "yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c"},
+ {file = "yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601"},
+ {file = "yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a"},
+ {file = "yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df"},
+ {file = "yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2"},
+ {file = "yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b"},
+ {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273"},
+ {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a"},
+ {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d"},
+ {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02"},
+ {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67"},
+ {file = "yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95"},
+ {file = "yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d"},
+ {file = "yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b"},
+ {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10"},
+ {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3"},
+ {file = "yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9"},
+ {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f"},
+ {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0"},
+ {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e"},
+ {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708"},
+ {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f"},
+ {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d"},
+ {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8"},
+ {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5"},
+ {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f"},
+ {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62"},
+ {file = "yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03"},
+ {file = "yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249"},
+ {file = "yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b"},
+ {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4"},
+ {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683"},
+ {file = "yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b"},
+ {file = "yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e"},
+ {file = "yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590"},
+ {file = "yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2"},
+ {file = "yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da"},
+ {file = "yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784"},
+ {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b"},
+ {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694"},
+ {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d"},
+ {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd"},
+ {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da"},
+ {file = "yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2"},
+ {file = "yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79"},
+ {file = "yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33"},
+ {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1"},
+ {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca"},
+ {file = "yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53"},
+ {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c"},
+ {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf"},
+ {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face"},
+ {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b"},
+ {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486"},
+ {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138"},
+ {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a"},
+ {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529"},
+ {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093"},
+ {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c"},
+ {file = "yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e"},
+ {file = "yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27"},
+ {file = "yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1"},
+ {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748"},
+ {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859"},
+ {file = "yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9"},
+ {file = "yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054"},
+ {file = "yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b"},
+ {file = "yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60"},
+ {file = "yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890"},
+ {file = "yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba"},
+ {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca"},
+ {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba"},
+ {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b"},
+ {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e"},
+ {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8"},
+ {file = "yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b"},
+ {file = "yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed"},
+ {file = "yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2"},
+ {file = "yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff"},
+ {file = "yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71"},
+]
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+propcache = ">=0.2.1"
[metadata]
-lock-version = "2.0"
+lock-version = "2.1"
python-versions = ">=3.10.0,<3.12"
-content-hash = "3e72b2d094010386fa5fcf555dce38afaf78c71b086a23bec0ba978d018f7a17"
+content-hash = "7c7ac24ae226be789cb3f68667e1fc5d792e33746940032bc60ae9433e4766a9"
diff --git a/pyproject.toml b/pyproject.toml
index 6a4bcd7..ce9a6b6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ authors = ["Your Name AI Conversation Archive
-' in line or '' in line:
- in_code_block = True
- processed_lines.append(line)
- continue
- elif '' in line or '' in line:
- in_code_block = False
- processed_lines.append(line)
- continue
-
- # If we're in a code block, don't apply greentext styling
- if in_code_block:
- processed_lines.append(line)
- continue
-
- # Apply greentext styling to lines starting with '>'
- if line.strip().startswith('>'):
- # Wrap the line in p with greentext class
- processed_line = f'{line}
' - processed_lines.append(processed_line) - else: - # No changes needed - processed_lines.append(line) - - # Join lines back - processed_content = '\n'.join(processed_lines) - return processed_content - - except Exception as e: - print(f"Error applying greentext styling: {e}") - return html_content + return update_conversation_html(conversation) - def show_living_document_intro(self): - """Show an introduction to the Living Document mode""" - return - -class LiminalBackroomsManager: - """Main manager class for the Liminal Backrooms application""" - - def __init__(self): - """Initialize the manager""" - # Create the GUI - self.app = create_gui() - - # Initialize the worker thread pool - self.thread_pool = QThreadPool() - print(f"Multithreading with maximum {self.thread_pool.maxThreadCount()} threads") - - # List to store workers - self.workers = [] - - # Initialize the application - self.initialize() - -def create_gui(): - """Create the GUI application""" - app = QApplication(sys.argv) - - # Load custom fonts (Iosevka Term for better ASCII art rendering) - loaded_fonts = load_fonts() - if loaded_fonts: - print(f"Successfully loaded custom fonts: {', '.join(loaded_fonts)}") - else: - print("No custom fonts loaded - using system fonts") - - main_window = LiminalBackroomsApp() - - # Create conversation manager and store it on the app for access in ai_turn - manager = ConversationManager(main_window) - main_window.conversation_manager = manager # Store reference on app for prompt additions - manager.initialize() - - return main_window, app - -def run_gui(main_window, app): - """Run the GUI application""" - main_window.show() - sys.exit(app.exec()) + def start_ai2_turn(self, conversation, worker): + time.sleep(TURN_DELAY) + self.thread_pool.start(worker) -if __name__ == "__main__": - main_window, app = create_gui() - run_gui(main_window, app) \ No newline at end of file + def start_ai3_turn(self, conversation, worker): + time.sleep(TURN_DELAY) + self.thread_pool.start(worker) diff --git a/src/core/engine.py b/src/core/engine.py new file mode 100644 index 0000000..1a09ec9 --- /dev/null +++ b/src/core/engine.py @@ -0,0 +1,530 @@ +import os +import json +import logging +import re +from src.core.config import AI_MODELS +from src.services.llm_service import ( + call_claude_api, + call_openrouter_api, + call_openai_api, + call_replicate_api, + call_deepseek_api, +) +from src.services.media_service import generate_video_with_sora +from src.core.config import SORA_SECONDS, SORA_SIZE + +def ai_turn(ai_name, conversation, model, system_prompt, gui=None, is_branch=False, branch_output=None, streaming_callback=None): + """Execute an AI turn with the given parameters + + Args: + streaming_callback: Optional function(chunk: str) to call with each streaming token + """ + print(f"==================================================") + print(f"Starting {model} turn ({ai_name})...") + print(f"Current conversation length: {len(conversation)}") + + # HTML contributions and living document disabled + enhanced_system_prompt = system_prompt + + # Get the actual model ID from the display name + model_id = AI_MODELS.get(model, model) + + # Prepend model identity to system prompt so AI knows who it is + enhanced_system_prompt = f"You are {ai_name} ({model}).\n\n{enhanced_system_prompt}" + + # Apply any self-added prompt additions for this AI + # Also get custom temperature setting + ai_temperature = 1.0 # Default + if gui and hasattr(gui, 'conversation_manager') and gui.conversation_manager: + prompt_additions = gui.conversation_manager.get_prompt_additions_for_ai(ai_name) + if prompt_additions: + enhanced_system_prompt += prompt_additions + print(f"[Prompt] Applied prompt additions for {ai_name}") + + # Get custom temperature if set + ai_temperature = gui.conversation_manager.get_temperature_for_ai(ai_name) + if ai_temperature != 1.0: + print(f"[Temperature] Using custom temperature {ai_temperature} for {ai_name}") + + # Check for branch type and count AI responses + is_rabbithole = False + is_fork = False + branch_text = "" + ai_response_count = 0 + found_branch_marker = False + latest_branch_marker_index = -1 + + # First find the most recent branch marker + for i, msg in enumerate(conversation): + if isinstance(msg, dict) and msg.get("_type") == "branch_indicator": + latest_branch_marker_index = i + found_branch_marker = True + + # Determine branch type from the latest marker + msg_content = msg.get("content", "") + # Branch indicators are always plain strings + if isinstance(msg_content, str): + if "Rabbitholing down:" in msg_content: + is_rabbithole = True + branch_text = msg_content.split('"')[1] if '"' in msg_content else "" + print(f"Detected rabbithole branch for: '{branch_text}'") + elif "Forking off:" in msg_content: + is_fork = True + branch_text = msg_content.split('"')[1] if '"' in msg_content else "" + print(f"Detected fork branch for: '{branch_text}'") + + # Now count AI responses that occur AFTER the latest branch marker + ai_response_count = 0 + if found_branch_marker: + for i, msg in enumerate(conversation): + if i > latest_branch_marker_index and msg.get("role") == "assistant": + ai_response_count += 1 + print(f"Counting AI responses after latest branch marker: found {ai_response_count} responses") + + # Handle branch-specific system prompts + + # For rabbitholing: override system prompt for first TWO responses + if is_rabbithole and ai_response_count < 2: + print(f"USING RABBITHOLE PROMPT: '{branch_text}' - response #{ai_response_count+1} after branch") + system_prompt = f"'{branch_text}'!!!" + + # For forking: override system prompt ONLY for first response + elif is_fork and ai_response_count == 0: + print(f"USING FORK PROMPT: '{branch_text}' - response #{ai_response_count+1}") + system_prompt = f"The conversation forks from'{branch_text}'. Continue naturally from this point." + + # For all other cases, use the standard system prompt + else: + if is_rabbithole: + print(f"USING STANDARD PROMPT: Past initial rabbithole exploration (responses after branch: {ai_response_count})") + elif is_fork: + print(f"USING STANDARD PROMPT: Past initial fork response (responses after branch: {ai_response_count})") + + # Apply the enhanced system prompt (with HTML contribution instructions) + system_prompt = enhanced_system_prompt + + # CRITICAL: Always ensure we have the system prompt + # No matter what happens with the conversation, we need this + messages = [] + messages.append({ + "role": "system", + "content": system_prompt + }) + + # Filter out any existing system messages that might interfere + filtered_conversation = [] + for msg in conversation: + if not isinstance(msg, dict): + # Convert plain text to dictionary + msg = {"role": "user", "content": str(msg)} + + # Skip any hidden "connecting..." messages + msg_content = msg.get("content", "") + if msg.get("hidden") and isinstance(msg_content, str) and "connect" in msg_content.lower(): + continue + + # Skip empty messages + content = msg.get("content", "") + if isinstance(content, str): + if not content.strip(): + continue + elif isinstance(content, list): + # For structured content, skip if all parts are empty + if not any(part.get('text', '').strip() if part.get('type') == 'text' else True for part in content): + continue + else: + if not content: + continue + + # Skip system messages (we already added our own above) + if msg.get("role") == "system": + continue + + # Skip special system messages (branch indicators, etc.) + if msg.get("role") == "system" and msg.get("_type"): + continue + + # Skip duplicate messages - check if this exact content exists already + is_duplicate = False + for existing in filtered_conversation: + if existing.get("content") == msg.get("content"): + is_duplicate = True + content = msg.get('content', '') + # Safely preview content - handle both string and list (structured) content + if isinstance(content, str): + preview = content[:30] + "..." if len(content) > 30 else content + else: + preview = f"[structured content with {len(content)} parts]" + print(f"Skipping duplicate message: {preview}") + break + + if not is_duplicate: + filtered_conversation.append(msg) + + # Process filtered conversation + for i, msg in enumerate(filtered_conversation): + # Check if this message is from the current AI + is_from_this_ai = False + if msg.get("ai_name") == ai_name: + is_from_this_ai = True + + # Determine role + if is_from_this_ai: + role = "assistant" + else: + role = "user" + + # Get content - preserve structure for images + content = msg.get("content", "") + + # Inject speaker name for messages from other participants (not from current AI) + if not is_from_this_ai and content: + # Use the model name (e.g., "Claude 4.5 Sonnet") if available, otherwise fall back to ai_name or "User" + speaker_name = msg.get("model") or msg.get("ai_name", "User") + + # Handle different content types + if isinstance(content, str): + # Simple string content - prefix with speaker name + content = f"[{speaker_name}]: {content}" + elif isinstance(content, list): + # Structured content (e.g., with images) - prefix text parts + modified_content = [] + for part in content: + if part.get('type') == 'text': + # Prefix the first text part with speaker name + text = part.get('text', '') + modified_part = part.copy() + modified_part['text'] = f"[{speaker_name}]: {text}" + modified_content.append(modified_part) + # Only prefix the first text part + break + else: + modified_content.append(part) + + # Add remaining parts unchanged + first_text_found = False + for part in content: + if part.get('type') == 'text' and not first_text_found: + first_text_found = True + continue # Skip, already added above + modified_content.append(part) + + content = modified_content if modified_content else content + + # Add to messages + messages.append({ + "role": role, + "content": content # Now includes speaker names for non-current-AI messages + }) + + # For logging, handle both string and structured content + if isinstance(content, list): + print(f"Message {i} - AI: {msg.get('ai_name', 'User')} - Assigned role: {role} - Content: [structured message with {len(content)} parts]") + else: + content_preview = content[:50] + "..." if len(str(content)) > 50 else content + print(f"Message {i} - AI: {msg.get('ai_name', 'User')} - Assigned role: {role} - Preview: {content_preview}") + + # Ensure the last message is a user message so the AI responds + if len(messages) > 1 and messages[-1].get("role") == "assistant": + # Find an appropriate message to use + if is_rabbithole and branch_text: + # Add a special rabbitholing instruction as the last message + messages.append({ + "role": "user", + "content": f"Please explore the concept of '{branch_text}' in depth. What are the most interesting aspects or connections related to this concept?" + }) + elif is_fork and branch_text: + # Add a special forking instruction as the last message + messages.append({ + "role": "user", + "content": f"Continue on naturally from the point about '{branch_text}' without including this text." + }) + else: + # Standard handling for other conversations + # Find the most recent message from the other AI to use as prompt + other_ai_message = None + for msg in reversed(filtered_conversation): + if msg.get("ai_name") != ai_name: + other_ai_message = msg.get("content", "") + break + + if other_ai_message: + messages.append({ + "role": "user", + "content": other_ai_message + }) + else: + # Fallback - only if no other AI message found + messages.append({ + "role": "user", + "content": "Let's continue our conversation." + }) + + # Print the processed messages for debugging + print(f"Sending to {model} ({ai_name}):") + for i, msg in enumerate(messages): + role = msg.get("role", "unknown") + content_raw = msg.get("content", "") + + # Handle both string and list content for logging + if isinstance(content_raw, list): + text_parts = [part.get('text', '') for part in content_raw if part.get('type') == 'text'] + has_image = any(part.get('type') == 'image' for part in content_raw) + content_str = ' '.join(text_parts) + if has_image: + content_str = f"[Image] {content_str}" if content_str else "[Image]" + else: + content_str = str(content_raw) + + # Truncate for display + content = content_str[:50] + "..." if len(content_str) > 50 else content_str + print(f"[{i}] {role}: {content}") + + # Load any available memories for this AI + memories = [] + try: + if os.path.exists(f'memories/{ai_name.lower()}_memories.json'): + with open(f'memories/{ai_name.lower()}_memories.json', 'r') as f: + memories = json.load(f) + print(f"Loaded {len(memories)} memories for {ai_name}") + else: + print(f"Loaded 0 memories for {ai_name}") + except Exception as e: + print(f"Error loading memories: {e}") + print(f"Loaded 0 memories for {ai_name}") + + # Display the final processed messages for debugging (avoid printing base64 images) + print(f"Sending to Claude:") + print(f"Messages: {len(messages)} message(s)") + + # Display the prompt + print(f"--- Prompt to {model} ({ai_name}) ---") + + try: + # Route Sora video models + if model_id in ("sora-2", "sora-2-pro"): + print(f"Using Sora Video API for model: {model_id}") + # Use last user message as the video prompt + prompt_content = "" + if len(messages) > 0: + last_content = messages[-1].get("content", "") + # Extract text from structured content if needed + if isinstance(last_content, list): + text_parts = [part.get('text', '') for part in last_content if part.get('type') == 'text'] + prompt_content = ' '.join(text_parts) + elif isinstance(last_content, str): + prompt_content = last_content + + if not prompt_content or not prompt_content.strip(): + prompt_content = "A short abstract motion graphic in warm colors" + + # Use config values with env var override + sora_seconds = int(os.getenv("SORA_SECONDS", str(SORA_SECONDS))) + sora_size = os.getenv("SORA_SIZE", SORA_SIZE) or None + + print(f"[Sora] Starting job with seconds={sora_seconds} size={sora_size}") + video_result = generate_video_with_sora( + prompt=prompt_content, + model=model_id, + seconds=sora_seconds, + size=sora_size, + ) + + if video_result.get("success"): + print(f"[Sora] Completed: id={video_result.get('video_id')} path={video_result.get('video_path')}") + # Return a lightweight textual confirmation; video is saved to disk + return { + "role": "assistant", + "content": f"[Sora] Video created: {video_result.get('video_path')}", + "model": model, + "ai_name": ai_name + } + else: + err = video_result.get("error", "unknown error") + print(f"[Sora] Failed: {err}") + return { + "role": "system", + "content": f"[Sora] Video generation failed: {err}", + "model": model, + "ai_name": ai_name + } + + # Route Claude models through OpenRouter instead of direct Anthropic API + # This avoids issues with image handling differences between the APIs + # Set to False to use OpenRouter for Claude (recommended for image support) + USE_DIRECT_ANTHROPIC_API = False + + if USE_DIRECT_ANTHROPIC_API and ("claude" in model_id.lower() or model_id in ["anthropic/claude-3-opus-20240229", "anthropic/claude-3-sonnet-20240229", "anthropic/claude-3-haiku-20240307"]): + print(f"Using Claude API for model: {model_id}") + + # CRITICAL: Make sure there are no duplicates in the messages and system prompt is included + final_messages = [] + seen_contents = set() + + for msg in messages: + # Skip empty messages - handle both string and list content + content = msg.get("content", "") + is_empty = False + if isinstance(content, list): + # For structured content, check if all parts are empty + text_parts = [part.get('text', '').strip() for part in content if part.get('type') == 'text'] + has_image = any(part.get('type') == 'image' for part in content) + is_empty = not text_parts and not has_image + elif isinstance(content, str): + is_empty = not content + else: + is_empty = not content + + if is_empty: + continue + + # Handle system message separately + if msg.get("role") == "system": + continue + + # Check for duplicates by content - create hashable representation + content = msg.get("content", "") + + # Create a hashable content_hash for duplicate detection + if isinstance(content, list): + # For structured messages, use text parts for hash + text_parts = [part.get('text', '') for part in content if part.get('type') == 'text'] + content_hash = ''.join(text_parts) + elif isinstance(content, str): + content_hash = content + else: + content_hash = str(content) if content else "" + + if content_hash and content_hash in seen_contents: + print(f"Skipping duplicate message in AI turn: {content_hash[:30]}...") + continue + + if content_hash: + seen_contents.add(content_hash) + final_messages.append(msg) + + # Ensure we have at least one message + if not final_messages: + print("Warning: No messages left after filtering. Adding a default message.") + final_messages.append({"role": "user", "content": "Connecting..."}) + + # Get the prompt content safely + prompt_content = "" + if len(final_messages) > 0: + prompt_content = final_messages[-1].get("content", "") + # Use all messages except the last one as context + context_messages = final_messages[:-1] + else: + context_messages = [] + prompt_content = "Connecting..." # Default fallback + + # Call Claude API with filtered messages (with streaming if callback provided) + response = call_claude_api(prompt_content, context_messages, model_id, system_prompt, stream_callback=streaming_callback, temperature=ai_temperature) + + return { + "role": "assistant", + "content": response, + "model": model, + "ai_name": ai_name + } + + # Check for DeepSeek models to use Replicate via DeepSeek API function + if "deepseek" in model.lower(): + print(f"Using Replicate API for DeepSeek model: {model_id}") + + # Ensure we have at least one message for the prompt + if len(messages) > 0: + prompt_content = messages[-1].get("content", "") + context_messages = messages[:-1] + else: + prompt_content = "Connecting..." + context_messages = [] + + response = call_deepseek_api(prompt_content, context_messages, model_id, system_prompt) + + # Ensure response has the required format for the Worker class + if isinstance(response, dict) and 'content' in response: + # Add model info to the response + response['model'] = model + response['role'] = 'assistant' + response['ai_name'] = ai_name + + # Check for HTML contribution + if "html_contribution" in response: + html_contribution = response["html_contribution"] + + # Don't update HTML document here - we'll do it in on_ai_result_received + # Just add indicator to the conversation part + response["content"] += "\n\n..." + if "display" in response: + response["display"] += "\n\n..." + + return response + else: + # Create a formatted response if not already in the right format + return { + "role": "assistant", + "content": str(response) if response else "No response from model", + "model": model, + "ai_name": ai_name, + "display": str(response) if response else "No response from model" + } + + # Use OpenRouter for all other models + else: + print(f"Using OpenRouter API for model: {model_id}") + + try: + # Ensure we have valid messages + if len(messages) > 0: + prompt_content = messages[-1].get("content", "") + context_messages = messages[:-1] + else: + prompt_content = "Connecting..." + context_messages = [] + + # Call OpenRouter API with streaming support + response = call_openrouter_api(prompt_content, context_messages, model_id, system_prompt, stream_callback=streaming_callback, temperature=ai_temperature) + + # Avoid printing full response which could be large + response_preview = str(response)[:200] + "..." if response and len(str(response)) > 200 else response + print(f"Raw {model} Response: {response_preview}") + + result = { + "role": "assistant", + "content": response, + "model": model, + "ai_name": ai_name + } + + return result + except Exception as e: + error_message = f"Error making API request: {str(e)}" + print(f"Error: {error_message}") + print(f"Error type: {type(e)}") + + # Create an error response + result = { + "role": "system", + "content": f"Error: {error_message}", + "model": model, + "ai_name": ai_name + } + + # Return the error result + return result + + except Exception as e: + error_message = f"Error making API request: {str(e)}" + print(f"Error: {error_message}") + + # Create an error response + result = { + "role": "system", + "content": f"Error: {error_message}", + "model": model, + "ai_name": ai_name + } + + # Return the error result + return result diff --git a/src/core/session_manager.py b/src/core/session_manager.py new file mode 100644 index 0000000..25f1b3b --- /dev/null +++ b/src/core/session_manager.py @@ -0,0 +1,59 @@ +import json +import os +from datetime import datetime + +class SessionManager: + """Manages saving and loading of conversation sessions""" + + def __init__(self, sessions_dir="sessions"): + self.sessions_dir = sessions_dir + if not os.path.exists(self.sessions_dir): + os.makedirs(self.sessions_dir) + + def save_session(self, filename, conversation, branch_conversations=None, active_branch=None, metadata=None): + """Save the current session to a JSON file""" + if not filename.endswith('.json'): + filename += '.json' + + filepath = os.path.join(self.sessions_dir, filename) + + data = { + "conversation": conversation, + "branch_conversations": branch_conversations or {}, + "active_branch": active_branch, + "metadata": metadata or {}, + "saved_at": datetime.now().isoformat(), + "version": "1.0" + } + + try: + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + return True, filepath + except Exception as e: + return False, str(e) + + def load_session(self, filename): + """Load a session from a JSON file""" + filepath = os.path.join(self.sessions_dir, filename) + + if not os.path.exists(filepath): + return False, f"File not found: {filename}" + + try: + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + return True, data + except Exception as e: + return False, str(e) + + def list_sessions(self): + """List available saved sessions""" + try: + files = [f for f in os.listdir(self.sessions_dir) if f.endswith('.json')] + # Sort by modification time, newest first + files.sort(key=lambda x: os.path.getmtime(os.path.join(self.sessions_dir, x)), reverse=True) + return files + except Exception as e: + print(f"Error listing sessions: {e}") + return [] diff --git a/src/core/worker.py b/src/core/worker.py new file mode 100644 index 0000000..e62afb0 --- /dev/null +++ b/src/core/worker.py @@ -0,0 +1,95 @@ +import os +import json +import logging +import threading +from PyQt6.QtCore import QObject, QRunnable, pyqtSignal, pyqtSlot + +from src.core.config import AI_MODELS +from src.services.llm_service import ( + call_claude_api, + call_openrouter_api, + call_openai_api, + call_replicate_api, + call_deepseek_api, +) +from src.services.media_service import generate_video_with_sora +from src.core.config import SORA_SECONDS, SORA_SIZE + +class WorkerSignals(QObject): + """Defines the signals available from a running worker thread""" + finished = pyqtSignal() + error = pyqtSignal(str) + response = pyqtSignal(str, str) + result = pyqtSignal(str, object) # Signal for complete result object + progress = pyqtSignal(str) + streaming_chunk = pyqtSignal(str, str) # Signal for streaming tokens: (ai_name, chunk) + + +class Worker(QRunnable): + """Worker thread for processing AI turns using QThreadPool""" + + def __init__(self, ai_name, conversation, model, system_prompt, is_branch=False, branch_id=None, gui=None): + super().__init__() + self.ai_name = ai_name + self.conversation = conversation.copy() # Make a copy to prevent race conditions + self.model = model + self.system_prompt = system_prompt + self.is_branch = is_branch + self.branch_id = branch_id + self.gui = gui + + # Create signals object + self.signals = WorkerSignals() + + @pyqtSlot() + def run(self): + """Process the AI turn when the thread is started""" + print(f"[Worker] >>> Starting run() for {self.ai_name} ({self.model})") + try: + # Emit progress update + self.signals.progress.emit(f"Processing {self.ai_name} turn with {self.model}...") + + # Define streaming callback + def stream_chunk(chunk: str): + self.signals.streaming_chunk.emit(self.ai_name, chunk) + + # Process the turn with streaming + from src.core.engine import ai_turn + + print(f"[Worker] Calling ai_turn for {self.ai_name}...") + result = ai_turn( + self.ai_name, + self.conversation, + self.model, + self.system_prompt, + gui=self.gui, + streaming_callback=stream_chunk + ) + print(f"[Worker] ai_turn completed for {self.ai_name}, result type: {type(result)}") + + # Emit both the text response and the full result object + if isinstance(result, dict): + response_content = result.get('content', '') + print(f"[Worker] Emitting response for {self.ai_name}, content length: {len(response_content) if response_content else 0}") + # Emit the simple text response for backward compatibility + self.signals.response.emit(self.ai_name, response_content) + # Also emit the full result object for HTML contribution processing + self.signals.result.emit(self.ai_name, result) + else: + # Handle simple string responses + print(f"[Worker] Emitting string response for {self.ai_name}") + self.signals.response.emit(self.ai_name, result if result else "") + self.signals.result.emit(self.ai_name, {"content": result, "model": self.model}) + + # Emit finished signal + print(f"[Worker] <<< Finished run() for {self.ai_name}, emitting finished signal") + self.signals.finished.emit() + + except Exception as e: + # Emit error signal + print(f"[Worker] !!! ERROR in run() for {self.ai_name}: {e}") + import traceback + traceback.print_exc() + self.signals.error.emit(str(e)) + # Still emit finished signal even if there's an error + self.signals.finished.emit() diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..cc4c3d3 --- /dev/null +++ b/src/main.py @@ -0,0 +1,27 @@ +import sys +import os +from PyQt6.QtWidgets import QApplication +from src.ui.main_window import LiminalBackroomsApp +from src.ui.widgets.custom_widgets import COLORS # Ensure fonts or styles are loaded if needed +from src.utils.font_loader import load_fonts + +def main(): + """Main entry point for the application""" + # Create the application + app = QApplication(sys.argv) + app.setApplicationName("Liminal Backrooms") + + # Load custom fonts + load_fonts() + + # Create the main window + main_window = LiminalBackroomsApp() + + # Show the window + main_window.show() + + # Run the event loop + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/html_generator.py b/src/services/html_generator.py new file mode 100644 index 0000000..f5dfcd8 --- /dev/null +++ b/src/services/html_generator.py @@ -0,0 +1,418 @@ +import os +from datetime import datetime +from src.ui.colors import COLORS + +def update_conversation_html(conversation, filename="conversation_full.html"): + """Update the full conversation HTML document with all messages""" + try: + # Generate HTML content for the conversation + html_content = f""" + + +AI Conversation Archive
+{code_content}'
+ result.append(formatted_code)
+ except Exception as e:
+ # If there's an error, just add the original escaped content
+ print(f"Error processing code block: {e}")
+ result.append(part)
+ else:
+ # Process inline code in non-code-block parts
+ inline_parts = re.split(r'(`[^`]+`)', part)
+ processed_part = []
+
+ for inline_part in inline_parts:
+ if inline_part.startswith("`") and inline_part.endswith("`") and len(inline_part) > 2:
+ # This is inline code
+ code = inline_part[1:-1]
+ processed_part.append(f'{code}')
+ else:
+ processed_part.append(inline_part)
+
+ result.append(''.join(processed_part))
+
+ return ''.join(result)
+
+ def start_loading(self):
+ """Start loading animation"""
+ self.loading = True
+ self.loading_dots = 0
+ self.input_field.setEnabled(False)
+ self.submit_button.setEnabled(False)
+ self.submit_button.setText("Processing")
+ self.loading_timer.start()
+
+ # Add subtle pulsing animation to the button
+ self.pulse_animation = QPropertyAnimation(self.submit_button, b"styleSheet")
+ self.pulse_animation.setDuration(1000)
+ self.pulse_animation.setLoopCount(-1) # Infinite loop
+
+ # Define keyframes for the animation
+ normal_style = f"""
+ QPushButton {{
+ background-color: {COLORS['border']};
+ color: {COLORS['text_dim']};
+ border: none;
+ border-radius: 4px;
+ padding: 4px 12px;
+ font-weight: bold;
+ font-size: 11px;
+ }}
+ """
+
+ pulse_style = f"""
+ QPushButton {{
+ background-color: {COLORS['border_highlight']};
+ color: {COLORS['text_dim']};
+ border: none;
+ border-radius: 4px;
+ padding: 4px 12px;
+ font-weight: bold;
+ font-size: 11px;
+ }}
+ """
+
+ self.pulse_animation.setStartValue(normal_style)
+ self.pulse_animation.setEndValue(pulse_style)
+ self.pulse_animation.start()
+
+ def stop_loading(self):
+ """Stop loading animation"""
+ self.loading = False
+ self.loading_timer.stop()
+ self.input_field.setEnabled(True)
+ self.submit_button.setEnabled(True)
+ self.submit_button.setText("Propagate")
+
+ # Stop the pulsing animation
+ if hasattr(self, 'pulse_animation'):
+ self.pulse_animation.stop()
+
+ # Reset button style
+ self.submit_button.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {COLORS['accent_cyan']};
+ color: {COLORS['bg_dark']};
+ border: 1px solid {COLORS['accent_cyan']};
+ border-radius: 0px;
+ padding: 6px 16px;
+ font-weight: bold;
+ font-size: 11px;
+ letter-spacing: 1px;
+ }}
+ QPushButton:hover {{
+ background-color: {COLORS['bg_dark']};
+ color: {COLORS['accent_cyan']};
+ }}
+ QPushButton:pressed {{
+ background-color: {COLORS['accent_cyan_active']};
+ color: {COLORS['text_bright']};
+ }}
+ """)
+
+ def update_loading_animation(self):
+ """Update loading animation dots"""
+ self.loading_dots = (self.loading_dots + 1) % 4
+ dots = "." * self.loading_dots
+ self.submit_button.setText(f"Processing{dots}")
+
+ def show_context_menu(self, position):
+ """Show context menu at the given position"""
+ # Get selected text
+ cursor = self.conversation_display.textCursor()
+ selected_text = cursor.selectedText()
+
+ # Only show context menu if text is selected
+ if selected_text:
+ # Show the context menu at cursor position
+ self.context_menu.exec(self.conversation_display.mapToGlobal(position))
+
+ def rabbithole_from_selection(self):
+ """Create a rabbithole branch from selected text"""
+ cursor = self.conversation_display.textCursor()
+ selected_text = cursor.selectedText()
+
+ if selected_text and hasattr(self, 'rabbithole_callback'):
+ self.rabbithole_callback(selected_text)
+
+ def fork_from_selection(self):
+ """Create a fork branch from selected text"""
+ cursor = self.conversation_display.textCursor()
+ selected_text = cursor.selectedText()
+
+ if selected_text and hasattr(self, 'fork_callback'):
+ self.fork_callback(selected_text)
+
+ def append_text(self, text, format_type="normal"):
+ """Append text to the conversation display with the specified format"""
+ # Check if user is at the bottom before appending
+ scrollbar = self.conversation_display.verticalScrollBar()
+ was_at_bottom = scrollbar.value() >= scrollbar.maximum() - 20
+
+ cursor = self.conversation_display.textCursor()
+ cursor.movePosition(QTextCursor.MoveOperation.End)
+
+ # Apply the format if specified
+ if format_type in self.text_formats:
+ self.conversation_display.setCurrentCharFormat(self.text_formats[format_type])
+
+ # Insert the text
+ cursor.insertText(text)
+
+ # Reset to normal format after insertion
+ if format_type != "normal":
+ self.conversation_display.setCurrentCharFormat(self.text_formats["normal"])
+
+ # Only auto-scroll if user was already at the bottom
+ if was_at_bottom:
+ self.conversation_display.setTextCursor(cursor)
+ self.conversation_display.ensureCursorVisible()
+
+ def clear_conversation(self):
+ """Clear the conversation display"""
+ self.conversation_display.clear()
+ self.images = []
+
+ def display_conversation(self, conversation, branch_data=None):
+ """Display the conversation in the text edit widget"""
+ self.conversation = conversation
+
+ # Check if we're in a branch
+ is_branch = branch_data is not None
+ branch_type = branch_data.get('type', '') if is_branch else ''
+ selected_text = branch_data.get('selected_text', '') if is_branch else ''
+
+ # Update title if in a branch
+ if is_branch:
+ branch_emoji = "🐇" if branch_type == "rabbithole" else "🍴"
+ self.title_label.setText(f"{branch_emoji} {branch_type.capitalize()}: {selected_text[:30]}...")
+ self.info_label.setText(f"Branch conversation")
+ else:
+ self.title_label.setText("Liminal Backrooms")
+ self.info_label.setText("AI-to-AI conversation")
+
+ # Render conversation
+ self.render_conversation()
+
+ def display_image(self, image_path):
+ """Display an image in the conversation"""
+ try:
+ # Check if the image path is valid
+ if not image_path or not os.path.exists(image_path):
+ self.append_text("[Image not found]\n", "error")
+ return
+
+ # Load the image
+ image = QImage(image_path)
+ if image.isNull():
+ self.append_text("[Invalid image format]\n", "error")
+ return
+
+ # Create a pixmap from the image
+ pixmap = QPixmap.fromImage(image)
+
+ # Scale the image to fit the conversation display
+ max_width = self.conversation_display.width() - 50
+ if pixmap.width() > max_width:
+ pixmap = pixmap.scaledToWidth(max_width, Qt.TransformationMode.SmoothTransformation)
+
+ # Insert the image into the conversation display
+ cursor = self.conversation_display.textCursor()
+ cursor.movePosition(QTextCursor.MoveOperation.End)
+ cursor.insertImage(pixmap.toImage())
+ cursor.insertText("\n\n")
+
+ # Store the image to prevent garbage collection
+ self.images.append(pixmap)
+ self.image_paths.append(image_path)
+
+ except Exception as e:
+ self.append_text(f"[Error displaying image: {str(e)}]\n", "error")
+
+ def export_conversation(self):
+ """Export the conversation and all session media to a folder"""
+ base_dir = os.path.join(os.getcwd(), "exports")
+
+ # Create the base directory if it doesn't exist
+ os.makedirs(base_dir, exist_ok=True)
+
+ # Generate a session folder name based on date/time
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ default_folder = os.path.join(base_dir, f"session_{timestamp}")
+
+ # Get the folder from a dialog
+ folder_name = QFileDialog.getExistingDirectory(
+ self,
+ "Select Export Folder (or create new)",
+ base_dir,
+ QFileDialog.Option.ShowDirsOnly
+ )
+
+ if not folder_name:
+ reply = QMessageBox.question(
+ self,
+ "Create Export Folder?",
+ f"Create new export folder?\n\n{default_folder}",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
+ )
+ if reply == QMessageBox.StandardButton.Yes:
+ folder_name = default_folder
+ else:
+ return
+
+ try:
+ # Create the export folder
+ os.makedirs(folder_name, exist_ok=True)
+
+ # Get main window for accessing session data
+ main_window = self.window()
+
+ # Export conversation as multiple formats
+ # Plain text
+ text_path = os.path.join(folder_name, "conversation.txt")
+ with open(text_path, 'w', encoding='utf-8') as f:
+ f.write(self.conversation_display.toPlainText())
+
+ # HTML
+ html_path = os.path.join(folder_name, "conversation.html")
+ with open(html_path, 'w', encoding='utf-8') as f:
+ f.write(self.conversation_display.toHtml())
+
+ # Full HTML document if it exists
+ full_html_path = os.path.join(os.getcwd(), "conversation_full.html")
+ if os.path.exists(full_html_path):
+ shutil.copy2(full_html_path, os.path.join(folder_name, "conversation_full.html"))
+
+ # Copy session images
+ images_copied = 0
+ if hasattr(main_window, 'right_sidebar') and hasattr(main_window.right_sidebar, 'image_preview_pane'):
+ session_images = main_window.right_sidebar.image_preview_pane.session_images
+ if session_images:
+ images_dir = os.path.join(folder_name, "images")
+ os.makedirs(images_dir, exist_ok=True)
+ for img_path in session_images:
+ if os.path.exists(img_path):
+ shutil.copy2(img_path, images_dir)
+ images_copied += 1
+
+ # Copy session videos
+ videos_copied = 0
+ if hasattr(main_window, 'session_videos'):
+ session_videos = main_window.session_videos
+ if session_videos:
+ videos_dir = os.path.join(folder_name, "videos")
+ os.makedirs(videos_dir, exist_ok=True)
+ for vid_path in session_videos:
+ if os.path.exists(vid_path):
+ shutil.copy2(vid_path, videos_dir)
+ videos_copied += 1
+
+ # Create a manifest/summary file
+ manifest_path = os.path.join(folder_name, "manifest.txt")
+ with open(manifest_path, 'w', encoding='utf-8') as f:
+ f.write(f"Liminal Backrooms Session Export\n")
+ f.write(f"================================\n")
+ f.write(f"Exported: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
+ f.write(f"Contents:\n")
+ f.write(f"- conversation.txt (plain text)\n")
+ f.write(f"- conversation.html (HTML format)\n")
+ if os.path.exists(os.path.join(folder_name, "conversation_full.html")):
+ f.write(f"- conversation_full.html (styled document)\n")
+ f.write(f"- images/ ({images_copied} files)\n")
+ f.write(f"- videos/ ({videos_copied} files)\n")
+
+ # Status message
+ status_msg = f"Exported to {folder_name} ({images_copied} images, {videos_copied} videos)"
+ if main_window.statusBar():
+ main_window.statusBar().showMessage(status_msg)
+ print(f"Session exported to {folder_name}")
+
+ # Show success message
+ QMessageBox.information(
+ self,
+ "Export Complete",
+ f"Session exported successfully!\n\n"
+ f"Location: {folder_name}\n\n"
+ f"• Conversation (txt, html)\n"
+ f"• {images_copied} images\n"
+ f"• {videos_copied} videos"
+ )
+
+ except Exception as e:
+ error_msg = f"Error exporting session: {str(e)}"
+ QMessageBox.critical(self, "Export Error", error_msg)
+ print(error_msg)
+ import traceback
+ traceback.print_exc()
diff --git a/src/ui/main_window.py b/src/ui/main_window.py
new file mode 100644
index 0000000..507606b
--- /dev/null
+++ b/src/ui/main_window.py
@@ -0,0 +1,435 @@
+from PyQt6.QtWidgets import (
+ QMainWindow, QWidget, QHBoxLayout, QSplitter, QLabel, QCheckBox
+)
+from PyQt6.QtCore import Qt, QTimer
+from PyQt6.QtGui import QPainter, QRadialGradient, QColor, QPen
+import os
+import math
+import random
+import json
+
+from src.ui.colors import COLORS
+from src.ui.conversation_pane import ConversationPane
+from src.ui.sidebar import RightSidebar
+from src.ui.widgets.custom_widgets import SignalIndicator, ScanlineOverlayWidget
+from src.core.conversation_manager import ConversationManager
+
+class CentralContainer(QWidget):
+ """Central container widget with animated background and overlay support"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+
+ # Background animation state
+ self.bg_offset = 0
+ self.noise_offset = 0
+
+ # Animation timer for background
+ self.bg_timer = QTimer(self)
+ self.bg_timer.timeout.connect(self._animate_bg)
+ self.bg_timer.start(80) # ~12 FPS for subtle movement
+
+ # Create scanline overlay as child widget
+ self.scanline_overlay = ScanlineOverlayWidget(self)
+ self.scanline_overlay.hide()
+
+ def _animate_bg(self):
+ self.bg_offset = (self.bg_offset + 1) % 360
+ self.noise_offset = (self.noise_offset + 0.5) % 100
+ self.update()
+
+ def set_scanlines_enabled(self, enabled):
+ """Toggle scanline effect"""
+ if enabled:
+ # Ensure overlay has correct geometry before showing
+ self.scanline_overlay.setGeometry(self.rect())
+ self.scanline_overlay.show()
+ self.scanline_overlay.raise_()
+ self.scanline_overlay.start_animation()
+ else:
+ self.scanline_overlay.stop_animation()
+ self.scanline_overlay.hide()
+
+ def resizeEvent(self, event):
+ """Update scanline overlay size when container resizes"""
+ super().resizeEvent(event)
+ self.scanline_overlay.setGeometry(self.rect())
+
+ def paintEvent(self, event):
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+
+ # ═══ ANIMATED BACKGROUND ═══
+ # Create shifting gradient with more visible movement
+ center_x = self.width() / 2 + math.sin(math.radians(self.bg_offset)) * 100
+ center_y = self.height() / 2 + math.cos(math.radians(self.bg_offset * 0.7)) * 60
+
+ gradient = QRadialGradient(center_x, center_y, max(self.width(), self.height()) * 0.9)
+
+ # More visible atmospheric colors with cyan tint
+ pulse = 0.5 + 0.5 * math.sin(math.radians(self.bg_offset * 2))
+ center_r = int(10 + 8 * pulse)
+ center_g = int(15 + 10 * pulse)
+ center_b = int(30 + 15 * pulse)
+
+ gradient.setColorAt(0, QColor(center_r, center_g, center_b))
+ gradient.setColorAt(0.4, QColor(10, 14, 26))
+ gradient.setColorAt(1, QColor(6, 8, 14))
+
+ painter.fillRect(self.rect(), gradient)
+
+ # Add subtle glow lines at edges
+ glow_alpha = int(15 + 10 * pulse)
+ glow_color = QColor(6, 182, 212, glow_alpha) # Cyan glow
+ painter.setPen(QPen(glow_color, 2))
+
+ # Top edge glow
+ painter.drawLine(0, 0, self.width(), 0)
+ # Bottom edge glow
+ painter.drawLine(0, self.height() - 1, self.width(), self.height() - 1)
+ # Left edge glow
+ painter.drawLine(0, 0, 0, self.height())
+ # Right edge glow
+ painter.drawLine(self.width() - 1, 0, self.width() - 1, self.height())
+
+ # Add subtle noise/grain pattern
+ noise_color = QColor(COLORS['accent_cyan'])
+ noise_color.setAlpha(8)
+ painter.setPen(Qt.PenStyle.NoPen)
+ painter.setBrush(noise_color)
+
+ # Sparse random dots for grain effect
+ random.seed(int(self.noise_offset))
+ for _ in range(50):
+ x = random.randint(0, self.width())
+ y = random.randint(0, self.height())
+ painter.drawEllipse(x, y, 1, 1)
+
+class LiminalBackroomsApp(QMainWindow):
+ """Main application window"""
+ def __init__(self):
+ super().__init__()
+
+ # Main app state
+ self.conversation = []
+ self.turn_count = 0
+ self.images = []
+ self.image_paths = []
+ self.session_videos = []
+ self.branch_conversations = {}
+ self.active_branch = None
+ self.muted_ais = set()
+
+ # Set up the UI
+ self.setup_ui()
+
+ # Connect signals and slots
+ self.connect_signals()
+
+ # Dark theme
+ self.apply_dark_theme()
+
+ # Restore splitter state if available
+ self.restore_splitter_state()
+
+ # Start maximized
+ self.showMaximized()
+
+ # Create conversation manager
+ self.conversation_manager = ConversationManager(self)
+ self.conversation_manager.initialize()
+
+ def setup_ui(self):
+ """Set up the user interface"""
+ self.setWindowTitle("╔═ LIMINAL BACKROOMS v0.7 ═╗")
+ self.setGeometry(100, 100, 1600, 900)
+ self.setMinimumSize(1200, 800)
+
+ self.central_container = CentralContainer()
+ self.setCentralWidget(self.central_container)
+
+ main_layout = QHBoxLayout(self.central_container)
+ main_layout.setContentsMargins(10, 10, 10, 10)
+ main_layout.setSpacing(5)
+
+ self.splitter = QSplitter(Qt.Orientation.Horizontal)
+ self.splitter.setHandleWidth(8)
+ self.splitter.setChildrenCollapsible(False)
+ self.splitter.setStyleSheet(f"""
+ QSplitter::handle {{
+ background-color: {COLORS['border']};
+ border: 1px solid {COLORS['border_highlight']};
+ border-radius: 2px;
+ }}
+ QSplitter::handle:hover {{
+ background-color: {COLORS['accent_blue']};
+ }}
+ """)
+ main_layout.addWidget(self.splitter)
+
+ self.left_pane = ConversationPane()
+ self.right_sidebar = RightSidebar()
+
+ self.splitter.addWidget(self.left_pane)
+ self.splitter.addWidget(self.right_sidebar)
+
+ total_width = 1600
+ self.splitter.setSizes([int(total_width * 0.70), int(total_width * 0.30)])
+
+ self.right_sidebar.add_node('main', 'Seed', 'main')
+
+ # Status bar
+ self.statusBar().setStyleSheet(f"""
+ QStatusBar {{
+ background-color: {COLORS['bg_dark']};
+ color: {COLORS['text_dim']};
+ border-top: 1px solid {COLORS['border']};
+ padding: 3px;
+ font-size: 11px;
+ }}
+ """)
+ self.statusBar().showMessage("Ready")
+
+ self.notification_label = QLabel("")
+ self.notification_label.setStyleSheet(f"""
+ QLabel {{
+ color: {COLORS['accent_cyan']};
+ font-size: 11px;
+ padding: 2px 10px;
+ background-color: transparent;
+ }}
+ """)
+ self.notification_label.setMaximumWidth(500)
+ self.statusBar().addWidget(self.notification_label, 1)
+
+ self.signal_indicator = SignalIndicator()
+ self.statusBar().addPermanentWidget(self.signal_indicator)
+
+ self.crt_checkbox = QCheckBox("CRT")
+ self.crt_checkbox.setStyleSheet(f"""
+ QCheckBox {{
+ color: {COLORS['text_dim']};
+ font-size: 10px;
+ spacing: 4px;
+ }}
+ QCheckBox::indicator {{
+ width: 12px;
+ height: 12px;
+ border: 1px solid {COLORS['border_glow']};
+ border-radius: 2px;
+ background: {COLORS['bg_dark']};
+ }}
+ QCheckBox::indicator:checked {{
+ background: {COLORS['accent_cyan']};
+ }}
+ """)
+ self.crt_checkbox.setToolTip("Toggle CRT scanline effect")
+ self.crt_checkbox.toggled.connect(self.toggle_crt_effect)
+ self.statusBar().addPermanentWidget(self.crt_checkbox)
+
+ def toggle_crt_effect(self, enabled):
+ """Toggle the CRT scanline effect"""
+ if hasattr(self, 'central_container'):
+ self.central_container.set_scanlines_enabled(enabled)
+
+ def set_signal_active(self, active):
+ """Set signal indicator to active (waiting for response)"""
+ self.signal_indicator.set_active(active)
+
+ def update_signal_latency(self, latency_ms):
+ """Update signal indicator with response latency"""
+ self.signal_indicator.set_latency(latency_ms)
+
+ def connect_signals(self):
+ """Connect all signals and slots"""
+ self.right_sidebar.nodeSelected.connect(self.on_branch_select)
+
+ # Save/Load Session buttons
+ if hasattr(self.right_sidebar.control_panel, 'save_session_button'):
+ self.right_sidebar.control_panel.save_session_button.clicked.connect(self.save_session)
+ if hasattr(self.right_sidebar.control_panel, 'load_session_button'):
+ self.right_sidebar.control_panel.load_session_button.clicked.connect(self.load_session)
+
+ if hasattr(self.right_sidebar.network_pane.network_view, 'nodeHovered'):
+ self.right_sidebar.network_pane.network_view.nodeHovered.connect(self.on_node_hover)
+
+ self.left_pane.set_rabbithole_callback(self.conversation_manager.rabbithole_callback if hasattr(self, 'conversation_manager') else self.dummy_callback)
+ self.left_pane.set_fork_callback(self.conversation_manager.fork_callback if hasattr(self, 'conversation_manager') else self.dummy_callback)
+
+ self.splitter.splitterMoved.connect(self.save_splitter_state)
+
+ def dummy_callback(self, *args):
+ pass
+
+ def apply_dark_theme(self):
+ """Apply dark theme to the application"""
+ self.setStyleSheet(f"""
+ QMainWindow {{
+ background-color: {COLORS['bg_dark']};
+ color: {COLORS['text_normal']};
+ }}
+ QWidget {{
+ background-color: {COLORS['bg_dark']};
+ color: {COLORS['text_normal']};
+ }}
+ QToolTip {{
+ background-color: {COLORS['bg_light']};
+ color: {COLORS['text_normal']};
+ border: 1px solid {COLORS['border']};
+ padding: 5px;
+ }}
+ """)
+
+ def on_node_hover(self, node_id):
+ if node_id == 'main':
+ self.statusBar().showMessage("Main conversation")
+ elif node_id in self.branch_conversations:
+ branch_data = self.branch_conversations[node_id]
+ branch_type = branch_data.get('type', 'branch')
+ selected_text = branch_data.get('selected_text', '')
+ self.statusBar().showMessage(f"{branch_type.capitalize()}: {selected_text[:50]}...")
+
+ def on_branch_select(self, branch_id):
+ try:
+ if branch_id == 'main':
+ self.active_branch = None
+ if not hasattr(self, 'main_conversation'):
+ self.main_conversation = []
+ self.conversation = self.main_conversation
+ self.left_pane.display_conversation(self.conversation)
+ self.statusBar().showMessage("Switched to main conversation")
+ return
+
+ if branch_id not in self.branch_conversations:
+ self.statusBar().showMessage(f"Branch {branch_id} not found")
+ return
+
+ branch_data = self.branch_conversations[branch_id]
+ self.active_branch = branch_id
+ self.conversation = branch_data['conversation']
+ self.left_pane.display_conversation(self.conversation, branch_data)
+ self.statusBar().showMessage(f"Switched to {branch_data['type']} branch: {branch_id}")
+
+ except Exception as e:
+ print(f"Error selecting branch: {e}")
+ self.statusBar().showMessage(f"Error selecting branch: {e}")
+
+ def save_splitter_state(self):
+ try:
+ if not os.path.exists('settings'):
+ os.makedirs('settings')
+ with open('settings/splitter_state.json', 'w') as f:
+ json.dump({'sizes': self.splitter.sizes()}, f)
+ except Exception as e:
+ print(f"Error saving splitter state: {e}")
+
+ def restore_splitter_state(self):
+ try:
+ if os.path.exists('settings/splitter_state.json'):
+ with open('settings/splitter_state.json', 'r') as f:
+ state = json.load(f)
+ if 'sizes' in state:
+ self.splitter.setSizes(state['sizes'])
+ except Exception as e:
+ print(f"Error restoring splitter state: {e}")
+
+ def save_session(self):
+ """Save the current session"""
+ from PyQt6.QtWidgets import QFileDialog, QMessageBox
+
+ # Get filename
+ file_path, _ = QFileDialog.getSaveFileName(
+ self,
+ "Save Session",
+ os.path.join(os.getcwd(), "sessions"),
+ "JSON Files (*.json)"
+ )
+
+ if not file_path:
+ return
+
+ filename = os.path.basename(file_path)
+
+ # Gather data
+ conversation = self.main_conversation if hasattr(self, 'main_conversation') else []
+
+ success, result = self.conversation_manager.session_manager.save_session(
+ filename,
+ conversation,
+ self.branch_conversations,
+ self.active_branch,
+ {
+ "ai_models": {
+ "AI-1": self.right_sidebar.control_panel.ai1_model_selector.currentText(),
+ "AI-2": self.right_sidebar.control_panel.ai2_model_selector.currentText(),
+ "AI-3": self.right_sidebar.control_panel.ai3_model_selector.currentText(),
+ "AI-4": self.right_sidebar.control_panel.ai4_model_selector.currentText(),
+ "AI-5": self.right_sidebar.control_panel.ai5_model_selector.currentText(),
+ },
+ "scenario": self.right_sidebar.control_panel.prompt_pair_selector.currentText()
+ }
+ )
+
+ if success:
+ self.statusBar().showMessage(f"Session saved to {result}")
+ QMessageBox.information(self, "Success", "Session saved successfully!")
+ else:
+ QMessageBox.critical(self, "Error", f"Failed to save session: {result}")
+
+ def load_session(self):
+ """Load a saved session"""
+ from PyQt6.QtWidgets import QFileDialog, QMessageBox
+
+ # Get filename
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ "Load Session",
+ os.path.join(os.getcwd(), "sessions"),
+ "JSON Files (*.json)"
+ )
+
+ if not file_path:
+ return
+
+ filename = os.path.basename(file_path)
+
+ success, result = self.conversation_manager.session_manager.load_session(filename)
+
+ if success:
+ data = result
+
+ # Restore conversation
+ self.main_conversation = data.get("conversation", [])
+ self.branch_conversations = data.get("branch_conversations", {})
+ self.active_branch = data.get("active_branch")
+
+ # Restore active branch or main conversation
+ if self.active_branch and self.active_branch in self.branch_conversations:
+ self.on_branch_select(self.active_branch)
+ else:
+ self.active_branch = None
+ self.conversation = self.main_conversation
+ self.left_pane.display_conversation(self.conversation)
+
+ # Restore graph
+ # This requires rebuilding the graph structure in network pane
+ # For now, we'll just clear and rebuild basic nodes if possible
+ # Ideally, NetworkPane should support bulk loading
+
+ # Restore UI state if metadata exists
+ metadata = data.get("metadata", {})
+ if "ai_models" in metadata:
+ models = metadata["ai_models"]
+ self.right_sidebar.control_panel.ai1_model_selector.setCurrentText(models.get("AI-1", ""))
+ self.right_sidebar.control_panel.ai2_model_selector.setCurrentText(models.get("AI-2", ""))
+ self.right_sidebar.control_panel.ai3_model_selector.setCurrentText(models.get("AI-3", ""))
+ self.right_sidebar.control_panel.ai4_model_selector.setCurrentText(models.get("AI-4", ""))
+ self.right_sidebar.control_panel.ai5_model_selector.setCurrentText(models.get("AI-5", ""))
+
+ if "scenario" in metadata:
+ self.right_sidebar.control_panel.prompt_pair_selector.setCurrentText(metadata["scenario"])
+
+ self.statusBar().showMessage(f"Session loaded from {filename}")
+ QMessageBox.information(self, "Success", "Session loaded successfully!")
+ else:
+ QMessageBox.critical(self, "Error", f"Failed to load session: {result}")
diff --git a/src/ui/sidebar.py b/src/ui/sidebar.py
new file mode 100644
index 0000000..a6244ca
--- /dev/null
+++ b/src/ui/sidebar.py
@@ -0,0 +1,261 @@
+from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QStackedWidget
+from PyQt6.QtCore import pyqtSignal
+from src.ui.colors import COLORS
+from src.ui.control_panel import ControlPanel
+from src.ui.widgets.network_graph import NetworkGraphWidget
+from src.ui.widgets.media_panes import ImagePreviewPane, VideoPreviewPane
+
+class NetworkPane(QWidget):
+ nodeSelected = pyqtSignal(str)
+
+ def __init__(self):
+ super().__init__()
+
+ # Main layout
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(10, 10, 10, 10)
+
+ # Network view
+ from src.ui.widgets.network_graph import NetworkGraphWidget
+ from PyQt6.QtWidgets import QSizePolicy, QLabel
+ from PyQt6.QtCore import Qt
+
+ # Title
+ title = QLabel("Propagation Network")
+ title.setStyleSheet("color: #D4D4D4; font-size: 14px; font-weight: bold; font-family: 'Orbitron', sans-serif;")
+ layout.addWidget(title, alignment=Qt.AlignmentFlag.AlignCenter)
+
+ self.network_view = NetworkGraphWidget()
+ self.network_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
+ layout.addWidget(self.network_view, 1)
+
+ # Connect signals
+ self.network_view.nodeSelected.connect(self.nodeSelected)
+
+ # Initialize graph logic - moved from original NetworkPane to avoid circular imports if needed
+ # But for now, we'll implement wrappers
+
+ def add_node(self, node_id, label, node_type='branch'):
+ # Simplified: Pass to widget if it had full logic, or implement here
+ # Since logic was mixed in original, I'll reimplement minimal logic here or in widget
+ # The widget handles drawing, we handle graph structure
+ # Wait, the original NetworkGraphWidget was just drawing. NetworkPane had the graph logic.
+ # I need to port the graph logic here.
+ import networkx as nx
+ import math
+ import random
+
+ if not hasattr(self, 'graph'):
+ self.graph = nx.DiGraph()
+ self.node_positions = {}
+ self.node_colors = {}
+ self.node_labels = {}
+ self.node_sizes = {}
+
+ try:
+ self.graph.add_node(node_id)
+
+ if node_type == 'main':
+ color = '#569CD6'
+ size = 800
+ elif node_type == 'rabbithole':
+ color = '#B5CEA8'
+ size = 600
+ elif node_type == 'fork':
+ color = '#DCDCAA'
+ size = 600
+ else:
+ color = '#CE9178'
+ size = 400
+
+ self.node_colors[node_id] = color
+ self.node_labels[node_id] = label
+ self.node_sizes[node_id] = size
+
+ # Position calculation
+ num_nodes = len(self.graph.nodes) - 1
+ if node_type == 'main':
+ self.node_positions[node_id] = (0, 0)
+ else:
+ golden_ratio = 1.618033988749895
+ angle = 2 * math.pi * golden_ratio * num_nodes
+ base_distance = 200
+ count_factor = min(1.0, num_nodes / 20)
+
+ if node_type == 'rabbithole':
+ distance = base_distance * (1.0 + count_factor * 0.5)
+ elif node_type == 'fork':
+ distance = base_distance * (1.2 + count_factor * 0.5)
+ else:
+ distance = base_distance * (1.4 + count_factor * 0.5)
+
+ x = distance * math.cos(angle)
+ y = distance * math.sin(angle)
+ x += random.uniform(-30, 30)
+ y += random.uniform(-30, 30)
+
+ self.node_positions[node_id] = (x, y)
+
+ self.update_graph()
+
+ except Exception as e:
+ print(f"Error adding node: {e}")
+
+ def add_edge(self, source_id, target_id):
+ if not hasattr(self, 'graph'):
+ return
+ try:
+ self.graph.add_edge(source_id, target_id)
+ self.update_graph()
+ except Exception as e:
+ print(f"Error adding edge: {e}")
+
+ def update_graph(self):
+ if hasattr(self, 'network_view'):
+ self.network_view.nodes = list(self.graph.nodes())
+ self.network_view.edges = list(self.graph.edges())
+ self.network_view.node_positions = self.node_positions
+ self.network_view.node_colors = self.node_colors
+ self.network_view.node_labels = self.node_labels
+ self.network_view.node_sizes = self.node_sizes
+ self.network_view.update()
+
+class RightSidebar(QWidget):
+ """Right sidebar with tabbed interface for Setup and Network Graph"""
+ nodeSelected = pyqtSignal(str)
+
+ def __init__(self):
+ super().__init__()
+ self.setMinimumWidth(300)
+ self.setup_ui()
+
+ def setup_ui(self):
+ """Set up the tabbed sidebar interface"""
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(5, 5, 5, 5)
+ layout.setSpacing(0)
+
+ # Create tab bar at the top (custom styled)
+ tab_container = QWidget()
+ tab_container.setStyleSheet(f"""
+ QWidget {{
+ background-color: {COLORS['bg_medium']};
+ border-bottom: 1px solid {COLORS['border_glow']};
+ }}
+ """)
+ tab_layout = QHBoxLayout(tab_container)
+ tab_layout.setContentsMargins(0, 0, 0, 0)
+ tab_layout.setSpacing(0)
+
+ # Tab buttons
+ self.setup_button = QPushButton("⚙ SETUP")
+ self.graph_button = QPushButton("🌐 GRAPH")
+ self.image_button = QPushButton("🖼 IMAGE")
+ self.video_button = QPushButton("🎬 VIDEO")
+
+ # Cyberpunk tab button styling
+ tab_style = f"""
+ QPushButton {{
+ background-color: {COLORS['bg_medium']};
+ color: {COLORS['text_dim']};
+ border: none;
+ border-bottom: 2px solid transparent;
+ padding: 12px 12px;
+ font-weight: bold;
+ font-size: 10px;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ }}
+ QPushButton:hover {{
+ background-color: {COLORS['bg_light']};
+ color: {COLORS['text_normal']};
+ }}
+ QPushButton:checked {{
+ background-color: {COLORS['bg_dark']};
+ color: {COLORS['accent_cyan']};
+ border-bottom: 2px solid {COLORS['accent_cyan']};
+ }}
+ """
+
+ self.setup_button.setStyleSheet(tab_style)
+ self.graph_button.setStyleSheet(tab_style)
+ self.image_button.setStyleSheet(tab_style)
+ self.video_button.setStyleSheet(tab_style)
+
+ # Make buttons checkable for tab behavior
+ self.setup_button.setCheckable(True)
+ self.graph_button.setCheckable(True)
+ self.image_button.setCheckable(True)
+ self.video_button.setCheckable(True)
+ self.setup_button.setChecked(True) # Start with setup tab active
+
+ # Connect tab buttons
+ self.setup_button.clicked.connect(lambda: self.switch_tab(0))
+ self.graph_button.clicked.connect(lambda: self.switch_tab(1))
+ self.image_button.clicked.connect(lambda: self.switch_tab(2))
+ self.video_button.clicked.connect(lambda: self.switch_tab(3))
+
+ tab_layout.addWidget(self.setup_button)
+ tab_layout.addWidget(self.graph_button)
+ tab_layout.addWidget(self.image_button)
+ tab_layout.addWidget(self.video_button)
+
+ layout.addWidget(tab_container)
+
+ # Create stacked widget for tab content
+ self.stack = QStackedWidget()
+ self.stack.setStyleSheet(f"""
+ QStackedWidget {{
+ background-color: {COLORS['bg_dark']};
+ border: none;
+ }}
+ """)
+
+ # Create tab pages
+ self.control_panel = ControlPanel()
+ self.network_pane = NetworkPane()
+ self.image_preview_pane = ImagePreviewPane()
+ self.video_preview_pane = VideoPreviewPane()
+
+ # Add pages to stack
+ self.stack.addWidget(self.control_panel)
+ self.stack.addWidget(self.network_pane)
+ self.stack.addWidget(self.image_preview_pane)
+ self.stack.addWidget(self.video_preview_pane)
+
+ layout.addWidget(self.stack, 1) # Stretch to fill
+
+ # Connect network pane signal to forward it
+ self.network_pane.nodeSelected.connect(self.nodeSelected)
+
+ def switch_tab(self, index):
+ """Switch between tabs"""
+ self.stack.setCurrentIndex(index)
+
+ # Update button states
+ self.setup_button.setChecked(index == 0)
+ self.graph_button.setChecked(index == 1)
+ self.image_button.setChecked(index == 2)
+ self.video_button.setChecked(index == 3)
+
+ def update_image_preview(self, image_path):
+ """Update the image preview pane with a new image"""
+ if hasattr(self, 'image_preview_pane'):
+ self.image_preview_pane.set_image(image_path)
+
+ def update_video_preview(self, video_path):
+ """Update the video preview pane with a new video"""
+ if hasattr(self, 'video_preview_pane'):
+ self.video_preview_pane.set_video(video_path)
+
+ def add_node(self, node_id, label, node_type):
+ """Forward to network pane"""
+ self.network_pane.add_node(node_id, label, node_type)
+
+ def add_edge(self, source_id, target_id):
+ """Forward to network pane"""
+ self.network_pane.add_edge(source_id, target_id)
+
+ def update_graph(self):
+ """Forward to network pane"""
+ self.network_pane.update_graph()
diff --git a/src/ui/widgets/__init__.py b/src/ui/widgets/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/ui/widgets/custom_widgets.py b/src/ui/widgets/custom_widgets.py
new file mode 100644
index 0000000..c4f6637
--- /dev/null
+++ b/src/ui/widgets/custom_widgets.py
@@ -0,0 +1,264 @@
+from PyQt6.QtWidgets import QPushButton, QWidget, QGraphicsDropShadowEffect
+from PyQt6.QtGui import QColor, QPainter, QPen, QLinearGradient
+from PyQt6.QtCore import Qt, QTimer, QRectF
+import math
+import random
+from src.ui.colors import COLORS
+
+class GlowButton(QPushButton):
+ """Enhanced button with glow effect on hover"""
+
+ def __init__(self, text, glow_color=COLORS['accent_cyan'], parent=None):
+ super().__init__(text, parent)
+ self.glow_color = glow_color
+ self.base_blur = 8
+ self.hover_blur = 20
+
+ # Create shadow effect
+ self.shadow = QGraphicsDropShadowEffect()
+ self.shadow.setBlurRadius(self.base_blur)
+ self.shadow.setColor(QColor(glow_color))
+ self.shadow.setOffset(0, 2)
+ self.setGraphicsEffect(self.shadow)
+
+ # Track hover state for animation
+ self.setMouseTracking(True)
+
+ def enterEvent(self, event):
+ """Increase glow on hover"""
+ self.shadow.setBlurRadius(self.hover_blur)
+ self.shadow.setColor(QColor(self.glow_color))
+ super().enterEvent(event)
+
+ def leaveEvent(self, event):
+ """Decrease glow when not hovering"""
+ self.shadow.setBlurRadius(self.base_blur)
+ super().leaveEvent(event)
+
+class DepthGauge(QWidget):
+ """Vertical gauge showing conversation depth/turn progress"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.current_turn = 0
+ self.max_turns = 10
+ self.setFixedWidth(24)
+ self.setMinimumHeight(100)
+
+ # Animation
+ self.pulse_offset = 0
+ self.pulse_timer = QTimer(self)
+ self.pulse_timer.timeout.connect(self._animate_pulse)
+ self.pulse_timer.start(50)
+
+ def _animate_pulse(self):
+ self.pulse_offset = (self.pulse_offset + 2) % 360
+ self.update()
+
+ def set_progress(self, current, maximum):
+ """Update the gauge progress"""
+ self.current_turn = current
+ self.max_turns = max(maximum, 1)
+ self.update()
+
+ def paintEvent(self, event):
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+
+ w, h = self.width(), self.height()
+ margin = 4
+ gauge_width = w - margin * 2
+ gauge_height = h - margin * 2
+
+ # Background track
+ painter.setPen(Qt.PenStyle.NoPen)
+ painter.setBrush(QColor(COLORS['bg_dark']))
+ painter.drawRoundedRect(margin, margin, gauge_width, gauge_height, 4, 4)
+
+ # Border
+ painter.setPen(QPen(QColor(COLORS['border_glow']), 1))
+ painter.setBrush(Qt.BrushStyle.NoBrush)
+ painter.drawRoundedRect(margin, margin, gauge_width, gauge_height, 4, 4)
+
+ # Calculate fill height (fills from bottom to top)
+ progress = min(self.current_turn / self.max_turns, 1.0)
+ fill_height = int(gauge_height * progress)
+ fill_y = margin + gauge_height - fill_height
+
+ if fill_height > 0:
+ # Gradient fill
+ gradient = QLinearGradient(0, fill_y, 0, margin + gauge_height)
+
+ # Color shifts based on depth
+ if progress < 0.33:
+ gradient.setColorAt(0, QColor(COLORS['accent_cyan']))
+ gradient.setColorAt(1, QColor(COLORS['accent_cyan']).darker(130))
+ elif progress < 0.66:
+ gradient.setColorAt(0, QColor(COLORS['accent_purple']))
+ gradient.setColorAt(1, QColor(COLORS['accent_cyan']))
+ else:
+ gradient.setColorAt(0, QColor(COLORS['accent_pink']))
+ gradient.setColorAt(1, QColor(COLORS['accent_purple']))
+
+ painter.setPen(Qt.PenStyle.NoPen)
+ painter.setBrush(gradient)
+ painter.drawRoundedRect(margin + 2, fill_y, gauge_width - 4, fill_height, 2, 2)
+
+ # Pulsing glow line
+ pulse_alpha = int(100 + 80 * math.sin(math.radians(self.pulse_offset)))
+ glow_color = QColor(COLORS['accent_cyan'])
+ glow_color.setAlpha(pulse_alpha)
+ painter.setPen(QPen(glow_color, 2))
+ painter.drawLine(margin + 2, fill_y, margin + gauge_width - 2, fill_y)
+
+ # Turn counter text
+ painter.setPen(QColor(COLORS['text_dim']))
+ font = painter.font()
+ font.setPixelSize(9)
+ painter.setFont(font)
+ text = f"{self.current_turn}"
+ painter.drawText(self.rect(), Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, text)
+
+class SignalIndicator(QWidget):
+ """Signal strength/latency indicator"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setFixedSize(80, 20)
+ self.signal_strength = 1.0 # 0.0 to 1.0
+ self.latency_ms = 0
+ self.is_active = False
+
+ # Animation for activity
+ self.bar_offset = 0
+ self.activity_timer = QTimer(self)
+ self.activity_timer.timeout.connect(self._animate)
+
+ def _animate(self):
+ self.bar_offset = (self.bar_offset + 1) % 5
+ self.update()
+
+ def set_active(self, active):
+ """Set whether we're actively waiting for a response"""
+ self.is_active = active
+ if active:
+ self.activity_timer.start(100)
+ else:
+ self.activity_timer.stop()
+ self.update()
+
+ def set_latency(self, latency_ms):
+ """Update the latency display"""
+ self.latency_ms = latency_ms
+ # Calculate signal strength based on latency
+ if latency_ms < 500:
+ self.signal_strength = 1.0
+ elif latency_ms < 1500:
+ self.signal_strength = 0.75
+ elif latency_ms < 3000:
+ self.signal_strength = 0.5
+ else:
+ self.signal_strength = 0.25
+ self.update()
+
+ def paintEvent(self, event):
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+
+ # Draw signal bars
+ bar_heights = [4, 7, 10, 13, 16]
+ bar_width = 4
+ spacing = 2
+ start_x = 5
+ base_y = 18
+
+ for i, bar_h in enumerate(bar_heights):
+ x = start_x + i * (bar_width + spacing)
+ y = base_y - bar_h
+
+ # Determine if this bar should be lit
+ threshold = (i + 1) / len(bar_heights)
+ is_lit = self.signal_strength >= threshold
+
+ if self.is_active:
+ # Animated pattern when active
+ is_lit = ((i + self.bar_offset) % 5) < 3
+ color = QColor(COLORS['accent_cyan']) if is_lit else QColor(COLORS['bg_light'])
+ else:
+ if is_lit:
+ # Color based on signal strength
+ if self.signal_strength > 0.7:
+ color = QColor(COLORS['accent_green'])
+ elif self.signal_strength > 0.4:
+ color = QColor(COLORS['accent_yellow'])
+ else:
+ color = QColor(COLORS['accent_pink'])
+ else:
+ color = QColor(COLORS['bg_light'])
+
+ painter.setPen(Qt.PenStyle.NoPen)
+ painter.setBrush(color)
+ painter.drawRoundedRect(x, y, bar_width, bar_h, 1, 1)
+
+ # Draw latency text
+ painter.setPen(QColor(COLORS['text_dim']))
+ font = painter.font()
+ font.setPixelSize(9)
+ painter.setFont(font)
+
+ if self.is_active:
+ text = "···"
+ elif self.latency_ms > 0:
+ text = f"{self.latency_ms}ms"
+ else:
+ text = "IDLE"
+
+ painter.drawText(40, 3, 40, 16, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, text)
+
+class ScanlineOverlayWidget(QWidget):
+ """Transparent overlay widget for CRT scanline effect"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
+ self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
+
+ self.scanline_offset = 0
+ self.intensity = 0.25 # More visible scanlines
+
+ self.anim_timer = QTimer(self)
+ self.anim_timer.timeout.connect(self._animate)
+
+ def start_animation(self):
+ self.anim_timer.start(100)
+
+ def stop_animation(self):
+ self.anim_timer.stop()
+
+ def _animate(self):
+ self.scanline_offset = (self.scanline_offset + 1) % 4
+ self.update()
+
+ def paintEvent(self, event):
+ painter = QPainter(self)
+
+ # Draw horizontal scanlines - more visible
+ line_alpha = int(255 * self.intensity)
+ line_color = QColor(0, 0, 0, line_alpha)
+ painter.setPen(QPen(line_color, 1))
+
+ # Draw every 2nd line for more visible effect
+ for y in range(self.scanline_offset, self.height(), 2):
+ painter.drawLine(0, y, self.width(), y)
+
+ # Subtle vignette effect at edges
+ from PyQt6.QtGui import QRadialGradient
+ gradient = QRadialGradient(self.width() / 2, self.height() / 2,
+ max(self.width(), self.height()) * 0.7)
+ gradient.setColorAt(0, QColor(0, 0, 0, 0))
+ gradient.setColorAt(0.7, QColor(0, 0, 0, 0))
+ gradient.setColorAt(1, QColor(0, 0, 0, int(255 * self.intensity * 1.5)))
+
+ painter.setPen(Qt.PenStyle.NoPen)
+ painter.setBrush(gradient)
+ painter.drawRect(self.rect())
diff --git a/src/ui/widgets/media_panes.py b/src/ui/widgets/media_panes.py
new file mode 100644
index 0000000..bf98520
--- /dev/null
+++ b/src/ui/widgets/media_panes.py
@@ -0,0 +1,536 @@
+import os
+from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSizePolicy
+from PyQt6.QtGui import QPixmap
+from PyQt6.QtCore import Qt, QSize
+from src.ui.colors import COLORS
+
+class ImagePreviewPane(QWidget):
+ """Pane to display generated images with navigation"""
+ def __init__(self):
+ super().__init__()
+ self.current_image_path = None
+ self.session_images = [] # List of all images generated this session
+ self.current_index = -1 # Current image index
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(10, 10, 10, 10)
+ layout.setSpacing(10)
+
+ # Title label
+ self.title = QLabel("🎨 GENERATED IMAGES")
+ self.title.setStyleSheet(f"""
+ QLabel {{
+ color: {COLORS['accent_purple']};
+ font-weight: bold;
+ font-size: 12px;
+ padding: 5px;
+ }}
+ """)
+ self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ layout.addWidget(self.title)
+
+ # Image display label
+ self.image_label = QLabel("No images generated yet")
+ self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.image_label.setStyleSheet(f"""
+ QLabel {{
+ background-color: {COLORS['bg_medium']};
+ border: 2px dashed {COLORS['border']};
+ border-radius: 8px;
+ color: {COLORS['text_dim']};
+ padding: 20px;
+ min-height: 200px;
+ }}
+ """)
+ self.image_label.setWordWrap(True)
+ self.image_label.setScaledContents(False)
+ layout.addWidget(self.image_label, 1)
+
+ # Navigation controls
+ nav_layout = QHBoxLayout()
+ nav_layout.setSpacing(8)
+
+ # Previous button
+ self.prev_button = QPushButton("◀ Prev")
+ self.prev_button.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {COLORS['bg_medium']};
+ color: {COLORS['text_normal']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 4px;
+ padding: 6px 12px;
+ font-weight: bold;
+ }}
+ QPushButton:hover {{
+ background-color: {COLORS['bg_light']};
+ border-color: {COLORS['accent_purple']};
+ }}
+ QPushButton:disabled {{
+ color: {COLORS['text_dim']};
+ background-color: {COLORS['bg_dark']};
+ }}
+ """)
+ self.prev_button.clicked.connect(self.show_previous)
+ self.prev_button.setEnabled(False)
+ nav_layout.addWidget(self.prev_button)
+
+ # Position indicator
+ self.position_label = QLabel("")
+ self.position_label.setStyleSheet(f"""
+ QLabel {{
+ color: {COLORS['text_dim']};
+ font-size: 11px;
+ }}
+ """)
+ self.position_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ nav_layout.addWidget(self.position_label, 1)
+
+ # Next button
+ self.next_button = QPushButton("Next ▶")
+ self.next_button.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {COLORS['bg_medium']};
+ color: {COLORS['text_normal']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 4px;
+ padding: 6px 12px;
+ font-weight: bold;
+ }}
+ QPushButton:hover {{
+ background-color: {COLORS['bg_light']};
+ border-color: {COLORS['accent_purple']};
+ }}
+ QPushButton:disabled {{
+ color: {COLORS['text_dim']};
+ background-color: {COLORS['bg_dark']};
+ }}
+ """)
+ self.next_button.clicked.connect(self.show_next)
+ self.next_button.setEnabled(False)
+ nav_layout.addWidget(self.next_button)
+
+ layout.addLayout(nav_layout)
+
+ # Image info label
+ self.info_label = QLabel("")
+ self.info_label.setStyleSheet(f"""
+ QLabel {{
+ color: {COLORS['text_dim']};
+ font-size: 10px;
+ padding: 5px;
+ }}
+ """)
+ self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.info_label.setWordWrap(True)
+ layout.addWidget(self.info_label)
+
+ # Open in folder button
+ self.open_button = QPushButton("📂 Open Images Folder")
+ self.open_button.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {COLORS['bg_medium']};
+ color: {COLORS['text_normal']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 4px;
+ padding: 8px;
+ }}
+ QPushButton:hover {{
+ background-color: {COLORS['bg_light']};
+ border-color: {COLORS['accent_purple']};
+ }}
+ """)
+ self.open_button.clicked.connect(self.open_images_folder)
+ layout.addWidget(self.open_button)
+
+ def add_image(self, image_path):
+ """Add a new image to the session gallery and display it"""
+ if image_path and os.path.exists(image_path):
+ if image_path not in self.session_images:
+ self.session_images.append(image_path)
+ self.current_index = len(self.session_images) - 1
+ self._display_current()
+
+ def set_image(self, image_path):
+ """Display an image - also adds to gallery if new"""
+ self.add_image(image_path)
+
+ def _display_current(self):
+ """Display the image at current_index"""
+ if not self.session_images or self.current_index < 0:
+ self.image_label.setText("No images generated yet")
+ self.info_label.setText("")
+ self.position_label.setText("")
+ self.prev_button.setEnabled(False)
+ self.next_button.setEnabled(False)
+ return
+
+ image_path = self.session_images[self.current_index]
+ self.current_image_path = image_path
+
+ if os.path.exists(image_path):
+ pixmap = QPixmap(image_path)
+ if not pixmap.isNull():
+ scaled = pixmap.scaled(
+ self.image_label.size() - QSize(20, 20),
+ Qt.AspectRatioMode.KeepAspectRatio,
+ Qt.TransformationMode.SmoothTransformation
+ )
+ self.image_label.setPixmap(scaled)
+ self.image_label.setStyleSheet(f"""
+ QLabel {{
+ background-color: {COLORS['bg_medium']};
+ border: 2px solid {COLORS['accent_purple']};
+ border-radius: 8px;
+ padding: 10px;
+ }}
+ """)
+
+ filename = os.path.basename(image_path)
+ self.info_label.setText(f"📁 {filename}")
+ else:
+ self.image_label.setText("Failed to load image")
+ self.info_label.setText("")
+ else:
+ self.image_label.setText("Image not found")
+ self.info_label.setText("")
+
+ total = len(self.session_images)
+ current = self.current_index + 1
+ self.position_label.setText(f"{current} of {total}")
+ self.prev_button.setEnabled(self.current_index > 0)
+ self.next_button.setEnabled(self.current_index < total - 1)
+
+ def show_previous(self):
+ """Show the previous image"""
+ if self.current_index > 0:
+ self.current_index -= 1
+ self._display_current()
+
+ def show_next(self):
+ """Show the next image"""
+ if self.current_index < len(self.session_images) - 1:
+ self.current_index += 1
+ self._display_current()
+
+ def clear_session(self):
+ """Clear all session images"""
+ self.session_images = []
+ self.current_index = -1
+ self.current_image_path = None
+ self.image_label.setText("No images generated yet")
+ self.image_label.setStyleSheet(f"""
+ QLabel {{
+ background-color: {COLORS['bg_medium']};
+ border: 2px dashed {COLORS['border']};
+ border-radius: 8px;
+ color: {COLORS['text_dim']};
+ padding: 20px;
+ min-height: 200px;
+ }}
+ """)
+ self.info_label.setText("")
+ self.position_label.setText("")
+ self.prev_button.setEnabled(False)
+ self.next_button.setEnabled(False)
+
+ def open_images_folder(self):
+ """Open the images folder in file explorer"""
+ import subprocess
+ images_dir = os.path.abspath("images")
+ if os.path.exists(images_dir):
+ if os.name == 'nt':
+ os.startfile(images_dir)
+ elif os.name == 'posix':
+ subprocess.Popen(['open', images_dir] if os.uname().sysname == 'Darwin' else ['xdg-open', images_dir])
+ else:
+ os.makedirs(images_dir, exist_ok=True)
+ if os.name == 'nt':
+ os.startfile(images_dir)
+ elif os.name == 'posix':
+ subprocess.Popen(['open', images_dir] if os.uname().sysname == 'Darwin' else ['xdg-open', images_dir])
+
+ def resizeEvent(self, event):
+ """Re-scale image when pane is resized"""
+ super().resizeEvent(event)
+ if self.current_image_path:
+ self._display_current()
+
+
+class VideoPreviewPane(QWidget):
+ """Pane to display generated videos with navigation"""
+ def __init__(self):
+ super().__init__()
+ self.current_video_path = None
+ self.session_videos = []
+ self.current_index = -1
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(10, 10, 10, 10)
+ layout.setSpacing(10)
+
+ # Title label
+ self.title = QLabel("🎬 GENERATED VIDEOS")
+ self.title.setStyleSheet(f"""
+ QLabel {{
+ color: {COLORS['accent_cyan']};
+ font-weight: bold;
+ font-size: 12px;
+ padding: 5px;
+ }}
+ """)
+ self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ layout.addWidget(self.title)
+
+ # Video display area
+ self.video_label = QLabel("No videos generated yet")
+ self.video_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.video_label.setStyleSheet(f"""
+ QLabel {{
+ background-color: {COLORS['bg_medium']};
+ border: 2px dashed {COLORS['border']};
+ border-radius: 8px;
+ color: {COLORS['text_dim']};
+ padding: 20px;
+ min-height: 150px;
+ }}
+ """)
+ self.video_label.setWordWrap(True)
+ layout.addWidget(self.video_label, 1)
+
+ # Play button
+ self.play_button = QPushButton("▶ Play Video")
+ self.play_button.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {COLORS['accent_cyan']};
+ color: {COLORS['bg_dark']};
+ border: none;
+ border-radius: 4px;
+ padding: 10px 20px;
+ font-weight: bold;
+ font-size: 12px;
+ }}
+ QPushButton:hover {{
+ background-color: {COLORS['accent_purple']};
+ }}
+ QPushButton:disabled {{
+ background-color: {COLORS['bg_medium']};
+ color: {COLORS['text_dim']};
+ }}
+ """)
+ self.play_button.clicked.connect(self.play_current_video)
+ self.play_button.setEnabled(False)
+ layout.addWidget(self.play_button)
+
+ # Navigation controls
+ nav_layout = QHBoxLayout()
+ nav_layout.setSpacing(8)
+
+ # Previous button
+ self.prev_button = QPushButton("◀ Prev")
+ self.prev_button.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {COLORS['bg_medium']};
+ color: {COLORS['text_normal']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 4px;
+ padding: 6px 12px;
+ font-weight: bold;
+ }}
+ QPushButton:hover {{
+ background-color: {COLORS['bg_light']};
+ border-color: {COLORS['accent_cyan']};
+ }}
+ QPushButton:disabled {{
+ color: {COLORS['text_dim']};
+ background-color: {COLORS['bg_dark']};
+ }}
+ """)
+ self.prev_button.clicked.connect(self.show_previous)
+ self.prev_button.setEnabled(False)
+ nav_layout.addWidget(self.prev_button)
+
+ # Position indicator
+ self.position_label = QLabel("")
+ self.position_label.setStyleSheet(f"""
+ QLabel {{
+ color: {COLORS['text_dim']};
+ font-size: 11px;
+ }}
+ """)
+ self.position_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ nav_layout.addWidget(self.position_label, 1)
+
+ # Next button
+ self.next_button = QPushButton("Next ▶")
+ self.next_button.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {COLORS['bg_medium']};
+ color: {COLORS['text_normal']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 4px;
+ padding: 6px 12px;
+ font-weight: bold;
+ }}
+ QPushButton:hover {{
+ background-color: {COLORS['bg_light']};
+ border-color: {COLORS['accent_cyan']};
+ }}
+ QPushButton:disabled {{
+ color: {COLORS['text_dim']};
+ background-color: {COLORS['bg_dark']};
+ }}
+ """)
+ self.next_button.clicked.connect(self.show_next)
+ self.next_button.setEnabled(False)
+ nav_layout.addWidget(self.next_button)
+
+ layout.addLayout(nav_layout)
+
+ # Video info label
+ self.info_label = QLabel("")
+ self.info_label.setStyleSheet(f"""
+ QLabel {{
+ color: {COLORS['text_dim']};
+ font-size: 10px;
+ padding: 5px;
+ }}
+ """)
+ self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ self.info_label.setWordWrap(True)
+ layout.addWidget(self.info_label)
+
+ # Open in folder button
+ self.open_button = QPushButton("📂 Open Videos Folder")
+ self.open_button.setStyleSheet(f"""
+ QPushButton {{
+ background-color: {COLORS['bg_medium']};
+ color: {COLORS['text_normal']};
+ border: 1px solid {COLORS['border']};
+ border-radius: 4px;
+ padding: 8px;
+ }}
+ QPushButton:hover {{
+ background-color: {COLORS['bg_light']};
+ border-color: {COLORS['accent_cyan']};
+ }}
+ """)
+ self.open_button.clicked.connect(self.open_videos_folder)
+ layout.addWidget(self.open_button)
+
+ def add_video(self, video_path):
+ """Add a new video to the session gallery and display it"""
+ if video_path and os.path.exists(video_path):
+ if video_path not in self.session_videos:
+ self.session_videos.append(video_path)
+ self.current_index = len(self.session_videos) - 1
+ self._display_current()
+
+ def set_video(self, video_path):
+ """Display a video - also adds to gallery if new"""
+ self.add_video(video_path)
+
+ def _display_current(self):
+ """Display the video at current_index"""
+ if not self.session_videos or self.current_index < 0:
+ self.video_label.setText("No videos generated yet")
+ self.info_label.setText("")
+ self.position_label.setText("")
+ self.prev_button.setEnabled(False)
+ self.next_button.setEnabled(False)
+ self.play_button.setEnabled(False)
+ return
+
+ video_path = self.session_videos[self.current_index]
+ self.current_video_path = video_path
+
+ if os.path.exists(video_path):
+ filename = os.path.basename(video_path)
+ self.video_label.setText(f"🎬 {filename}\n\n(Click Play to view)")
+ self.video_label.setStyleSheet(f"""
+ QLabel {{
+ background-color: {COLORS['bg_medium']};
+ border: 2px solid {COLORS['accent_cyan']};
+ border-radius: 8px;
+ color: {COLORS['text_bright']};
+ padding: 20px;
+ min-height: 150px;
+ }}
+ """)
+ self.info_label.setText(f"📁 {filename}")
+ self.play_button.setEnabled(True)
+ else:
+ self.video_label.setText("Video not found")
+ self.info_label.setText("")
+ self.play_button.setEnabled(False)
+
+ total = len(self.session_videos)
+ current = self.current_index + 1
+ self.position_label.setText(f"{current} of {total}")
+ self.prev_button.setEnabled(self.current_index > 0)
+ self.next_button.setEnabled(self.current_index < total - 1)
+
+ def show_previous(self):
+ """Show the previous video"""
+ if self.current_index > 0:
+ self.current_index -= 1
+ self._display_current()
+
+ def show_next(self):
+ """Show the next video"""
+ if self.current_index < len(self.session_videos) - 1:
+ self.current_index += 1
+ self._display_current()
+
+ def play_current_video(self):
+ """Open the current video in the default video player"""
+ if self.current_video_path and os.path.exists(self.current_video_path):
+ import subprocess
+ import sys
+ if sys.platform == 'win32':
+ os.startfile(self.current_video_path)
+ elif sys.platform == 'darwin':
+ subprocess.Popen(['open', self.current_video_path])
+ else:
+ subprocess.Popen(['xdg-open', self.current_video_path])
+
+ def clear_session(self):
+ """Clear all session videos"""
+ self.session_videos = []
+ self.current_index = -1
+ self.current_video_path = None
+ self.video_label.setText("No videos generated yet")
+ self.video_label.setStyleSheet(f"""
+ QLabel {{
+ background-color: {COLORS['bg_medium']};
+ border: 2px dashed {COLORS['border']};
+ border-radius: 8px;
+ color: {COLORS['text_dim']};
+ padding: 20px;
+ min-height: 150px;
+ }}
+ """)
+ self.info_label.setText("")
+ self.position_label.setText("")
+ self.prev_button.setEnabled(False)
+ self.next_button.setEnabled(False)
+ self.play_button.setEnabled(False)
+
+ def open_videos_folder(self):
+ """Open the videos folder in file explorer"""
+ import subprocess
+ videos_dir = os.path.abspath("videos")
+ if os.path.exists(videos_dir):
+ if os.name == 'nt':
+ os.startfile(videos_dir)
+ elif os.name == 'posix':
+ subprocess.Popen(['open', videos_dir] if os.uname().sysname == 'Darwin' else ['xdg-open', videos_dir])
+ else:
+ os.makedirs(videos_dir, exist_ok=True)
+ if os.name == 'nt':
+ os.startfile(videos_dir)
+ elif os.name == 'posix':
+ subprocess.Popen(['open', videos_dir] if os.uname().sysname == 'Darwin' else ['xdg-open', videos_dir])
diff --git a/src/ui/widgets/network_graph.py b/src/ui/widgets/network_graph.py
new file mode 100644
index 0000000..08aba96
--- /dev/null
+++ b/src/ui/widgets/network_graph.py
@@ -0,0 +1,555 @@
+from PyQt6.QtWidgets import QWidget, QToolTip
+from PyQt6.QtGui import QColor, QPainter, QPen, QBrush, QLinearGradient, QRadialGradient, QPainterPath
+from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QPointF
+import math
+import random
+from src.ui.colors import COLORS
+
+class NetworkGraphWidget(QWidget):
+ nodeSelected = pyqtSignal(str)
+ nodeHovered = pyqtSignal(str)
+
+ def __init__(self):
+ super().__init__()
+
+ # Graph data
+ self.nodes = []
+ self.edges = []
+ self.node_positions = {}
+ self.node_colors = {}
+ self.node_labels = {}
+ self.node_sizes = {}
+
+ # Edge animation data
+ self.growing_edges = {} # Dictionary to track growing edges: {(source, target): growth_progress}
+ self.edge_growth_speed = 0.05 # Increased speed of edge growth animation
+
+ # Visual settings
+ self.margin = 50
+ self.selected_node = None
+ self.hovered_node = None
+ self.animation_progress = 0
+ self.animation_timer = QTimer(self)
+ self.animation_timer.timeout.connect(self.update_animation)
+ self.animation_timer.start(50) # 20 FPS animation
+
+ # Mycelial node settings
+ self.hyphae_count = 5 # Number of hyphae per node
+ self.hyphae_length_factor = 0.4 # Length of hyphae relative to node radius
+ self.hyphae_variation = 0.3 # Random variation in hyphae
+
+ # Node colors - use global color palette with mycelial theme
+ self.node_colors_by_type = {
+ 'main': '#8E9DCC', # Soft blue-purple
+ 'rabbithole': '#7FB069', # Soft green
+ 'fork': '#F2C14E', # Soft yellow
+ 'branch': '#F78154' # Soft orange
+ }
+
+ # Collision dynamics
+ self.node_velocities = {} # Store velocities for each node
+ self.repulsion_strength = 0.5 # Strength of repulsion between nodes
+ self.attraction_strength = 0.1 # Strength of attraction along edges
+ self.damping = 0.8 # Damping factor to prevent oscillation
+ self.apply_physics = True # Toggle for physics simulation
+
+ # Set up the widget
+ self.setMinimumSize(300, 300)
+ self.setMouseTracking(True)
+
+ def add_edge(self, source, target):
+ """Add an edge with growth animation"""
+ if (source, target) not in self.edges:
+ self.edges.append((source, target))
+ # Initialize edge growth at 0
+ self.growing_edges[(source, target)] = 0.0
+ # Force update to start animation immediately
+ self.update()
+
+ def update_animation(self):
+ """Update animation state"""
+ self.animation_progress = (self.animation_progress + 0.05) % 1.0
+
+ # Update growing edges
+ edges_to_remove = []
+ has_growing_edges = False
+
+ for edge, progress in self.growing_edges.items():
+ if progress < 1.0:
+ self.growing_edges[edge] = min(progress + self.edge_growth_speed, 1.0)
+ has_growing_edges = True
+ else:
+ # Mark fully grown edges for removal from animation tracking
+ edges_to_remove.append(edge)
+
+ # Remove fully grown edges from tracking
+ for edge in edges_to_remove:
+ if edge in self.growing_edges:
+ self.growing_edges.pop(edge)
+
+ # Apply collision dynamics if enabled
+ if self.apply_physics and len(self.nodes) > 1:
+ self.apply_collision_dynamics()
+
+ # Update the widget
+ self.update()
+
+ def apply_collision_dynamics(self):
+ """Apply collision dynamics to prevent node overlap"""
+ # Initialize velocities if needed
+ for node_id in self.nodes:
+ if node_id not in self.node_velocities:
+ self.node_velocities[node_id] = (0, 0)
+
+ # Calculate repulsive forces between nodes
+ new_velocities = {}
+ for node_id in self.nodes:
+ if node_id not in self.node_positions:
+ continue
+
+ vx, vy = self.node_velocities.get(node_id, (0, 0))
+ x1, y1 = self.node_positions[node_id]
+
+ # Apply repulsion between nodes
+ for other_id in self.nodes:
+ if other_id == node_id or other_id not in self.node_positions:
+ continue
+
+ x2, y2 = self.node_positions[other_id]
+
+ # Calculate distance
+ dx = x1 - x2
+ dy = y1 - y2
+ distance = max(0.1, math.sqrt(dx*dx + dy*dy)) # Avoid division by zero
+
+ # Get node sizes
+ size1 = math.sqrt(self.node_sizes.get(node_id, 400))
+ size2 = math.sqrt(self.node_sizes.get(other_id, 400))
+ min_distance = (size1 + size2) / 2
+
+ # Apply repulsive force if nodes are too close
+ if distance < min_distance * 2:
+ # Normalize direction vector
+ nx = dx / distance
+ ny = dy / distance
+
+ # Calculate repulsion strength (stronger when closer)
+ strength = self.repulsion_strength * (1.0 - distance / (min_distance * 2))
+
+ # Apply force
+ vx += nx * strength
+ vy += ny * strength
+
+ # Apply attraction along edges
+ for edge in self.edges:
+ source, target = edge
+
+ # Skip edges that are still growing
+ if (source, target) in self.growing_edges and self.growing_edges[(source, target)] < 1.0:
+ continue
+
+ if source == node_id and target in self.node_positions:
+ # This node is the source, attract towards target
+ x2, y2 = self.node_positions[target]
+ dx = x2 - x1
+ dy = y2 - y1
+ distance = max(0.1, math.sqrt(dx*dx + dy*dy))
+
+ # Normalize and apply attraction
+ vx += (dx / distance) * self.attraction_strength
+ vy += (dy / distance) * self.attraction_strength
+
+ elif target == node_id and source in self.node_positions:
+ # This node is the target, attract towards source
+ x2, y2 = self.node_positions[source]
+ dx = x2 - x1
+ dy = y2 - y1
+ distance = max(0.1, math.sqrt(dx*dx + dy*dy))
+
+ # Normalize and apply attraction
+ vx += (dx / distance) * self.attraction_strength
+ vy += (dy / distance) * self.attraction_strength
+
+ # Apply damping to prevent oscillation
+ vx *= self.damping
+ vy *= self.damping
+
+ # Store new velocity
+ new_velocities[node_id] = (vx, vy)
+
+ # Update positions based on velocities
+ for node_id, (vx, vy) in new_velocities.items():
+ if node_id in self.node_positions:
+ # Skip the main node to keep it centered
+ if node_id == 'main':
+ continue
+
+ x, y = self.node_positions[node_id]
+ self.node_positions[node_id] = (x + vx, y + vy)
+
+ # Update velocities for next frame
+ self.node_velocities = new_velocities
+
+ def paintEvent(self, event):
+ """Paint the network graph"""
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+
+ # Get widget dimensions
+ width = self.width()
+ height = self.height()
+
+ # Set background with subtle gradient
+ gradient = QLinearGradient(0, 0, 0, height)
+ gradient.setColorAt(0, QColor('#1A1A1E')) # Dark blue-gray
+ gradient.setColorAt(1, QColor('#0F0F12')) # Darker at bottom
+ painter.fillRect(0, 0, width, height, gradient)
+
+ # Draw subtle grid lines
+ painter.setPen(QPen(QColor(COLORS['border']).darker(150), 0.5, Qt.PenStyle.DotLine))
+ grid_size = 40
+ for x in range(0, width, grid_size):
+ painter.drawLine(x, 0, x, height)
+ for y in range(0, height, grid_size):
+ painter.drawLine(0, y, width, y)
+
+ # Calculate center point and scale factor
+ center_x = width / 2
+ center_y = height / 2
+ scale = min(width, height) / 500
+
+ # Draw edges first so they appear behind nodes
+ for edge in self.edges:
+ source, target = edge
+ if source in self.node_positions and target in self.node_positions:
+ src_x, src_y = self.node_positions[source]
+ dst_x, dst_y = self.node_positions[target]
+
+ # Transform coordinates to screen space
+ screen_src_x = center_x + src_x * scale
+ screen_src_y = center_y + src_y * scale
+ screen_dst_x = center_x + dst_x * scale
+ screen_dst_y = center_y + dst_y * scale
+
+ # Get growth progress for this edge (default to 1.0 if not growing)
+ growth_progress = self.growing_edges.get((source, target), 1.0)
+
+ # Calculate the actual destination based on growth progress
+ if growth_progress < 1.0:
+ # Interpolate between source and destination
+ actual_dst_x = screen_src_x + (screen_dst_x - screen_src_x) * growth_progress
+ actual_dst_y = screen_src_y + (screen_dst_y - screen_src_y) * growth_progress
+ else:
+ actual_dst_x = screen_dst_x
+ actual_dst_y = screen_dst_y
+
+ # Draw mycelial connection (multiple thin lines with variations)
+ source_color = QColor(self.node_colors.get(source, self.node_colors_by_type['main']))
+ target_color = QColor(self.node_colors.get(target, self.node_colors_by_type['main']))
+
+ # Number of filaments per connection
+ num_filaments = 3
+
+ for i in range(num_filaments):
+ # Create a path with multiple segments for organic look
+ path = QPainterPath()
+ path.moveTo(screen_src_x, screen_src_y)
+
+ # Calculate distance between points
+ distance = math.sqrt((actual_dst_x - screen_src_x)**2 + (actual_dst_y - screen_src_y)**2)
+
+ # Number of segments increases with distance
+ num_segments = max(3, int(distance / 40))
+
+ # Create intermediate points with slight random variations
+ prev_x, prev_y = screen_src_x, screen_src_y
+
+ for j in range(1, num_segments):
+ # Calculate position along the line
+ ratio = j / num_segments
+
+ # Base position
+ base_x = screen_src_x + (actual_dst_x - screen_src_x) * ratio
+ base_y = screen_src_y + (actual_dst_y - screen_src_y) * ratio
+
+ # Add random variation perpendicular to the line
+ angle = math.atan2(actual_dst_y - screen_src_y, actual_dst_x - screen_src_x) + math.pi/2
+ variation = (random.random() - 0.5) * 10 * scale
+
+ # Variation decreases near endpoints
+ endpoint_factor = min(ratio, 1 - ratio) * 4 # Maximum at middle
+ variation *= endpoint_factor
+
+ # Apply variation
+ point_x = base_x + variation * math.cos(angle)
+ point_y = base_y + variation * math.sin(angle)
+
+ # Add point to path
+ path.lineTo(point_x, point_y)
+ prev_x, prev_y = point_x, point_y
+
+ # Complete the path to destination
+ path.lineTo(actual_dst_x, actual_dst_y)
+
+ # Create gradient along the path
+ gradient = QLinearGradient(screen_src_x, screen_src_y, actual_dst_x, actual_dst_y)
+
+ # Make colors more transparent for mycelial effect
+ source_color_trans = QColor(source_color)
+ target_color_trans = QColor(target_color)
+
+ # Vary transparency by filament
+ alpha = 70 + i * 20
+ source_color_trans.setAlpha(alpha)
+ target_color_trans.setAlpha(alpha)
+
+ gradient.setColorAt(0, source_color_trans)
+ gradient.setColorAt(1, target_color_trans)
+
+ # Animate flow along edge
+ flow_pos = (self.animation_progress + i * 0.3) % 1.0
+ flow_color = QColor(255, 255, 255, 100)
+ gradient.setColorAt(flow_pos, flow_color)
+
+ # Draw the edge with varying thickness
+ thickness = 1.0 + (i * 0.5)
+ pen = QPen(QBrush(gradient), thickness)
+ pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ painter.setPen(pen)
+ painter.drawPath(path)
+
+ # Draw small nodes along the path for mycelial effect
+ if growth_progress == 1.0: # Only for fully grown edges
+ num_nodes = int(distance / 50)
+ for j in range(1, num_nodes):
+ ratio = j / num_nodes
+ node_x = screen_src_x + (screen_dst_x - screen_src_x) * ratio
+ node_y = screen_src_y + (screen_dst_y - screen_src_y) * ratio
+
+ # Add small random offset
+ offset_angle = random.random() * math.pi * 2
+ offset_dist = random.random() * 5
+ node_x += math.cos(offset_angle) * offset_dist
+ node_y += math.sin(offset_angle) * offset_dist
+
+ # Draw small node
+ node_color = QColor(source_color)
+ node_color.setAlpha(100)
+ painter.setPen(Qt.PenStyle.NoPen)
+ painter.setBrush(QBrush(node_color))
+ node_size = 1 + random.random() * 2
+ painter.drawEllipse(QPointF(node_x, node_y), node_size, node_size)
+
+ # Draw nodes
+ for node_id in self.nodes:
+ if node_id in self.node_positions:
+ x, y = self.node_positions[node_id]
+
+ # Transform coordinates to screen space
+ screen_x = center_x + x * scale
+ screen_y = center_y + y * scale
+
+ # Get node properties
+ node_color = self.node_colors.get(node_id, self.node_colors_by_type['branch'])
+ node_label = self.node_labels.get(node_id, 'Node')
+ node_size = self.node_sizes.get(node_id, 400)
+
+ # Scale the node size
+ radius = math.sqrt(node_size) * scale / 2
+
+ # Adjust radius for hover/selection
+ if node_id == self.selected_node:
+ radius *= 1.1 # Larger when selected
+ elif node_id == self.hovered_node:
+ radius *= 1.05 # Slightly larger when hovered
+
+ # Draw node glow for selected/hovered nodes
+ if node_id == self.selected_node or node_id == self.hovered_node:
+ glow_radius = radius * 1.5
+ glow_color = QColor(node_color)
+
+ for i in range(5):
+ r = glow_radius - (i * radius * 0.1)
+ alpha = 40 - (i * 8)
+ glow_color.setAlpha(alpha)
+ painter.setPen(Qt.PenStyle.NoPen)
+ painter.setBrush(glow_color)
+ painter.drawEllipse(QPointF(screen_x, screen_y), r, r)
+
+ # Draw mycelial node (irregular shape with hyphae)
+ painter.setPen(Qt.PenStyle.NoPen)
+
+ # Create gradient fill for node
+ gradient = QRadialGradient(screen_x, screen_y, radius)
+ base_color = QColor(node_color)
+ lighter_color = QColor(node_color).lighter(130)
+ darker_color = QColor(node_color).darker(130)
+
+ gradient.setColorAt(0, lighter_color)
+ gradient.setColorAt(0.7, base_color)
+ gradient.setColorAt(1, darker_color)
+
+ # Fill main node body
+ painter.setBrush(QBrush(gradient))
+
+ # Draw irregular node shape
+ path = QPainterPath()
+
+ # Create irregular circle with random variations
+ num_points = 20
+ start_angle = random.random() * math.pi * 2
+
+ for i in range(num_points + 1):
+ angle = start_angle + (i * 2 * math.pi / num_points)
+ # Vary radius slightly for organic look
+ variation = 1.0 + (random.random() - 0.5) * 0.2
+ point_radius = radius * variation
+
+ x_point = screen_x + math.cos(angle) * point_radius
+ y_point = screen_y + math.sin(angle) * point_radius
+
+ if i == 0:
+ path.moveTo(x_point, y_point)
+ else:
+ # Use quadratic curves for smoother shape
+ control_angle = start_angle + ((i - 0.5) * 2 * math.pi / num_points)
+ control_radius = radius * (1.0 + (random.random() - 0.5) * 0.1)
+ control_x = screen_x + math.cos(control_angle) * control_radius
+ control_y = screen_y + math.sin(control_angle) * control_radius
+
+ path.quadTo(control_x, control_y, x_point, y_point)
+
+ # Draw the main node body
+ painter.drawPath(path)
+
+ # Draw hyphae (mycelial extensions)
+ hyphae_count = self.hyphae_count
+ if node_id == 'main':
+ hyphae_count += 3 # More hyphae for main node
+
+ for i in range(hyphae_count):
+ # Random angle for hyphae
+ angle = random.random() * math.pi * 2
+
+ # Base length varies by node type
+ base_length = radius * self.hyphae_length_factor
+ if node_id == 'main':
+ base_length *= 1.5
+
+ # Random variation in length
+ length = base_length * (1.0 + (random.random() - 0.5) * self.hyphae_variation)
+
+ # Calculate end point
+ end_x = screen_x + math.cos(angle) * (radius + length)
+ end_y = screen_y + math.sin(angle) * (radius + length)
+
+ # Start point is on the node perimeter
+ start_x = screen_x + math.cos(angle) * radius * 0.9
+ start_y = screen_y + math.sin(angle) * radius * 0.9
+
+ # Create hyphae path with slight curve
+ hypha_path = QPainterPath()
+ hypha_path.moveTo(start_x, start_y)
+
+ # Control point for curve
+ ctrl_angle = angle + (random.random() - 0.5) * 0.5 # Slight angle variation
+ ctrl_dist = radius + length * 0.5
+ ctrl_x = screen_x + math.cos(ctrl_angle) * ctrl_dist
+ ctrl_y = screen_y + math.sin(ctrl_angle) * ctrl_dist
+
+ hypha_path.quadTo(ctrl_x, ctrl_y, end_x, end_y)
+
+ # Draw hypha with gradient
+ hypha_gradient = QLinearGradient(start_x, start_y, end_x, end_y)
+
+ # Hypha color starts as node color and fades out
+ hypha_start_color = QColor(node_color)
+ hypha_end_color = QColor(node_color)
+ hypha_start_color.setAlpha(150)
+ hypha_end_color.setAlpha(30)
+
+ hypha_gradient.setColorAt(0, hypha_start_color)
+ hypha_gradient.setColorAt(1, hypha_end_color)
+
+ # Draw hypha with varying thickness
+ thickness = 1.0 + random.random() * 1.5
+ hypha_pen = QPen(QBrush(hypha_gradient), thickness)
+ hypha_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
+ painter.setPen(hypha_pen)
+ painter.drawPath(hypha_path)
+
+ # Add small nodes at the end of some hyphae
+ if random.random() > 0.5:
+ small_node_color = QColor(node_color)
+ small_node_color.setAlpha(100)
+ painter.setPen(Qt.PenStyle.NoPen)
+ painter.setBrush(QBrush(small_node_color))
+ small_node_size = 1 + random.random() * 2
+ painter.drawEllipse(QPointF(end_x, end_y), small_node_size, small_node_size)
+
+ def mousePressEvent(self, event):
+ """Handle mouse press events"""
+ if event.button() == Qt.MouseButton.LeftButton:
+ pos = event.position()
+ clicked_node = self.get_node_at_position(pos)
+ if clicked_node:
+ self.selected_node = clicked_node
+ self.update()
+ self.nodeSelected.emit(clicked_node)
+
+ def mouseMoveEvent(self, event):
+ """Handle mouse move events for hover effects"""
+ pos = event.position()
+ hovered_node = self.get_node_at_position(pos)
+
+ if hovered_node != self.hovered_node:
+ self.hovered_node = hovered_node
+ self.update()
+ if hovered_node:
+ self.nodeHovered.emit(hovered_node)
+
+ # Show tooltip with node info
+ if hovered_node in self.node_labels:
+ # Get node type from the ID
+ node_type = "main"
+ if "rabbithole_" in hovered_node:
+ node_type = "rabbithole"
+ elif "fork_" in hovered_node:
+ node_type = "fork"
+
+ # Set emoji based on node type
+ emoji = "🌱" # Default/main
+ if node_type == "rabbithole":
+ emoji = "🕳️" # Rabbithole emoji
+ elif node_type == "fork":
+ emoji = "🔱" # Fork emoji
+
+ # Show tooltip with emoji and label
+ QToolTip.showText(
+ event.globalPosition().toPoint(),
+ f"{emoji} {self.node_labels[hovered_node]}",
+ self
+ )
+
+ def get_node_at_position(self, pos):
+ """Get the node at the given position"""
+ width = self.width()
+ height = self.height()
+ center_x = width / 2
+ center_y = height / 2
+ scale = min(width, height) / 500
+
+ for node_id in self.nodes:
+ if node_id in self.node_positions:
+ x, y = self.node_positions[node_id]
+ screen_x = center_x + x * scale
+ screen_y = center_y + y * scale
+
+ node_size = self.node_sizes.get(node_id, 400)
+ radius = math.sqrt(node_size) * scale / 2
+
+ distance = math.sqrt((pos.x() - screen_x)**2 + (pos.y() - screen_y)**2)
+ if distance <= radius:
+ return node_id
+
+ return None
diff --git a/src/utils/__init__.py b/src/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/utils/font_loader.py b/src/utils/font_loader.py
new file mode 100644
index 0000000..f665d76
--- /dev/null
+++ b/src/utils/font_loader.py
@@ -0,0 +1,46 @@
+from PyQt6.QtGui import QFontDatabase
+from pathlib import Path
+import os
+
+def load_fonts():
+ """Load custom fonts for the application"""
+ # Look for fonts in the root 'fonts' directory
+ # If running from run.py, root is current directory
+ # If running from src/main.py, root is one level up
+
+ font_dir = Path("fonts")
+ if not font_dir.exists():
+ # Try going up one level if we are inside src
+ font_dir = Path("../fonts")
+
+ if not font_dir.exists():
+ # Try absolute path from file location
+ current_file = Path(__file__).resolve()
+ project_root = current_file.parent.parent.parent
+ font_dir = project_root / "fonts"
+
+ # List of fonts to load
+ fonts = [
+ ("IosevkaTerm-Regular.ttf", "Iosevka Term"),
+ ("IosevkaTerm-Bold.ttf", "Iosevka Term"),
+ ("IosevkaTerm-Italic.ttf", "Iosevka Term"),
+ ]
+
+ loaded_fonts = []
+
+ if font_dir.exists():
+ for font_file, font_name in fonts:
+ font_path = font_dir / font_file
+ if font_path.exists():
+ font_id = QFontDatabase.addApplicationFont(str(font_path))
+ if font_id >= 0:
+ if font_name not in loaded_fonts:
+ loaded_fonts.append(font_name)
+ print(f"Loaded font: {font_name} from {font_file}")
+ else:
+ print(f"Failed to load font: {font_file}")
+ else:
+ pass
+ # Silent fail for missing font files, fallback will be used
+
+ return loaded_fonts
diff --git a/src/utils/shared_utils.py b/src/utils/shared_utils.py
new file mode 100644
index 0000000..e2565f5
--- /dev/null
+++ b/src/utils/shared_utils.py
@@ -0,0 +1,154 @@
+import json
+import webbrowser
+import os
+from pathlib import Path
+from datetime import datetime
+
+def setup_image_directory():
+ """Create an 'images' directory in the project root if it doesn't exist"""
+ image_dir = Path("images")
+ image_dir.mkdir(exist_ok=True)
+ return image_dir
+
+def cleanup_old_images(image_dir, max_age_hours=24):
+ """Remove images older than max_age_hours"""
+ current_time = datetime.now()
+ for image_file in image_dir.glob("*.jpg"):
+ file_age = datetime.fromtimestamp(image_file.stat().st_mtime)
+ if (current_time - file_age).total_seconds() > max_age_hours * 3600:
+ image_file.unlink()
+
+def load_ai_memory(ai_number):
+ """Load AI conversation memory from JSON files"""
+ try:
+ memory_path = f"memory/ai{ai_number}/conversations.json"
+ with open(memory_path, 'r', encoding='utf-8') as f:
+ conversations = json.load(f)
+ # Ensure we're working with the array part
+ if isinstance(conversations, dict) and "memories" in conversations:
+ conversations = conversations["memories"]
+ return conversations
+ except Exception as e:
+ print(f"Error loading AI{ai_number} memory: {e}")
+ return []
+
+def create_memory_prompt(conversations):
+ """Convert memory JSON into conversation examples"""
+ if not conversations:
+ return ""
+
+ prompt = "Previous conversations that demonstrate your personality:\n\n"
+
+ # Add example conversations
+ for convo in conversations:
+ prompt += f"Human: {convo['human']}\n"
+ prompt += f"Assistant: {convo['assistant']}\n\n"
+
+ prompt += "Maintain this conversation style in your responses."
+ return prompt
+
+def print_conversation_state(conversation):
+ print("Current conversation state:")
+ for message in conversation:
+ content = message.get('content', '')
+ # Safely preview content - handle both string and list (structured) content
+ if isinstance(content, str):
+ preview = content[:50] + "..." if len(content) > 50 else content
+ else:
+ preview = f"[structured content with {len(content)} parts]"
+ print(f"{message['role']}: {preview}")
+
+def open_html_in_browser(file_path="conversation_full.html"):
+ import webbrowser, os
+ full_path = os.path.abspath(file_path)
+ webbrowser.open('file://' + full_path)
+
+def create_initial_living_document(*args, **kwargs):
+ return ""
+
+def read_living_document(*args, **kwargs):
+ return ""
+
+def process_living_document_edits(result, model_name):
+ return result
+
+def read_shared_html(*args, **kwargs):
+ return ""
+
+def update_shared_html(*args, **kwargs):
+ return False
+
+def web_search(query: str, max_results: int = 5) -> dict:
+ """
+ Search the web using DuckDuckGo.
+
+ Args:
+ query: Search query string
+ max_results: Maximum number of results to return
+
+ Returns:
+ dict with keys: success, results (list of {title, url, snippet}), error
+ """
+ try:
+ from ddgs import DDGS
+ except ImportError:
+ return {
+ "success": False,
+ "error": "ddgs package not installed. Run: pip install ddgs"
+ }
+
+ try:
+ print(f"[WebSearch] Searching for: {query}")
+
+ # Use the new ddgs API - prioritize news for current events queries
+ ddgs = DDGS()
+ formatted_results = []
+
+ # For queries about current events, use news search first
+ is_news_query = any(term in query.lower() for term in ["news", "today", "latest", "2025", "drama", "announcement", "release"])
+
+ if is_news_query:
+ print(f"[WebSearch] Detected news query, searching news first...")
+ try:
+ news_results = list(ddgs.news(query, region="wt-wt", safesearch="off", max_results=max_results))
+ for r in news_results:
+ formatted_results.append({
+ "title": r.get("title", ""),
+ "url": r.get("url", r.get("link", "")),
+ "snippet": r.get("body", r.get("excerpt", ""))
+ })
+ print(f"[WebSearch] Found {len(formatted_results)} news results")
+ except Exception as e:
+ print(f"[WebSearch] News search failed: {e}")
+
+ # If we don't have enough results, try text search
+ if len(formatted_results) < max_results:
+ remaining = max_results - len(formatted_results)
+ try:
+ text_results = list(ddgs.text(
+ query,
+ region="us-en", # Force US English results
+ safesearch="off",
+ max_results=remaining
+ ))
+ for r in text_results:
+ formatted_results.append({
+ "title": r.get("title", ""),
+ "url": r.get("href", r.get("link", "")),
+ "snippet": r.get("body", r.get("snippet", ""))
+ })
+ print(f"[WebSearch] Added {len(text_results)} text results, total: {len(formatted_results)}")
+ except Exception as e:
+ print(f"[WebSearch] Text search failed: {e}")
+
+ return {
+ "success": True,
+ "results": formatted_results,
+ "query": query
+ }
+ except Exception as e:
+ print(f"[WebSearch] Error: {e}")
+ return {
+ "success": False,
+ "error": str(e)
+ }