修复
This commit is contained in:
parent
479049501b
commit
c3b69670df
@ -146,16 +146,28 @@ 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);
|
||||
|
||||
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)
|
||||
.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!("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, "正在分析语音片段")?;
|
||||
set_status(&window, &app_state, &mut task, TaskStatus::VadProcessing, 15.0, "正在分析语音片段")?;
|
||||
let samples = AudioPipeline::load_wav_f32(&wav_path)?;
|
||||
let vad = VadEngine::new(payload.vad_model_path.clone(), VadConfig::default())?;
|
||||
let speech_ranges = vad.detect_segments(&samples).await;
|
||||
@ -165,7 +177,7 @@ async fn run_pipeline(
|
||||
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 task_id_for_progress = task.id.clone();
|
||||
let task_id_for_segment = task.id.clone();
|
||||
@ -181,7 +193,7 @@ async fn run_pipeline(
|
||||
should_translate,
|
||||
&speech_ranges,
|
||||
|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(
|
||||
"task:progress",
|
||||
ProgressEvent {
|
||||
@ -232,11 +244,13 @@ async fn run_pipeline(
|
||||
.clone()
|
||||
.or_else(load_translation_config)
|
||||
.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 task_id_for_translate = task.id.clone();
|
||||
let app_state_for_translate = app_state.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
|
||||
.translate_segments_with_progress(
|
||||
&segments,
|
||||
@ -244,6 +258,18 @@ async fn run_pipeline(
|
||||
|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| {
|
||||
if let Ok(mut current_task) = app_state_for_translate.get_task(&task_id_for_translate) {
|
||||
upsert_segment(&mut current_task.segments, segment.clone());
|
||||
|
||||
@ -70,15 +70,17 @@ impl Translator {
|
||||
Ok(Self { client, config })
|
||||
}
|
||||
|
||||
pub async fn translate_segments_with_progress<LF, SF>(
|
||||
pub async fn translate_segments_with_progress<LF, PF, SF>(
|
||||
&self,
|
||||
segments: &[SubtitleSegment],
|
||||
target_language: &TargetLanguage,
|
||||
mut log: LF,
|
||||
mut on_progress: PF,
|
||||
mut emit_segment: SF,
|
||||
) -> Result<Vec<SubtitleSegment>>
|
||||
where
|
||||
LF: FnMut(String),
|
||||
PF: FnMut(f32),
|
||||
SF: FnMut(SubtitleSegment),
|
||||
{
|
||||
let batch_size = self.config.batch_size.clamp(10, 15);
|
||||
@ -88,8 +90,9 @@ impl Translator {
|
||||
TargetLanguage::Zh => "简体中文",
|
||||
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 context_start = batch_start.saturating_sub(context_size);
|
||||
let context = &segments[context_start..batch_start];
|
||||
@ -104,6 +107,8 @@ impl Translator {
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
let batch_progress = (batch_index + 1) as f32 / total_batches.max(1) as f32;
|
||||
on_progress(batch_progress);
|
||||
let rows = self
|
||||
.translate_batch_with_retries(context, batch, target_language_name)
|
||||
.await?;
|
||||
|
||||
@ -341,6 +341,7 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
|
||||
:tasks="taskStore.tasks"
|
||||
:selected-task-id="taskStore.selectedTaskId"
|
||||
@select="taskStore.selectTask"
|
||||
@retry="taskStore.retryTask"
|
||||
/>
|
||||
<SubtitleEditor
|
||||
:task="selectedTask"
|
||||
|
||||
@ -8,6 +8,7 @@ defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [taskId: string]
|
||||
retry: [taskId: string]
|
||||
}>()
|
||||
|
||||
const statusLabel: Record<SubtitleTask['status'], string> = {
|
||||
@ -43,6 +44,7 @@ const statusLabel: Record<SubtitleTask['status'], string> = {
|
||||
:class="{
|
||||
active: task.id === selectedTaskId,
|
||||
completed: task.status === 'completed',
|
||||
failed: task.status === 'failed',
|
||||
}"
|
||||
@click="emit('select', task.id)"
|
||||
>
|
||||
@ -56,7 +58,10 @@ const statusLabel: Record<SubtitleTask['status'], string> = {
|
||||
<div class="progress">
|
||||
<div class="progress-bar" :style="{ width: `${task.progress}%` }" />
|
||||
</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>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@ -29,6 +29,7 @@ export const useTaskStore = defineStore('tasks', {
|
||||
selectedTaskId: '' as string,
|
||||
ready: false,
|
||||
unlisteners: [] as UnlistenFn[],
|
||||
pendingPayloads: {} as Record<string, StartTaskPayload>,
|
||||
}),
|
||||
getters: {
|
||||
selectedTask(state) {
|
||||
@ -111,10 +112,24 @@ export const useTaskStore = defineStore('tasks', {
|
||||
async startTask(payload: StartTaskPayload) {
|
||||
const task = await invoke<SubtitleTask>('start_subtitle_task', { payload })
|
||||
this.tasks.unshift(task)
|
||||
this.pendingPayloads[task.id] = payload
|
||||
this.logsByTaskId[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) {
|
||||
this.selectedTaskId = taskId
|
||||
},
|
||||
|
||||
@ -418,6 +418,46 @@ textarea {
|
||||
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 {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user