fix: identifier改为cn.meijiaka.ai-zy + 清理audios.json + 修复临时文件残留
- 应用标识符从 cn.meijiaka.ai-jian 改为 cn.meijiaka.ai-zy - 删除 audios.json 相关代码(前后端 IPC/存储/状态) - 修复 video_processing.rs 临时文件路径:从 projects/ 根目录改为项目目录 - 修复 video_processing.rs rename/copy 双失败后文件残留(有音频/无音频两分支) - 修复 ffmpeg concat_videos_robust 标准化失败后 std_*.mp4 残留 - 修复 ffmpeg burn_ass_subtitle 首次失败原因被静默丢弃 - VideoGeneration Step1 截取片段上传后立即删除
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
**美家卡智影**是一款面向桌面端的 AI 视频创作应用,采用"Python 后端 API + Tauri 桌面前端"的混合架构。
|
||||
|
||||
- **产品标识**: `cn.meijiaka.ai-video` / `cn.meijiaka.ai-jian`
|
||||
- **产品标识**: `cn.meijiaka.ai-video` / `cn.meijiaka.ai-zy`
|
||||
- **版本**: `0.1.0`
|
||||
- **核心功能**: AI 脚本生成、AI 配音合成(TTS)、声音复刻、视频生成(Vidu)、视频字幕生成、压制成片(FFmpeg)、项目本地持久化
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ pub struct SaveAudioArgs {
|
||||
pub name: String,
|
||||
pub voice_id: String,
|
||||
pub duration: f64,
|
||||
pub skip_list: Option<bool>,
|
||||
}
|
||||
|
||||
/// 保存音频文件(前端传入 base64 编码)
|
||||
@@ -119,7 +118,6 @@ pub async fn save_audio(
|
||||
&args.name,
|
||||
&args.voice_id,
|
||||
args.duration,
|
||||
args.skip_list.unwrap_or(false),
|
||||
) {
|
||||
Ok(meta) => ApiResponse {
|
||||
code: 200,
|
||||
@@ -134,45 +132,6 @@ pub async fn save_audio(
|
||||
}
|
||||
}
|
||||
|
||||
/// 列出项目所有音频文件
|
||||
#[tauri::command]
|
||||
pub async fn list_project_audios(
|
||||
project_id: String,
|
||||
) -> ApiResponse<Vec<voice_storage::AudioMeta>> {
|
||||
match voice_storage::list_project_audios(&project_id) {
|
||||
Ok(audios) => ApiResponse {
|
||||
code: 200,
|
||||
message: "Audio list retrieved".to_string(),
|
||||
data: Some(audios),
|
||||
},
|
||||
Err(e) => ApiResponse {
|
||||
code: 500,
|
||||
message: format!("Failed to list audios: {}", e),
|
||||
data: Some(vec![]),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除音频文件
|
||||
#[tauri::command]
|
||||
pub async fn delete_audio(
|
||||
project_id: String,
|
||||
audio_id: String,
|
||||
) -> ApiResponse<bool> {
|
||||
match voice_storage::delete_audio_file(&project_id, &audio_id) {
|
||||
Ok(_) => ApiResponse {
|
||||
code: 200,
|
||||
message: "Audio deleted successfully".to_string(),
|
||||
data: Some(true),
|
||||
},
|
||||
Err(e) => ApiResponse {
|
||||
code: 500,
|
||||
message: format!("Failed to delete audio: {}", e),
|
||||
data: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取项目音频目录(供前端 FFmpeg 调用)
|
||||
#[tauri::command]
|
||||
pub async fn get_project_audios_dir(
|
||||
|
||||
@@ -196,8 +196,16 @@ pub async fn concat_videos_robust(app: &AppHandle, video_paths: Vec<String>, out
|
||||
// 1. 标准化每个片段(内部已验证路径)
|
||||
for (i, path) in video_paths.iter().enumerate() {
|
||||
let std_path = output_parent.join(format!("std_{}_{}.mp4", timestamp, i));
|
||||
standardize_video(app, path, std_path.to_str().unwrap()).await?;
|
||||
standardized_paths.push(std_path);
|
||||
match standardize_video(app, path, std_path.to_str().unwrap()).await {
|
||||
Ok(()) => standardized_paths.push(std_path),
|
||||
Err(e) => {
|
||||
// 清理本轮已创建的标准化文件
|
||||
for p in &standardized_paths {
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 生成 concat 列表
|
||||
@@ -443,7 +451,8 @@ pub async fn burn_ass_subtitle(
|
||||
|
||||
match run_ffmpeg(app, args).await {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(_e) => {
|
||||
Err(e) => {
|
||||
eprintln!("[ffmpeg] 带 fontsdir 的 ASS 烧录失败,尝试回退: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,8 +107,6 @@ pub fn run() {
|
||||
commands::product::export_product,
|
||||
// 音频管理
|
||||
commands::voice::save_audio,
|
||||
commands::voice::list_project_audios,
|
||||
commands::voice::delete_audio,
|
||||
commands::voice::get_project_audios_dir,
|
||||
commands::voice::extract_audio_segment,
|
||||
commands::voice::upload_audio_file,
|
||||
@@ -356,13 +354,17 @@ async fn video_composite_synthesis(
|
||||
app: tauri::AppHandle,
|
||||
request: VideoCompositeRequest,
|
||||
) -> ApiResponse<String> {
|
||||
let project_dir = match crate::storage::get_projects_root_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(e) => return ApiResponse {
|
||||
code: 500,
|
||||
message: format!("获取项目目录失败: {}", e),
|
||||
data: None,
|
||||
},
|
||||
// 从 output_path 解析项目目录(格式:.../projects/{project_id}/products/{filename})
|
||||
let output_path = std::path::PathBuf::from(&request.output_path);
|
||||
let project_dir = match output_path.parent().and_then(|p| p.parent()) {
|
||||
Some(dir) => dir.to_path_buf(),
|
||||
None => {
|
||||
return ApiResponse {
|
||||
code: 500,
|
||||
message: "无法从输出路径解析项目目录".to_string(),
|
||||
data: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
let payload = serde_json::json!({
|
||||
"videoPaths": request.video_paths,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Voice 音频文件存储模块
|
||||
//!
|
||||
//! 管理 TTS 合成音频和配音文件的本地存储。
|
||||
//! 管理 TTS 合成音频文件的本地存储。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::storage::engine::{atomic_write_bytes, atomic_write_json, read_json, ensure_dir, StorageError};
|
||||
use crate::storage::paths::{get_project_dir, get_voices_json_path};
|
||||
use crate::storage::engine::{atomic_write_bytes, read_json, ensure_dir, StorageError};
|
||||
use crate::storage::paths::get_project_dir;
|
||||
|
||||
/// 音频文件元数据
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -22,15 +22,6 @@ pub struct AudioMeta {
|
||||
pub segment_id: Option<String>,
|
||||
}
|
||||
|
||||
/// 音频列表元数据文件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AudioListMeta {
|
||||
pub project_id: String,
|
||||
pub audios: Vec<AudioMeta>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// 保存音频文件到项目目录
|
||||
///
|
||||
/// 将 base64 编码的音频数据写入 `audios/` 子目录。
|
||||
@@ -41,7 +32,6 @@ pub fn save_audio_file(
|
||||
name: &str,
|
||||
voice_id: &str,
|
||||
duration: f64,
|
||||
skip_list: bool,
|
||||
) -> Result<AudioMeta, StorageError> {
|
||||
let project_dir = get_project_dir(project_id)?;
|
||||
let audios_dir = project_dir.join("audios");
|
||||
@@ -64,75 +54,9 @@ pub fn save_audio_file(
|
||||
segment_id: None,
|
||||
};
|
||||
|
||||
// 仅在非 skip_list 模式下更新音频列表元数据(audios.json 用于素材库)
|
||||
if !skip_list {
|
||||
let list = load_audio_list_meta(project_id)?;
|
||||
let mut list = list.unwrap_or(AudioListMeta {
|
||||
project_id: project_id.to_string(),
|
||||
audios: vec![],
|
||||
updated_at: String::new(),
|
||||
});
|
||||
|
||||
// 替换已存在的同名音频
|
||||
if let Some(pos) = list.audios.iter().position(|a| a.id == audio_id) {
|
||||
list.audios[pos] = meta.clone();
|
||||
} else {
|
||||
list.audios.push(meta.clone());
|
||||
}
|
||||
list.updated_at = chrono_lite_now();
|
||||
|
||||
save_audio_list_meta(project_id, &list)?;
|
||||
}
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
/// 列出项目所有音频文件
|
||||
pub fn list_project_audios(project_id: &str) -> Result<Vec<AudioMeta>, StorageError> {
|
||||
let list = load_audio_list_meta(project_id)?;
|
||||
Ok(list.map(|l| l.audios).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// 删除项目中的音频文件
|
||||
pub fn delete_audio_file(project_id: &str, audio_id: &str) -> Result<(), StorageError> {
|
||||
let list = load_audio_list_meta(project_id)?;
|
||||
let mut list = list.ok_or_else(|| StorageError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"音频列表不存在",
|
||||
)))?;
|
||||
|
||||
let pos = list.audios.iter().position(|a| a.id == audio_id)
|
||||
.ok_or_else(|| StorageError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("音频 {} 不存在", audio_id),
|
||||
)))?;
|
||||
|
||||
let meta = list.audios.remove(pos);
|
||||
let path = Path::new(&meta.file_path);
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
|
||||
list.updated_at = chrono_lite_now();
|
||||
save_audio_list_meta(project_id, &list)
|
||||
}
|
||||
|
||||
/// 获取音频列表元数据文件路径
|
||||
fn get_audio_list_path(project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let project_dir = get_project_dir(project_id)?;
|
||||
Ok(project_dir.join("audios.json"))
|
||||
}
|
||||
|
||||
fn load_audio_list_meta(project_id: &str) -> Result<Option<AudioListMeta>, StorageError> {
|
||||
let path = get_audio_list_path(project_id)?;
|
||||
read_json(&path)
|
||||
}
|
||||
|
||||
fn save_audio_list_meta(project_id: &str, list: &AudioListMeta) -> Result<(), StorageError> {
|
||||
let path = get_audio_list_path(project_id)?;
|
||||
atomic_write_json(&path, list)
|
||||
}
|
||||
|
||||
/// 获取项目音频目录路径(供 FFmpeg 使用)
|
||||
pub fn get_project_audios_dir(project_id: &str) -> Result<PathBuf, StorageError> {
|
||||
let project_dir = get_project_dir(project_id)?;
|
||||
@@ -172,14 +96,14 @@ pub struct VoiceMaterialsList {
|
||||
|
||||
/// 加载音色素材库
|
||||
pub fn load_voice_materials() -> Result<VoiceMaterialsList, StorageError> {
|
||||
let path = get_voices_json_path()?;
|
||||
let path = crate::storage::paths::get_voices_json_path()?;
|
||||
Ok(read_json(&path)?.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// 保存音色素材库
|
||||
pub fn save_voice_materials(list: &VoiceMaterialsList) -> Result<(), StorageError> {
|
||||
let path = get_voices_json_path()?;
|
||||
atomic_write_json(&path, list)
|
||||
let path = crate::storage::paths::get_voices_json_path()?;
|
||||
crate::storage::engine::atomic_write_json(&path, list)
|
||||
}
|
||||
|
||||
/// 添加音色素材
|
||||
|
||||
@@ -96,6 +96,10 @@ pub async fn handle_video_synthesis(
|
||||
let _ = std::fs::remove_file(&final_output);
|
||||
})
|
||||
});
|
||||
// 兜底:如果 rename 和 copy 都失败,强制删除 final_output
|
||||
if move_result.is_err() {
|
||||
let _ = std::fs::remove_file(&final_output);
|
||||
}
|
||||
// 清理临时文件
|
||||
let _ = std::fs::remove_file(&concat_output);
|
||||
if let Some(ref p) = temp_cover_video {
|
||||
@@ -136,6 +140,10 @@ pub async fn handle_video_synthesis(
|
||||
let _ = std::fs::remove_file(&concat_output);
|
||||
})
|
||||
});
|
||||
// 兜底:如果 rename 和 copy 都失败,强制删除 concat_output
|
||||
if move_result.is_err() {
|
||||
let _ = std::fs::remove_file(&concat_output);
|
||||
}
|
||||
if let Some(ref p) = temp_cover_video {
|
||||
let _ = std::fs::remove_file(p);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "美家卡智影",
|
||||
"version": "0.1.0",
|
||||
"identifier": "cn.meijiaka.ai-jian",
|
||||
"identifier": "cn.meijiaka.ai-zy",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
|
||||
@@ -162,7 +162,6 @@ export async function saveAudio(args: {
|
||||
name: string;
|
||||
voiceId: string;
|
||||
duration: number;
|
||||
skipList?: boolean;
|
||||
}): Promise<AudioMeta> {
|
||||
const result = await invoke<{ code: number; data?: AudioMeta; message: string }>('save_audio', { args });
|
||||
if (result.code !== 200 || !result.data) {
|
||||
@@ -171,23 +170,6 @@ export async function saveAudio(args: {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/** 列出项目音频文件 */
|
||||
export async function listProjectAudios(projectId: string): Promise<AudioMeta[]> {
|
||||
const result = await invoke<{ code: number; data?: AudioMeta[]; message: string }>('list_project_audios', { projectId });
|
||||
if (result.code !== 200) {
|
||||
throw new Error(result.message || '获取音频列表失败');
|
||||
}
|
||||
return result.data || [];
|
||||
}
|
||||
|
||||
/** 删除音频文件 */
|
||||
export async function deleteAudio(projectId: string, audioId: string): Promise<void> {
|
||||
const result = await invoke<{ code: number; message: string }>('delete_audio', { projectId, audioId });
|
||||
if (result.code !== 200) {
|
||||
throw new Error(result.message || '删除音频失败');
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 音频处理命令(Tauri IPC) ======================
|
||||
|
||||
interface ExtractAudioSegmentRequest {
|
||||
|
||||
@@ -633,6 +633,16 @@ export default function VideoGeneration() {
|
||||
// 1c. 上传切割后的视频到七牛云
|
||||
const clipUrl = await uploadVideoFile(clipPath);
|
||||
|
||||
// 上传成功后删除本地临时截取片段
|
||||
try {
|
||||
await invoke<{ code: number; message: string }>('delete_project_file', {
|
||||
projectId,
|
||||
filePath: clipPath,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[VideoGeneration] 删除临时截取片段失败: ${clipPath}`, e);
|
||||
}
|
||||
|
||||
// 1d. 提交视频生成任务(仅当该分镜有 clipAudioUrl 时)
|
||||
if (!shot.clipAudioUrl) {
|
||||
console.warn(`[VideoGeneration] Segment ${shot.id} 无 clipAudioUrl,跳过视频生成`);
|
||||
|
||||
@@ -40,8 +40,6 @@ export default function VoiceSynthesis() {
|
||||
voiceMaterials,
|
||||
loadPresetVoices,
|
||||
loadVoiceMaterials,
|
||||
loadProjectAudios,
|
||||
setAudioMapping,
|
||||
} = useVoiceStore();
|
||||
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
@@ -59,8 +57,7 @@ export default function VoiceSynthesis() {
|
||||
useEffect(() => {
|
||||
loadPresetVoices();
|
||||
loadVoiceMaterials();
|
||||
if (projectId) {loadProjectAudios(projectId);}
|
||||
}, [loadPresetVoices, loadProjectAudios, loadVoiceMaterials, projectId]);
|
||||
}, [loadPresetVoices, loadVoiceMaterials, projectId]);
|
||||
|
||||
// 组件卸载时清理音频播放
|
||||
useEffect(() => {
|
||||
@@ -314,7 +311,6 @@ export default function VoiceSynthesis() {
|
||||
const meta = await saveAudio({
|
||||
projectId, audioId, audioData: base64,
|
||||
name: `配音合成-${segments.length}段`, voiceId: currentVoiceId || 'tianxin_xiaoling', duration: 0,
|
||||
skipList: true,
|
||||
});
|
||||
|
||||
// 更新 projectStore 和 meta.json(项目级配音信息)
|
||||
@@ -327,14 +323,6 @@ export default function VoiceSynthesis() {
|
||||
dubbingAudioPath: meta.filePath,
|
||||
});
|
||||
|
||||
// 配音合成音频是项目级别的,不写入各分镜
|
||||
for (const seg of segments) {
|
||||
const segId = seg.id;
|
||||
if (segId) {
|
||||
setAudioMapping(segId.toString(), meta.id);
|
||||
}
|
||||
}
|
||||
|
||||
// dubbingAudioUrl 已通过 store 持久化,无需再存到组件本地 state
|
||||
|
||||
// 生成完成后自动执行打轴+截取
|
||||
@@ -350,7 +338,7 @@ export default function VoiceSynthesis() {
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
}, [projectId, segments, setAudioMapping, handleAlignAndClip, checkBalance, handleError]);
|
||||
}, [projectId, segments, handleAlignAndClip, checkBalance, handleError]);
|
||||
|
||||
const handleToggleGeneratedAudio = useCallback(() => {
|
||||
if (!dubbingAudioUrl) {return;}
|
||||
|
||||
@@ -2,30 +2,19 @@
|
||||
* Voice Store - Zustand
|
||||
* ======================
|
||||
*
|
||||
* 管理 TTS 合成、音频文件、本地音色库的全局状态。
|
||||
* 管理 TTS 合成、本地音色库的全局状态。
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type { VoiceInfo, AudioMeta, VoiceMaterial } from '../api/modules/voice';
|
||||
import type { VoiceInfo, VoiceMaterial } from '../api/modules/voice';
|
||||
import * as voiceApi from '../api/modules/voice';
|
||||
|
||||
interface VoiceState {
|
||||
// 预设音色列表
|
||||
presetVoices: VoiceInfo[];
|
||||
|
||||
// 项目音频文件列表
|
||||
projectAudios: AudioMeta[];
|
||||
|
||||
// 音频替换映射(segmentId → audioId)
|
||||
// 记录每个分镜使用了哪个音频文件
|
||||
audioMapping: Record<string, string>; // segmentId → audioId
|
||||
|
||||
// 当前项目 ID
|
||||
currentProjectId: string | null;
|
||||
|
||||
// 加载状态
|
||||
isLoadingVoices: boolean;
|
||||
isLoadingAudios: boolean;
|
||||
|
||||
// 素材库(用户上传的克隆音色)
|
||||
voiceMaterials: VoiceMaterial[];
|
||||
@@ -43,41 +32,19 @@ interface VoiceActions {
|
||||
renameVoiceMaterial: (_id: string, _name: string) => Promise<void>;
|
||||
deleteVoiceMaterial: (_materialId: string) => Promise<void>;
|
||||
|
||||
// 项目音频操作
|
||||
loadProjectAudios: (_projectId: string) => Promise<void>;
|
||||
saveAudio: (_args: {
|
||||
projectId: string;
|
||||
audioId: string;
|
||||
audioData: string;
|
||||
name: string;
|
||||
voiceId: string;
|
||||
duration: number;
|
||||
segmentId?: string;
|
||||
}) => Promise<AudioMeta>;
|
||||
deleteAudio: (_projectId: string, _audioId: string) => Promise<void>;
|
||||
|
||||
// 音频映射操作
|
||||
setAudioMapping: (_segmentId: string, _audioId: string | undefined) => void;
|
||||
getAudioForSegment: (_segmentId: string) => AudioMeta | undefined;
|
||||
|
||||
// 重置
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
|
||||
const initialState: VoiceState = {
|
||||
presetVoices: [],
|
||||
projectAudios: [],
|
||||
audioMapping: {},
|
||||
currentProjectId: null,
|
||||
isLoadingVoices: false,
|
||||
isLoadingAudios: false,
|
||||
voiceMaterials: [],
|
||||
isLoadingMaterials: false,
|
||||
};
|
||||
|
||||
export const useVoiceStore = create<VoiceState & VoiceActions>()(
|
||||
(set, get) => ({
|
||||
(set) => ({
|
||||
...initialState,
|
||||
|
||||
// ====================== 音色操作 ======================
|
||||
@@ -230,92 +197,8 @@ export const useVoiceStore = create<VoiceState & VoiceActions>()(
|
||||
}));
|
||||
},
|
||||
|
||||
// ====================== 项目音频操作 ======================
|
||||
|
||||
loadProjectAudios: async (projectId) => {
|
||||
set({ isLoadingAudios: true, currentProjectId: projectId });
|
||||
try {
|
||||
const audios = await voiceApi.listProjectAudios(projectId);
|
||||
set({ projectAudios: audios });
|
||||
|
||||
// 从现有音频恢复 audioMapping
|
||||
const mapping: Record<string, string> = {};
|
||||
for (const audio of audios) {
|
||||
if (audio.segmentId) {
|
||||
mapping[audio.segmentId] = audio.id;
|
||||
}
|
||||
}
|
||||
set({ audioMapping: mapping });
|
||||
} catch (err) {
|
||||
console.error('[VoiceStore] 加载项目音频失败:', err);
|
||||
set({ projectAudios: [] });
|
||||
} finally {
|
||||
set({ isLoadingAudios: false });
|
||||
}
|
||||
},
|
||||
|
||||
saveAudio: async (args) => {
|
||||
const meta = await voiceApi.saveAudio(args);
|
||||
const state = get();
|
||||
|
||||
// 更新 projectAudios 列表
|
||||
const existing = state.projectAudios.findIndex(a => a.id === meta.id);
|
||||
const updatedAudios = existing >= 0
|
||||
? state.projectAudios.map((a, i) => i === existing ? meta : a)
|
||||
: [...state.projectAudios, meta];
|
||||
set({ projectAudios: updatedAudios });
|
||||
|
||||
// 如果有 segmentId,更新 audioMapping
|
||||
if (args.segmentId) {
|
||||
set(state => ({
|
||||
audioMapping: { ...state.audioMapping, [args.segmentId!]: meta.id },
|
||||
}));
|
||||
}
|
||||
|
||||
return meta;
|
||||
},
|
||||
|
||||
deleteAudio: async (projectId, audioId) => {
|
||||
await voiceApi.deleteAudio(projectId, audioId);
|
||||
|
||||
const state = get();
|
||||
const updatedAudios = state.projectAudios.filter(a => a.id !== audioId);
|
||||
|
||||
// 从 audioMapping 中移除
|
||||
const updatedMapping = { ...state.audioMapping };
|
||||
for (const [segId, audioId2] of Object.entries(updatedMapping)) {
|
||||
if (audioId2 === audioId) {
|
||||
delete updatedMapping[segId];
|
||||
}
|
||||
}
|
||||
|
||||
set({ projectAudios: updatedAudios, audioMapping: updatedMapping });
|
||||
},
|
||||
|
||||
// ====================== 音频映射操作 ======================
|
||||
|
||||
setAudioMapping: (segmentId, audioId) => {
|
||||
set(state => {
|
||||
if (audioId === undefined) {
|
||||
const updated = { ...state.audioMapping };
|
||||
delete updated[segmentId];
|
||||
return { audioMapping: updated };
|
||||
}
|
||||
return { audioMapping: { ...state.audioMapping, [segmentId]: audioId } };
|
||||
});
|
||||
},
|
||||
|
||||
getAudioForSegment: (segmentId) => {
|
||||
const state = get();
|
||||
const audioId = state.audioMapping[segmentId];
|
||||
if (!audioId) {return undefined;}
|
||||
return state.projectAudios.find(a => a.id === audioId);
|
||||
},
|
||||
|
||||
// ====================== 重置 ======================
|
||||
|
||||
reset: () => set(initialState),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user