using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Data; using Data.Enum; using Data.Model; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; namespace DanbooruDownloader.HostedServices { public class FindWaifuService : IHostedService { private const double MIN_CHAR_FRANCHISE_INTERSECT_FOR_AFFILIATION = 0.85d; /// /// Фильтр фажества. Минимальный процент /// private const double MIN_COUNT_PERCENT_BY_ARTIST_TO_CHARACTER_FOR_FAGGING = 0.28; private const double MIN_COUNT_BY_ARTIST_TO_CHARACTER_FOR_FAGGING = 10; private const double MAX_FAG_POINTS_PER_CHARACTER = 10; private const double FAG_POINTS_LOG10_MULTIPLIER = 0.15; private const int MIN_CHAR_COUNT_TO_CREATE_STAT = 10; private const int MAX_CHARACTERS_PER_IMAGE_FOR_FAGGING_COUNT = 2; private readonly NFContextFactory NFContextFactory; // ReSharper disable once NotAccessedField.Local private readonly IConfiguration Configuration; private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); private CancellationToken CancellationToken => CancellationTokenSource.Token; private Task MainTaskItself; private Task Count123MeanTaskItself; public FindWaifuService( NFContextFactory nfContextFactory, IConfiguration configuration ) { NFContextFactory = nfContextFactory; Configuration = configuration; } public Task StartAsync(CancellationToken cancellationToken) { MainTaskItself = Task.Run(SearchForWaifu, cancellationToken); Count123MeanTaskItself = Task.CompletedTask; // Task.Run(CalculateMeanCount123, cancellationToken); return Task.CompletedTask; } public async Task StopAsync(CancellationToken cancellationToken) { CancellationTokenSource.Cancel(); await MainTaskItself; await Count123MeanTaskItself; } #region SearchForWaifu private DanbooruTag[] tags; private Dictionary tagDictionary; private Dictionary countsByTagId; private ICollection tagArtistIds; private ICollection tagCharacterIds; private ICollection intersects; private IDictionary parentImages; private int GetRealImageId(int imageId) { return parentImages.ContainsKey(imageId) ? parentImages[imageId] : imageId; } private IDictionary characterToCopyright; private int tagOriginalId; private async Task SearchForWaifu_GenerateCopyright(NFContext context) { var tagCopyrightIds = tags .Where(x => x.type == DanbooruTagType.Copyright) .Where(x => x.id != tagOriginalId) .Select(t => t.id) .ToHashSet(); Console.WriteLine( "{0}\ttagCopyrightIds.Length = {1,7}", DateTimeOffset.UtcNow, tagCopyrightIds.Count() ); var i1 = await context .DanbooruTagIntersect .Where(x => tagCopyrightIds.Contains(x.tag_id)) .ToArrayAsync(); Console.WriteLine( "{0}\tDb: i1.Length = {1,7}", DateTimeOffset.UtcNow, i1.Count() ); i1 = i1 .Select(t => (art_id: GetRealImageId(t.art_id), t.tag_id)) .Distinct() .Select(t => new DanbooruTagIntersect() {art_id = t.art_id, tag_id = t.tag_id}) .ToArray(); Console.WriteLine( "{0}\tParented: i1.Length = {1,7}", DateTimeOffset.UtcNow, i1.Count() ); var franchisesByArtId = i1 .GroupBy(t => t.art_id) .Select(t => (art_id: t.Key, copyrights: t.Select(t1 => t1.tag_id).Distinct().ToArray())) .Where(x => x.copyrights.Length == 1) .ToDictionary( t => t.art_id, t => t.copyrights.First() ); var charCounts1 = intersects .Where(x => tagCharacterIds.Contains(x.tag_id)) .GroupBy(t => t.tag_id) .ToDictionary( t => t.Key, t => t.Count() ); var charCounts2 = intersects .Where(x => franchisesByArtId.ContainsKey(x.art_id) && tagCharacterIds.Contains(x.tag_id)) .GroupBy(t => t.tag_id) .ToDictionary( t => t.Key, t => t.Count() ); Console.WriteLine( "{0}\tfranchisesByArtId.Length = {1,7}" + "\n\t\t\t\tcharCounts1.Length = {2,7}" + "\n\t\t\t\tcharCounts2.Length = {3,7}", DateTimeOffset.UtcNow, franchisesByArtId.Count(), charCounts1.Count(), charCounts2.Count() ); characterToCopyright = intersects .Where(x => tagCharacterIds.Contains(x.tag_id) && charCounts1.ContainsKey(x.tag_id)) .Select(t => { // ReSharper disable once ConvertToLambdaExpression return (charId: t.tag_id, copyright: franchisesByArtId.ContainsKey(t.art_id) ? franchisesByArtId[t.art_id] : -1); }) .Where(x => x.copyright != -1) .GroupBy(t => t.charId) .Select(t => { var charId = t.Key; var minCount = charCounts1[charId] * MIN_CHAR_FRANCHISE_INTERSECT_FOR_AFFILIATION; var (copyright, count) = t .GroupBy(t1 => t1.copyright) .Select(t1 => (t1.Key, count: t1.Count())) .OrderByDescending(t1 => t1.count) .First(); // ReSharper disable once ConvertIfStatementToReturnStatement if (count >= minCount) { return (charId, copyright: copyright); } if (!charCounts2.ContainsKey(charId)) { return (charId, copyright: -1); } minCount = charCounts2[charId] * MIN_CHAR_FRANCHISE_INTERSECT_FOR_AFFILIATION; // ReSharper disable once ConvertIfStatementToReturnStatement if (count >= minCount) { return (charId, copyright: copyright); } return (charId, copyright: -1); }) .Where(x => x.copyright != -1) .OrderBy(t => t.charId) .ToDictionary( t => t.charId, t => t.copyright ); Console.WriteLine( "{0}\tcharacterToCopyright.Length = {1,7}", DateTimeOffset.UtcNow, characterToCopyright.Count() ); } private async Task SearchForWaifu_Step1() { Console.WriteLine( "{0}\tSearchForWaifu_Step1", DateTimeOffset.UtcNow ); await using var context = NFContextFactory.CreateContext(); Console.WriteLine( "{0}\tDB connected", DateTimeOffset.UtcNow ); parentImages = await context .DanbooruImage .Where(x => x.parent_id != null) .ToDictionaryAsync( t => t.id, t => t.parent_id!.Value ); Console.WriteLine( "{0}\tparentImages.Length = {1,7}", DateTimeOffset.UtcNow, parentImages.Count() ); tags = await context .DanbooruTag .ToArrayAsync(); tagDictionary = tags.ToDictionary(t => t.id, t => t); Console.WriteLine( "{0}\ttags.Length = {1,7}", DateTimeOffset.UtcNow, tags.Count() ); tagOriginalId = tags.First(x => x.tag == "original").id; tagArtistIds = tags .Where(x => x.type == DanbooruTagType.Artist) .Select(t => t.id) .ToHashSet(); tagCharacterIds = tags .Where(x => x.type == DanbooruTagType.Character) .Select(t => t.id) .ToHashSet(); var artistCharacterIds = tagArtistIds.Concat(tagCharacterIds).ToArray(); Console.WriteLine( "{0}\ttagArtistIds.Length = {1,7}" + "\n\t\t\t\ttagCharacterIds = {2,7}" + "\n\t\t\t\tOriginal tag = {3,7}", DateTimeOffset.UtcNow, tagArtistIds.Count(), tagCharacterIds.Count(), tagOriginalId ); var sw = Stopwatch.StartNew(); intersects = await context .DanbooruTagIntersect .Where(x => artistCharacterIds.Contains(x.tag_id)) .ToArrayAsync(); sw.Stop(); var intersectsRawCount = intersects.Count; Console.WriteLine( "{0}\tDb: intersects.Length = {1,7}", DateTimeOffset.UtcNow, intersects.Count() ); intersects = intersects .Select(t => (art_id: GetRealImageId(t.art_id), t.tag_id)) .Distinct() .Select(t => new DanbooruTagIntersect() { art_id = t.art_id, tag_id = t.tag_id, }) .ToArray(); Console.WriteLine( "{0}\tParented: intersects.Length = {1,7}", DateTimeOffset.UtcNow, intersects.Count() ); await SearchForWaifu_GenerateCopyright(context); { var artId1 = intersects .Where(x => tagArtistIds.Contains(x.tag_id)) .Select(t => t.art_id) .ToHashSet(); artId1 = intersects .Where(x => tagCharacterIds.Contains(x.tag_id)) .Select(t => t.art_id) .Where(x => artId1.Contains(x)) .ToHashSet(); Console.WriteLine( "{0}\tClear artId1 with characters and artists = {1,7}", DateTimeOffset.UtcNow, artId1.Count() ); intersects = intersects .Where(x => artId1.Contains(x.art_id)) .ToArray(); } ClearIntersectsFromMultipleChars(); await ClearIntersectsFromBadTags(context); countsByTagId = intersects .GroupBy(t => t.tag_id) .ToDictionary( t => t.Key, t => t.Count() ); Console.WriteLine( "{0}\tGet intersects: {6}" + "\n\t\t\t\tArtists count = {1,6}" + "\n\t\t\t\tCharacter count = {2,6}" + "\n\t\t\t\tIntersects {3,7} ({4,7} {5:P1})" + "\n\t\t\t\tcountsByTagId.Length = {7,7}", DateTimeOffset.UtcNow, tagArtistIds.Count, tagCharacterIds.Count, intersects.Count, intersectsRawCount, intersects.Count * 1d / intersectsRawCount, sw.Elapsed, countsByTagId.Count() ); } /// /// Delete game_cg & Delete official art /// /// private async Task ClearIntersectsFromBadTags(NFContext context) { var gameCG = tags.First(x => x.tag == "game_cg").id; var officialArt = tags.First(x => x.tag == "official_art").id; var a = new[] {gameCG, officialArt}; ICollection badArtIds = await context .DanbooruTagIntersect .Where(x => a.Contains(x.tag_id)) .Select(t => t.art_id) .Distinct() .ToArrayAsync(); badArtIds = badArtIds.ToHashSet(); intersects = intersects .Where(x => !badArtIds.Contains(x.art_id)) .ToArray(); Console.WriteLine( "{0}\tClearIntersectsFromBadTags: badArtIds.Length = {1,7}" + "\n\t\t\t\tintersects.Length = {2,7}", DateTimeOffset.UtcNow, badArtIds.Count(), intersects.Count() ); } /// /// Delete multi-characters (4+) arts /// private void ClearIntersectsFromMultipleChars() { var badArtIds = intersects .Where(x => tagCharacterIds.Contains(x.tag_id)) .GroupBy(t => t.art_id) .Select(t => (art_id: t.Key, count: t.Count())) .Where(x => x.count > MAX_CHARACTERS_PER_IMAGE_FOR_FAGGING_COUNT) .Select(t => t.art_id) .ToHashSet(); intersects = intersects .Where(x => !badArtIds.Contains(x.art_id)) .ToArray(); Console.WriteLine( "{0}\tClearIntersectsFromMultipleChars: badArtIds.Length = {1,7}" + "\n\t\t\t\tintersects.Length = {2,7}", DateTimeOffset.UtcNow, badArtIds.Count(), intersects.Count() ); } private async Task SearchForWaifu() { Console.WriteLine( "{0}\tSearchForWaifu", DateTimeOffset.UtcNow ); await SearchForWaifu_Step1(); var byArts = intersects .GroupBy(t => t.art_id) .Select(t => { var a = t.Select(t1 => t1.tag_id).ToArray(); var artists = a.Where(tagArtistIds.Contains).ToArray(); var characters = a.Where(tagCharacterIds.Contains).ToArray(); return (artId: t.Key, artists, characters); }) .Where(x => x.artists.Any() && x.characters.Any()) .ToDictionary( t => t.artId, t => t ); Console.WriteLine( "{0}\tbyArts.Length = {1,7}", DateTimeOffset.UtcNow, byArts.Count() ); var semiRawData = byArts .Values .SelectMany(t => { var (_, artists, characters) = t; return artists .SelectMany(t1 => characters.Select(t2 => (artist: t1, character: t2))); }) .GroupBy(t => t) .Select(t => (key: t.Key, count: t.Count())) .Select(t => (key: t.key, count: t.count, countP: t.count * 1d / countsByTagId[t.key.artist])) .ToArray(); var countDictionary = semiRawData .Where(x => x.count > MIN_COUNT_BY_ARTIST_TO_CHARACTER_FOR_FAGGING) .OrderByDescending(t => t.countP + Math.Log10(t.count) * FAG_POINTS_LOG10_MULTIPLIER) .ToDictionary( t => t.key, t => t.count ); Console.WriteLine( "{0}\tsemiRawData.Length = {1,7}" + "\n\t\t\t\tcountDictionary = {2,7}", DateTimeOffset.UtcNow, semiRawData.Count(), countDictionary.Count() ); { // ReSharper disable once NotAccessedVariable var realCount = 0; var lines = new List(); foreach (var ((artist, character), count) in countDictionary /*.TakeWhile(_ => realCount < 5000)*/) { var artistCount = countsByTagId[artist]; var characterCount = countsByTagId[character]; if (characterCount - count < MIN_COUNT_BY_ARTIST_TO_CHARACTER_FOR_FAGGING) { continue; } if (count < artistCount * 0.1) { continue; } realCount++; lines.Add(string.Format( "+------T------+\tartist = {0,-30}\tcharacter = {1}", tagDictionary[artist].tag, tagDictionary[character].tag )); lines.Add(string.Format( "|{0,6}|{1,6}|", 0, artistCount - count )); lines.Add("+------+------+"); lines.Add(string.Format( "|{0,6}|{1,6}|", characterCount - count, count )); lines.Add("+------+------+"); lines.Add(string.Format( "Arts by the artist to this character: {0,-6:P1}\n" + "Arts with the char by this artist: {1,-6:P1}", count * 1d / artistCount, count * 1d / characterCount )); lines.Add("============================================="); } await File.WriteAllLinesAsync(@"waifu-list.txt", lines, CancellationToken); Console.WriteLine( "{0}\twaifu-list.txt done", DateTimeOffset.UtcNow ); } { // 1й, 2й, 3й персонаж для каждого художника var count123 = semiRawData .GroupBy(t => t.key.artist) .Select(t => { var sum = t.Sum(t1 => t1.count); if (sum < MIN_CHAR_COUNT_TO_CREATE_STAT) { return Array.Empty(); } var r = 1d / sum; var a = t .OrderByDescending(t1 => t1.count) .Take(3) .Select(t1 => t1.count * r) .ToArray(); return a.Length switch { 3 => a, 1 => a.Concat(new[] {0d, 0d}).ToArray(), 2 => a.Concat(new[] {0d}).ToArray(), _ => throw new ArgumentOutOfRangeException() }; }) .Where(x => x.Any()) .ToArray(); Console.WriteLine( "{0}\tcount123.Length = {1,7}", DateTimeOffset.UtcNow, count123.Count() ); // TODO 1й, 2й, 3й персонаж для каждого художника var textInput = JsonConvert.SerializeObject(count123); await File.WriteAllTextAsync(@"count123-input.json", textInput); } { var rawCharFag = semiRawData .Where(x => (x.count >= MIN_COUNT_BY_ARTIST_TO_CHARACTER_FOR_FAGGING) && (x.countP >= MIN_COUNT_PERCENT_BY_ARTIST_TO_CHARACTER_FOR_FAGGING)) .Select(t => (fagPoint: t.countP + Math.Log10(t.count) * FAG_POINTS_LOG10_MULTIPLIER, t.key.character)) .ToArray(); var fagCharacters = rawCharFag .GroupBy(t => t.character) .Select(t => (character: t.Key, sum: t.Sum(t1 => t1.fagPoint), count: t.Count())) .OrderByDescending(t => t.sum) .ToArray(); Console.WriteLine( "{0}\trawCharFag.Length = {1,7}" + "\n\t\t\t\tfagCharacters = {2,7}", DateTimeOffset.UtcNow, rawCharFag.Count(), fagCharacters.Count() ); var lines = new List(); foreach (var (charId, sum, count) in fagCharacters) { var character = tagDictionary[charId]; lines.Add(string.Format("{0}\t{1:F2}\t{2}", character.tag, sum, count)); } await File.WriteAllLinesAsync(@"character-fag-list.txt", lines, CancellationToken); Console.WriteLine( "{0}\tfag-list.txt done", DateTimeOffset.UtcNow ); // var franchiseFagDic = fagCharacters .Select(t => { var (charId, fagPoint, _) = t; if (!characterToCopyright.ContainsKey(charId)) { return (copyright: -1, fagPoint: 0d); } var copyright = characterToCopyright[charId]; return (copyright, fagPoint); }) .Where(x => x.copyright != -1) .GroupBy(t => t.copyright) .ToDictionary( t => t.Key, t => ( tag: tagDictionary[t.Key], rawSum: t.Sum(t1 => t1.fagPoint), limitedSum: t.Sum(t1 => Math.Min(MAX_FAG_POINTS_PER_CHARACTER, t1.fagPoint)), charCount: t.Count() ) ); var lines1 = franchiseFagDic .OrderByDescending(t => t.Value.rawSum) .Take(100) .Select(t => { var fagPoint = t.Value.rawSum; return string.Format("{0}\t{1:F2}\t{2}", t.Value.tag.tag, fagPoint, t.Value.charCount); }) .ToArray(); await File.WriteAllLinesAsync(@"franchise-fag-list1.txt", lines1, CancellationToken); var lines2 = franchiseFagDic .OrderByDescending(t => t.Value.limitedSum) .Take(100) .Select(t => { var fagPoint = t.Value.limitedSum; return string.Format("{0}\t{1:F2}\t{2}", t.Value.tag.tag, fagPoint, t.Value.charCount); }) .ToArray(); await File.WriteAllLinesAsync(@"franchise-fag-list2.txt", lines2, CancellationToken); Console.WriteLine( "{0}\tfranchise-fag-list [both].txt done", DateTimeOffset.UtcNow ); } } #endregion #region CalculateMeanCount123 private async Task CalculateMeanCount123() { List<(double[] v, int count)[]> count123Chunks; const int CHUNK_SIZE = 1000; const int MIN_CHUNK_SIZE = 500; { var text = await File.ReadAllTextAsync(@"count123-input.json"); var rawCount123 = JsonConvert.DeserializeObject(text); var rnd = new Random(); var lists = new List<(double[] v, int count)[]>(); var currentList = new List<(double[] v, int count)>(); foreach (var values in rawCount123.OrderBy(_ => rnd.NextDouble())) { currentList.Add((values, 1)); if (currentList.Count() >= CHUNK_SIZE) { lists.Add(currentList .OrderByDescending(t => t.v[0]) .ThenByDescending(t => t.v[1]) .ToArray()); currentList = new List<(double[] v, int count)>(); } } if (currentList.Any()) { lists.Add(currentList .OrderByDescending(t => t.v[0]) .ThenByDescending(t => t.v[1]) .ToArray()); } count123Chunks = lists.ToList(); } var fullCount = count123Chunks.Sum(t => t.Sum(t1 => t1.count)); (double[] v, int count)[] count123 = count123Chunks.First(); count123Chunks.RemoveAt(0); var sw = Stopwatch.StartNew(); while (count123.Length > 2) { var len = count123.Length; Console.WriteLine( "\ncount123.Length = {0} + {1,4} * {2,5}", count123.Length, count123Chunks.Count(), CHUNK_SIZE ); var calculatedPairs = Enumerable .Range(1, len - 1) .SelectMany(index1 => { return Enumerable .Range(0, index1) .Select(index2 => (index1, index2)); }) .AsParallel() .Select(t => { var (index1, index2) = t; // ReSharper disable once AccessToModifiedClosure var item1 = count123[index1]; // ReSharper disable once AccessToModifiedClosure var item2 = count123[index2]; var sqrt = 0d; for (var i = 0; i < 3; i++) { sqrt += Math.Pow(item1.v[i] - item2.v[i], 2); } sqrt = Math.Sqrt(sqrt / 3d); return (index1: index1, index2: index2, sqrt); }) .OrderBy(t => t.sqrt) .ToArray(); var bestPair = calculatedPairs.First(); Console.WriteLine(bestPair); if (bestPair.sqrt > 0.05) { break; } { var item1 = count123[bestPair.index1]; var item2 = count123[bestPair.index2]; var r1 = item1.count * 1d / (item1.count + item2.count); var r2 = 1 - r1; var newItem = ( v: Enumerable.Range(0, 3).Select(i => item1.v[i] * r1 + item2.v[i] * r2).ToArray(), count: item1.count + item2.count ); Console.WriteLine("\tnew item\t{1,-5}\t{0}", newItem.v.Select(t => t.ToString("F3")).Aggregate((a, b) => a + ", " + b), newItem.count); count123 = count123 .Select((t, index) => (t, index)) .Where(x => (x.index != bestPair.index1) && (x.index != bestPair.index2)) .Select(t => t.t) .Concat(new[] {newItem}) .OrderByDescending(t => t.count) .ToArray(); } if ((count123.Length <= MIN_CHUNK_SIZE) && (count123Chunks.Any())) { var elapsed = sw.Elapsed; var leftCount = count123.Length + count123Chunks.Count * CHUNK_SIZE; var p = 1d - leftCount * 1d / fullCount; Console.WriteLine( "--------------------------\t{0,-7:P1} (elapsed: {1})\tETA: {2}", p, elapsed, (1 - p) / p * elapsed ); var r = 1d / count123.Sum(t => t.count); for (int i = 0; i < Math.Min(10, count123.Length); i++) { var (v, count) = count123[i]; Console.WriteLine("\t{1,-5}\t{0}\t{2,6:P1}", v.Select(t => t.ToString("F3")).Aggregate((a, b) => a + ", " + b), count, count * r ); } count123 = count123 .Concat(count123Chunks.First()) .ToArray(); count123Chunks.RemoveAt(0); } } Console.WriteLine("=================================================="); Console.WriteLine("=================================================="); Console.WriteLine("=================================================="); var rFullCount = 1d / fullCount; for (int i = 0; i < Math.Min(20, count123.Length); i++) { var (v, count) = count123[i]; Console.WriteLine("\t{1,-5}\t{0}\t{2,6:P1}", v.Select(t => t.ToString("F3")).Aggregate((a, b) => a + ", " + b), count, count * rFullCount ); } } #endregion } }