From a7e2dd65909a4a4eab795f86efd12f90a905626a Mon Sep 17 00:00:00 2001 From: crosswk Date: Tue, 2 Jun 2026 15:22:30 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=95=B4=E5=90=88=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=8E=E6=96=87=E6=A1=A3=EF=BC=8C=E5=87=86?= =?UTF-8?q?=E5=A4=87=200.0.4=20=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: ASR 性能优化与热词幻觉修复 (asr.py / main.py),调整 config.example.yaml vllm_max_model_len - 补充 docs AI/ASR 服务提供商文档 - 同步客户端原生层(src-tauri)、overlay、recorder 等既有改动 - .gitignore 忽略 we-media/ 运营资料 --- .gitignore | 2 + README.md | 2 +- client/src-tauri/src/commands/window.rs | 7 + client/src-tauri/src/main.rs | 1 + client/src-tauri/src/window/mod.rs | 123 ++++++++++- .../src/features/settings/AppearancePage.tsx | 59 +++--- client/src/overlay/main.tsx | 9 + client/src/pages/About.tsx | 2 +- client/src/pages/History.tsx | 193 +++++++++++++----- client/src/services/bridge.ts | 21 +- client/src/services/recorder.ts | 5 + .../services/recorder/RecorderOrchestrator.ts | 6 + .../src/services/webviewKeyboardFallback.ts | 37 ++++ docs/SayIt AI 服务提供商.md | 83 ++++++++ docs/SayIt AI 润色供应商配置.md | 8 +- docs/SayIt ASR 服务提供商.md | 95 +++++++++ server/backend/app/asr.py | 105 +++++++++- server/backend/app/main.py | 37 +++- server/config.example.yaml | 2 +- 19 files changed, 691 insertions(+), 106 deletions(-) create mode 100644 docs/SayIt AI 服务提供商.md create mode 100644 docs/SayIt ASR 服务提供商.md diff --git a/.gitignore b/.gitignore index 034e5ab..132aa99 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ config.yaml # ── Development-only (not for open source) ── dev-docs/ dev-scripts/ +dev-notes/ +we-media/ sayit说话波形图效果.html # ── Server dev artifacts ── diff --git a/README.md b/README.md index 130d6b4..818dce7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # SayIt -**开口即成稿 — 用说话代替打字,AI 实时把口语变成书面表达。** +**随口说,出色写 — 用说话代替打字,AI 实时把口语变成书面表达。** 按下快捷键开始说话,再按一次,润色后的文字自动输入到光标位置。 diff --git a/client/src-tauri/src/commands/window.rs b/client/src-tauri/src/commands/window.rs index 44f6a9f..d99ae8d 100644 --- a/client/src-tauri/src/commands/window.rs +++ b/client/src-tauri/src/commands/window.rs @@ -23,3 +23,10 @@ pub async fn update_overlay_state(data: Value, app: AppHandle, window_state: Sta window_state.update_overlay_state(&app, &data); Ok(()) } + +/// Reported by the overlay webview in response to an `overlay-ping` event. +/// Used to detect WebView2 unresponsiveness. +#[tauri::command] +pub fn overlay_pong(seq: u64) { + crate::window::record_overlay_pong(seq); +} diff --git a/client/src-tauri/src/main.rs b/client/src-tauri/src/main.rs index 54c5116..f242419 100644 --- a/client/src-tauri/src/main.rs +++ b/client/src-tauri/src/main.rs @@ -302,6 +302,7 @@ fn main() { commands::window::show_overlay, commands::window::hide_overlay, commands::window::update_overlay_state, + commands::window::overlay_pong, // Paste / Context commands::paste::paste_text, commands::paste::get_probe_result, diff --git a/client/src-tauri/src/window/mod.rs b/client/src-tauri/src/window/mod.rs index b330e5b..74d3e6a 100644 --- a/client/src-tauri/src/window/mod.rs +++ b/client/src-tauri/src/window/mod.rs @@ -1,12 +1,30 @@ use serde_json::Value; +use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; use std::sync::Mutex; use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; +use crate::commands::system::write_log_line; + const OVERLAY_DEFAULT_BASE_WIDTH: f64 = 360.0; const OVERLAY_BASE_HEIGHT: f64 = 56.0; const OVERLAY_FALLBACK_WIDTH: f64 = 520.0; const OVERLAY_FALLBACK_HEIGHT: f64 = 224.0; +/// Time of last received pong from overlay (epoch millis). 0 = never. +/// Used to detect WebView2 unresponsiveness in `show_overlay`. +static LAST_PONG_MS: AtomicI64 = AtomicI64::new(0); +/// Monotonic id for ping/pong correlation +static PING_SEQ: AtomicU64 = AtomicU64::new(0); + +pub fn record_overlay_pong(_seq: u64) { + let now = chrono::Utc::now().timestamp_millis(); + LAST_PONG_MS.store(now, Ordering::SeqCst); +} + +fn now_ms() -> i64 { + chrono::Utc::now().timestamp_millis() +} + #[derive(Debug, Clone, PartialEq)] enum OverlayLayout { Base, @@ -31,25 +49,93 @@ impl WindowState { let is_fallback = layout == OverlayLayout::Fallback; let base_w = *self.overlay_base_width.lock().unwrap(); + let bounds = calc_overlay_bounds(app, is_fallback, base_w); + let (mon_w, mon_h, mon_scale) = monitor_info(app); + if let Some(overlay) = app.get_webview_window("overlay") { - let bounds = calc_overlay_bounds(app, is_fallback, base_w); - let _ = overlay.set_position(tauri::Position::Logical(tauri::LogicalPosition::new(bounds.0, bounds.1))); - let _ = overlay.set_size(tauri::Size::Logical(tauri::LogicalSize::new(bounds.2, bounds.3))); - let _ = overlay.show(); - let _ = overlay.set_always_on_top(true); + // ── Diagnostic snapshot before any state change ── + let was_visible = overlay.is_visible().unwrap_or(false); + let outer_pos = overlay.outer_position() + .map(|p| format!("{},{}", p.x, p.y)) + .unwrap_or_else(|_| "err".to_string()); + let outer_size = overlay.outer_size() + .map(|s| format!("{}x{}", s.width, s.height)) + .unwrap_or_else(|_| "err".to_string()); + + // Probe webview responsiveness — send a ping; pong arrival is logged async. + let seq = PING_SEQ.fetch_add(1, Ordering::SeqCst) + 1; + let last_pong = LAST_PONG_MS.load(Ordering::SeqCst); + let pong_age_ms = if last_pong == 0 { -1 } else { now_ms() - last_pong }; + + let ping_emit_err = overlay.emit("overlay-ping", seq).err() + .map(|e| format!("{:?}", e)); + + write_log_line(&format!( + "[overlay-diag] show begin handle=present layout={:?} fallback={} was_visible={} \ + outer_pos={} outer_size={} target_bounds={:?} monitor={}x{}@{:.2} \ + ping_seq={} last_pong_age_ms={} ping_emit_err={:?}", + layout, is_fallback, was_visible, + outer_pos, outer_size, bounds, mon_w, mon_h, mon_scale, + seq, pong_age_ms, ping_emit_err, + )); + + let pos_err = overlay.set_position(tauri::Position::Logical( + tauri::LogicalPosition::new(bounds.0, bounds.1) + )).err().map(|e| format!("{:?}", e)); + let size_err = overlay.set_size(tauri::Size::Logical( + tauri::LogicalSize::new(bounds.2, bounds.3) + )).err().map(|e| format!("{:?}", e)); + let show_err = overlay.show().err().map(|e| format!("{:?}", e)); + let aot_err = overlay.set_always_on_top(true).err().map(|e| format!("{:?}", e)); set_overlay_interactivity(&overlay, is_fallback); + + // Re-snapshot to confirm visibility actually changed + let now_visible = overlay.is_visible().unwrap_or(false); + let now_outer_pos = overlay.outer_position() + .map(|p| format!("{},{}", p.x, p.y)) + .unwrap_or_else(|_| "err".to_string()); + + let any_err = pos_err.is_some() || size_err.is_some() + || show_err.is_some() || aot_err.is_some(); + let level = if any_err { "WARN" } else { "INFO" }; + write_log_line(&format!( + "[overlay-diag] show end [{}] now_visible={} now_outer_pos={} \ + pos_err={:?} size_err={:?} show_err={:?} aot_err={:?}", + level, now_visible, now_outer_pos, + pos_err, size_err, show_err, aot_err, + )); } else { + write_log_line(&format!( + "[overlay-diag] show begin handle=MISSING layout={:?} fallback={} \ + target_bounds={:?} monitor={}x{}@{:.2} — recreating", + layout, is_fallback, bounds, mon_w, mon_h, mon_scale, + )); self.create_overlay(app, is_fallback, base_w); } } pub fn hide_overlay(&self, app: &AppHandle) { // Reset to base layout when hiding - *self.overlay_layout.lock().unwrap() = OverlayLayout::Base; + let prev_layout = { + let mut g = self.overlay_layout.lock().unwrap(); + let prev = g.clone(); + *g = OverlayLayout::Base; + prev + }; if let Some(overlay) = app.get_webview_window("overlay") { set_overlay_interactivity(&overlay, false); - let _ = overlay.hide(); + let was_visible = overlay.is_visible().unwrap_or(false); + let hide_err = overlay.hide().err().map(|e| format!("{:?}", e)); + write_log_line(&format!( + "[overlay-diag] hide prev_layout={:?} was_visible={} hide_err={:?}", + prev_layout, was_visible, hide_err, + )); + } else { + write_log_line(&format!( + "[overlay-diag] hide handle=MISSING prev_layout={:?}", + prev_layout, + )); } } @@ -120,11 +206,21 @@ impl WindowState { match builder.build() { Ok(overlay) => { set_overlay_interactivity(&overlay, is_fallback); - let _ = overlay.show(); + let show_err = overlay.show().err().map(|e| format!("{:?}", e)); + let now_visible = overlay.is_visible().unwrap_or(false); + write_log_line(&format!( + "[overlay-diag] create_overlay OK is_fallback={} bounds={:?} \ + show_err={:?} now_visible={}", + is_fallback, bounds, show_err, now_visible, + )); } Err(e) => { eprintln!("[overlay] create_overlay: FAILED: {}", e); log::error!("Failed to create overlay window: {}", e); + write_log_line(&format!( + "[overlay-diag] create_overlay FAILED is_fallback={} bounds={:?} err={:?}", + is_fallback, bounds, e, + )); } } } @@ -134,6 +230,17 @@ fn set_overlay_interactivity(overlay: &tauri::WebviewWindow, interactive: bool) let _ = overlay.set_ignore_cursor_events(!interactive); } +/// Returns (logical_width, logical_height, scale_factor) of primary monitor. +fn monitor_info(app: &AppHandle) -> (f64, f64, f64) { + if let Some(monitor) = app.primary_monitor().ok().flatten() { + let size = monitor.size(); + let scale = monitor.scale_factor(); + (size.width as f64 / scale, size.height as f64 / scale, scale) + } else { + (1920.0, 1080.0, 1.0) + } +} + /// Returns (x, y, width, height) fn calc_overlay_bounds(app: &AppHandle, is_fallback: bool, base_w: f64) -> (f64, f64, f64, f64) { let (desired_w, desired_h) = if is_fallback { diff --git a/client/src/features/settings/AppearancePage.tsx b/client/src/features/settings/AppearancePage.tsx index c579104..31f431b 100644 --- a/client/src/features/settings/AppearancePage.tsx +++ b/client/src/features/settings/AppearancePage.tsx @@ -1,12 +1,12 @@ // 外观设置页面 — 主题 + 悬浮窗样式 + 预览 -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { Card, CardContent } from '@/components/ui/card' import { Switch } from '@/components/ui/switch' import { themeList } from '@/themes' import { switchTheme, getActiveThemeId } from '@/stores/theme' import { getSetting, setSetting } from '@/services/store' -import { refreshRecorderSettings } from '@/services/recorder' +import { refreshOverlaySettings } from '@/services/recorder' import { OVERLAY_WIDTH_PRESETS, type OverlayWidthPreset } from '@/services/recorder/types' import { type OverlayWaveTheme } from './utils' @@ -46,26 +46,37 @@ function getTimerColor(theme: OverlayWaveTheme): string { } function OverlayPreview({ theme, showDuration, barCount }: { theme: OverlayWaveTheme; showDuration: boolean; barCount: number }) { - const [bars, setBars] = useState(Array.from({ length: barCount }, () => 3)) - - useEffect(() => { - setBars(Array.from({ length: barCount }, () => 3)) - }, [barCount]) + const barRefs = useRef>([]) useEffect(() => { + const heights = new Array(barCount).fill(3) let running = true - const animate = () => { + let rafId = 0 + let lastFrame = 0 + const FRAME_INTERVAL = 1000 / 30 // 30fps 足够流畅,且不占满主线程 + + const animate = (now: number) => { if (!running) return - setBars((prev) => - prev.map((v) => { + if (now - lastFrame >= FRAME_INTERVAL) { + lastFrame = now + for (let i = 0; i < heights.length; i++) { const target = 3 + Math.random() * 15 - return v + (target - v) * 0.15 - }), - ) - requestAnimationFrame(animate) + heights[i] = heights[i] + (target - heights[i]) * 0.15 + const el = barRefs.current[i] + if (el) { + const h = Math.min(18, Math.max(3, heights[i])) + el.style.height = `${h}px` + el.style.opacity = String(0.7 + (h / 18) * 0.3) + } + } + } + rafId = requestAnimationFrame(animate) + } + rafId = requestAnimationFrame(animate) + return () => { + running = false + cancelAnimationFrame(rafId) } - requestAnimationFrame(animate) - return () => { running = false } }, [barCount]) return ( @@ -73,17 +84,17 @@ function OverlayPreview({ theme, showDuration, barCount }: { theme: OverlayWaveT {/* 1:1 还原真实悬浮窗样式 */}
- {bars.map((height, index) => { - const h = Math.min(18, Math.max(3, height)) - const color = getBarColor(index, bars.length, theme) + {Array.from({ length: barCount }, (_, index) => { + const color = getBarColor(index, barCount, theme) return (
{ barRefs.current[index] = el }} className="w-[2.5px] rounded-full" style={{ backgroundColor: color, - height: `${h}px`, - opacity: 0.7 + (h / 18) * 0.3, + height: '3px', + opacity: 0.7, transition: 'height 50ms ease-out, opacity 50ms ease-out', }} /> @@ -130,20 +141,20 @@ export default function AppearancePage() { const handleOverlayThemeChange = async (theme: OverlayWaveTheme) => { setOverlayWaveTheme(theme) await setSetting('overlayWaveTheme', theme) - await refreshRecorderSettings() + await refreshOverlaySettings() } const handleToggleDuration = async () => { const next = !overlayShowDuration setOverlayShowDuration(next) await setSetting('overlayShowDuration', next) - await refreshRecorderSettings() + await refreshOverlaySettings() } const handleOverlayWidthChange = async (preset: OverlayWidthPreset) => { setOverlayWidth(preset) await setSetting('overlayWidth', preset) - await refreshRecorderSettings() + await refreshOverlaySettings() } return ( diff --git a/client/src/overlay/main.tsx b/client/src/overlay/main.tsx index 7e8deea..b549f6e 100644 --- a/client/src/overlay/main.tsx +++ b/client/src/overlay/main.tsx @@ -1,6 +1,8 @@ import { startWebviewKeyboardFallback } from '../services/webviewKeyboardFallback' import React from 'react' import ReactDOM from 'react-dom/client' +import { listen } from '@tauri-apps/api/event' +import { invoke } from '@tauri-apps/api/core' import Overlay from './Overlay' import '../index.css' @@ -11,6 +13,13 @@ document.head.appendChild(style) void startWebviewKeyboardFallback() +// Health check: respond to ping from main process so it can detect +// WebView2 unresponsiveness. Bug 003 diagnostic. +void listen('overlay-ping', (event) => { + const seq = typeof event.payload === 'number' ? event.payload : 0 + void invoke('overlay_pong', { seq }).catch(() => {}) +}) + ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/client/src/pages/About.tsx b/client/src/pages/About.tsx index 09bfb37..9188981 100644 --- a/client/src/pages/About.tsx +++ b/client/src/pages/About.tsx @@ -44,7 +44,7 @@ export default function About() { v{currentVersion}