11import Foundation
2+ import NaturalLanguage
23import Postbox
34import SwiftSignalKit
45import TelegramCore
@@ -111,6 +112,196 @@ public func updateChatTranslationStateInteractively(engine: TelegramEngine, peer
111112
112113
113114
115+ private func chatTranslationLanguageCode ( _ code : String ) -> String {
116+ let components = code . replacingOccurrences ( of : " _ " , with : " - " ) . lowercased ( ) . components ( separatedBy : " - " )
117+ let language = components . first ?? code
118+ switch language {
119+ case " zh " :
120+ if components . contains ( " hans " ) {
121+ return " zh-CN "
122+ }
123+ if components . contains ( " hant " ) || components . contains ( " tw " ) || components . contains ( " hk " ) || components . contains ( " mo " ) {
124+ return " zh-TW "
125+ }
126+ return " zh-CN "
127+ case " iw " :
128+ return " he "
129+ default :
130+ return Translate . normalizeLanguageCode ( language )
131+ }
132+ }
133+
134+ @ available ( macOS 10 . 14 , * )
135+ private let chatTranslationRecognizableLanguages : Set < String > = {
136+ var languages : [ NLLanguage ] = [
137+ . amharic , . arabic , . armenian , . bengali , . bulgarian , . burmese ,
138+ . catalan , . cherokee , . croatian , . czech , . danish , . dutch ,
139+ . english , . finnish , . french , . georgian , . german , . greek ,
140+ . gujarati , . hebrew , . hindi , . hungarian , . icelandic , . indonesian ,
141+ . italian , . japanese , . kannada , . khmer , . korean , . lao ,
142+ . malay , . malayalam , . marathi , . mongolian , . norwegian , . oriya ,
143+ . persian , . polish , . portuguese , . punjabi , . romanian , . russian ,
144+ . simplifiedChinese , . sinhalese , . slovak , . spanish , . swedish ,
145+ . tamil , . telugu , . thai , . tibetan , . traditionalChinese , . turkish ,
146+ . ukrainian , . urdu , . vietnamese
147+ ]
148+ if #available( macOS 13 . 0 , * ) {
149+ languages . append ( . kazakh )
150+ }
151+ return Set ( languages . map { chatTranslationLanguageCode ( $0 . rawValue ) } )
152+ } ( )
153+
154+ private func chatTranslationNeedsTranslation ( _ texts : [ String ] , toLang : String ) -> Bool {
155+ let target = chatTranslationLanguageCode ( toLang )
156+ guard #available( macOS 10 . 14 , * ) else {
157+ return false
158+ }
159+ guard chatTranslationRecognizableLanguages . contains ( target ) else {
160+ return false
161+ }
162+ let recognizer = NLLanguageRecognizer ( )
163+ for text in texts {
164+ var needsTranslation = false
165+ text . enumerateSubstrings ( in : text . startIndex ..< text . endIndex , options : . bySentences ) { sentence , _ , _ , stop in
166+ var remaining = ( sentence ?? " " ) [ ... ]
167+ while ! remaining . isEmpty {
168+ let sample = String ( remaining . prefix ( 256 ) )
169+ remaining = remaining . dropFirst ( sample . count )
170+ recognizer . processString ( sample )
171+ let language = recognizer . languageHypotheses ( withMaximum : 1 ) . first
172+ recognizer . reset ( )
173+ if let language = language , language . value >= 0.9 , chatTranslationLanguageCode ( language . key . rawValue ) != target {
174+ needsTranslation = true
175+ stop = true
176+ break
177+ }
178+ }
179+ }
180+ if needsTranslation {
181+ return true
182+ }
183+ }
184+ return false
185+ }
186+
187+ private func chatTranslationTexts ( _ blocks : [ InstantPageBlock ] ) -> [ String ] {
188+ var texts : [ String ] = [ ]
189+ for block in blocks {
190+ switch block {
191+ case let . title ( text ) , let . subtitle ( text ) , let . header ( text ) , let . subheader ( text ) ,
192+ let . paragraph ( text ) , let . footer ( text ) , let . kicker ( text ) , let . thinking ( text ) ,
193+ let . heading ( text , _) , let . authorDate ( text , _) , let . preformatted ( text , _) :
194+ texts . append ( text . plainText )
195+ case let . list ( items , _) :
196+ for item in items {
197+ switch item {
198+ case let . text ( text , _, _) :
199+ texts . append ( text . plainText )
200+ case let . blocks ( blocks , _, _) :
201+ texts . append ( contentsOf : chatTranslationTexts ( blocks ) )
202+ case . unknown :
203+ break
204+ }
205+ }
206+ case let . blockQuote ( blocks , caption , _) , let . details ( caption , blocks , _) :
207+ texts . append ( caption . plainText )
208+ texts . append ( contentsOf : chatTranslationTexts ( blocks ) )
209+ case let . pullQuote ( text , caption ) :
210+ texts . append ( contentsOf : [ text . plainText , caption . plainText ] )
211+ case let . cover ( block ) :
212+ texts . append ( contentsOf : chatTranslationTexts ( [ block ] ) )
213+ case let . table ( title , rows , _, _, _) :
214+ texts . append ( title . plainText )
215+ for row in rows {
216+ texts . append ( contentsOf : row . cells . compactMap { $0 . text ? . plainText } )
217+ }
218+ case let . postEmbed ( _, _, _, _, _, blocks , caption ) , let . collage ( blocks , caption ) , let . slideshow ( blocks , caption ) :
219+ texts . append ( contentsOf : chatTranslationTexts ( blocks ) )
220+ texts . append ( contentsOf : [ caption . text . plainText , caption . credit . plainText ] )
221+ case let . image ( _, caption , _, _, _) , let . video ( _, caption , _, _, _) ,
222+ let . audio ( _, caption ) , let . document ( _, caption ) , let . webEmbed ( _, _, _, caption , _, _, _) ,
223+ let . map ( _, _, _, _, caption ) :
224+ texts . append ( contentsOf : [ caption . text . plainText , caption . credit . plainText ] )
225+ case let . buttonRow ( _, buttons ) :
226+ texts . append ( contentsOf : buttons . map { $0 . text . plainText } )
227+ case let . relatedArticles ( title , _) :
228+ texts . append ( title . plainText )
229+ default :
230+ break
231+ }
232+ }
233+ return texts
234+ }
235+
236+ public func canTranslateChatMessage ( _ message : Message , accountPeerId : PeerId ? , toLang : String , transcription : Bool = false ) -> Bool {
237+ guard let accountPeerId = accountPeerId , message . author ? . id == accountPeerId || ! message . effectivelyIncoming ( accountPeerId ) else {
238+ return true
239+ }
240+ if transcription {
241+ guard let transcription = message . audioTranscription , ! transcription . isPending else {
242+ return false
243+ }
244+ return chatTranslationNeedsTranslation ( [ transcription . text ] , toLang : toLang )
245+ }
246+ var texts = [ message . text ]
247+ if let rich = message . attributes . compactMap ( { $0 as? RichTextMessageAttribute } ) . first {
248+ texts . append ( contentsOf : chatTranslationTexts ( ( rich . fullInstantPage ?? rich . instantPage ) . blocks ) )
249+ }
250+ if let poll = message . media . first as? TelegramMediaPoll {
251+ texts . append ( poll . text )
252+ texts . append ( contentsOf : poll . options . map { $0 . text } )
253+ if let solution = poll . results . solution {
254+ texts . append ( solution . text )
255+ }
256+ }
257+ return chatTranslationNeedsTranslation ( texts , toLang : toLang )
258+ }
259+
260+ public struct ChatTranslationSource : Equatable {
261+ private let text : String
262+ private let entities : [ MessageTextEntity ]
263+ private let richText : [ InstantPageBlock ] ?
264+ private let pollTexts : [ String ]
265+
266+ public init ( _ message : Message ) {
267+ self . text = message . text
268+ self . entities = message . textEntitiesAttribute ? . entities ?? [ ]
269+ let richText = message . attributes . compactMap { $0 as? RichTextMessageAttribute } . first
270+ self . richText = richText . map { ( $0 . fullInstantPage ?? $0 . instantPage ) . blocks }
271+ if let poll = message . media . first as? TelegramMediaPoll {
272+ self . pollTexts = [ poll . text ] + poll . options . map { $0 . text } + ( poll . results . solution . map { [ $0 . text ] } ?? [ ] )
273+ } else {
274+ self . pollTexts = [ ]
275+ }
276+ }
277+ }
278+
279+ struct ChatTranslationMessages {
280+ let messages : [ Message ]
281+ let sources : [ ChatLiveTranslateContext . State . Key : ChatTranslationSource ]
282+ let eligibility : [ ChatLiveTranslateContext . State . Key : ChatTranslationSource ]
283+
284+ init ( _ messages : [ Message ] , replies : [ Message ] , accountPeerId : PeerId ? , toLang : String ) {
285+ let eligible = messages . filter { canTranslateChatMessage ( $0 , accountPeerId : accountPeerId , toLang : toLang ) }
286+ var ids = Set < MessageId > ( )
287+ self . messages = ( eligible + replies ) . filter { ids . insert ( $0 . id ) . inserted } . sorted ( by : { $0 . id < $1 . id } )
288+ var sources : [ ChatLiveTranslateContext . State . Key : ChatTranslationSource ] = [ : ]
289+ for message in messages + replies {
290+ let key = ChatLiveTranslateContext . State . Key ( id : message . id , toLang : toLang )
291+ if sources [ key ] == nil {
292+ sources [ key ] = ChatTranslationSource ( message )
293+ }
294+ }
295+ self . sources = sources
296+ var eligibility : [ ChatLiveTranslateContext . State . Key : ChatTranslationSource ] = [ : ]
297+ for message in eligible {
298+ let key = ChatLiveTranslateContext . State . Key ( id : message . id , toLang : toLang )
299+ eligibility [ key ] = sources [ key ]
300+ }
301+ self . eligibility = eligibility
302+ }
303+ }
304+
114305public final class ChatLiveTranslateContext {
115306
116307 public struct State : Equatable {
@@ -209,13 +400,60 @@ public final class ChatLiveTranslateContext {
209400 }
210401
211402 public var result : [ Key : Result ]
403+ public var translationEligibility : [ Key : ChatTranslationSource ] = [ : ]
212404 public var transcriptionResult : [ TranscriptionKey : TranscriptionResult ]
213405 public var summarizeResult : [ SummarizeKey : SummarizeResult ]
214406
215407 public static var `default` : State {
216408 return . init ( canTranslate : false , translate : false , autotranslate : false , from : " " , to : " " , paywall : false , tone : . neutral , result : [ : ] , transcriptionResult : [ : ] , summarizeResult : [ : ] )
217409 }
218410
411+ public func translationResult ( for message : Message , accountPeerId : PeerId ? ) -> Result ? {
412+ guard canTranslate , translate , canTranslateChatMessage ( message , accountPeerId : accountPeerId , toLang : to ) else {
413+ return nil
414+ }
415+ if message . hasTranslationAttribute ( toLang : to ) {
416+ return . complete ( toLang : to )
417+ }
418+ return result [ . Key ( id : message . id , toLang : to ) ]
419+ }
420+
421+ public func replyTranslationResult ( for message : Message ) -> Result ? {
422+ if canTranslate , translate , message . hasTranslationAttribute ( toLang : to ) {
423+ return . complete ( toLang : to )
424+ }
425+ return result [ . Key ( id : message . id , toLang : to ) ]
426+ }
427+
428+ mutating func updateTranslation ( _ batch : ChatTranslationMessages , toLang : String ) {
429+ guard canTranslate , translate , to == toLang else {
430+ return
431+ }
432+ for ( key , source ) in batch . sources {
433+ if let previous = translationEligibility [ key ] , previous != source {
434+ result . removeValue ( forKey : key )
435+ queued . removeAll ( where : { $0 . id == key . id } )
436+ }
437+ }
438+ for message in batch . messages {
439+ let key = Key ( id : message . id , toLang : toLang )
440+ if message . hasTranslationAttribute ( toLang : toLang ) {
441+ result [ key ] = . complete ( toLang : toLang )
442+ } else if result [ key ] == . complete ( toLang : toLang ) {
443+ result . removeValue ( forKey : key )
444+ queued . removeAll ( where : { $0 . id == message . id } )
445+ }
446+ }
447+ translationEligibility = batch . eligibility
448+ }
449+
450+ public func transcriptionTranslationResult ( for message : Message , accountPeerId : PeerId ? ) -> TranscriptionResult ? {
451+ guard canTranslate , translate , canTranslateChatMessage ( message , accountPeerId : accountPeerId , toLang : to , transcription : true ) else {
452+ return nil
453+ }
454+ return transcriptionResult [ . Key ( id : message . id , toLang : to ) ]
455+ }
456+
219457 public struct Transcription : Equatable {
220458 public var messageId : MessageId
221459 public var string : String
@@ -347,6 +585,10 @@ public final class ChatLiveTranslateContext {
347585 current . paywall = isPremium ? false : ( state ? . paywall ?? false )
348586 if ! current . canTranslate || ! current . translate || toUpdated {
349587 current . result = [ : ]
588+ current . translationEligibility = [ : ]
589+ current . queued = [ ]
590+ current . queuedTranscriptions = [ ]
591+ self ? . holder . removeAll ( )
350592 //current.transcriptionResult = [:]
351593 }
352594 return current
@@ -545,9 +787,9 @@ public final class ChatLiveTranslateContext {
545787 return statePromise . get ( )
546788 }
547789
548- private var holder : [ MessageId ] = [ ]
790+ private var holder : [ MessageId : ChatTranslationSource ] = [ : ]
549791
550- public func translate ( _ message : [ Message ] ) {
792+ public func translate ( _ message : [ Message ] , replies : [ Message ] = [ ] ) {
551793 prepareQueue . justDispatch { [ weak self ] in
552794 guard let `self` = self else {
553795 return
@@ -557,29 +799,17 @@ public final class ChatLiveTranslateContext {
557799 if ! isEnabled {
558800 return
559801 }
560- let msgs = message . filter { ! $0 . hasTranslationAttribute ( toLang : toLang ) } . map { $0 }
561- let translated = message . filter { $0 . hasTranslationAttribute ( toLang : toLang ) } . map { $0 }
562-
563- if ! translated . isEmpty {
564- self . updateState { current in
565- var current = current
566- if current . translate {
567- for msg in translated {
568- current . result [ . Key ( id : msg . id , toLang : toLang ) ] = . complete ( toLang : toLang )
569- }
570- for msg in msgs {
571- if ! translated . contains ( msg ) , case . complete ( toLang : toLang ) = current . result [ . Key ( id : msg . id , toLang : toLang ) ] {
572- current . result . removeValue ( forKey : . Key ( id : msg . id , toLang : toLang ) )
573- }
574- }
575- }
576- return current
577- }
578-
802+ let batch = ChatTranslationMessages ( message , replies : replies , accountPeerId : self . context . peerId , toLang : toLang )
803+ let msgs = batch . messages . filter { ! $0 . hasTranslationAttribute ( toLang : toLang ) }
804+
805+ self . updateState { current in
806+ var current = current
807+ current . updateTranslation ( batch , toLang : toLang )
808+ return current
579809 }
580- let ids = msgs . map ( { $0 . id } )
581- if self . holder != ids {
582- self . holder = ids
810+ let sources = Dictionary ( uniqueKeysWithValues : msgs . map { ( $0 . id , ChatTranslationSource ( $0 ) ) } )
811+ if self . holder != sources {
812+ self . holder = sources
583813 let signal = Signal < Void , NoError > . complete ( ) |> delay ( 0.01 , queue : prepareQueue )
584814
585815 self . holderDisposable . set ( signal . start ( completed : { [ weak self ] in
@@ -589,7 +819,7 @@ public final class ChatLiveTranslateContext {
589819 // self.holder.removeAll()
590820 self . updateState { current in
591821 var current = current
592- if current . translate {
822+ if current . canTranslate , current . translate , current . to == toLang {
593823 for msg in msgs {
594824 if current . result [ . Key ( id : msg . id , toLang : current . to ) ] == nil {
595825 current . queued . append ( msg )
@@ -640,7 +870,7 @@ public final class ChatLiveTranslateContext {
640870 let toLang = self . stateValue . with { $0 . to }
641871
642872 let messagesToQueue = messages . compactMap { message -> State . Transcription ? in
643- guard let transcription = message . audioTranscription , ! transcription . text . isEmpty else {
873+ guard canTranslateChatMessage ( message , accountPeerId : self . context . peerId , toLang : toLang , transcription : true ) , let transcription = message . audioTranscription , ! transcription . text . isEmpty else {
644874 return nil
645875 }
646876 let existingResult = self . stateValue . with { $0 . transcriptionResult [ . Key ( id : message . id , toLang : toLang ) ] }
0 commit comments