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:
Thorsten SommerandPaul Koudelka authored and GitHub committed 2026-10-18 12:06:15 +02:00
1 parent 8642ce2d81
commit 752f7d828a
82 files changed
+3231 -302

No files matched your search

@@ -0,0 +1,233 @@
using AIStudio.Settings;
using AIStudio.Tools.Media;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.Tests.Tools.Media;
/// <summary>
/// Checks where data sources keep the transcripts of their audio and video files.
/// </summary>
[TestFixture]
public sealed class DataSourceTranscriptStoreTests
{
private static readonly string DATA_SOURCE_ID = Guid.NewGuid().ToString();
private string rootPath = string.Empty;
private string dataDirectory = string.Empty;
private string userFolder = string.Empty;
[SetUp]
public void CreateFolders()
{
this.rootPath = Path.Combine(Path.GetTempPath(), $"ai-studio-transcript-store-{Guid.NewGuid():N}");
this.dataDirectory = Path.Combine(this.rootPath, "data");
this.userFolder = Path.Combine(this.rootPath, "user");
Directory.CreateDirectory(this.dataDirectory);
Directory.CreateDirectory(this.userFolder);
}
[TearDown]
public void DeleteFolders()
{
if (Directory.Exists(this.rootPath))
Directory.Delete(this.rootPath, true);
}
[Test]
public async Task TheTranscriptLiesBelowTheDataDirectoryOfItsDataSource()
{
var mediaPath = await this.CreateMediaAsync("recording.mp3", [1, 2, 3]);
var slot = await this.Store().ResolveAsync(mediaPath, CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(DataSourceTranscriptStore.IsKey(slot.Key), Is.True);
Assert.That(slot.Path, Is.EqualTo(Path.Join(this.dataDirectory, DataSourceTranscriptStore.FOLDER_NAME, DATA_SOURCE_ID, slot.Key + ".md")));
});
}
[Test]
public async Task TheSameContentFindsTheSameTranscriptWhereverTheFileLies()
{
var original = await this.CreateMediaAsync("recording.mp3", [1, 2, 3]);
var copy = await this.CreateMediaAsync(Path.Combine("moved", "renamed.mp3"), [1, 2, 3]);
File.SetLastWriteTimeUtc(copy, DateTime.UtcNow.AddDays(-3));
var other = await this.CreateMediaAsync("other.mp3", [1, 2, 3, 4]);
var store = this.Store();
var originalSlot = await store.ResolveAsync(original, CancellationToken.None);
var copySlot = await store.ResolveAsync(copy, CancellationToken.None);
var otherSlot = await store.ResolveAsync(other, CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(copySlot, Is.EqualTo(originalSlot));
Assert.That(otherSlot.Key, Is.Not.EqualTo(originalSlot.Key));
});
}
[Test]
public async Task ARenamedRecordingIsPaidForOnlyOnce()
{
var mediaPath = await this.CreateMediaAsync("recording.mp3", [1, 2, 3]);
var provider = new TranscriptionProvider { Id = "test-transcription-provider" };
var calls = 0;
Task<MediaTranscriptionResult> Transcribe(string path, TranscriptionProvider _, CancellationToken __)
{
calls++;
return Task.FromResult(MediaTranscriptionResult.Succeeded("Transcript"));
}
var first = await PersistentMediaTranscript.ReadOrCreateAsync(this.Store(), mediaPath, provider, Transcribe, NullLogger.Instance, CancellationToken.None);
var renamedPath = Path.Combine(this.userFolder, "renamed.mp3");
File.Move(mediaPath, renamedPath);
File.SetLastWriteTimeUtc(renamedPath, DateTime.UtcNow.AddMinutes(5));
var second = await PersistentMediaTranscript.ReadOrCreateAsync(this.Store(), renamedPath, provider, Transcribe, NullLogger.Instance, CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(first.Status, Is.EqualTo(PersistentMediaTranscriptStatus.READY));
Assert.That(second.Status, Is.EqualTo(PersistentMediaTranscriptStatus.READY));
Assert.That(second.Reused, Is.True);
Assert.That(calls, Is.EqualTo(1));
Assert.That(Directory.GetFiles(this.userFolder), Has.Length.EqualTo(1), "The data source wrote into the folder of the user.");
});
}
[Test]
public async Task OnlyTranscriptsAreListedAndDeleted()
{
var store = this.Store();
var key = new string('a', 64);
var transcriptPath = store.GetPath(key);
var folder = Path.GetDirectoryName(transcriptPath)!;
Directory.CreateDirectory(folder);
await File.WriteAllTextAsync(transcriptPath, "Transcript");
await File.WriteAllTextAsync($"{transcriptPath}.{Guid.NewGuid():N}.tmp", "Interrupted");
await File.WriteAllTextAsync(Path.Combine(folder, "notes.md"), "Not ours");
Assert.That(store.ListKeys(), Is.EqualTo(new[] { key }));
store.DeleteAll();
Assert.Multiple(() =>
{
Assert.That(File.Exists(transcriptPath), Is.False);
Assert.That(Directory.GetFiles(folder, "*.tmp"), Is.Empty);
Assert.That(File.Exists(Path.Combine(folder, "notes.md")), Is.True, "A file which the store did not write was deleted.");
});
}
[Test]
public async Task DeletingAllTranscriptsRemovesTheFolderOfTheDataSource()
{
var store = this.Store();
var transcriptPath = store.GetPath(new string('b', 64));
Directory.CreateDirectory(Path.GetDirectoryName(transcriptPath)!);
await File.WriteAllTextAsync(transcriptPath, "Transcript");
store.DeleteAll();
Assert.That(Directory.Exists(Path.GetDirectoryName(transcriptPath)), Is.False);
}
[Test]
public void DeletingADataSourceWithoutTranscriptsWorks()
{
Assert.DoesNotThrow(() => this.Store().DeleteAll());
}
[Test]
public async Task ALinkInTheFolderIsNeitherFollowedNorDeleted()
{
var store = this.Store();
var key = new string('c', 64);
var linkPath = store.GetPath(key);
Directory.CreateDirectory(Path.GetDirectoryName(linkPath)!);
var targetPath = Path.Combine(this.userFolder, "precious.md");
await File.WriteAllTextAsync(targetPath, "Precious");
try
{
File.CreateSymbolicLink(linkPath, targetPath);
}
catch (Exception exception) when (exception is UnauthorizedAccessException or IOException)
{
Assert.Ignore("Creating symbolic links needs a privilege this system does not grant.");
}
var deleted = store.Delete(key);
store.DeleteAll();
Assert.Multiple(() =>
{
Assert.That(deleted, Is.False);
Assert.That(File.ReadAllText(targetPath), Is.EqualTo("Precious"));
});
}
[Test]
public async Task ABackupCopiesOnlyTheTranscriptsOfAllDataSources()
{
var secondId = Guid.NewGuid().ToString();
var firstPath = this.Store().GetPath(new string('d', 64));
var secondPath = new DataSourceTranscriptStore(this.dataDirectory, secondId).GetPath(new string('e', 64));
foreach (var path in new[] { firstPath, secondPath })
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllTextAsync(path, "Transcript");
}
await File.WriteAllTextAsync($"{firstPath}.{Guid.NewGuid():N}.tmp", "Interrupted");
var targetDirectory = Path.Combine(this.rootPath, "copies");
var copied = await DataSourceTranscriptStore.CopyAllAsync(this.dataDirectory, targetDirectory, CancellationToken.None);
var copiedFiles = Directory.GetFiles(targetDirectory, "*", SearchOption.AllDirectories).Select(path => Path.GetRelativePath(targetDirectory, path).Replace('\\', '/'));
Assert.Multiple(() =>
{
Assert.That(copied, Is.EqualTo(2));
Assert.That(copiedFiles, Is.EquivalentTo(new[]
{
$"{DataSourceTranscriptStore.FOLDER_NAME}/{DATA_SOURCE_ID}/{new string('d', 64)}.md",
$"{DataSourceTranscriptStore.FOLDER_NAME}/{secondId}/{new string('e', 64)}.md",
}));
});
}
[Test]
public async Task ABackupWithoutTranscriptsCopiesNothing()
{
Assert.That(await DataSourceTranscriptStore.CopyAllAsync(this.dataDirectory, Path.Combine(this.rootPath, "copies"), CancellationToken.None), Is.Zero);
}
[TestCase("")]
[TestCase("ABCDEF")]
[TestCase("../outside")]
public void ADataSourceIdWhichIsNoGuidIsRejected(string dataSourceId)
{
Assert.Throws<ArgumentException>(() => _ = new DataSourceTranscriptStore(this.dataDirectory, dataSourceId));
}
[TestCase("")]
[TestCase("../../settings")]
[TestCase("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")]
public void AKeyWhichIsNoHashIsRejected(string key)
{
Assert.Throws<ArgumentException>(() => this.Store().GetPath(key));
}
private DataSourceTranscriptStore Store() => new(this.dataDirectory, DATA_SOURCE_ID);
private async Task<string> CreateMediaAsync(string relativePath, byte[] content)
{
var path = Path.Combine(this.userFolder, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllBytesAsync(path, content);
return path;
}
}
@@ -0,0 +1,57 @@
using AIStudio.Tools.Media;
namespace AIStudio.Tests.Tools.Media;
/// <summary>
/// Checks the transcripts which the Batch Processing assistant keeps next to its media files.
/// </summary>
[TestFixture]
public sealed class MediaSidecarTranscriptsTests
{
[TestCase("recording.mp3.transcript.md")]
[TestCase("recording.mp3.transcript.md.0123456789abcdef.tmp")]
[TestCase("RECORDING.MP3.TRANSCRIPT.MD")]
public void GeneratedTranscriptFilesAreRecognized(string fileName)
{
Assert.That(MediaSidecarTranscripts.IsArtifactPath(fileName), Is.True);
}
[TestCase("notes.md")]
[TestCase("recording.mp3")]
[TestCase("recording.mp3.transcript.md.backup")]
public void OtherFilesAreNoTranscripts(string fileName)
{
Assert.That(MediaSidecarTranscripts.IsArtifactPath(fileName), Is.False);
}
[Test]
public async Task TheTranscriptLiesNextToItsMediaFile()
{
var mediaPath = Path.Combine(Path.GetTempPath(), "recording.mp3");
var slot = await MediaSidecarTranscripts.INSTANCE.ResolveAsync(mediaPath, CancellationToken.None);
Assert.That(slot.Path, Is.EqualTo(mediaPath + MediaSidecarTranscripts.FILE_SUFFIX));
}
[Test]
public async Task OnlyATranscriptWithContentCanBeReused()
{
var directoryPath = Path.Combine(Path.GetTempPath(), $"ai-studio-sidecar-transcript-{Guid.NewGuid():N}");
var mediaPath = Path.Combine(directoryPath, "recording.mp3");
Directory.CreateDirectory(directoryPath);
try
{
Assert.That(MediaSidecarTranscripts.HasReusableTranscript(mediaPath), Is.False, "A missing transcript counted as reusable.");
await File.WriteAllTextAsync(MediaSidecarTranscripts.GetFilePath(mediaPath), string.Empty);
Assert.That(MediaSidecarTranscripts.HasReusableTranscript(mediaPath), Is.False, "An empty transcript counted as reusable.");
await File.WriteAllTextAsync(MediaSidecarTranscripts.GetFilePath(mediaPath), "Transcript");
Assert.That(MediaSidecarTranscripts.HasReusableTranscript(mediaPath), Is.True);
}
finally
{
Directory.Delete(directoryPath, true);
}
}
}
@@ -0,0 +1,252 @@
using AIStudio.Settings;
using AIStudio.Tools.Media;
using AIStudio.Tools.Rust;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.Tests.Tools.Media;
/// <summary>
/// Checks the rules which keep a paid transcription from being paid twice.
/// </summary>
[TestFixture]
public sealed class PersistentMediaTranscriptTests
{
private TranscriptionProvider? provider;
private string directoryPath = string.Empty;
private string mediaPath = string.Empty;
[SetUp]
public async Task CreateMediaFile()
{
this.directoryPath = Path.Combine(Path.GetTempPath(), $"ai-studio-persistent-transcript-{Guid.NewGuid():N}");
Directory.CreateDirectory(this.directoryPath);
this.mediaPath = Path.Combine(this.directoryPath, "recording.mp3");
this.provider = new TranscriptionProvider { Id = "test-transcription-provider" };
await File.WriteAllBytesAsync(this.mediaPath, [1, 2, 3]);
}
[TearDown]
public void DeleteMediaFile()
{
if (Directory.Exists(this.directoryPath))
Directory.Delete(this.directoryPath, true);
}
[Test]
public async Task AnExistingTranscriptIsUsedWithoutAskingTheProvider()
{
await File.WriteAllTextAsync(MediaSidecarTranscripts.GetFilePath(this.mediaPath), "Transcript");
var transcriber = new CountingTranscriber(() => MediaTranscriptionResult.Succeeded("New transcript"));
var result = await this.ReadOrCreateAsync(transcriber, this.provider);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.READY));
Assert.That(result.Reused, Is.True);
Assert.That(transcriber.Calls, Is.Zero);
});
}
[Test]
public async Task AnExistingTranscriptIsUsedEvenWithoutAProvider()
{
await File.WriteAllTextAsync(MediaSidecarTranscripts.GetFilePath(this.mediaPath), "Transcript");
var result = await this.ReadOrCreateAsync(new CountingTranscriber(() => MediaTranscriptionResult.Succeeded("New transcript")), null);
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.READY));
}
[Test]
public async Task WithoutATranscriptAndAProviderNothingIsTranscribed()
{
var transcriber = new CountingTranscriber(() => MediaTranscriptionResult.Succeeded("Transcript"));
var result = await this.ReadOrCreateAsync(transcriber, null);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.NO_PROVIDER));
Assert.That(transcriber.Calls, Is.Zero);
});
}
[Test]
public async Task ANewTranscriptIsStoredAndPaidForOnlyOnce()
{
var transcriber = new CountingTranscriber(() => MediaTranscriptionResult.Succeeded("Transcript"));
var first = await this.ReadOrCreateAsync(transcriber, this.provider);
var second = await this.ReadOrCreateAsync(transcriber, this.provider);
Assert.Multiple(() =>
{
Assert.That(first.Status, Is.EqualTo(PersistentMediaTranscriptStatus.READY));
Assert.That(first.Reused, Is.False);
Assert.That(File.ReadAllText(first.TranscriptPath), Is.EqualTo("Transcript"));
Assert.That(second.Reused, Is.True);
Assert.That(transcriber.Calls, Is.EqualTo(1));
Assert.That(Directory.GetFiles(this.directoryPath, "*.tmp"), Is.Empty);
});
}
[Test]
public async Task AnEmptyTranscriptIsNeverStored()
{
var result = await this.ReadOrCreateAsync(new CountingTranscriber(() => MediaTranscriptionResult.Succeeded(" \n ")), this.provider);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.EMPTY));
Assert.That(File.Exists(MediaSidecarTranscripts.GetFilePath(this.mediaPath)), Is.False);
});
}
[Test]
public async Task ATranscriptHoldingOnlyWhiteSpaceIsReplaced()
{
await File.WriteAllTextAsync(MediaSidecarTranscripts.GetFilePath(this.mediaPath), " \n ");
var transcriber = new CountingTranscriber(() => MediaTranscriptionResult.Succeeded("Transcript"));
var result = await this.ReadOrCreateAsync(transcriber, this.provider);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.READY));
Assert.That(transcriber.Calls, Is.EqualTo(1));
Assert.That(File.ReadAllText(result.TranscriptPath), Is.EqualTo("Transcript"));
});
}
[Test]
public async Task AFileWhichChangesDuringItsTranscriptionKeepsNoTranscript()
{
var transcriber = new CountingTranscriber(() =>
{
File.WriteAllBytes(this.mediaPath, [1, 2, 3, 4]);
return MediaTranscriptionResult.Succeeded("Transcript of the old file");
});
var result = await this.ReadOrCreateAsync(transcriber, this.provider);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.SOURCE_CHANGED));
Assert.That(File.Exists(MediaSidecarTranscripts.GetFilePath(this.mediaPath)), Is.False);
});
}
[Test]
public async Task ATranscriptWhichArrivesAfterACancellationIsStillStored()
{
using var cancellation = new CancellationTokenSource();
var transcriber = new CountingTranscriber(() => MediaTranscriptionResult.Succeeded("Paid transcript"), cancellation.Cancel);
var result = await PersistentMediaTranscript.ReadOrCreateAsync(MediaSidecarTranscripts.INSTANCE, this.mediaPath, this.provider, transcriber.TranscribeAsync, NullLogger.Instance, cancellation.Token);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.READY));
Assert.That(File.ReadAllText(MediaSidecarTranscripts.GetFilePath(this.mediaPath)), Is.EqualTo("Paid transcript"));
});
}
[Test]
public async Task ACancelledTranscriptionStoresNothing()
{
var result = await this.ReadOrCreateAsync(new CountingTranscriber(MediaTranscriptionResult.Cancelled), this.provider);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.CANCELLED));
Assert.That(File.Exists(MediaSidecarTranscripts.GetFilePath(this.mediaPath)), Is.False);
});
}
[Test]
public async Task AFailedTranscriptionKeepsTheReasonOfTheMediaPipeline()
{
var result = await this.ReadOrCreateAsync(new CountingTranscriber(() => MediaTranscriptionResult.Failed("The container is damaged.", MediaJobErrorCode.DAMAGED_CONTAINER)), this.provider);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.TRANSCRIPTION_FAILED));
Assert.That(result.ErrorCode, Is.EqualTo(MediaJobErrorCode.DAMAGED_CONTAINER));
Assert.That(result.UserMessage, Is.EqualTo("The container is damaged."));
});
}
[Test]
public async Task AFileTheProviderRefusedIsReportedAsSuch()
{
var result = await this.ReadOrCreateAsync(new CountingTranscriber(() => MediaTranscriptionResult.Rejected("The file is too large.")), this.provider);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.REJECTED));
Assert.That(result.UserMessage, Is.EqualTo("The file is too large."));
Assert.That(File.Exists(MediaSidecarTranscripts.GetFilePath(this.mediaPath)), Is.False);
});
}
[Test]
public async Task ATranscriptWhichCannotBeStoredIsReportedAndLeavesNoTemporaryFile()
{
//
// A file where the folder of the transcript should be: writing fails for every user, root
// included, unlike a folder without write permission.
//
var blockingFilePath = Path.Combine(this.directoryPath, "not-a-folder");
await File.WriteAllTextAsync(blockingFilePath, "occupied");
var location = new FixedLocation(Path.Combine(blockingFilePath, "transcript.md"));
var result = await PersistentMediaTranscript.ReadOrCreateAsync(location, this.mediaPath, this.provider, new CountingTranscriber(() => MediaTranscriptionResult.Succeeded("Transcript")).TranscribeAsync, NullLogger.Instance, CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.STORE_FAILED));
Assert.That(Directory.GetFiles(this.directoryPath, "*.tmp", SearchOption.AllDirectories), Is.Empty);
});
}
[Test]
public async Task AMissingMediaFileIsReportedWithoutAskingTheProvider()
{
File.Delete(this.mediaPath);
var transcriber = new CountingTranscriber(() => MediaTranscriptionResult.Succeeded("Transcript"));
var result = await this.ReadOrCreateAsync(transcriber, this.provider);
Assert.Multiple(() =>
{
Assert.That(result.Status, Is.EqualTo(PersistentMediaTranscriptStatus.SOURCE_UNREADABLE));
Assert.That(transcriber.Calls, Is.Zero);
});
}
private Task<PersistentMediaTranscriptResult> ReadOrCreateAsync(CountingTranscriber transcriber, TranscriptionProvider? transcriptionProvider) => PersistentMediaTranscript.ReadOrCreateAsync(MediaSidecarTranscripts.INSTANCE, this.mediaPath, transcriptionProvider, transcriber.TranscribeAsync, NullLogger.Instance, CancellationToken.None);
/// <summary>
/// Stands in for the provider and counts how often it was asked.
/// </summary>
/// <param name="answer">Creates the answer of the provider.</param>
/// <param name="whileAnswering">Runs while the provider is answering, e.g., to cancel the run in the middle of an upload.</param>
private sealed class CountingTranscriber(Func<MediaTranscriptionResult> answer, Action? whileAnswering = null)
{
public int Calls { get; private set; }
public Task<MediaTranscriptionResult> TranscribeAsync(string mediaPath, TranscriptionProvider provider, CancellationToken token)
{
this.Calls++;
whileAnswering?.Invoke();
return Task.FromResult(answer());
}
}
private sealed class FixedLocation(string transcriptPath) : IMediaTranscriptLocation
{
public Task<MediaTranscriptSlot> ResolveAsync(string mediaPath, CancellationToken token) => Task.FromResult(new MediaTranscriptSlot(transcriptPath, "fixed"));
}
}
@@ -0,0 +1,110 @@
using AIStudio.Tools.Media;
using AIStudio.Tools.Services;
using AIStudio.Tools.Services.Indexing;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.Tests.Tools.Media;
/// <summary>
/// Checks that the transcript of a recording leaves the data source together with the recording.
/// </summary>
/// <remarks>
/// A transcript holds what was said. Once its recording is gone from the data source, keeping it
/// below the data directory would keep that text, unseen by the user.
/// </remarks>
[TestFixture]
public sealed class TranscriptCleanupTests
{
private static readonly string KEY_A = new('a', 64);
private static readonly string KEY_B = new('b', 64);
private static readonly string KEY_C = new('c', 64);
private string dataDirectory = string.Empty;
private DataSourceTranscriptStore store = null!;
[SetUp]
public async Task CreateTranscripts()
{
this.dataDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-transcript-cleanup-{Guid.NewGuid():N}");
this.store = new DataSourceTranscriptStore(this.dataDirectory, Guid.NewGuid().ToString());
foreach (var key in new[] { KEY_A, KEY_B, KEY_C })
{
var path = this.store.GetPath(key);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllTextAsync(path, $"Transcript {key[0]}");
}
}
[TearDown]
public void DeleteTranscripts()
{
if (Directory.Exists(this.dataDirectory))
Directory.Delete(this.dataDirectory, true);
}
[Test]
public void TheTranscriptOfARemovedRecordingIsDeleted()
{
var manifest = Manifest(("/data/kept.mp3", KEY_A));
FileSourceIndexer.ForgetTranscript(this.store, manifest, KEY_B, NullLogger.Instance);
Assert.That(this.store.ListKeys(), Is.EquivalentTo(new[] { KEY_A, KEY_C }));
}
[Test]
public void ATranscriptWhichAnotherRecordingSharesIsKept()
{
var manifest = Manifest(("/data/copy-of-the-removed-recording.mp3", KEY_B));
FileSourceIndexer.ForgetTranscript(this.store, manifest, KEY_B, NullLogger.Instance);
Assert.That(this.store.ListKeys(), Does.Contain(KEY_B), "Two recordings with the same content share one transcript.");
}
[Test]
public void AFileWithoutTranscriptDeletesNothing()
{
FileSourceIndexer.ForgetTranscript(this.store, new DataSourceEmbeddingManifest(), null, NullLogger.Instance);
Assert.That(this.store.ListKeys(), Has.Count.EqualTo(3));
}
[Test]
public async Task ACompleteRunDeletesWhatNoFileUses()
{
var temporaryPath = $"{this.store.GetPath(KEY_A)}.{Guid.NewGuid():N}.tmp";
await File.WriteAllTextAsync(temporaryPath, "Interrupted");
var manifest = Manifest(("/data/meeting.mp3", KEY_A), ("/data/report.pdf", null));
FileSourceIndexer.DeleteUnusedTranscripts(this.store, manifest, NullLogger.Instance);
Assert.Multiple(() =>
{
Assert.That(this.store.ListKeys(), Is.EqualTo(new[] { KEY_A }));
Assert.That(File.Exists(temporaryPath), Is.False, "A temporary file of an interrupted write is no transcript of anything.");
});
}
[Test]
public void WithoutAStoreNothingHappens()
{
Assert.DoesNotThrow(() =>
{
FileSourceIndexer.DeleteUnusedTranscripts(null, new DataSourceEmbeddingManifest(), NullLogger.Instance);
FileSourceIndexer.ForgetTranscript(null, new DataSourceEmbeddingManifest(), KEY_A, NullLogger.Instance);
});
}
private static DataSourceEmbeddingManifest Manifest(params (string Path, string? TranscriptKey)[] files)
{
var manifest = new DataSourceEmbeddingManifest();
foreach (var (path, transcriptKey) in files)
manifest.Files[path] = new EmbeddedFileRecord("fingerprint", 16, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, 1, transcriptKey);
return manifest;
}
}