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

This commit is contained in:
nilskruthoff 2026-07-21 12:02:32 +02:00 committed by GitHub
parent 36194a545d
commit 33b0850c71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 571 additions and 224 deletions

View File

@ -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");

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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,30 +464,78 @@ 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(
md_content,
content,
Metadata::Presentation {
slide_number: slide.slide_number,
image: None,
}
);
if tx.send(Ok(chunk)).await.is_err() {
break;
}
if tx.blocking_send(Ok(chunk)).is_err() {
return;
}
if let Some(images) = slide.load_images_manually() {
@ -513,8 +565,8 @@ async fn stream_pptx(file_path: &str, extract_images: bool) -> Result<ChunkStrea
}
);
if tx.send(Ok(chunk)).await.is_err() {
break;
if tx.blocking_send(Ok(chunk)).is_err() {
return;
}
offset = end;
@ -522,14 +574,57 @@ async fn stream_pptx(file_path: &str, extract_images: bool) -> Result<ChunkStrea
}
}
}
},
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("--", "&#45;&#45;")
}