mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-07-22 22:06:27 +00:00
Improved slide import function (#878)
Some checks are pending
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-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (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
Some checks are pending
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-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (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
This commit is contained in:
parent
36194a545d
commit
33b0850c71
@ -48,7 +48,7 @@ public static class FileTypes
|
||||
public static readonly FileTypeFilter MS_WORD = FileTypeFilter.Leaf("Microsoft Word", "docx");
|
||||
public static readonly FileTypeFilter WORD = FileTypeFilter.Composite("Word", ["odt"], MS_WORD);
|
||||
public static readonly FileTypeFilter EXCEL = FileTypeFilter.Leaf("Excel", "xls", "xlsx");
|
||||
public static readonly FileTypeFilter POWER_POINT = FileTypeFilter.Leaf("PowerPoint", "ppt", "pptx");
|
||||
public static readonly FileTypeFilter POWER_POINT = FileTypeFilter.Leaf("PowerPoint", "ppt", "pptx", "odp");
|
||||
public static readonly FileTypeFilter MAIL = FileTypeFilter.Leaf(TB("Mail"), "eml", "msg", "mbox");
|
||||
public static readonly FileTypeFilter LATEX = FileTypeFilter.Leaf("LaTeX", "tex", "bib", "sty", "cls", "log");
|
||||
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
# v26.7.3, build 248 (2026-07-19 20:50 UTC)
|
||||
- Added support for OpenAI GPT-5.6 Sol, Terra, and Luna; Anthropic Claude Fable 5 and Mythos 5; and Google Gemini 3 Flash, Gemini 3.1 Flash-Lite, Gemini 3.1 Pro, and Gemini 3.5 Flash.
|
||||
- Added support for OpenDocument presentations (`.odp`) when attaching and reading presentation files.
|
||||
- Added a log viewer assistant that shows AI Studio log files in a read-only view with search, log filters, highlighting, and auto-refresh.
|
||||
- Added audio and video transcription for chats and assistants. AI Studio now prepares supported media locally, sends only normalized audio to the configured transcription provider, and attaches the resulting transcript instead of the original media.
|
||||
- Added AI-assisted editing and revision for assistants created with the Assistant Builder. Thanks, Nils Kruthoff (`nilskruthoff`), for this contribution.
|
||||
- Added options to view and edit the code of AI-generated assistants and to delete your own generated assistants. Thanks, Nils Kruthoff (`nilskruthoff`), for this contribution.
|
||||
- Added enterprise configuration options to hide the last changelog and vision panels on the welcome page. Thanks, Dominic Neuburg (`donework`), for the contribution.
|
||||
- Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks.
|
||||
- Improved presentation imports so AI Studio can include speaker notes, slide comments, and presentation metadata in the extracted content.
|
||||
- Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.
|
||||
- Improved secure API-key storage diagnostics on Linux. AI Studio now provides specific guidance when the default password collection is missing or locked, a password-manager prompt is dismissed, or no compatible Secret Service is available.
|
||||
- Improved the file dialogs to prevent opening multiple times when you click "Open" or "Save" multiple times in a row.
|
||||
|
||||
584
runtime/Cargo.lock
generated
584
runtime/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
||||
@ -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("--", "--")
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user