不重要了

This commit is contained in:
kura 2026-03-19 14:50:22 +08:00
parent 2a057e6917
commit ab28fd225f
414 changed files with 26461 additions and 40 deletions

View File

@ -43,7 +43,7 @@ npm install
```bash
export OPENAI_API_BASE=https://your-openai-compatible-endpoint/v1
export OPENAI_API_KEY=your_api_key
export OPENAI_MODEL=gpt-4o-mini
export OPENAI_MODEL=GLM-4-Flash-250414
```
6. 若要真正启用 ONNX Runtime 推理,请确保本机存在可被 `ort` 动态加载的 ONNX Runtime 库,或按你的部署方式提供运行库。

View File

@ -5,9 +5,14 @@
"type": "module",
"scripts": {
"dev": "vite",
"tauri": "tauri dev",
"tauri-dev": "tauri dev",
"tauri": "tauri",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
"preview": "vite preview",
"prepare-ffmpeg-macos": "sh ./scripts/prepare-bundled-ffmpeg.sh",
"prepare-licenses-macos": "python3 ./scripts/prepare-bundled-licenses.py",
"tauri-build-app": "npm run prepare-ffmpeg-macos && npm run prepare-licenses-macos && tauri build --bundles app",
"tauri-build-dmg": "sh ./scripts/build-macos-dmg.sh"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",

View File

@ -0,0 +1,35 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
APP_NAME="CrossSubtitle-AI"
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
ARCH=$(uname -m)
APP_PATH="$ROOT_DIR/src-tauri/target/release/bundle/macos/$APP_NAME.app"
DMG_DIR="$ROOT_DIR/src-tauri/target/release/bundle/dmg"
DMG_PATH="$DMG_DIR/${APP_NAME}_${VERSION}_${ARCH}.dmg"
STAGING_DIR=$(mktemp -d "${TMPDIR:-/tmp}/crosssubtitle-dmg.XXXXXX")
cleanup() {
rm -rf "$STAGING_DIR"
}
trap cleanup EXIT
cd "$ROOT_DIR"
npm run tauri-build-app
mkdir -p "$DMG_DIR"
rm -f "$DMG_PATH"
ditto "$APP_PATH" "$STAGING_DIR/$APP_NAME.app"
ln -s /Applications "$STAGING_DIR/Applications"
hdiutil create \
-volname "$APP_NAME" \
-srcfolder "$STAGING_DIR" \
-ov \
-format UDZO \
"$DMG_PATH"
printf 'DMG created: %s\n' "$DMG_PATH"

View File

@ -0,0 +1,156 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
TARGET_ARCH=$(uname -m)
FFMPEG_SOURCE=${FFMPEG_SOURCE_PATH:-$(command -v ffmpeg)}
VENDOR_ROOT="$ROOT_DIR/src-tauri/vendor/ffmpeg/macos-$TARGET_ARCH"
BIN_DIR="$VENDOR_ROOT/bin"
LIB_DIR="$VENDOR_ROOT/lib"
MARK_DIR="$VENDOR_ROOT/.processed"
rm -rf "$VENDOR_ROOT"
mkdir -p "$BIN_DIR" "$LIB_DIR" "$MARK_DIR"
copy_binary() {
src_path=$1
dest_path=$2
cp -fL "$src_path" "$dest_path"
chmod 755 "$dest_path"
}
resolve_dep_source() {
file_path=$1
dep_path=$2
case "$dep_path" in
/System/*|/usr/lib/*)
return 1
;;
/*)
if [ -f "$dep_path" ]; then
printf '%s\n' "$dep_path"
return 0
fi
;;
@loader_path/*)
candidate="$(dirname "$file_path")/$(basename "$dep_path")"
if [ -f "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
;;
@executable_path/*)
candidate="$LIB_DIR/$(basename "$dep_path")"
if [ -f "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
;;
@rpath/*)
dep_name=$(basename "$dep_path")
for candidate in \
"$LIB_DIR/$dep_name" \
"/opt/homebrew/lib/$dep_name" \
$(find /opt/homebrew/Cellar -path "*/lib/$dep_name" -print 2>/dev/null)
do
if [ -n "$candidate" ] && [ -f "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
;;
esac
return 1
}
marker_path() {
printf '%s' "$1" | shasum -a 1 | awk '{print "'"$MARK_DIR"'/" $1 ".done"}'
}
process_file() {
file_path=$1
loader_prefix=$2
mark_file=$(marker_path "$file_path")
if [ -f "$mark_file" ]; then
return
fi
: > "$mark_file"
otool -L "$file_path" | tail -n +2 | awk '{print $1}' | while read -r dep_path; do
resolved_dep=$(resolve_dep_source "$file_path" "$dep_path" || true)
if [ -z "$resolved_dep" ]; then
continue
fi
dep_name=$(basename "$resolved_dep")
vendored_dep="$LIB_DIR/$dep_name"
if [ ! -f "$vendored_dep" ]; then
copy_binary "$resolved_dep" "$vendored_dep"
fi
install_name_tool -change "$dep_path" "$loader_prefix/$dep_name" "$file_path"
process_file "$vendored_dep" "@loader_path"
done
if [ "$loader_prefix" = "@loader_path" ]; then
install_name_tool -id "@loader_path/$(basename "$file_path")" "$file_path"
fi
}
normalize_file() {
file_path=$1
loader_prefix=$2
otool -L "$file_path" | tail -n +2 | awk '{print $1}' | while read -r dep_path; do
case "$dep_path" in
/System/*|/usr/lib/*)
continue
;;
@executable_path/*|@loader_path/*)
continue
;;
esac
dep_name=$(basename "$dep_path")
vendored_dep="$LIB_DIR/$dep_name"
if [ -f "$vendored_dep" ]; then
install_name_tool -change "$dep_path" "$loader_prefix/$dep_name" "$file_path"
fi
done
if [ "$loader_prefix" = "@loader_path" ]; then
install_name_tool -id "@loader_path/$(basename "$file_path")" "$file_path"
fi
}
sign_file() {
file_path=$1
codesign --force --sign - --timestamp=none "$file_path" >/dev/null 2>&1
}
echo "Bundling ffmpeg from: $FFMPEG_SOURCE"
copy_binary "$FFMPEG_SOURCE" "$BIN_DIR/ffmpeg"
process_file "$BIN_DIR/ffmpeg" "@executable_path/../lib"
normalize_file "$BIN_DIR/ffmpeg" "@executable_path/../lib"
find "$LIB_DIR" -type f -name '*.dylib' | while read -r dylib_path; do
normalize_file "$dylib_path" "@loader_path"
done
find "$LIB_DIR" -type f -name '*.dylib' | while read -r dylib_path; do
normalize_file "$dylib_path" "@loader_path"
done
find "$LIB_DIR" -type f -name '*.dylib' | while read -r dylib_path; do
sign_file "$dylib_path"
done
sign_file "$BIN_DIR/ffmpeg"
echo "Bundled ffmpeg ready at: $VENDOR_ROOT"

View File

@ -0,0 +1,119 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import shutil
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parent.parent
CELLAR_DIR = Path("/opt/homebrew/Cellar")
LICENSE_ROOT = ROOT_DIR / "src-tauri" / "vendor" / "licenses" / "homebrew"
LICENSE_BUNDLE_ROOT = ROOT_DIR / "src-tauri" / "vendor" / "licenses_bundle"
FFMPEG_RECEIPT = sorted(CELLAR_DIR.glob("ffmpeg/*/INSTALL_RECEIPT.json"))[-1]
LICENSE_PATTERNS = [
"LICENSE*",
"LICENCE*",
"COPYING*",
"COPYRIGHT*",
"NOTICE*",
]
def load_runtime_formulas() -> list[tuple[str, str | None]]:
data = json.loads(FFMPEG_RECEIPT.read_text())
formulas: list[tuple[str, str | None]] = [("ffmpeg", None)]
for item in data.get("runtime_dependencies", []):
full_name = item.get("full_name")
version = item.get("version")
if full_name:
formulas.append((full_name, version))
return formulas
def resolve_formula_dir(name: str, version: str | None) -> Path | None:
if version:
exact = CELLAR_DIR / name / version
if exact.exists():
return exact
matches = sorted((CELLAR_DIR / name).glob("*"))
return matches[-1] if matches else None
def collect_license_files(formula_dir: Path) -> list[Path]:
found: list[Path] = []
seen: set[Path] = set()
for pattern in LICENSE_PATTERNS:
for path in sorted(formula_dir.rglob(pattern)):
if path.is_file() and path not in seen:
seen.add(path)
found.append(path)
return found
def main() -> None:
if LICENSE_ROOT.exists():
shutil.rmtree(LICENSE_ROOT)
LICENSE_ROOT.mkdir(parents=True, exist_ok=True)
LICENSE_BUNDLE_ROOT.mkdir(parents=True, exist_ok=True)
manifest_lines = [
"CrossSubtitle-AI Third-Party Notices",
"",
"This directory contains license files collected from Homebrew formulas",
"used by the bundled ffmpeg binary and its runtime dependencies.",
"",
]
for formula_name, version in load_runtime_formulas():
formula_dir = resolve_formula_dir(formula_name, version)
target_dir = LICENSE_ROOT / formula_name.replace("@", "_at_")
target_dir.mkdir(parents=True, exist_ok=True)
target_dir.chmod(0o755)
if formula_dir is None:
manifest_lines.append(f"- {formula_name}: formula directory not found")
continue
copied: list[str] = []
for license_file in collect_license_files(formula_dir):
destination = target_dir / license_file.name
if destination.exists():
destination.chmod(0o644)
shutil.copy2(license_file, destination)
destination.chmod(0o644)
copied.append(license_file.name)
actual_version = formula_dir.name
if copied:
manifest_lines.append(
f"- {formula_name} ({actual_version}): {', '.join(sorted(copied))}"
)
else:
note = target_dir / "MISSING_LICENSE.txt"
note.write_text(
f"No license file matching common patterns was found in {formula_dir}\n"
)
manifest_lines.append(
f"- {formula_name} ({actual_version}): no matching license file found"
)
manifest_path = LICENSE_ROOT / "THIRD_PARTY_NOTICES.txt"
manifest_path.write_text("\n".join(manifest_lines) + "\n")
manifest_path.chmod(0o644)
bundle_manifest = LICENSE_BUNDLE_ROOT / "THIRD_PARTY_NOTICES.txt"
shutil.copy2(manifest_path, bundle_manifest)
bundle_manifest.chmod(0o644)
archive_base = LICENSE_BUNDLE_ROOT / "homebrew-licenses"
archive_path = shutil.make_archive(str(archive_base), "zip", LICENSE_ROOT)
Path(archive_path).chmod(0o644)
print(f"Prepared bundled licenses at: {LICENSE_ROOT}")
print(f"Prepared bundled license archive at: {archive_path}")
if __name__ == "__main__":
main()

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 B

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -9,12 +9,19 @@ use anyhow::{anyhow, Context, Result};
pub struct AudioPipeline;
impl AudioPipeline {
pub fn extract_to_wav(input_path: &str, workspace: &Path) -> Result<PathBuf> {
pub fn extract_to_wav(ffmpeg_path: &Path, input_path: &str, workspace: &Path) -> Result<PathBuf> {
fs::create_dir_all(workspace)
.with_context(|| format!("failed to create workspace: {}", workspace.display()))?;
let output_path = workspace.join("normalized.wav");
let status = Command::new("ffmpeg")
let mut command = Command::new(ffmpeg_path);
if let Some(lib_dir) = ffmpeg_path.parent().and_then(|bin_dir| bin_dir.parent()).map(|root| root.join("lib")) {
if lib_dir.exists() {
command.env("DYLD_FALLBACK_LIBRARY_PATH", &lib_dir);
}
}
let output = command
.arg("-y")
.arg("-i")
.arg(input_path)
@ -25,11 +32,19 @@ impl AudioPipeline {
.arg("-f")
.arg("wav")
.arg(&output_path)
.status()
.context("failed to launch ffmpeg, please install ffmpeg and ensure it is in PATH")?;
.output()
.with_context(|| format!("failed to launch ffmpeg: {}", ffmpeg_path.display()))?;
if !status.success() {
return Err(anyhow!("ffmpeg exited with status: {status}"));
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if stderr.is_empty() {
return Err(anyhow!("ffmpeg exited with status: {}", output.status));
}
return Err(anyhow!(
"ffmpeg exited with status: {} | stderr: {}",
output.status,
stderr
));
}
Ok(output_path)

View File

@ -7,7 +7,7 @@ mod translate;
mod vad;
mod whisper;
use models::{StartTaskPayload, SubtitleSegment, SubtitleTask};
use models::{DefaultModelPaths, StartTaskPayload, SubtitleSegment, SubtitleTask};
use state::AppState;
use tauri::{
menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder},
@ -59,6 +59,11 @@ fn export_subtitles(
task::export_task(state, task_id, format).map_err(error_to_string)
}
#[tauri::command]
fn get_default_model_paths(app: tauri::AppHandle) -> std::result::Result<DefaultModelPaths, String> {
task::get_default_model_paths(&app).map_err(error_to_string)
}
fn error_to_string(error: anyhow::Error) -> String {
format!("{error:#}")
}
@ -78,7 +83,8 @@ pub fn run() {
start_subtitle_task,
list_tasks,
update_segment_text,
export_subtitles
export_subtitles,
get_default_model_paths
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -111,3 +111,10 @@ pub struct TranslationConfig {
pub batch_size: usize,
pub context_size: usize,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DefaultModelPaths {
pub whisper_model_path: String,
pub vad_model_path: String,
}

View File

@ -1,6 +1,6 @@
use std::{
fs,
path::PathBuf,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
@ -10,8 +10,8 @@ use uuid::Uuid;
use crate::{
audio::AudioPipeline,
models::{
ErrorEvent, LogEvent, OutputMode, ProgressEvent, ResetSegmentsEvent, StartTaskPayload,
SubtitleSegment, SubtitleTask, TaskStatus, TranslationConfig,
DefaultModelPaths, ErrorEvent, LogEvent, OutputMode, ProgressEvent, ResetSegmentsEvent,
StartTaskPayload, SubtitleSegment, SubtitleTask, TaskStatus, TranslationConfig,
},
state::AppState,
subtitle::{render, SubtitleFormat},
@ -20,10 +20,12 @@ use crate::{
whisper::WhisperEngine,
};
const DEFAULT_WHISPER_MODEL: &str =
"/Users/kura/Documents/work/tauri/CrossSubtitle/src-tauri/model/ggml-small-q5_1.bin";
const DEFAULT_VAD_MODEL: &str =
"/Users/kura/Documents/work/tauri/CrossSubtitle/src-tauri/model/silero_vad.onnx";
const DEFAULT_WHISPER_MODEL: &str = "model/ggml-small-q5_1.bin";
const DEFAULT_VAD_MODEL: &str = "model/silero_vad.onnx";
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
const DEFAULT_FFMPEG_BINARY: &str = "vendor/ffmpeg/macos-arm64/bin/ffmpeg";
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
const DEFAULT_FFMPEG_BINARY: &str = "vendor/ffmpeg/macos-x86_64/bin/ffmpeg";
pub async fn start_task(
app: tauri::AppHandle,
@ -32,10 +34,10 @@ pub async fn start_task(
mut payload: StartTaskPayload,
) -> Result<SubtitleTask> {
if payload.whisper_model_path.as_deref().is_none_or(str::is_empty) {
payload.whisper_model_path = Some(DEFAULT_WHISPER_MODEL.to_string());
payload.whisper_model_path = resolve_default_model_path(&app, DEFAULT_WHISPER_MODEL);
}
if payload.vad_model_path.as_deref().is_none_or(str::is_empty) {
payload.vad_model_path = Some(DEFAULT_VAD_MODEL.to_string());
payload.vad_model_path = resolve_default_model_path(&app, DEFAULT_VAD_MODEL);
}
if payload.source_lang.as_deref().is_none_or(str::is_empty) {
payload.source_lang = Some("auto".to_string());
@ -83,6 +85,58 @@ pub async fn start_task(
Ok(task)
}
pub fn get_default_model_paths(app: &tauri::AppHandle) -> Result<DefaultModelPaths> {
let whisper_model_path = resolve_default_model_path(app, DEFAULT_WHISPER_MODEL)
.ok_or_else(|| anyhow::anyhow!("未找到内置 Whisper 模型: {}", DEFAULT_WHISPER_MODEL))?;
let vad_model_path = resolve_default_model_path(app, DEFAULT_VAD_MODEL)
.ok_or_else(|| anyhow::anyhow!("未找到内置 VAD 模型: {}", DEFAULT_VAD_MODEL))?;
Ok(DefaultModelPaths {
whisper_model_path,
vad_model_path,
})
}
fn resolve_default_model_path(app: &tauri::AppHandle, relative_path: &str) -> Option<String> {
if let Ok(resource_dir) = app.path().resource_dir() {
let bundled_path = resource_dir.join(relative_path);
if bundled_path.exists() {
return Some(bundled_path.to_string_lossy().to_string());
}
}
let local_fallback_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative_path);
if local_fallback_path.exists() {
return Some(local_fallback_path.to_string_lossy().to_string());
}
None
}
fn resolve_ffmpeg_path(app: &tauri::AppHandle) -> Option<PathBuf> {
if let Ok(resource_dir) = app.path().resource_dir() {
let bundled_path = resource_dir.join(DEFAULT_FFMPEG_BINARY);
if bundled_path.exists() {
return Some(bundled_path);
}
}
let local_bundled_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(DEFAULT_FFMPEG_BINARY);
if local_bundled_path.exists() {
return Some(local_bundled_path);
}
let path_var = std::env::var_os("PATH")?;
for directory in std::env::split_paths(&path_var) {
let candidate = directory.join("ffmpeg");
if candidate.exists() {
return Some(candidate);
}
}
None
}
async fn run_pipeline(
app: tauri::AppHandle,
window: Window,
@ -92,10 +146,13 @@ async fn run_pipeline(
let app_state = app.state::<AppState>();
let workspace = std::env::temp_dir().join("crosssubtitle-ai").join(&task.id);
let should_translate = matches!(payload.output_mode, OutputMode::Translate);
let ffmpeg_path = resolve_ffmpeg_path(&app)
.ok_or_else(|| anyhow::anyhow!("未找到可用 ffmpeg请重新执行打包命令或在系统中安装 ffmpeg"))?;
set_status(&window, &app_state, &mut task, TaskStatus::Extracting, 8.0, "正在抽取音频")?;
emit_log(&window, &task.id, format!("task: input file={}", payload.file_path))?;
let wav_path = AudioPipeline::extract_to_wav(&payload.file_path, &workspace)?;
emit_log(&window, &task.id, format!("audio: ffmpeg={}", ffmpeg_path.display()))?;
let wav_path = AudioPipeline::extract_to_wav(&ffmpeg_path, &payload.file_path, &workspace)?;
emit_log(&window, &task.id, format!("audio: normalized wav={}", wav_path.display()))?;
set_status(&window, &app_state, &mut task, TaskStatus::VadProcessing, 22.0, "正在分析语音片段")?;
@ -210,7 +267,7 @@ async fn run_pipeline(
fn load_translation_config() -> Option<TranslationConfig> {
let api_base = std::env::var("OPENAI_API_BASE").ok()?;
let api_key = std::env::var("OPENAI_API_KEY").ok()?;
let model = std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-4o-mini".to_string());
let model = std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "GLM-4-Flash-250414".to_string());
Some(TranslationConfig {
api_base,
api_key,

View File

@ -27,7 +27,16 @@
"bundle": {
"active": true,
"targets": "all",
"icon": [],
"icon": [
"icons/icon.png",
"icons/icon.icns"
],
"resources": [
"model/ggml-small-q5_1.bin",
"model/silero_vad.onnx",
"vendor/ffmpeg/**/*",
"vendor/licenses_bundle/**/*"
],
"macOS": {
"minimumSystemVersion": "10.15"
}

Some files were not shown because too many files have changed in this diff Show More