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
}
}