diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor index 1a26b997..83efbef2 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor @@ -16,6 +16,10 @@ + + @T("Supported audio and video files are transcribed automatically without an additional dialog. Each transcript is stored next to its media file as '.transcript.md' and reused when an interrupted run is continued.") + + @if (this.includeSubdirectories) diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Content.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Content.cs new file mode 100644 index 00000000..d971b802 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Content.cs @@ -0,0 +1,166 @@ +using System.Text; + +using AIStudio.Tools.Media; +using AIStudio.Tools.Rust; + +namespace AIStudio.Assistants.BatchProcessing; + +public partial class AssistantBatchProcessing +{ + /// + /// Loads a document through the Rust content stream or resolves a persistent + /// transcript for an audio or video file. + /// + private Task LoadInputContentAsync(BatchProcessingFileResult fileResult, CancellationToken token) + { + return IsTranscribableMedia(fileResult.FilePath) + ? this.LoadMediaTranscriptAsync(fileResult, token) + : this.LoadDocumentContentAsync(fileResult); + } + + private async Task LoadDocumentContentAsync(BatchProcessingFileResult fileResult) + { + FileExtractionResult extraction; + try + { + extraction = await this.RustService.ReadArbitraryFileData(fileResult.FilePath, int.MaxValue); + } + catch (Exception e) + { + this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, string.Format(T("Was not able to read the file: {0}"), e.Message), e); + return null; + } + + if (!extraction.HasUsableContent) + { + this.Logger.LogError("Reading the batch file '{FilePath}' failed: code={ErrorCode}, message='{ErrorMessage}'.", fileResult.FilePath, extraction.ErrorCode, extraction.ErrorMessage); + this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, extraction.ToUserMessage(fileResult.FileName)); + return null; + } + + if (extraction.Outcome is FileExtractionOutcome.PARTIAL) + { + this.Logger.LogWarning("Parts of the batch file '{FilePath}' could not be read: pages={FailedPages}.", fileResult.FilePath, string.Join(", ", extraction.FailedPages)); + await this.MessageBus.SendWarning(new(Icons.Material.Filled.Description, extraction.ToPartialUserMessage(fileResult.FileName))); + } + + if (extraction.HasExtensionMismatch) + { + this.Logger.LogWarning("The batch file '{FilePath}' is actually a '{DetectedFormat}'.", fileResult.FilePath, extraction.DetectedFormat); + await this.MessageBus.SendWarning(new(Icons.Material.Filled.RuleFolder, extraction.ToExtensionMismatchUserMessage(fileResult.FileName))); + } + + if (!string.IsNullOrWhiteSpace(extraction.Content)) + return extraction.Content; + + this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("Was not able to extract any text from this file.")); + return null; + } + + private async Task LoadMediaTranscriptAsync(BatchProcessingFileResult fileResult, CancellationToken token) + { + var transcriptFilePath = GetTranscriptFilePath(fileResult.FilePath); + if (File.Exists(transcriptFilePath)) + { + try + { + var existingTranscript = await File.ReadAllTextAsync(transcriptFilePath, token); + if (!string.IsNullOrWhiteSpace(existingTranscript)) + { + this.Logger.LogInformation("Reusing the existing batch transcript '{TranscriptFilePath}' for media file '{MediaFilePath}'.", transcriptFilePath, fileResult.FilePath); + return existingTranscript; + } + + this.Logger.LogWarning("The existing batch transcript '{TranscriptFilePath}' for media file '{MediaFilePath}' is empty and will be replaced.", transcriptFilePath, fileResult.FilePath); + } + catch (OperationCanceledException) + { + this.FinishFileResult(fileResult, BatchProcessingFileStatus.CANCELED, T("The batch run was canceled.")); + return null; + } + catch (Exception e) + { + this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, string.Format(T("Was not able to read the existing transcript: {0}"), e.Message), e); + return null; + } + } + + if (!this.MediaTranscriptionService.HasUsableTranscriptionProvider) + { + this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("No usable transcription provider is configured.")); + return null; + } + + var transcription = await this.MediaTranscriptionService.TranscribeAsync(fileResult.FilePath, token); + if (transcription.Status is MediaTranscriptionResultStatus.CANCELLED) + { + this.FinishFileResult(fileResult, BatchProcessingFileStatus.CANCELED, T("The batch run was canceled.")); + return null; + } + + if (transcription.Status is not MediaTranscriptionResultStatus.SUCCEEDED) + { + this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, transcription.UserMessage); + return null; + } + + if (string.IsNullOrWhiteSpace(transcription.Text)) + { + this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("The transcription provider returned an empty transcript.")); + return null; + } + + return await this.StoreMediaTranscriptAsync(fileResult, transcriptFilePath, transcription.Text); + } + + private async Task StoreMediaTranscriptAsync(BatchProcessingFileResult fileResult, string transcriptFilePath, string transcript) + { + var tempFilePath = transcriptFilePath + ".tmp"; + try + { + // Complete the small persistence step even if cancellation arrived + // after transcription, so the expensive provider result can be + // reused when the interrupted batch is continued. + await File.WriteAllTextAsync(tempFilePath, transcript, new UTF8Encoding(false), CancellationToken.None); + File.Move(tempFilePath, transcriptFilePath, true); + this.Logger.LogInformation("Stored the batch transcript '{TranscriptFilePath}' next to media file '{MediaFilePath}'.", transcriptFilePath, fileResult.FilePath); + return transcript; + } + catch (Exception e) + { + this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, string.Format(T("Was not able to store the transcript next to the media file: {0}"), e.Message), e); + return null; + } + finally + { + try + { + if (File.Exists(tempFilePath)) + File.Delete(tempFilePath); + } + catch (Exception e) + { + this.Logger.LogWarning(e, "Was not able to remove the temporary batch transcript '{TempFilePath}'.", tempFilePath); + } + } + } + + private static bool IsTranscribableMedia(string filePath) => FileTypes.IsAllowedPath(filePath, FileTypes.AUDIO, FileTypes.VIDEO); + + private static string GetTranscriptFilePath(string mediaFilePath) => mediaFilePath + TRANSCRIPT_FILE_SUFFIX; + + private static bool HasReusableTranscript(string mediaFilePath) + { + var transcriptFilePath = GetTranscriptFilePath(mediaFilePath); + try + { + return File.Exists(transcriptFilePath) && new FileInfo(transcriptFilePath).Length > 0; + } + catch + { + // The concrete read error is reported when the affected file is + // processed. Here we only decide whether a provider is required. + return File.Exists(transcriptFilePath); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs index de69d506..cdf36856 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Run.cs @@ -167,42 +167,9 @@ public partial class AssistantBatchProcessing /// private async Task ProcessOneFileAsync(BatchProcessingFileResult fileResult, string resolvedOutputDirectory, CancellationToken token) { - FileExtractionResult extraction; - try - { - extraction = await this.RustService.ReadArbitraryFileData(fileResult.FilePath, int.MaxValue); - } - catch (Exception e) - { - this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, string.Format(T("Was not able to read the file: {0}"), e.Message), e); + var fileContent = await this.LoadInputContentAsync(fileResult, token); + if (fileContent is null) return; - } - - if (!extraction.HasUsableContent) - { - this.Logger.LogError("Reading the batch file '{FilePath}' failed: code={ErrorCode}, message='{ErrorMessage}'.", fileResult.FilePath, extraction.ErrorCode, extraction.ErrorMessage); - this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, extraction.ToUserMessage(fileResult.FileName)); - return; - } - - if (extraction.Outcome is FileExtractionOutcome.PARTIAL) - { - this.Logger.LogWarning("Parts of the batch file '{FilePath}' could not be read: pages={FailedPages}.", fileResult.FilePath, string.Join(", ", extraction.FailedPages)); - await this.MessageBus.SendWarning(new(Icons.Material.Filled.Description, extraction.ToPartialUserMessage(fileResult.FileName))); - } - - if (extraction.HasExtensionMismatch) - { - this.Logger.LogWarning("The batch file '{FilePath}' is actually a '{DetectedFormat}'.", fileResult.FilePath, extraction.DetectedFormat); - await this.MessageBus.SendWarning(new(Icons.Material.Filled.RuleFolder, extraction.ToExtensionMismatchUserMessage(fileResult.FileName))); - } - - var fileContent = extraction.Content; - if (string.IsNullOrWhiteSpace(fileContent)) - { - this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("Was not able to extract any text from this file.")); - return; - } string aiAnswer; try diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Validation.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Validation.cs index 925a0591..d1b6cba4 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Validation.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.Validation.cs @@ -120,6 +120,9 @@ public partial class AssistantBatchProcessing foreach (var file in Directory.EnumerateFiles(this.inputDirectory, pattern, searchOption)) { var normalizedFile = Path.GetFullPath(file); + if (IsTranscriptArtifact(normalizedFile)) + continue; + if (isOutputSeparateFolder) { if (normalizedFile.StartsWith(outputDirectoryPrefix, StringComparison.OrdinalIgnoreCase)) @@ -178,6 +181,16 @@ public partial class AssistantBatchProcessing return fileName.EndsWith(RESULT_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase); } + /// + /// Checks for persistent or interrupted media transcript artifacts. They + /// always live beside their source file, independently of the output folder. + /// + private static bool IsTranscriptArtifact(string filePath) + { + var fileName = Path.GetFileName(filePath); + return fileName.EndsWith(TRANSCRIPT_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase) || fileName.EndsWith(TRANSCRIPT_FILE_SUFFIX + ".tmp", StringComparison.OrdinalIgnoreCase); + } + /// /// Validates the form, finds the documents, and creates the output folder. /// @@ -215,6 +228,13 @@ public partial class AssistantBatchProcessing return null; } + var requiresTranscription = files.Any(file => IsTranscribableMedia(file) && !HasReusableTranscript(file)); + if (requiresTranscription && !this.MediaTranscriptionService.HasUsableTranscriptionProvider) + { + this.AddInputIssue(T("The selected files include audio or video without an existing transcript, but no usable transcription provider is configured. Configure one in the transcription settings or remove the media patterns.")); + return null; + } + try { Directory.CreateDirectory(resolvedOutputDirectory); diff --git a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs index 10922c87..f13065d8 100644 --- a/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs +++ b/app/MindWork AI Studio/Assistants/BatchProcessing/AssistantBatchProcessing.razor.cs @@ -15,6 +15,7 @@ public partial class AssistantBatchProcessing : AssistantBaseCore @@ -27,7 +28,7 @@ public partial class AssistantBatchProcessing : AssistantBaseCore T("Batch Processing Assistant"); - protected override string Description => T("Process all documents of a folder in one batch run: each document is converted to Markdown and sent to the AI along with your instructions. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table. A log records what happened to every document, so a run which was interrupted or produced errors can be continued later. A single failing document never stops the entire run."); + protected override string Description => T("Process all documents and media files of a folder in one batch run: documents are converted to Markdown, while audio and video files are transcribed automatically, before their content is sent to the AI along with your instructions. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table. A log records what happened to every file, so a run which was interrupted or produced errors can be continued later. A single failing file never stops the entire run."); protected override string SystemPrompt => this.BuildSystemPrompt(); diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index cb462174..01f36b7c 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -331,6 +331,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T4242312602"] = "Send to . -- Copy result UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T83711157"] = "Copy result" +-- The transcription provider returned an empty transcript. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1080540822"] = "The transcription provider returned an empty transcript." + -- Name of the results table (optional) UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1127380661"] = "Name of the results table (optional)" @@ -343,9 +346,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING -- Please use only single asterisks as wildcards, e.g., *.pdf or report-*.docx. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1187528282"] = "Please use only single asterisks as wildcards, e.g., *.pdf or report-*.docx." +-- Supported audio and video files are transcribed automatically without an additional dialog. Each transcript is stored next to its media file as '.transcript.md' and reused when an interrupted run is continued. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T120341322"] = "Supported audio and video files are transcribed automatically without an additional dialog. Each transcript is stored next to its media file as '.transcript.md' and reused when an interrupted run is continued." + -- Instructions UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1221801316"] = "Instructions" +-- Process all documents and media files of a folder in one batch run: documents are converted to Markdown, while audio and video files are transcribed automatically, before their content is sent to the AI along with your instructions. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table. A log records what happened to every file, so a run which was interrupted or produced errors can be continued later. A single failing file never stops the entire run. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T131887991"] = "Process all documents and media files of a folder in one batch run: documents are converted to Markdown, while audio and video files are transcribed automatically, before their content is sent to the AI along with your instructions. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table. A log records what happened to every file, so a run which was interrupted or produced errors can be continued later. A single failing file never stops the entire run." + -- Batch Processing Assistant UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T132410578"] = "Batch Processing Assistant" @@ -415,18 +424,30 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING -- We always write a log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. Its columns are separated by a vertical bar, so you can open it with any spreadsheet application. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T225540173"] = "We always write a log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. Its columns are separated by a vertical bar, so you can open it with any spreadsheet application. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder." +-- No usable transcription provider is configured. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2282521655"] = "No usable transcription provider is configured." + -- Was not able to create the output folder: {0} UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2290092642"] = "Was not able to create the output folder: {0}" -- The AI answer was empty. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T230354366"] = "The AI answer was empty." +-- The batch run finished, but {0} files could not be processed. See the progress table and log for details. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2334361705"] = "The batch run finished, but {0} files could not be processed. See the progress table and log for details." + -- The AI request failed: {0} UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2376918044"] = "The AI request failed: {0}" -- Done UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2379421585"] = "Done" +-- The selected files include audio or video without an existing transcript, but no usable transcription provider is configured. Configure one in the transcription settings or remove the media patterns. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2390162661"] = "The selected files include audio or video without an existing transcript, but no usable transcription provider is configured. Configure one in the transcription settings or remove the media patterns." + +-- Was not able to read the existing transcript: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2397111152"] = "Was not able to read the existing transcript: {0}" + -- File patterns UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2460883298"] = "File patterns" @@ -463,6 +484,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING -- Was not able to write the result file: {0} UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2991295581"] = "Was not able to write the result file: {0}" +-- The batch run finished, but one file could not be processed. See the progress table and log for details. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3201532790"] = "The batch run finished, but one file could not be processed. See the progress table and log for details." + -- Please select the folder that contains the documents you want to process. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T33077198"] = "Please select the folder that contains the documents you want to process." @@ -487,6 +511,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING -- Please remove empty file patterns. Separate valid patterns with a single semicolon. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T368919579"] = "Please remove empty file patterns. Separate valid patterns with a single semicolon." +-- Was not able to store the transcript next to the media file: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3691287653"] = "Was not able to store the transcript next to the media file: {0}" + -- Time UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3756319748"] = "Time" @@ -535,6 +562,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING -- Please use file name patterns without folder paths, e.g., *.pdf or report-*.docx. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T515934256"] = "Please use file name patterns without folder paths, e.g., *.pdf or report-*.docx." +-- Was not able to read the results table of the previous run. Its completed documents cannot be restored and will be processed again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T544244392"] = "Was not able to read the results table of the previous run. Its completed documents cannot be restored and will be processed again." + -- Yes, process files in subfolders as well UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T618448696"] = "Yes, process files in subfolders as well" @@ -553,9 +583,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING -- A separate output folder is excluded from document discovery. This includes the default 'ai-results' folder, so results from an earlier run are not processed again. If the input folder itself is used for output, known batch result files are excluded instead. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T822136905"] = "A separate output folder is excluded from document discovery. This includes the default 'ai-results' folder, so results from an earlier run are not processed again. If the input folder itself is used for output, known batch result files are excluded instead." --- Process all documents of a folder in one batch run: each document is converted to Markdown and sent to the AI along with your instructions. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table. A log records what happened to every document, so a run which was interrupted or produced errors can be continued later. A single failing document never stops the entire run. -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T998860382"] = "Process all documents of a folder in one batch run: each document is converted to Markdown and sent to the AI along with your instructions. You choose whether each answer is stored as its own Markdown file or whether all answers are collected in one CSV results table. A log records what happened to every document, so a run which was interrupted or produced errors can be continued later. A single failing document never stops the entire run." - -- One CSV results table, where each answer becomes one row UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGOUTPUTMODEEXTENSIONS::T1515293131"] = "One CSV results table, where each answer becomes one row" @@ -6508,6 +6535,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T22 -- When enabled, new batch runs start with the defaults configured below. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2592677194"] = "When enabled, new batch runs start with the defaults configured below." +-- Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2594325620"] = "Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats." + -- Subfolders are included UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2607092632"] = "Subfolders are included" @@ -6586,9 +6616,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T74 -- Leave empty when an input folder should be selected for every batch run. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T762890100"] = "Leave empty when an input folder should be selected for every batch run." --- Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T953507412"] = "Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx." - -- Preselect one of your chat templates? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1402022556"] = "Preselect one of your chat templates?" diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor index 22a25b00..80351ae4 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor @@ -17,7 +17,7 @@ @T("Input") - + @T("Instructions") diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 0fbe7e3b..3fd0a22d 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -406,7 +406,7 @@ CONFIG["SETTINGS"] = {} -- -- Configure the default file patterns and whether subfolders are included. -- Separate multiple patterns with semicolons. --- CONFIG["SETTINGS"]["DataBatchProcessing.FilePatterns"] = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt" +-- CONFIG["SETTINGS"]["DataBatchProcessing.FilePatterns"] = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm" -- CONFIG["SETTINGS"]["DataBatchProcessing.IncludeSubdirectories"] = false -- -- Configure the default instruction source. diff --git a/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs b/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs index 5f5d285d..48268019 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataBatchProcessing.cs @@ -11,7 +11,7 @@ namespace AIStudio.Settings.DataModel; /// The managed-configuration selector. public sealed class DataBatchProcessing(Expression>? configSelection = null) { - public const string DEFAULT_FILE_PATTERNS = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt"; + public const string DEFAULT_FILE_PATTERNS = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm"; /// /// Initializes an unmanaged Batch Processing settings instance. diff --git a/app/MindWork AI Studio/Tools/Services/MediaTranscriptionService.cs b/app/MindWork AI Studio/Tools/Services/MediaTranscriptionService.cs index d39f9413..6da9290f 100644 --- a/app/MindWork AI Studio/Tools/Services/MediaTranscriptionService.cs +++ b/app/MindWork AI Studio/Tools/Services/MediaTranscriptionService.cs @@ -61,6 +61,9 @@ public sealed class MediaTranscriptionService( return this.activeBatches.Contains(owner); } + /// Gets whether the currently configured transcription provider can be used. + public bool HasUsableTranscriptionProvider => this.ResolveProvider() is not null; + /// Gets the last retained state for one owner. public MediaImportSnapshot? GetSnapshot(MediaImportOwner owner) { @@ -403,12 +406,12 @@ public sealed class MediaTranscriptionService( } /// - /// Transcribes a voice recording independently of the visible import lane. + /// Transcribes an audio or video file without starting a visible import operation. /// - /// Voice recording path. + /// Audio or video file path. /// Caller cancellation token. /// A typed terminal result. - public async Task TranscribeVoiceAsync(string mediaPath, CancellationToken token = default) + public async Task TranscribeAsync(string mediaPath, CancellationToken token = default) { this.ThrowIfDisposed(); var operation = this.CreateOperation(null, token); @@ -423,6 +426,14 @@ public sealed class MediaTranscriptionService( } } + /// + /// Transcribes a voice recording independently of the visible import lane. + /// + /// Voice recording path. + /// Caller cancellation token. + /// A typed terminal result. + public Task TranscribeVoiceAsync(string mediaPath, CancellationToken token = default) => this.TranscribeAsync(mediaPath, token); + /// Cancels only the queued or active operation belonging to one owner. public async Task StopAsync(MediaImportOwner owner) {