mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-04 23:29:40 +00:00
Added the visual briefing assistant (#893)
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Sync Flatpak repo (push) Has been cancelled
Build and Release / Collect Flatpak artifacts (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Sync Flatpak repo (push) Has been cancelled
Build and Release / Collect Flatpak artifacts (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
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) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
This commit is contained in:
1 parent
df4663fff4
commit
58cc811a58
214 files changed
+18943
-353
No files matched your search
@@ -46,7 +46,7 @@ pub struct SelectFileOptions {
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct SaveFileOptions {
|
||||
title: String,
|
||||
name_file: Option<PreviousFile>,
|
||||
previous_file: Option<PreviousFile>,
|
||||
filter: Option<FileTypeFilter>,
|
||||
}
|
||||
|
||||
@@ -275,10 +275,15 @@ pub async fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json
|
||||
// Set the file type filter if provided:
|
||||
file_dialog = apply_filter(file_dialog, &payload.filter);
|
||||
|
||||
// Set the previous file path if provided:
|
||||
if let Some(previous) = &payload.name_file {
|
||||
let previous_path = previous.file_path.as_str();
|
||||
file_dialog = file_dialog.set_directory(previous_path);
|
||||
// Set the initial directory and file name if provided:
|
||||
if let Some(previous) = &payload.previous_file {
|
||||
let (directory, file_name) = split_save_file_path(&previous.file_path);
|
||||
if let Some(directory) = directory {
|
||||
file_dialog = file_dialog.set_directory(directory);
|
||||
}
|
||||
if let Some(file_name) = file_name {
|
||||
file_dialog = file_dialog.set_file_name(file_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Displays the file dialogue box and select the file:
|
||||
@@ -396,6 +401,21 @@ fn apply_filter<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &O
|
||||
}
|
||||
}
|
||||
|
||||
fn split_save_file_path(file_path: &str) -> (Option<PathBuf>, Option<String>) {
|
||||
let path = Path::new(file_path);
|
||||
let directory = path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
.map(Path::to_path_buf);
|
||||
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.filter(|name| !name.is_empty());
|
||||
|
||||
(directory, file_name)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct FileManagerTarget {
|
||||
path: PathBuf,
|
||||
@@ -529,6 +549,39 @@ mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn save_file_options_accept_the_previous_file_contract() {
|
||||
let options: SaveFileOptions = serde_json::from_str(
|
||||
r#"{"title":"Export visual briefing","previous_file":{"file_path":"Quarterly briefing.html"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(options.title, "Export visual briefing");
|
||||
assert_eq!(
|
||||
options.previous_file.unwrap().file_path,
|
||||
"Quarterly briefing.html",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_file_name_without_directory_is_preserved() {
|
||||
let (directory, file_name) = split_save_file_path("Quarterly briefing.html");
|
||||
|
||||
assert_eq!(directory, None);
|
||||
assert_eq!(file_name.as_deref(), Some("Quarterly briefing.html"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_file_path_is_split_into_directory_and_name() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let initial_path = temp_dir.path().join("Quarterly briefing.html");
|
||||
|
||||
let (directory, file_name) = split_save_file_path(initial_path.to_str().unwrap());
|
||||
|
||||
assert_eq!(directory.as_deref(), Some(temp_dir.path()));
|
||||
assert_eq!(file_name.as_deref(), Some("Quarterly briefing.html"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_file_is_revealed_and_falls_back_to_its_parent() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
@@ -575,4 +628,4 @@ mod tests {
|
||||
|
||||
assert!(resolve_file_manager_target(&invalid_path).is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
//! Local image preparation: decode a file, apply the size policy, and return it as a Data URL.
|
||||
//!
|
||||
//! This module is deliberately free of any feature-specific behavior so that every part of
|
||||
//! AI Studio that needs an embeddable image can use it. The size policy is a single maximum edge
|
||||
//! length; callers that want the original bytes pass `optimize = false`.
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::imageops::FilterType;
|
||||
use image::{DynamicImage, ImageFormat, ImageReader};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The longest edge an optimized image may have. Larger images are scaled down proportionally.
|
||||
const MAX_EDGE_PIXELS: u32 = 2_560;
|
||||
|
||||
/// The quality used when re-encoding JPEG images. Pinned so that repeated runs are byte-identical.
|
||||
const JPEG_QUALITY: u8 = 85;
|
||||
|
||||
/// The request to prepare one local image file.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PrepareImageRequest {
|
||||
/// The absolute path of the image file to read.
|
||||
path: String,
|
||||
|
||||
/// Whether the size policy and re-encoding are applied. When false, the original bytes are used.
|
||||
optimize: bool,
|
||||
}
|
||||
|
||||
/// The prepared image together with the dimensions the caller can lay out against.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct PrepareImageResponse {
|
||||
/// The complete `data:` URL, ready to embed.
|
||||
data_url: String,
|
||||
|
||||
/// The MIME type matching the source format.
|
||||
mime_type: String,
|
||||
|
||||
/// The width of the prepared image in pixels.
|
||||
width: u32,
|
||||
|
||||
/// The height of the prepared image in pixels.
|
||||
height: u32,
|
||||
|
||||
/// Whether the size policy actually scaled the image down.
|
||||
was_resized: bool,
|
||||
}
|
||||
|
||||
/// Decodes one supported image, applies the size policy, and returns a Data URL.
|
||||
///
|
||||
/// Decoding runs on a blocking worker because it is CPU-bound and would otherwise stall the
|
||||
/// async runtime for large images.
|
||||
pub async fn prepare_image(
|
||||
Json(request): Json<PrepareImageRequest>,
|
||||
) -> Result<Json<PrepareImageResponse>, (StatusCode, String)> {
|
||||
tokio::task::spawn_blocking(move || prepare_image_sync(&request))
|
||||
.await
|
||||
.map_err(|error| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("The image worker failed: {error}"),
|
||||
)
|
||||
})?
|
||||
.map(Json)
|
||||
}
|
||||
|
||||
/// Performs the blocking part of [`prepare_image`].
|
||||
///
|
||||
/// Only absolute paths to existing files are accepted, and the decoded format has to match the
|
||||
/// file extension. Rejecting a mismatch keeps a file that merely claims to be an image from being
|
||||
/// embedded under a MIME type derived from its name.
|
||||
fn prepare_image_sync(
|
||||
request: &PrepareImageRequest,
|
||||
) -> Result<PrepareImageResponse, (StatusCode, String)> {
|
||||
let path = Path::new(&request.path);
|
||||
if !path.is_absolute() || !path.is_file() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"The image path is not an accessible absolute file path.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let format = supported_format(path)?;
|
||||
let reader = ImageReader::open(path)
|
||||
.and_then(|reader| reader.with_guessed_format())
|
||||
.map_err(|error| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("The image could not be opened: {error}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
if reader.format() != Some(format) {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"The image content does not match its file extension.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let decoded = reader.decode().map_err(|error| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("The image could not be decoded: {error}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let original_width = decoded.width();
|
||||
let original_height = decoded.height();
|
||||
let should_resize = request.optimize && original_width.max(original_height) > MAX_EDGE_PIXELS;
|
||||
|
||||
let prepared = if should_resize {
|
||||
resize_to_max_edge(decoded)
|
||||
} else {
|
||||
decoded
|
||||
};
|
||||
|
||||
let width = prepared.width();
|
||||
let height = prepared.height();
|
||||
|
||||
let bytes = if request.optimize {
|
||||
encode(&prepared, format)?
|
||||
} else {
|
||||
std::fs::read(path).map_err(|error| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("The image could not be read: {error}"),
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
let mime_type = match format {
|
||||
ImageFormat::Jpeg => "image/jpeg",
|
||||
ImageFormat::Png => "image/png",
|
||||
ImageFormat::WebP => "image/webp",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
.to_string();
|
||||
|
||||
Ok(PrepareImageResponse {
|
||||
data_url: format!(
|
||||
"data:{mime_type};base64,{}",
|
||||
general_purpose::STANDARD.encode(bytes)
|
||||
),
|
||||
mime_type,
|
||||
width,
|
||||
height,
|
||||
was_resized: should_resize,
|
||||
})
|
||||
}
|
||||
|
||||
/// Maps a file extension to the one image format AI Studio embeds.
|
||||
///
|
||||
/// The result is only the expected format; [`prepare_image_sync`] still verifies it against the
|
||||
/// actual file content.
|
||||
fn supported_format(path: &Path) -> Result<ImageFormat, (StatusCode, String)> {
|
||||
match path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.as_deref()
|
||||
{
|
||||
Some("jpg" | "jpeg") => Ok(ImageFormat::Jpeg),
|
||||
Some("png") => Ok(ImageFormat::Png),
|
||||
Some("webp") => Ok(ImageFormat::WebP),
|
||||
|
||||
_ => Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Images must be PNG, JPEG, or WebP files.".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Scales an image down so that its longest edge equals [`MAX_EDGE_PIXELS`].
|
||||
///
|
||||
/// The aspect ratio is preserved, and both edges stay at least one pixel wide.
|
||||
fn resize_to_max_edge(image: DynamicImage) -> DynamicImage {
|
||||
let width = image.width();
|
||||
let height = image.height();
|
||||
let scale = MAX_EDGE_PIXELS as f64 / width.max(height) as f64;
|
||||
let target_width = (width as f64 * scale).round().max(1.0) as u32;
|
||||
let target_height = (height as f64 * scale).round().max(1.0) as u32;
|
||||
image.resize_exact(target_width, target_height, FilterType::Lanczos3)
|
||||
}
|
||||
|
||||
/// Encodes a prepared image back into its source format.
|
||||
///
|
||||
/// JPEG uses the pinned [`JPEG_QUALITY`] so that the same input always produces the same bytes,
|
||||
/// which keeps artifact hashes stable across runs.
|
||||
fn encode(image: &DynamicImage, format: ImageFormat) -> Result<Vec<u8>, (StatusCode, String)> {
|
||||
let mut bytes = Vec::new();
|
||||
match format {
|
||||
ImageFormat::Jpeg => JpegEncoder::new_with_quality(&mut bytes, JPEG_QUALITY)
|
||||
.encode_image(image)
|
||||
.map_err(|error| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("The JPEG image could not be encoded: {error}"),
|
||||
)
|
||||
})?,
|
||||
|
||||
ImageFormat::Png | ImageFormat::WebP => image
|
||||
.write_to(&mut Cursor::new(&mut bytes), format)
|
||||
.map_err(|error| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("The image could not be encoded: {error}"),
|
||||
)
|
||||
})?,
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temporary_image_path(extension: &str) -> std::path::PathBuf {
|
||||
let unique = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
std::env::temp_dir().join(format!("mwai-visual-briefing-test-{unique}.{extension}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsupported_visual_asset_extension() {
|
||||
let issue = supported_format(Path::new("/tmp/asset.gif")).unwrap_err();
|
||||
assert_eq!(issue.0, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_supported_formats_stable() {
|
||||
assert_eq!(
|
||||
supported_format(Path::new("/tmp/asset.jpeg")).unwrap(),
|
||||
ImageFormat::Jpeg
|
||||
);
|
||||
assert_eq!(
|
||||
supported_format(Path::new("/tmp/asset.png")).unwrap(),
|
||||
ImageFormat::Png
|
||||
);
|
||||
assert_eq!(
|
||||
supported_format(Path::new("/tmp/asset.webp")).unwrap(),
|
||||
ImageFormat::WebP
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_response_in_snake_case_for_the_rust_service_contract() {
|
||||
let response = PrepareImageResponse {
|
||||
data_url: "data:image/jpeg;base64,/9j/".to_string(),
|
||||
mime_type: "image/jpeg".to_string(),
|
||||
width: 17,
|
||||
height: 11,
|
||||
was_resized: false,
|
||||
};
|
||||
let json = serde_json::to_value(response).unwrap();
|
||||
assert_eq!(json["data_url"], "data:image/jpeg;base64,/9j/");
|
||||
assert_eq!(json["mime_type"], "image/jpeg");
|
||||
assert_eq!(json["width"], 17);
|
||||
assert_eq!(json["height"], 11);
|
||||
assert_eq!(json["was_resized"], false);
|
||||
assert!(json.get("dataUrl").is_none());
|
||||
assert!(json.get("mimeType").is_none());
|
||||
assert!(json.get("wasResized").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_optimization_preserves_original_bytes() {
|
||||
let path = temporary_image_path("png");
|
||||
DynamicImage::new_rgb8(4, 3)
|
||||
.save_with_format(&path, ImageFormat::Png)
|
||||
.unwrap();
|
||||
let original = std::fs::read(&path).unwrap();
|
||||
let response = prepare_image_sync(&PrepareImageRequest {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
optimize: false,
|
||||
})
|
||||
.unwrap();
|
||||
let encoded = response.data_url.split_once(',').unwrap().1;
|
||||
assert_eq!(general_purpose::STANDARD.decode(encoded).unwrap(), original);
|
||||
assert!(!response.was_resized);
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimization_resizes_only_images_over_the_maximum_edge() {
|
||||
let path = temporary_image_path("png");
|
||||
DynamicImage::new_rgb8(MAX_EDGE_PIXELS + 1, 1)
|
||||
.save_with_format(&path, ImageFormat::Png)
|
||||
.unwrap();
|
||||
let response = prepare_image_sync(&PrepareImageRequest {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
optimize: true,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(response.width, MAX_EDGE_PIXELS);
|
||||
assert_eq!(response.height, 1);
|
||||
assert!(response.was_resized);
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimization_keeps_images_at_the_maximum_edge_unchanged() {
|
||||
let path = temporary_image_path("png");
|
||||
DynamicImage::new_rgb8(MAX_EDGE_PIXELS, 2)
|
||||
.save_with_format(&path, ImageFormat::Png)
|
||||
.unwrap();
|
||||
let response = prepare_image_sync(&PrepareImageRequest {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
optimize: true,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(response.width, MAX_EDGE_PIXELS);
|
||||
assert_eq!(response.height, 2);
|
||||
assert!(!response.was_resized);
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimized_jpeg_uses_the_pinned_quality_encoder() {
|
||||
let path = temporary_image_path("jpg");
|
||||
let image = DynamicImage::new_rgb8(17, 11);
|
||||
image.save_with_format(&path, ImageFormat::Jpeg).unwrap();
|
||||
let response = prepare_image_sync(&PrepareImageRequest {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
optimize: true,
|
||||
})
|
||||
.unwrap();
|
||||
let actual = general_purpose::STANDARD
|
||||
.decode(response.data_url.split_once(',').unwrap().1)
|
||||
.unwrap();
|
||||
let mut expected = Vec::new();
|
||||
JpegEncoder::new_with_quality(&mut expected, JPEG_QUALITY)
|
||||
.encode_image(&image)
|
||||
.unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
assert_eq!(response.mime_type, "image/jpeg");
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optimization_keeps_png_and_webp_formats_stable() {
|
||||
for (extension, format, expected_mime) in [
|
||||
("png", ImageFormat::Png, "image/png"),
|
||||
("webp", ImageFormat::WebP, "image/webp"),
|
||||
] {
|
||||
let path = temporary_image_path(extension);
|
||||
DynamicImage::new_rgba8(9, 7)
|
||||
.save_with_format(&path, format)
|
||||
.unwrap();
|
||||
let response = prepare_image_sync(&PrepareImageRequest {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
optimize: true,
|
||||
})
|
||||
.unwrap();
|
||||
let bytes = general_purpose::STANDARD
|
||||
.decode(response.data_url.split_once(',').unwrap().1)
|
||||
.unwrap();
|
||||
assert_eq!(image::guess_format(&bytes).unwrap(), format);
|
||||
assert_eq!(response.mime_type, expected_mime);
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_file_contents_that_do_not_match_the_extension() {
|
||||
let path = temporary_image_path("png");
|
||||
std::fs::write(&path, b"not an image").unwrap();
|
||||
let issue = prepare_image_sync(&PrepareImageRequest {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
optimize: true,
|
||||
})
|
||||
.unwrap_err();
|
||||
assert_eq!(issue.0, StatusCode::BAD_REQUEST);
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod runtime_certificate;
|
||||
pub mod file_data;
|
||||
pub mod metadata;
|
||||
pub mod media;
|
||||
pub mod image;
|
||||
pub mod pdfium;
|
||||
pub mod pandoc;
|
||||
pub mod qdrant_edge_database;
|
||||
|
||||
@@ -63,6 +63,7 @@ pub fn start_runtime_api() {
|
||||
.route("/media/jobs", post(crate::media::create_job))
|
||||
.route("/media/jobs/{id}/events", get(crate::media::get_job_events))
|
||||
.route("/media/jobs/{id}", delete(crate::media::cancel_job))
|
||||
.route("/image/prepare", post(crate::image::prepare_image))
|
||||
.route("/log/paths", get(crate::log::get_log_paths))
|
||||
.route("/log/event", post(crate::log::log_event))
|
||||
.route("/shortcuts/register", post(crate::app_window::register_shortcut))
|
||||
|
||||
Reference in new issue
Block a user