Skip to content

Commit

Permalink
Merge pull request #6 from Hekku2/image-analyzer
Browse files Browse the repository at this point in the history
Add support for cognitive services
  • Loading branch information
Hekku2 authored Jun 28, 2024
2 parents 89f9aa8 + 8ccdfca commit 76d3ab7
Show file tree
Hide file tree
Showing 18 changed files with 351 additions and 132 deletions.
13 changes: 13 additions & 0 deletions infra/functions.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ param keyVaultName string
@description('The name of the identity that will be used by the function app.')
param identityName string

@description('Cognitive services account name.')
param cognitiveServicesAccountName string

var hostingPlanName = 'asp-${baseName}'
var functionAppName = 'func-${baseName}'
var storageBlobDataOwnerRoleDefinitionId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'
Expand All @@ -43,6 +46,11 @@ resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-p
scope: resourceGroup()
}

resource cognitiveServiceAccount 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = {
name: cognitiveServicesAccountName
scope: resourceGroup()
}

resource functionStorageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {
name: 'st${uniqueString(resourceGroup().id)}'
location: location
Expand Down Expand Up @@ -80,6 +88,7 @@ resource hostingPlan 'Microsoft.Web/serverfarms@2021-02-01' = {
var discordSettingsKey = 'DiscordConfiguration'
var blobStorageKey = 'BlobStorageImageSourceOptions'
var imageIndexStorageKey = 'ImageIndexOptions'
var imageAnalysisKey = 'ImageAnalysisConfiguration'

resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
kind: 'linux,functionapp'
Expand Down Expand Up @@ -181,6 +190,10 @@ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
name: '${imageIndexStorageKey}__BlobContainerUri'
value: '${functionStorageAccount.properties.primaryEndpoints.blob}index'
}
{
name: '${imageAnalysisKey}__Endpoint'
value: cognitiveServiceAccount.properties.endpoint
}
]
}
}
Expand Down
51 changes: 51 additions & 0 deletions infra/image-analyzer.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@minLength(5)
param baseName string

@description('Location for all resources.')
param location string = resourceGroup().location

@description('Identities of which are assigned with Blob Data Reader to the containers.')
param cognitiveServiceUserIdentityNames string[]

var cognitiveServicesUserRoleDefinitionId = subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'a97b65f3-24c7-4388-baec-2e87135dc908'
)

resource identities 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' existing = [
for identityName in cognitiveServiceUserIdentityNames: {
name: identityName
scope: resourceGroup()
}
]

var cognitiveServiceName = 'aisa-${baseName}'
resource cognitiveServiceAccount 'Microsoft.CognitiveServices/accounts@2023-05-01' = {
name: cognitiveServiceName
location: location
kind: 'ComputerVision'
identity: {
type: 'SystemAssigned'
}
sku: {
name: 'F0'
}
properties: {
publicNetworkAccess: 'Enabled'
customSubDomainName: cognitiveServiceName
}
}

resource cognitiveServiceAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
for (identityName, i) in cognitiveServiceUserIdentityNames: {
scope: cognitiveServiceAccount
name: guid(identities[i].id, cognitiveServicesUserRoleDefinitionId, cognitiveServiceAccount.id)
properties: {
principalId: identities[i].properties.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: cognitiveServicesUserRoleDefinitionId
}
}
]

output cognitiveServiceAccountName string = cognitiveServiceAccount.name
12 changes: 12 additions & 0 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ var imageSettings = {
folderPath: 'root'
}

module imageAnalyzerModule 'image-analyzer.bicep' = {
name: 'image-analyzer'
params: {
location: location
baseName: baseName
cognitiveServiceUserIdentityNames: [
functionAppIdentity.name
]
}
}

module functions 'functions.bicep' = {
name: 'functions'
params: {
Expand All @@ -81,5 +92,6 @@ module functions 'functions.bicep' = {
webSitePackageLocation: webSitePackageLocation
disableDiscordSending: disableDiscordSending
identityName: functionAppIdentity.name
cognitiveServicesAccountName: imageAnalyzerModule.outputs.cognitiveServiceAccountName
}
}
1 change: 1 addition & 0 deletions src/Common/Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.Vision.ImageAnalysis" Version="1.0.0-beta.3" />
<PackageReference Include="Azure.Identity" Version="1.12.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.20.0" />
<PackageReference Include="Discord.Net.Rest" Version="3.15.2" />
Expand Down
12 changes: 6 additions & 6 deletions src/Common/Discord/IDiscordImagePoster.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace DiscordImagePoster.Common.Discord;

public interface IDiscordImagePoster
{
Task SendImage(Stream stream, string fileName, string? description);
}
namespace DiscordImagePoster.Common.Discord;

public interface IDiscordImagePoster
{
Task SendImage(Stream stream, string fileName, string? description);
}
36 changes: 24 additions & 12 deletions src/Common/Discord/NoOpDiscordImagePoster.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
namespace DiscordImagePoster.Common.Discord;

/// <summary>
/// A no-op implementation of <see cref="IDiscordImagePoster"/>.
/// </summary>
public class NoOpDiscordImagePoster : IDiscordImagePoster
{
public Task SendImage(Stream stream, string fileName, string? description)
{
return Task.CompletedTask;
}
}
using Microsoft.Extensions.Logging;

namespace DiscordImagePoster.Common.Discord;

/// <summary>
/// A no-op implementation of <see cref="IDiscordImagePoster"/>.
/// </summary>
public class NoOpDiscordImagePoster : IDiscordImagePoster
{
private readonly ILogger<NoOpDiscordImagePoster> _logger;

public NoOpDiscordImagePoster(
ILogger<NoOpDiscordImagePoster> logger
)
{
_logger = logger;
}

public Task SendImage(Stream stream, string fileName, string? description)
{
_logger.LogWarning("Discord sending is disabled. Filename was {fileName} with description {description}.", fileName, description);
return Task.CompletedTask;
}
}
14 changes: 14 additions & 0 deletions src/Common/ImageAnalysis/IImageAnalysisService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace DiscordImagePoster.Common.ImageAnalysis;

/// <summary>
/// Service for analyzing images.
/// </summary>
public interface IImageAnalysisService
{
/// <summary>
/// Analyzes the image in stream.
/// </summary>
/// <param name="stream">The stream containing the image.</param>
/// <returns>The analysis results.</returns>
Task<ImageAnalysisResults> AnalyzeImageAsync(Stream stream);
}
13 changes: 13 additions & 0 deletions src/Common/ImageAnalysis/ImageAnalysisConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace DiscordImagePoster.Common.ImageAnalysis;

/// <summary>
/// Configuration for image analysis service.
/// </summary>
public class ImageAnalysisConfiguration
{
/// <summary>
/// The endpoint of the image analysis service.
/// For example https://service-name-here.cognitiveservices.azure.com/
/// </summary>
public required string Endpoint { get; set; }
}
17 changes: 17 additions & 0 deletions src/Common/ImageAnalysis/ImageAnalysisResults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace DiscordImagePoster.Common.ImageAnalysis;

/// <summary>
/// Result of image analysis. Basically a subset of the data received from the service.
/// </summary>
public class ImageAnalysisResults
{
/// <summary>
/// The caption of the image.
/// </summary>
public required string Caption { get; set; }

/// <summary>
/// The tags of the image.
/// </summary>
public required string[] Tags { get; set; }
}
38 changes: 38 additions & 0 deletions src/Common/ImageAnalysis/ImageAnalysisService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Text.Json;
using Azure.AI.Vision.ImageAnalysis;
using Microsoft.Extensions.Logging;

namespace DiscordImagePoster.Common.ImageAnalysis;

/// <summary>
/// Image analysis service using Azure Cognitive Services.
/// </summary>
public class ImageAnalysisService : IImageAnalysisService
{
private readonly ILogger<ImageAnalysisService> _logger;
private readonly ImageAnalysisClient _imageAnalysisClient;

public ImageAnalysisService(
ILogger<ImageAnalysisService> logger,
ImageAnalysisClient imageAnalysisClient)
{
_logger = logger;
_imageAnalysisClient = imageAnalysisClient;
}

/// <inheritdoc/>
public async Task<ImageAnalysisResults> AnalyzeImageAsync(Stream stream)
{
_logger.LogDebug("Analyzing image...");
var result = await _imageAnalysisClient.AnalyzeAsync(BinaryData.FromStream(stream), VisualFeatures.Caption | VisualFeatures.Tags);
var tags = result.Value.Tags.Values.Select(tag => $"{tag.Name} {tag.Confidence}").ToArray();
_logger.LogDebug("Image analysis result with caption: {result}, and tags {tags}", result.Value.Caption.Text, tags);
_logger.LogTrace("Image analysis result: {result}", JsonSerializer.Serialize(result));

return new ImageAnalysisResults
{
Caption = result.Value.Caption.Text,
Tags = result.Value.Tags.Values.Select(tag => tag.Name).ToArray()
};
}
}
100 changes: 50 additions & 50 deletions src/Common/IndexService/BlobStorageIndexStorageService.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
using System.Text.Json;
using Azure.Storage.Blobs;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace DiscordImagePoster.Common.IndexService;

public class BlobStorageIndexStorageService : IIndexStorageService
{
private readonly ILogger<BlobStorageIndexStorageService> _logger;
private readonly ImageIndexOptions _options;
private readonly BlobContainerClient _blobContainerClient;

public BlobStorageIndexStorageService
(
ILogger<BlobStorageIndexStorageService> logger,
IOptions<ImageIndexOptions> options,
[FromKeyedServices(KeyedServiceConstants.ImageIndexBlobContainerClient)] BlobContainerClient blobContainerClient
)
{
_logger = logger;
_options = options.Value;
_blobContainerClient = blobContainerClient;
}

public async Task<ImageIndex?> GetImageIndexAsync()
{
_logger.LogTrace("Getting image index from {IndexFileName}", _options.IndexFileName);
var blobClient = _blobContainerClient.GetBlobClient(_options.IndexFileName);
var exists = await blobClient.ExistsAsync();
if (!exists)
{
_logger.LogWarning("Index blob not found.");
return null;
}

using var stream = await blobClient.OpenReadAsync();
return await JsonSerializer.DeserializeAsync<ImageIndex>(stream);
}

public async Task UpdateIndexAsync(ImageIndex index)
{
_logger.LogTrace("Updating image index to {IndexFileName}", _options.IndexFileName);
await _blobContainerClient.CreateIfNotExistsAsync();
var bytes = JsonSerializer.SerializeToUtf8Bytes(index);
var blobClient = _blobContainerClient.GetBlobClient(_options.IndexFileName);
await blobClient.UploadAsync(new MemoryStream(bytes), true);
}
}
using System.Text.Json;
using Azure.Storage.Blobs;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace DiscordImagePoster.Common.IndexService;

public class BlobStorageIndexStorageService : IIndexStorageService
{
private readonly ILogger<BlobStorageIndexStorageService> _logger;
private readonly ImageIndexOptions _options;
private readonly BlobContainerClient _blobContainerClient;

public BlobStorageIndexStorageService
(
ILogger<BlobStorageIndexStorageService> logger,
IOptions<ImageIndexOptions> options,
[FromKeyedServices(KeyedServiceConstants.ImageIndexBlobContainerClient)] BlobContainerClient blobContainerClient
)
{
_logger = logger;
_options = options.Value;
_blobContainerClient = blobContainerClient;
}

public async Task<ImageIndex?> GetImageIndexAsync()
{
_logger.LogTrace("Getting image index from {IndexFileName}", _options.IndexFileName);
var blobClient = _blobContainerClient.GetBlobClient(_options.IndexFileName);
var exists = await blobClient.ExistsAsync();
if (!exists)
{
_logger.LogWarning("Index blob not found.");
return null;
}

using var stream = await blobClient.OpenReadAsync();
return await JsonSerializer.DeserializeAsync<ImageIndex>(stream);
}

public async Task UpdateIndexAsync(ImageIndex index)
{
_logger.LogTrace("Updating image index to {IndexFileName}", _options.IndexFileName);
await _blobContainerClient.CreateIfNotExistsAsync();
var bytes = JsonSerializer.SerializeToUtf8Bytes(index);
var blobClient = _blobContainerClient.GetBlobClient(_options.IndexFileName);
await blobClient.UploadAsync(new MemoryStream(bytes), true);
}
}
5 changes: 3 additions & 2 deletions src/Common/IndexService/IIndexService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public interface IIndexService
Task<ImageIndex> RefreshIndexAsync();

/// <summary>
/// Increases the posting count for an image.
/// Increases the posting count for an image and updates image metadata.
/// </summary>
Task IncreasePostingCountAsync(string imagePath);
/// <param name="imageMetadataUpdate">The image metadata update.</param>
Task IncreasePostingCountAndUpdateMetadataAsync(ImageMetadataUpdate imageMetadataUpdate);
}
Loading

0 comments on commit 76d3ab7

Please sign in to comment.