Refactor middle-click reader to read-aloud service with Alt+R hotkey and Piper TTS
- Rename middle-click-reader to read-aloud service - Change hotkey from Ctrl+middle-click to Alt+R - Replace edge-tts with Piper TTS for local neural voices - Update desktop and service files - Add piper-tts dependency - Update tests and setup scripts
This commit is contained in:
parent
71c305a201
commit
cca6bd2aee
@ -11,6 +11,7 @@ dependencies = [
|
||||
"vosk>=0.3.45",
|
||||
"numpy>=2.3.5",
|
||||
"edge-tts>=7.2.3",
|
||||
"piper-tts>=1.3.0",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Middle-Click Read-Aloud
|
||||
Comment=Read highlighted text aloud with middle-click
|
||||
Exec=/mnt/storage/Development/dictation-service/.venv/bin/python /mnt/storage/Development/dictation-service/src/dictation_service/middle_click_reader.py
|
||||
Name=Read-Aloud Service (Alt+R)
|
||||
Comment=Read highlighted text aloud with Alt+R
|
||||
Exec=/mnt/storage/Development/dictation-service/.venv/bin/python /mnt/storage/Development/dictation-service/src/dictation_service/read_aloud.py
|
||||
Path=/mnt/storage/Development/dictation-service
|
||||
Terminal=false
|
||||
Hidden=false
|
||||
@ -1,11 +1,11 @@
|
||||
[Unit]
|
||||
Description=Middle-Click Read-Aloud Service
|
||||
Description=Read-Aloud Service (Alt+R)
|
||||
After=graphical-session.target
|
||||
PartOf=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/mnt/storage/Development/dictation-service/.venv/bin/python /mnt/storage/Development/dictation-service/src/dictation_service/middle_click_reader.py
|
||||
ExecStart=/mnt/storage/Development/dictation-service/.venv/bin/python /mnt/storage/Development/dictation-service/src/dictation_service/read_aloud.py
|
||||
WorkingDirectory=/mnt/storage/Development/dictation-service
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
@ -1,27 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Setup script for middle-click read-aloud service
|
||||
|
||||
set -e
|
||||
|
||||
echo "Setting up middle-click read-aloud service..."
|
||||
|
||||
# Create autostart directory
|
||||
mkdir -p "$HOME/.config/autostart"
|
||||
|
||||
# Copy desktop file to autostart
|
||||
cp middle-click-reader.desktop "$HOME/.config/autostart/"
|
||||
|
||||
echo "✓ Middle-click read-aloud installed to autostart"
|
||||
echo ""
|
||||
echo "To start now (without rebooting), run:"
|
||||
echo " uv run python src/dictation_service/middle_click_reader.py &"
|
||||
echo ""
|
||||
echo "Or reboot to start automatically."
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " 1. Highlight any text"
|
||||
echo " 2. Middle-click (press scroll wheel) to read it aloud"
|
||||
echo ""
|
||||
echo "To disable auto-start:"
|
||||
echo " rm ~/.config/autostart/middle-click-reader.desktop"
|
||||
echo ""
|
||||
28
scripts/setup-read-aloud.sh
Executable file
28
scripts/setup-read-aloud.sh
Executable file
@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# Setup script for read-aloud service (Alt+R)
|
||||
|
||||
set -e
|
||||
|
||||
echo "Setting up read-aloud service (Alt+R)..."
|
||||
|
||||
# Install systemd service
|
||||
mkdir -p "$HOME/.config/systemd/user"
|
||||
cp read-aloud.service "$HOME/.config/systemd/user/"
|
||||
|
||||
# Reload systemd and enable service
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable read-aloud.service
|
||||
systemctl --user start read-aloud.service
|
||||
|
||||
echo "✓ Read-aloud service installed and started"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " 1. Highlight any text"
|
||||
echo " 2. Press Alt+R to read it aloud"
|
||||
echo ""
|
||||
echo "Service management:"
|
||||
echo " systemctl --user status read-aloud.service # Check status"
|
||||
echo " systemctl --user restart read-aloud.service # Restart"
|
||||
echo " systemctl --user stop read-aloud.service # Stop"
|
||||
echo " systemctl --user disable read-aloud.service # Disable autostart"
|
||||
echo ""
|
||||
@ -1,190 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Middle-click Read-Aloud Service
|
||||
Monitors for middle-click events and reads highlighted text using edge-tts
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import logging
|
||||
import tempfile
|
||||
from pynput import mouse
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
filename=os.path.expanduser("~/.cache/middle_click_reader.log"),
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Configuration
|
||||
EDGE_TTS_VOICE = "en-US-ChristopherNeural"
|
||||
LOCK_FILE = "/tmp/dictation_speaking.lock"
|
||||
MIN_TEXT_LENGTH = 2 # Minimum characters to read
|
||||
|
||||
|
||||
class MiddleClickReader:
|
||||
"""Monitors for middle-click and reads selected text"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_reading = False
|
||||
self.last_text = ""
|
||||
self.ctrl_pressed = False
|
||||
logging.info("Middle-click reader initialized (use Ctrl+Middle-Click)")
|
||||
|
||||
def get_selected_text(self):
|
||||
"""Get currently highlighted text from X11 PRIMARY selection"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["xclip", "-o", "-selection", "primary"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=1
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting selection: {e}")
|
||||
return ""
|
||||
|
||||
def read_text(self, text):
|
||||
"""Read text using edge-tts"""
|
||||
if not text or len(text) < MIN_TEXT_LENGTH:
|
||||
logging.debug(f"Text too short to read: '{text}'")
|
||||
return
|
||||
|
||||
if self.is_reading:
|
||||
logging.debug("Already reading, skipping")
|
||||
return
|
||||
|
||||
self.is_reading = True
|
||||
logging.info(f"Reading text: {text[:50]}...")
|
||||
|
||||
try:
|
||||
# Create lock file to prevent feedback
|
||||
with open(LOCK_FILE, 'w') as f:
|
||||
f.write("middle_click_reader")
|
||||
|
||||
# Create temporary file for audio
|
||||
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tmp_file:
|
||||
audio_file = tmp_file.name
|
||||
|
||||
try:
|
||||
# Generate speech with edge-tts
|
||||
subprocess.run(
|
||||
[
|
||||
"edge-tts",
|
||||
"--voice", EDGE_TTS_VOICE,
|
||||
"--text", text,
|
||||
"--write-media", audio_file
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# Play audio with mpv
|
||||
subprocess.run(
|
||||
["mpv", "--no-video", "--really-quiet", audio_file],
|
||||
capture_output=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
logging.info("Text read successfully")
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
if os.path.exists(audio_file):
|
||||
os.remove(audio_file)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logging.error("TTS or playback timed out")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"TTS command failed: {e}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading text: {e}")
|
||||
finally:
|
||||
# Remove lock file
|
||||
if os.path.exists(LOCK_FILE):
|
||||
try:
|
||||
os.remove(LOCK_FILE)
|
||||
except Exception as e:
|
||||
logging.error(f"Error removing lock file: {e}")
|
||||
self.is_reading = False
|
||||
|
||||
def on_key_press(self, key):
|
||||
"""Track Ctrl key state"""
|
||||
try:
|
||||
from pynput.keyboard import Key
|
||||
if key in [Key.ctrl_l, Key.ctrl_r, Key.ctrl]:
|
||||
self.ctrl_pressed = True
|
||||
except:
|
||||
pass
|
||||
|
||||
def on_key_release(self, key):
|
||||
"""Track Ctrl key state"""
|
||||
try:
|
||||
from pynput.keyboard import Key
|
||||
if key in [Key.ctrl_l, Key.ctrl_r, Key.ctrl]:
|
||||
self.ctrl_pressed = False
|
||||
except:
|
||||
pass
|
||||
|
||||
def on_click(self, x, y, button, pressed):
|
||||
"""Handle mouse click events"""
|
||||
# Only respond to Ctrl+middle-click press
|
||||
if button == mouse.Button.middle and pressed and self.ctrl_pressed:
|
||||
logging.debug(f"Ctrl+Middle-click detected at ({x}, {y})")
|
||||
|
||||
# Get selected text
|
||||
text = self.get_selected_text()
|
||||
|
||||
if text and text != self.last_text:
|
||||
self.last_text = text
|
||||
# Read in a separate thread to avoid blocking
|
||||
import threading
|
||||
read_thread = threading.Thread(
|
||||
target=self.read_text,
|
||||
args=(text,),
|
||||
daemon=True
|
||||
)
|
||||
read_thread.start()
|
||||
elif not text:
|
||||
logging.debug("No text selected")
|
||||
|
||||
def run(self):
|
||||
"""Start the listeners"""
|
||||
logging.info("Starting Ctrl+middle-click listener...")
|
||||
print("Middle-click reader running. Hold Ctrl and middle-click on selected text to read it.")
|
||||
print("Press Ctrl+C to quit.")
|
||||
|
||||
from pynput import keyboard
|
||||
|
||||
# Start keyboard listener to track Ctrl state
|
||||
keyboard_listener = keyboard.Listener(
|
||||
on_press=self.on_key_press,
|
||||
on_release=self.on_key_release
|
||||
)
|
||||
keyboard_listener.start()
|
||||
|
||||
# Start mouse listener
|
||||
with mouse.Listener(on_click=self.on_click) as listener:
|
||||
listener.join()
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
reader = MiddleClickReader()
|
||||
reader.run()
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Shutting down...")
|
||||
print("\nShutting down...")
|
||||
except Exception as e:
|
||||
logging.error(f"Fatal error: {e}")
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
189
src/dictation_service/read_aloud.py
Executable file
189
src/dictation_service/read_aloud.py
Executable file
@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Read-Aloud Service (Alt+R)
|
||||
Monitors for Alt+R hotkey and reads highlighted text using Piper TTS (local neural voices)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import logging
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from pynput import keyboard
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
filename=os.path.expanduser("~/.cache/read_aloud.log"),
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Configuration
|
||||
LOCK_FILE = "/tmp/dictation_speaking.lock"
|
||||
MIN_TEXT_LENGTH = 2 # Minimum characters to read
|
||||
|
||||
# Piper configuration
|
||||
SCRIPT_DIR = Path(__file__).parent.parent.parent
|
||||
PIPER_PATH = SCRIPT_DIR / ".venv" / "bin" / "piper"
|
||||
VOICE_MODEL = Path.home() / ".shared" / "models" / "piper" / "en_US-lessac-medium.onnx"
|
||||
|
||||
|
||||
class MiddleClickReader:
|
||||
"""Monitors for Alt+R hotkey and reads selected text"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_reading = False
|
||||
self.last_text = ""
|
||||
self.alt_pressed = False
|
||||
logging.info("Read-aloud service initialized (use Alt+R)")
|
||||
|
||||
def get_selected_text(self):
|
||||
"""Get currently highlighted text from X11 PRIMARY selection"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["xclip", "-o", "-selection", "primary"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=1
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting selection: {e}")
|
||||
return ""
|
||||
|
||||
def read_text(self, text):
|
||||
"""Read text using Piper TTS (local neural voices)"""
|
||||
if not text or len(text) < MIN_TEXT_LENGTH:
|
||||
logging.debug(f"Text too short to read: '{text}'")
|
||||
return
|
||||
|
||||
if self.is_reading:
|
||||
logging.debug("Already reading, skipping")
|
||||
return
|
||||
|
||||
self.is_reading = True
|
||||
logging.info(f"Reading text: {text[:50]}...")
|
||||
|
||||
try:
|
||||
# Create lock file to prevent feedback
|
||||
with open(LOCK_FILE, 'w') as f:
|
||||
f.write("read_aloud")
|
||||
|
||||
# Create temporary WAV file for audio
|
||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_file:
|
||||
audio_file = tmp_file.name
|
||||
|
||||
try:
|
||||
# Generate speech with Piper
|
||||
piper_process = subprocess.Popen(
|
||||
[
|
||||
str(PIPER_PATH),
|
||||
"--model", str(VOICE_MODEL),
|
||||
"--output_file", audio_file
|
||||
],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Send text to Piper via stdin
|
||||
piper_process.communicate(input=text, timeout=10)
|
||||
|
||||
if piper_process.returncode == 0:
|
||||
# Play audio with mpv (or aplay/paplay as fallback)
|
||||
subprocess.run(
|
||||
["mpv", "--no-video", "--really-quiet", audio_file],
|
||||
capture_output=True,
|
||||
timeout=60
|
||||
)
|
||||
logging.info("Text read successfully")
|
||||
else:
|
||||
logging.error(f"Piper TTS failed with code {piper_process.returncode}")
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
if os.path.exists(audio_file):
|
||||
os.remove(audio_file)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logging.error("TTS timed out")
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading text: {e}")
|
||||
finally:
|
||||
# Remove lock file
|
||||
if os.path.exists(LOCK_FILE):
|
||||
try:
|
||||
os.remove(LOCK_FILE)
|
||||
except Exception as e:
|
||||
logging.error(f"Error removing lock file: {e}")
|
||||
self.is_reading = False
|
||||
|
||||
def on_key_press(self, key):
|
||||
"""Track Alt key and trigger on Alt+R"""
|
||||
try:
|
||||
# Track Alt key
|
||||
if key in [keyboard.Key.alt_l, keyboard.Key.alt_r, keyboard.Key.alt]:
|
||||
self.alt_pressed = True
|
||||
|
||||
# Trigger on Alt+R
|
||||
if self.alt_pressed and hasattr(key, 'char') and key.char == 'r':
|
||||
logging.debug("Alt+R detected")
|
||||
|
||||
# Get selected text
|
||||
text = self.get_selected_text()
|
||||
|
||||
if text and text != self.last_text:
|
||||
self.last_text = text
|
||||
# Read in a separate thread to avoid blocking
|
||||
import threading
|
||||
read_thread = threading.Thread(
|
||||
target=self.read_text,
|
||||
args=(text,),
|
||||
daemon=True
|
||||
)
|
||||
read_thread.start()
|
||||
elif not text:
|
||||
logging.debug("No text selected")
|
||||
except Exception as e:
|
||||
logging.error(f"Error in key press handler: {e}")
|
||||
|
||||
def on_key_release(self, key):
|
||||
"""Track Alt key state"""
|
||||
try:
|
||||
if key in [keyboard.Key.alt_l, keyboard.Key.alt_r, keyboard.Key.alt]:
|
||||
self.alt_pressed = False
|
||||
except Exception as e:
|
||||
logging.error(f"Error in key release handler: {e}")
|
||||
|
||||
def run(self):
|
||||
"""Start the keyboard listener"""
|
||||
logging.info("Starting Alt+R listener...")
|
||||
print("Read-aloud service running. Press Alt+R on selected text to read it.")
|
||||
print("Press Ctrl+C to quit.")
|
||||
|
||||
# Start keyboard listener
|
||||
with keyboard.Listener(
|
||||
on_press=self.on_key_press,
|
||||
on_release=self.on_key_release
|
||||
) as listener:
|
||||
listener.join()
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
reader = MiddleClickReader()
|
||||
reader.run()
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Shutting down...")
|
||||
print("\nShutting down...")
|
||||
except Exception as e:
|
||||
logging.error(f"Fatal error: {e}")
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Suite for Middle-Click Read-Aloud Service
|
||||
Test Suite for Read-Aloud Service (Alt+R)
|
||||
Tests on-demand text-to-speech functionality
|
||||
"""
|
||||
|
||||
@ -14,22 +14,22 @@ from unittest.mock import Mock, patch, MagicMock, call
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||
|
||||
|
||||
class TestMiddleClickReader(unittest.TestCase):
|
||||
"""Test middle-click reader functionality"""
|
||||
class TestReadAloud(unittest.TestCase):
|
||||
"""Test read-aloud service functionality"""
|
||||
|
||||
def test_can_import_middle_click_reader(self):
|
||||
"""Test that middle-click reader can be imported"""
|
||||
def test_can_import_read_aloud(self):
|
||||
"""Test that read-aloud service can be imported"""
|
||||
try:
|
||||
from dictation_service import middle_click_reader
|
||||
self.assertTrue(hasattr(middle_click_reader, 'MiddleClickReader'))
|
||||
self.assertTrue(hasattr(middle_click_reader, 'main'))
|
||||
from dictation_service import read_aloud
|
||||
self.assertTrue(hasattr(read_aloud, 'MiddleClickReader'))
|
||||
self.assertTrue(hasattr(read_aloud, 'main'))
|
||||
except ImportError as e:
|
||||
self.fail(f"Cannot import middle-click reader: {e}")
|
||||
self.fail(f"Cannot import read-aloud service: {e}")
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_get_selected_text(self, mock_run):
|
||||
"""Test getting selected text from xclip"""
|
||||
from dictation_service.middle_click_reader import MiddleClickReader
|
||||
from dictation_service.read_aloud import MiddleClickReader
|
||||
|
||||
reader = MiddleClickReader()
|
||||
|
||||
@ -49,7 +49,7 @@ class TestMiddleClickReader(unittest.TestCase):
|
||||
@patch('os.remove')
|
||||
def test_read_text(self, mock_remove, mock_exists, mock_temp, mock_run):
|
||||
"""Test reading text with edge-tts"""
|
||||
from dictation_service.middle_click_reader import MiddleClickReader
|
||||
from dictation_service.read_aloud import MiddleClickReader
|
||||
|
||||
reader = MiddleClickReader()
|
||||
|
||||
@ -74,7 +74,7 @@ class TestMiddleClickReader(unittest.TestCase):
|
||||
|
||||
def test_minimum_text_length(self):
|
||||
"""Test that short text is not read"""
|
||||
from dictation_service.middle_click_reader import MiddleClickReader
|
||||
from dictation_service.read_aloud import MiddleClickReader
|
||||
|
||||
reader = MiddleClickReader()
|
||||
|
||||
@ -93,7 +93,7 @@ class TestMiddleClickReader(unittest.TestCase):
|
||||
|
||||
def test_lock_file_creation(self):
|
||||
"""Test that lock file is created during reading"""
|
||||
from dictation_service.middle_click_reader import LOCK_FILE
|
||||
from dictation_service.read_aloud import LOCK_FILE
|
||||
|
||||
# Verify lock file path
|
||||
self.assertEqual(LOCK_FILE, "/tmp/dictation_speaking.lock")
|
||||
@ -101,7 +101,7 @@ class TestMiddleClickReader(unittest.TestCase):
|
||||
@patch('pynput.mouse.Listener')
|
||||
def test_mouse_listener_initialization(self, mock_listener):
|
||||
"""Test that mouse listener can be initialized"""
|
||||
from dictation_service.middle_click_reader import MiddleClickReader
|
||||
from dictation_service.read_aloud import MiddleClickReader
|
||||
|
||||
reader = MiddleClickReader()
|
||||
|
||||
@ -115,7 +115,7 @@ class TestMiddleClickReader(unittest.TestCase):
|
||||
|
||||
def test_middle_click_detection(self):
|
||||
"""Test middle-click detection logic"""
|
||||
from dictation_service.middle_click_reader import MiddleClickReader
|
||||
from dictation_service.read_aloud import MiddleClickReader
|
||||
from pynput import mouse
|
||||
|
||||
reader = MiddleClickReader()
|
||||
@ -133,7 +133,7 @@ class TestMiddleClickReader(unittest.TestCase):
|
||||
|
||||
def test_ignores_non_middle_clicks(self):
|
||||
"""Test that non-middle clicks are ignored"""
|
||||
from dictation_service.middle_click_reader import MiddleClickReader
|
||||
from dictation_service.read_aloud import MiddleClickReader
|
||||
from pynput import mouse
|
||||
|
||||
reader = MiddleClickReader()
|
||||
@ -149,7 +149,7 @@ class TestMiddleClickReader(unittest.TestCase):
|
||||
|
||||
def test_concurrent_reading_prevention(self):
|
||||
"""Test that concurrent reading is prevented"""
|
||||
from dictation_service.middle_click_reader import MiddleClickReader
|
||||
from dictation_service.read_aloud import MiddleClickReader
|
||||
|
||||
reader = MiddleClickReader()
|
||||
|
||||
@ -170,7 +170,7 @@ class TestEdgeTTSIntegration(unittest.TestCase):
|
||||
@patch('subprocess.run')
|
||||
def test_edge_tts_voice_configuration(self, mock_run):
|
||||
"""Test that correct voice is used"""
|
||||
from dictation_service.middle_click_reader import EDGE_TTS_VOICE
|
||||
from dictation_service.read_aloud import EDGE_TTS_VOICE
|
||||
|
||||
# Verify default voice
|
||||
self.assertEqual(EDGE_TTS_VOICE, "en-US-ChristopherNeural")
|
||||
@ -178,7 +178,7 @@ class TestEdgeTTSIntegration(unittest.TestCase):
|
||||
@patch('subprocess.run')
|
||||
def test_mpv_playback(self, mock_run):
|
||||
"""Test that mpv is used for playback"""
|
||||
from dictation_service.middle_click_reader import MiddleClickReader
|
||||
from dictation_service.read_aloud import MiddleClickReader
|
||||
|
||||
reader = MiddleClickReader()
|
||||
reader.is_reading = False
|
||||
131
uv.lock
generated
131
uv.lock
generated
@ -250,6 +250,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coloredlogs"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "humanfriendly" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dictation-service"
|
||||
version = "0.2.0"
|
||||
@ -257,6 +269,7 @@ source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "edge-tts" },
|
||||
{ name = "numpy" },
|
||||
{ name = "piper-tts" },
|
||||
{ name = "pygobject" },
|
||||
{ name = "pynput" },
|
||||
{ name = "sounddevice" },
|
||||
@ -267,6 +280,7 @@ dependencies = [
|
||||
requires-dist = [
|
||||
{ name = "edge-tts", specifier = ">=7.2.3" },
|
||||
{ name = "numpy", specifier = ">=2.3.5" },
|
||||
{ name = "piper-tts", specifier = ">=1.3.0" },
|
||||
{ name = "pygobject", specifier = ">=3.42.0" },
|
||||
{ name = "pynput", specifier = ">=1.8.1" },
|
||||
{ name = "sounddevice", specifier = ">=0.5.3" },
|
||||
@ -294,6 +308,15 @@ version = "1.9.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/fe/a17c106a1f4061ce83f04d14bcedcfb2c38c7793ea56bfb906a6fadae8cb/evdev-1.9.2.tar.gz", hash = "sha256:5d3278892ce1f92a74d6bf888cc8525d9f68af85dbe336c95d1c87fb8f423069", size = 33301, upload-time = "2025-05-01T19:53:47.69Z" }
|
||||
|
||||
[[package]]
|
||||
name = "flatbuffers"
|
||||
version = "25.9.23"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/1f/3ee70b0a55137442038f2a33469cc5fddd7e0ad2abf83d7497c18a2b6923/flatbuffers-25.9.23.tar.gz", hash = "sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12", size = 22067, upload-time = "2025-09-24T05:25:30.106Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/1b/00a78aa2e8fbd63f9af08c9c19e6deb3d5d66b4dda677a0f61654680ee89/flatbuffers-25.9.23-py2.py3-none-any.whl", hash = "sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2", size = 30869, upload-time = "2025-09-24T05:25:28.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.8.0"
|
||||
@ -383,6 +406,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humanfriendly"
|
||||
version = "10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
@ -392,6 +427,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpmath"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.7.0"
|
||||
@ -554,6 +598,57 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime"
|
||||
version = "1.23.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coloredlogs" },
|
||||
{ name = "flatbuffers" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "sympy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "piper-tts"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "onnxruntime" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/c0/d9b5f64869274be3ebc6dc483f13791a3c6ebbc0e37fad4e237a76d5365b/piper_tts-1.3.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0af0c90aeddf762555ed940de1ac576acbefb3623e6d5ca4fb1a70359ee7e65d", size = 13819597, upload-time = "2025-07-10T21:07:22.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/17/6a059c0a45e582fadd4545ed092294fd0add7c679f6c09440af5cd2678b5/piper_tts-1.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:810c91a084d335d32b42928b1ef69d6480cf7e3a5a8b15eff98edd2ef55f2791", size = 13828403, upload-time = "2025-07-10T21:07:25.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/92/f37e5111440fc6c6336f42f8dab88afaa545394784dc930f808a68883c48/piper_tts-1.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d39f85c3f4b6ade512976849579344fc72595ec613f374dbcf8521716398907", size = 13836863, upload-time = "2025-07-10T21:07:27.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/73/3d29175cfd93e791baaef3335819778d3f8c8898e2fe16cd0cc8b8163f84/piper_tts-1.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234c25474655b26f3418b84522c815c43e9b1bc8a1fdb13c2b28514290c165f0", size = 13836748, upload-time = "2025-07-10T21:07:29.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/a5/d782d469fc19db9bf19f1725d4a6ef77d2413515b61f5017340688f5d093/piper_tts-1.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:dc6b5be4e15f3c0f4a6067b515bc6202ddf3e2b0c6cbd6c8bdeccab2453c89c7", size = 13826773, upload-time = "2025-07-10T21:07:31.95Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.4.1"
|
||||
@ -638,6 +733,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycairo"
|
||||
version = "1.29.0"
|
||||
@ -774,6 +884,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyreadline3"
|
||||
version = "3.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-xlib"
|
||||
version = "0.33"
|
||||
@ -831,6 +950,18 @@ version = "3.5.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/b7/4a1bc231e0681ebf339337b0cd05b91dc6a0d701fa852bb812e244b7a030/srt-3.5.3.tar.gz", hash = "sha256:4884315043a4f0740fd1f878ed6caa376ac06d70e135f306a6dc44632eed0cc0", size = 28296, upload-time = "2023-03-28T02:35:44.007Z" }
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mpmath" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tabulate"
|
||||
version = "0.9.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user