Skip to content
Open
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
63 changes: 63 additions & 0 deletions .github/workflows/build-macos-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Build macOS App

on:
workflow_dispatch:
push:
branches:
- fix/startup-degradation-and-windows-tooling
paths:
- ".github/workflows/build-macos-app.yml"
- "main.py"
- "core/**"
- "gui/**"
- "pb2/**"
- "scripts/build_mac.sh"
- "requirements.txt"
- "realtime_translator_mac.spec"

jobs:
build-macos:
runs-on: macos-14

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: |
python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txt

- name: Build macOS app
run: |
chmod +x scripts/build_mac.sh
. .venv/bin/activate
bash scripts/build_mac.sh

- name: Inspect dist
run: |
ls -R dist

- name: Zip app bundle
run: |
ditto -c -k --sequesterRsrc --keepParent dist/realtime_translator.app dist/realtime_translator.app.zip
ditto -c -k --keepParent dist/realtime_translator_support dist/realtime_translator_support.zip

- name: Upload app artifact
uses: actions/upload-artifact@v4
with:
name: realtime_translator-macos-app
path: dist/realtime_translator.app.zip

- name: Upload support artifact
uses: actions/upload-artifact@v4
with:
name: realtime_translator-macos-support
path: dist/realtime_translator_support.zip
10 changes: 9 additions & 1 deletion core/audio_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import Optional
import queue
import threading
import shutil

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -425,14 +426,21 @@ def _start_ffmpeg_process(self):
import subprocess

try:
ffmpeg_bin = shutil.which("ffmpeg")
if not ffmpeg_bin:
raise RuntimeError(
"未找到 ffmpeg 可执行文件。请安装 FFmpeg 并加入 PATH,"
"或在 config.yaml 中关闭需要语音输出的 zh_to_en 通道。"
)

logger.info("🎬 启动持久FFmpeg进程...")
# VB-CABLE需要48kHz采样率,强制重采样
output_sample_rate = 48000
logger.info(f"📊 重采样: {self.sample_rate}Hz → {output_sample_rate}Hz")

self._ffmpeg_process = subprocess.Popen(
[
'ffmpeg',
ffmpeg_bin,
'-loglevel', 'error',
'-f', 'ogg', # 输入格式: Ogg容器
'-i', 'pipe:0', # 从stdin读取流式数据
Expand Down
82 changes: 54 additions & 28 deletions core/system_audio_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ def _test_device(self, device_id: int) -> bool:
logger.debug(f" 设备 [{device_id}] 测试失败: {e}")
return False

@staticmethod
def _is_microphone_device(device_name: str) -> bool:
"""过滤明显的麦克风设备,避免错误回退到人声输入"""
lowered_name = device_name.lower()
mic_keywords = ("mic", "microphone", "麦克风")
return any(keyword in lowered_name for keyword in mic_keywords)

@staticmethod
def _system_audio_score(device_name: str) -> int:
"""为常见系统回采设备打分,分数越高越可能是正确输入源"""
lowered_name = device_name.lower()
score_keywords = (
"stereo mix",
"立体声混音",
"what u hear",
"wave out mix",
"blackhole",
"loopback",
"monitor",
)
return sum(1 for keyword in score_keywords if keyword in lowered_name)

def _find_device(self) -> int:
"""
查找系统音频设备
Expand Down Expand Up @@ -114,44 +136,48 @@ def _find_device(self) -> int:
logger.warning(f"⚠️ 设备验证失败,尝试降级设备...")
break

# 2. 尝试降级设备(排除VB-CABLE Output以避免回声)
# 2. 优先尝试显式配置的降级设备
logger.warning(f"⚠️ 主设备 '{self.device_name}' 不可用,尝试降级设备...")
logger.warning(f"⚠️ 警告: 使用CABLE Output会导致音频回路!")

# 搜索所有可用的输入设备(排除CABLE Output)
available_devices = []
for i, device in enumerate(devices):
device_name = device['name']
max_input_channels = device['max_input_channels']

# 排除VB-CABLE相关设备
if max_input_channels > 0 and "CABLE" not in device_name.upper():
if self._test_device(i):
available_devices.append((i, device_name))
logger.info(f"✅ 找到可用设备: [{i}] {device_name}")

# 如果找到可用设备,使用第一个
if available_devices:
device_id, device_name = available_devices[0]
logger.info(f"✅ 使用设备: [{device_id}] {device_name}")
return device_id
if self.fallback_device in device_name and max_input_channels > 0:
logger.warning(f"🔄 测试降级设备: [{i}] {device_name}")

# 如果没有找到任何设备,最后才尝试CABLE Output(并警告)
logger.error("❌ 未找到非VB-CABLE的音频设备!")
logger.error("⚠️ 将尝试使用CABLE Output,但这会导致音频回路!")
if self._test_device(i):
logger.warning(f"⚠️ 使用显式降级设备(有回声风险): [{i}] {device_name}")
return i

# 3. 仅尝试常见的系统回采设备,避免误选普通麦克风
logger.warning("⚠️ 未找到可用的显式降级设备,尝试推断系统回采设备...")
candidates = []
for i, device in enumerate(devices):
device_name = device['name']
max_input_channels = device['max_input_channels']

if self.fallback_device in device_name and max_input_channels > 0:
logger.warning(f"🔄 测试降级设备: [{i}] {device_name}")

if self._test_device(i):
logger.warning(f"⚠️ 使用降级设备(有回声风险): [{i}] {device_name}")
return i
if max_input_channels <= 0:
continue
if "CABLE" in device_name.upper():
continue
if self._is_microphone_device(device_name):
logger.debug(f"⏭️ 跳过麦克风设备: [{i}] {device_name}")
continue

score = self._system_audio_score(device_name)
if score <= 0:
continue
if self._test_device(i):
candidates.append((score, i, device_name))
logger.info(f"✅ 找到候选系统回采设备: [{i}] {device_name} (score={score})")

if candidates:
candidates.sort(reverse=True)
_, device_id, device_name = candidates[0]
logger.warning(f"⚠️ 使用推断的系统回采设备: [{device_id}] {device_name}")
return device_id

# 3. 抛出异常
# 4. 抛出异常,不再静默回退到普通输入设备
logger.error("❌ 未找到任何可用的系统音频设备!")
logger.error(f" 请确保已启用: '{self.device_name}' 或 '{self.fallback_device}'")
logger.error("")
Expand Down Expand Up @@ -241,8 +267,8 @@ def get_stats(self) -> dict:
"""
return {
'device_index': self.device_index,
'device_name': self.device_name if self.device_index is not None
else sd.query_devices(self.device_index)['name'],
'device_name': sd.query_devices(self.device_index)['name']
if self.device_index is not None else self.device_name,
'sample_rate': self.sample_rate,
'channels': self.channels,
'chunk_size': self.chunk_size,
Expand Down
3 changes: 2 additions & 1 deletion gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"""

from .subtitle_window import SubtitleWindow, SubtitleWindowThread
from .control_window import ControlWindow, launch_control_window

__all__ = ['SubtitleWindow', 'SubtitleWindowThread']
__all__ = ['SubtitleWindow', 'SubtitleWindowThread', 'ControlWindow', 'launch_control_window']
Loading