mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 13:47:21 +00:00
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
101 lines
5.3 KiB
C#
101 lines
5.3 KiB
C#
namespace AIStudio.Tools.PowerPoint;
|
|
|
|
/// <summary>
|
|
/// Reads a folder of pictures and icons, which the LLM may choose from by their names.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The LLM reads only the names, never the pictures: a library holds hundreds or thousands of them.
|
|
/// Only PNG and JPEG pictures count, since the writer can place nothing else; many icons come as SVG,
|
|
/// which is why the other files are counted, so that the user learns about them. Hidden files and
|
|
/// folders are left out, as everywhere in AI Studio.
|
|
/// </remarks>
|
|
internal static class PictureLibrary
|
|
{
|
|
/// <summary>
|
|
/// The most pictures of a library the LLM reads about in one request.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Every name costs tokens in every request, and 5,000 names come to about 50,000 tokens. A
|
|
/// larger library is sampled at random, anew for every request, so that every picture has the
|
|
/// same chance rather than those which the file system happens to list first.
|
|
/// </remarks>
|
|
public const int MAX_LIBRARY_PICTURES = 5_000;
|
|
|
|
/// <summary>
|
|
/// What the name of an icon, or of one of the folders it is in, starts with, in any case.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The library folder counts as well, so that a folder of icons, such as "Icons", makes all of
|
|
/// its pictures icons. The folders above the library do not count: they say where the library
|
|
/// lies, not what it holds.
|
|
/// </remarks>
|
|
public const string ICON_PREFIX = "icon";
|
|
|
|
private static readonly HashSet<string> PICTURE_EXTENSIONS = new(StringComparer.OrdinalIgnoreCase) { ".png", ".jpg", ".jpeg" };
|
|
|
|
/// <summary>
|
|
/// Reads a library on a thread of its own, since a folder on a network drive may take a while.
|
|
/// </summary>
|
|
/// <param name="folder">The library folder.</param>
|
|
/// <param name="random">Where the sample of a large library comes from.</param>
|
|
/// <param name="maxPictures">The most pictures the sample holds, e.g., MAX_LIBRARY_PICTURES, or 0 to count the pictures only.</param>
|
|
/// <param name="reportInaccessibleFolder">Told about every folder which could not be read.</param>
|
|
/// <param name="token">The cancellation token.</param>
|
|
/// <returns>What the library offers.</returns>
|
|
public static Task<PictureLibraryContent> ReadAsync(string folder, Random random, int maxPictures, Action<string, Exception>? reportInaccessibleFolder, CancellationToken token) => Task.Run(() => Read(folder, random, maxPictures, reportInaccessibleFolder, token), token);
|
|
|
|
/// <summary>
|
|
/// Reads a library.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The sample is drawn while the folder is read, by reservoir sampling: the first pictures fill
|
|
/// the sample, and each later one replaces a random member of it with the chance that keeps every
|
|
/// picture equally likely. So the list of all pictures is never held in memory.
|
|
/// </remarks>
|
|
/// <param name="folder">The library folder.</param>
|
|
/// <param name="random">Where the sample of a large library comes from.</param>
|
|
/// <param name="maxPictures">The most pictures the sample holds.</param>
|
|
/// <param name="reportInaccessibleFolder">Told about every folder which could not be read.</param>
|
|
/// <param name="token">The cancellation token.</param>
|
|
/// <returns>What the library offers.</returns>
|
|
public static PictureLibraryContent Read(string folder, Random random, int maxPictures, Action<string, Exception>? reportInaccessibleFolder, CancellationToken token)
|
|
{
|
|
var sample = new List<LibraryPicture>();
|
|
var isIconFolder = StartsWithIconPrefix(Path.GetFileName(Path.TrimEndingDirectorySeparator(folder)));
|
|
var pictureCount = 0;
|
|
var iconCount = 0;
|
|
var skippedFileCount = 0;
|
|
foreach (var path in DirectoryFileTraversal.EnumerateFiles(folder, true, false, reportInaccessibleDirectory: reportInaccessibleFolder))
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (!PICTURE_EXTENSIONS.Contains(Path.GetExtension(path)))
|
|
{
|
|
skippedFileCount++;
|
|
continue;
|
|
}
|
|
|
|
var name = Path.GetRelativePath(folder, path).Replace(Path.DirectorySeparatorChar, '/');
|
|
var picture = new LibraryPicture(path, name, isIconFolder || IsIcon(name));
|
|
pictureCount++;
|
|
if (picture.IsIcon)
|
|
iconCount++;
|
|
|
|
if (sample.Count < maxPictures)
|
|
sample.Add(picture);
|
|
else if (random.NextInt64(pictureCount) is var slot && slot < maxPictures)
|
|
sample[(int)slot] = picture;
|
|
}
|
|
|
|
sample.Sort((first, second) => StringComparer.OrdinalIgnoreCase.Compare(first.Name, second.Name));
|
|
return new PictureLibraryContent(sample, pictureCount, iconCount, skippedFileCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether a picture is an icon by its own path: its name or the name of one of its folders below the library starts with "icon".
|
|
/// </summary>
|
|
/// <param name="name">The path of the picture below the library folder, with forward slashes.</param>
|
|
private static bool IsIcon(string name) => name.Split('/').Any(StartsWithIconPrefix);
|
|
|
|
private static bool StartsWithIconPrefix(string name) => name.StartsWith(ICON_PREFIX, StringComparison.OrdinalIgnoreCase);
|
|
}
|