[AdvancedPaste]Add paste actions to allow transcoding of media files (#37188)

* [AdvancedPaste] Additional actions, including Image to text

* Spellcheck issue

* [AdvancedPaste] Paste as file and many other improvements

* Fixed typo

* Fixed typo

* [AdvancedPaste] Improved paste window menu layout

* [AdvancedPaste] Improved settings window layout

* [AdvancedPaste] Removed AudioToText for the moment

* Code cleanup

* Minor fixes

* [AdvancedPaste] Semantic Kernel support

* Changed log-line with potentially sensitive info

* Spellcheck issues

* Various improvements for Semantic Kernel

* Spellcheck issue

* Refactored Clipboard routines

* Added integration tests for KernelService

* Extra telemetry for AdvancedPaste

* Added 'Hotkey' suffix to AdvancedPaste_Settings telemetry event

* Added IsSavedQuery

* Added KernelQueryCache

* Refactoring

* Added KernelQueryCache to BugReportTool delete list

* Added opt-n for Semantic Kernel

* Fixed bug with KernelQueryCache

* Ability to view last AI chat message on error

* Improved kernel query cache

* Used System.IO.Abstractions and improved tests

* Fixed under-count of token usage

* Used Semantic Kernel icon

* Cleanup

* Add missing EndProject line

* Fix dependency version conflicts

* Fix NOTICE.md

* Correct place of SemanticKernel in NOTICE.md

* Unlinked CustomPreview toggle from AI

* Added Microsoft.Bcl.AsyncInterfaces dependency to AdvancedPaste

* Fixed NOTICE.md order

* Moved Custom Preview to behaviour section

* Made Image to Text raise error on empty output

* Added AIServiceBatchIntegrationTests

* Updated AIServiceBatchIntegrationTests

* Added prompt moderation

* [AdvancedPaste] Media Transcoding support

* Spellcheck issue

* Improved transcoding output profile and added tests

* Moved GPO Infobar to better location

* Added cancel button and minor bug fixes

* Fixed crash

* Minor cleanups

* Improved transcoding error messages

* Used software back when transcoding fails with hardware accerlation

* Added Reencode to spellcheck

* Spellcheck issue

---------

Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com>
This commit is contained in:
Ani
2025-02-25 22:33:39 +01:00
committed by GitHub
parent c09a5337c4
commit f263042aeb
40 changed files with 843 additions and 92 deletions

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using AdvancedPaste.Models.KernelQueryCache;

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace AdvancedPaste.UnitTests.Mocks;
internal sealed class NoOpProgress : IProgress<double>
{
public void Report(double value)
{
}
}

View File

@@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -131,17 +132,18 @@ public sealed class AIServiceBatchIntegrationTests
{
VaultCredentialsProvider credentialsProvider = new();
PromptModerationService promptModerationService = new(credentialsProvider);
NoOpProgress progress = new();
CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService);
switch (format)
{
case PasteFormats.CustomTextTransformation:
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard));
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress));
case PasteFormats.KernelQuery:
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false);
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress);
default:
throw new InvalidOperationException($"Unexpected format {format}");

View File

@@ -6,6 +6,7 @@ using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -130,7 +131,7 @@ public sealed class KernelServiceIntegrationTests : IDisposable
private async Task<DataPackageView> GetKernelOutputAsync(string prompt, DataPackage input)
{
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false);
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false, CancellationToken.None, new NoOpProgress());
Assert.AreEqual(1, _eventListener.SemanticKernelEvents.Count);
Assert.IsTrue(_eventListener.SemanticKernelTokens > 0);

View File

@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.UnitTests.Mocks;
using ManagedCommon;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Storage;
using Windows.Storage.FileProperties;
namespace AdvancedPaste.UnitTests.ServicesTests;
[TestClass]
public sealed class TranscodeHelperIntegrationTests
{
private sealed record class MediaProperties(BasicProperties Basic, MusicProperties Music, VideoProperties Video);
private const string InputRootFolder = @"%USERPROFILE%\AdvancedPasteTranscodeMediaTestData";
/// <summary> Tests transforming a folder of media files.
/// - Verifies that the output file has the same basic properties (e.g. duration) as the input file.
/// - Copies the output file to a subfolder of the input folder for manual inspection.
/// </summary>
[TestMethod]
[DataRow(@"audio", PasteFormats.TranscodeToMp3)]
[DataRow(@"video", PasteFormats.TranscodeToMp4)]
public async Task TestTransformFolder(string inputSubfolder, PasteFormats format)
{
var inputFolder = Environment.ExpandEnvironmentVariables(Path.Combine(InputRootFolder, inputSubfolder));
if (!Directory.Exists(inputFolder))
{
Assert.Inconclusive($"Skipping tests for {inputFolder} as it does not exist");
}
var outputPath = Path.Combine(inputFolder, $"test_output_{format}");
foreach (var inputPath in Directory.EnumerateFiles(inputFolder))
{
await RunTestTransformFileAsync(inputPath, outputPath, format);
}
}
private async Task RunTestTransformFileAsync(string inputPath, string finalOutputPath, PasteFormats format)
{
Logger.LogDebug($"Running {nameof(RunTestTransformFileAsync)} for {inputPath}/{format}");
Directory.CreateDirectory(finalOutputPath);
var inputPackage = await DataPackageHelpers.CreateFromFileAsync(inputPath);
var inputProperties = await GetPropertiesAsync(await StorageFile.GetFileFromPathAsync(inputPath));
var outputPackage = await TransformHelpers.TransformAsync(format, inputPackage.GetView(), CancellationToken.None, new NoOpProgress());
var outputItems = await outputPackage.GetView().GetStorageItemsAsync();
Assert.AreEqual(1, outputItems.Count);
var outputFile = outputItems.Single() as StorageFile;
Assert.IsNotNull(outputFile);
var outputProperties = await GetPropertiesAsync(outputFile);
AssertPropertiesMatch(format, inputProperties, outputProperties);
await outputFile.CopyAsync(await StorageFolder.GetFolderFromPathAsync(finalOutputPath), outputFile.Name, NameCollisionOption.ReplaceExisting);
await outputPackage.GetView().TryCleanupAfterDelayAsync(TimeSpan.Zero);
}
private static void AssertPropertiesMatch(PasteFormats format, MediaProperties inputProperties, MediaProperties outputProperties)
{
Assert.IsTrue(outputProperties.Basic.Size > 0);
Assert.AreEqual(inputProperties.Music.Title, outputProperties.Music.Title);
Assert.AreEqual(inputProperties.Music.Album, outputProperties.Music.Album);
Assert.AreEqual(inputProperties.Music.Artist, outputProperties.Music.Artist);
AssertDurationsApproxEqual(inputProperties.Music.Duration, outputProperties.Music.Duration);
if (format == PasteFormats.TranscodeToMp4)
{
Assert.AreEqual(inputProperties.Video.Title, outputProperties.Video.Title);
AssertDurationsApproxEqual(inputProperties.Video.Duration, outputProperties.Video.Duration);
var inputVideoDimensions = GetNormalizedDimensions(inputProperties.Video);
if (inputVideoDimensions != null)
{
Assert.AreEqual(inputVideoDimensions, GetNormalizedDimensions(outputProperties.Video));
}
}
}
private static async Task<MediaProperties> GetPropertiesAsync(StorageFile file) =>
new(await file.GetBasicPropertiesAsync(), await file.Properties.GetMusicPropertiesAsync(), await file.Properties.GetVideoPropertiesAsync());
private static void AssertDurationsApproxEqual(TimeSpan expected, TimeSpan actual) =>
Assert.AreEqual(expected.Ticks, actual.Ticks, delta: TimeSpan.FromSeconds(1).Ticks);
/// <summary>
/// Gets the dimensions of a video, if available. Accounts for the fact that the dimensions may sometimes be swapped.
/// </summary>
private static (uint Width, uint Height)? GetNormalizedDimensions(VideoProperties properties) =>
properties.Width == 0 || properties.Height == 0
? null
: (Math.Max(properties.Width, properties.Height), Math.Min(properties.Width, properties.Height));
}