mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 14:58:00 +00:00
Added transcription of audio and video files to local data sources (#1081)
Co-authored-by: Paul Koudelka <106623909+PaulKoudelka@users.noreply.github.com>
This commit is contained in:
1 parent
8642ce2d81
commit
752f7d828a
82 files changed
+3231
-302
No files matched your search
+14
-88
@@ -1,7 +1,4 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Assistants.BatchProcessing;
|
||||
|
||||
@@ -69,108 +66,37 @@ public partial class AssistantBatchProcessing
|
||||
|
||||
private async Task<string?> 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)
|
||||
var provider = this.MediaTranscriptionService.GetUsableProvider();
|
||||
var transcript = await PersistentMediaTranscript.ReadOrCreateAsync(MediaSidecarTranscripts.INSTANCE, fileResult.FilePath, provider, this.MediaTranscriptionService.TranscribeAsync, this.Logger, token);
|
||||
if (transcript.Status is PersistentMediaTranscriptStatus.CANCELLED)
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.CANCELED, T("The batch run was canceled."));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (transcription.Status is not MediaTranscriptionResultStatus.SUCCEEDED)
|
||||
if (transcript.Status is not PersistentMediaTranscriptStatus.READY)
|
||||
{
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, transcription.UserMessage);
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, transcript.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<string?> 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;
|
||||
return await File.ReadAllTextAsync(transcript.TranscriptPath, token);
|
||||
}
|
||||
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 store the transcript next to the media file: {0}"), e.Message), e);
|
||||
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, string.Format(T("Was not able to read the existing transcript: {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 bool IsTranscribableMedia(string filePath) => PersistentMediaTranscript.IsMediaPath(filePath);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
private static bool HasReusableTranscript(string mediaFilePath) => MediaSidecarTranscripts.HasReusableTranscript(mediaFilePath);
|
||||
}
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
using AIStudio.Tools.Media;
|
||||
|
||||
namespace AIStudio.Assistants.BatchProcessing;
|
||||
|
||||
@@ -160,8 +161,7 @@ public partial class AssistantBatchProcessing
|
||||
/// </summary>
|
||||
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);
|
||||
return MediaSidecarTranscripts.IsArtifactPath(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -19,7 +19,6 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
|
||||
private const string DEFAULT_RESULTS_FILENAME = "batch-results.csv";
|
||||
private const string CSV_EXTENSION = ".csv";
|
||||
private const string RESULT_FILE_SUFFIX = "_result";
|
||||
private const string TRANSCRIPT_FILE_SUFFIX = ".transcript.md";
|
||||
private const string TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
private const char LOG_SEPARATOR = ';';
|
||||
|
||||
|
||||
@@ -343,9 +343,6 @@ 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."
|
||||
|
||||
-- Hide Expert Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1108876344"] = "Hide Expert Settings"
|
||||
|
||||
@@ -460,9 +457,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- Tools for this batch run
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2247412388"] = "Tools for this batch run"
|
||||
|
||||
-- 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}"
|
||||
|
||||
@@ -580,9 +574,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- {0} of {1} files processed
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3648144402"] = "{0} of {1} files processed"
|
||||
|
||||
-- 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"
|
||||
|
||||
@@ -3958,6 +3949,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T2373663376"] = "Good to kno
|
||||
-- Data folder
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T2422278827"] = "Data folder"
|
||||
|
||||
-- The transcripts of the audio and video files in your local data sources.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T260303721"] = "The transcripts of the audio and video files in your local data sources."
|
||||
|
||||
-- Explanation
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T2840582448"] = "Explanation"
|
||||
|
||||
@@ -4036,9 +4030,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T1159107
|
||||
-- No, I will choose another embedding
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T1246976418"] = "No, I will choose another embedding"
|
||||
|
||||
-- No, I will choose other providers or files
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T1811383874"] = "No, I will choose other providers or files"
|
||||
|
||||
-- Warning: Your transcription provider '{0}' is not self-hosted. Audio and video files this data source selects are sent to it to be transcribed, which can cost money. AI Studio keeps every transcript, so an unchanged recording is sent only once. MindWork AI Studio has no control over what that third party does with the recordings after they are sent.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T200667320"] = "Warning: Your transcription provider '{0}' is not self-hosted. Audio and video files this data source selects are sent to it to be transcribed, which can cost money. AI Studio keeps every transcript, so an unchanged recording is sent only once. MindWork AI Studio has no control over what that third party does with the recordings after they are sent."
|
||||
|
||||
-- Every mail of this mailbox in the selected folder and period, together with the text of its attachments,
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T2046044636"] = "Every mail of this mailbox in the selected folder and period, together with the text of its attachments,"
|
||||
|
||||
-- Yes, please send my data to the external providers
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T2239851837"] = "Yes, please send my data to the external providers"
|
||||
|
||||
-- The data source '{0}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T2503488371"] = "The data source '{0}'"
|
||||
|
||||
@@ -6775,6 +6778,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T286958948"]
|
||||
-- Token limit
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2961294165"] = "Token limit"
|
||||
|
||||
-- Audio and video files which this data source selects are skipped, because your transcription provider does not meet the confidence level this data source requires.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T3017942214"] = "Audio and video files which this data source selects are skipped, because your transcription provider does not meet the confidence level this data source requires."
|
||||
|
||||
-- Audio and video files which this data source selects are skipped until you choose a transcription provider in the settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T3119708878"] = "Audio and video files which this data source selects are skipped until you choose a transcription provider in the settings."
|
||||
|
||||
-- Please enter 0 or a positive overlap length.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T3242265813"] = "Please enter 0 or a positive overlap length."
|
||||
|
||||
@@ -6799,12 +6808,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T529871858"]
|
||||
-- Select the base directory
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T562479068"] = "Select the base directory"
|
||||
|
||||
-- Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file. Audio and video files, e.g., *.mp3 or *.mp4, are transcribed by your transcription provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T599406584"] = "Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file. Audio and video files, e.g., *.mp3 or *.mp4, are transcribed by your transcription provider."
|
||||
|
||||
-- Yes, index hidden files and folders as well
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T607153623"] = "Yes, index hidden files and folders as well"
|
||||
|
||||
-- Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T761052435"] = "Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file."
|
||||
|
||||
-- The data source token limit must not be larger than the embedding provider token limit ({0}).
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T787118522"] = "The data source token limit must not be larger than the embedding provider token limit ({0})."
|
||||
|
||||
@@ -6940,9 +6949,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1108876344"] = "
|
||||
-- Optional expert settings for how this data source is split before embedding.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1133561850"] = "Optional expert settings for how this data source is split before embedding."
|
||||
|
||||
-- Select a file for this data source. The content of this file will be processed for the data source.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1190880267"] = "Select a file for this data source. The content of this file will be processed for the data source."
|
||||
|
||||
-- Default tokenizer
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1220918127"] = "Default tokenizer"
|
||||
|
||||
@@ -6976,6 +6982,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T236253137"] = "R
|
||||
-- Please enter a token limit of at least 1.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T2406580478"] = "Please enter a token limit of at least 1."
|
||||
|
||||
-- This audio or video file is skipped, because your transcription provider does not meet the confidence level this data source requires.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T2468760910"] = "This audio or video file is skipped, because your transcription provider does not meet the confidence level this data source requires."
|
||||
|
||||
-- Add
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T2646845972"] = "Add"
|
||||
|
||||
@@ -7003,6 +7012,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T3361153305"] = "
|
||||
-- Select the file
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T3740148848"] = "Select the file"
|
||||
|
||||
-- This audio or video file is skipped until you choose a transcription provider in the settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T4026150982"] = "This audio or video file is skipped until you choose a transcription provider in the settings."
|
||||
|
||||
-- In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T529871858"] = "In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider."
|
||||
|
||||
@@ -7021,6 +7033,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T900713019"] = "C
|
||||
-- Selected file path for this data source
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T939749563"] = "Selected file path for this data source"
|
||||
|
||||
-- Select a file for this data source. The content of this file will be processed for the data source. An audio or video file is transcribed by your transcription provider first.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T966073786"] = "Select a file for this data source. The content of this file will be processed for the data source. An audio or video file is transcribed by your transcription provider first."
|
||||
|
||||
-- Token overlap
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T981382809"] = "Token overlap"
|
||||
|
||||
@@ -10153,15 +10168,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1143368054"] = "Other cause"
|
||||
-- Current file: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1166856644"] = "Current file: {0}"
|
||||
|
||||
-- Transcription provider not allowed
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1283933457"] = "Transcription provider not allowed"
|
||||
|
||||
-- Mailboxes are synced every 16 minutes while the automatic refresh is on. Each mail is embedded only once: when it moves to another folder, AI Studio merely notes where it lies now.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1297391911"] = "Mailboxes are synced every 16 minutes while the automatic refresh is on. Each mail is embedded only once: when it moves to another folder, AI Studio merely notes where it lies now."
|
||||
|
||||
-- File {0} of {1} is being indexed: block {2}, page {3}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1298290372"] = "File {0} of {1} is being indexed: block {2}, page {3}."
|
||||
|
||||
-- These image, audio, or video files match the data source patterns, but they cannot currently be added to the index.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1302021733"] = "These image, audio, or video files match the data source patterns, but they cannot currently be added to the index."
|
||||
|
||||
-- Mail
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1399880782"] = "Mail"
|
||||
|
||||
@@ -10192,6 +10207,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1616414701"] = "File {0} of {1} i
|
||||
-- Skipped: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1775451753"] = "Skipped: {0}"
|
||||
|
||||
-- These audio or video files match the data source patterns, but your transcription provider does not meet the confidence level this data source requires. Choose another transcription provider, or change the confidence level of the data source.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1811387825"] = "These audio or video files match the data source patterns, but your transcription provider does not meet the confidence level this data source requires. Choose another transcription provider, or change the confidence level of the data source."
|
||||
|
||||
-- Tried again during the next run
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1946414905"] = "Tried again during the next run"
|
||||
|
||||
@@ -10264,6 +10282,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3628102488"] = "{0} files ({1})"
|
||||
-- No local data source has been queued for embedding yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3774205531"] = "No local data source has been queued for embedding yet."
|
||||
|
||||
-- These audio or video files match the data source patterns, but no transcription provider is configured that AI Studio may use. Once you choose one in the settings, AI Studio transcribes the files and adds their text to the index.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3775894071"] = "These audio or video files match the data source patterns, but no transcription provider is configured that AI Studio may use. Once you choose one in the settings, AI Studio transcribes the files and adds their text to the index."
|
||||
|
||||
-- Change the settings of the mailbox
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3788376715"] = "Change the settings of the mailbox"
|
||||
|
||||
@@ -10282,9 +10303,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T419343388"] = "Remove them from t
|
||||
-- Not synced completely yet. AI Studio works through the mailbox piece by piece, the newest mails first, and older mails are still missing.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4205079539"] = "Not synced completely yet. AI Studio works through the mailbox piece by piece, the newest mails first, and older mails are still missing."
|
||||
|
||||
-- These image files match the data source patterns, but images cannot currently be added to the index.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T422032787"] = "These image files match the data source patterns, but images cannot currently be added to the index."
|
||||
|
||||
-- Current mail: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4242653147"] = "Current mail: {0}"
|
||||
|
||||
-- No transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4253595107"] = "No transcription provider"
|
||||
|
||||
-- Skipped files: {0}. Open a group below to see why and what you can do.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4287145754"] = "Skipped files: {0}. Open a group below to see why and what you can do."
|
||||
|
||||
@@ -10303,6 +10330,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T819555436"] = "Failed mails: {0}"
|
||||
-- Skipped
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T852993367"] = "Skipped"
|
||||
|
||||
-- Open the transcription settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T905182399"] = "Open the transcription settings"
|
||||
|
||||
-- Indexed: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T933775466"] = "Indexed: {0}"
|
||||
|
||||
@@ -12328,6 +12358,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T677355172"] = "Un
|
||||
-- Not a readable spreadsheet
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1175970425"] = "Not a readable spreadsheet"
|
||||
|
||||
-- The media file '{0}' could not be processed, so it was not indexed. AI Studio reads it again as soon as the file changes.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1193172614"] = "The media file '{0}' could not be processed, so it was not indexed. AI Studio reads it again as soon as the file changes."
|
||||
|
||||
-- Media file could not be processed
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1302965532"] = "Media file could not be processed"
|
||||
|
||||
-- Not a text file
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1465212038"] = "Not a text file"
|
||||
|
||||
@@ -12562,6 +12598,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::MAIL::MAILBOXPROVIDERTEMPLATEEXTENSIONS::T4026
|
||||
-- Edit Mailbox
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MAILBOXEDITING::T3250445637"] = "Edit Mailbox"
|
||||
|
||||
-- The transcription provider returned an empty transcript.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T1080540822"] = "The transcription provider returned an empty transcript."
|
||||
|
||||
-- No usable transcription provider is configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T2282521655"] = "No usable transcription provider is configured."
|
||||
|
||||
-- Was not able to read the existing transcript: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T2397111152"] = "Was not able to read the existing transcript: {0}"
|
||||
|
||||
-- The media file changed while it was being transcribed, so its transcript was not kept.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T2455890169"] = "The media file changed while it was being transcribed, so its transcript was not kept."
|
||||
|
||||
-- The media file could not be read.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T3826117761"] = "The media file could not be read."
|
||||
|
||||
-- Was not able to store the transcript: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T920159834"] = "Was not able to store the transcript: {0}"
|
||||
|
||||
-- AI Studio couldn't install Pandoc because the archive was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
|
||||
|
||||
@@ -13633,6 +13687,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T1225
|
||||
-- Some mails could not be indexed. The list below says which ones and why.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T2360744026"] = "Some mails could not be indexed. The list below says which ones and why."
|
||||
|
||||
-- The media file '{0}' could not be processed, so it was not indexed. Reason: {1} AI Studio reads it again as soon as the file changes.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T2967004571"] = "The media file '{0}' could not be processed, so it was not indexed. Reason: {1} AI Studio reads it again as soon as the file changes."
|
||||
|
||||
-- The mail '{0}' could not be indexed. AI Studio tries again during the next sync.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T804280374"] = "The mail '{0}' could not be indexed. AI Studio tries again during the next sync."
|
||||
|
||||
@@ -14443,8 +14500,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2222598306"
|
||||
-- Please select your security policy.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2250909198"] = "Please select your security policy."
|
||||
|
||||
-- The selected file is not a supported document or readable text file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2728355006"] = "The selected file is not a supported document or readable text file."
|
||||
-- Please acknowledge that you are aware of the cloud transcription implications.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2818048408"] = "Please acknowledge that you are aware of the cloud transcription implications."
|
||||
|
||||
-- Please test the connection before saving.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T285470497"] = "Please test the connection before saving."
|
||||
@@ -14491,6 +14548,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T490875633"]
|
||||
-- The file path must not be empty. Please select a file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T878007824"] = "The file path must not be empty. Please select a file."
|
||||
|
||||
-- The selected file is not a supported document, readable text file, audio file, or video file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T90753429"] = "The selected file is not a supported document, readable text file, audio file, or video file."
|
||||
|
||||
-- Please enter the secret necessary for authentication.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T968385876"] = "Please enter the secret necessary for authentication."
|
||||
|
||||
|
||||
+1
-1
@@ -147,7 +147,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
|
||||
VisualBriefingBuildStage.SOURCE_PREPARATION,
|
||||
"A briefing source is no longer reachable.",
|
||||
$"Source {source.SourceId:D} failed the reachability check.");
|
||||
var sourceHash = await VisualBriefingHashing.ComputeFileAsync(source.Path, token);
|
||||
var sourceHash = await FileContentHash.ComputeAsync(source.Path, token);
|
||||
var transcriptHash = string.Empty;
|
||||
if (source.IsMedia)
|
||||
{
|
||||
|
||||
@@ -23,25 +23,6 @@ internal static class VisualBriefingHashing
|
||||
/// <returns>The lowercase hexadecimal hash.</returns>
|
||||
internal static string ComputeSections(params string?[] values) => Compute(string.Join('\u001e', values.Select(value => value ?? string.Empty)));
|
||||
|
||||
/// <summary>
|
||||
/// Computes a lowercase SHA-256 hash for a file without loading it fully into memory.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The lowercase hexadecimal hash.</returns>
|
||||
internal static async Task<string> ComputeFileAsync(string path, CancellationToken token)
|
||||
{
|
||||
await using var stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read,
|
||||
65_536,
|
||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
|
||||
return Convert.ToHexStringLower(await SHA256.HashDataAsync(stream, token));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns canonical JSON for one value, with ordinally sorted object properties.
|
||||
/// </summary>
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ internal sealed class VisualBriefingSourcePreparationService(VisualBriefingStore
|
||||
var info = new FileInfo(source.Path);
|
||||
totalBytes += info.Length;
|
||||
|
||||
var sourceHash = await VisualBriefingHashing.ComputeFileAsync(source.Path, token);
|
||||
var sourceHash = await FileContentHash.ComputeAsync(source.Path, token);
|
||||
var transcriptHash = string.Empty;
|
||||
|
||||
if (source.IsMedia)
|
||||
|
||||
@@ -83,6 +83,7 @@ public partial class DataFolders : MSGComponentBase
|
||||
DataBackupEntryKind.VISUAL_BRIEFINGS => T("Your projects in the Visual Briefing Assistant."),
|
||||
DataBackupEntryKind.DATA_SOURCE_INDEX => T("The index of your local data sources: which files AI Studio embedded, and their text."),
|
||||
DataBackupEntryKind.VECTOR_STORES => T("The embeddings of your local data sources."),
|
||||
DataBackupEntryKind.MEDIA_TRANSCRIPTS => T("The transcripts of the audio and video files in your local data sources."),
|
||||
|
||||
DataBackupEntryKind.SETTINGS_OF_OTHER_VERSIONS => T("Copies of your settings, which every version of AI Studio keeps for itself. Your current settings are all a backup needs."),
|
||||
DataBackupEntryKind.BUILT_IN_PLUGINS => T("Plugins which come with AI Studio, such as the language packs. AI Studio brings them back by itself."),
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true" Class="mb-3">
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@this.WarningText
|
||||
</MudJustifiedText>
|
||||
@if (this.SendsEmbeddingsToCloud)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body2">
|
||||
@this.WarningText
|
||||
</MudJustifiedText>
|
||||
}
|
||||
@if (this.SendsMediaToCloudTranscription)
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body2" Class="@(this.SendsEmbeddingsToCloud ? "mt-2" : string.Empty)">
|
||||
@this.TranscriptionWarningText
|
||||
</MudJustifiedText>
|
||||
}
|
||||
</MudAlert>
|
||||
|
||||
<MudTextSwitch Value="@this.UserAcknowledged" ValueChanged="@this.UserAcknowledgedChanged" Label="@T("I confirm that I have read and understood the above")" LabelOn="@T("Yes, please send my data to the external embedding provider")" LabelOff="@T("No, I will choose another embedding")" Validation="@this.Validation"/>
|
||||
<MudTextSwitch Value="@this.UserAcknowledged" ValueChanged="@this.UserAcknowledgedChanged" Label="@T("I confirm that I have read and understood the above")" LabelOn="@this.LabelOn" LabelOff="@this.LabelOff" Validation="@this.Validation"/>
|
||||
@@ -4,6 +4,10 @@ using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Warns that a data source sends its content to providers which are not self-hosted: its text to
|
||||
/// the embedding provider, and its audio and video files to the transcription provider.
|
||||
/// </summary>
|
||||
public partial class DataSourceCloudEmbeddingWarning : MSGComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
@@ -21,6 +25,33 @@ public partial class DataSourceCloudEmbeddingWarning : MSGComponentBase
|
||||
[Parameter]
|
||||
public Func<bool, string?> Validation { get; set; } = _ => null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the content goes to an embedding provider which is not self-hosted.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool SendsEmbeddingsToCloud { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the transcription provider which is not self-hosted and receives the audio and
|
||||
/// video files of the data source, or empty when no media file leaves the computer.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string CloudTranscriptionProvider { get; set; } = string.Empty;
|
||||
|
||||
private bool SendsMediaToCloudTranscription => !string.IsNullOrWhiteSpace(this.CloudTranscriptionProvider);
|
||||
|
||||
private string LabelOn => this.SendsMediaToCloudTranscription
|
||||
? T("Yes, please send my data to the external providers")
|
||||
: T("Yes, please send my data to the external embedding provider");
|
||||
|
||||
private string LabelOff => this.SendsMediaToCloudTranscription
|
||||
? T("No, I will choose other providers or files")
|
||||
: T("No, I will choose another embedding");
|
||||
|
||||
private string TranscriptionWarningText => string.Format(
|
||||
T("Warning: Your transcription provider '{0}' is not self-hosted. Audio and video files this data source selects are sent to it to be transcribed, which can cost money. AI Studio keeps every transcript, so an unchanged recording is sent only once. MindWork AI Studio has no control over what that third party does with the recordings after they are sent."),
|
||||
this.CloudTranscriptionProvider);
|
||||
|
||||
private string WarningText
|
||||
{
|
||||
get
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
Class="mb-3"
|
||||
Immediate="@true"
|
||||
Validation="@this.dataSourceValidation.ValidateFilePatterns"
|
||||
HelperText="@T("Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file.")"
|
||||
HelperText="@T("Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file. Audio and video files, e.g., *.mp3 or *.mp4, are transcribed by your transcription provider.")"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.FilterAlt"
|
||||
AdornmentColor="Color.Info"
|
||||
@@ -117,20 +117,17 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
@if (this.CurrentCloudProcessing is not CloudProcessing.NONE)
|
||||
{
|
||||
if (this.SelectedCloudEmbedding)
|
||||
{
|
||||
<DataSourceCloudEmbeddingWarning DataSourceType="DataSourceType.LOCAL_DIRECTORY" SourcePath="@this.dataPath" @bind-UserAcknowledged="@this.dataUserAcknowledgedCloudEmbedding" Validation="@this.dataSourceValidation.ValidateUserAcknowledgedCloudEmbedding"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||
@T("The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
<DataSourceCloudEmbeddingWarning DataSourceType="DataSourceType.LOCAL_DIRECTORY" SourcePath="@this.dataPath" SendsEmbeddingsToCloud="@this.SelectedCloudEmbedding" CloudTranscriptionProvider="@this.CloudTranscriptionProviderName" @bind-UserAcknowledged="@this.UserAcknowledgedCloudProcessing" Validation="@this.dataSourceValidation.ValidateUserAcknowledgedCloudEmbedding"/>
|
||||
}
|
||||
|
||||
else if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||
@T("The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<ManagePandocDependency IntroText="@T("For some data types, such as Office files, MindWork AI Studio requires the open-source application Pandoc.")"/>
|
||||
|
||||
<MudSelect @ref="@this.confidenceLevelSelect" @bind-Value="@this.dataConfidenceLevel" Text="@this.dataConfidenceLevel.GetName()" Label="@T("Required provider confidence level")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateDataSourceConfidenceLevel">
|
||||
@@ -141,7 +138,14 @@
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
|
||||
@if (this.MediaTranscriptionHint is { } mediaTranscriptionHint)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="@true" Class="mb-3">
|
||||
@mediaTranscriptionHint
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudStack Class="mb-3">
|
||||
<MudButton OnClick="@this.ToggleExpertSettings" Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Tune">
|
||||
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
|
||||
|
||||
@@ -42,6 +42,9 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
[Inject]
|
||||
private DataSourceEmbeddingService DataSourceEmbeddingService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private MediaTranscriptionService MediaTranscriptionService { get; init; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
private readonly DataSourceValidation dataSourceValidation;
|
||||
@@ -59,7 +62,7 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
private string dataId = Guid.NewGuid().ToString();
|
||||
private string dataName = string.Empty;
|
||||
private string dataDescription = string.Empty;
|
||||
private bool dataUserAcknowledgedCloudEmbedding;
|
||||
private CloudProcessing acknowledgedCloudProcessing;
|
||||
private string dataEmbeddingId = string.Empty;
|
||||
private string dataPath = string.Empty;
|
||||
private string dataFilePatterns = DataSourceLocalDirectory.DEFAULT_FILE_PATTERNS;
|
||||
@@ -88,6 +91,7 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
this.dataSourceValidation = new()
|
||||
{
|
||||
GetSelectedCloudEmbedding = () => this.SelectedCloudEmbedding,
|
||||
GetSelectedCloudTranscription = () => this.SelectedCloudTranscription,
|
||||
GetSelectedEmbeddingProvider = () => this.SelectedEmbedding,
|
||||
GetConfidenceLevel = () => this.dataConfidenceLevel,
|
||||
GetSettingsManager = () => this.SettingsManager,
|
||||
@@ -162,6 +166,42 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
|
||||
|
||||
private bool SelectedCloudEmbedding => this.SelectedEmbedding is { IsSelfHosted: false };
|
||||
|
||||
/// <summary>
|
||||
/// Whether the file patterns may select audio or video files, which the data source transcribes.
|
||||
/// </summary>
|
||||
private bool SelectsMedia => FileNamePatterns.MaySelectMedia(FileNamePatterns.Parse(this.dataFilePatterns));
|
||||
|
||||
/// <summary>
|
||||
/// The transcription provider which may hear the audio and video files of this data source, or null.
|
||||
/// </summary>
|
||||
private TranscriptionProvider? AllowedTranscriptionProvider => this.MediaTranscriptionService.GetUsableProvider(this.CreateDataSource());
|
||||
|
||||
private bool SelectedCloudTranscription => this.SelectsMedia && this.AllowedTranscriptionProvider is { IsSelfHosted: false };
|
||||
|
||||
private string CloudTranscriptionProviderName => this.SelectedCloudTranscription ? this.AllowedTranscriptionProvider?.Name ?? string.Empty : string.Empty;
|
||||
|
||||
private CloudProcessing CurrentCloudProcessing => (this.SelectedCloudEmbedding ? CloudProcessing.EMBEDDING : CloudProcessing.NONE) | (this.SelectedCloudTranscription ? CloudProcessing.TRANSCRIPTION : CloudProcessing.NONE);
|
||||
|
||||
/// <remarks>
|
||||
/// The acknowledgment holds for the providers it was given for, cf. CloudProcessingExtensions.Covers.
|
||||
/// </remarks>
|
||||
private bool UserAcknowledgedCloudProcessing
|
||||
{
|
||||
get => this.acknowledgedCloudProcessing.Covers(this.CurrentCloudProcessing);
|
||||
set => this.acknowledgedCloudProcessing = value ? this.CurrentCloudProcessing : CloudProcessing.NONE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Why audio and video files which the patterns select would not be indexed, or null when they would.
|
||||
/// </summary>
|
||||
private string? MediaTranscriptionHint => !this.SelectsMedia
|
||||
? null
|
||||
: this.MediaTranscriptionService.GetUsableProvider() is null
|
||||
? T("Audio and video files which this data source selects are skipped until you choose a transcription provider in the settings.")
|
||||
: this.AllowedTranscriptionProvider is null
|
||||
? T("Audio and video files which this data source selects are skipped, because your transcription provider does not meet the confidence level this data source requires.")
|
||||
: null;
|
||||
|
||||
private bool CanChangeSource => !this.IsEditing || !this.LockSource;
|
||||
|
||||
private IEnumerable<ConfigurationSelectData<ConfidenceLevel>> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData();
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
/>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("Select a file for this data source. The content of this file will be processed for the data source.")
|
||||
@T("Select a file for this data source. The content of this file will be processed for the data source. An audio or video file is transcribed by your transcription provider first.")
|
||||
</MudJustifiedText>
|
||||
@if (!this.CanChangeSource)
|
||||
{
|
||||
@@ -90,18 +90,15 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
@if (this.CurrentCloudProcessing is not CloudProcessing.NONE)
|
||||
{
|
||||
if (this.SelectedCloudEmbedding)
|
||||
{
|
||||
<DataSourceCloudEmbeddingWarning DataSourceType="DataSourceType.LOCAL_FILE" SourcePath="@this.dataFilePath" @bind-UserAcknowledged="@this.dataUserAcknowledgedCloudEmbedding" Validation="@this.dataSourceValidation.ValidateUserAcknowledgedCloudEmbedding"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||
@T("The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
<DataSourceCloudEmbeddingWarning DataSourceType="DataSourceType.LOCAL_FILE" SourcePath="@this.dataFilePath" SendsEmbeddingsToCloud="@this.SelectedCloudEmbedding" CloudTranscriptionProvider="@this.CloudTranscriptionProviderName" @bind-UserAcknowledged="@this.UserAcknowledgedCloudProcessing" Validation="@this.dataSourceValidation.ValidateUserAcknowledgedCloudEmbedding"/>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Tertiary" Class="mb-3">
|
||||
@T("The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.")
|
||||
</MudJustifiedText>
|
||||
}
|
||||
|
||||
<ManagePandocDependency IntroText="@T("For some data types, such as Office files, MindWork AI Studio requires the open-source application Pandoc.")"/>
|
||||
@@ -115,6 +112,13 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@if (this.MediaTranscriptionHint is { } mediaTranscriptionHint)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="@true" Class="mb-3">
|
||||
@mediaTranscriptionHint
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudStack Class="mb-3">
|
||||
<MudButton OnClick="@this.ToggleExpertSettings" Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Tune">
|
||||
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
|
||||
|
||||
@@ -2,6 +2,7 @@ using AIStudio.Components;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.Validation;
|
||||
|
||||
@@ -42,6 +43,9 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
[Inject]
|
||||
private DataSourceEmbeddingService DataSourceEmbeddingService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private MediaTranscriptionService MediaTranscriptionService { get; init; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
private readonly DataSourceValidation dataSourceValidation;
|
||||
@@ -59,7 +63,7 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
private string dataId = Guid.NewGuid().ToString();
|
||||
private string dataName = string.Empty;
|
||||
private string dataDescription = string.Empty;
|
||||
private bool dataUserAcknowledgedCloudEmbedding;
|
||||
private CloudProcessing acknowledgedCloudProcessing;
|
||||
private string dataEmbeddingId = string.Empty;
|
||||
private string dataFilePath = string.Empty;
|
||||
private int dataMaxChunkTokenLength;
|
||||
@@ -83,6 +87,7 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
this.dataSourceValidation = new()
|
||||
{
|
||||
GetSelectedCloudEmbedding = () => this.SelectedCloudEmbedding,
|
||||
GetSelectedCloudTranscription = () => this.SelectedCloudTranscription,
|
||||
GetSelectedEmbeddingProvider = () => this.SelectedEmbedding,
|
||||
GetConfidenceLevel = () => this.dataConfidenceLevel,
|
||||
GetSettingsManager = () => this.SettingsManager,
|
||||
@@ -152,6 +157,42 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
|
||||
|
||||
private bool SelectedCloudEmbedding => this.SelectedEmbedding is { IsSelfHosted: false };
|
||||
|
||||
/// <summary>
|
||||
/// Whether the selected file is an audio or video file, which the data source transcribes.
|
||||
/// </summary>
|
||||
private bool SelectsMedia => PersistentMediaTranscript.IsMediaPath(this.dataFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// The transcription provider which may hear the audio and video files of this data source, or null.
|
||||
/// </summary>
|
||||
private TranscriptionProvider? AllowedTranscriptionProvider => this.MediaTranscriptionService.GetUsableProvider(this.CreateDataSource());
|
||||
|
||||
private bool SelectedCloudTranscription => this.SelectsMedia && this.AllowedTranscriptionProvider is { IsSelfHosted: false };
|
||||
|
||||
private string CloudTranscriptionProviderName => this.SelectedCloudTranscription ? this.AllowedTranscriptionProvider?.Name ?? string.Empty : string.Empty;
|
||||
|
||||
private CloudProcessing CurrentCloudProcessing => (this.SelectedCloudEmbedding ? CloudProcessing.EMBEDDING : CloudProcessing.NONE) | (this.SelectedCloudTranscription ? CloudProcessing.TRANSCRIPTION : CloudProcessing.NONE);
|
||||
|
||||
/// <remarks>
|
||||
/// The acknowledgment holds for the providers it was given for, cf. CloudProcessingExtensions.Covers.
|
||||
/// </remarks>
|
||||
private bool UserAcknowledgedCloudProcessing
|
||||
{
|
||||
get => this.acknowledgedCloudProcessing.Covers(this.CurrentCloudProcessing);
|
||||
set => this.acknowledgedCloudProcessing = value ? this.CurrentCloudProcessing : CloudProcessing.NONE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Why the selected audio or video file would not be indexed, or null when it would.
|
||||
/// </summary>
|
||||
private string? MediaTranscriptionHint => !this.SelectsMedia
|
||||
? null
|
||||
: this.MediaTranscriptionService.GetUsableProvider() is null
|
||||
? T("This audio or video file is skipped until you choose a transcription provider in the settings.")
|
||||
: this.AllowedTranscriptionProvider is null
|
||||
? T("This audio or video file is skipped, because your transcription provider does not meet the confidence level this data source requires.")
|
||||
: null;
|
||||
|
||||
private bool CanChangeSource => !this.IsEditing || !this.LockSource;
|
||||
|
||||
private IEnumerable<ConfigurationSelectData<ConfidenceLevel>> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData();
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
}
|
||||
@if (group.Cause.NeedsProviderSettings)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Settings" OnClick="@this.OpenEmbeddingProviderSettings">
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Settings" OnClick="@this.OpenProviderSettings">
|
||||
@T("Open the settings")
|
||||
</MudButton>
|
||||
}
|
||||
@@ -198,9 +198,18 @@
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">
|
||||
@T("Not added to the index")
|
||||
</MudChip>
|
||||
@if (group.Cause.NeedsDataSourceSettings)
|
||||
@if (group.Cause.NeedsDataSourceSettings || group.Cause.NeedsTranscriptionSettings)
|
||||
{
|
||||
<MudSpacer/>
|
||||
}
|
||||
@if (group.Cause.NeedsTranscriptionSettings)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Settings" OnClick="@this.OpenProviderSettings">
|
||||
@T("Open the transcription settings")
|
||||
</MudButton>
|
||||
}
|
||||
@if (group.Cause.NeedsDataSourceSettings)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Settings" OnClick="@this.OpenDataSourceSettings">
|
||||
@T("Manage the data sources")
|
||||
</MudButton>
|
||||
|
||||
@@ -276,7 +276,7 @@ public partial class Embeddings : MSGComponentBase
|
||||
|
||||
private sealed record FailureGroup(FailureCause Cause, string EmbeddingProviderName, IReadOnlyList<DataSourceEmbeddingFailure> Failures);
|
||||
|
||||
private sealed record IgnoredFileCause(int Priority, string Title, string Description, string Icon, Color Color, bool NeedsDataSourceSettings);
|
||||
private sealed record IgnoredFileCause(int Priority, string Title, string Description, string Icon, Color Color, bool NeedsDataSourceSettings, bool NeedsTranscriptionSettings);
|
||||
|
||||
private sealed record IgnoredFileGroup(IgnoredFileCause Cause, IReadOnlyList<DataSourceEmbeddingIgnoredFile> Files);
|
||||
|
||||
@@ -315,8 +315,9 @@ public partial class Embeddings : MSGComponentBase
|
||||
var hasCauseName = !string.IsNullOrWhiteSpace(causeName);
|
||||
var title = hasCauseName ? causeName : T("Other cause");
|
||||
|
||||
var showsMessagePerFile = !hasCauseName || failure.ExtractionCode is FileExtractionErrorCode.MEDIA_PROCESSING_FAILED;
|
||||
return failure.IsPermanent
|
||||
? new FailureCause(3, title, Icons.Material.Filled.SkipNext, Color.Default, true, false, !hasCauseName)
|
||||
? new FailureCause(3, title, Icons.Material.Filled.SkipNext, Color.Default, true, false, showsMessagePerFile)
|
||||
: new FailureCause(2, title, Icons.Material.Filled.ReportProblem, Color.Warning, false, false, !hasCauseName);
|
||||
}
|
||||
|
||||
@@ -342,13 +343,31 @@ public partial class Embeddings : MSGComponentBase
|
||||
string.Format(T("These files match the data source patterns, but they are larger than its limit of {0}. You can raise the limit in the expert settings of the data source."), this.GetMaxFileSizeText(status)),
|
||||
Icons.Material.Filled.Straighten,
|
||||
Color.Warning,
|
||||
true,
|
||||
false),
|
||||
DataSourceEmbeddingIgnoreReason.TRANSCRIPTION_PROVIDER_MISSING => new IgnoredFileCause(
|
||||
0,
|
||||
T("No transcription provider"),
|
||||
T("These audio or video files match the data source patterns, but no transcription provider is configured that AI Studio may use. Once you choose one in the settings, AI Studio transcribes the files and adds their text to the index."),
|
||||
Icons.Material.Filled.MicOff,
|
||||
Color.Warning,
|
||||
false,
|
||||
true),
|
||||
DataSourceEmbeddingIgnoreReason.MEDIA_NOT_SUPPORTED => new IgnoredFileCause(
|
||||
DataSourceEmbeddingIgnoreReason.TRANSCRIPTION_PROVIDER_NOT_ALLOWED => new IgnoredFileCause(
|
||||
0,
|
||||
T("Transcription provider not allowed"),
|
||||
T("These audio or video files match the data source patterns, but your transcription provider does not meet the confidence level this data source requires. Choose another transcription provider, or change the confidence level of the data source."),
|
||||
Icons.Material.Filled.GppBad,
|
||||
Color.Warning,
|
||||
true,
|
||||
true),
|
||||
DataSourceEmbeddingIgnoreReason.IMAGE_NOT_SUPPORTED => new IgnoredFileCause(
|
||||
1,
|
||||
FileTypes.MEDIA.FilterName,
|
||||
T("These image, audio, or video files match the data source patterns, but they cannot currently be added to the index."),
|
||||
Icons.Material.Filled.PermMedia,
|
||||
FileTypes.IMAGE.FilterName,
|
||||
T("These image files match the data source patterns, but images cannot currently be added to the index."),
|
||||
Icons.Material.Filled.ImageNotSupported,
|
||||
Color.Default,
|
||||
false,
|
||||
false),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(file.Reason), file.Reason, null),
|
||||
};
|
||||
@@ -445,7 +464,9 @@ public partial class Embeddings : MSGComponentBase
|
||||
{
|
||||
return this.DataSourceEmbeddingService.CanRefreshDataSource(status.DataSourceId) &&
|
||||
status is { VectorStoreUnreadable: false, State: not DataSourceEmbeddingState.RUNNING and not DataSourceEmbeddingState.QUEUED } &&
|
||||
(status.State is DataSourceEmbeddingState.FAILED || status.FailedDocuments > 0);
|
||||
(status.State is DataSourceEmbeddingState.FAILED
|
||||
|| status.FailedDocuments > 0
|
||||
|| status.NeedsTranscriptionProvider);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
@@ -460,13 +481,13 @@ public partial class Embeddings : MSGComponentBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the user to the settings, where the embedding providers are configured.
|
||||
/// Takes the user to the settings, where embedding and transcription providers are configured.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Offered only for the failures a setting fixes, such as a rejected API key. Reading what
|
||||
/// went wrong and then having to find the right page is where people give up.
|
||||
/// Offered only for a problem a provider setting fixes. Reading what went wrong and then
|
||||
/// having to find the right page is where people give up.
|
||||
/// </remarks>
|
||||
private void OpenEmbeddingProviderSettings() => this.NavigationManager.NavigateTo(Routes.SETTINGS);
|
||||
private void OpenProviderSettings() => this.NavigationManager.NavigateTo(Routes.SETTINGS);
|
||||
|
||||
private async Task RefreshDataSource(DataSourceEmbeddingStatus status)
|
||||
{
|
||||
|
||||
+82
-22
@@ -345,9 +345,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T4242312602"] = "Senden an
|
||||
-- Copy result
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::ASSISTANTBASE::T83711157"] = "Ergebnis kopieren"
|
||||
|
||||
-- The transcription provider returned an empty transcript.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1080540822"] = "Der Anbieter für Transkriptionen hat eine leere Transkription zurückgegeben."
|
||||
|
||||
-- Hide Expert Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1108876344"] = "Experten-Einstellungen ausblenden"
|
||||
|
||||
@@ -462,9 +459,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- Tools for this batch run
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2247412388"] = "Werkzeuge für diesen Durchlauf"
|
||||
|
||||
-- No usable transcription provider is configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2282521655"] = "Es ist kein verwendbarer Anbieter für Transkriptionen konfiguriert."
|
||||
|
||||
-- Was not able to create the output folder: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2290092642"] = "Der Ausgabeordner konnte nicht erstellt werden: {0}"
|
||||
|
||||
@@ -582,9 +576,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- {0} of {1} files processed
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3648144402"] = "{0} von {1} Dateien verarbeitet"
|
||||
|
||||
-- Was not able to store the transcript next to the media file: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3691287653"] = "Das Transkript konnte nicht neben der Mediendatei gespeichert werden: {0}"
|
||||
|
||||
-- Time
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3756319748"] = "Zeit"
|
||||
|
||||
@@ -3960,6 +3951,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T2373663376"] = "Gut zu wiss
|
||||
-- Data folder
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T2422278827"] = "Datenordner"
|
||||
|
||||
-- The transcripts of the audio and video files in your local data sources.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T260303721"] = "Die Transkripte der Audio- und Videodateien in Ihren lokalen Datenquellen."
|
||||
|
||||
-- Explanation
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T2840582448"] = "Erklärung"
|
||||
|
||||
@@ -4038,9 +4032,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T1159107
|
||||
-- No, I will choose another embedding
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T1246976418"] = "Nein, ich wähle eine andere Einbettung aus"
|
||||
|
||||
-- No, I will choose other providers or files
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T1811383874"] = "Nein, ich wähle andere Anbieter oder Dateien aus"
|
||||
|
||||
-- Warning: Your transcription provider '{0}' is not self-hosted. Audio and video files this data source selects are sent to it to be transcribed, which can cost money. AI Studio keeps every transcript, so an unchanged recording is sent only once. MindWork AI Studio has no control over what that third party does with the recordings after they are sent.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T200667320"] = "Warnung: Ihr Transkriptionsanbieter „{0}“ ist nicht selbst gehostet. Audio- und Videodateien, die diese Datenquelle auswählt, werden zur Transkription an ihn gesendet, was Geld kosten kann. AI Studio speichert jedes Transkript, sodass eine unveränderte Aufnahme nur einmal gesendet wird. MindWork AI Studio hat keine Kontrolle darüber, was dieser Dritte mit den Aufnahmen nach dem Versand macht."
|
||||
|
||||
-- Every mail of this mailbox in the selected folder and period, together with the text of its attachments,
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T2046044636"] = "Jede E-Mail dieses Postfachs im ausgewählten Ordner und Zeitraum, zusammen mit dem Text ihrer Anhänge,"
|
||||
|
||||
-- Yes, please send my data to the external providers
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T2239851837"] = "Ja, bitte senden Sie meine Daten an die externen Anbieter"
|
||||
|
||||
-- The data source '{0}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T2503488371"] = "Die Datenquelle „{0}“"
|
||||
|
||||
@@ -6777,6 +6780,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T286958948"]
|
||||
-- Token limit
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2961294165"] = "Token-Limit"
|
||||
|
||||
-- Audio and video files which this data source selects are skipped, because your transcription provider does not meet the confidence level this data source requires.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T3017942214"] = "Audio- und Videodateien, die diese Datenquelle auswählt, werden übersprungen, weil Ihr Transkriptionsanbieter das Vertrauensniveau nicht erfüllt, das diese Datenquelle erfordert."
|
||||
|
||||
-- Audio and video files which this data source selects are skipped until you choose a transcription provider in the settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T3119708878"] = "Audio- und Videodateien, die diese Datenquelle auswählt, werden übersprungen, bis Sie in den Einstellungen einen Transkriptionsanbieter auswählen."
|
||||
|
||||
-- Please enter 0 or a positive overlap length.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T3242265813"] = "Bitte geben Sie 0 oder eine positive Überlappungslänge ein."
|
||||
|
||||
@@ -6801,12 +6810,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T529871858"]
|
||||
-- Select the base directory
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T562479068"] = "Wählen Sie das Stammverzeichnis aus"
|
||||
|
||||
-- Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file. Audio and video files, e.g., *.mp3 or *.mp4, are transcribed by your transcription provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T599406584"] = "Welche Dateien sollen indexiert werden? Trennen Sie mehrere Muster durch ein Semikolon, z. B. *.txt;*.pdf;*.docx;.gitignore. Verwenden Sie :documents: für alle Dokumente und lesbaren Textdateien, :text: nur für lesbare Textdateien oder * für alle Dateien. Audio- und Videodateien, z. B. *.mp3 oder *.mp4, werden von Ihrem Transkriptionsanbieter transkribiert."
|
||||
|
||||
-- Yes, index hidden files and folders as well
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T607153623"] = "Ja, auch versteckte Dateien und Ordner indexieren"
|
||||
|
||||
-- Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T761052435"] = "Welche Dateien sollen indexiert werden? Trennen Sie mehrere Muster durch ein Semikolon, z. B. *.txt;*.pdf;*.docx;.gitignore. Verwenden Sie :documents: für alle Dokumente und lesbaren Textdateien, :text: nur für lesbare Textdateien oder * für alle Dateien."
|
||||
|
||||
-- The data source token limit must not be larger than the embedding provider token limit ({0}).
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T787118522"] = "Das Token-Limit der Datenquelle darf nicht größer sein als das Token-Limit des Einbettungsanbieters ({0})."
|
||||
|
||||
@@ -6942,9 +6951,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1108876344"] = "
|
||||
-- Optional expert settings for how this data source is split before embedding.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1133561850"] = "Optionale Experteneinstellungen für die Aufteilung dieser Datenquelle vor dem Einbetten."
|
||||
|
||||
-- Select a file for this data source. The content of this file will be processed for the data source.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1190880267"] = "Wählen Sie eine Datei für diese Datenquelle aus. Der Inhalt dieser Datei wird für die Datenquelle verarbeitet."
|
||||
|
||||
-- Default tokenizer
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1220918127"] = "Standard-Tokenizer"
|
||||
|
||||
@@ -6978,6 +6984,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T236253137"] = "E
|
||||
-- Please enter a token limit of at least 1.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T2406580478"] = "Bitte geben Sie ein Token-Limit von mindestens 1 ein."
|
||||
|
||||
-- This audio or video file is skipped, because your transcription provider does not meet the confidence level this data source requires.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T2468760910"] = "Diese Audio- oder Videodatei wird übersprungen, weil Ihr Transkriptionsanbieter das Vertrauensniveau nicht erfüllt, das diese Datenquelle erfordert."
|
||||
|
||||
-- Add
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T2646845972"] = "Hinzufügen"
|
||||
|
||||
@@ -7005,6 +7014,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T3361153305"] = "
|
||||
-- Select the file
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T3740148848"] = "Datei auswählen"
|
||||
|
||||
-- This audio or video file is skipped until you choose a transcription provider in the settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T4026150982"] = "Diese Audio- oder Videodatei wird übersprungen, bis Sie in den Einstellungen einen Transkriptionsanbieter auswählen."
|
||||
|
||||
-- In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T529871858"] = "Damit die KI jederzeit die passenden Daten ermitteln kann, müssen Sie einen Einbettungsanbieter auswählen."
|
||||
|
||||
@@ -7023,6 +7035,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T900713019"] = "A
|
||||
-- Selected file path for this data source
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T939749563"] = "Ausgewählter Dateipfad für diese Datenquelle"
|
||||
|
||||
-- Select a file for this data source. The content of this file will be processed for the data source. An audio or video file is transcribed by your transcription provider first.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T966073786"] = "Wählen Sie eine Datei für diese Datenquelle aus. Der Inhalt dieser Datei wird für die Datenquelle verarbeitet. Eine Audio- oder Videodatei wird zunächst von Ihrem Transkriptionsanbieter transkribiert."
|
||||
|
||||
-- Token overlap
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T981382809"] = "Token-Überlappung"
|
||||
|
||||
@@ -10155,15 +10170,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1143368054"] = "Andere Ursache"
|
||||
-- Current file: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1166856644"] = "Aktuelle Datei: {0}"
|
||||
|
||||
-- Transcription provider not allowed
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1283933457"] = "Transkriptionsanbieter nicht zulässig"
|
||||
|
||||
-- Mailboxes are synced every 16 minutes while the automatic refresh is on. Each mail is embedded only once: when it moves to another folder, AI Studio merely notes where it lies now.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1297391911"] = "Postfächer werden alle 16 Minuten synchronisiert, solange die automatische Aktualisierung aktiviert ist. Jede E-Mail wird nur einmal eingebettet: Wird sie in einen anderen Ordner verschoben, merkt sich AI Studio lediglich, wo sie sich jetzt befindet."
|
||||
|
||||
-- File {0} of {1} is being indexed: block {2}, page {3}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1298290372"] = "Datei {0} von {1} wird indexiert: Block {2}, Seite {3}."
|
||||
|
||||
-- These image, audio, or video files match the data source patterns, but they cannot currently be added to the index.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1302021733"] = "Diese Bild-, Audio- oder Videodateien entsprechen den Mustern der Datenquelle, können derzeit jedoch nicht zum Index hinzugefügt werden."
|
||||
|
||||
-- Mail
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1399880782"] = "E-Mail"
|
||||
|
||||
@@ -10194,6 +10209,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1616414701"] = "Datei {0} von {1}
|
||||
-- Skipped: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1775451753"] = "Übersprungen: {0}"
|
||||
|
||||
-- These audio or video files match the data source patterns, but your transcription provider does not meet the confidence level this data source requires. Choose another transcription provider, or change the confidence level of the data source.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1811387825"] = "Diese Audio- oder Videodateien entsprechen den Mustern der Datenquelle, aber Ihr Transkriptionsanbieter erfüllt nicht das Vertrauensniveau, das die Datenquelle erfordert. Wählen Sie einen anderen Transkriptionsanbieter oder ändern Sie das Vertrauensniveau der Datenquelle."
|
||||
|
||||
-- Tried again during the next run
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1946414905"] = "Beim nächsten Durchlauf erneut versucht"
|
||||
|
||||
@@ -10266,6 +10284,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3628102488"] = "{0} Dateien ({1})
|
||||
-- No local data source has been queued for embedding yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3774205531"] = "Es wurde noch keine lokale Datenquelle für die Einbettung in die Warteschlange aufgenommen."
|
||||
|
||||
-- These audio or video files match the data source patterns, but no transcription provider is configured that AI Studio may use. Once you choose one in the settings, AI Studio transcribes the files and adds their text to the index.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3775894071"] = "Diese Audio- oder Videodateien entsprechen den Mustern der Datenquelle, aber es ist kein Transkriptionsanbieter konfiguriert, den AI Studio verwenden darf. Sobald Sie in den Einstellungen einen Anbieter auswählen, transkribiert AI Studio die Dateien und fügt ihren Text zum Index hinzu."
|
||||
|
||||
-- Change the settings of the mailbox
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3788376715"] = "Einstellungen des Postfachs ändern"
|
||||
|
||||
@@ -10284,9 +10305,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T419343388"] = "Aus dem Index entf
|
||||
-- Not synced completely yet. AI Studio works through the mailbox piece by piece, the newest mails first, and older mails are still missing.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4205079539"] = "Noch nicht vollständig synchronisiert. AI Studio synchronisiert das Postfach nach und nach, beginnend mit den neuesten E-Mails. Ältere E-Mails fehlen noch."
|
||||
|
||||
-- These image files match the data source patterns, but images cannot currently be added to the index.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T422032787"] = "Diese Bilddateien entsprechen den Mustern der Datenquelle, können derzeit jedoch nicht zum Index hinzugefügt werden."
|
||||
|
||||
-- Current mail: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4242653147"] = "Aktuelle E-Mail: {0}"
|
||||
|
||||
-- No transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4253595107"] = "Kein Transkriptionsanbieter"
|
||||
|
||||
-- Skipped files: {0}. Open a group below to see why and what you can do.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4287145754"] = "Übersprungene Dateien: {0}. Öffnen Sie unten eine Gruppe, um zu sehen, warum und was Sie tun können."
|
||||
|
||||
@@ -10305,6 +10332,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T819555436"] = "Fehlgeschlagene E-
|
||||
-- Skipped
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T852993367"] = "Übersprungen"
|
||||
|
||||
-- Open the transcription settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T905182399"] = "Transkriptionseinstellungen öffnen"
|
||||
|
||||
-- Indexed: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T933775466"] = "Indexiert: {0}"
|
||||
|
||||
@@ -12330,6 +12360,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T677355172"] = "Un
|
||||
-- Not a readable spreadsheet
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1175970425"] = "Keine lesbare Tabellenkalkulation"
|
||||
|
||||
-- The media file '{0}' could not be processed, so it was not indexed. AI Studio reads it again as soon as the file changes.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1193172614"] = "Die Mediendatei „{0}“ konnte nicht verarbeitet und daher nicht indexiert werden. AI Studio liest sie erneut ein, sobald sich die Datei ändert."
|
||||
|
||||
-- Media file could not be processed
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1302965532"] = "Mediendatei konnte nicht verarbeitet werden"
|
||||
|
||||
-- Not a text file
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1465212038"] = "Keine Textdatei"
|
||||
|
||||
@@ -12564,6 +12600,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::MAIL::MAILBOXPROVIDERTEMPLATEEXTENSIONS::T4026
|
||||
-- Edit Mailbox
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MAILBOXEDITING::T3250445637"] = "Postfach bearbeiten"
|
||||
|
||||
-- The transcription provider returned an empty transcript.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T1080540822"] = "Der Transkriptionsanbieter hat ein leeres Transkript zurückgegeben."
|
||||
|
||||
-- No usable transcription provider is configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T2282521655"] = "Es ist kein nutzbarer Transkriptionsanbieter konfiguriert."
|
||||
|
||||
-- Was not able to read the existing transcript: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T2397111152"] = "Das vorhandene Transkript konnte nicht gelesen werden: {0}"
|
||||
|
||||
-- The media file changed while it was being transcribed, so its transcript was not kept.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T2455890169"] = "Die Mediendatei wurde während der Transkription geändert, daher wurde das Transkript verworfen."
|
||||
|
||||
-- The media file could not be read.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T3826117761"] = "Die Mediendatei konnte nicht gelesen werden."
|
||||
|
||||
-- Was not able to store the transcript: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T920159834"] = "Das Transkript konnte nicht gespeichert werden: {0}"
|
||||
|
||||
-- AI Studio couldn't install Pandoc because the archive was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio konnte Pandoc nicht installieren, da das Archiv nicht gefunden wurde."
|
||||
|
||||
@@ -13635,6 +13689,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T1225
|
||||
-- Some mails could not be indexed. The list below says which ones and why.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T2360744026"] = "Einige E-Mails konnten nicht indexiert werden. In der folgenden Liste steht, welche E-Mails betroffen sind und warum."
|
||||
|
||||
-- The media file '{0}' could not be processed, so it was not indexed. Reason: {1} AI Studio reads it again as soon as the file changes.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T2967004571"] = "Die Mediendatei „{0}“ konnte nicht verarbeitet und daher nicht indexiert werden. Grund: {1} AI Studio liest sie erneut ein, sobald sich die Datei ändert."
|
||||
|
||||
-- The mail '{0}' could not be indexed. AI Studio tries again during the next sync.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T804280374"] = "Die E-Mail „{0}“ konnte nicht indexiert werden. AI Studio versucht es bei der nächsten Synchronisierung erneut."
|
||||
|
||||
@@ -14407,7 +14464,7 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1013354736"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1120586536"] = "Bitte wählen Sie ein erforderliches Vertrauensniveau des Anbieters aus."
|
||||
|
||||
-- The connection test failed. Please check the connection settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T132896331"] = "Der Verbindungstest ist fehlgeschlagen. Bitte überprüfe die Verbindungseinstellungen."
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T132896331"] = "Der Verbindungstest ist fehlgeschlagen. Bitte überprüfen Sie die Verbindungseinstellungen."
|
||||
|
||||
-- Please select an embedding provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1461238528"] = "Bitte wählen Sie einen Anbieter für die Einbettung aus."
|
||||
@@ -14416,7 +14473,7 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1461238528"
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1481612527"] = "Bitte wählen Sie aus, wie die Verbindung zum Server verschlüsselt wird."
|
||||
|
||||
-- The file does not exist. Please select a valid file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T148464502"] = "Die Datei existiert nicht. Bitte wähle eine gültige Datei aus."
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T148464502"] = "Die Datei existiert nicht. Bitte wählen Sie eine gültige Datei aus."
|
||||
|
||||
-- Please select one retrieval process.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T1515898234"] = "Bitte wählen Sie einen Abrufprozess aus."
|
||||
@@ -14445,8 +14502,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2222598306"
|
||||
-- Please select your security policy.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2250909198"] = "Bitte wählen Sie Ihre Sicherheitsrichtlinie aus."
|
||||
|
||||
-- The selected file is not a supported document or readable text file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2728355006"] = "Die ausgewählte Datei ist weder ein unterstütztes Dokument noch eine lesbare Textdatei."
|
||||
-- Please acknowledge that you are aware of the cloud transcription implications.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2818048408"] = "Bitte bestätigen Sie, dass Ihnen die Auswirkungen der Cloud-Transkription bewusst sind."
|
||||
|
||||
-- Please test the connection before saving.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T285470497"] = "Bitte testen Sie die Verbindung, bevor Sie speichern."
|
||||
@@ -14493,6 +14550,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T490875633"]
|
||||
-- The file path must not be empty. Please select a file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T878007824"] = "Der Dateipfad darf nicht leer sein. Bitte wählen Sie eine Datei aus."
|
||||
|
||||
-- The selected file is not a supported document, readable text file, audio file, or video file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T90753429"] = "Die ausgewählte Datei ist kein unterstütztes Dokument, keine lesbare Textdatei und keine Audio- oder Videodatei."
|
||||
|
||||
-- Please enter the secret necessary for authentication.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T968385876"] = "Bitte geben Sie das für die Authentifizierung benötigte Geheimnis ein."
|
||||
|
||||
|
||||
+80
-20
@@ -345,9 +345,6 @@ 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."
|
||||
|
||||
-- Hide Expert Settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1108876344"] = "Hide Expert Settings"
|
||||
|
||||
@@ -462,9 +459,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- Tools for this batch run
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2247412388"] = "Tools for this batch run"
|
||||
|
||||
-- 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}"
|
||||
|
||||
@@ -582,9 +576,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
|
||||
-- {0} of {1} files processed
|
||||
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3648144402"] = "{0} of {1} files processed"
|
||||
|
||||
-- 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"
|
||||
|
||||
@@ -3960,6 +3951,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T2373663376"] = "Good to kno
|
||||
-- Data folder
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T2422278827"] = "Data folder"
|
||||
|
||||
-- The transcripts of the audio and video files in your local data sources.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T260303721"] = "The transcripts of the audio and video files in your local data sources."
|
||||
|
||||
-- Explanation
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T2840582448"] = "Explanation"
|
||||
|
||||
@@ -4038,9 +4032,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T1159107
|
||||
-- No, I will choose another embedding
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T1246976418"] = "No, I will choose another embedding"
|
||||
|
||||
-- No, I will choose other providers or files
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T1811383874"] = "No, I will choose other providers or files"
|
||||
|
||||
-- Warning: Your transcription provider '{0}' is not self-hosted. Audio and video files this data source selects are sent to it to be transcribed, which can cost money. AI Studio keeps every transcript, so an unchanged recording is sent only once. MindWork AI Studio has no control over what that third party does with the recordings after they are sent.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T200667320"] = "Warning: Your transcription provider '{0}' is not self-hosted. Audio and video files this data source selects are sent to it to be transcribed, which can cost money. AI Studio keeps every transcript, so an unchanged recording is sent only once. MindWork AI Studio has no control over what that third party does with the recordings after they are sent."
|
||||
|
||||
-- Every mail of this mailbox in the selected folder and period, together with the text of its attachments,
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T2046044636"] = "Every mail of this mailbox in the selected folder and period, together with the text of its attachments,"
|
||||
|
||||
-- Yes, please send my data to the external providers
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T2239851837"] = "Yes, please send my data to the external providers"
|
||||
|
||||
-- The data source '{0}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCECLOUDEMBEDDINGWARNING::T2503488371"] = "The data source '{0}'"
|
||||
|
||||
@@ -6777,6 +6780,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T286958948"]
|
||||
-- Token limit
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2961294165"] = "Token limit"
|
||||
|
||||
-- Audio and video files which this data source selects are skipped, because your transcription provider does not meet the confidence level this data source requires.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T3017942214"] = "Audio and video files which this data source selects are skipped, because your transcription provider does not meet the confidence level this data source requires."
|
||||
|
||||
-- Audio and video files which this data source selects are skipped until you choose a transcription provider in the settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T3119708878"] = "Audio and video files which this data source selects are skipped until you choose a transcription provider in the settings."
|
||||
|
||||
-- Please enter 0 or a positive overlap length.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T3242265813"] = "Please enter 0 or a positive overlap length."
|
||||
|
||||
@@ -6801,12 +6810,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T529871858"]
|
||||
-- Select the base directory
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T562479068"] = "Select the base directory"
|
||||
|
||||
-- Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file. Audio and video files, e.g., *.mp3 or *.mp4, are transcribed by your transcription provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T599406584"] = "Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file. Audio and video files, e.g., *.mp3 or *.mp4, are transcribed by your transcription provider."
|
||||
|
||||
-- Yes, index hidden files and folders as well
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T607153623"] = "Yes, index hidden files and folders as well"
|
||||
|
||||
-- Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T761052435"] = "Which files should be indexed? Separate multiple patterns with a semicolon, e.g., *.txt;*.pdf;*.docx;.gitignore. Use :documents: for every document and readable text file, :text: for readable text files only, or * for every file."
|
||||
|
||||
-- The data source token limit must not be larger than the embedding provider token limit ({0}).
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T787118522"] = "The data source token limit must not be larger than the embedding provider token limit ({0})."
|
||||
|
||||
@@ -6942,9 +6951,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1108876344"] = "
|
||||
-- Optional expert settings for how this data source is split before embedding.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1133561850"] = "Optional expert settings for how this data source is split before embedding."
|
||||
|
||||
-- Select a file for this data source. The content of this file will be processed for the data source.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1190880267"] = "Select a file for this data source. The content of this file will be processed for the data source."
|
||||
|
||||
-- Default tokenizer
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T1220918127"] = "Default tokenizer"
|
||||
|
||||
@@ -6978,6 +6984,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T236253137"] = "R
|
||||
-- Please enter a token limit of at least 1.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T2406580478"] = "Please enter a token limit of at least 1."
|
||||
|
||||
-- This audio or video file is skipped, because your transcription provider does not meet the confidence level this data source requires.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T2468760910"] = "This audio or video file is skipped, because your transcription provider does not meet the confidence level this data source requires."
|
||||
|
||||
-- Add
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T2646845972"] = "Add"
|
||||
|
||||
@@ -7005,6 +7014,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T3361153305"] = "
|
||||
-- Select the file
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T3740148848"] = "Select the file"
|
||||
|
||||
-- This audio or video file is skipped until you choose a transcription provider in the settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T4026150982"] = "This audio or video file is skipped until you choose a transcription provider in the settings."
|
||||
|
||||
-- In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T529871858"] = "In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider."
|
||||
|
||||
@@ -7023,6 +7035,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T900713019"] = "C
|
||||
-- Selected file path for this data source
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T939749563"] = "Selected file path for this data source"
|
||||
|
||||
-- Select a file for this data source. The content of this file will be processed for the data source. An audio or video file is transcribed by your transcription provider first.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T966073786"] = "Select a file for this data source. The content of this file will be processed for the data source. An audio or video file is transcribed by your transcription provider first."
|
||||
|
||||
-- Token overlap
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T981382809"] = "Token overlap"
|
||||
|
||||
@@ -10155,15 +10170,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1143368054"] = "Other cause"
|
||||
-- Current file: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1166856644"] = "Current file: {0}"
|
||||
|
||||
-- Transcription provider not allowed
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1283933457"] = "Transcription provider not allowed"
|
||||
|
||||
-- Mailboxes are synced every 16 minutes while the automatic refresh is on. Each mail is embedded only once: when it moves to another folder, AI Studio merely notes where it lies now.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1297391911"] = "Mailboxes are synced every 16 minutes while the automatic refresh is on. Each mail is embedded only once: when it moves to another folder, AI Studio merely notes where it lies now."
|
||||
|
||||
-- File {0} of {1} is being indexed: block {2}, page {3}.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1298290372"] = "File {0} of {1} is being indexed: block {2}, page {3}."
|
||||
|
||||
-- These image, audio, or video files match the data source patterns, but they cannot currently be added to the index.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1302021733"] = "These image, audio, or video files match the data source patterns, but they cannot currently be added to the index."
|
||||
|
||||
-- Mail
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1399880782"] = "Mail"
|
||||
|
||||
@@ -10194,6 +10209,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1616414701"] = "File {0} of {1} i
|
||||
-- Skipped: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1775451753"] = "Skipped: {0}"
|
||||
|
||||
-- These audio or video files match the data source patterns, but your transcription provider does not meet the confidence level this data source requires. Choose another transcription provider, or change the confidence level of the data source.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1811387825"] = "These audio or video files match the data source patterns, but your transcription provider does not meet the confidence level this data source requires. Choose another transcription provider, or change the confidence level of the data source."
|
||||
|
||||
-- Tried again during the next run
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1946414905"] = "Tried again during the next run"
|
||||
|
||||
@@ -10266,6 +10284,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3628102488"] = "{0} files ({1})"
|
||||
-- No local data source has been queued for embedding yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3774205531"] = "No local data source has been queued for embedding yet."
|
||||
|
||||
-- These audio or video files match the data source patterns, but no transcription provider is configured that AI Studio may use. Once you choose one in the settings, AI Studio transcribes the files and adds their text to the index.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3775894071"] = "These audio or video files match the data source patterns, but no transcription provider is configured that AI Studio may use. Once you choose one in the settings, AI Studio transcribes the files and adds their text to the index."
|
||||
|
||||
-- Change the settings of the mailbox
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T3788376715"] = "Change the settings of the mailbox"
|
||||
|
||||
@@ -10284,9 +10305,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T419343388"] = "Remove them from t
|
||||
-- Not synced completely yet. AI Studio works through the mailbox piece by piece, the newest mails first, and older mails are still missing.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4205079539"] = "Not synced completely yet. AI Studio works through the mailbox piece by piece, the newest mails first, and older mails are still missing."
|
||||
|
||||
-- These image files match the data source patterns, but images cannot currently be added to the index.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T422032787"] = "These image files match the data source patterns, but images cannot currently be added to the index."
|
||||
|
||||
-- Current mail: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4242653147"] = "Current mail: {0}"
|
||||
|
||||
-- No transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4253595107"] = "No transcription provider"
|
||||
|
||||
-- Skipped files: {0}. Open a group below to see why and what you can do.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T4287145754"] = "Skipped files: {0}. Open a group below to see why and what you can do."
|
||||
|
||||
@@ -10305,6 +10332,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T819555436"] = "Failed mails: {0}"
|
||||
-- Skipped
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T852993367"] = "Skipped"
|
||||
|
||||
-- Open the transcription settings
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T905182399"] = "Open the transcription settings"
|
||||
|
||||
-- Indexed: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T933775466"] = "Indexed: {0}"
|
||||
|
||||
@@ -12330,6 +12360,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXPORTFORMATEXTENSIONS::T677355172"] = "Un
|
||||
-- Not a readable spreadsheet
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1175970425"] = "Not a readable spreadsheet"
|
||||
|
||||
-- The media file '{0}' could not be processed, so it was not indexed. AI Studio reads it again as soon as the file changes.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1193172614"] = "The media file '{0}' could not be processed, so it was not indexed. AI Studio reads it again as soon as the file changes."
|
||||
|
||||
-- Media file could not be processed
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1302965532"] = "Media file could not be processed"
|
||||
|
||||
-- Not a text file
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONERRORCODEEXTENSIONS::T1465212038"] = "Not a text file"
|
||||
|
||||
@@ -12564,6 +12600,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::MAIL::MAILBOXPROVIDERTEMPLATEEXTENSIONS::T4026
|
||||
-- Edit Mailbox
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MAILBOXEDITING::T3250445637"] = "Edit Mailbox"
|
||||
|
||||
-- The transcription provider returned an empty transcript.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T1080540822"] = "The transcription provider returned an empty transcript."
|
||||
|
||||
-- No usable transcription provider is configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T2282521655"] = "No usable transcription provider is configured."
|
||||
|
||||
-- Was not able to read the existing transcript: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T2397111152"] = "Was not able to read the existing transcript: {0}"
|
||||
|
||||
-- The media file changed while it was being transcribed, so its transcript was not kept.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T2455890169"] = "The media file changed while it was being transcribed, so its transcript was not kept."
|
||||
|
||||
-- The media file could not be read.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T3826117761"] = "The media file could not be read."
|
||||
|
||||
-- Was not able to store the transcript: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::MEDIA::PERSISTENTMEDIATRANSCRIPT::T920159834"] = "Was not able to store the transcript: {0}"
|
||||
|
||||
-- AI Studio couldn't install Pandoc because the archive was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
|
||||
|
||||
@@ -13635,6 +13689,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T1225
|
||||
-- Some mails could not be indexed. The list below says which ones and why.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T2360744026"] = "Some mails could not be indexed. The list below says which ones and why."
|
||||
|
||||
-- The media file '{0}' could not be processed, so it was not indexed. Reason: {1} AI Studio reads it again as soon as the file changes.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T2967004571"] = "The media file '{0}' could not be processed, so it was not indexed. Reason: {1} AI Studio reads it again as soon as the file changes."
|
||||
|
||||
-- The mail '{0}' could not be indexed. AI Studio tries again during the next sync.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T804280374"] = "The mail '{0}' could not be indexed. AI Studio tries again during the next sync."
|
||||
|
||||
@@ -14445,8 +14502,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2222598306"
|
||||
-- Please select your security policy.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2250909198"] = "Please select your security policy."
|
||||
|
||||
-- The selected file is not a supported document or readable text file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2728355006"] = "The selected file is not a supported document or readable text file."
|
||||
-- Please acknowledge that you are aware of the cloud transcription implications.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T2818048408"] = "Please acknowledge that you are aware of the cloud transcription implications."
|
||||
|
||||
-- Please test the connection before saving.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T285470497"] = "Please test the connection before saving."
|
||||
@@ -14493,6 +14550,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T490875633"]
|
||||
-- The file path must not be empty. Please select a file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T878007824"] = "The file path must not be empty. Please select a file."
|
||||
|
||||
-- The selected file is not a supported document, readable text file, audio file, or video file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T90753429"] = "The selected file is not a supported document, readable text file, audio file, or video file."
|
||||
|
||||
-- Please enter the secret necessary for authentication.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T968385876"] = "Please enter the secret necessary for authentication."
|
||||
|
||||
|
||||
@@ -635,6 +635,30 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads which parameter of the request a provider blamed in the body of a failed response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// OpenAI and the APIs compatible with it name it in the field 'param' of their error object.
|
||||
/// </remarks>
|
||||
/// <param name="responseBody">The body of the failed response.</param>
|
||||
/// <returns>The name of the parameter, or null when the body names none.</returns>
|
||||
private static string? ReadProviderErrorParameter(string responseBody)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(responseBody))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(responseBody);
|
||||
return TryGetErrorElement(document.RootElement, out var errorElement) ? TryGetString(errorElement, "param") : null;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetErrorElement(JsonElement root, out JsonElement errorElement)
|
||||
{
|
||||
if (root.ValueKind is JsonValueKind.Object &&
|
||||
@@ -1473,7 +1497,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
if (!string.IsNullOrWhiteSpace(classifiedMessage))
|
||||
return classifiedMessage;
|
||||
|
||||
if (statusCode is HttpStatusCode.BadRequest && responseBody.Contains("not decode", StringComparison.OrdinalIgnoreCase))
|
||||
if (IsUndecodableFormatFailure(statusCode, responseBody))
|
||||
return string.Format(TB("The provider '{0}' was not able to read the audio file. It probably does not support the WebM/Opus format which AI Studio sends. Please contact the provider about it."), this.InstanceName);
|
||||
|
||||
var providerMessage = ReadProviderErrorMessage(responseBody);
|
||||
@@ -1483,6 +1507,33 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static bool IsUndecodableFormatFailure(HttpStatusCode statusCode, string responseBody) => statusCode is HttpStatusCode.BadRequest && responseBody.Contains("not decode", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a transcription provider refused the media file itself rather than the request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The provider gives the same answer for the same file every time, so sending it again only
|
||||
/// repeats the upload. That holds for a file which is too large or of a type the provider does
|
||||
/// not accept, and for a bad request which names the file as the faulty parameter, as OpenAI and
|
||||
/// the APIs compatible with it do, e.g., for a damaged recording. A provider which cannot decode
|
||||
/// WebM/Opus at all is the exception: it refuses every file the same way, and the user fixes
|
||||
/// that by choosing another provider, not by changing the file. Everything else, such as rate
|
||||
/// limits, keys, models, and outages, may pass.
|
||||
/// </remarks>
|
||||
/// <param name="statusCode">The status code the provider answered with.</param>
|
||||
/// <param name="responseBody">The body the provider answered with.</param>
|
||||
/// <returns>True, when the provider would refuse the unchanged file again.</returns>
|
||||
internal static bool IsMediaFileRejection(HttpStatusCode statusCode, string responseBody)
|
||||
{
|
||||
if (statusCode is HttpStatusCode.RequestEntityTooLarge or HttpStatusCode.UnsupportedMediaType)
|
||||
return true;
|
||||
|
||||
return statusCode is HttpStatusCode.BadRequest
|
||||
&& !IsUndecodableFormatFailure(statusCode, responseBody)
|
||||
&& string.Equals(ReadProviderErrorParameter(responseBody), "file", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected async Task<TranscriptionResult> PerformStandardTranscriptionRequest(RequestedSecret requestedSecret, Model transcriptionModel, string audioFilePath, Host host = Host.NONE, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
@@ -1561,7 +1612,10 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger.LogError("Transcription request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
|
||||
return TranscriptionResult.Failure(this.GetTranscriptionFailureUserMessage(response.StatusCode, responseBody));
|
||||
var failureMessage = this.GetTranscriptionFailureUserMessage(response.StatusCode, responseBody);
|
||||
return IsMediaFileRejection(response.StatusCode, responseBody)
|
||||
? TranscriptionResult.Rejection(failureMessage)
|
||||
: TranscriptionResult.Failure(failureMessage);
|
||||
}
|
||||
|
||||
var transcriptionResponse = JsonSerializer.Deserialize<TranscriptionResponse>(responseBody, JSON_SERIALIZER_OPTIONS);
|
||||
@@ -1582,7 +1636,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
if (this.IsTimeoutException(e, token))
|
||||
await this.SendTimeoutError("transcribing audio");
|
||||
|
||||
this.logger.LogError("Failed to perform transcription request: '{Message}'.", e.Message);
|
||||
this.logger.LogError(e, "Failed to perform transcription request: '{Message}'.", e.Message);
|
||||
return TranscriptionResult.Failure();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
public sealed record TranscriptionResult(bool Success, string Text, string ErrorMessage = "")
|
||||
/// <summary>
|
||||
/// What a transcription provider answered.
|
||||
/// </summary>
|
||||
/// <param name="Success">Whether the provider returned a transcript.</param>
|
||||
/// <param name="Text">The transcript.</param>
|
||||
/// <param name="ErrorMessage">Why the transcription failed, for the user, when it is known.</param>
|
||||
/// <param name="FileRejected">Whether the provider refused the media file itself, so that it would refuse the same file again.</param>
|
||||
public sealed record TranscriptionResult(bool Success, string Text, string ErrorMessage = "", bool FileRejected = false)
|
||||
{
|
||||
public static TranscriptionResult FromText(string text) => new(true, text);
|
||||
|
||||
public static TranscriptionResult Failure(string errorMessage = "") => new(false, string.Empty, errorMessage);
|
||||
|
||||
public static TranscriptionResult Rejection(string errorMessage) => new(false, string.Empty, errorMessage, true);
|
||||
}
|
||||
@@ -53,6 +53,14 @@ public static class DataSourceSecurityTrustExtensions
|
||||
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
|
||||
}
|
||||
|
||||
public static ConfidenceLevel GetConfidenceLevel(this TranscriptionProvider provider, SettingsManager settingsManager)
|
||||
{
|
||||
if (provider == TranscriptionProvider.NONE)
|
||||
return ConfidenceLevel.NONE;
|
||||
|
||||
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
|
||||
}
|
||||
|
||||
public static ConfidenceLevel GetConfidenceLevel(this IProvider provider, SettingsManager settingsManager)
|
||||
{
|
||||
if (provider is NoProvider)
|
||||
|
||||
@@ -46,6 +46,7 @@ public static class DataBackupContents
|
||||
new(DataBackupLocation.DATA_DIRECTORY, "visualBriefings/", DataBackupEntryKind.VISUAL_BRIEFINGS),
|
||||
new(DataBackupLocation.DATA_DIRECTORY, "databases/sqlite/", DataBackupEntryKind.DATA_SOURCE_INDEX),
|
||||
new(DataBackupLocation.DATA_DIRECTORY, "databases/vector_database/stores/", DataBackupEntryKind.VECTOR_STORES),
|
||||
new(DataBackupLocation.DATA_DIRECTORY, "mediaTranscripts/", DataBackupEntryKind.MEDIA_TRANSCRIPTS),
|
||||
|
||||
new(DataBackupLocation.CONFIG_DIRECTORY, "settings.v*.json", DataBackupEntryKind.SETTINGS_OF_OTHER_VERSIONS),
|
||||
new(DataBackupLocation.DATA_DIRECTORY, "plugins/.internal/", DataBackupEntryKind.BUILT_IN_PLUGINS),
|
||||
|
||||
@@ -18,7 +18,20 @@ public sealed record DataBackupEntry(DataBackupLocation Location, string Relativ
|
||||
or DataBackupEntryKind.TOKENIZERS
|
||||
or DataBackupEntryKind.VISUAL_BRIEFINGS
|
||||
or DataBackupEntryKind.DATA_SOURCE_INDEX
|
||||
or DataBackupEntryKind.VECTOR_STORES;
|
||||
or DataBackupEntryKind.VECTOR_STORES
|
||||
or DataBackupEntryKind.MEDIA_TRANSCRIPTS;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a backup takes the entry from the copies it makes while the indexing pauses, rather than from the data folder.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The indexing writes to these entries, and they refer to each other: the index knows which
|
||||
/// transcript belongs to which file. A backup therefore copies them together while nothing
|
||||
/// writes, cf. DataBackupService.CopyIndexesAsync, and a restore replaces them only together.
|
||||
/// </remarks>
|
||||
public bool ComesFromIndexCopies => this.Kind is DataBackupEntryKind.DATA_SOURCE_INDEX
|
||||
or DataBackupEntryKind.VECTOR_STORES
|
||||
or DataBackupEntryKind.MEDIA_TRANSCRIPTS;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entry is a folder rather than a single file.
|
||||
|
||||
@@ -45,6 +45,11 @@ public enum DataBackupEntryKind
|
||||
/// </summary>
|
||||
VECTOR_STORES,
|
||||
|
||||
/// <summary>
|
||||
/// The transcripts of the audio and video files of the data sources. Making them again costs money.
|
||||
/// </summary>
|
||||
MEDIA_TRANSCRIPTS,
|
||||
|
||||
/// <summary>
|
||||
/// The copies of the settings which every version of AI Studio keeps for itself.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace AIStudio.Tools.DataBackup;
|
||||
/// <param name="CreatedAtUtc">When the backup was written.</param>
|
||||
/// <param name="SourceDataDirectory">The data folder the backup was made from, for whoever wonders where it came from.</param>
|
||||
/// <param name="SourceConfigDirectory">The configuration folder the backup was made from.</param>
|
||||
/// <param name="IncludesDataSourceIndexes">Whether the backup holds the indexes of the data sources. Without them, a restore keeps the indexes there are.</param>
|
||||
/// <param name="IncludesDataSourceIndexes">Whether the backup holds the indexes of the data sources and the transcripts of their media files. Without them, a restore keeps the indexes and transcripts there are.</param>
|
||||
/// <param name="Entries">Every file of the backup, the manifest aside.</param>
|
||||
public sealed record DataBackupManifest(
|
||||
int FormatVersion,
|
||||
|
||||
@@ -101,7 +101,7 @@ public static class DataBackupWriter
|
||||
var files = new List<BackupFile>();
|
||||
foreach (var entry in DataBackupContents.ENTRIES.Where(entry => entry.IsIncluded && entry.Location is DataBackupLocation.DATA_DIRECTORY))
|
||||
{
|
||||
var root = entry.Kind is DataBackupEntryKind.DATA_SOURCE_INDEX or DataBackupEntryKind.VECTOR_STORES
|
||||
var root = entry.ComesFromIndexCopies
|
||||
? sources.IndexCopiesDirectory
|
||||
: sources.DataDirectory;
|
||||
|
||||
|
||||
@@ -147,15 +147,16 @@ public static class DataRestorePreparer
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An element the backup holds nothing for stays as it is, since nothing tells whether it was empty
|
||||
/// or whether the version of AI Studio which made the backup knew it at all. The indexes are
|
||||
/// replaced only when the backup holds both of them, cf. DataBackupManifest.IncludesDataSourceIndexes.
|
||||
/// or whether the version of AI Studio which made the backup knew it at all. The indexes and the
|
||||
/// media transcripts are replaced only when the backup holds the indexes, cf.
|
||||
/// DataBackupManifest.IncludesDataSourceIndexes and DataBackupEntry.ComesFromIndexCopies.
|
||||
/// </remarks>
|
||||
private static List<DataRestoreElement> DetermineElements(DataBackupManifest manifest, DataRestoreTarget target)
|
||||
{
|
||||
var elements = new List<DataRestoreElement>();
|
||||
foreach (var entry in DataBackupContents.ENTRIES.Where(entry => entry.IsIncluded))
|
||||
{
|
||||
if (entry.Kind is DataBackupEntryKind.DATA_SOURCE_INDEX or DataBackupEntryKind.VECTOR_STORES && !manifest.IncludesDataSourceIndexes)
|
||||
if (entry.ComesFromIndexCopies && !manifest.IncludesDataSourceIndexes)
|
||||
continue;
|
||||
|
||||
var entryName = DataBackupFormat.EntryName(entry.Location, entry.RelativePath);
|
||||
|
||||
@@ -11,4 +11,5 @@ public sealed record EmbeddingStateFile(
|
||||
DateTimeOffset CreationUtc,
|
||||
DateTimeOffset LastWriteUtc,
|
||||
DateTimeOffset EmbeddedAtUtc,
|
||||
int ChunkCount);
|
||||
int ChunkCount,
|
||||
string? TranscriptKey = null);
|
||||
@@ -26,6 +26,8 @@ internal sealed class EmbeddingStateFileEntity
|
||||
|
||||
public int ChunkCount { get; set; }
|
||||
|
||||
public string? TranscriptKey { get; set; }
|
||||
|
||||
public EmbeddingStateDataSourceEntity? DataSource { get; set; }
|
||||
|
||||
public List<EmbeddingStateChunkEntity> Chunks { get; set; } = [];
|
||||
|
||||
@@ -80,6 +80,7 @@ internal sealed class IndexStoreDbContext(DbContextOptions<IndexStoreDbContext>
|
||||
entity.Property(file => file.LastWriteUtc).HasColumnName("last_write_utc").HasConversion(utcDateTimeOffsetConverter).IsRequired();
|
||||
entity.Property(file => file.EmbeddedAtUtc).HasColumnName("embedded_at_utc").HasConversion(utcDateTimeOffsetConverter).IsRequired();
|
||||
entity.Property(file => file.ChunkCount).HasColumnName("chunk_count");
|
||||
entity.Property(file => file.TranscriptKey).HasColumnName("transcript_key");
|
||||
|
||||
entity.HasIndex(file => file.DataSourceId).HasDatabaseName("idx_embedded_files_data_source");
|
||||
entity.HasIndex(file => file.AbsolutePath).HasDatabaseName("idx_embedded_files_absolute_path");
|
||||
|
||||
@@ -11,6 +11,7 @@ internal static class IndexStoreSchemaMigrator
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Migrations.DropFileConfidenceLevel))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Migrations.DropDataSourceName))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Migrations.MailboxIndex))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Migrations.MediaTranscriptKey))]
|
||||
public static async Task MigrateAsync(IndexStoreDbContext context, CancellationToken token)
|
||||
{
|
||||
await context.Database.MigrateAsync(token);
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#nullable disable
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace AIStudio.Tools.Databases.IndexStore.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the key of the transcript to the indexed files, for audio and video files which a data source transcribed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The transcripts lie below the data directory under the hash of the content of their media file.
|
||||
/// The index has to know which transcript belongs to which file, so that AI Studio can remove a
|
||||
/// transcript once its file is gone or changed. Every other file keeps NULL.
|
||||
/// </remarks>
|
||||
[DbContext(typeof(IndexStoreDbContext))]
|
||||
[Migration("20261018000000_MediaTranscriptKey")]
|
||||
public partial class MediaTranscriptKey : Migration
|
||||
{
|
||||
/// <remarks>
|
||||
/// Both directions go through raw SQL, like DropDataSourceName: the SQLite provider answers
|
||||
/// DropColumn by rebuilding the table, and dropping the old embedded_files table would let the
|
||||
/// cascade of the foreign key in embedding_chunks take every chunk with it. Adding a nullable
|
||||
/// column is native in SQLite and leaves the rows alone.
|
||||
/// </remarks>
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("ALTER TABLE embedded_files ADD COLUMN transcript_key TEXT NULL;");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("ALTER TABLE embedded_files DROP COLUMN transcript_key;");
|
||||
}
|
||||
}
|
||||
+4
@@ -119,6 +119,10 @@ partial class IndexStoreDbContextModelSnapshot : ModelSnapshot
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("relative_path");
|
||||
|
||||
entity.Property<string>("TranscriptKey")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("transcript_key");
|
||||
|
||||
entity.HasKey("ParentFileId");
|
||||
|
||||
entity.HasIndex("AbsolutePath")
|
||||
|
||||
+3
-1
@@ -148,7 +148,8 @@ public sealed partial class SqliteIndexStoreClientImplementation(string name, st
|
||||
file.FileSize,
|
||||
file.LastWriteUtc,
|
||||
file.EmbeddedAtUtc,
|
||||
file.ChunkCount);
|
||||
file.ChunkCount,
|
||||
file.TranscriptKey);
|
||||
}
|
||||
|
||||
var permanentFailures = await context.PermanentIndexingFailures
|
||||
@@ -690,6 +691,7 @@ public sealed partial class SqliteIndexStoreClientImplementation(string name, st
|
||||
fileEntity.LastWriteUtc = file.LastWriteUtc;
|
||||
fileEntity.EmbeddedAtUtc = file.EmbeddedAtUtc;
|
||||
fileEntity.ChunkCount = file.ChunkCount;
|
||||
fileEntity.TranscriptKey = file.TranscriptKey;
|
||||
}
|
||||
|
||||
private static void ApplyPermanentFailure(IndexingFailureEntity failureEntity, string dataSourceId, PermanentIndexingFailure failure)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the hash of the content of a file.
|
||||
/// </summary>
|
||||
internal static class FileContentHash
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a lowercase SHA-256 hash for a file without loading it fully into memory.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The lowercase hexadecimal hash.</returns>
|
||||
internal static async Task<string> ComputeAsync(string path, CancellationToken token)
|
||||
{
|
||||
await using var stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read,
|
||||
65_536,
|
||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
|
||||
return Convert.ToHexStringLower(await SHA256.HashDataAsync(stream, token));
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,12 @@ public enum FileExtractionErrorCode
|
||||
/// </summary>
|
||||
NO_CONTENT,
|
||||
|
||||
/// <summary>
|
||||
/// A selected audio or video file could not be prepared for transcription for a reason which
|
||||
/// lies in the file itself, such as a damaged container or an unsupported audio codec.
|
||||
/// </summary>
|
||||
MEDIA_PROCESSING_FAILED,
|
||||
|
||||
/// <summary>
|
||||
/// The caller no longer needs the content, e.g. because the user closed the dialog which
|
||||
/// asked for it. This is not a failure: nobody has to be told about it, which is why there
|
||||
|
||||
@@ -37,6 +37,7 @@ internal static class FileExtractionErrorCodeExtensions
|
||||
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => true,
|
||||
FileExtractionErrorCode.EXECUTABLE_REJECTED => true,
|
||||
FileExtractionErrorCode.UNSUPPORTED => true,
|
||||
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => true,
|
||||
|
||||
// Pages holding nothing but images are one of the recurring cases here, and that is a
|
||||
// property of the document, not of the environment:
|
||||
@@ -71,6 +72,7 @@ internal static class FileExtractionErrorCodeExtensions
|
||||
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("Unknown file type"),
|
||||
FileExtractionErrorCode.EXECUTABLE_REJECTED => TB("Executable program"),
|
||||
FileExtractionErrorCode.UNSUPPORTED => TB("Unsupported file type"),
|
||||
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => TB("Media file could not be processed"),
|
||||
FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => TB("Pages without readable text"),
|
||||
|
||||
FileExtractionErrorCode.FILE_NOT_FOUND => TB("File does not exist anymore"),
|
||||
@@ -122,6 +124,7 @@ internal static class FileExtractionErrorCodeExtensions
|
||||
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("The file type of '{0}' could not be determined, so the file was not indexed. AI Studio reads it again as soon as the file changes."),
|
||||
FileExtractionErrorCode.EXECUTABLE_REJECTED => TB("The file '{0}' is an executable program and was not indexed, regardless of its file extension."),
|
||||
FileExtractionErrorCode.UNSUPPORTED => TB("The file type of '{0}' is not supported, so the file was not indexed. AI Studio reads it again as soon as the file changes."),
|
||||
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => TB("The media file '{0}' could not be processed, so it was not indexed. AI Studio reads it again as soon as the file changes."),
|
||||
FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => TB("Pages of the file '{0}' could not be read, so it was not indexed. They might contain images only. AI Studio reads it again as soon as the file changes."),
|
||||
|
||||
//
|
||||
|
||||
@@ -80,6 +80,33 @@ internal static class FileNamePatterns
|
||||
&& FileSystemName.MatchesSimpleExpression(pattern, fileName, ignoreCase: true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the patterns may select audio or video files, which a data source sends to the transcription provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately cautious, since it answers without looking at a single file: a pattern which
|
||||
/// may match any extension, such as * or meeting*, counts. The selectors never select media.
|
||||
/// </remarks>
|
||||
/// <param name="patterns">The parsed patterns.</param>
|
||||
/// <returns>True when audio or video files may match.</returns>
|
||||
public static bool MaySelectMedia(IReadOnlyList<string> patterns) => patterns.Any(MaySelectMedia);
|
||||
|
||||
private static bool MaySelectMedia(string pattern)
|
||||
{
|
||||
if (pattern.StartsWith(SELECTOR_DELIMITER) && pattern.EndsWith(SELECTOR_DELIMITER))
|
||||
return false;
|
||||
|
||||
var extensionStart = pattern.LastIndexOf('.');
|
||||
if (extensionStart < 0)
|
||||
return pattern.IndexOfAny(['*', '?']) >= 0;
|
||||
|
||||
var extension = pattern[(extensionStart + 1)..];
|
||||
if (extension.IndexOfAny(['*', '?']) >= 0)
|
||||
return true;
|
||||
|
||||
return FileTypes.IsAllowedPath($"media.{extension}", FileTypes.AUDIO, FileTypes.VIDEO);
|
||||
}
|
||||
|
||||
public static bool AreEquivalent(IReadOnlyCollection<string>? left, IReadOnlyCollection<string>? right)
|
||||
{
|
||||
if (left is null || right is null)
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the transcripts of the audio and video files of one data source below the data directory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A data source never writes into the folders of the user: they may be read-only, a network share,
|
||||
/// or the read-only home folder of the Flatpak, and a transcript which cannot be stored would be paid
|
||||
/// for again with every run. The transcripts live in 'mediaTranscripts/<data source id>/<key>.md'
|
||||
/// instead, where the key is the SHA-256 hash of the content of the media file. Renaming or moving a
|
||||
/// recording, or a sync tool which only touches its write time, finds the transcript again. The path
|
||||
/// follows from the id and the key, so nothing stores it.
|
||||
/// </remarks>
|
||||
/// <param name="dataDirectory">The data directory of AI Studio.</param>
|
||||
/// <param name="dataSourceId">The id of the data source, a GUID.</param>
|
||||
internal sealed partial class DataSourceTranscriptStore(string dataDirectory, string dataSourceId) : IMediaTranscriptLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// The folder below the data directory which holds the transcripts of all data sources.
|
||||
/// </summary>
|
||||
public const string FOLDER_NAME = "mediaTranscripts";
|
||||
|
||||
private const string FILE_EXTENSION = ".md";
|
||||
|
||||
private readonly string directoryPath = Path.Join(dataDirectory, FOLDER_NAME, NormalizeId(dataSourceId));
|
||||
|
||||
/// <summary>
|
||||
/// The store of a data source in the data directory of the running app.
|
||||
/// </summary>
|
||||
/// <param name="dataSourceId">The id of the data source.</param>
|
||||
/// <returns>The store.</returns>
|
||||
public static DataSourceTranscriptStore ForDataSource(string dataSourceId) => new(SettingsManager.DataDirectory ?? throw new InvalidOperationException("The data directory is not known yet."), dataSourceId);
|
||||
|
||||
/// <summary>
|
||||
/// The store of a data source in the data directory of the running app, or null when there is none to clean up.
|
||||
/// </summary>
|
||||
/// <param name="dataSourceId">The id of the data source.</param>
|
||||
/// <returns>The store, or null while the data directory is not known or for an id which is no GUID.</returns>
|
||||
public static DataSourceTranscriptStore? TryForDataSource(string dataSourceId) => string.IsNullOrWhiteSpace(SettingsManager.DataDirectory) || !Guid.TryParse(dataSourceId, out _)
|
||||
? null
|
||||
: new(SettingsManager.DataDirectory, dataSourceId);
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Reads the whole media file to hash it, in pieces. Callers ask only for files which are about
|
||||
/// to be read anyway because they are new or changed.
|
||||
/// </remarks>
|
||||
public async Task<MediaTranscriptSlot> ResolveAsync(string mediaPath, CancellationToken token)
|
||||
{
|
||||
var key = await FileContentHash.ComputeAsync(mediaPath, token);
|
||||
return new(this.GetPath(key), key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a text is a key of this store, i.e. a lowercase SHA-256 hash.
|
||||
/// </summary>
|
||||
/// <param name="key">The text to check.</param>
|
||||
/// <returns>True for a key.</returns>
|
||||
public static bool IsKey(string key) => KeyPattern().IsMatch(key);
|
||||
|
||||
/// <summary>
|
||||
/// The path of the transcript with the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the transcript.</param>
|
||||
/// <returns>The path, whether or not the transcript exists.</returns>
|
||||
public string GetPath(string key)
|
||||
{
|
||||
if (!IsKey(key))
|
||||
throw new ArgumentException("The key of a media transcript has to be a lowercase SHA-256 hash.", nameof(key));
|
||||
|
||||
return Path.Join(this.directoryPath, key + FILE_EXTENSION);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The keys of all transcripts of this data source.
|
||||
/// </summary>
|
||||
/// <returns>The keys; empty when the data source has no transcripts.</returns>
|
||||
public IReadOnlyList<string> ListKeys()
|
||||
{
|
||||
return this.EnumerateOwnFiles()
|
||||
.Select(file => Path.GetFileName(file.FullName))
|
||||
.Where(fileName => TranscriptFileNamePattern().IsMatch(fileName))
|
||||
.Select(Path.GetFileNameWithoutExtension)
|
||||
.OfType<string>()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the transcript with the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the transcript.</param>
|
||||
/// <returns>True when a transcript was deleted.</returns>
|
||||
public bool Delete(string key)
|
||||
{
|
||||
var file = new FileInfo(this.GetPath(key));
|
||||
if (!file.Exists || file.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||
return false;
|
||||
|
||||
file.Delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the temporary files which an interrupted write left behind.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call it only while nothing writes a transcript of this data source, e.g. at the end of a run.
|
||||
/// </remarks>
|
||||
/// <returns>The number of deleted files.</returns>
|
||||
public int DeleteTemporaryFiles()
|
||||
{
|
||||
var deleted = 0;
|
||||
foreach (var file in this.EnumerateOwnFiles().Where(file => TemporaryFileNamePattern().IsMatch(file.Name)))
|
||||
{
|
||||
file.Delete();
|
||||
deleted++;
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes every transcript of this data source, together with temporary files a crash left behind.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Removes only what this store wrote. Anything else in the folder stays, and the folder with it.
|
||||
/// </remarks>
|
||||
public void DeleteAll()
|
||||
{
|
||||
foreach (var file in this.EnumerateOwnFiles())
|
||||
file.Delete();
|
||||
|
||||
var directory = new DirectoryInfo(this.directoryPath);
|
||||
if (directory.Exists && !directory.Attributes.HasFlag(FileAttributes.ReparsePoint) && !directory.EnumerateFileSystemInfos().Any())
|
||||
directory.Delete();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the transcripts of all data sources, e.g. for a backup.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Copies only transcripts, file by file and in pieces. Temporary files of an interrupted write,
|
||||
/// links, and anything else in the folders stay behind.
|
||||
/// </remarks>
|
||||
/// <param name="dataDirectory">The data directory to copy from.</param>
|
||||
/// <param name="targetDirectory">The folder which receives the transcripts below 'mediaTranscripts/', in the same layout.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The number of copied transcripts.</returns>
|
||||
public static async Task<int> CopyAllAsync(string dataDirectory, string targetDirectory, CancellationToken token)
|
||||
{
|
||||
var root = new DirectoryInfo(Path.Join(dataDirectory, FOLDER_NAME));
|
||||
if (!root.Exists || root.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||
return 0;
|
||||
|
||||
var copied = 0;
|
||||
foreach (var dataSourceFolder in root.EnumerateDirectories("*", new EnumerationOptions { RecurseSubdirectories = false, AttributesToSkip = FileAttributes.ReparsePoint, IgnoreInaccessible = false }))
|
||||
{
|
||||
if (!Guid.TryParse(dataSourceFolder.Name, out _))
|
||||
continue;
|
||||
|
||||
var store = new DataSourceTranscriptStore(dataDirectory, dataSourceFolder.Name);
|
||||
foreach (var file in store.EnumerateOwnFiles().Where(file => TranscriptFileNamePattern().IsMatch(file.Name)))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var targetPath = Path.Join(targetDirectory, FOLDER_NAME, dataSourceFolder.Name, file.Name);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
|
||||
|
||||
await using var source = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 65_536, FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
await using var target = new FileStream(targetPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 65_536, FileOptions.Asynchronous);
|
||||
await source.CopyToAsync(target, token);
|
||||
copied++;
|
||||
}
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The files this store wrote: transcripts and the temporary files of interrupted writes. Links are left out.
|
||||
/// </summary>
|
||||
private IEnumerable<FileInfo> EnumerateOwnFiles()
|
||||
{
|
||||
var directory = new DirectoryInfo(this.directoryPath);
|
||||
if (!directory.Exists || directory.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||
return [];
|
||||
|
||||
return directory
|
||||
.EnumerateFiles("*", new EnumerationOptions { RecurseSubdirectories = false, AttributesToSkip = FileAttributes.ReparsePoint, IgnoreInaccessible = true })
|
||||
.Where(file => TranscriptFileNamePattern().IsMatch(file.Name) || TemporaryFileNamePattern().IsMatch(file.Name))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string NormalizeId(string dataSourceId) => Guid.TryParse(dataSourceId, out var id)
|
||||
? id.ToString("D")
|
||||
: throw new ArgumentException("The id of a data source with media transcripts has to be a GUID.", nameof(dataSourceId));
|
||||
|
||||
[GeneratedRegex("^[0-9a-f]{64}$")]
|
||||
private static partial Regex KeyPattern();
|
||||
|
||||
[GeneratedRegex(@"^[0-9a-f]{64}\.md$")]
|
||||
private static partial Regex TranscriptFileNamePattern();
|
||||
|
||||
[GeneratedRegex(@"^[0-9a-f]{64}\.md\.[0-9a-f]{32}\.tmp$")]
|
||||
private static partial Regex TemporaryFileNamePattern();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Where the persistent transcript of an audio or video file lives, cf. PersistentMediaTranscript.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A location only names the place. Reading and writing the transcript is left to
|
||||
/// PersistentMediaTranscript, so every location keeps the same rules against paying twice.
|
||||
/// </remarks>
|
||||
internal interface IMediaTranscriptLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds the place of the transcript of a media file.
|
||||
/// </summary>
|
||||
/// <param name="mediaPath">The audio or video file.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The place of the transcript, whether or not a transcript is there yet.</returns>
|
||||
Task<MediaTranscriptSlot> ResolveAsync(string mediaPath, CancellationToken token);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the transcript of a media file next to it, as '<media file>.transcript.md'.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Batch Processing assistant writes its results into the folder the user chose, and its
|
||||
/// transcripts belong there as well, where people find and reuse them. Data sources must not write
|
||||
/// into the folders of the user and keep their transcripts below the data directory instead.
|
||||
/// </remarks>
|
||||
internal sealed class MediaSidecarTranscripts : IMediaTranscriptLocation
|
||||
{
|
||||
public const string FILE_SUFFIX = ".transcript.md";
|
||||
|
||||
public static readonly MediaSidecarTranscripts INSTANCE = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<MediaTranscriptSlot> ResolveAsync(string mediaPath, CancellationToken token)
|
||||
{
|
||||
var path = GetFilePath(mediaPath);
|
||||
return Task.FromResult(new MediaTranscriptSlot(path, path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The path of the transcript next to a media file.
|
||||
/// </summary>
|
||||
/// <param name="mediaPath">The audio or video file.</param>
|
||||
/// <returns>The path of its transcript.</returns>
|
||||
public static string GetFilePath(string mediaPath) => mediaPath + FILE_SUFFIX;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a file is a transcript next to a media file, or a temporary file left while one was stored.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the file.</param>
|
||||
/// <returns>True for transcripts and their temporary files.</returns>
|
||||
public static bool IsArtifactPath(string path)
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
if (fileName.EndsWith(FILE_SUFFIX, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
var suffixIndex = fileName.LastIndexOf(FILE_SUFFIX + ".", StringComparison.OrdinalIgnoreCase);
|
||||
return suffixIndex >= 0 && fileName.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a file is the transcript of a media file which the selection of a folder data source selects as well.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Such a transcript holds the same text the data source gets from transcribing the media file,
|
||||
/// so indexing it as a document of its own would only duplicate that text. A transcript whose
|
||||
/// media file is gone, or not selected, stays a document like every other Markdown file.
|
||||
/// </remarks>
|
||||
/// <param name="path">The path of the file.</param>
|
||||
/// <param name="selection">The files the folder data source selects.</param>
|
||||
/// <returns>True when the data source indexes the media file of this transcript.</returns>
|
||||
public static bool IsCoveredArtifact(string path, FolderFileSelection selection)
|
||||
{
|
||||
if (!TryGetMediaPath(path, out var mediaPath))
|
||||
return false;
|
||||
|
||||
return PersistentMediaTranscript.IsMediaPath(mediaPath) && File.Exists(mediaPath) && selection.Selects(mediaPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a media file has a transcript next to it, so no transcription provider is needed for it.
|
||||
/// </summary>
|
||||
/// <param name="mediaPath">The audio or video file.</param>
|
||||
/// <returns>True when a transcript which is not empty exists.</returns>
|
||||
public static bool HasReusableTranscript(string mediaPath)
|
||||
{
|
||||
var transcriptFilePath = GetFilePath(mediaPath);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The media file a transcript or one of its temporary files belongs to.
|
||||
/// </summary>
|
||||
private static bool TryGetMediaPath(string path, out string mediaPath)
|
||||
{
|
||||
var suffixIndex = path.LastIndexOf(FILE_SUFFIX, StringComparison.OrdinalIgnoreCase);
|
||||
if (suffixIndex <= 0)
|
||||
{
|
||||
mediaPath = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
var remainder = path[(suffixIndex + FILE_SUFFIX.Length)..];
|
||||
if (remainder.Length > 0 && !(remainder[0] is '.' && remainder.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
mediaPath = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
mediaPath = path[..suffixIndex];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
/// <summary>
|
||||
/// The place of the persistent transcript of one audio or video file.
|
||||
/// </summary>
|
||||
/// <param name="Path">The absolute path of the Markdown transcript.</param>
|
||||
/// <param name="Key">What identifies the transcript at its location, e.g. the hash of the media content.</param>
|
||||
internal sealed record MediaTranscriptSlot(string Path, string Key);
|
||||
@@ -9,7 +9,8 @@ namespace AIStudio.Tools.Media;
|
||||
/// <param name="Text">Transcript text for a successful operation.</param>
|
||||
/// <param name="UserMessage">Localized message suitable for display after a warning or failure.</param>
|
||||
/// <param name="ErrorCode">Optional stable runtime failure category.</param>
|
||||
public sealed record MediaTranscriptionResult(MediaTranscriptionResultStatus Status, string Text, string UserMessage, MediaJobErrorCode? ErrorCode = null)
|
||||
/// <param name="FileRejected">Whether the transcription provider refused the media file itself, so that it would refuse the unchanged file again.</param>
|
||||
public sealed record MediaTranscriptionResult(MediaTranscriptionResultStatus Status, string Text, string UserMessage, MediaJobErrorCode? ErrorCode = null, bool FileRejected = false)
|
||||
{
|
||||
/// <summary>Creates a successful result.</summary>
|
||||
/// <param name="text">Provider transcript.</param>
|
||||
@@ -20,6 +21,10 @@ public sealed record MediaTranscriptionResult(MediaTranscriptionResultStatus Sta
|
||||
/// <param name="errorCode">Optional runtime error category.</param>
|
||||
public static MediaTranscriptionResult Failed(string userMessage, MediaJobErrorCode? errorCode = null) => new(MediaTranscriptionResultStatus.FAILED, string.Empty, userMessage, errorCode);
|
||||
|
||||
/// <summary>Creates a failed result for a media file which the transcription provider refused.</summary>
|
||||
/// <param name="userMessage">Localized visible message.</param>
|
||||
public static MediaTranscriptionResult Rejected(string userMessage) => new(MediaTranscriptionResultStatus.FAILED, string.Empty, userMessage, FileRejected: true);
|
||||
|
||||
/// <summary>Creates a warning result for media without an audible signal.</summary>
|
||||
/// <param name="userMessage">Localized visible warning.</param>
|
||||
public static MediaTranscriptionResult NoAudibleSignal(string userMessage) => new(
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Reads or creates the persistent Markdown transcript of an audio or video file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every transcript which AI Studio keeps comes from here, so the rules which keep a paid
|
||||
/// transcription from being paid twice hold for every caller: an existing transcript is used as it
|
||||
/// is, a transcript which arrived is stored even when the caller cancelled meanwhile, and nothing is
|
||||
/// stored for a file which changed during its transcription or for a transcription without text.
|
||||
/// Where a transcript lives is up to the location, e.g. next to the media file or below the data
|
||||
/// directory.
|
||||
/// </remarks>
|
||||
internal static class PersistentMediaTranscript
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PersistentMediaTranscript).Namespace, nameof(PersistentMediaTranscript));
|
||||
|
||||
/// <summary>
|
||||
/// Whether a file is an audio or video file, which holds no text until it is transcribed.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the file.</param>
|
||||
/// <returns>True for audio and video files.</returns>
|
||||
public static bool IsMediaPath(string path) => FileTypes.IsAllowedPath(path, FileTypes.AUDIO, FileTypes.VIDEO);
|
||||
|
||||
/// <summary>
|
||||
/// Returns where the transcript of a media file is, transcribing the file only when its location holds no transcript yet.
|
||||
/// </summary>
|
||||
/// <param name="location">Where the transcript lives.</param>
|
||||
/// <param name="mediaPath">The audio or video file.</param>
|
||||
/// <param name="provider">The transcription provider the caller allows, or null when no provider may hear the file.</param>
|
||||
/// <param name="transcribe">Transcribes the media file with the given provider, usually MediaTranscriptionService.TranscribeAsync.</param>
|
||||
/// <param name="logger">The logger of the caller.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>Where the transcript is, or why there is none.</returns>
|
||||
public static async Task<PersistentMediaTranscriptResult> ReadOrCreateAsync(IMediaTranscriptLocation location, string mediaPath, TranscriptionProvider? provider, Func<string, TranscriptionProvider, CancellationToken, Task<MediaTranscriptionResult>> transcribe, ILogger logger, CancellationToken token)
|
||||
{
|
||||
//
|
||||
// The stamp comes first: a location may read the whole file to find the place of its
|
||||
// transcript, and a change from then on must keep a transcript out of that place.
|
||||
//
|
||||
FileStamp sourceBefore;
|
||||
MediaTranscriptSlot slot;
|
||||
try
|
||||
{
|
||||
sourceBefore = FileStamp.Read(mediaPath);
|
||||
slot = await location.ResolveAsync(mediaPath, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return PersistentMediaTranscriptResult.Cancelled();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not inspect the media file '{MediaFilePath}' before its transcription.", mediaPath);
|
||||
return PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.SOURCE_UNREADABLE, null, TB("The media file could not be read."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (await HoldsTextAsync(slot.Path, token))
|
||||
{
|
||||
logger.LogInformation("Reusing the transcript '{TranscriptFilePath}' of the media file '{MediaFilePath}'.", slot.Path, mediaPath);
|
||||
return PersistentMediaTranscriptResult.Ready(slot, reused: true);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return PersistentMediaTranscriptResult.Cancelled();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
//
|
||||
// Transcribing again would mean paying for a transcript which exists, and storing it
|
||||
// would most likely fail for the same reason:
|
||||
//
|
||||
logger.LogWarning(exception, "Could not read the transcript '{TranscriptFilePath}' of the media file '{MediaFilePath}'.", slot.Path, mediaPath);
|
||||
return PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.TRANSCRIPT_UNREADABLE, slot, string.Format(TB("Was not able to read the existing transcript: {0}"), exception.Message));
|
||||
}
|
||||
|
||||
if (provider is null)
|
||||
return PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.NO_PROVIDER, slot, TB("No usable transcription provider is configured."));
|
||||
|
||||
MediaTranscriptionResult transcription;
|
||||
try
|
||||
{
|
||||
transcription = await transcribe(mediaPath, provider, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return PersistentMediaTranscriptResult.Cancelled();
|
||||
}
|
||||
|
||||
switch (transcription.Status)
|
||||
{
|
||||
case MediaTranscriptionResultStatus.CANCELLED:
|
||||
return PersistentMediaTranscriptResult.Cancelled();
|
||||
|
||||
case MediaTranscriptionResultStatus.NO_AUDIBLE_SIGNAL:
|
||||
return PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.NO_AUDIBLE_SIGNAL, slot, transcription.UserMessage);
|
||||
|
||||
case MediaTranscriptionResultStatus.FAILED when transcription.FileRejected:
|
||||
return PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.REJECTED, slot, transcription.UserMessage);
|
||||
|
||||
case MediaTranscriptionResultStatus.FAILED:
|
||||
return PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.TRANSCRIPTION_FAILED, slot, transcription.UserMessage, transcription.ErrorCode);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(transcription.Text))
|
||||
return PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.EMPTY, slot, TB("The transcription provider returned an empty transcript."));
|
||||
|
||||
if (!sourceBefore.StillMatches(mediaPath))
|
||||
return SourceChanged(slot);
|
||||
|
||||
var temporaryPath = $"{slot.Path}.{Guid.NewGuid():N}.tmp";
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(slot.Path);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
//
|
||||
// The transcription is paid for at this point. Storing it is quick, so it is completed
|
||||
// even when the caller cancelled meanwhile, and the next attempt can use it:
|
||||
//
|
||||
await File.WriteAllTextAsync(temporaryPath, transcription.Text, new UTF8Encoding(false), CancellationToken.None);
|
||||
if (!sourceBefore.StillMatches(mediaPath))
|
||||
return SourceChanged(slot);
|
||||
|
||||
File.Move(temporaryPath, slot.Path, true);
|
||||
logger.LogInformation("Stored the transcript '{TranscriptFilePath}' of the media file '{MediaFilePath}'.", slot.Path, mediaPath);
|
||||
return PersistentMediaTranscriptResult.Ready(slot, reused: false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not store the transcript '{TranscriptFilePath}' of the media file '{MediaFilePath}'.", slot.Path, mediaPath);
|
||||
return PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.STORE_FAILED, slot, string.Format(TB("Was not able to store the transcript: {0}"), exception.Message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(temporaryPath))
|
||||
File.Delete(temporaryPath);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not remove the temporary transcript '{TempFilePath}'.", temporaryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PersistentMediaTranscriptResult SourceChanged(MediaTranscriptSlot slot) => PersistentMediaTranscriptResult.Unavailable(PersistentMediaTranscriptStatus.SOURCE_CHANGED, slot, TB("The media file changed while it was being transcribed, so its transcript was not kept."));
|
||||
|
||||
/// <summary>
|
||||
/// Whether a transcript exists and holds more than white space.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reads only as far as the first character which is not white space, so a large file is
|
||||
/// never loaded as a whole.
|
||||
/// </remarks>
|
||||
private static async Task<bool> HoldsTextAsync(string path, CancellationToken token)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
return false;
|
||||
|
||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
var buffer = new char[1024];
|
||||
int read;
|
||||
while ((read = await reader.ReadAsync(buffer.AsMemory(), token)) > 0)
|
||||
{
|
||||
for (var index = 0; index < read; index++)
|
||||
if (!char.IsWhiteSpace(buffer[index]))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The size and write time of a media file, to notice a change during its transcription.
|
||||
/// </summary>
|
||||
private readonly record struct FileStamp(long Length, DateTime LastWriteTimeUtc)
|
||||
{
|
||||
public static FileStamp Read(string path)
|
||||
{
|
||||
var file = new FileInfo(path);
|
||||
file.Refresh();
|
||||
if (!file.Exists)
|
||||
throw new FileNotFoundException("The media file does not exist.", path);
|
||||
|
||||
return new(file.Length, file.LastWriteTimeUtc);
|
||||
}
|
||||
|
||||
public bool StillMatches(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Read(path) == this;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
/// <summary>
|
||||
/// The outcome of a request for a persistent transcript, cf. PersistentMediaTranscript.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The transcript itself is not part of it: whoever needs the text reads the file at
|
||||
/// TranscriptPath, so a transcript reaches the AI by the same path no matter whether it was just
|
||||
/// made or stored long ago.
|
||||
/// </remarks>
|
||||
/// <param name="Status">How the request ended.</param>
|
||||
/// <param name="TranscriptPath">The path of the transcript; empty when the place could not be determined.</param>
|
||||
/// <param name="Key">What identifies the transcript at its location; empty when the place could not be determined.</param>
|
||||
/// <param name="Reused">Whether an existing transcript was used instead of a new transcription.</param>
|
||||
/// <param name="UserMessage">A localized explanation for every status but READY and CANCELLED.</param>
|
||||
/// <param name="ErrorCode">Why the media pipeline failed, for TRANSCRIPTION_FAILED, when it is known.</param>
|
||||
internal sealed record PersistentMediaTranscriptResult(PersistentMediaTranscriptStatus Status, string TranscriptPath, string Key, bool Reused, string UserMessage, MediaJobErrorCode? ErrorCode = null)
|
||||
{
|
||||
public static PersistentMediaTranscriptResult Ready(MediaTranscriptSlot slot, bool reused) => new(PersistentMediaTranscriptStatus.READY, slot.Path, slot.Key, reused, string.Empty);
|
||||
|
||||
public static PersistentMediaTranscriptResult Cancelled() => new(PersistentMediaTranscriptStatus.CANCELLED, string.Empty, string.Empty, false, string.Empty);
|
||||
|
||||
public static PersistentMediaTranscriptResult Unavailable(PersistentMediaTranscriptStatus status, MediaTranscriptSlot? slot, string userMessage, MediaJobErrorCode? errorCode = null) => new(status, slot?.Path ?? string.Empty, slot?.Key ?? string.Empty, false, userMessage, errorCode);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace AIStudio.Tools.Media;
|
||||
|
||||
/// <summary>
|
||||
/// How the request for a persistent transcript ended, cf. PersistentMediaTranscript.
|
||||
/// </summary>
|
||||
public enum PersistentMediaTranscriptStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The transcript is stored at its place and holds text.
|
||||
/// </summary>
|
||||
READY,
|
||||
|
||||
/// <summary>
|
||||
/// There is no transcript yet, and the caller allows no transcription provider.
|
||||
/// </summary>
|
||||
NO_PROVIDER,
|
||||
|
||||
/// <summary>
|
||||
/// The media file could not be read before it was transcribed.
|
||||
/// </summary>
|
||||
SOURCE_UNREADABLE,
|
||||
|
||||
/// <summary>
|
||||
/// A transcript exists, but it could not be read.
|
||||
/// </summary>
|
||||
TRANSCRIPT_UNREADABLE,
|
||||
|
||||
/// <summary>
|
||||
/// The transcription failed. The error code of the media pipeline tells why, when it is known.
|
||||
/// </summary>
|
||||
TRANSCRIPTION_FAILED,
|
||||
|
||||
/// <summary>
|
||||
/// The transcription provider refused the media file itself, e.g., because it is too large
|
||||
/// or damaged. It would refuse the unchanged file again.
|
||||
/// </summary>
|
||||
REJECTED,
|
||||
|
||||
/// <summary>
|
||||
/// The media file holds no audible signal, so there was nothing to transcribe.
|
||||
/// </summary>
|
||||
NO_AUDIBLE_SIGNAL,
|
||||
|
||||
/// <summary>
|
||||
/// The transcription provider returned no text. Nothing was stored.
|
||||
/// </summary>
|
||||
EMPTY,
|
||||
|
||||
/// <summary>
|
||||
/// The media file changed while it was transcribed. Nothing was stored.
|
||||
/// </summary>
|
||||
SOURCE_CHANGED,
|
||||
|
||||
/// <summary>
|
||||
/// The transcript could not be stored at its place.
|
||||
/// </summary>
|
||||
STORE_FAILED,
|
||||
|
||||
/// <summary>
|
||||
/// The caller cancelled before a transcript was available.
|
||||
/// </summary>
|
||||
CANCELLED,
|
||||
}
|
||||
@@ -170,7 +170,7 @@ public sealed class DataBackupService(ILogger<DataBackupService> logger, Setting
|
||||
var settingsJson = JsonSerializer.Serialize(settings, SettingsManager.JSON_OPTIONS);
|
||||
var organizationPluginFolders = DataBackupContents.FindOrganizationPluginsAmongUserPlugins(PluginFactory.AvailablePlugins.OfType<IAvailablePlugin>(), dataDirectory);
|
||||
|
||||
indexCopiesDirectory = await this.CopyIndexesAsync(token);
|
||||
indexCopiesDirectory = await this.CopyIndexesAsync(dataDirectory, token);
|
||||
|
||||
var sources = new DataBackupSources(dataDirectory, configDirectory, settingsJson, indexCopiesDirectory, organizationPluginFolders);
|
||||
var manifest = new DataBackupManifest(
|
||||
@@ -207,7 +207,7 @@ public sealed class DataBackupService(ILogger<DataBackupService> logger, Setting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies both indexes of the data sources, while the indexing pauses.
|
||||
/// Copies both indexes of the data sources and the transcripts of their media files, while the indexing pauses.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Both copies are made during one pause: the vector stores have to fit to the index, or a
|
||||
@@ -217,8 +217,10 @@ public sealed class DataBackupService(ILogger<DataBackupService> logger, Setting
|
||||
///
|
||||
/// Without either database, the backup leaves both indexes out, and its manifest says so.
|
||||
/// </remarks>
|
||||
/// <param name="dataDirectory">The data folder, which holds the transcripts.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The directory of the copies, in the layout of the data folder, or an empty string when the backup leaves the indexes out.</returns>
|
||||
private async Task<string> CopyIndexesAsync(CancellationToken token)
|
||||
private async Task<string> CopyIndexesAsync(string dataDirectory, CancellationToken token)
|
||||
{
|
||||
var indexStore = await databaseClientProvider.GetIndexStoreAsync(token);
|
||||
var vectorStore = await databaseClientProvider.GetVectorStoreAsync(token);
|
||||
@@ -241,6 +243,12 @@ public sealed class DataBackupService(ILogger<DataBackupService> logger, Setting
|
||||
await vectorStore.CopyStoreForBackupAsync(preparation.BackupId, preparation.Stores[index].StoreName, token);
|
||||
}
|
||||
|
||||
//
|
||||
// The index knows which transcript belongs to which media file, so the transcripts are
|
||||
// copied while the indexing pauses as well, and the backup holds both as they fit together:
|
||||
//
|
||||
await DataSourceTranscriptStore.CopyAllAsync(dataDirectory, preparation.StagingPath, token);
|
||||
|
||||
return preparation.StagingPath;
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -5,6 +5,24 @@ namespace AIStudio.Tools.Services;
|
||||
/// </summary>
|
||||
public enum DataSourceEmbeddingIgnoreReason
|
||||
{
|
||||
MEDIA_NOT_SUPPORTED,
|
||||
/// <summary>
|
||||
/// An image, which cannot be added to the index yet.
|
||||
/// </summary>
|
||||
IMAGE_NOT_SUPPORTED,
|
||||
|
||||
/// <summary>
|
||||
/// An audio or video file without a transcript, while no usable transcription provider is configured.
|
||||
/// </summary>
|
||||
TRANSCRIPTION_PROVIDER_MISSING,
|
||||
|
||||
/// <summary>
|
||||
/// An audio or video file without a transcript, while the transcription provider falls short of
|
||||
/// the confidence level the data source asks for.
|
||||
/// </summary>
|
||||
TRANSCRIPTION_PROVIDER_NOT_ALLOWED,
|
||||
|
||||
/// <summary>
|
||||
/// A file larger than the limit of the data source.
|
||||
/// </summary>
|
||||
TOO_LARGE,
|
||||
}
|
||||
@@ -6,7 +6,8 @@ namespace AIStudio.Tools.Services;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Files below folders the user chose not to scan never reach enumeration and therefore do not
|
||||
/// appear here. Temporary and system files are implementation details and stay hidden as well.
|
||||
/// appear here. Generated transcript artifacts and temporary/system files are implementation
|
||||
/// details and stay hidden as well.
|
||||
/// </remarks>
|
||||
/// <param name="FilePath">The absolute path of the ignored file.</param>
|
||||
/// <param name="Reason">Why the file was ignored.</param>
|
||||
|
||||
@@ -7,6 +7,7 @@ using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Databases;
|
||||
using AIStudio.Tools.Databases.IndexStore;
|
||||
using AIStudio.Tools.Databases.VectorStore;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Security;
|
||||
using AIStudio.Tools.Services.Indexing;
|
||||
@@ -16,7 +17,7 @@ using static AIStudio.Tools.Services.Indexing.IndexingLogFormat;
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class DataSourceEmbeddingService(SettingsManager settingsManager, RustService rustService, DatabaseClientProvider databaseClientProvider,
|
||||
PromptInjectionGuardService guardService, ILogger<DataSourceEmbeddingService> logger) : BackgroundService, IMessageBusReceiver
|
||||
PromptInjectionGuardService guardService, MediaTranscriptionService mediaTranscriptionService, ILogger<DataSourceEmbeddingService> logger) : BackgroundService, IMessageBusReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// How long the re-index check waits for the index database before it gives up.
|
||||
@@ -40,7 +41,7 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
/// <summary>
|
||||
/// One indexer per kind of data source this service indexes.
|
||||
/// </summary>
|
||||
private readonly IReadOnlyList<IIndexedSourceIndexer> indexers = CreateIndexers(settingsManager, rustService, guardService, logger);
|
||||
private readonly IReadOnlyList<IIndexedSourceIndexer> indexers = CreateIndexers(settingsManager, rustService, guardService, mediaTranscriptionService, logger);
|
||||
|
||||
private readonly DataSourceRunQueue runQueue = new();
|
||||
|
||||
@@ -93,12 +94,12 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
/// <summary>
|
||||
/// Creates one indexer per kind of data source, all of them cutting their text with the same chunker.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<IIndexedSourceIndexer> CreateIndexers(SettingsManager settingsManager, RustService rustService, PromptInjectionGuardService guardService, ILogger logger)
|
||||
private static IReadOnlyList<IIndexedSourceIndexer> CreateIndexers(SettingsManager settingsManager, RustService rustService, PromptInjectionGuardService guardService, MediaTranscriptionService mediaTranscriptionService, ILogger logger)
|
||||
{
|
||||
var textChunker = new TextChunker(rustService, logger);
|
||||
return
|
||||
[
|
||||
new FileSourceIndexer(settingsManager, rustService, textChunker, logger),
|
||||
new FileSourceIndexer(settingsManager, rustService, mediaTranscriptionService, textChunker, logger),
|
||||
new MailboxIndexer(settingsManager, rustService, guardService, textChunker, logger),
|
||||
];
|
||||
}
|
||||
@@ -518,10 +519,35 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
|
||||
this.statuses.TryRemove(dataSource.Id, out _);
|
||||
await this.ResetPersistedStateAsync(dataSource.Id, null, null, CancellationToken.None);
|
||||
this.DeleteMediaTranscripts(dataSource);
|
||||
this.statuses.TryRemove(dataSource.Id, out _);
|
||||
PublishStatusChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the transcripts of the audio and video files of a data source which the user removed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only the removal of a data source does this. A reset for new embedding settings keeps them,
|
||||
/// cf. ResetPersistedStateAsync: they are still valid, and the new index would otherwise pay
|
||||
/// for every recording once more.
|
||||
/// </remarks>
|
||||
/// <param name="dataSource">The removed data source.</param>
|
||||
private void DeleteMediaTranscripts(IDataSourceBase dataSource)
|
||||
{
|
||||
if (dataSource is not (DataSourceLocalDirectory or DataSourceLocalFile))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
DataSourceTranscriptStore.TryForDataSource(dataSource.Id)?.DeleteAll();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not remove the transcripts of the deleted data source '{DataSourceName}' ({DataSourceId}).", dataSource.Name, dataSource.Id);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Changes to the embedding settings which do not pass through the queue, cf. ProcessMessage:
|
||||
@@ -1086,6 +1112,18 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
|
||||
? embeddingProviderConfidence.AllowsMailboxConfidenceLevel(dataSource.ConfidenceLevel)
|
||||
: embeddingProviderConfidence.AllowsDataSourceConfidenceLevel(dataSource.ConfidenceLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a transcription provider of the given confidence level may hear the audio and video files of a data source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The transcription provider receives the content of the data source just like the embedding
|
||||
/// provider does, so the same rule applies to both, cf. AllowsEmbedding.
|
||||
/// </remarks>
|
||||
/// <param name="dataSource">The data source to index.</param>
|
||||
/// <param name="transcriptionProviderConfidence">The confidence level of the transcription provider.</param>
|
||||
/// <returns>True when the provider may transcribe the media files of the data source.</returns>
|
||||
internal static bool AllowsTranscription(IIndexedDataSource dataSource, ConfidenceLevel transcriptionProviderConfidence) => AllowsEmbedding(dataSource, transcriptionProviderConfidence);
|
||||
|
||||
/// <summary>
|
||||
/// The configured data sources this service indexes, from every list which holds some.
|
||||
/// </summary>
|
||||
|
||||
@@ -68,6 +68,13 @@ public sealed record DataSourceEmbeddingStatus(
|
||||
/// </summary>
|
||||
public int SkippedDocuments => this.PermanentlySkippedDocuments + this.IgnoredFiles.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Whether selected audio or video files wait for a transcription provider which this data
|
||||
/// source allows, so they cannot be indexed until the settings change.
|
||||
/// </summary>
|
||||
public bool NeedsTranscriptionProvider => this.IgnoredFiles.Any(file =>
|
||||
file.Reason is DataSourceEmbeddingIgnoreReason.TRANSCRIPTION_PROVIDER_MISSING or DataSourceEmbeddingIgnoreReason.TRANSCRIPTION_PROVIDER_NOT_ALLOWED);
|
||||
|
||||
/// <summary>
|
||||
/// The settled health of this data source. Activity is represented by State
|
||||
/// instead, so queued and running sources can keep their activity indicator.
|
||||
@@ -84,7 +91,8 @@ public sealed record DataSourceEmbeddingStatus(
|
||||
if (this.State is DataSourceEmbeddingState.FAILED || this.FailedDocuments > 0)
|
||||
return DataSourceEmbeddingHealth.ERROR;
|
||||
|
||||
if (this.State is DataSourceEmbeddingState.COMPLETED && this.IndexedDocuments == 0 && this.SkippedDocuments > 0)
|
||||
if (this.NeedsTranscriptionProvider ||
|
||||
this.State is DataSourceEmbeddingState.COMPLETED && this.IndexedDocuments == 0 && this.SkippedDocuments > 0)
|
||||
return DataSourceEmbeddingHealth.WARNING;
|
||||
|
||||
return DataSourceEmbeddingHealth.HEALTHY;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed record EmbeddedFileRecord(string Fingerprint, long FileSize, DateTimeOffset LastWriteUtc, DateTimeOffset EmbeddedAtUtc, int ChunkCount);
|
||||
public sealed record EmbeddedFileRecord(string Fingerprint, long FileSize, DateTimeOffset LastWriteUtc, DateTimeOffset EmbeddedAtUtc, int ChunkCount, string? TranscriptKey = null);
|
||||
@@ -226,7 +226,7 @@ internal sealed class DocumentRunProgress(IndexedRunContext context, int totalDo
|
||||
var embeddedAtUtc = DateTimeOffset.UtcNow;
|
||||
var state = document.State with { ChunkCount = chunkCount, EmbeddedAtUtc = embeddedAtUtc };
|
||||
await context.IndexStore.UpsertFileAsync(context.DataSource.Id, state, token);
|
||||
context.Manifest.Files[document.Key] = new EmbeddedFileRecord(state.Fingerprint, state.FileSize, state.LastWriteUtc, embeddedAtUtc, chunkCount);
|
||||
context.Manifest.Files[document.Key] = new EmbeddedFileRecord(state.Fingerprint, state.FileSize, state.LastWriteUtc, embeddedAtUtc, chunkCount, state.TranscriptKey);
|
||||
await context.ForgetPermanentFailureAsync(document.Key, token);
|
||||
context.Pass.Forget(document.Key);
|
||||
|
||||
@@ -300,7 +300,7 @@ internal sealed class DocumentRunProgress(IndexedRunContext context, int totalDo
|
||||
//
|
||||
this.PermanentlySkippedDocuments++;
|
||||
var occurredAtUtc = DateTimeOffset.UtcNow;
|
||||
var indexingMessage = this.GetFailureMessage(extractionFailure.Code, document);
|
||||
var indexingMessage = this.GetFailureMessage(extractionFailure.Code, document, extractionFailure.Message);
|
||||
this.failures.Add(new DataSourceEmbeddingFailure(document.Key, indexingMessage, occurredAtUtc, ExtractionCode: extractionFailure.Code, IsPermanent: true, DisplayName: document.DisplayName));
|
||||
context.Manifest.Files.Remove(document.Key);
|
||||
context.Pass.Forget(document.Key);
|
||||
@@ -533,10 +533,37 @@ internal sealed class DocumentRunProgress(IndexedRunContext context, int totalDo
|
||||
/// </remarks>
|
||||
/// <param name="code">Why reading the document failed, NONE when it was not about reading it.</param>
|
||||
/// <param name="document">The document.</param>
|
||||
/// <param name="details">A localized explanation supplied by a specialized reader, when available.</param>
|
||||
/// <returns>The message, ready to show.</returns>
|
||||
private string GetFailureMessage(FileExtractionErrorCode code, EmbeddingDocument document) => context.DataSource is DataSourceMailbox
|
||||
? string.Format(TB("The mail '{0}' could not be indexed. AI Studio tries again during the next sync."), document.DisplayName)
|
||||
: code.ToIndexingUserMessage(document.DisplayName);
|
||||
private string GetFailureMessage(FileExtractionErrorCode code, EmbeddingDocument document, string? details = null)
|
||||
{
|
||||
if (context.DataSource is DataSourceMailbox)
|
||||
return string.Format(TB("The mail '{0}' could not be indexed. AI Studio tries again during the next sync."), document.DisplayName);
|
||||
|
||||
if (code is FileExtractionErrorCode.MEDIA_PROCESSING_FAILED && !string.IsNullOrWhiteSpace(details))
|
||||
{
|
||||
return string.Format(
|
||||
TB("The media file '{0}' could not be processed, so it was not indexed. Reason: {1} AI Studio reads it again as soon as the file changes."),
|
||||
document.DisplayName,
|
||||
AsSentence(details));
|
||||
}
|
||||
|
||||
return code.ToIndexingUserMessage(document.DisplayName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends a reason with a full stop, so the sentence after it does not run into it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Providers write their error messages as they like, often without a full stop at the end.
|
||||
/// </remarks>
|
||||
/// <param name="reason">The reason.</param>
|
||||
/// <returns>The reason, ending with a punctuation mark.</returns>
|
||||
internal static string AsSentence(string reason)
|
||||
{
|
||||
var sentence = reason.TrimEnd();
|
||||
return sentence.Length is 0 || sentence[^1] is '.' or '!' or '?' or '…' ? sentence : $"{sentence}.";
|
||||
}
|
||||
|
||||
private DataSourceEmbeddingStatus CreateStatus(DataSourceEmbeddingState state, string currentDocument, string lastError, int? currentBlock, int? currentPage, DataSourceAttention attention = DataSourceAttention.NONE, int? pendingRemovalCount = null) => new(
|
||||
context.DataSource.Id,
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
|
||||
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Media;
|
||||
|
||||
namespace AIStudio.Tools.Services.Indexing;
|
||||
|
||||
@@ -268,6 +269,14 @@ internal sealed partial class FileSourceIndexer
|
||||
if (Directory.Exists(path))
|
||||
return true;
|
||||
|
||||
//
|
||||
// The Batch Processing assistant writes the transcript of a media file next to it. When
|
||||
// the data source selects the media file itself, the transcript is no document of its
|
||||
// own, so writing it must not start a run either:
|
||||
//
|
||||
if (MediaSidecarTranscripts.IsCoveredArtifact(path, selection))
|
||||
return false;
|
||||
|
||||
if (selection.Selects(path))
|
||||
return true;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Databases.IndexStore;
|
||||
using AIStudio.Tools.Databases.VectorStore;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Security;
|
||||
@@ -32,9 +33,10 @@ namespace AIStudio.Tools.Services.Indexing;
|
||||
/// </remarks>
|
||||
/// <param name="settingsManager">The settings, read for whether local data sources refresh on their own.</param>
|
||||
/// <param name="rustService">The runtime, which extracts the text of the files.</param>
|
||||
/// <param name="mediaTranscriptionService">Transcribes selected audio and video files.</param>
|
||||
/// <param name="textChunker">Cuts the text of a file into chunks.</param>
|
||||
/// <param name="logger">The logger of the embedding service, so the log reads the same whoever writes it.</param>
|
||||
internal sealed partial class FileSourceIndexer(SettingsManager settingsManager, RustService rustService, TextChunker textChunker, ILogger logger) : IIndexedSourceIndexer
|
||||
internal sealed partial class FileSourceIndexer(SettingsManager settingsManager, RustService rustService, MediaTranscriptionService mediaTranscriptionService, TextChunker textChunker, ILogger logger) : IIndexedSourceIndexer
|
||||
{
|
||||
private const bool IMAGE_EMBEDDING_ENABLED = false;
|
||||
private const int MAX_LOGGED_EXCLUDED_FILE_EXAMPLES = 3;
|
||||
@@ -45,12 +47,22 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
{
|
||||
INDEXABLE,
|
||||
EXCLUDED,
|
||||
MEDIA_NOT_SUPPORTED,
|
||||
IMAGE_NOT_SUPPORTED,
|
||||
TRANSCRIPTION_PROVIDER_MISSING,
|
||||
TRANSCRIPTION_PROVIDER_NOT_ALLOWED,
|
||||
TOO_LARGE,
|
||||
}
|
||||
|
||||
private sealed record DataSourceMetadataSnapshot(string SourceHash, IReadOnlyDictionary<string, string> FileHashes);
|
||||
|
||||
/// <summary>
|
||||
/// What decides about the files of a data source while they are listed.
|
||||
/// </summary>
|
||||
/// <param name="Selection">The files of a folder data source, or null for a single file, which has no size limit.</param>
|
||||
/// <param name="Manifest">What the index stores about the data source, or null when it does not matter.</param>
|
||||
/// <param name="MediaTranscription">Which transcription provider may hear audio and video files, asked for only once such a file turns up.</param>
|
||||
private sealed record InputFileRules(FolderFileSelection? Selection, DataSourceEmbeddingManifest? Manifest, Lazy<MediaTranscriptionAvailability> MediaTranscription);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IDataSourceBase dataSource) => dataSource is DataSourceLocalDirectory or DataSourceLocalFile;
|
||||
|
||||
@@ -66,7 +78,7 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
var manifest = context.Manifest;
|
||||
|
||||
var enumerationStartedUtc = DateTimeOffset.UtcNow;
|
||||
var inputFiles = this.GetInputFiles(dataSource);
|
||||
var inputFiles = this.GetInputFiles(dataSource, manifest);
|
||||
var enumerationDuration = DateTimeOffset.UtcNow - enumerationStartedUtc;
|
||||
if (inputFiles.UnreachableRootReason is { } unreachableRootReason)
|
||||
return this.StopWithoutRoot(context, dataSource, unreachableRootReason);
|
||||
@@ -133,6 +145,7 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
progress.RecordStillUnreadable(filePath, permanentFailure);
|
||||
|
||||
await progress.CompleteRunAsync(metadataSnapshot.SourceHash, "data source finished after removing missing files", token);
|
||||
this.CleanUpTranscripts(context, progress);
|
||||
return IndexedRunOutcome.DONE;
|
||||
}
|
||||
|
||||
@@ -181,7 +194,7 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
var fingerprint = metadataSnapshot.FileHashes[file.FullName];
|
||||
var existingRecord = manifest.Files.GetValueOrDefault(file.FullName);
|
||||
|
||||
var document = this.CreateFileDocument(context, dataSource, file, fingerprint);
|
||||
var document = this.CreateFileDocument(context, dataSource, file, fingerprint, null);
|
||||
var reportBlockProgress = progress.BeginDocument(document);
|
||||
var startedAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
@@ -196,6 +209,14 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
ShortHash(fingerprint),
|
||||
progress.DoneDocuments + 1,
|
||||
totalFiles);
|
||||
|
||||
//
|
||||
// Audio and video hold no text until they are transcribed. The document then reads
|
||||
// its transcript instead, and its row knows which transcript that is:
|
||||
//
|
||||
if (PersistentMediaTranscript.IsMediaPath(file.FullName))
|
||||
document = this.CreateFileDocument(context, dataSource, file, fingerprint, await this.ReadOrCreateTranscriptAsync(dataSource, file.FullName, token));
|
||||
|
||||
var chunkCount = await context.IndexDocumentAsync(document, reportBlockProgress, token);
|
||||
token.ThrowIfCancellationRequested();
|
||||
var fingerprintAfterEmbedding = BuildFileMetadataHash(file);
|
||||
@@ -203,6 +224,11 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
throw new IOException(string.Format(TB("The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run."), file.FullName));
|
||||
|
||||
await progress.RecordDocumentIndexedAsync(document, chunkCount, existingRecord is null, token);
|
||||
|
||||
// The recording changed, so its old transcript is no longer what it says:
|
||||
if (existingRecord?.TranscriptKey is { } previousTranscriptKey && !string.Equals(previousTranscriptKey, document.State.TranscriptKey, StringComparison.Ordinal))
|
||||
ForgetTranscript(DataSourceTranscriptStore.TryForDataSource(dataSource.Id), manifest, previousTranscriptKey, logger);
|
||||
|
||||
logger.LogInformation(
|
||||
"Embedded file '{FilePath}' for data source '{DataSourceName}' ({DataSourceId}) successfully. Chunks={ChunkCount}, DurationMs={DurationMs}.",
|
||||
file.FullName,
|
||||
@@ -246,6 +272,7 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
}
|
||||
|
||||
await progress.CompleteRunAsync(metadataSnapshot.SourceHash, "data source embedding run finished", token);
|
||||
this.CleanUpTranscripts(context, progress);
|
||||
logger.LogInformation(
|
||||
"Finished background embeddings for data source '{DataSourceName}' ({DataSourceId}). RefreshMode={RefreshMode}, Embedded={EmbeddedFiles}, New={NewFiles}, Changed={ChangedFiles}, Skipped={SkippedFiles}, PermanentlySkipped={PermanentlySkippedFiles}, RemovedMissing={RemovedMissingFiles}, Failed={FailedFiles}, Total={TotalFiles}, SourceHashPrefix={SourceHashPrefix}.",
|
||||
dataSource.Name,
|
||||
@@ -355,6 +382,79 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
return IndexedRunOutcome.DONE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the transcripts which no file of the data source uses anymore, once a run got through all of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only a run without a failure knows every transcript which is still needed: a file which
|
||||
/// failed may have its transcript already, and its next attempt must not pay for it again.
|
||||
/// The transcript of a recording which is gone has to go as well, since it holds what was said.
|
||||
/// </remarks>
|
||||
/// <param name="context">The run.</param>
|
||||
/// <param name="progress">How the run went.</param>
|
||||
private void CleanUpTranscripts(IndexedRunContext context, DocumentRunProgress progress)
|
||||
{
|
||||
if (progress.FailedDocuments > 0)
|
||||
return;
|
||||
|
||||
DeleteUnusedTranscripts(DataSourceTranscriptStore.TryForDataSource(context.DataSource.Id), context.Manifest, logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes every transcript of a data source which no indexed file uses, and the temporary files of interrupted writes.
|
||||
/// </summary>
|
||||
/// <param name="store">The transcripts of the data source, or null when there are none to clean up.</param>
|
||||
/// <param name="manifest">What the index stores about the data source.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
internal static void DeleteUnusedTranscripts(DataSourceTranscriptStore? store, DataSourceEmbeddingManifest manifest, ILogger logger)
|
||||
{
|
||||
if (store is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var usedKeys = manifest.Files.Values
|
||||
.Select(record => record.TranscriptKey)
|
||||
.OfType<string>()
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
var deleted = store.ListKeys().Count(key => !usedKeys.Contains(key) && store.Delete(key));
|
||||
deleted += store.DeleteTemporaryFiles();
|
||||
if (deleted > 0)
|
||||
logger.LogInformation("Removed {DeletedTranscripts} transcript file(s) which no file of the data source uses anymore.", deleted);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not remove the transcripts which no file of the data source uses anymore. The next complete run tries again.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the transcript with the given key, unless another file of the data source still uses it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Two recordings with the same content share one transcript, cf. DataSourceTranscriptStore.
|
||||
/// </remarks>
|
||||
/// <param name="store">The transcripts of the data source, or null when there are none to clean up.</param>
|
||||
/// <param name="manifest">What the index stores about the data source, without the file the transcript belonged to.</param>
|
||||
/// <param name="key">The key of the transcript, or null when the file had none.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
internal static void ForgetTranscript(DataSourceTranscriptStore? store, DataSourceEmbeddingManifest manifest, string? key, ILogger logger)
|
||||
{
|
||||
if (store is null || string.IsNullOrWhiteSpace(key) || manifest.Files.Values.Any(record => string.Equals(record.TranscriptKey, key, StringComparison.Ordinal)))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (store.Delete(key))
|
||||
logger.LogInformation("Removed the transcript '{TranscriptKey}', since no file of the data source uses it anymore.", key);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Could not remove the transcript '{TranscriptKey}'. The next complete run tries again.", key);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> RemoveMissingFileEmbeddingsAsync(IndexedRunContext context, FileEnumerationResult inputFiles, CancellationToken token)
|
||||
{
|
||||
var manifest = context.Manifest;
|
||||
@@ -363,7 +463,9 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
{
|
||||
await context.DeleteDocumentPointsAsync(removedFilePath, token);
|
||||
await context.IndexStore.DeleteFileAsync(context.DataSource.Id, removedFilePath, token);
|
||||
var removedTranscriptKey = manifest.Files.GetValueOrDefault(removedFilePath)?.TranscriptKey;
|
||||
manifest.Files.Remove(removedFilePath);
|
||||
ForgetTranscript(DataSourceTranscriptStore.TryForDataSource(context.DataSource.Id), manifest, removedTranscriptKey, logger);
|
||||
removedFiles++;
|
||||
logger.LogInformation(
|
||||
"Removed stale embeddings for deleted file '{FilePath}' from data source '{DataSourceName}' ({DataSourceId}).",
|
||||
@@ -487,19 +589,123 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
: string.Join("; ", reasons);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<EmbeddingChunk> StreamEmbeddingChunksAsync(string filePath, IDataSource dataSource, EmbeddingProvider embeddingProvider, PromptInjectionReportingScope reportingScope, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token)
|
||||
private async IAsyncEnumerable<EmbeddingChunk> StreamEmbeddingChunksAsync(string filePath, MediaTranscriptSlot? transcript, IDataSource dataSource, EmbeddingProvider embeddingProvider, PromptInjectionReportingScope reportingScope, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token)
|
||||
{
|
||||
var options = DataSourceEmbeddingService.GetChunkingOptions(dataSource, embeddingProvider);
|
||||
var strategy = TextChunker.GetStrategyForFile(filePath);
|
||||
var content = await ExtractedFileText.ReadAsync(rustService, filePath, embeddingProvider, null, reportingScope, token);
|
||||
|
||||
//
|
||||
// An audio or video file is read through its transcript. The runtime reads the transcript
|
||||
// like every other document, so it passes the same prompt injection filter, and a notice
|
||||
// names the media file rather than the transcript below the data directory:
|
||||
//
|
||||
var contentPath = transcript?.Path ?? filePath;
|
||||
PromptInjectionSource? reportAs = transcript is null ? null : PromptInjectionSource.FileContent(filePath);
|
||||
var strategy = TextChunker.GetStrategyForFile(contentPath);
|
||||
var content = await ExtractedFileText.ReadAsync(rustService, contentPath, embeddingProvider, reportAs, reportingScope, token);
|
||||
|
||||
await foreach (var chunk in textChunker.SplitAsync(content, strategy, options, embeddingProvider, token))
|
||||
yield return chunk;
|
||||
}
|
||||
|
||||
internal FileEnumerationResult GetInputFiles(IDataSource dataSource)
|
||||
/// <summary>
|
||||
/// Returns the transcript of an audio or video file, transcribing the file only when the data source holds no transcript of its content yet.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The provider is checked against the data source once more, right before the upload: the
|
||||
/// settings may have changed since the files were listed. A transcript which exists is used
|
||||
/// without any provider, since nothing leaves the computer for it.<br/><br/>
|
||||
///
|
||||
/// A transcription which cannot succeed for this file, such as one without text, is recorded
|
||||
/// for good, so the file is not paid for again until it changes.
|
||||
/// </remarks>
|
||||
/// <param name="dataSource">The data source.</param>
|
||||
/// <param name="mediaPath">The audio or video file.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>Where the transcript is.</returns>
|
||||
private async Task<MediaTranscriptSlot> ReadOrCreateTranscriptAsync(IDataSource dataSource, string mediaPath, CancellationToken token)
|
||||
{
|
||||
var provider = this.GetMediaTranscriptionAvailability(dataSource).Provider;
|
||||
var transcript = await PersistentMediaTranscript.ReadOrCreateAsync(DataSourceTranscriptStore.ForDataSource(dataSource.Id), mediaPath, provider, mediaTranscriptionService.TranscribeAsync, logger, token);
|
||||
return GetTranscriptOrThrow(transcript, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the transcript, or throws what tells the run how to treat the media file without one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A FileExtractionException records the file for good, until it changes; any other exception
|
||||
/// lets the next pass try again. A transcription without text, a file which the media pipeline
|
||||
/// cannot read, and a file which the transcription provider refused would fail the same way
|
||||
/// every time, and every attempt would cost money or another upload.
|
||||
/// </remarks>
|
||||
/// <param name="transcript">The outcome of the request for the transcript.</param>
|
||||
/// <param name="token">The cancellation token of the run.</param>
|
||||
/// <returns>Where the transcript is.</returns>
|
||||
internal static MediaTranscriptSlot GetTranscriptOrThrow(PersistentMediaTranscriptResult transcript, CancellationToken token) => transcript.Status switch
|
||||
{
|
||||
PersistentMediaTranscriptStatus.READY => new(transcript.TranscriptPath, transcript.Key),
|
||||
PersistentMediaTranscriptStatus.CANCELLED => throw new OperationCanceledException(token),
|
||||
PersistentMediaTranscriptStatus.EMPTY or PersistentMediaTranscriptStatus.NO_AUDIBLE_SIGNAL => throw new FileExtractionException(FileExtractionErrorCode.NO_CONTENT, transcript.UserMessage),
|
||||
PersistentMediaTranscriptStatus.TRANSCRIPTION_FAILED when IsPermanentMediaFailure(transcript.ErrorCode) => throw new FileExtractionException(FileExtractionErrorCode.MEDIA_PROCESSING_FAILED, transcript.UserMessage),
|
||||
PersistentMediaTranscriptStatus.REJECTED => throw new FileExtractionException(FileExtractionErrorCode.MEDIA_PROCESSING_FAILED, transcript.UserMessage),
|
||||
_ => throw new IOException(transcript.UserMessage),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Which transcription provider may hear the audio and video files of a data source right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The transcription provider receives the content of the data source just like the embedding
|
||||
/// provider, so it has to meet the confidence level of the data source as well, cf.
|
||||
/// DataSourceEmbeddingService.AllowsTranscription. A data source without a confidence level
|
||||
/// of its own is never sent anywhere.
|
||||
/// </remarks>
|
||||
/// <param name="dataSource">The data source.</param>
|
||||
/// <returns>The provider, or why there is none.</returns>
|
||||
private MediaTranscriptionAvailability GetMediaTranscriptionAvailability(IDataSource dataSource)
|
||||
{
|
||||
var provider = mediaTranscriptionService.GetUsableProvider();
|
||||
if (provider is null)
|
||||
return MediaTranscriptionAvailability.MISSING;
|
||||
|
||||
var providerConfidence = provider.GetConfidenceLevel(settingsManager);
|
||||
if (dataSource is IIndexedDataSource indexedDataSource && DataSourceEmbeddingService.AllowsTranscription(indexedDataSource, providerConfidence))
|
||||
return MediaTranscriptionAvailability.Allowed(provider);
|
||||
|
||||
logger.LogInformation(
|
||||
"The transcription provider '{ProviderName}' may not hear the audio and video files of data source '{DataSourceName}' ({DataSourceId}): the data source asks for the confidence level {RequiredConfidence}, while the provider has {ProviderConfidence}.",
|
||||
provider.Name,
|
||||
dataSource.Name,
|
||||
dataSource.Id,
|
||||
(dataSource as IIndexedDataSource)?.ConfidenceLevel,
|
||||
providerConfidence);
|
||||
|
||||
return MediaTranscriptionAvailability.NOT_ALLOWED;
|
||||
}
|
||||
|
||||
private static bool IsPermanentMediaFailure(MediaJobErrorCode? code) => code is
|
||||
MediaJobErrorCode.UNKNOWN_FORMAT
|
||||
or MediaJobErrorCode.UNSAFE_FILE
|
||||
or MediaJobErrorCode.NOT_MEDIA
|
||||
or MediaJobErrorCode.UNSUPPORTED_CONTAINER
|
||||
or MediaJobErrorCode.NO_AUDIO_TRACK
|
||||
or MediaJobErrorCode.UNSUPPORTED_CODEC
|
||||
or MediaJobErrorCode.INVALID_AUDIO_PARAMETERS
|
||||
or MediaJobErrorCode.INVALID_OPUS_HEADER
|
||||
or MediaJobErrorCode.UNSUPPORTED_OPUS_MAPPING
|
||||
or MediaJobErrorCode.DAMAGED_CONTAINER;
|
||||
|
||||
/// <summary>
|
||||
/// Lists the files of a data source and decides what a run does with each of them.
|
||||
/// </summary>
|
||||
/// <param name="dataSource">The data source.</param>
|
||||
/// <param name="manifest">What the index stores about the data source, or null when it does not matter.</param>
|
||||
/// <param name="mediaTranscription">Which transcription provider may hear its audio and video files, or null to ask the settings once a media file turns up.</param>
|
||||
/// <returns>The files, and those which were left out.</returns>
|
||||
internal FileEnumerationResult GetInputFiles(IDataSource dataSource, DataSourceEmbeddingManifest? manifest = null, MediaTranscriptionAvailability? mediaTranscription = null)
|
||||
{
|
||||
var result = new FileEnumerationResult();
|
||||
var mediaTranscriptionAvailability = new Lazy<MediaTranscriptionAvailability>(() => mediaTranscription ?? this.GetMediaTranscriptionAvailability(dataSource));
|
||||
|
||||
switch (dataSource)
|
||||
{
|
||||
@@ -507,13 +713,13 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
var file = new FileInfo(localFile.FilePath);
|
||||
|
||||
// No size limit: the user chose this very file, cf. FolderFileSelection.IsTooLarge.
|
||||
if (this.AddInputFile(file, null, result) is RagFileIndexingDecision.EXCLUDED)
|
||||
if (this.AddInputFile(file, new InputFileRules(null, manifest, mediaTranscriptionAvailability), result) is RagFileIndexingDecision.EXCLUDED)
|
||||
logger.LogDebug("Skipping excluded file '{FilePath}' while indexing.", file.FullName);
|
||||
|
||||
return result;
|
||||
|
||||
case DataSourceLocalDirectory localDirectory when Directory.Exists(localDirectory.Path):
|
||||
this.EnumerateAccessibleFiles(localDirectory, result);
|
||||
this.EnumerateAccessibleFiles(localDirectory, manifest, mediaTranscriptionAvailability, result);
|
||||
if (result.UnreadableDirectories.Contains(localDirectory.Path))
|
||||
result.UnreachableRootReason = string.Format(TB("The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index."), localDirectory.Path);
|
||||
|
||||
@@ -537,9 +743,10 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
return result;
|
||||
}
|
||||
|
||||
private void EnumerateAccessibleFiles(DataSourceLocalDirectory dataSource, FileEnumerationResult result)
|
||||
private void EnumerateAccessibleFiles(DataSourceLocalDirectory dataSource, DataSourceEmbeddingManifest? manifest, Lazy<MediaTranscriptionAvailability> mediaTranscription, FileEnumerationResult result)
|
||||
{
|
||||
var selection = FolderFileSelection.Of(dataSource);
|
||||
var rules = new InputFileRules(selection, manifest, mediaTranscription);
|
||||
|
||||
//
|
||||
// Every log line travels to the runtime as a request of its own. A folder holding many
|
||||
@@ -580,7 +787,17 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.AddInputFile(fileInfo, selection, result) is not RagFileIndexingDecision.EXCLUDED)
|
||||
//
|
||||
// The transcript which the Batch Processing assistant wrote next to a selected media
|
||||
// file holds the text the data source gets from the media file itself:
|
||||
//
|
||||
if (MediaSidecarTranscripts.IsCoveredArtifact(fileInfo.FullName, selection))
|
||||
{
|
||||
logger.LogDebug("Skipping the transcript '{FilePath}', because its media file is indexed as the document.", fileInfo.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.AddInputFile(fileInfo, rules, result) is not RagFileIndexingDecision.EXCLUDED)
|
||||
continue;
|
||||
|
||||
excludedFiles++;
|
||||
@@ -593,11 +810,11 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
}
|
||||
|
||||
/// <param name="file">The file.</param>
|
||||
/// <param name="selection">The files of a folder data source, or null for a single file, which has no size limit.</param>
|
||||
/// <param name="rules">What decides about the files of the data source.</param>
|
||||
/// <param name="result">Where the file goes, if anywhere.</param>
|
||||
private RagFileIndexingDecision AddInputFile(FileInfo file, FolderFileSelection? selection, FileEnumerationResult result)
|
||||
private RagFileIndexingDecision AddInputFile(FileInfo file, InputFileRules rules, FileEnumerationResult result)
|
||||
{
|
||||
var decision = this.GetRagFileIndexingDecision(file, selection);
|
||||
var decision = this.GetRagFileIndexingDecision(file, rules);
|
||||
switch (decision)
|
||||
{
|
||||
case RagFileIndexingDecision.INDEXABLE:
|
||||
@@ -607,8 +824,16 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
case RagFileIndexingDecision.EXCLUDED:
|
||||
break;
|
||||
|
||||
case RagFileIndexingDecision.MEDIA_NOT_SUPPORTED:
|
||||
result.AddIgnoredFile(file.FullName, DataSourceEmbeddingIgnoreReason.MEDIA_NOT_SUPPORTED);
|
||||
case RagFileIndexingDecision.IMAGE_NOT_SUPPORTED:
|
||||
result.AddIgnoredFile(file.FullName, DataSourceEmbeddingIgnoreReason.IMAGE_NOT_SUPPORTED);
|
||||
break;
|
||||
|
||||
case RagFileIndexingDecision.TRANSCRIPTION_PROVIDER_MISSING:
|
||||
result.AddIgnoredFile(file.FullName, DataSourceEmbeddingIgnoreReason.TRANSCRIPTION_PROVIDER_MISSING);
|
||||
break;
|
||||
|
||||
case RagFileIndexingDecision.TRANSCRIPTION_PROVIDER_NOT_ALLOWED:
|
||||
result.AddIgnoredFile(file.FullName, DataSourceEmbeddingIgnoreReason.TRANSCRIPTION_PROVIDER_NOT_ALLOWED);
|
||||
break;
|
||||
|
||||
case RagFileIndexingDecision.TOO_LARGE:
|
||||
@@ -630,24 +855,24 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
return FileTypes.IsAllowedPath(filePath, FileTypes.IMAGE);
|
||||
}
|
||||
|
||||
private RagFileIndexingDecision GetRagFileIndexingDecision(FileInfo file, FolderFileSelection? selection)
|
||||
private RagFileIndexingDecision GetRagFileIndexingDecision(FileInfo file, InputFileRules rules)
|
||||
{
|
||||
if (this.IsSkippedRagFile(file))
|
||||
return RagFileIndexingDecision.EXCLUDED;
|
||||
|
||||
if (!IMAGE_EMBEDDING_ENABLED && this.IsImageFilePath(file.FullName))
|
||||
return RagFileIndexingDecision.MEDIA_NOT_SUPPORTED;
|
||||
return RagFileIndexingDecision.IMAGE_NOT_SUPPORTED;
|
||||
|
||||
//
|
||||
// Audio and video hold no text until they are transcribed, and data sources do not
|
||||
// transcribe them yet:
|
||||
// Before any transcription: a video of several gigabytes is left out like every other
|
||||
// file above the limit, rather than sent to the transcription provider.
|
||||
//
|
||||
if (FileTypes.IsAllowedPath(file.FullName, FileTypes.AUDIO, FileTypes.VIDEO))
|
||||
return RagFileIndexingDecision.MEDIA_NOT_SUPPORTED;
|
||||
|
||||
if (selection?.IsTooLarge(file) is true)
|
||||
if (rules.Selection?.IsTooLarge(file) is true)
|
||||
return RagFileIndexingDecision.TOO_LARGE;
|
||||
|
||||
if (PersistentMediaTranscript.IsMediaPath(file.FullName))
|
||||
return GetMediaIndexingDecision(file, rules);
|
||||
|
||||
// A directory source reaches this point only after its user-configured pattern selected
|
||||
// the file. A single-file source was validated before it was stored. Extraction is the
|
||||
// authoritative capability check and records an unreadable selected file through the
|
||||
@@ -655,6 +880,39 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
return RagFileIndexingDecision.INDEXABLE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides about an audio or video file, which holds no text until it is transcribed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A media file which the index holds unchanged needs no transcript in this run, nor does one
|
||||
/// which failed for good before: neither is read. A missing transcription provider, or one which
|
||||
/// the data source no longer allows, therefore takes nothing out of the index. Every other media
|
||||
/// file needs a provider which may hear it, unless the data source has its transcript already;
|
||||
/// that is only known once the file is read, so such a file waits for the provider as well.
|
||||
/// </remarks>
|
||||
/// <param name="file">The audio or video file.</param>
|
||||
/// <param name="rules">What decides about the files of the data source.</param>
|
||||
/// <returns>INDEXABLE, or why the file is left out.</returns>
|
||||
private static RagFileIndexingDecision GetMediaIndexingDecision(FileInfo file, InputFileRules rules)
|
||||
{
|
||||
if (rules.Manifest is { } manifest)
|
||||
{
|
||||
var fingerprint = BuildFileMetadataHash(file);
|
||||
if (manifest.Files.TryGetValue(file.FullName, out var existingRecord) && string.Equals(existingRecord.Fingerprint, fingerprint, StringComparison.Ordinal))
|
||||
return RagFileIndexingDecision.INDEXABLE;
|
||||
|
||||
if (manifest.PermanentFailures.TryGetValue(file.FullName, out var permanentFailure) && string.Equals(permanentFailure.Fingerprint, fingerprint, StringComparison.Ordinal))
|
||||
return RagFileIndexingDecision.INDEXABLE;
|
||||
}
|
||||
|
||||
return rules.MediaTranscription.Value.IgnoreReason switch
|
||||
{
|
||||
null => RagFileIndexingDecision.INDEXABLE,
|
||||
DataSourceEmbeddingIgnoreReason.TRANSCRIPTION_PROVIDER_NOT_ALLOWED => RagFileIndexingDecision.TRANSCRIPTION_PROVIDER_NOT_ALLOWED,
|
||||
_ => RagFileIndexingDecision.TRANSCRIPTION_PROVIDER_MISSING,
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsSkippedRagFile(FileInfo file)
|
||||
{
|
||||
if (FileExclusions.IsAlwaysExcludedFile(file.Name))
|
||||
@@ -731,7 +989,7 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
return BuildMetadataHash(parts);
|
||||
}
|
||||
|
||||
private static string BuildFileMetadataHash(FileInfo file)
|
||||
internal static string BuildFileMetadataHash(FileInfo file)
|
||||
{
|
||||
file.Refresh();
|
||||
if (!file.Exists)
|
||||
@@ -774,12 +1032,13 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
|
||||
/// <param name="dataSource">The data source the file belongs to.</param>
|
||||
/// <param name="file">The file.</param>
|
||||
/// <param name="fingerprint">The fingerprint of the file as it is about to be read.</param>
|
||||
/// <param name="transcript">The transcript an audio or video file is read through, or null for every other file.</param>
|
||||
/// <returns>The document.</returns>
|
||||
private EmbeddingDocument CreateFileDocument(IndexedRunContext context, IDataSource dataSource, FileInfo file, string fingerprint) => new(
|
||||
private EmbeddingDocument CreateFileDocument(IndexedRunContext context, IDataSource dataSource, FileInfo file, string fingerprint, MediaTranscriptSlot? transcript) => new(
|
||||
file.FullName,
|
||||
this.CreateEmbeddingStateFile(dataSource, file, fingerprint, 0, DateTimeOffset.UtcNow),
|
||||
this.CreateEmbeddingStateFile(dataSource, file, fingerprint, 0, DateTimeOffset.UtcNow) with { TranscriptKey = transcript?.Key },
|
||||
file.Name,
|
||||
token => this.StreamEmbeddingChunksAsync(file.FullName, dataSource, context.EmbeddingProvider, context.PromptInjectionScope, token));
|
||||
token => this.StreamEmbeddingChunksAsync(file.FullName, transcript, dataSource, context.EmbeddingProvider, context.PromptInjectionScope, token));
|
||||
|
||||
private EmbeddingStateFile CreateEmbeddingStateFile(IDataSource dataSource, FileInfo file, string fingerprint, int chunkCount, DateTimeOffset embeddedAtUtc)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Tools.Services.Indexing;
|
||||
|
||||
/// <summary>
|
||||
/// Which transcription provider may hear the audio and video files of a data source.
|
||||
/// </summary>
|
||||
/// <param name="Provider">The provider, or null when none may.</param>
|
||||
/// <param name="IgnoreReason">Why audio and video files without a transcript are left out when no provider may hear them; null when there is a provider.</param>
|
||||
internal sealed record MediaTranscriptionAvailability(TranscriptionProvider? Provider, DataSourceEmbeddingIgnoreReason? IgnoreReason)
|
||||
{
|
||||
/// <summary>
|
||||
/// No usable transcription provider is configured.
|
||||
/// </summary>
|
||||
public static readonly MediaTranscriptionAvailability MISSING = new(null, DataSourceEmbeddingIgnoreReason.TRANSCRIPTION_PROVIDER_MISSING);
|
||||
|
||||
/// <summary>
|
||||
/// The configured transcription provider falls short of the confidence level of the data source.
|
||||
/// </summary>
|
||||
public static readonly MediaTranscriptionAvailability NOT_ALLOWED = new(null, DataSourceEmbeddingIgnoreReason.TRANSCRIPTION_PROVIDER_NOT_ALLOWED);
|
||||
|
||||
/// <summary>
|
||||
/// The given provider may hear the media files.
|
||||
/// </summary>
|
||||
/// <param name="provider">The provider.</param>
|
||||
/// <returns>The availability.</returns>
|
||||
public static MediaTranscriptionAvailability Allowed(TranscriptionProvider provider) => new(provider, null);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public sealed class MediaTranscriptionService(
|
||||
}
|
||||
|
||||
/// <summary>Gets whether the currently configured transcription provider can be used.</summary>
|
||||
public bool HasUsableTranscriptionProvider => this.ResolveProvider() is not null;
|
||||
public bool HasUsableTranscriptionProvider => this.GetUsableProvider() is not null;
|
||||
|
||||
/// <summary>Gets the last retained state for one owner.</summary>
|
||||
public MediaImportSnapshot? GetSnapshot(MediaImportOwner owner)
|
||||
@@ -411,14 +411,31 @@ public sealed class MediaTranscriptionService(
|
||||
/// <param name="mediaPath">Audio or video file path.</param>
|
||||
/// <param name="token">Caller cancellation token.</param>
|
||||
/// <returns>A typed terminal result.</returns>
|
||||
public async Task<MediaTranscriptionResult> TranscribeAsync(string mediaPath, CancellationToken token = default)
|
||||
public Task<MediaTranscriptionResult> TranscribeAsync(string mediaPath, CancellationToken token = default) => this.TranscribeWithoutImportAsync(mediaPath, null, token);
|
||||
|
||||
/// <summary>
|
||||
/// Transcribes an audio or video file with exactly the given provider, without starting a visible import operation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For callers which checked the provider against rules of their own, such as the confidence
|
||||
/// level of a data source. The provider is used as given and not resolved from the settings
|
||||
/// again, so a change of the settings between that check and the upload cannot send the media
|
||||
/// to another provider.
|
||||
/// </remarks>
|
||||
/// <param name="mediaPath">Audio or video file path.</param>
|
||||
/// <param name="provider">The transcription provider the caller checked.</param>
|
||||
/// <param name="token">Caller cancellation token.</param>
|
||||
/// <returns>A typed terminal result.</returns>
|
||||
public Task<MediaTranscriptionResult> TranscribeAsync(string mediaPath, TranscriptionProvider provider, CancellationToken token = default) => this.TranscribeWithoutImportAsync(mediaPath, provider, token);
|
||||
|
||||
private async Task<MediaTranscriptionResult> TranscribeWithoutImportAsync(string mediaPath, TranscriptionProvider? provider, CancellationToken token)
|
||||
{
|
||||
this.ThrowIfDisposed();
|
||||
var operation = this.CreateOperation(null, token);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
return await this.TranscribeCoreAsync(mediaPath, operation, updateImportState: false);
|
||||
return await this.TranscribeCoreAsync(mediaPath, operation, updateImportState: false, provider);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -458,8 +475,9 @@ public sealed class MediaTranscriptionService(
|
||||
/// <param name="mediaPath">Source media path.</param>
|
||||
/// <param name="operation">Operation-specific cancellation and Rust-job state.</param>
|
||||
/// <param name="updateImportState">Whether progress belongs to the visible import lane.</param>
|
||||
/// <param name="checkedProvider">The provider a caller checked itself, or null for the configured one.</param>
|
||||
/// <returns>A typed terminal result.</returns>
|
||||
private async Task<MediaTranscriptionResult> TranscribeCoreAsync(string mediaPath, MediaOperation operation, bool updateImportState)
|
||||
private async Task<MediaTranscriptionResult> TranscribeCoreAsync(string mediaPath, MediaOperation operation, bool updateImportState, TranscriptionProvider? checkedProvider = null)
|
||||
{
|
||||
var normalizedPath = Path.Combine(Path.GetTempPath(), "mindwork-ai-studio-media", $"{operation.Id:N}.webm");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(normalizedPath)!);
|
||||
@@ -497,7 +515,7 @@ public sealed class MediaTranscriptionService(
|
||||
return MediaTranscriptionResult.NoAudibleSignal(TB("The audio track contains no audible signal, so there is nothing to transcribe."));
|
||||
}
|
||||
|
||||
var providerSettings = this.ResolveProvider();
|
||||
var providerSettings = checkedProvider ?? this.GetUsableProvider();
|
||||
if (providerSettings is null)
|
||||
return MediaTranscriptionResult.Failed(TB("No usable transcription provider is configured."));
|
||||
|
||||
@@ -530,16 +548,20 @@ public sealed class MediaTranscriptionService(
|
||||
"The transcription provider failed for '{FileName}' (operation {OperationId}): {Diagnostic}",
|
||||
fileName,
|
||||
operation.Id,
|
||||
providerResult.ErrorMessage);
|
||||
string.IsNullOrWhiteSpace(providerResult.ErrorMessage) ? "<empty>" : providerResult.ErrorMessage);
|
||||
|
||||
//
|
||||
// When the provider told us why it failed, the user gets to read it. Only that
|
||||
// message says whether to wait, to check the API key, or to ask the provider for
|
||||
// a format it can read:
|
||||
//
|
||||
return MediaTranscriptionResult.Failed(string.IsNullOrWhiteSpace(providerResult.ErrorMessage)
|
||||
var failureMessage = string.IsNullOrWhiteSpace(providerResult.ErrorMessage)
|
||||
? TB("The transcription provider could not transcribe the media file.")
|
||||
: providerResult.ErrorMessage);
|
||||
: providerResult.ErrorMessage;
|
||||
|
||||
return providerResult.FileRejected
|
||||
? MediaTranscriptionResult.Rejected(failureMessage)
|
||||
: MediaTranscriptionResult.Failed(failureMessage);
|
||||
}
|
||||
|
||||
return MediaTranscriptionResult.Succeeded(providerResult.Text.Trim());
|
||||
@@ -719,7 +741,12 @@ public sealed class MediaTranscriptionService(
|
||||
}
|
||||
|
||||
/// <summary>Resolves the configured provider after confidence validation.</summary>
|
||||
private TranscriptionProvider? ResolveProvider()
|
||||
/// <remarks>
|
||||
/// Checks the global minimum confidence only. A caller with rules of its own, such as the
|
||||
/// confidence level of a data source, checks the provider against them as well.
|
||||
/// </remarks>
|
||||
/// <returns>The configured provider, or null when none is configured or it falls short of the global minimum.</returns>
|
||||
public TranscriptionProvider? GetUsableProvider()
|
||||
{
|
||||
var providerId = settingsManager.ConfigurationData.App.UseTranscriptionProvider;
|
||||
if (string.IsNullOrWhiteSpace(providerId))
|
||||
@@ -735,6 +762,19 @@ public sealed class MediaTranscriptionService(
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The configured provider, when it may also hear the audio and video files of the given data source.
|
||||
/// </summary>
|
||||
/// <param name="dataSource">The data source, with the confidence level it asks for.</param>
|
||||
/// <returns>The provider, or null when none is configured or it falls short of the global minimum or of the data source.</returns>
|
||||
public TranscriptionProvider? GetUsableProvider(IIndexedDataSource dataSource)
|
||||
{
|
||||
var provider = this.GetUsableProvider();
|
||||
return provider is not null && DataSourceEmbeddingService.AllowsTranscription(dataSource, provider.GetConfidenceLevel(settingsManager))
|
||||
? provider
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Creates and registers operation-owned cancellation state.</summary>
|
||||
/// <param name="target">Optional visible media import target.</param>
|
||||
/// <param name="token">Caller token linked to the operation.</param>
|
||||
|
||||
@@ -65,7 +65,7 @@ public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
|
||||
var transportTimestamp = FormatTransportTimestamp(now);
|
||||
var logLevel = logEntry.LogLevel.ToString();
|
||||
var category = logEntry.Category;
|
||||
var exceptionMessage = logEntry.Exception?.Message;
|
||||
var exceptionMessage = DescribeException(logEntry.Exception);
|
||||
var stackTrace = logEntry.Exception?.StackTrace;
|
||||
var colorCode = GetColorForLogLevel(logEntry.LogLevel);
|
||||
|
||||
@@ -108,6 +108,34 @@ public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
|
||||
/// <returns>The time in the round-trip format.</returns>
|
||||
public static string FormatTransportTimestamp(DateTimeOffset timestamp) => timestamp.ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Describes an exception by its message and the messages of its inner exceptions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The outer message alone often hides the cause. HttpClient, for example, reports a broken
|
||||
/// upload as "Error while copying content to a stream.", and only an inner exception says
|
||||
/// whether the other side closed the connection.
|
||||
/// </remarks>
|
||||
/// <param name="exception">The exception, or null when the log event has none.</param>
|
||||
/// <returns>The messages, from the outer exception inwards, or null without an exception.</returns>
|
||||
public static string? DescribeException(Exception? exception)
|
||||
{
|
||||
if (exception is null)
|
||||
return null;
|
||||
|
||||
var description = exception.Message;
|
||||
for (var innerException = exception.InnerException; innerException is not null; innerException = innerException.InnerException)
|
||||
{
|
||||
// An AggregateException names the messages of its inner exceptions already:
|
||||
if (description.Contains(innerException.Message, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
description = $"{description} ---> {innerException.Message}";
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
private static string GetColorForLogLevel(LogLevel logLevel) => logLevel switch
|
||||
{
|
||||
LogLevel.Trace => ANSI_GRAY,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace AIStudio.Tools.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// What a data source sends to a provider which is not self-hosted.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum CloudProcessing
|
||||
{
|
||||
/// <summary>
|
||||
/// Nothing leaves the computer or the organization.
|
||||
/// </summary>
|
||||
NONE = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The content goes to an embedding provider in the cloud.
|
||||
/// </summary>
|
||||
EMBEDDING = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Audio and video files go to a transcription provider in the cloud.
|
||||
/// </summary>
|
||||
TRANSCRIPTION = 2,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace AIStudio.Tools.Validation;
|
||||
|
||||
public static class CloudProcessingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether what the user acknowledged covers what the data source sends to the cloud now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An acknowledgment holds only for the providers it was given for. When the data source starts
|
||||
/// to send its recordings to a transcription provider in the cloud, say since a new pattern
|
||||
/// selects them, the user has to acknowledge that once more.
|
||||
/// </remarks>
|
||||
/// <param name="acknowledged">What the user acknowledged.</param>
|
||||
/// <param name="current">What the data source sends to the cloud with its current settings.</param>
|
||||
/// <returns>True when nothing is sent that the user did not acknowledge.</returns>
|
||||
public static bool Covers(this CloudProcessing acknowledged, CloudProcessing current) => (current & ~acknowledged) is CloudProcessing.NONE;
|
||||
}
|
||||
@@ -31,6 +31,8 @@ public sealed class DataSourceValidation
|
||||
|
||||
public Func<bool> GetSelectedCloudEmbedding { get; init; } = () => false;
|
||||
|
||||
public Func<bool> GetSelectedCloudTranscription { get; init; } = () => false;
|
||||
|
||||
public Func<EmbeddingProvider?> GetSelectedEmbeddingProvider { get; init; } = () => null;
|
||||
|
||||
public Func<ConfidenceLevel> GetConfidenceLevel { get; init; } = () => ConfidenceLevel.NONE;
|
||||
@@ -158,8 +160,8 @@ public sealed class DataSourceValidation
|
||||
if(!File.Exists(filePath))
|
||||
return TB("The file does not exist. Please select a valid file.");
|
||||
|
||||
if(!FileTypes.IsAllowedPath(filePath, FileTypes.DOCUMENT))
|
||||
return TB("The selected file is not a supported document or readable text file.");
|
||||
if(!FileTypes.IsAllowedPath(filePath, FileTypes.DOCUMENT, FileTypes.AUDIO, FileTypes.VIDEO))
|
||||
return TB("The selected file is not a supported document, readable text file, audio file, or video file.");
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -250,6 +252,9 @@ public sealed class DataSourceValidation
|
||||
|
||||
public string? ValidateUserAcknowledgedCloudEmbedding(bool value)
|
||||
{
|
||||
if(this.GetSelectedCloudTranscription() && !value)
|
||||
return TB("Please acknowledge that you are aware of the cloud transcription implications.");
|
||||
|
||||
if(this.GetSelectedCloudEmbedding() && !value)
|
||||
return TB("Please acknowledge that you are aware of the cloud embedding implications.");
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
- Added Requesty as a new LLM provider for chats, embeddings, and transcriptions. Requesty is an AI gateway from the United Kingdom that gives you models from many providers with a single API key. Because where your data is processed depends on the model you choose, you set its trust level yourself. Thanks, Thibault Jaigu (`Thibaultjaigu`), for this first contribution.
|
||||
- Added [llmman](https://github.com/llmmanorg/llmman) as a new option for the models you host yourself. llmman runs open models on your own computer or server, and AI Studio can use them for chats, embeddings, and transcriptions. Thanks, Eric Curtin (`ericcurtin`), for this first contribution.
|
||||
- Added support for Mistral Large 4.
|
||||
- Added transcription of audio and video files in local data sources. When you select such files, for example with the pattern `*.mp3` or `*`, AI Studio sends them to your transcription provider and adds the transcripts to the index. It keeps each transcript, so an unchanged recording is transcribed only once. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
|
||||
- Added backups of all your data. Under Data Backup & Restore, on the information page and in the settings, you save your chats, settings, plugins, and the index of your data sources into a single file, for example, to take everything to a new computer. Restoring such a backup replaces your data when AI Studio restarts, or you add only its chats to the ones you have. API keys and passwords never go into a backup, and after a restore, AI Studio tells you where to enter them again. Thanks, Jens Erler (`j-erler`), for this contribution.
|
||||
- Added organization-wide management for backups. IT departments can prevent users from backing up their data or from restoring a backup. A backup leaves out what the configurations of your organization bring along, such as their providers and plugins, so nothing appears twice after a restore.
|
||||
- Added an overview of the data folders of AI Studio to the information page, for when you back up your computer with a tool of your own. It shows where the folders are and which of their contents belong into a backup. AI Studio now stores the locations of its own files, such as transcripts, relative to its data folder, so such a copy also works on a computer with another user name.
|
||||
|
||||
Reference in new issue
Block a user