This commit is contained in:
949036910@qq.com
2026-05-29 23:16:42 +08:00
parent 595820c24a
commit 30e019d345
15 changed files with 808 additions and 86 deletions

View File

@@ -3,6 +3,8 @@
use base64::{engine::general_purpose::STANDARD, Engine as _};
use std::path::{Component, Path, PathBuf};
use tauri::Manager;
use crate::ffmpeg;
fn normalize_path(path: &str) -> PathBuf {
@@ -28,6 +30,8 @@ fn is_allowed_local_media(path: &Path) -> bool {
|| lower.contains("aiclient-cover-previews")
|| lower.contains("video-material")
|| lower.contains("avatars")
|| lower.contains("voice_clones")
|| lower.contains("aiclient-voice-ref")
{
return true;
}
@@ -126,6 +130,119 @@ pub async fn copy_local_file(source_path: String, dest_path: String) -> Result<(
Ok(())
}
/// 将 base64 写入受信任目录下的二进制文件(如录音 wav
#[tauri::command]
pub async fn write_local_file_base64(path: String, base64_data: String) -> Result<(), String> {
let dest = normalize_path(path.trim());
if dest.as_os_str().is_empty() {
return Err("路径为空".into());
}
if !is_allowed_local_media(&dest) {
return Err("不允许写入该路径".into());
}
if let Some(parent) = dest.parent() {
if !parent.as_os_str().is_empty() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("创建目录失败: {e}"))?;
}
}
let bytes = STANDARD
.decode(base64_data.trim())
.map_err(|e| format!("base64 解码失败: {e}"))?;
tokio::fs::write(&dest, &bytes)
.await
.map_err(|e| format!("保存文件失败: {e}"))?;
Ok(())
}
fn voice_clones_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
let dir = app
.path()
.app_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?
.join("voice_clones");
Ok(dir)
}
/// 生成临时参考音频路径(录音/上传中转)。
#[tauri::command]
pub async fn temp_voice_reference_path(ext: String) -> Result<String, String> {
let ext = ext.trim().trim_start_matches('.');
let ext = if ext.is_empty() { "webm" } else { ext };
let dir = std::env::temp_dir().join("aiclient-voice-ref");
tokio::fs::create_dir_all(&dir)
.await
.map_err(|e| format!("创建临时目录失败: {e}"))?;
let name = format!(
"ref_{}_{}.{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0),
rand_simple(),
ext
);
Ok(dir.join(name).to_string_lossy().into_owned())
}
fn rand_simple() -> u32 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
std::time::SystemTime::now().hash(&mut h);
(h.finish() & 0xffff) as u32
}
/// 将参考音频复制到应用数据目录 `voice_clones/{voice_id}{ext}`。
#[tauri::command]
pub async fn import_voice_clone_reference(
app: tauri::AppHandle,
source_path: String,
voice_id: String,
) -> Result<String, String> {
let source = normalize_path(source_path.trim());
if !source.is_file() {
return Err(format!("源文件不存在: {}", source.display()));
}
let allowed_source = is_allowed_local_media(&source)
|| source
.to_string_lossy()
.to_lowercase()
.contains("voice_clones");
if !allowed_source {
if let Ok(temp) = std::env::temp_dir().canonicalize() {
if let Ok(canonical) = source.canonicalize() {
if !canonical.starts_with(&temp) {
return Err("不允许导入该路径下的文件".into());
}
} else {
return Err("不允许导入该路径下的文件".into());
}
} else {
return Err("不允许导入该路径下的文件".into());
}
}
let id = voice_id.trim();
if id.is_empty() {
return Err("voice_id 为空".into());
}
let ext = source
.extension()
.and_then(|e| e.to_str())
.unwrap_or("wav");
let ext = if ext.is_empty() { "wav" } else { ext };
let dir = voice_clones_dir(&app)?;
tokio::fs::create_dir_all(&dir)
.await
.map_err(|e| format!("创建目录失败: {e}"))?;
let dest = dir.join(format!("{id}.{ext}"));
tokio::fs::copy(&source, &dest)
.await
.map_err(|e| format!("保存参考音频失败: {e}"))?;
Ok(dest.to_string_lossy().into_owned())
}
/// 将文本写入用户指定路径(供 ASR 结果「另存为」)。
#[tauri::command]
pub async fn write_local_text_file(path: String, content: String) -> Result<(), String> {

View File

@@ -132,9 +132,12 @@ pub fn run() {
commands::nodejs::list_nodejs_scripts,
commands::nodejs::stop_nodejs_script,
commands::fs_util::read_local_file_base64,
commands::fs_util::write_local_file_base64,
commands::fs_util::get_media_duration_seconds,
commands::fs_util::copy_local_file,
commands::fs_util::write_local_text_file,
commands::fs_util::temp_voice_reference_path,
commands::fs_util::import_voice_clone_reference,
commands::fs_util::reveal_local_file_in_folder,
commands::avatar::list_avatars,
commands::avatar::insert_avatar,