Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Windows regression

on:
push:
branches: [develop, 'feat--**']
pull_request:
branches: [develop]

permissions:
contents: read

jobs:
test:
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
version: '0.11.29'
enable-cache: true
- run: uv python install 3.12
- run: uv sync --all-groups --frozen
- run: uv run python -c "from core.client.app import CapsWriterClient; from core.client.shortcut.key_mapper import KeyMapper"
- run: uv run pytest -q
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ MANIFEST
*.manifest
*.spec
*.zip
!packaging/macos/CapsWriterClient.spec

# 安装日志
pip-log.txt
Expand Down Expand Up @@ -176,6 +177,8 @@ test_*.py
test_*.ipynb
test_*.md
test_*.txt
!tests/
!tests/**/*.py
stocks.txt
example_*.py
example_.ipynb
Expand All @@ -187,4 +190,4 @@ release
*.dll
*.exe

models/**/*.cfg
models/**/*.cfg
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12.13
82 changes: 54 additions & 28 deletions config_client.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,67 @@
import os
from collections.abc import Iterable
import sys
from pathlib import Path

# 版本信息
__version__ = '2.6'

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def _default_base_dir() -> Path:
if sys.platform == 'darwin' and getattr(sys, 'frozen', False):
return Path.home() / 'Library' / 'Application Support' / 'CapsWriter-Offline'
return Path(__file__).resolve().parent


def _macos_shortcut_key() -> str:
return os.environ.get('CAPSWRITER_HOTKEY', 'shift_r').strip() or 'shift_r'


# 源码运行时使用仓库目录;macOS App 使用可写的 Application Support 目录。
BASE_DIR = str(Path(os.environ.get('CAPSWRITER_HOME', _default_base_dir())).expanduser().resolve())
Path(BASE_DIR).mkdir(parents=True, exist_ok=True)


# 客户端配置
class ClientConfig:
addr = '127.0.0.1' # Server 地址
port = '6016' # Server 端口
addr = os.environ.get('CAPSWRITER_SERVER_ADDR', '127.0.0.1')
port = os.environ.get('CAPSWRITER_SERVER_PORT', '6016')

# 快捷键配置列表
shortcuts = [
{
'key': 'caps_lock', # 监听大写锁定键
'type': 'keyboard', # 是键盘快捷键
'suppress': True, # 阻塞按键(短按会补发)
'hold_mode': True, # 长按模式
'enabled': True # 启用此快捷键
},
{
'key': 'x2',
'type': 'mouse',
'suppress': True,
'hold_mode': True,
'enabled': True
},
]
shortcuts = (
[
{
'key': _macos_shortcut_key(),
'type': 'keyboard',
'suppress': False,
'hold_mode': True,
'enabled': True,
},
]
if sys.platform == 'darwin'
else [
{
'key': 'caps_lock', # 监听大写锁定键
'type': 'keyboard', # 是键盘快捷键
'suppress': True, # 阻塞按键(短按会补发)
'hold_mode': True, # 长按模式
'enabled': True # 启用此快捷键
},
{
'key': 'x2',
'type': 'mouse',
'suppress': True,
'hold_mode': True,
'enabled': True
},
]
)

threshold = 0.3 # 快捷键触发阈值(秒)

paste = False # 是否以写入剪切板然后模拟 Ctrl-V 粘贴的方式输出结果
paste = sys.platform == 'darwin' # macOS 使用剪贴板与 Command-V 输出
restore_clip = True # 模拟粘贴后是否恢复剪贴板
paste_apps = ['WeiXin.exe', 'Telegram.exe'] # 匹配时强制粘贴
paste_apps = [] if sys.platform == 'darwin' else ['WeiXin.exe', 'Telegram.exe']

enter_apps = [('happ.exe', 0.5), ('hexin.exe', 0.5)] # (应用名, 延迟秒数) 输出完成后自动回车,如同花顺,输入股票名后,需要回车才能切换
enter_apps = [] if sys.platform == 'darwin' else [('happ.exe', 0.5), ('hexin.exe', 0.5)]

save_audio = True # 是否保存录音文件
audio_name_len = 20 # 将录音识别结果的前多少个字存储到录音文件名中,建议不要超过200
Expand All @@ -48,7 +71,7 @@ class ClientConfig:

trash_punc = ',。,.' # 识别结果要消除的末尾标点
trash_punc_thresh = 8 # 识别结果的单词数量低于阈值时,强制去除末尾标点
trash_punc_apps = ['WeiXin.exe', ] # 对于指定的应用,强制去除末尾标点
trash_punc_apps = [] if sys.platform == 'darwin' else ['WeiXin.exe', ]

traditional_convert = False # 是否将识别结果转换为繁体中文
traditional_locale = 'zh-hant' # 繁体地区:'zh-hant'(标准繁体), 'zh-tw'(台湾繁体), 'zh-hk'(香港繁体)
Expand All @@ -58,16 +81,20 @@ class ClientConfig:
hot_similar = 0.6 # RAG 相似热词阈值(低阈值,用于 LLM 上下文)
hot_rule = True # 是否启用自定义规则替换(基于正则表达式)

llm_enabled = True # 是否启用 LLM 润色功能,需要配置 LLM/ 目录下的角色文件
llm_enabled = sys.platform != 'darwin' # macOS 首版先验证离线 ASR 主链路
llm_stop_key = 'esc' # 中断 LLM 输出的快捷键

enable_tray = True # 客户端默认启用托盘图标功能
enable_tray = sys.platform == 'win32' # 当前托盘生命周期仅适配 Windows

# 日志配置
log_level = 'DEBUG' # 日志级别:'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'

mic_seg_duration = 60 # 麦克风听写时分段长度:60秒
mic_seg_overlap = 4 # 麦克风听写时分段重叠:4秒
audio_device = os.environ.get('CAPSWRITER_AUDIO_DEVICE') or None
if isinstance(audio_device, str) and audio_device.isdigit():
audio_device = int(audio_device)
audio_sample_rate = int(os.environ.get('CAPSWRITER_AUDIO_SAMPLE_RATE', '48000'))

file_seg_duration = 60 # 转录文件时分段长度
file_seg_overlap = 4 # 转录文件时分段重叠
Expand Down Expand Up @@ -130,4 +157,3 @@ class ClientConfig:
{'key': 'f12', 'type': 'keyboard', 'suppress': True, 'hold_mode': True, 'enabled': True},
{'key': 'x2', 'type': 'mouse', 'suppress': True, 'hold_mode': True, 'enabled': True},
"""

20 changes: 15 additions & 5 deletions config_server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys
from pathlib import Path

# 版本信息
Expand All @@ -10,16 +11,26 @@

# 服务端配置
class ServerConfig:
addr = '0.0.0.0'
port = '6016'
addr = os.environ.get('CAPSWRITER_SERVER_BIND', '127.0.0.1' if sys.platform == 'darwin' else '0.0.0.0')
port = os.environ.get('CAPSWRITER_SERVER_PORT', '6016')
max_message_bytes = 8 * 1024 * 1024
max_connections = 4
max_audio_chunk_bytes = 4 * 1024 * 1024
max_audio_seconds = 4 * 60 * 60
max_segment_duration = 300
max_segment_overlap = 30
max_context_chars = 4096

# 语音模型选择:'qwen_asr', 'fun_asr_nano', 'sensevoice', 'paraformer'
model_type = 'qwen_asr'
model_type = os.environ.get(
'CAPSWRITER_MODEL_TYPE',
'paraformer' if sys.platform == 'darwin' else 'qwen_asr',
)

format_num = True # 输出时是否将中文数字转为阿拉伯数字
format_spell = True # 输出时是否调整中英之间的空格

enable_tray = True # 是否启用托盘图标功能
enable_tray = sys.platform == 'win32' # 当前托盘实现依赖 Windows 控制台生命周期
hotwords_path = Path() / 'hot-server.txt' # 全局热词配置文件路径

# 日志配置
Expand Down Expand Up @@ -173,4 +184,3 @@ class ForceAlignerGGUFArgs:
# 对齐细节
n_ctx = 3072 # 上下文窗口大小
dml_pad_to = 30 # 开启 DirectML 加速时,短音频统一填充到指定长度,有加速效果

10 changes: 7 additions & 3 deletions core/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@
setup_logger('client', level=Config.log_level)
logger = get_logger('client')

# 门面类
from core.client.app import CapsWriterClient

__all__ = [
'CapsWriterClient',
]


def __getattr__(name):
if name == 'CapsWriterClient':
from core.client.app import CapsWriterClient

return CapsWriterClient
raise AttributeError(name)
50 changes: 41 additions & 9 deletions core/client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@

from .state import ClientState
from . import logger
from config_client import ClientConfig as Config, __version__
from config_client import BASE_DIR, ClientConfig as Config
from core.tools.signal_handler import register_signal
from .state import console
from .connection import WebSocketManager
from typing import TYPE_CHECKING, Optional
from .manager import (
TrayManager,
MicRunner, FileRunner
Expand All @@ -32,8 +31,24 @@
from .llm.llm_handler import LLMHandler
from .output.text_output import TextOutput
from .diary.diary_writer import DiaryWriter
from .macos_permissions import MacOSPermissionError, show_macos_error, show_permission_error
from core.tools.empty_working_set import empty_current_working_set
from platform import system


def _resolve_input_files(arguments: list[str], launch_dir: Path) -> list[Path]:
"""Resolve command-line files before the client changes its working directory."""
files = []
for argument in arguments:
try:
path = Path(argument).expanduser()
if not path.is_absolute():
path = launch_dir / path
path = path.resolve()
if path.is_file():
files.append(path)
except (OSError, RuntimeError) as exc:
logger.warning(f'忽略无法解析的输入路径 {argument!r}: {exc}')
return files



Expand All @@ -45,7 +60,8 @@ class CapsWriterClient:
"""
def __init__(self):
# 确保正确的工作目录
self.base_dir = Path(__file__).parents[2]
self.launch_dir = Path.cwd()
self.base_dir = Path(BASE_DIR)
os.chdir(self.base_dir)

# 初始化事件循环
Expand Down Expand Up @@ -124,7 +140,7 @@ def start(self):
# 注册退出函数
register_signal(self.stop)

files = [Path(f) for f in sys.argv[1:] if os.path.exists(f)]
files = _resolve_input_files(sys.argv[1:], self.launch_dir)

if files:
# 文件转录模式
Expand All @@ -135,7 +151,23 @@ def start(self):

try:
self.loop.run_until_complete(runner.run())
except RuntimeError:
...


except MacOSPermissionError as exc:
logger.error(str(exc))
console.print(f'[bold red]{exc}[/bold red]')
if sys.platform == 'darwin' and getattr(sys, 'frozen', False):
try:
show_permission_error(str(exc))
except Exception:
logger.exception('macOS 权限错误弹窗显示失败')
raise SystemExit(2) from None
except RuntimeError as exc:
if str(exc) == 'Event loop stopped before Future completed.':
return
logger.exception('客户端运行异常')
console.print(f'[bold red]{exc}[/bold red]')
if sys.platform == 'darwin' and getattr(sys, 'frozen', False):
try:
show_macos_error(str(exc))
except Exception:
logger.exception('macOS 运行错误弹窗显示失败')
raise SystemExit(1) from None
Loading
Loading