11
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user