Skip to content

Commit 6d1ecb7

Browse files
Improved PowerPoint implementation for reading slide data (#517)
1 parent 68f5bb1 commit 6d1ecb7

File tree

9 files changed

+208
-57
lines changed

9 files changed

+208
-57
lines changed

app/MindWork AI Studio/Tools/ContentStreamSseHandler.cs

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace AIStudio.Tools;
66
public static class ContentStreamSseHandler
77
{
88
private static readonly ConcurrentDictionary<string, List<ContentStreamPptxImageData>> CHUNKED_IMAGES = new();
9-
private static readonly ConcurrentDictionary<string, int> CURRENT_SLIDE_NUMBERS = new();
9+
private static readonly ConcurrentDictionary<string, SlideManager> SLIDE_MANAGERS = new();
1010

1111
public static string? ProcessEvent(ContentStreamSseEvent? sseEvent, bool extractImages = true)
1212
{
@@ -44,31 +44,13 @@ public static class ContentStreamSseHandler
4444
return sseEvent.Content;
4545

4646
case ContentStreamPresentationMetadata presentationMetadata:
47-
var slideNumber = presentationMetadata.Presentation?.SlideNumber ?? 0;
48-
var image = presentationMetadata.Presentation?.Image ?? null;
49-
var presentationResult = new StringBuilder();
50-
var streamId = sseEvent.StreamId;
47+
var slideManager = SLIDE_MANAGERS.GetOrAdd(
48+
sseEvent.StreamId!,
49+
_ => new()
50+
);
5151

52-
CURRENT_SLIDE_NUMBERS.TryGetValue(streamId!, out var currentSlideNumber);
53-
if (slideNumber != currentSlideNumber)
54-
{
55-
presentationResult.AppendLine();
56-
presentationResult.AppendLine($"# Slide {slideNumber}");
57-
}
58-
59-
if(!string.IsNullOrWhiteSpace(sseEvent.Content))
60-
presentationResult.AppendLine(sseEvent.Content);
61-
62-
if (extractImages && image is not null)
63-
{
64-
var imageId = $"{streamId}-{image.Id!}";
65-
var isEnd = ProcessImageSegment(imageId, image);
66-
if (isEnd && extractImages)
67-
presentationResult.AppendLine(BuildImage(imageId));
68-
}
69-
70-
CURRENT_SLIDE_NUMBERS[streamId!] = slideNumber;
71-
return presentationResult.Length is 0 ? null : presentationResult.ToString();
52+
slideManager.AddSlide(presentationMetadata, sseEvent.Content, extractImages);
53+
return null;
7254

7355
default:
7456
return sseEvent.Content;
@@ -81,8 +63,8 @@ public static class ContentStreamSseHandler
8163
return null;
8264
}
8365
}
84-
85-
private static bool ProcessImageSegment(string imageId, ContentStreamPptxImageData contentStreamPptxImageData)
66+
67+
public static bool ProcessImageSegment(string imageId, ContentStreamPptxImageData contentStreamPptxImageData)
8668
{
8769
if (string.IsNullOrWhiteSpace(contentStreamPptxImageData.Id) || string.IsNullOrWhiteSpace(imageId))
8870
return false;
@@ -112,7 +94,7 @@ private static bool ProcessImageSegment(string imageId, ContentStreamPptxImageDa
11294
return isEnd;
11395
}
11496

115-
private static string BuildImage(string id)
97+
public static string BuildImage(string id)
11698
{
11799
if (!CHUNKED_IMAGES.TryGetValue(id, out var imageSegments))
118100
return string.Empty;
@@ -128,4 +110,25 @@ private static string BuildImage(string id)
128110
CHUNKED_IMAGES.Remove(id, out _);
129111
return base64Image;
130112
}
113+
114+
public static string? Clear(string streamId)
115+
{
116+
if (string.IsNullOrWhiteSpace(streamId))
117+
return null;
118+
119+
var finalContentChunk = new StringBuilder();
120+
if(SLIDE_MANAGERS.TryGetValue(streamId, out var slideManager))
121+
{
122+
var result = slideManager.GetAllSlidesInOrder();
123+
if (!string.IsNullOrWhiteSpace(result))
124+
finalContentChunk.Append(result);
125+
}
126+
127+
SLIDE_MANAGERS.TryRemove(streamId, out _);
128+
var imageIdPrefix = $"{streamId}-";
129+
foreach (var key in CHUNKED_IMAGES.Keys.Where(k => k.StartsWith(imageIdPrefix, StringComparison.InvariantCultureIgnoreCase)))
130+
CHUNKED_IMAGES.TryRemove(key, out _);
131+
132+
return finalContentChunk.Length > 0 ? finalContentChunk.ToString() : null;
133+
}
131134
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace AIStudio.Tools;
2+
3+
public interface ISlideContent;

app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,52 @@ public async Task<string> ReadArbitraryFileData(string path, int maxChunks, bool
1515
if (!response.IsSuccessStatusCode)
1616
return string.Empty;
1717

18-
await using var stream = await response.Content.ReadAsStreamAsync();
19-
using var reader = new StreamReader(stream);
20-
2118
var resultBuilder = new StringBuilder();
22-
var chunkCount = 0;
2319

24-
while (!reader.EndOfStream && chunkCount < maxChunks)
20+
try
2521
{
26-
var line = await reader.ReadLineAsync();
27-
if (string.IsNullOrWhiteSpace(line))
28-
continue;
29-
30-
if (!line.StartsWith("data:", StringComparison.InvariantCulture))
31-
continue;
32-
33-
var jsonContent = line[5..];
34-
35-
try
22+
await using var stream = await response.Content.ReadAsStreamAsync();
23+
using var reader = new StreamReader(stream);
24+
var chunkCount = 0;
25+
26+
while (!reader.EndOfStream && chunkCount < maxChunks)
3627
{
37-
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
38-
if (sseEvent is not null)
28+
var line = await reader.ReadLineAsync();
29+
if (string.IsNullOrWhiteSpace(line))
30+
continue;
31+
32+
if (!line.StartsWith("data:", StringComparison.InvariantCulture))
33+
continue;
34+
35+
var jsonContent = line[5..];
36+
37+
try
3938
{
40-
var content = ContentStreamSseHandler.ProcessEvent(sseEvent, extractImages);
41-
if(content is not null)
42-
resultBuilder.AppendLine(content);
43-
44-
chunkCount++;
39+
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
40+
if (sseEvent is not null)
41+
{
42+
var content = ContentStreamSseHandler.ProcessEvent(sseEvent, extractImages);
43+
if (content is not null)
44+
resultBuilder.AppendLine(content);
45+
46+
chunkCount++;
47+
}
48+
}
49+
catch (JsonException)
50+
{
51+
this.logger?.LogError("Failed to deserialize SSE event: {JsonContent}", jsonContent);
4552
}
4653
}
47-
catch (JsonException)
48-
{
49-
this.logger?.LogError("Failed to deserialize SSE event: {JsonContent}", jsonContent);
50-
}
54+
}
55+
catch(Exception e)
56+
{
57+
this.logger?.LogError(e, "Error reading file data from stream: {Path}", path);
58+
}
59+
finally
60+
{
61+
var finalContentChunk = ContentStreamSseHandler.Clear(streamId);
62+
if (!string.IsNullOrWhiteSpace(finalContentChunk))
63+
resultBuilder.AppendLine(finalContentChunk);
5164
}
5265

5366
return resultBuilder.ToString();

app/MindWork AI Studio/Tools/Slide.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace AIStudio.Tools;
2+
3+
public sealed class Slide
4+
{
5+
public bool Delivered { get; set; }
6+
7+
public int Position { get; init; }
8+
9+
public List<ISlideContent> Content { get; } = new();
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.Text;
2+
3+
namespace AIStudio.Tools;
4+
5+
public sealed class SlideImageContent(string base64Image) : ISlideContent
6+
{
7+
public StringBuilder Base64Image => new(base64Image);
8+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System.Text;
2+
3+
namespace AIStudio.Tools;
4+
5+
public sealed class SlideManager
6+
{
7+
private readonly Dictionary<int, Slide> slides = new();
8+
9+
public void AddSlide(ContentStreamPresentationMetadata metadata, string? content, bool extractImages = false)
10+
{
11+
var slideNumber = metadata.Presentation?.SlideNumber ?? 0;
12+
if(slideNumber is 0)
13+
return;
14+
15+
var image = metadata.Presentation?.Image ?? null;
16+
var addImage = false;
17+
if (extractImages && image is not null)
18+
{
19+
var isEnd = ContentStreamSseHandler.ProcessImageSegment(image.Id!, image);
20+
if (isEnd)
21+
addImage = true;
22+
}
23+
24+
if (!this.slides.TryGetValue(slideNumber, out var slide))
25+
{
26+
//
27+
// Case: No existing slide content for this slide number.
28+
//
29+
30+
var contentBuilder = new StringBuilder();
31+
contentBuilder.AppendLine();
32+
contentBuilder.AppendLine($"# Slide {slideNumber}");
33+
34+
// Add any text content to the slide?
35+
if(!string.IsNullOrWhiteSpace(content))
36+
contentBuilder.AppendLine(content);
37+
38+
//
39+
// Add the text content to the slide:
40+
//
41+
var slideText = new SlideTextContent(contentBuilder.ToString());
42+
var createdSlide = new Slide
43+
{
44+
Delivered = false,
45+
Position = slideNumber
46+
};
47+
48+
createdSlide.Content.Add(slideText);
49+
50+
//
51+
// Add image content to the slide?
52+
//
53+
if (addImage)
54+
{
55+
var img = ContentStreamSseHandler.BuildImage(image!.Id!);
56+
var slideImage = new SlideImageContent(img);
57+
createdSlide.Content.Add(slideImage);
58+
}
59+
60+
this.slides[slideNumber] = createdSlide;
61+
}
62+
else
63+
{
64+
//
65+
// Case: Existing slide content for this slide number.
66+
//
67+
68+
// Add any text content?
69+
if (!string.IsNullOrWhiteSpace(content))
70+
{
71+
var textContent = slide.Content.OfType<SlideTextContent>().First();
72+
textContent.Text.AppendLine(content);
73+
}
74+
75+
// Add any image content?
76+
if (addImage)
77+
{
78+
var img = ContentStreamSseHandler.BuildImage(image!.Id!);
79+
var slideImage = new SlideImageContent(img);
80+
slide.Content.Add(slideImage);
81+
}
82+
}
83+
}
84+
85+
public string? GetAllSlidesInOrder()
86+
{
87+
var content = new StringBuilder();
88+
foreach (var slide in this.slides.Values.Where(s => !s.Delivered).OrderBy(s => s.Position))
89+
{
90+
slide.Delivered = true;
91+
foreach (var text in slide.Content.OfType<SlideTextContent>())
92+
{
93+
content.AppendLine(text.Text.ToString());
94+
content.AppendLine();
95+
}
96+
97+
foreach (var image in slide.Content.OfType<SlideImageContent>())
98+
{
99+
content.AppendLine(image.Base64Image.ToString());
100+
content.AppendLine();
101+
}
102+
}
103+
104+
return content.Length > 0 ? content.ToString() : null;
105+
}
106+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.Text;
2+
3+
namespace AIStudio.Tools;
4+
5+
public sealed class SlideTextContent(string textContent) : ISlideContent
6+
{
7+
public StringBuilder Text => new(textContent);
8+
}

runtime/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

runtime/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ calamine = "0.28.0"
3838
pdfium-render = "0.8.33"
3939
sys-locale = "0.3.2"
4040
cfg-if = "1.0.1"
41-
pptx-to-md = "0.3.0"
41+
pptx-to-md = "0.4.0"
4242

4343
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
4444
url = "2.5"

0 commit comments

Comments
 (0)