TranslationEndpointManager.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using UnityEngine;
  6. using XUnity.AutoTranslator.Plugin.Core.Configuration;
  7. using XUnity.AutoTranslator.Plugin.Core.Extensions;
  8. using XUnity.AutoTranslator.Plugin.Core.Parsing;
  9. using XUnity.AutoTranslator.Plugin.Core.Utilities;
  10. namespace XUnity.AutoTranslator.Plugin.Core.Endpoints
  11. {
  12. internal class TranslationEndpointManager
  13. {
  14. private Dictionary<string, byte> _failedTranslations;
  15. private Dictionary<string, TranslationJob> _unstartedJobs;
  16. private Dictionary<string, TranslationJob> _ongoingJobs;
  17. private int _ongoingTranslations;
  18. // used for prototyping
  19. private Dictionary<string, string> _translations;
  20. public TranslationEndpointManager( ITranslateEndpoint endpoint, Exception error )
  21. {
  22. Endpoint = endpoint;
  23. Error = error;
  24. _ongoingTranslations = 0;
  25. _failedTranslations = new Dictionary<string, byte>();
  26. _unstartedJobs = new Dictionary<string, TranslationJob>();
  27. _ongoingJobs = new Dictionary<string, TranslationJob>();
  28. _translations = new Dictionary<string, string>();
  29. HasBatchLogicFailed = false;
  30. AvailableBatchOperations = Settings.MaxAvailableBatchOperations;
  31. }
  32. public TranslationManager Manager { get; set; }
  33. public ITranslateEndpoint Endpoint { get; }
  34. public Exception Error { get; }
  35. public bool IsBusy => _ongoingTranslations >= Endpoint.MaxConcurrency;
  36. public bool HasBatchLogicFailed { get; set; }
  37. public int AvailableBatchOperations { get; set; }
  38. public int ConsecutiveErrors { get; set; }
  39. public bool CanBatch => Endpoint.MaxTranslationsPerRequest > 1 && _unstartedJobs.Count > 1 && !HasBatchLogicFailed && AvailableBatchOperations > 0;
  40. public bool HasUnstartedBatch => _unstartedJobs.Count > 0 && AvailableBatchOperations > 0;
  41. public bool HasUnstartedJob => _unstartedJobs.Count > 0;
  42. public bool HasFailedDueToConsecutiveErrors => ConsecutiveErrors >= Settings.MaxErrors;
  43. public bool TryGetTranslation( TranslationKey key, out string value )
  44. {
  45. return TryGetTranslation( key.GetDictionaryLookupKey(), out value );
  46. }
  47. public bool TryGetTranslation( string key, out string value )
  48. {
  49. return _translations.TryGetValue( key, out value );
  50. }
  51. private void AddTranslation( TranslationKey key, string value )
  52. {
  53. var lookup = key.GetDictionaryLookupKey();
  54. _translations[ lookup ] = value;
  55. }
  56. private void AddTranslation( string key, string value )
  57. {
  58. _translations[ key ] = value;
  59. }
  60. private void QueueNewTranslationForDisk( string key, string value )
  61. {
  62. // FIXME: Implement
  63. }
  64. public void AddTranslationToCache( TranslationKey key, string value )
  65. {
  66. AddTranslationToCache( key.GetDictionaryLookupKey(), value );
  67. }
  68. public void AddTranslationToCache( string key, string value )
  69. {
  70. if( !HasTranslated( key ) )
  71. {
  72. AddTranslation( key, value );
  73. QueueNewTranslationForDisk( key, value );
  74. }
  75. }
  76. private bool HasTranslated( string key )
  77. {
  78. return _translations.ContainsKey( key );
  79. }
  80. public void HandleNextBatch()
  81. {
  82. try
  83. {
  84. var kvps = _unstartedJobs.Take( Endpoint.MaxTranslationsPerRequest ).ToList();
  85. var untranslatedTexts = new List<string>();
  86. var jobs = new List<TranslationJob>();
  87. foreach( var kvp in kvps )
  88. {
  89. var key = kvp.Key;
  90. var job = kvp.Value;
  91. _unstartedJobs.Remove( key );
  92. Manager.UnstartedTranslations--;
  93. var untranslatedText = job.Key.GetDictionaryLookupKey();
  94. if( CanTranslate( untranslatedText ) )
  95. {
  96. jobs.Add( job );
  97. untranslatedTexts.Add( untranslatedText );
  98. _ongoingJobs[ key ] = job;
  99. Manager.OngoingTranslations++;
  100. }
  101. else
  102. {
  103. XuaLogger.Current.Warn( $"Dequeued: '{untranslatedText}' because the current endpoint has already failed this translation 3 times." );
  104. job.State = TranslationJobState.Failed;
  105. job.ErrorMessage = "The endpoint failed to perform this translation 3 or more times.";
  106. Manager.InvokeJobFailed( job );
  107. }
  108. }
  109. if( jobs.Count > 0 )
  110. {
  111. AvailableBatchOperations--;
  112. var jobsArray = jobs.ToArray();
  113. foreach( var untranslatedText in untranslatedTexts )
  114. {
  115. XuaLogger.Current.Debug( "Started: '" + untranslatedText + "'" );
  116. }
  117. CoroutineHelper.Start(
  118. Translate(
  119. untranslatedTexts.ToArray(),
  120. Settings.FromLanguage,
  121. Settings.Language,
  122. translatedText => OnBatchTranslationCompleted( jobsArray, translatedText ),
  123. ( msg, e ) => OnTranslationFailed( jobsArray, msg, e ) ) );
  124. }
  125. }
  126. finally
  127. {
  128. if( _unstartedJobs.Count == 0 )
  129. {
  130. Manager.UnscheduleUnstartedJobs( this );
  131. }
  132. }
  133. }
  134. public void HandleNextJob()
  135. {
  136. try
  137. {
  138. var kvp = _unstartedJobs.FirstOrDefault();
  139. var key = kvp.Key;
  140. var job = kvp.Value;
  141. _unstartedJobs.Remove( key );
  142. Manager.UnstartedTranslations--;
  143. var untranslatedText = job.Key.GetDictionaryLookupKey();
  144. if( CanTranslate( untranslatedText ) )
  145. {
  146. _ongoingJobs[ key ] = job;
  147. Manager.OngoingTranslations++;
  148. XuaLogger.Current.Debug( "Started: '" + untranslatedText + "'" );
  149. CoroutineHelper.Start(
  150. Translate(
  151. new[] { untranslatedText },
  152. Settings.FromLanguage,
  153. Settings.Language,
  154. translatedText => OnSingleTranslationCompleted( job, translatedText ),
  155. ( msg, e ) => OnTranslationFailed( new[] { job }, msg, e ) ) );
  156. }
  157. else
  158. {
  159. XuaLogger.Current.Warn( $"Dequeued: '{untranslatedText}' because the current endpoint has already failed this translation 3 times." );
  160. job.State = TranslationJobState.Failed;
  161. job.ErrorMessage = "The endpoint failed to perform this translation 3 or more times.";
  162. Manager.InvokeJobFailed( job );
  163. }
  164. }
  165. finally
  166. {
  167. if( _unstartedJobs.Count == 0 )
  168. {
  169. Manager.UnscheduleUnstartedJobs( this );
  170. }
  171. }
  172. }
  173. private void OnBatchTranslationCompleted( TranslationJob[] jobs, string[] translatedTexts )
  174. {
  175. ConsecutiveErrors = 0;
  176. var succeeded = jobs.Length == translatedTexts.Length;
  177. if( succeeded )
  178. {
  179. for( int i = 0; i < jobs.Length; i++ )
  180. {
  181. var job = jobs[ i ];
  182. var translatedText = translatedTexts[ i ];
  183. job.TranslatedText = PostProcessTranslation( job.Key, translatedText );
  184. job.State = TranslationJobState.Succeeded;
  185. _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
  186. Manager.OngoingTranslations--;
  187. XuaLogger.Current.Info( $"Completed: '{job.Key.GetDictionaryLookupKey()}' => '{job.TranslatedText}'" );
  188. Manager.InvokeJobCompleted( job );
  189. }
  190. }
  191. else
  192. {
  193. if( !HasBatchLogicFailed )
  194. {
  195. CoroutineHelper.Start( EnableBatchingAfterDelay() );
  196. }
  197. HasBatchLogicFailed = true;
  198. for( int i = 0; i < jobs.Length; i++ )
  199. {
  200. var job = jobs[ i ];
  201. var key = job.Key.GetDictionaryLookupKey();
  202. AddUnstartedJob( key, job );
  203. _ongoingJobs.Remove( key );
  204. Manager.OngoingTranslations--;
  205. }
  206. XuaLogger.Current.Error( "A batch operation failed. Disabling batching and restarting failed jobs." );
  207. }
  208. }
  209. private void OnSingleTranslationCompleted( TranslationJob job, string[] translatedTexts )
  210. {
  211. var translatedText = translatedTexts[ 0 ];
  212. ConsecutiveErrors = 0;
  213. job.TranslatedText = PostProcessTranslation( job.Key, translatedText );
  214. job.State = TranslationJobState.Succeeded;
  215. _ongoingJobs.Remove( job.Key.GetDictionaryLookupKey() );
  216. Manager.OngoingTranslations--;
  217. XuaLogger.Current.Info( $"Completed: '{job.Key.GetDictionaryLookupKey()}' => '{job.TranslatedText}'" );
  218. Manager.InvokeJobCompleted( job );
  219. }
  220. private string PostProcessTranslation( TranslationKey key, string translatedText )
  221. {
  222. var hasTranslation = !string.IsNullOrEmpty( translatedText );
  223. if( hasTranslation )
  224. {
  225. translatedText = key.RepairTemplate( translatedText );
  226. if( Settings.Language == Settings.Romaji && Settings.RomajiPostProcessing != TextPostProcessing.None )
  227. {
  228. translatedText = RomanizationHelper.PostProcess( translatedText, Settings.RomajiPostProcessing );
  229. }
  230. else if( Settings.TranslationPostProcessing != TextPostProcessing.None )
  231. {
  232. translatedText = RomanizationHelper.PostProcess( translatedText, Settings.TranslationPostProcessing );
  233. }
  234. if( Settings.ForceSplitTextAfterCharacters > 0 )
  235. {
  236. translatedText = translatedText.SplitToLines( Settings.ForceSplitTextAfterCharacters, '\n', ' ', ' ' );
  237. }
  238. }
  239. return translatedText;
  240. }
  241. private void OnTranslationFailed( TranslationJob[] jobs, string error, Exception e )
  242. {
  243. if( e == null )
  244. {
  245. XuaLogger.Current.Error( error );
  246. }
  247. else
  248. {
  249. XuaLogger.Current.Error( e, error );
  250. }
  251. if( jobs.Length == 1 )
  252. {
  253. foreach( var job in jobs )
  254. {
  255. var untranslatedText = job.Key.GetDictionaryLookupKey();
  256. job.State = TranslationJobState.Failed;
  257. job.ErrorMessage = error;
  258. _ongoingJobs.Remove( untranslatedText );
  259. Manager.OngoingTranslations--;
  260. RegisterTranslationFailureFor( untranslatedText );
  261. Manager.InvokeJobFailed( job );
  262. }
  263. }
  264. else
  265. {
  266. if( !HasBatchLogicFailed )
  267. {
  268. CoroutineHelper.Start( EnableBatchingAfterDelay() );
  269. }
  270. HasBatchLogicFailed = true;
  271. for( int i = 0; i < jobs.Length; i++ )
  272. {
  273. var job = jobs[ i ];
  274. var key = job.Key.GetDictionaryLookupKey();
  275. AddUnstartedJob( key, job );
  276. _ongoingJobs.Remove( key );
  277. Manager.OngoingTranslations--;
  278. }
  279. XuaLogger.Current.Error( "A batch operation failed. Disabling batching and restarting failed jobs." );
  280. }
  281. if( !HasFailedDueToConsecutiveErrors )
  282. {
  283. ConsecutiveErrors++;
  284. if( HasFailedDueToConsecutiveErrors )
  285. {
  286. XuaLogger.Current.Error( $"{Settings.MaxErrors} or more consecutive errors occurred. Shutting down plugin." );
  287. ClearAllJobs();
  288. }
  289. }
  290. }
  291. private IEnumerator EnableBatchingAfterDelay()
  292. {
  293. yield return new WaitForSeconds( 240 );
  294. HasBatchLogicFailed = false;
  295. XuaLogger.Current.Info( "Re-enabled batching." );
  296. }
  297. public bool EnqueueTranslation( object ui, TranslationKey key, TranslationResult translationResult, ParserTranslationContext context )
  298. {
  299. var lookupKey = key.GetDictionaryLookupKey();
  300. var added = AssociateWithExistingJobIfPossible( ui, lookupKey, translationResult, context );
  301. if( added )
  302. {
  303. return false;
  304. }
  305. var checkOtherEndpoints = translationResult == null;
  306. if( checkOtherEndpoints )
  307. {
  308. var endpoints = Manager.ConfiguredEndpoints;
  309. var len = endpoints.Count;
  310. for( int i = 0; i < len; i++ )
  311. {
  312. var endpoint = endpoints[ i ];
  313. if( endpoint == this ) continue;
  314. added = endpoint.AssociateWithExistingJobIfPossible( ui, lookupKey, translationResult, context );
  315. if( added )
  316. {
  317. return false;
  318. }
  319. }
  320. }
  321. XuaLogger.Current.Debug( "Queued: '" + lookupKey + "'" );
  322. var saveResultGlobally = checkOtherEndpoints;
  323. var newJob = new TranslationJob( this, key, saveResultGlobally );
  324. newJob.Associate( ui, translationResult, context );
  325. return AddUnstartedJob( lookupKey, newJob );
  326. }
  327. public bool AssociateWithExistingJobIfPossible( object ui, string key, TranslationResult translationResult, ParserTranslationContext context )
  328. {
  329. if( _unstartedJobs.TryGetValue( key, out TranslationJob unstartedJob ) )
  330. {
  331. unstartedJob.Associate( ui, translationResult, context );
  332. return true;
  333. }
  334. if( _ongoingJobs.TryGetValue( key, out TranslationJob ongoingJob ) )
  335. {
  336. ongoingJob.Associate( ui, translationResult, context );
  337. return true;
  338. }
  339. return false;
  340. }
  341. private bool AddUnstartedJob( string key, TranslationJob job )
  342. {
  343. if( !_unstartedJobs.ContainsKey( key ) )
  344. {
  345. int countBefore = _unstartedJobs.Count;
  346. _unstartedJobs.Add( key, job );
  347. Manager.UnstartedTranslations++;
  348. if( countBefore == 0 )
  349. {
  350. Manager.ScheduleUnstartedJobs( this );
  351. }
  352. return true;
  353. }
  354. return false;
  355. }
  356. public void ClearAllJobs()
  357. {
  358. var ongoingCount = _ongoingJobs.Count;
  359. var unstartedCount = _unstartedJobs.Count;
  360. var unstartedJobs = _unstartedJobs.ToList();
  361. _ongoingJobs.Clear();
  362. _unstartedJobs.Clear();
  363. foreach( var job in unstartedJobs )
  364. {
  365. XuaLogger.Current.Warn( $"Dequeued: '{job.Key}'" );
  366. job.Value.State = TranslationJobState.Failed;
  367. job.Value.ErrorMessage = "Translation failed because all jobs on endpoint was cleared.";
  368. Manager.InvokeJobFailed( job.Value );
  369. }
  370. Manager.OngoingTranslations -= ongoingCount;
  371. Manager.UnstartedTranslations -= unstartedCount;
  372. Manager.UnscheduleUnstartedJobs( this );
  373. }
  374. public bool CanTranslate( string untranslatedText )
  375. {
  376. if( _failedTranslations.TryGetValue( untranslatedText, out var count ) )
  377. {
  378. return count < Settings.MaxFailuresForSameTextPerEndpoint;
  379. }
  380. return true;
  381. }
  382. public void RegisterTranslationFailureFor( string untranslatedText )
  383. {
  384. byte count;
  385. if( !_failedTranslations.TryGetValue( untranslatedText, out count ) )
  386. {
  387. count = 1;
  388. }
  389. else
  390. {
  391. count++;
  392. }
  393. _failedTranslations[ untranslatedText ] = count;
  394. }
  395. public IEnumerator Translate( string[] untranslatedTexts, string from, string to, Action<string[]> success, Action<string, Exception> failure )
  396. {
  397. var startTime = Time.realtimeSinceStartup;
  398. var context = new TranslationContext( untranslatedTexts, from, to, success, failure );
  399. _ongoingTranslations++;
  400. try
  401. {
  402. bool ok = false;
  403. var iterator = Endpoint.Translate( context );
  404. if( iterator != null )
  405. {
  406. TryMe: try
  407. {
  408. ok = iterator.MoveNext();
  409. // check for timeout
  410. var now = Time.realtimeSinceStartup;
  411. if( now - startTime > Settings.Timeout )
  412. {
  413. ok = false;
  414. context.FailWithoutThrowing( $"Timeout occurred during translation (took more than {Settings.Timeout} seconds)", null );
  415. }
  416. }
  417. catch( TranslationContextException )
  418. {
  419. ok = false;
  420. }
  421. catch( Exception e )
  422. {
  423. ok = false;
  424. context.FailWithoutThrowing( "Error occurred during translation.", e );
  425. }
  426. if( ok )
  427. {
  428. yield return iterator.Current;
  429. goto TryMe;
  430. }
  431. }
  432. }
  433. finally
  434. {
  435. _ongoingTranslations--;
  436. context.FailIfNotCompleted();
  437. }
  438. }
  439. }
  440. }