mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-04 21:49:40 +00:00
Improved slide import function (#878)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
This commit is contained in:
1 parent
36194a545d
commit
33b0850c71
5 files changed
+571
-224
No files matched your search
Generated
+417
-167
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -49,7 +49,7 @@ pdfium-render = "0.9.1"
|
||||
sys-locale = "0.3.2"
|
||||
whoami = "2.1.2"
|
||||
cfg-if = "1.0.4"
|
||||
pptx-to-md = "0.4.0"
|
||||
pptx-to-md = "1.0.0"
|
||||
tempfile = "3.27.0"
|
||||
strum_macros = "0.28.0"
|
||||
sysinfo = "0.39.6"
|
||||
|
||||
+149
-54
@@ -12,7 +12,7 @@ use calamine::{open_workbook_auto, Reader};
|
||||
use file_format::{FileFormat, Kind};
|
||||
use futures::{Stream, StreamExt};
|
||||
use pdfium_render::prelude::Pdfium;
|
||||
use pptx_to_md::{ImageHandlingMode, ParserConfig, PptxContainer};
|
||||
use pptx_to_md::{DiagnosticSeverity, ImageHandlingMode, MarkdownOptions, ParserConfig, PresentationContainer, PresentationFormat, PresentationMetadata, ReadingOrder};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde::de::{Error as SerdeError, Visitor};
|
||||
use std::path::Path;
|
||||
@@ -207,7 +207,8 @@ async fn stream_data(file_path: &str, extract_images: bool) -> Result<ChunkStrea
|
||||
stream_text_file(file_path, true, Some("csv".to_string())).await?
|
||||
},
|
||||
|
||||
"pptx" => stream_pptx(file_path, extract_images).await?,
|
||||
"pptx" => stream_presentation(file_path, extract_images, PresentationFormat::Pptx).await?,
|
||||
"odp" => stream_presentation(file_path, extract_images, PresentationFormat::Odp).await?,
|
||||
|
||||
"xlsx" | "ods" | "xls" | "xlsm" | "xlsb" | "xla" | "xlam" => {
|
||||
stream_spreadsheet_as_csv(file_path).await?
|
||||
@@ -248,8 +249,11 @@ async fn stream_data(file_path: &str, extract_images: bool) -> Result<ChunkStrea
|
||||
|
||||
Kind::Presentation => match fmt {
|
||||
FileFormat::OfficeOpenXmlPresentation => {
|
||||
stream_pptx(file_path, extract_images).await?
|
||||
stream_presentation(file_path, extract_images, PresentationFormat::Pptx).await?
|
||||
},
|
||||
FileFormat::OpendocumentPresentation => {
|
||||
stream_presentation(file_path, extract_images, PresentationFormat::Odp).await?
|
||||
}
|
||||
|
||||
_ => stream_text_file(file_path, false, None).await?,
|
||||
},
|
||||
@@ -452,7 +456,7 @@ async fn chunk_image(file_path: &str) -> Result<ChunkStream> {
|
||||
Ok(Box::pin(stream))
|
||||
}
|
||||
|
||||
async fn stream_pptx(file_path: &str, extract_images: bool) -> Result<ChunkStream> {
|
||||
async fn stream_presentation(file_path: &str, extract_images: bool, format: PresentationFormat) -> Result<ChunkStream> {
|
||||
let path = Path::new(file_path).to_owned();
|
||||
|
||||
let parser_config = ParserConfig::builder()
|
||||
@@ -460,76 +464,167 @@ async fn stream_pptx(file_path: &str, extract_images: bool) -> Result<ChunkStrea
|
||||
.compress_images(true)
|
||||
.quality(75)
|
||||
.image_handling_mode(ImageHandlingMode::Manually)
|
||||
.include_presentation_metadata(true)
|
||||
.build();
|
||||
|
||||
let markdown_options = MarkdownOptions {
|
||||
reading_order: ReadingOrder::Spatial,
|
||||
include_slide_number_as_comment: true,
|
||||
include_speaker_notes: true,
|
||||
include_comments: true,
|
||||
render_unsupported_comments: true,
|
||||
};
|
||||
|
||||
let mut streamer = tokio::task::spawn_blocking(move || {
|
||||
PptxContainer::open(&path, parser_config).map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
PresentationContainer::open_as(&path, parser_config, format).map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||
}).await??;
|
||||
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
let worker_error_tx = tx.clone();
|
||||
|
||||
// Slide iteration performs synchronous ZIP/XML work and image compression,
|
||||
// so the complete producer must stay outside Tokio's asynchronous workers.
|
||||
let worker = tokio::task::spawn_blocking(move || {
|
||||
let mut metadata_md = presentation_metadata_to_markdown(streamer.metadata());
|
||||
|
||||
tokio::spawn(async move {
|
||||
for slide_result in streamer.iter_slides() {
|
||||
match slide_result {
|
||||
Ok(slide) => {
|
||||
if let Some(md_content) = slide.convert_to_md() {
|
||||
let slide = match slide_result {
|
||||
Ok(slide) => slide,
|
||||
Err(e) => {
|
||||
let _ = tx.blocking_send(Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>));
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
for diagnostic in &slide.diagnostics {
|
||||
let source = diagnostic.source.as_deref().unwrap_or("presentation");
|
||||
match diagnostic.severity {
|
||||
DiagnosticSeverity::Warning => warn!(
|
||||
"Presentation slide {} warning in '{}': {}",
|
||||
slide.slide_number,
|
||||
source,
|
||||
diagnostic.message
|
||||
),
|
||||
DiagnosticSeverity::Error => error!(
|
||||
"Presentation slide {} error in '{}': {}",
|
||||
slide.slide_number,
|
||||
source,
|
||||
diagnostic.message
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let mut content = match slide.to_markdown(&markdown_options) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
let _ = tx.blocking_send(Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>));
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(metadata) = metadata_md.take() {
|
||||
content = format!("{metadata}\n\n{content}");
|
||||
}
|
||||
|
||||
let chunk = Chunk::new(
|
||||
content,
|
||||
Metadata::Presentation {
|
||||
slide_number: slide.slide_number,
|
||||
image: None,
|
||||
}
|
||||
);
|
||||
|
||||
if tx.blocking_send(Ok(chunk)).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(images) = slide.load_images_manually() {
|
||||
for image in images.iter() {
|
||||
let base64_data = &image.base64_content;
|
||||
let total_length = base64_data.len();
|
||||
let mut offset = 0;
|
||||
let mut segment_index = 0;
|
||||
|
||||
while offset < total_length {
|
||||
let end = min(offset + IMAGE_SEGMENT_SIZE_IN_CHARS, total_length);
|
||||
let segment_content = &base64_data[offset..end];
|
||||
let is_end = end == total_length;
|
||||
|
||||
let base64_image = Base64Image::new(
|
||||
image.img_ref.id.clone(),
|
||||
segment_content.to_string(),
|
||||
segment_index,
|
||||
is_end
|
||||
);
|
||||
|
||||
let chunk = Chunk::new(
|
||||
md_content,
|
||||
String::new(),
|
||||
Metadata::Presentation {
|
||||
slide_number: slide.slide_number,
|
||||
image: None,
|
||||
image: Some(base64_image),
|
||||
}
|
||||
);
|
||||
|
||||
if tx.send(Ok(chunk)).await.is_err() {
|
||||
break;
|
||||
if tx.blocking_send(Ok(chunk)).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
offset = end;
|
||||
segment_index += 1;
|
||||
}
|
||||
|
||||
if let Some(images) = slide.load_images_manually() {
|
||||
for image in images.iter() {
|
||||
let base64_data = &image.base64_content;
|
||||
let total_length = base64_data.len();
|
||||
let mut offset = 0;
|
||||
let mut segment_index = 0;
|
||||
|
||||
while offset < total_length {
|
||||
let end = min(offset + IMAGE_SEGMENT_SIZE_IN_CHARS, total_length);
|
||||
let segment_content = &base64_data[offset..end];
|
||||
let is_end = end == total_length;
|
||||
|
||||
let base64_image = Base64Image::new(
|
||||
image.img_ref.id.clone(),
|
||||
segment_content.to_string(),
|
||||
segment_index,
|
||||
is_end
|
||||
);
|
||||
|
||||
let chunk = Chunk::new(
|
||||
String::new(),
|
||||
Metadata::Presentation {
|
||||
slide_number: slide.slide_number,
|
||||
image: Some(base64_image),
|
||||
}
|
||||
);
|
||||
|
||||
if tx.send(Ok(chunk)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
offset = end;
|
||||
segment_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let _ = tx.send(Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = worker.await {
|
||||
let _ = worker_error_tx.send(Err(format!("Presentation parser task failed: {e}").into())).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::pin(ReceiverStream::new(rx)))
|
||||
}
|
||||
|
||||
fn presentation_metadata_to_markdown(metadata: &PresentationMetadata) -> Option<String> {
|
||||
let mut fields = Vec::new();
|
||||
push_presentation_metadata_field(&mut fields, "Title", metadata.title.as_deref());
|
||||
push_presentation_metadata_field(&mut fields, "Author", metadata.author.as_deref());
|
||||
push_presentation_metadata_field(&mut fields, "Last Modified By", metadata.last_modified_by.as_deref());
|
||||
push_presentation_metadata_field(&mut fields, "Subject", metadata.subject.as_deref());
|
||||
push_presentation_metadata_field(&mut fields, "Description", metadata.description.as_deref());
|
||||
if !metadata.keywords.is_empty() {
|
||||
fields.push(format!(
|
||||
"Keywords: {}",
|
||||
sanitize_presentation_metadata_value(&metadata.keywords.join("; "))
|
||||
));
|
||||
}
|
||||
push_presentation_metadata_field(&mut fields, "Created", metadata.created_at.as_deref());
|
||||
push_presentation_metadata_field(&mut fields, "Modified", metadata.modified_at.as_deref());
|
||||
|
||||
if fields.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!(
|
||||
"<!-- Presentation Metadata\n{}\n-->",
|
||||
fields.join("\n")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn push_presentation_metadata_field(fields: &mut Vec<String>, label: &str, value: Option<&str>) {
|
||||
if let Some(value) = value {
|
||||
fields.push(format!(
|
||||
"{label}: {}",
|
||||
sanitize_presentation_metadata_value(value)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_presentation_metadata_value(value: &str) -> String {
|
||||
value
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.replace("--", "--")
|
||||
}
|
||||
Reference in new issue
Block a user