LLM reel caption and video description + Refactor in services

This commit is contained in:
2025-06-30 16:14:29 +02:00
parent 21abbcdff5
commit 228d67a48d
20 changed files with 575 additions and 151 deletions

View File

@ -33,7 +33,7 @@ abstract class BrowserJob implements ShouldQueue
public int $jobId;
public $timeout = 500;
public $timeout = 300; // 5 minutes
public function __construct(int $jobId)
{
@ -53,6 +53,7 @@ abstract class BrowserJob implements ShouldQueue
$this->browse(function (Browser $browser) use ($callback, &$log) {
try {
$browser->driver->manage()->timeouts()->implicitlyWait(20);
$log = $callback($browser);
// } catch (Exception $e) {
// $browser->screenshot("failure-{$this->jobId}");
@ -160,7 +161,7 @@ abstract class BrowserJob implements ShouldQueue
'--disable-setuid-sandbox',
'--whitelisted-ips=""',
'--disable-dev-shm-usage',
'--user-data-dir=/home/seluser/profile/',
'--user-data-dir=/home/seluser/profile/nigga/', // seems that selenium doesn't like docker having a volume on the exact same folder ("session not created: probably user data directory is already in use")
])->all());
return RemoteWebDriver::create(
@ -168,7 +169,12 @@ abstract class BrowserJob implements ShouldQueue
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY,
$options
),
)
->setCapability('timeouts', [
'implicit' => 20000, // 20 seconds
'pageLoad' => 300000, // 5 minutes
'script' => 30000, // 30 seconds
]),
4000,
$this->timeout * 1000
);

View File

@ -17,11 +17,14 @@ use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Laravel\Dusk\Browser;
use App\Services\AIPrompt\OpenAPIPrompt;
class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
{
// === CONFIGURATION ===
public $timeout = 1800; // 30 minutes
private const APPROXIMATIVE_RUNNING_MINUTES = 2;
private Collection $jobInfos;
@ -29,6 +32,10 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
protected IInstagramVideoDownloader $videoDownloader;
protected ReelDescriptor $ReelDescriptor;
protected OpenAPIPrompt $openAPIPrompt;
protected string $downloadFolder = "app/Browser/downloads/InstagramRepost/";
/**
@ -40,12 +47,14 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
*/
protected InstagramDescriptionPipeline $descriptionPipeline;
public function __construct($jobId = 4)
public function __construct($jobId = 4, ReelDescriptor $ReelDescriptor = null, OpenAPIPrompt $openAPIPrompt = null)
{
parent::__construct($jobId);
$this->downloadFolder = base_path($this->downloadFolder);
$this->videoDownloader = new YTDLPDownloader();
$this->ReelDescriptor = $ReelDescriptor ?? app(ReelDescriptor::class);
$this->openAPIPrompt = $openAPIPrompt ?? app(OpenAPIPrompt::class);
$this->descriptionPipeline = new InstagramDescriptionPipeline([
// Add steps to the pipeline here
new DescriptionPipeline\RemoveAccountsReferenceStep(),
@ -152,13 +161,17 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
*/
$downloadedReels = [];
foreach ($toDownloadReels as $repost) {
$downloadInfos = $this->downloadReel(
$browser,
$repost
);
$downloadedReels[] = [
$repost,
$this->downloadReel(
$browser,
$repost
)
$downloadInfos
];
$this->describeReel($repost, $downloadInfos);
}
$this->jobRun->addArtifact(new JobArtifact([
@ -278,6 +291,15 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
return $videoInfo;
}
protected function describeReel(InstagramRepost $reel, IInstagramVideo $videoInfo): void
{
// Set the video description to the reel description
$reel->video_description = $this->ReelDescriptor->getDescription($videoInfo->getFilename());
$reel->save();
Log::info("Reel description set: {$reel->reel_id} - {$reel->video_description}");
}
protected function repostReel(Browser $browser, InstagramRepost $reel, IInstagramVideo $videoInfo): bool
{
try {
@ -317,16 +339,17 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
$this->clickNext($browser); // Skip cover photo and trim
// Add a caption
$captionText = $this->descriptionPipeline->process($videoInfo->getDescription());
$captionText = $this->descriptionPipeline->process($this->getReelCaption($reel, $videoInfo));
$this->pasteText($browser, $captionText, 'div[contenteditable]');
sleep(2); // Wait for the caption to be added
if (config("app.environment") !== "local") { // Don't share the post in local environment
if (config("app.env") !== "local") { // Don't share the post in local environment
$this->clickNext($browser); // Share the post
}
sleep(5); // Wait for the post to be completed
sleep(7); // Wait for the post to be completed
$this->removePopups($browser);
// Check if the post was successful
try {
@ -360,6 +383,56 @@ class InstagramRepostJob extends BrowserJob implements ShouldBeUniqueUntilProces
}
}
private function getReelCaption(InstagramRepost $reel, IInstagramVideo $videoInfo): string
{
if (isset($reel->instagram_caption)) {
return $reel->instagram_caption;
}
// Get the reel description from the database or the video info
$reelDescription = $reel->video_description;
$originalDescription = $videoInfo->getDescription();
$llmAnswer = $this->openAPIPrompt->generate(
config('llm.models.chat.name'),
"Original Caption: {$originalDescription}
Video Description/Directive: {$reelDescription}",
[],
outputFormat: '{"type": "object", "properties": {"answer": {"type": "string"}}, "required": ["answer"]}',
systemMessage: "You are an AI assistant specialized in creating engaging and concise Instagram Reel captions. Your primary task is to transform the provided original caption (often from Twitter) and description/directions into a fresh, unique, but still relevant caption for Instagram Reels format.
Key instructions:
1. **Analyze Input:** You will receive two things: an *original reel caption* (usually starting with \"credit:\" or mentioning a Twitter handle like `t/TwitterUser`), and either a *video description* or explicit directions about the joke/idea behind the video.
2. **Transform, Don't Reproduce:** Your output must be significantly different from the original provided caption. It should capture the essence of the content described but phrase it anew often with humor if appropriate.
3. **Keep it Short & Punchy:** Instagram Reels thrive on quick engagement. Prioritize brevity (ideally under two lines, or three lines max) and impact. Make sure your caption is concise enough for fast-scroll viewing.
4. **Maintain the Core Idea:** The new caption must directly relate to the video's content/direction/joke without simply restating it like a description would. Focus on what makes the reel *interesting* or *funny* in its own right.
5. **Preserve Original Credit (Optional):** If an explicit \"credit\" line is provided, you may incorporate this into your new caption naturally, perhaps using `(via...)` or similar phrasing if it fits well and doesn't sound awkward. **Do not** include any original Instagram account mentions (@handles). They are often intended for promotion which isn't our goal.
6. **Use Emoji Judiciously:** Incorporate relevant emojis to enhance the tone (funny, relatable, etc.) or add visual interest. Use them purposefully and in moderation they should complement the caption, not overwhelm it.
7. **Add Hashtags (Optional but Recommended):** Generate a few relevant Instagram hashtags automatically at the end of your output to increase visibility. Keep these organic to the content and avoid forcing irrelevant tags.
Your response structure is as follows:
- The generated caption (your core answer).
- Then, if you generate any hashtags, list them on the next line(s) prefixed with `#`.
Example Input Structure:
Original Caption: credit: t/otherhandle This banana is looking fly today!
Video Description/Directive: A man walks into a store holding a banana and wearing sunglasses. He looks around confidently before leaving.
Your answer should only contain the generated caption, and optionally hashtags if relevant.
Remember to be creative and ensure the generated caption feels like something you would see naturally on an Instagram Reel. Aim for personality and relevance.
",
keepAlive: true,
shouldThink: config('llm.models.chat.shouldThink')
);
$llmAnswer = json_decode($llmAnswer, true)['answer'] ?? null;
if ($llmAnswer !== null) {
$reel->instagram_caption = $llmAnswer;
$reel->save();
Log::info("Reel caption generated: {$reel->reel_id} - {$llmAnswer}");
}
return $llmAnswer;
}
private function clickNext(Browser $browser) {
$nextButton = $browser->driver->findElement(WebDriverBy::xpath('//div[contains(text(), "Next") or contains(text(), "Share")]'));
$nextButton->click();

View File

@ -1,12 +0,0 @@
<?php
namespace App\Browser\Jobs\InstagramRepost;
class OCRLLMReelDescriptor extends \App\FileTools\VideoDescriptor\OCRLLMVideoDescriptor
{
public const DESCRIPTION_PROMPT = "Describe the Instagram reel based on the screenshots. Each screenshot has a timestamp of when in the video the screenshot was taken, an OCR result and a description of the screenshot by an LLM. Do not specify that it is a reel, just try to describe the video and most importantly the joke behind it if there is one. The description must have a maximum of 500 words.\n";
public function __construct() {
parent::__construct();
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Browser\Jobs\InstagramRepost;
use App\Services\AIPrompt\OpenAPIPrompt;
use App\Services\FileTools\OCR\IImageOCR;
class ReelDescriptor extends \App\Services\FileTools\VideoDescriptor\OCRLLMVideoDescriptor
{
public const DESCRIPTION_PROMPT = "Analyze this Instagram Reel sequence. You are given information for each individual screenshot/analysis from the video:";
}