翻译调整
This commit is contained in:
parent
ab28fd225f
commit
4798647b40
@ -208,15 +208,7 @@ async fn run_pipeline(
|
|||||||
},
|
},
|
||||||
|segment| {
|
|segment| {
|
||||||
if let Ok(mut current_task) = app_state_for_segment.get_task(&task_id_for_segment) {
|
if let Ok(mut current_task) = app_state_for_segment.get_task(&task_id_for_segment) {
|
||||||
if let Some(existing) = current_task
|
upsert_segment(&mut current_task.segments, segment.clone());
|
||||||
.segments
|
|
||||||
.iter_mut()
|
|
||||||
.find(|item| item.id == segment.id)
|
|
||||||
{
|
|
||||||
*existing = segment.clone();
|
|
||||||
} else {
|
|
||||||
current_task.segments.push(segment.clone());
|
|
||||||
}
|
|
||||||
let _ = app_state_for_segment.upsert_task(current_task);
|
let _ = app_state_for_segment.upsert_task(current_task);
|
||||||
}
|
}
|
||||||
window.emit(
|
window.emit(
|
||||||
@ -242,7 +234,31 @@ async fn run_pipeline(
|
|||||||
.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, 72.0, "正在生成译文")?;
|
||||||
let translator = Translator::new(config)?;
|
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();
|
task.segments = segments.clone();
|
||||||
app_state.upsert_task(task.clone())?;
|
app_state.upsert_task(task.clone())?;
|
||||||
|
|
||||||
@ -352,3 +368,19 @@ fn emit_log(window: &Window, task_id: &str, message: String) -> Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
Ok(())
|
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))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -70,11 +70,17 @@ impl Translator {
|
|||||||
Ok(Self { client, config })
|
Ok(Self { client, config })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn translate_segments(
|
pub async fn translate_segments_with_progress<LF, SF>(
|
||||||
&self,
|
&self,
|
||||||
segments: &[SubtitleSegment],
|
segments: &[SubtitleSegment],
|
||||||
target_language: &TargetLanguage,
|
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 batch_size = self.config.batch_size.clamp(10, 15);
|
||||||
let context_size = self.config.context_size.min(5);
|
let context_size = self.config.context_size.min(5);
|
||||||
let mut translated = segments.to_vec();
|
let mut translated = segments.to_vec();
|
||||||
@ -88,13 +94,25 @@ impl Translator {
|
|||||||
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];
|
||||||
let batch = &segments[batch_start..batch_end];
|
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
|
let rows = self
|
||||||
.translate_batch_with_retries(context, batch, target_language_name)
|
.translate_batch_with_retries(context, batch, target_language_name)
|
||||||
.await?;
|
.await?;
|
||||||
|
log(format!("translation: batch done, translated={}", rows.len()));
|
||||||
|
|
||||||
for row in rows {
|
for row in rows {
|
||||||
if let Some(segment) = translated.iter_mut().find(|item| item.id == row.id) {
|
if let Some(segment) = translated.iter_mut().find(|item| item.id == row.id) {
|
||||||
segment.translated_text = Some(row.text);
|
segment.translated_text = Some(row.text);
|
||||||
|
emit_segment(segment.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@ -241,6 +241,9 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
|
|||||||
<div class="toolbar-title">
|
<div class="toolbar-title">
|
||||||
<strong>CrossSubtitle</strong>
|
<strong>CrossSubtitle</strong>
|
||||||
<span>桌面字幕工作台</span>
|
<span>桌面字幕工作台</span>
|
||||||
|
<span class="credit-line">
|
||||||
|
作者:<a href="https://kuraa.cc" target="_blank" rel="noreferrer">kuraa</a> gpt5.4
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<button class="button" type="button" :disabled="pending" @click="handlePickFiles">
|
<button class="button" type="button" :disabled="pending" @click="handlePickFiles">
|
||||||
|
|||||||
@ -12,7 +12,13 @@ const emit = defineEmits<{
|
|||||||
export: [format: 'srt' | 'vtt' | 'ass']
|
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)
|
const logsExpanded = ref(false)
|
||||||
|
|
||||||
function formatTime(seconds: number) {
|
function formatTime(seconds: number) {
|
||||||
|
|||||||
@ -14,6 +14,14 @@ import type {
|
|||||||
|
|
||||||
type ExportFormat = 'srt' | 'vtt' | 'ass'
|
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', {
|
export const useTaskStore = defineStore('tasks', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
tasks: [] as SubtitleTask[],
|
tasks: [] as SubtitleTask[],
|
||||||
@ -53,6 +61,7 @@ export const useTaskStore = defineStore('tasks', {
|
|||||||
} else {
|
} else {
|
||||||
task.segments.push(payload.segment)
|
task.segments.push(payload.segment)
|
||||||
}
|
}
|
||||||
|
sortSegments(task.segments)
|
||||||
})
|
})
|
||||||
|
|
||||||
const resetSegmentsUnlisten = await listen<ResetSegmentsEvent>('task:segments_reset', ({ payload }) => {
|
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 }) => {
|
const doneUnlisten = await listen<SubtitleTask>('task:done', ({ payload }) => {
|
||||||
|
sortSegments(payload.segments)
|
||||||
const index = this.tasks.findIndex((item) => item.id === payload.id)
|
const index = this.tasks.findIndex((item) => item.id === payload.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
this.tasks[index] = payload
|
this.tasks[index] = payload
|
||||||
|
|||||||
@ -89,6 +89,22 @@ textarea {
|
|||||||
font-size: 12px;
|
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,
|
.workspace-toolbar,
|
||||||
.advanced-shell {
|
.advanced-shell {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user