Skip to content

Commit ea4bc87

Browse files
xjanovaclaude
andauthored
feat: Make video creation pipeline production-ready (#175)
Browser Persistent Auth: - BrowserController: Add LaunchPersistentContextAsync support for cookie/session persistence across browser restarts - Add SaveStorageStateAsync/LoadStorageStateAsync for session state - FreepikAutomationService: Replace broken learned-workflow login with persistent browser context + manual login flow - SunoAutomationService: Same cookie persistence approach, integrated with SunoSessionManager for token extraction Video Publishing (Real Implementations): - YouTube: Implement resumable upload protocol (Data API v3) with chunked 5MB uploads, progress tracking, and real video IDs - Facebook: Implement Graph Video API upload with MultipartFormData - TikTok: Implement FILE_UPLOAD source type for local file uploads with chunked upload and Content-Range headers - Instagram: Implement Reels Container API with status polling Infrastructure: - Create LocalFileServerService: lightweight HTTP server on port 5010 that serves local files as temporary URLs for platforms requiring public URLs (Instagram, TikTok PULL_FROM_URL) - Support HTTP Range requests for video streaming - Auto-cleanup expired file mappings API & Integration: - Add POST /api/Pipeline/publish-video endpoint for Laravel integration - Add Credentials, ThumbnailPath, AccountId to VideoPostRequest model - Wire PostPublisherService to extract credentials per platform - Fix Laravel endpoint URL: /api/Tasks/publish-video -> /api/Pipeline/publish-video - Register LocalFileServerService in DI + auto-start on app launch Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 009d6ca commit ea4bc87

10 files changed

Lines changed: 2027 additions & 715 deletions

File tree

AIManagerCore/src/AIManager.API/Controllers/PipelineController.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Mvc;
22
using AIManager.Core.Services;
3+
using AIManager.Core.Models;
34
using System.Diagnostics;
45

56
namespace AIManager.API.Controllers;
@@ -11,17 +12,20 @@ public class PipelineController : ControllerBase
1112
private readonly ThaiTtsService _ttsService;
1213
private readonly LipSyncService _lipSyncService;
1314
private readonly FFmpegService _ffmpegService;
15+
private readonly PostPublisherService _postPublisherService;
1416
private readonly ILogger<PipelineController> _logger;
1517

1618
public PipelineController(
1719
ThaiTtsService ttsService,
1820
LipSyncService lipSyncService,
1921
FFmpegService ffmpegService,
22+
PostPublisherService postPublisherService,
2023
ILogger<PipelineController> logger)
2124
{
2225
_ttsService = ttsService;
2326
_lipSyncService = lipSyncService;
2427
_ffmpegService = ffmpegService;
28+
_postPublisherService = postPublisherService;
2529
_logger = logger;
2630
}
2731

@@ -267,6 +271,61 @@ public async Task<ActionResult> CheckServices()
267271
});
268272
}
269273

274+
/// <summary>
275+
/// Publish a video to a social media platform
276+
/// เผยแพร่วิดีโอไปยัง social media platform
277+
/// </summary>
278+
[HttpPost("publish-video")]
279+
public async Task<ActionResult> PublishVideo([FromBody] PublishVideoRequest request, CancellationToken ct)
280+
{
281+
if (string.IsNullOrEmpty(request.VideoPath))
282+
{
283+
return BadRequest(new { success = false, error = "video_path is required" });
284+
}
285+
286+
if (!System.IO.File.Exists(request.VideoPath))
287+
{
288+
return BadRequest(new { success = false, error = $"Video file not found: {request.VideoPath}" });
289+
}
290+
291+
if (string.IsNullOrEmpty(request.Platform))
292+
{
293+
return BadRequest(new { success = false, error = "platform is required" });
294+
}
295+
296+
// Parse platform string to SocialPlatform enum
297+
if (!Enum.TryParse<SocialPlatform>(request.Platform, ignoreCase: true, out var platform))
298+
{
299+
return BadRequest(new { success = false, error = $"Unknown platform: {request.Platform}" });
300+
}
301+
302+
var videoRequest = new VideoPostRequest
303+
{
304+
Platform = platform,
305+
VideoPath = request.VideoPath,
306+
ThumbnailPath = request.ThumbnailPath,
307+
Title = request.Title,
308+
Description = request.Description,
309+
Hashtags = request.Hashtags,
310+
AccountId = request.AccountId,
311+
Credentials = request.Credentials,
312+
};
313+
314+
var result = await _postPublisherService.PublishVideoAsync(videoRequest, ct);
315+
316+
if (!result.Success)
317+
{
318+
return StatusCode(500, new { success = false, error = result.Error });
319+
}
320+
321+
return Ok(new
322+
{
323+
success = true,
324+
post_id = result.PostId,
325+
post_url = result.PostUrl,
326+
});
327+
}
328+
270329
// ====================================================================
271330
// Private helpers
272331
// ====================================================================
@@ -515,3 +574,15 @@ public class SlideshowRequest
515574
public string? Effect { get; set; }
516575
public string? OutputDir { get; set; }
517576
}
577+
578+
public class PublishVideoRequest
579+
{
580+
public string Platform { get; set; } = "";
581+
public string VideoPath { get; set; } = "";
582+
public string? ThumbnailPath { get; set; }
583+
public string? Title { get; set; }
584+
public string? Description { get; set; }
585+
public List<string>? Hashtags { get; set; }
586+
public string? AccountId { get; set; }
587+
public Dictionary<string, string>? Credentials { get; set; }
588+
}

AIManagerCore/src/AIManager.API/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@
258258
// Self-Healing Worker Factory
259259
builder.Services.AddSingleton<SelfHealingWorker>();
260260

261+
// Local File Server (serves local media files as temporary URLs for platforms)
262+
builder.Services.AddSingleton<LocalFileServerService>();
263+
261264
// GPU Pool Service (Distributed GPU Worker Management)
262265
builder.Services.AddSingleton<GpuPoolService>();
263266

@@ -342,6 +345,10 @@
342345
app.MapHub<AIManagerHub>("/hub/aimanager");
343346
app.MapHub<GpuPoolHub>("/hub/gpupool");
344347

348+
// Start Local File Server (serves local media as temporary URLs)
349+
var fileServer = app.Services.GetRequiredService<LocalFileServerService>();
350+
_ = fileServer.StartAsync();
351+
345352
// Start orchestrator on startup
346353
var orchestrator = app.Services.GetRequiredService<ProcessOrchestrator>();
347354
app.Lifetime.ApplicationStarted.Register(async () =>

AIManagerCore/src/AIManager.Core/Services/FreepikAutomationService.cs

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ public class FreepikAutomationService
1616
private readonly WorkflowExecutor _workflowExecutor;
1717
private readonly WorkflowStorage _workflowStorage;
1818
private readonly string _downloadPath;
19+
private readonly string _userDataDir;
1920

2021
// Freepik URLs
2122
private const string FREEPIK_HOME = "https://www.freepik.com";
2223
private const string FREEPIK_LOGIN = "https://www.freepik.com/log-in";
2324
private const string FREEPIK_IMAGE_GENERATOR = "https://www.freepik.com/pikaso/ai-image-generator";
2425
private const string FREEPIK_VIDEO_GENERATOR = "https://www.freepik.com/pikaso/ai-video";
2526

27+
// Manual login timeout
28+
private static readonly TimeSpan ManualLoginTimeout = TimeSpan.FromMinutes(5);
29+
2630
// รายชื่อโมเดลที่ Unlimited (ฟรี ไม่เสีย credits)
2731
private static readonly HashSet<string> UnlimitedModels = new(StringComparer.OrdinalIgnoreCase)
2832
{
@@ -57,6 +61,9 @@ public FreepikAutomationService(
5761
_downloadPath = downloadPath ?? Path.Combine(
5862
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
5963
"PostXAgent", "Downloads", "Freepik");
64+
_userDataDir = Path.Combine(
65+
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
66+
"PostXAgent", "browser_data", "freepik");
6067

6168
Directory.CreateDirectory(_downloadPath);
6269
}
@@ -292,6 +299,52 @@ public bool IsUnlimitedModel(string modelName)
292299
/// </summary>
293300
public IReadOnlyCollection<string> GetUnlimitedModels() => UnlimitedModels;
294301

302+
/// <summary>
303+
/// เปิด browser ให้ user login ด้วยตัวเอง (headed mode)
304+
/// รอสูงสุด 5 นาทีให้ login เสร็จ
305+
/// </summary>
306+
public async Task<bool> LoginManuallyAsync(CancellationToken ct = default)
307+
{
308+
try
309+
{
310+
_logger.LogInformation("Starting manual login flow for Freepik...");
311+
312+
// Launch browser with persistent context (non-headless for manual login)
313+
if (!_browserController.IsLaunched)
314+
{
315+
await _browserController.InitializeAsync(ct);
316+
await _browserController.LaunchPersistentAsync(_userDataDir, ct);
317+
}
318+
319+
// Navigate to login page
320+
await _browserController.NavigateAsync(FREEPIK_LOGIN, ct);
321+
_logger.LogInformation(
322+
"Please login to Freepik in the browser window. Waiting up to {Minutes} minutes...",
323+
ManualLoginTimeout.TotalMinutes);
324+
325+
// Poll for login success
326+
var endTime = DateTime.UtcNow.Add(ManualLoginTimeout);
327+
while (DateTime.UtcNow < endTime && !ct.IsCancellationRequested)
328+
{
329+
await Task.Delay(3000, ct);
330+
331+
if (await CheckIfLoggedInAsync(ct))
332+
{
333+
_logger.LogInformation("Freepik login detected successfully!");
334+
return true;
335+
}
336+
}
337+
338+
_logger.LogWarning("Manual login timed out after {Minutes} minutes", ManualLoginTimeout.TotalMinutes);
339+
return false;
340+
}
341+
catch (Exception ex)
342+
{
343+
_logger.LogError(ex, "Error during manual login flow");
344+
return false;
345+
}
346+
}
347+
295348
#endregion
296349

297350
#region Private Methods - Browser Actions
@@ -300,12 +353,13 @@ private async Task<bool> EnsureBrowserReadyAsync(CancellationToken ct)
300353
{
301354
try
302355
{
303-
if (_browserController.CurrentUrl == null)
356+
if (_browserController.IsLaunched)
304357
{
305-
await _browserController.InitializeAsync(ct);
306-
await _browserController.LaunchAsync(ct);
358+
return true;
307359
}
308-
return true;
360+
361+
await _browserController.InitializeAsync(ct);
362+
return await _browserController.LaunchPersistentAsync(_userDataDir, ct);
309363
}
310364
catch (Exception ex)
311365
{
@@ -318,39 +372,37 @@ private async Task<bool> EnsureLoggedInAsync(CancellationToken ct)
318372
{
319373
try
320374
{
321-
// ตรวจสอบว่า login แล้วหรือยังโดยดู URL หรือ element
322-
var currentUrl = _browserController.CurrentUrl ?? "";
375+
// Navigate to home to check login status via persistent cookies
376+
await _browserController.NavigateAsync(FREEPIK_HOME, ct);
377+
await Task.Delay(2000, ct);
323378

324-
// ถ้าอยู่หน้า login ให้ทำการ login
325-
if (currentUrl.Contains("log-in") || currentUrl.Contains("login"))
379+
// Check if already logged in (persistent context preserves cookies)
380+
if (await CheckIfLoggedInAsync(ct))
326381
{
327-
// TODO: ใช้ learned workflow สำหรับ login
328-
_logger.LogWarning("Not logged in. Please login manually or use learned workflow.");
329-
return false;
382+
_logger.LogDebug("Already logged in to Freepik (persistent session)");
383+
return true;
330384
}
331385

332-
// ไปที่หน้า home เพื่อตรวจสอบ
333-
await _browserController.NavigateAsync(FREEPIK_HOME, ct);
334-
await Task.Delay(2000, ct);
386+
// Not logged in - navigate to login page and wait for manual login
387+
_logger.LogInformation(
388+
"Not logged in to Freepik. Navigating to login page. Please login in the browser window...");
335389

336-
// ตรวจสอบว่ามี user avatar หรือ profile icon
337-
var isLoggedIn = await CheckIfLoggedInAsync(ct);
390+
await _browserController.NavigateAsync(FREEPIK_LOGIN, ct);
338391

339-
if (!isLoggedIn)
392+
var endTime = DateTime.UtcNow.Add(ManualLoginTimeout);
393+
while (DateTime.UtcNow < endTime && !ct.IsCancellationRequested)
340394
{
341-
_logger.LogInformation("Not logged in, attempting login workflow...");
395+
await Task.Delay(3000, ct);
342396

343-
// ลองใช้ learned workflow
344-
var loginWorkflow = await _workflowStorage.LoadWorkflowAsync("freepik_login", ct);
345-
if (loginWorkflow != null)
397+
if (await CheckIfLoggedInAsync(ct))
346398
{
347-
var executeResult = await _workflowExecutor.ExecuteAsync(
348-
loginWorkflow, new WebPostContent(), null, ct);
349-
return executeResult.Success;
399+
_logger.LogInformation("Freepik login detected successfully!");
400+
return true;
350401
}
351402
}
352403

353-
return isLoggedIn;
404+
_logger.LogWarning("Login timed out. Please use LoginManuallyAsync() to login.");
405+
return false;
354406
}
355407
catch (Exception ex)
356408
{
@@ -363,11 +415,10 @@ private async Task<bool> CheckIfLoggedInAsync(CancellationToken ct)
363415
{
364416
try
365417
{
366-
// ตรวจสอบ element ที่บ่งบอกว่า login แล้ว
418+
// Check for avatar/profile elements that indicate logged-in state
367419
var script = @"
368420
(function() {
369-
// ลองหา avatar, profile icon, หรือ logout button
370-
var avatar = document.querySelector('[data-cy=""user-avatar""], .user-avatar, .profile-icon');
421+
var avatar = document.querySelector('[data-cy=""user-avatar""], .user-avatar, .profile-icon, img[alt*=""avatar""], img[alt*=""profile""]');
371422
var logoutBtn = document.querySelector('[data-cy=""logout""], a[href*=""logout""]');
372423
var loginBtn = document.querySelector('[data-cy=""login""], a[href*=""log-in""]');
373424

0 commit comments

Comments
 (0)