翻译调整

This commit is contained in:
kura 2026-03-19 15:37:19 +08:00
parent ab28fd225f
commit 4798647b40
7 changed files with 98 additions and 13 deletions

View File

@ -208,15 +208,7 @@ async fn run_pipeline(
},
|segment| {
if let Ok(mut current_task) = app_state_for_segment.get_task(&task_id_for_segment) {
if let Some(existing) = current_task
.segments
.iter_mut()
.find(|item| item.id == segment.id)
{
*existing = segment.clone();
} else {
current_task.segments.push(segment.clone());
}
upsert_segment(&mut current_task.segments, segment.clone());
let _ = app_state_for_segment.upsert_task(current_task);
}
window.emit(
@ -242,7 +234,31 @@ async fn run_pipeline(
.ok_or_else(|| anyhow::anyhow!("翻译模式需要填写 LLM API 配置,或设置 OPENAI_API_BASE / OPENAI_API_KEY"))?;
set_status(&window, &app_state, &mut task, TaskStatus::Translating, 72.0, "正在生成译文")?;
let translator = Translator::new(config)?;
segments = translator.translate_segments(&segments, &task.target_lang).await?;
let task_id_for_translate = task.id.clone();
let app_state_for_translate = app_state.clone();
let window_for_translate = window.clone();
segments = translator
.translate_segments_with_progress(
&segments,
&task.target_lang,
|message| {
let _ = emit_log(&window_for_translate, &task_id_for_translate, message);
},
|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());
let _ = app_state_for_translate.upsert_task(current_task);
}
let _ = window_for_translate.emit(
"task:segment",
crate::models::SegmentEvent {
task_id: task_id_for_translate.clone(),
segment,
},
);
},
)
.await?;
task.segments = segments.clone();
app_state.upsert_task(task.clone())?;
@ -352,3 +368,19 @@ fn emit_log(window: &Window, task_id: &str, message: String) -> Result<()> {
)?;
Ok(())
}
fn upsert_segment(segments: &mut Vec<SubtitleSegment>, segment: SubtitleSegment) {
if let Some(existing) = segments.iter_mut().find(|item| item.id == segment.id) {
*existing = segment;
} else {
segments.push(segment);
}
segments.sort_by(|left, right| {
left.start
.partial_cmp(&right.start)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| left.end.partial_cmp(&right.end).unwrap_or(std::cmp::Ordering::Equal))
.then_with(|| left.id.cmp(&right.id))
});
}

View File

@ -70,11 +70,17 @@ impl Translator {
Ok(Self { client, config })
}
pub async fn translate_segments(
pub async fn translate_segments_with_progress<LF, SF>(
&self,
segments: &[SubtitleSegment],
target_language: &TargetLanguage,
) -> Result<Vec<SubtitleSegment>> {
mut log: LF,
mut emit_segment: SF,
) -> Result<Vec<SubtitleSegment>>
where
LF: FnMut(String),
SF: FnMut(SubtitleSegment),
{
let batch_size = self.config.batch_size.clamp(10, 15);
let context_size = self.config.context_size.min(5);
let mut translated = segments.to_vec();
@ -88,13 +94,25 @@ impl Translator {
let context_start = batch_start.saturating_sub(context_size);
let context = &segments[context_start..batch_start];
let batch = &segments[batch_start..batch_end];
log(format!(
"translation: batch {}-{}, segments={}",
batch_start + 1,
batch_end,
batch
.iter()
.map(|segment| segment.id.as_str())
.collect::<Vec<_>>()
.join(", ")
));
let rows = self
.translate_batch_with_retries(context, batch, target_language_name)
.await?;
log(format!("translation: batch done, translated={}", rows.len()));
for row in rows {
if let Some(segment) = translated.iter_mut().find(|item| item.id == row.id) {
segment.translated_text = Some(row.text);
emit_segment(segment.clone());
}
}
}

View File

@ -241,6 +241,9 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
<div class="toolbar-title">
<strong>CrossSubtitle</strong>
<span>桌面字幕工作台</span>
<span class="credit-line">
作者<a href="https://kuraa.cc" target="_blank" rel="noreferrer">kuraa</a> gpt5.4
</span>
</div>
<div class="toolbar-actions">
<button class="button" type="button" :disabled="pending" @click="handlePickFiles">

View File

@ -12,7 +12,13 @@ const emit = defineEmits<{
export: [format: 'srt' | 'vtt' | 'ass']
}>()
const segments = computed(() => props.task?.segments ?? [])
const segments = computed(() =>
[...(props.task?.segments ?? [])].sort((left, right) => {
if (left.start !== right.start) return left.start - right.start
if (left.end !== right.end) return left.end - right.end
return left.id.localeCompare(right.id)
}),
)
const logsExpanded = ref(false)
function formatTime(seconds: number) {

View File

@ -14,6 +14,14 @@ import type {
type ExportFormat = 'srt' | 'vtt' | 'ass'
function sortSegments(segments: SubtitleSegment[]) {
segments.sort((left, right) => {
if (left.start !== right.start) return left.start - right.start
if (left.end !== right.end) return left.end - right.end
return left.id.localeCompare(right.id)
})
}
export const useTaskStore = defineStore('tasks', {
state: () => ({
tasks: [] as SubtitleTask[],
@ -53,6 +61,7 @@ export const useTaskStore = defineStore('tasks', {
} else {
task.segments.push(payload.segment)
}
sortSegments(task.segments)
})
const resetSegmentsUnlisten = await listen<ResetSegmentsEvent>('task:segments_reset', ({ payload }) => {
@ -79,6 +88,7 @@ export const useTaskStore = defineStore('tasks', {
})
const doneUnlisten = await listen<SubtitleTask>('task:done', ({ payload }) => {
sortSegments(payload.segments)
const index = this.tasks.findIndex((item) => item.id === payload.id)
if (index >= 0) {
this.tasks[index] = payload

View File

@ -89,6 +89,22 @@ textarea {
font-size: 12px;
}
.credit-line {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.credit-line a {
color: #0f172a;
text-decoration: none;
}
.credit-line a:hover {
text-decoration: underline;
}
.workspace-toolbar,
.advanced-shell {
margin-top: 12px;