一篇散乱的思路整理
最近浏览器上用 kimi 拓展各种划词提问辅助阅读体验可以说非常棒。
特别是上下文识别能力、划词打开提问窗口后可以继续提问对话这两点。
但是脱离浏览器后,在编辑场景下就很难获得这种快速易用的辅助搜索体验了。在主流编辑器倒是还是通过 copilot 的 ctrl + i
快捷键来快速提问。
但是像写个文档辅助查资料(思源里可以通过直接开伺服在浏览器里临时解决)、一些复杂任务的临时查询还是需要回归到传统的切换窗口然后搜索(包括多屏幕下的切屏),于是最近就想着看一下有哪些能够实现全局划线提问能力的 AI 方案。
在简单搜索后,找到了 讯飞星火、 豆包 这样标榜包括全局任意划词提问以及全功能 AI 能力并附赠 chromium 内核的方案。实际体验后,都存在一次划线解释提问后继续对话就必须打开它的软件本体切换窗口来继续对话的以及功能太繁杂而没有足够的配置项隐藏它们的问题。
于是就开始想:
“像 pot 这类划词翻译工具是否能拓展一个 AI 搜索功能呢?”
于是顺着在 Github 上搜索划词关键词找到了以下两个具有参考意义的应用
openai-translator: 基于 ChatGPT API 的划词翻译浏览器插件和跨平台桌面端应用
一个是 ai 翻译, 一个是功能足够轻量。
第一个虽然已经引入 ai 润色了,但却没有提问功能(
第二个是功能做到了相对的极简,作为一个临时翻译方案也不错就保留了。
然后思路变成
“如果自己写一个类似的工具需要考虑那些方面呢”
于是问了问 GPT + 之前用 Quicker 写一些获取划选内容的脚本时快捷键设置为 ctrl+c
发生的循环问题。可以确认大概是通过检测
- 鼠标按下
- 检测鼠标移动范围
- 鼠标释放
- 发送复制事件(模拟 ctrl+c)复制内容,然后通过剪切板获取划取内容
- 最后通过向剪切板写入空白信息覆盖刚才复制的内容避免污染剪切板
就实现了最关键的全局获取划线内容这一项功能(进一步的监听键盘 shift+方向键
或 ctrl+a
都差不多)
接着让 GPT 补全处理多屏幕问题,就简单实现了一个全局划线后显示一个 web 窗口查询内容的功能。似乎进一步优化性能和逻辑就能够实现一个符合自己需求的划词 AI 提问工具了。
但当前 GPT 给出的都是 python QT 代码,没什么改的欲望,就先贴一份在这吧
import sys
import threading
import time
from pynput import mouse, keyboard
import pyperclip
import pyautogui
from PyQt5 import QtWidgets, QtCore, QtGui, QtWebEngineWidgets
class SelectionListener(QtCore.QObject):
# 定义信号,传递选中文本和坐标
selection_made = QtCore.pyqtSignal(str, int, int)
selection_cleared = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
self.current_selection = ""
self.run_service = True
# 鼠标状态变量
self.mouse_listener = mouse.Listener(on_click=self.on_click, on_move=self.on_move)
self.is_dragging = False
self.mouse_pressed = False
self.last_mouse_time = 0
self.click_count = 0
# 键盘状态变量
self.keyboard_listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
self.shift_pressed = False
self.arrow_key_pressed = False
# 鼠标事件处理
def on_click(self, x, y, button, pressed):
if pressed:
self.mouse_pressed = True
current_time = time.time()
if current_time - self.last_mouse_time < 0.4:
self.click_count += 1
else:
self.click_count = 1
self.last_mouse_time = current_time
if self.click_count == 2:
# 检测到双击
time.sleep(0.1)
self.simulate_copy_and_get_selection()
self.click_count = 0
else:
if self.mouse_pressed:
if self.is_dragging:
# 拖拽选择完成
time.sleep(0.1)
self.simulate_copy_and_get_selection()
else:
# 单击未拖拽,可能清除了选择
self.selection_cleared.emit()
self.is_dragging = False
self.mouse_pressed = False
def on_move(self, x, y):
if self.mouse_pressed:
if not self.is_dragging:
pass # 开始拖拽
self.is_dragging = True
# 键盘事件处理
def on_press(self, key):
try:
if key == keyboard.Key.shift:
self.shift_pressed = True
elif self.shift_pressed and key in [keyboard.Key.left, keyboard.Key.right, keyboard.Key.up, keyboard.Key.down]:
self.arrow_key_pressed = True
except AttributeError:
pass
def on_release(self, key):
try:
if key == keyboard.Key.shift:
if self.arrow_key_pressed:
# Shift选择完成
time.sleep(0.1)
self.simulate_copy_and_get_selection()
else:
# Shift释放,无箭头键,可能清除了选择
self.selection_cleared.emit()
self.shift_pressed = False
self.arrow_key_pressed = False
except AttributeError:
pass
def simulate_copy_and_get_selection(self):
try:
# 保存当前剪贴板内容
previous_clipboard_content = pyperclip.paste()
# 模拟复制
pyautogui.hotkey("ctrl", "c")
time.sleep(0.05) # 等待剪贴板更新
# 获取选中文本
new_clipboard_content = pyperclip.paste()
# 恢复之前的剪贴板内容
pyperclip.copy(previous_clipboard_content)
# 获取鼠标位置
cursor_pos = QtGui.QCursor.pos()
x = cursor_pos.x()
y = cursor_pos.y()
# 如果有新文本被选中,发射信号
if new_clipboard_content != previous_clipboard_content and new_clipboard_content.strip():
self.current_selection = new_clipboard_content
self.selection_made.emit(self.current_selection, x, y)
else:
# 没有选中文本,可能清除了选择
self.selection_cleared.emit()
except Exception as e:
print(f"模拟复制时出错: {e}")
def start(self):
self.mouse_listener.start()
self.keyboard_listener.start()
try:
while self.run_service:
time.sleep(1) # 保持线程存活
except KeyboardInterrupt:
self.stop()
def stop(self):
self.mouse_listener.stop()
self.keyboard_listener.stop()
self.run_service = False
class FloatingWindow(QtWidgets.QWidget):
def __init__(self, text, x, y, parent=None):
super().__init__(parent)
self.text = text
self.initUI(x, y)
def initUI(self, x, y):
self.setWindowFlags(
QtCore.Qt.Popup |
QtCore.Qt.FramelessWindowHint |
QtCore.Qt.WindowStaysOnTopHint
)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)
# 调整位置,适应多显示器环境
cursor_pos = QtCore.QPoint(x, y)
screen = QtGui.QGuiApplication.screenAt(cursor_pos)
if screen is None:
screen = QtWidgets.QApplication.primaryScreen()
# 创建按钮
self.button = QtWidgets.QPushButton("🔍", self)
self.button.setStyleSheet("""
QPushButton {
font-size: 18px;
background-color: rgba(255, 255, 255, 200);
border: none;
border-radius: 15px;
}
QPushButton::hover {
background-color: rgba(200, 200, 200, 200);
}
""")
self.button.setFixedSize(30, 30)
self.button.clicked.connect(self.show_webview)
# 调整大小
self.resize(30, 30)
# 移动窗口到鼠标位置
self.move(x - self.width() // 2, y - self.height() // 2)
# 设置焦点策略
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.button.setFocusPolicy(QtCore.Qt.NoFocus)
# 显示窗口
self.show()
def show_webview(self):
self.webview = WebViewWindow(self.text)
self.webview.show()
# 关闭浮动窗口
self.close()
def focusOutEvent(self, event):
self.close()
class WebViewWindow(QtWidgets.QWidget):
def __init__(self, text):
super().__init__()
self.text = text
self.initUI()
def initUI(self):
# 设置无边框窗口
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.resize(800, 600)
layout = QtWidgets.QVBoxLayout(self)
# 创建 QWebEngineView
self.browser = QtWebEngineWidgets.QWebEngineView(self)
# 执行搜索
query = QtCore.QUrl.fromUserInput(f"https://www.google.com/search?q={self.text}")
self.browser.load(query)
layout.addWidget(self.browser)
# 显示窗口
self.show()
def start_selection_listener(listener):
listener.start()
def main():
app = QtWidgets.QApplication(sys.argv)
# 创建选择监听器
listener = SelectionListener()
# 在单独的线程中启动监听器
listener_thread = threading.Thread(target=start_selection_listener, args=(listener,))
listener_thread.daemon = True
listener_thread.start()
# 保持浮动窗口的引用
floating_window = None
# 处理监听器发出的信号
def on_selection_made(text, x, y):
nonlocal floating_window
# 关闭任何已存在的浮动窗口
if floating_window is not None:
floating_window.close()
floating_window = FloatingWindow(text, x, y)
def on_selection_cleared():
nonlocal floating_window
if floating_window is not None:
floating_window.close()
floating_window = None
listener.selection_made.connect(on_selection_made)
# listener.selection_cleared.connect(on_selection_cleared)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于