Initial import into Gitea
This commit is contained in:
230
Creative/Services/CreativeService.cs
Normal file
230
Creative/Services/CreativeService.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using System.Text.Json;
|
||||
using Creative.Models;
|
||||
|
||||
namespace Creative.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Main creative service - dispatches operations to appropriate handlers.
|
||||
/// Stateless: returns JSON, Gateway handles persistence.
|
||||
/// </summary>
|
||||
public class CreativeService
|
||||
{
|
||||
private readonly ScraperService _scraper;
|
||||
private readonly CopyGeneratorService _copyGen;
|
||||
private readonly ImageGeneratorService _imageGen;
|
||||
private readonly ILogger<CreativeService> _logger;
|
||||
|
||||
public CreativeService(
|
||||
ScraperService scraper,
|
||||
CopyGeneratorService copyGen,
|
||||
ImageGeneratorService imageGen,
|
||||
ILogger<CreativeService> logger)
|
||||
{
|
||||
_scraper = scraper;
|
||||
_copyGen = copyGen;
|
||||
_imageGen = imageGen;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main dispatch method - routes to appropriate operation handler.
|
||||
/// </summary>
|
||||
public async Task<CreativeResponse> ExecuteAsync(CreativeRequest request, CancellationToken ct)
|
||||
{
|
||||
var requestId = request.RequestId ?? Guid.NewGuid().ToString("N");
|
||||
var operation = (request.Operation ?? "").Trim();
|
||||
|
||||
_logger.LogInformation("[Creative] Executing {Operation} | RequestId={RequestId}",
|
||||
operation, requestId);
|
||||
|
||||
try
|
||||
{
|
||||
return operation switch
|
||||
{
|
||||
"Ping" => Ping(requestId),
|
||||
"AnalyzeUrl" => await AnalyzeUrlAsync(request, requestId, ct),
|
||||
"GenerateAssets" => await GenerateAssetsAsync(request, requestId, ct),
|
||||
"GetImages" => await GetImagesAsync(request, requestId, ct),
|
||||
"CreateDraft" => await CreateDraftAsync(request, requestId, ct),
|
||||
_ => CreativeResponse.Fail(requestId, "UNKNOWN_OPERATION",
|
||||
$"Unknown operation: {operation}")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[Creative] {Operation} failed | RequestId={RequestId}",
|
||||
operation, requestId);
|
||||
return CreativeResponse.Fail(requestId, "INTERNAL_ERROR", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Operations
|
||||
// ============================================================
|
||||
|
||||
/// <summary>
|
||||
/// Health check.
|
||||
/// </summary>
|
||||
private static CreativeResponse Ping(string requestId)
|
||||
{
|
||||
return CreativeResponse.Success(requestId, new
|
||||
{
|
||||
pong = true,
|
||||
timestamp = DateTimeOffset.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scrape and analyze a URL. Returns structured content.
|
||||
/// Payload: { "url": "https://..." }
|
||||
/// </summary>
|
||||
private async Task<CreativeResponse> AnalyzeUrlAsync(
|
||||
CreativeRequest request, string requestId, CancellationToken ct)
|
||||
{
|
||||
var url = GetPayloadString(request, "url");
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return CreativeResponse.Fail(requestId, "MISSING_URL", "payload.url is required");
|
||||
|
||||
url = NormalizeUrl(url);
|
||||
var analysis = await _scraper.AnalyzeUrlAsync(url, ct);
|
||||
|
||||
return CreativeResponse.Success(requestId, analysis);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate text assets from previously analyzed content.
|
||||
/// Payload: { "analysis": { ... } } (UrlAnalysis object)
|
||||
/// </summary>
|
||||
private async Task<CreativeResponse> GenerateAssetsAsync(
|
||||
CreativeRequest request, string requestId, CancellationToken ct)
|
||||
{
|
||||
var analysisJson = GetPayloadObject(request, "analysis");
|
||||
if (analysisJson == null)
|
||||
return CreativeResponse.Fail(requestId, "MISSING_ANALYSIS",
|
||||
"payload.analysis is required (pass result from AnalyzeUrl)");
|
||||
|
||||
var analysis = JsonSerializer.Deserialize<UrlAnalysis>(analysisJson.Value.GetRawText());
|
||||
if (analysis == null)
|
||||
return CreativeResponse.Fail(requestId, "INVALID_ANALYSIS",
|
||||
"Could not deserialize analysis object");
|
||||
|
||||
var (headlines, descriptions, source) = await _copyGen.GenerateAsync(analysis, ct);
|
||||
|
||||
return CreativeResponse.Success(requestId, new
|
||||
{
|
||||
headlines,
|
||||
descriptions,
|
||||
source
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get images matching previously analyzed content.
|
||||
/// Payload: { "analysis": { ... } } (UrlAnalysis object)
|
||||
/// </summary>
|
||||
private async Task<CreativeResponse> GetImagesAsync(
|
||||
CreativeRequest request, string requestId, CancellationToken ct)
|
||||
{
|
||||
var analysisJson = GetPayloadObject(request, "analysis");
|
||||
if (analysisJson == null)
|
||||
return CreativeResponse.Fail(requestId, "MISSING_ANALYSIS",
|
||||
"payload.analysis is required (pass result from AnalyzeUrl)");
|
||||
|
||||
var analysis = JsonSerializer.Deserialize<UrlAnalysis>(analysisJson.Value.GetRawText());
|
||||
if (analysis == null)
|
||||
return CreativeResponse.Fail(requestId, "INVALID_ANALYSIS",
|
||||
"Could not deserialize analysis object");
|
||||
|
||||
var (images, source) = await _imageGen.GenerateAsync(analysis, ct);
|
||||
|
||||
return CreativeResponse.Success(requestId, new
|
||||
{
|
||||
images,
|
||||
source
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full pipeline: scrape URL → generate copy → source images → return campaign draft.
|
||||
/// Payload: { "url": "https://..." }
|
||||
/// Gateway persists the returned draft to tbCreativeDraft.
|
||||
/// </summary>
|
||||
private async Task<CreativeResponse> CreateDraftAsync(
|
||||
CreativeRequest request, string requestId, CancellationToken ct)
|
||||
{
|
||||
var url = GetPayloadString(request, "url");
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return CreativeResponse.Fail(requestId, "MISSING_URL", "payload.url is required");
|
||||
|
||||
url = NormalizeUrl(url);
|
||||
|
||||
// Step 1: Scrape
|
||||
_logger.LogInformation("[Creative] CreateDraft step 1/3: scraping {Url}", url);
|
||||
var analysis = await _scraper.AnalyzeUrlAsync(url, ct);
|
||||
|
||||
// Step 2: Generate copy
|
||||
_logger.LogInformation("[Creative] CreateDraft step 2/3: generating copy");
|
||||
var (headlines, descriptions, copySource) = await _copyGen.GenerateAsync(analysis, ct);
|
||||
|
||||
// Step 3: Source images
|
||||
_logger.LogInformation("[Creative] CreateDraft step 3/3: sourcing images");
|
||||
var (images, imageSource) = await _imageGen.GenerateAsync(analysis, ct);
|
||||
|
||||
// Assemble draft - Gateway will persist this
|
||||
var draftId = Guid.NewGuid().ToString("N")[..12];
|
||||
var draft = new CampaignDraft
|
||||
{
|
||||
DraftId = draftId,
|
||||
Url = url,
|
||||
Analysis = analysis,
|
||||
Headlines = headlines,
|
||||
Descriptions = descriptions,
|
||||
Images = images,
|
||||
Source = copySource,
|
||||
ImageSource = imageSource,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"[Creative] Draft assembled | DraftId={DraftId} Headlines={H} Descriptions={D} Images={I} CopySource={CS} ImageSource={IS}",
|
||||
draftId, headlines.Count, descriptions.Count, images.Count, copySource, imageSource);
|
||||
|
||||
return CreativeResponse.Success(requestId, draft);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helpers
|
||||
// ============================================================
|
||||
|
||||
private static string? GetPayloadString(CreativeRequest request, string key)
|
||||
{
|
||||
if (request.Payload == null) return null;
|
||||
if (!request.Payload.TryGetValue(key, out var value)) return null;
|
||||
|
||||
return value switch
|
||||
{
|
||||
string s => s,
|
||||
JsonElement je when je.ValueKind == JsonValueKind.String => je.GetString(),
|
||||
_ => value?.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonElement? GetPayloadObject(CreativeRequest request, string key)
|
||||
{
|
||||
if (request.Payload == null) return null;
|
||||
if (!request.Payload.TryGetValue(key, out var value)) return null;
|
||||
|
||||
if (value is JsonElement je && je.ValueKind == JsonValueKind.Object)
|
||||
return je;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeUrl(string url)
|
||||
{
|
||||
url = url.Trim();
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
url = "https://" + url;
|
||||
return url;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user