修复
This commit is contained in:
parent
479049501b
commit
c3b69670df
@ -146,16 +146,28 @@ async fn run_pipeline(
|
|||||||
let app_state = app.state::<AppState>();
|
let app_state = app.state::<AppState>();
|
||||||
let workspace = std::env::temp_dir().join("crosssubtitle-ai").join(&task.id);
|
let workspace = std::env::temp_dir().join("crosssubtitle-ai").join(&task.id);
|
||||||
let should_translate = matches!(payload.output_mode, OutputMode::Translate);
|
let should_translate = matches!(payload.output_mode, OutputMode::Translate);
|
||||||
|
|
||||||
|
if let Some(ref whisper_path) = payload.whisper_model_path {
|
||||||
|
if !Path::new(whisper_path).exists() {
|
||||||
|
return Err(anyhow::anyhow!("Whisper 模型文件不存在: {whisper_path}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref vad_path) = payload.vad_model_path {
|
||||||
|
if !Path::new(vad_path).exists() {
|
||||||
|
return Err(anyhow::anyhow!("VAD 模型文件不存在: {vad_path}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ffmpeg_path = resolve_ffmpeg_path(&app)
|
let ffmpeg_path = resolve_ffmpeg_path(&app)
|
||||||
.ok_or_else(|| anyhow::anyhow!("未找到可用 ffmpeg,请重新执行打包命令或在系统中安装 ffmpeg"))?;
|
.ok_or_else(|| anyhow::anyhow!("未找到可用 ffmpeg,请重新执行打包命令或在系统中安装 ffmpeg"))?;
|
||||||
|
|
||||||
set_status(&window, &app_state, &mut task, TaskStatus::Extracting, 8.0, "正在抽取音频")?;
|
set_status(&window, &app_state, &mut task, TaskStatus::Extracting, 5.0, "正在抽取音频")?;
|
||||||
emit_log(&window, &task.id, format!("task: input file={}", payload.file_path))?;
|
emit_log(&window, &task.id, format!("task: input file={}", payload.file_path))?;
|
||||||
emit_log(&window, &task.id, format!("audio: ffmpeg={}", ffmpeg_path.display()))?;
|
emit_log(&window, &task.id, format!("audio: ffmpeg={}", ffmpeg_path.display()))?;
|
||||||
let wav_path = AudioPipeline::extract_to_wav(&ffmpeg_path, &payload.file_path, &workspace)?;
|
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()))?;
|
emit_log(&window, &task.id, format!("audio: normalized wav={}", wav_path.display()))?;
|
||||||
|
|
||||||
set_status(&window, &app_state, &mut task, TaskStatus::VadProcessing, 22.0, "正在分析语音片段")?;
|
set_status(&window, &app_state, &mut task, TaskStatus::VadProcessing, 15.0, "正在分析语音片段")?;
|
||||||
let samples = AudioPipeline::load_wav_f32(&wav_path)?;
|
let samples = AudioPipeline::load_wav_f32(&wav_path)?;
|
||||||
let vad = VadEngine::new(payload.vad_model_path.clone(), VadConfig::default())?;
|
let vad = VadEngine::new(payload.vad_model_path.clone(), VadConfig::default())?;
|
||||||
let speech_ranges = vad.detect_segments(&samples).await;
|
let speech_ranges = vad.detect_segments(&samples).await;
|
||||||
@ -165,7 +177,7 @@ async fn run_pipeline(
|
|||||||
format!("vad: detected {} speech ranges", speech_ranges.len()),
|
format!("vad: detected {} speech ranges", speech_ranges.len()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
set_status(&window, &app_state, &mut task, TaskStatus::Transcribing, 45.0, "正在执行 Whisper")?;
|
set_status(&window, &app_state, &mut task, TaskStatus::Transcribing, 30.0, "正在执行 Whisper")?;
|
||||||
let whisper = WhisperEngine::new(payload.whisper_model_path.clone());
|
let whisper = WhisperEngine::new(payload.whisper_model_path.clone());
|
||||||
let task_id_for_progress = task.id.clone();
|
let task_id_for_progress = task.id.clone();
|
||||||
let task_id_for_segment = task.id.clone();
|
let task_id_for_segment = task.id.clone();
|
||||||
@ -181,7 +193,7 @@ async fn run_pipeline(
|
|||||||
should_translate,
|
should_translate,
|
||||||
&speech_ranges,
|
&speech_ranges,
|
||||||
|ratio| {
|
|ratio| {
|
||||||
let progress = 45.0 + ratio.clamp(0.0, 1.0) * 27.0;
|
let progress = 30.0 + ratio.clamp(0.0, 1.0) * 40.0;
|
||||||
window.emit(
|
window.emit(
|
||||||
"task:progress",
|
"task:progress",
|
||||||
ProgressEvent {
|
ProgressEvent {
|
||||||
@ -232,11 +244,13 @@ async fn run_pipeline(
|
|||||||
.clone()
|
.clone()
|
||||||
.or_else(load_translation_config)
|
.or_else(load_translation_config)
|
||||||
.ok_or_else(|| anyhow::anyhow!("翻译模式需要填写 LLM API 配置,或设置 OPENAI_API_BASE / OPENAI_API_KEY"))?;
|
.ok_or_else(|| anyhow::anyhow!("翻译模式需要填写 LLM API 配置,或设置 OPENAI_API_BASE / OPENAI_API_KEY"))?;
|
||||||
set_status(&window, &app_state, &mut task, TaskStatus::Translating, 72.0, "正在生成译文")?;
|
set_status(&window, &app_state, &mut task, TaskStatus::Translating, 70.0, "正在生成译文")?;
|
||||||
let translator = Translator::new(config)?;
|
let translator = Translator::new(config)?;
|
||||||
let task_id_for_translate = task.id.clone();
|
let task_id_for_translate = task.id.clone();
|
||||||
let app_state_for_translate = app_state.clone();
|
let app_state_for_translate = app_state.clone();
|
||||||
let window_for_translate = window.clone();
|
let window_for_translate = window.clone();
|
||||||
|
let task_id_for_translate_progress = task.id.clone();
|
||||||
|
let window_for_translate_progress = window.clone();
|
||||||
segments = translator
|
segments = translator
|
||||||
.translate_segments_with_progress(
|
.translate_segments_with_progress(
|
||||||
&segments,
|
&segments,
|
||||||
@ -244,6 +258,18 @@ async fn run_pipeline(
|
|||||||
|message| {
|
|message| {
|
||||||
let _ = emit_log(&window_for_translate, &task_id_for_translate, message);
|
let _ = emit_log(&window_for_translate, &task_id_for_translate, message);
|
||||||
},
|
},
|
||||||
|
|ratio| {
|
||||||
|
let progress = 70.0 + ratio.clamp(0.0, 1.0) * 25.0;
|
||||||
|
let _ = window_for_translate_progress.emit(
|
||||||
|
"task:progress",
|
||||||
|
ProgressEvent {
|
||||||
|
task_id: task_id_for_translate_progress.clone(),
|
||||||
|
status: TaskStatus::Translating,
|
||||||
|
progress,
|
||||||
|
message: "正在生成译文".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|segment| {
|
|segment| {
|
||||||
if let Ok(mut current_task) = app_state_for_translate.get_task(&task_id_for_translate) {
|
if let Ok(mut current_task) = app_state_for_translate.get_task(&task_id_for_translate) {
|
||||||
upsert_segment(&mut current_task.segments, segment.clone());
|
upsert_segment(&mut current_task.segments, segment.clone());
|
||||||
|
|||||||
@ -70,15 +70,17 @@ impl Translator {
|
|||||||
Ok(Self { client, config })
|
Ok(Self { client, config })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn translate_segments_with_progress<LF, SF>(
|
pub async fn translate_segments_with_progress<LF, PF, SF>(
|
||||||
&self,
|
&self,
|
||||||
segments: &[SubtitleSegment],
|
segments: &[SubtitleSegment],
|
||||||
target_language: &TargetLanguage,
|
target_language: &TargetLanguage,
|
||||||
mut log: LF,
|
mut log: LF,
|
||||||
|
mut on_progress: PF,
|
||||||
mut emit_segment: SF,
|
mut emit_segment: SF,
|
||||||
) -> Result<Vec<SubtitleSegment>>
|
) -> Result<Vec<SubtitleSegment>>
|
||||||
where
|
where
|
||||||
LF: FnMut(String),
|
LF: FnMut(String),
|
||||||
|
PF: FnMut(f32),
|
||||||
SF: FnMut(SubtitleSegment),
|
SF: FnMut(SubtitleSegment),
|
||||||
{
|
{
|
||||||
let batch_size = self.config.batch_size.clamp(10, 15);
|
let batch_size = self.config.batch_size.clamp(10, 15);
|
||||||
@ -88,8 +90,9 @@ impl Translator {
|
|||||||
TargetLanguage::Zh => "简体中文",
|
TargetLanguage::Zh => "简体中文",
|
||||||
TargetLanguage::En => "英文",
|
TargetLanguage::En => "英文",
|
||||||
};
|
};
|
||||||
|
let total_batches = (segments.len() + batch_size - 1) / batch_size;
|
||||||
|
|
||||||
for batch_start in (0..segments.len()).step_by(batch_size) {
|
for (batch_index, batch_start) in (0..segments.len()).step_by(batch_size).enumerate() {
|
||||||
let batch_end = (batch_start + batch_size).min(segments.len());
|
let batch_end = (batch_start + batch_size).min(segments.len());
|
||||||
let context_start = batch_start.saturating_sub(context_size);
|
let context_start = batch_start.saturating_sub(context_size);
|
||||||
let context = &segments[context_start..batch_start];
|
let context = &segments[context_start..batch_start];
|
||||||
@ -104,6 +107,8 @@ impl Translator {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
));
|
));
|
||||||
|
let batch_progress = (batch_index + 1) as f32 / total_batches.max(1) as f32;
|
||||||
|
on_progress(batch_progress);
|
||||||
let rows = self
|
let rows = self
|
||||||
.translate_batch_with_retries(context, batch, target_language_name)
|
.translate_batch_with_retries(context, batch, target_language_name)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@ -341,6 +341,7 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
|
|||||||
:tasks="taskStore.tasks"
|
:tasks="taskStore.tasks"
|
||||||
:selected-task-id="taskStore.selectedTaskId"
|
:selected-task-id="taskStore.selectedTaskId"
|
||||||
@select="taskStore.selectTask"
|
@select="taskStore.selectTask"
|
||||||
|
@retry="taskStore.retryTask"
|
||||||
/>
|
/>
|
||||||
<SubtitleEditor
|
<SubtitleEditor
|
||||||
:task="selectedTask"
|
:task="selectedTask"
|
||||||
|
|||||||
@ -8,6 +8,7 @@ defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
select: [taskId: string]
|
select: [taskId: string]
|
||||||
|
retry: [taskId: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const statusLabel: Record<SubtitleTask['status'], string> = {
|
const statusLabel: Record<SubtitleTask['status'], string> = {
|
||||||
@ -43,6 +44,7 @@ const statusLabel: Record<SubtitleTask['status'], string> = {
|
|||||||
:class="{
|
:class="{
|
||||||
active: task.id === selectedTaskId,
|
active: task.id === selectedTaskId,
|
||||||
completed: task.status === 'completed',
|
completed: task.status === 'completed',
|
||||||
|
failed: task.status === 'failed',
|
||||||
}"
|
}"
|
||||||
@click="emit('select', task.id)"
|
@click="emit('select', task.id)"
|
||||||
>
|
>
|
||||||
@ -56,7 +58,10 @@ const statusLabel: Record<SubtitleTask['status'], string> = {
|
|||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div class="progress-bar" :style="{ width: `${task.progress}%` }" />
|
<div class="progress-bar" :style="{ width: `${task.progress}%` }" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="task.error" class="error-text">{{ task.error }}</p>
|
<div v-if="task.status === 'failed'" class="failed-footer">
|
||||||
|
<p class="error-text">{{ task.error }}</p>
|
||||||
|
<button class="retry-button" type="button" @click.stop="emit('retry', task.id)">重试</button>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export const useTaskStore = defineStore('tasks', {
|
|||||||
selectedTaskId: '' as string,
|
selectedTaskId: '' as string,
|
||||||
ready: false,
|
ready: false,
|
||||||
unlisteners: [] as UnlistenFn[],
|
unlisteners: [] as UnlistenFn[],
|
||||||
|
pendingPayloads: {} as Record<string, StartTaskPayload>,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
selectedTask(state) {
|
selectedTask(state) {
|
||||||
@ -111,10 +112,24 @@ export const useTaskStore = defineStore('tasks', {
|
|||||||
async startTask(payload: StartTaskPayload) {
|
async startTask(payload: StartTaskPayload) {
|
||||||
const task = await invoke<SubtitleTask>('start_subtitle_task', { payload })
|
const task = await invoke<SubtitleTask>('start_subtitle_task', { payload })
|
||||||
this.tasks.unshift(task)
|
this.tasks.unshift(task)
|
||||||
|
this.pendingPayloads[task.id] = payload
|
||||||
this.logsByTaskId[task.id] = []
|
this.logsByTaskId[task.id] = []
|
||||||
this.selectedTaskId = task.id
|
this.selectedTaskId = task.id
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async retryTask(taskId: string) {
|
||||||
|
const payload = this.pendingPayloads[taskId]
|
||||||
|
if (!payload) return
|
||||||
|
|
||||||
|
const index = this.tasks.findIndex((t) => t.id === taskId)
|
||||||
|
if (index >= 0) {
|
||||||
|
this.tasks.splice(index, 1)
|
||||||
|
}
|
||||||
|
delete this.logsByTaskId[taskId]
|
||||||
|
|
||||||
|
await this.startTask(payload)
|
||||||
|
},
|
||||||
|
|
||||||
selectTask(taskId: string) {
|
selectTask(taskId: string) {
|
||||||
this.selectedTaskId = taskId
|
this.selectedTaskId = taskId
|
||||||
},
|
},
|
||||||
|
|||||||
@ -418,6 +418,46 @@ textarea {
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-item.failed {
|
||||||
|
border-color: var(--c-error);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed-footer .error-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--c-error);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--c-error);
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background var(--transition), color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-button:hover {
|
||||||
|
background: var(--c-error);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.task-item.completed::after {
|
.task-item.completed::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user