@@ -204,10 +204,10 @@ export function setupProofreadHandlers(): void {
204204 items,
205205 name,
206206 } : {
207- items : Omit <
207+ items : ( Omit <
208208 ProofreadItem ,
209- 'id' | 'status' | ' lastPosition' | 'totalCount' | 'modifiedCount'
210- > [ ] ;
209+ 'id' | 'lastPosition' | 'totalCount' | 'modifiedCount'
210+ > & { status ?: ProofreadItem [ 'status' ] } ) [ ] ;
211211 name ?: string ;
212212 } ,
213213 ) => {
@@ -598,5 +598,330 @@ Only respond with the translation, nothing else.`;
598598 } ,
599599 ) ;
600600
601+ // 批量优化字幕
602+ ipcMain . handle (
603+ 'batchOptimizeSubtitles' ,
604+ async (
605+ event ,
606+ {
607+ subtitles,
608+ providerId,
609+ customPrompt,
610+ batchSize = 5 ,
611+ maxRetries = 2 ,
612+ } : {
613+ subtitles : Array < {
614+ id : string ;
615+ index : number ;
616+ sourceContent : string ;
617+ targetContent : string ;
618+ } > ;
619+ providerId ?: string ;
620+ customPrompt ?: string ;
621+ batchSize ?: number ;
622+ maxRetries ?: number ;
623+ } ,
624+ ) => {
625+ try {
626+ logMessage (
627+ `Starting batch optimization: ${ subtitles . length } subtitles in batches of ${ batchSize } ` ,
628+ 'info' ,
629+ ) ;
630+
631+ // 获取用户配置
632+ const userConfig = store . get ( 'userConfig' ) || { } ;
633+ const translateProviderId = providerId || userConfig . translateProvider ;
634+
635+ if ( ! translateProviderId || translateProviderId === '-1' ) {
636+ return {
637+ success : false ,
638+ error : '请先选择一个 AI 翻译服务' ,
639+ } ;
640+ }
641+
642+ // 获取翻译提供商
643+ const providers = store . get ( 'translationProviders' ) || [ ] ;
644+ const provider = providers . find (
645+ ( p : Provider ) => p . id === translateProviderId ,
646+ ) ;
647+
648+ if ( ! provider ) {
649+ return {
650+ success : false ,
651+ error : '未找到选择的翻译服务' ,
652+ } ;
653+ }
654+
655+ if ( ! provider . isAi ) {
656+ return {
657+ success : false ,
658+ error : 'AI 优化功能仅支持 AI 翻译服务' ,
659+ } ;
660+ }
661+
662+ const translator =
663+ TRANSLATOR_MAP [ provider . type as keyof typeof TRANSLATOR_MAP ] ;
664+ if ( ! translator ) {
665+ return {
666+ success : false ,
667+ error : `不支持的翻译服务类型: ${ provider . type } ` ,
668+ } ;
669+ }
670+
671+ const sourceLanguage = userConfig . sourceLanguage || 'en' ;
672+ const targetLanguage = userConfig . targetLanguage || 'zh' ;
673+
674+ // 构建默认批量优化提示词
675+ const defaultBatchPrompt = `You are a professional subtitle translator and proofreader. Optimize the following subtitle translations.
676+
677+ For each subtitle, improve the translation to:
678+ 1. More accurately convey the original meaning
679+ 2. Use natural and fluent expressions
680+ 3. Be appropriate for subtitle display (concise but complete)
681+ 4. Maintain the original tone and style
682+
683+ Input format: JSON object with subtitle IDs as keys and {source, target} as values
684+ Output format: JSON object with the same IDs and optimized translations as values
685+
686+ IMPORTANT: You MUST return a valid JSON object. Do NOT include any text before or after the JSON. Only output the JSON object.` ;
687+
688+ const results : Array < {
689+ id : string ;
690+ index : number ;
691+ sourceContent : string ;
692+ originalTarget : string ;
693+ optimizedTarget : string ;
694+ status : 'success' | 'error' | 'skipped' ;
695+ error ?: string ;
696+ } > = [ ] ;
697+
698+ const totalBatches = Math . ceil ( subtitles . length / batchSize ) ;
699+ let processedCount = 0 ;
700+
701+ // 分批处理
702+ for ( let i = 0 ; i < subtitles . length ; i += batchSize ) {
703+ const batch = subtitles . slice ( i , i + batchSize ) ;
704+ const currentBatchIndex = Math . floor ( i / batchSize ) + 1 ;
705+ let retryCount = 0 ;
706+ let batchSuccess = false ;
707+
708+ logMessage (
709+ `Processing batch ${ currentBatchIndex } /${ totalBatches } with ${ batch . length } subtitles` ,
710+ 'info' ,
711+ ) ;
712+
713+ // 发送进度更新
714+ const progress = Math . round (
715+ ( processedCount / subtitles . length ) * 100 ,
716+ ) ;
717+ event . sender . send ( 'batchOptimizeProgress' , {
718+ progress,
719+ currentBatch : currentBatchIndex ,
720+ totalBatches,
721+ processedCount,
722+ totalCount : subtitles . length ,
723+ } ) ;
724+
725+ while ( ! batchSuccess && retryCount <= maxRetries ) {
726+ try {
727+ // 构建批量输入
728+ const batchInput : Record <
729+ string ,
730+ { source : string ; target : string }
731+ > = { } ;
732+ batch . forEach ( ( sub ) => {
733+ batchInput [ sub . id ] = {
734+ source : sub . sourceContent ,
735+ target : sub . targetContent || '' ,
736+ } ;
737+ } ) ;
738+
739+ // 构建提示词
740+ let optimizePrompt = customPrompt || defaultBatchPrompt ;
741+ optimizePrompt = optimizePrompt
742+ . replace ( / \{ \{ s o u r c e L a n g u a g e \} \} / g, sourceLanguage )
743+ . replace ( / \{ \{ t a r g e t L a n g u a g e \} \} / g, targetLanguage ) ;
744+
745+ const fullPrompt = `${ optimizePrompt } \n\nSubtitles to optimize:\n${ JSON . stringify ( batchInput , null , 2 ) } ` ;
746+
747+ // 配置翻译器
748+ const optimizedProvider = {
749+ ...provider ,
750+ systemPrompt :
751+ 'You are a professional subtitle optimizer. Output ONLY valid JSON. No explanations, no markdown, just the JSON object.' ,
752+ useJsonMode : true ,
753+ structuredOutput : 'disabled' as const ,
754+ } ;
755+
756+ logMessage (
757+ `Batch ${ currentBatchIndex } attempt ${ retryCount + 1 } /${ maxRetries + 1 } ` ,
758+ 'info' ,
759+ ) ;
760+
761+ const response = await translator (
762+ fullPrompt ,
763+ optimizedProvider ,
764+ sourceLanguage ,
765+ targetLanguage ,
766+ ) ;
767+
768+ logMessage (
769+ `Batch ${ currentBatchIndex } response: ${ response } ` ,
770+ 'info' ,
771+ ) ;
772+
773+ // 解析响应
774+ const parsedResponse = parseOptimizationResponse ( response ) ;
775+
776+ if ( parsedResponse && typeof parsedResponse === 'object' ) {
777+ // 处理结果
778+ batch . forEach ( ( sub ) => {
779+ const optimized = parsedResponse [ sub . id ] ;
780+ if ( optimized !== undefined ) {
781+ results . push ( {
782+ id : sub . id ,
783+ index : sub . index ,
784+ sourceContent : sub . sourceContent ,
785+ originalTarget : sub . targetContent ,
786+ optimizedTarget :
787+ typeof optimized === 'string'
788+ ? optimized
789+ : optimized ?. target ||
790+ optimized ?. translation ||
791+ String ( optimized ) ,
792+ status : 'success' ,
793+ } ) ;
794+ } else {
795+ results . push ( {
796+ id : sub . id ,
797+ index : sub . index ,
798+ sourceContent : sub . sourceContent ,
799+ originalTarget : sub . targetContent ,
800+ optimizedTarget : sub . targetContent ,
801+ status : 'skipped' ,
802+ error : '未在响应中找到对应结果' ,
803+ } ) ;
804+ }
805+ } ) ;
806+
807+ processedCount += batch . length ;
808+ batchSuccess = true ;
809+ logMessage (
810+ `Batch ${ currentBatchIndex } /${ totalBatches } completed successfully` ,
811+ 'info' ,
812+ ) ;
813+ } else {
814+ throw new Error ( '无法解析 AI 响应' ) ;
815+ }
816+ } catch ( error ) {
817+ retryCount ++ ;
818+ if ( retryCount <= maxRetries ) {
819+ logMessage (
820+ `Batch ${ currentBatchIndex } failed, retry ${ retryCount } /${ maxRetries } : ${ error } ` ,
821+ 'warning' ,
822+ ) ;
823+ await new Promise ( ( resolve ) =>
824+ setTimeout ( resolve , 1000 * retryCount ) ,
825+ ) ;
826+ } else {
827+ logMessage (
828+ `Batch ${ currentBatchIndex } failed after ${ maxRetries } retries: ${ error } ` ,
829+ 'error' ,
830+ ) ;
831+ // 批次失败,标记所有字幕为错误
832+ batch . forEach ( ( sub ) => {
833+ results . push ( {
834+ id : sub . id ,
835+ index : sub . index ,
836+ sourceContent : sub . sourceContent ,
837+ originalTarget : sub . targetContent ,
838+ optimizedTarget : sub . targetContent ,
839+ status : 'error' ,
840+ error : String ( error ) ,
841+ } ) ;
842+ } ) ;
843+ processedCount += batch . length ;
844+ batchSuccess = true ; // 继续下一批
845+ }
846+ }
847+ }
848+ }
849+
850+ // 发送完成进度
851+ event . sender . send ( 'batchOptimizeProgress' , {
852+ progress : 100 ,
853+ currentBatch : totalBatches ,
854+ totalBatches,
855+ processedCount : subtitles . length ,
856+ totalCount : subtitles . length ,
857+ completed : true ,
858+ } ) ;
859+
860+ logMessage (
861+ `Batch optimization completed: ${ results . filter ( ( r ) => r . status === 'success' ) . length } /${ subtitles . length } successful` ,
862+ 'info' ,
863+ ) ;
864+
865+ return {
866+ success : true ,
867+ data : {
868+ results,
869+ summary : {
870+ total : subtitles . length ,
871+ success : results . filter ( ( r ) => r . status === 'success' ) . length ,
872+ error : results . filter ( ( r ) => r . status === 'error' ) . length ,
873+ skipped : results . filter ( ( r ) => r . status === 'skipped' ) . length ,
874+ } ,
875+ } ,
876+ } ;
877+ } catch ( error ) {
878+ logMessage ( `Error in batch optimization: ${ error } ` , 'error' ) ;
879+ return {
880+ success : false ,
881+ error : error instanceof Error ? error . message : String ( error ) ,
882+ } ;
883+ }
884+ } ,
885+ ) ;
886+
601887 logMessage ( 'Proofread IPC handlers initialized' , 'info' ) ;
602888}
889+
890+ // 辅助函数:解析优化响应
891+ function parseOptimizationResponse (
892+ response : string ,
893+ ) : Record < string , any > | null {
894+ const cleanResponse = response
895+ . replace ( / < t h i n k > [ \s \S ] * ?< \/ t h i n k > / g, '' )
896+ . trim ( ) ;
897+
898+ // 尝试直接解析
899+ try {
900+ return JSON . parse ( cleanResponse ) ;
901+ } catch { }
902+
903+ // 尝试提取 JSON 块
904+ const jsonMatch = cleanResponse . match ( / ` ` ` (?: j s o n ) ? \s * ( [ \s \S ] * ?) ` ` ` / ) ;
905+ if ( jsonMatch ) {
906+ try {
907+ return JSON . parse ( jsonMatch [ 1 ] . trim ( ) ) ;
908+ } catch { }
909+ }
910+
911+ // 尝试找到 JSON 对象
912+ const objectMatch = cleanResponse . match ( / \{ [ \s \S ] * \} / ) ;
913+ if ( objectMatch ) {
914+ try {
915+ return JSON . parse ( objectMatch [ 0 ] ) ;
916+ } catch {
917+ // 尝试修复常见的 JSON 错误
918+ try {
919+ const { jsonrepair } = require ( 'jsonrepair' ) ;
920+ const repaired = jsonrepair ( objectMatch [ 0 ] ) ;
921+ return JSON . parse ( repaired ) ;
922+ } catch { }
923+ }
924+ }
925+
926+ return null ;
927+ }
0 commit comments