Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ obj/
*.suo
/*.sln
*.sln
/*.slnx
*.slnx
*.user
*.unityproj
*.ipch
Expand Down
4 changes: 2 additions & 2 deletions OpenAI/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ csharp_new_line_before_finally = true
csharp_new_line_before_open_brace = all

# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:error
dotnet_style_require_accessibility_modifiers = error

# Code-block preferences
csharp_prefer_braces = true:error
Expand All @@ -33,8 +33,8 @@ dotnet_style_predefined_type_for_locals_parameters_members = true

# Code Style
csharp_style_var_when_type_is_apparent = true

dotnet_sort_system_directives_first = false
dotnet_analyzer_diagnostic.category-Style.severity = none

#### Resharper/Rider Rules ####
# https://www.jetbrains.com/help/resharper/EditorConfig_Properties.html
Expand Down
38 changes: 17 additions & 21 deletions OpenAI/Packages/com.openai.unity/Runtime/Audio/AudioEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,21 @@ public async Task<Tuple<string, AudioClip>> CreateSpeechAsync(SpeechRequest requ
[Obsolete("use GetSpeechAsync with Func<SpeechClip, Task> overload")]
public async Task<Tuple<string, AudioClip>> CreateSpeechStreamAsync(SpeechRequest request, Action<AudioClip> partialClipCallback, CancellationToken cancellationToken = default)
{
using var result = await GetSpeechAsync(request, speechClip =>
using var result = await GetSpeechAsync(request, async speechClip =>
{
partialClipCallback.Invoke(speechClip.AudioClip);
await Task.CompletedTask;
}, cancellationToken);
return Tuple.Create(result.CachePath, result.AudioClip);
}

[Obsolete("use GetSpeechAsync with Func<SpeechClip, Task> overload")]
public async Task<SpeechClip> GetSpeechAsync(SpeechRequest request, Action<SpeechClip> partialClipCallback, CancellationToken cancellationToken = default)
{
return await GetSpeechAsync(request, partialClipCallback: clip =>
=> await GetSpeechAsync(request, partialClipCallback: async clip =>
{
partialClipCallback?.Invoke(clip);
return Task.CompletedTask;
await Task.CompletedTask;
}, cancellationToken);
}

/// <summary>
/// Generates audio from the input text.
Expand All @@ -61,6 +60,11 @@ public async Task<SpeechClip> GetSpeechAsync(SpeechRequest request, Action<Speec
[Function("Generates audio from the input text.")]
public async Task<SpeechClip> GetSpeechAsync(SpeechRequest request, Func<SpeechClip, Task> partialClipCallback = null, CancellationToken cancellationToken = default)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}

if (partialClipCallback != null && request.ResponseFormat != SpeechResponseFormat.PCM)
{
Debug.LogWarning("Speech streaming only supported with PCM response format. Overriding to PCM...");
Expand All @@ -77,7 +81,7 @@ public async Task<SpeechClip> GetSpeechAsync(SpeechRequest request, Func<SpeechC
var payload = JsonConvert.SerializeObject(request, OpenAIClient.JsonSerializationOptions);
string clipName;

if (string.IsNullOrEmpty(request?.Voice))
if (string.IsNullOrEmpty(request.Voice))
{
throw new ArgumentNullException(nameof(request.Voice));
}
Expand All @@ -89,7 +93,7 @@ public async Task<SpeechClip> GetSpeechAsync(SpeechRequest request, Func<SpeechC
clipName = $"{voice}-{DateTime.UtcNow:yyyyMMddThhmmssfffff}.{ext}";
}

Rest.TryGetDownloadCacheItem(clipName, out var cachedPath);
var cachePath = Rest.GetCacheItemPath(clipName);

switch (request.ResponseFormat)
{
Expand All @@ -101,15 +105,7 @@ public async Task<SpeechClip> GetSpeechAsync(SpeechRequest request, Func<SpeechC
if (partialClipCallback != null && partialResponse.Data.Length > 0)
{
var partialClip = new SpeechClip($"{clipName}_{++part}", null, partialResponse.Data);

try
{
await partialClipCallback(partialClip).ConfigureAwait(false);
}
finally
{
partialClip.Dispose();
}
await partialClipCallback(partialClip).ConfigureAwait(true);
}
}, 8192, new RestParameters(client.DefaultRequestHeaders, debug: EnableDebug), cancellationToken);
pcmResponse.Validate(EnableDebug);
Expand All @@ -119,17 +115,17 @@ public async Task<SpeechClip> GetSpeechAsync(SpeechRequest request, Func<SpeechC
throw new Exception("No audio data received!");
}

await File.WriteAllBytesAsync(cachedPath, pcmResponse.Data, cancellationToken).ConfigureAwait(true);
return new SpeechClip(clipName, cachedPath, pcmResponse.Data);
await File.WriteAllBytesAsync(cachePath, pcmResponse.Data, cancellationToken).ConfigureAwait(true);
return new SpeechClip(clipName, cachePath, pcmResponse.Data);
}
default:
{
var audioResponse = await Rest.PostAsync(GetUrl("/speech"), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken);
audioResponse.Validate(EnableDebug);
await File.WriteAllBytesAsync(cachedPath, audioResponse.Data, cancellationToken).ConfigureAwait(true);
await File.WriteAllBytesAsync(cachePath, audioResponse.Data, cancellationToken).ConfigureAwait(true);
var audioType = request.ResponseFormat == SpeechResponseFormat.MP3 ? AudioType.MPEG : AudioType.WAV;
var finalClip = await Rest.DownloadAudioClipAsync(cachedPath, audioType, fileName: clipName, parameters: new RestParameters(debug: EnableDebug), cancellationToken: cancellationToken);
return new SpeechClip(clipName, cachedPath, finalClip);
var finalClip = await Rest.DownloadAudioClipAsync(cachePath, audioType, fileName: clipName, parameters: new RestParameters(debug: EnableDebug), cancellationToken: cancellationToken);
return new SpeechClip(clipName, cachePath, finalClip);
}
}
}
Expand Down
24 changes: 19 additions & 5 deletions OpenAI/Packages/com.openai.unity/Runtime/Audio/SpeechClip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ namespace OpenAI.Audio
[Preserve]
public sealed class SpeechClip : IDisposable
{
[Preserve]
private SpeechClip() { }

[Preserve]
internal SpeechClip(string name, string cachePath, AudioClip audioClip)
{
Expand All @@ -30,7 +33,8 @@ internal SpeechClip(string name, string cachePath, byte[] audioData, int sampleR
SampleRate = sampleRate;
}

~SpeechClip() => Dispose();
[Preserve]
~SpeechClip() => Dispose(false);

[Preserve]
public string Name { get; }
Expand Down Expand Up @@ -112,13 +116,23 @@ public float Length
[Preserve]
public static implicit operator string(SpeechClip clip) => clip?.CachePath;

[Preserve]
private void Dispose(bool disposing)
{
if (disposing)
{
audioSamples?.Dispose();
audioSamples = null;
audioData?.Dispose();
audioData = null;
}
}

[Preserve]
public void Dispose()
{
audioSamples?.Dispose();
audioSamples = null;
audioData?.Dispose();
audioData = null;
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ private void Dispose(bool disposing)
if (disposing)
{
audioSamples?.Dispose();
AudioData.Dispose();
audioSamples = null;
audioData?.Dispose();
audioData = null;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,94 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Unity.Collections;
using UnityEngine;
using Utilities.Extensions;
using OpenAI.Images;
using Utilities.Async;
using System;
using Utilities.WebRequestRest;

#if !PLATFORM_WEBGL
using System.IO;
#endif

namespace OpenAI.Extensions
{
internal static class TextureExtensions
public static class TextureExtensions
{
public static async Task<(Texture2D, string)> ConvertFromBase64Async(string b64, bool debug, CancellationToken cancellationToken)
internal static async Task<(Texture2D, Uri)> ConvertFromBase64Async(string b64, bool debug, CancellationToken cancellationToken)
{
var imageData = Convert.FromBase64String(b64);
using var imageData = NativeArrayExtensions.FromBase64String(b64, Allocator.Persistent);
#if PLATFORM_WEBGL
var texture = new Texture2D(2, 2);
#if UNITY_6000_0_OR_NEWER
texture.LoadImage(imageData);
return await Task.FromResult((texture, string.Empty));
#else
if (!Rest.TryGetDownloadCacheItem(b64, out var localFilePath))
texture.LoadImage(imageData.ToArray());
#endif // UNITY_6000_0_OR_NEWER
return await Task.FromResult((texture, null as Uri));
#else
if (!Rest.TryGetDownloadCacheItem(b64, out Uri localUri))
{
await File.WriteAllBytesAsync(localFilePath, imageData, cancellationToken).ConfigureAwait(true);
localFilePath = $"file://{localFilePath}";
await using var fs = new FileStream(localUri.LocalPath, FileMode.Create, FileAccess.Write);
await fs.WriteAsync(imageData, cancellationToken: cancellationToken);
}

var texture = await Rest.DownloadTextureAsync(localFilePath, parameters: new RestParameters(debug: debug), cancellationToken: cancellationToken);
Rest.TryGetDownloadCacheItem(b64, out var cachedPath);
return (texture, cachedPath);
#endif
var texture = await Rest.DownloadTextureAsync(localUri.LocalPath, parameters: new RestParameters(debug: debug), cancellationToken: cancellationToken);
Rest.TryGetDownloadCacheItem(b64, out Uri cachedUri);
return (texture, cachedUri);
#endif // !PLATFORM_WEBGL
}

/// <summary>
/// Loads a Texture2D from an ImageResult, handling base64, cached path, or URL.
/// </summary>
/// <param name="imageResult">The <see cref="ImageResult"/> to load the texture for.</param>
/// <param name="debug">Optional, debug flag.</param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns>
/// A tuple containing the converted <see cref="Texture2D"/> and the cached file path as a <see cref="Uri"/>.
/// </returns>
public static async Task<(Texture2D, Uri)> LoadTextureAsync(this ImageResult imageResult, bool debug = false, CancellationToken cancellationToken = default)
{
await Awaiters.UnityMainThread;

if (imageResult.Texture.IsNull())
{
if (!string.IsNullOrWhiteSpace(imageResult.B64_Json))
{
var (texture, cachedUri) = await ConvertFromBase64Async(imageResult.B64_Json, debug, cancellationToken);
imageResult.Texture = texture;
imageResult.CachedPathUri = cachedUri;
}
else
{
Texture2D texture;
Uri cachedPath;

if (imageResult.CachedPathUri != null)
{
texture = await Rest.DownloadTextureAsync(imageResult.CachedPathUri, parameters: new RestParameters(debug: debug), cancellationToken: cancellationToken);
cachedPath = imageResult.CachedPathUri;
}
else if (imageResult.Uri != null)
{
texture = await Rest.DownloadTextureAsync(imageResult.Uri, parameters: new RestParameters(debug: debug), cancellationToken: cancellationToken);
cachedPath = Rest.TryGetDownloadCacheItem(imageResult.Uri, out var path) ? path : null;
}
else
{
throw new InvalidOperationException("ImageResult does not contain valid image data.");
}

imageResult.Texture = texture;
imageResult.CachedPathUri = cachedPath;
}
}

return (imageResult.Texture, imageResult.CachedPathUri);
}
}
}
21 changes: 18 additions & 3 deletions OpenAI/Packages/com.openai.unity/Runtime/Images/ImageResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ internal ImageResult(
[JsonProperty("revised_prompt")] string revisedPrompt)
{
Url = url;

if (!string.IsNullOrWhiteSpace(url))
{
Uri = new Uri(url);
}

B64_Json = b64_json;
RevisedPrompt = revisedPrompt;
}
Expand All @@ -26,6 +32,10 @@ internal ImageResult(
[JsonProperty("url", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Url { get; private set; }

[Preserve]
[JsonIgnore]
public Uri Uri { get; }

[Preserve]
[JsonProperty("b64_json", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string B64_Json { get; private set; }
Expand Down Expand Up @@ -56,7 +66,12 @@ internal ImageResult(

[Preserve]
[JsonIgnore]
public string CachedPath { get; internal set; }
[Obsolete("use CachedPathUri")]
public string CachedPath => CachedPathUri?.ToString();

[Preserve]
[JsonIgnore]
public Uri CachedPathUri { get; internal set; }

[Preserve]
[JsonIgnore]
Expand All @@ -75,9 +90,9 @@ internal ImageResult(
[Preserve]
public override string ToString()
{
if (!string.IsNullOrWhiteSpace(CachedPath))
if (CachedPathUri != null)
{
return CachedPath;
return CachedPathUri.ToString();
}

if (!string.IsNullOrWhiteSpace(B64_Json))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using Utilities.Async;
using Utilities.WebRequestRest;

namespace OpenAI.Images
Expand Down Expand Up @@ -180,33 +179,15 @@ private async Task<IReadOnlyList<ImageResult>> DeserializeResponseAsync(Response
}

await Rest.ValidateCacheDirectoryAsync();
var downloads = imagesResponse.Results.Select(DownloadAsync).ToList();

async Task DownloadAsync(ImageResult result)
{
await Awaiters.UnityMainThread;

if (string.IsNullOrWhiteSpace(result.Url))
{
var (texture, cachePath) = await TextureExtensions.ConvertFromBase64Async(result.B64_Json, EnableDebug, cancellationToken);
result.Texture = texture;
result.CachedPath = cachePath;
}
else
{
result.Texture = await Rest.DownloadTextureAsync(result.Url, parameters: new RestParameters(debug: EnableDebug), cancellationToken: cancellationToken);

if (Rest.TryGetDownloadCacheItem(result.Url, out var cachedPath))
{
result.CachedPath = cachedPath;
}
}
}
Task<(Texture2D, Uri)> DownloadAsync(ImageResult result)
=> result.LoadTextureAsync(debug: EnableDebug, cancellationToken);

await Task.WhenAll(downloads).ConfigureAwait(true);
await Task.WhenAll(imagesResponse.Results.Select(DownloadAsync).ToList()).ConfigureAwait(true);

foreach (var result in imagesResponse.Results)
for (var i = 0; i < imagesResponse.Results.Count; i++)
{
var result = imagesResponse.Results[i];
result.CreatedAt = DateTimeOffset.FromUnixTimeSeconds(imagesResponse.CreatedAtUnixSeconds).UtcDateTime;
result.Background = imagesResponse.Background;
result.OutputFormat = imagesResponse.OutputFormat;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal Message(

[Preserve]
public Message(Role role, string text)
: this(role, new TextContent(text))
: this(role, new TextContent(text, role == Role.Assistant ? ResponseContentType.OutputText : ResponseContentType.InputText))
{
}

Expand Down
Loading
Loading