2026-08-11 20:14:21 +02:00
using System.Text ;
using AIStudio.Tools.Media ;
using AIStudio.Tools.Rust ;
namespace AIStudio.Assistants.BatchProcessing ;
public partial class AssistantBatchProcessing
{
/// <summary>
/// Loads a document through the Rust content stream or resolves a persistent
/// transcript for an audio or video file.
/// </summary>
private Task < string? > LoadInputContentAsync ( BatchProcessingFileResult fileResult , CancellationToken token )
{
return IsTranscribableMedia ( fileResult . FilePath )
? this . LoadMediaTranscriptAsync ( fileResult , token )
2026-08-23 21:15:37 +02:00
: this . LoadDocumentContentAsync ( fileResult , token );
2026-08-11 20:14:21 +02:00
}
2026-08-23 21:15:37 +02:00
private async Task < string? > LoadDocumentContentAsync ( BatchProcessingFileResult fileResult , CancellationToken token )
2026-08-11 20:14:21 +02:00
{
FileExtractionResult extraction ;
try
{
2026-08-23 21:15:37 +02:00
extraction = await this . RustService . ReadArbitraryFileData ( fileResult . FilePath , int . MaxValue , token : token );
2026-08-11 20:14:21 +02:00
}
catch ( Exception e )
{
this . FinishFileResult ( fileResult , BatchProcessingFileStatus . FAILED , string . Format ( T ( "Was not able to read the file: {0}" ), e . Message ), e );
return null ;
}
2026-08-23 21:15:37 +02:00
//
// The user stopped the batch run while we were reading this file. That says nothing about
// the file, so it gets the same status as a cancelled AI request instead of a failure:
//
if ( extraction . ErrorCode is FileExtractionErrorCode . CANCELLED )
{
this . FinishFileResult ( fileResult , BatchProcessingFileStatus . CANCELED , T ( "The batch run was canceled." ));
return null ;
}
2026-08-11 20:14:21 +02:00
if (! extraction . HasUsableContent )
{
this . Logger . LogError ( "Reading the batch file '{FilePath}' failed: code={ErrorCode}, message='{ErrorMessage}'." , fileResult . FilePath , extraction . ErrorCode , extraction . ErrorMessage );
this . FinishFileResult ( fileResult , BatchProcessingFileStatus . FAILED , extraction . ToUserMessage ( fileResult . FileName ));
return null ;
}
if ( extraction . Outcome is FileExtractionOutcome . PARTIAL )
{
this . Logger . LogWarning ( "Parts of the batch file '{FilePath}' could not be read: pages={FailedPages}." , fileResult . FilePath , string . Join ( ", " , extraction . FailedPages ));
await this . MessageBus . SendWarning ( new ( Icons . Material . Filled . Description , extraction . ToPartialUserMessage ( fileResult . FileName )));
}
if ( extraction . HasExtensionMismatch )
{
this . Logger . LogWarning ( "The batch file '{FilePath}' is actually a '{DetectedFormat}'." , fileResult . FilePath , extraction . DetectedFormat );
await this . MessageBus . SendWarning ( new ( Icons . Material . Filled . RuleFolder , extraction . ToExtensionMismatchUserMessage ( fileResult . FileName )));
}
if (! string . IsNullOrWhiteSpace ( extraction . Content ))
return extraction . Content ;
this . FinishFileResult ( fileResult , BatchProcessingFileStatus . FAILED , T ( "Was not able to extract any text from this file." ));
return null ;
}
private async Task < 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 )
{
this . FinishFileResult ( fileResult , BatchProcessingFileStatus . CANCELED , T ( "The batch run was canceled." ));
return null ;
}
if ( transcription . Status is not MediaTranscriptionResultStatus . SUCCEEDED )
{
this . FinishFileResult ( fileResult , BatchProcessingFileStatus . FAILED , transcription . UserMessage );
return null ;
}
if ( string . IsNullOrWhiteSpace ( transcription . Text ))
{
this . FinishFileResult ( fileResult , BatchProcessingFileStatus . FAILED , T ( "The transcription provider returned an empty transcript." ));
return null ;
}
return await this . StoreMediaTranscriptAsync ( fileResult , transcriptFilePath , transcription . Text );
}
private async Task < 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 ;
}
catch ( Exception e )
{
this . FinishFileResult ( fileResult , BatchProcessingFileStatus . FAILED , string . Format ( T ( "Was not able to store the transcript next to the media file: {0}" ), e . Message ), e );
return null ;
}
finally
{
try
{
if ( File . Exists ( tempFilePath ))
File . Delete ( tempFilePath );
}
catch ( Exception e )
{
this . Logger . LogWarning ( e , "Was not able to remove the temporary batch transcript '{TempFilePath}'." , tempFilePath );
}
}
}
private static bool IsTranscribableMedia ( string filePath ) => FileTypes . IsAllowedPath ( filePath , FileTypes . AUDIO , FileTypes . VIDEO );
private static string GetTranscriptFilePath ( string mediaFilePath ) => mediaFilePath + TRANSCRIPT_FILE_SUFFIX ;
private static bool HasReusableTranscript ( string mediaFilePath )
{
var transcriptFilePath = GetTranscriptFilePath ( mediaFilePath );
try
{
return File . Exists ( transcriptFilePath ) && new FileInfo ( transcriptFilePath ). Length > 0 ;
}
catch
{
// The concrete read error is reported when the affected file is
// processed. Here we only decide whether a provider is required.
return File . Exists ( transcriptFilePath );
}
}
}